feat: expand ISAC content modeling
This commit is contained in:
@@ -35,27 +35,15 @@ import {
|
||||
saveConditionalRuleInTransaction,
|
||||
} from '$lib/server/conditional-effects';
|
||||
import { MECHANIC_RECIPE_REGISTRY } from '$lib/calculations/mechanic-recipes';
|
||||
import {
|
||||
getOsProtocolProfileAttributeIds,
|
||||
type OsProtocolAttributeType,
|
||||
} from '$lib/server/os-protocol-profiles';
|
||||
import type { Database } from '@tursodatabase/database';
|
||||
|
||||
const qualities = ['Standard', 'Specialized', 'Superior', 'High-end'];
|
||||
const attributeTypes = ['Firepower', 'Toughness', 'Engineering'];
|
||||
const secondaryRoles = ['Core', 'Damage', 'Health'];
|
||||
const expectedSecondaryAttributes: Record<string, Record<string, string>> = {
|
||||
Firepower: {
|
||||
Core: 'Firepower',
|
||||
Damage: 'All Weapon Damage',
|
||||
Health: 'Health',
|
||||
},
|
||||
Toughness: {
|
||||
Core: 'Toughness',
|
||||
Damage: 'All Weapon Damage',
|
||||
Health: 'Health',
|
||||
},
|
||||
Engineering: {
|
||||
Core: 'Engineering',
|
||||
Damage: 'Skill Damage',
|
||||
Health: 'Health',
|
||||
},
|
||||
};
|
||||
|
||||
const renderAuditMetadata = (render: Record<string, any> | null | undefined) =>
|
||||
render
|
||||
@@ -88,15 +76,15 @@ const descriptionOutcomes = (
|
||||
Number(left.descriptionSlot) - Number(right.descriptionSlot),
|
||||
);
|
||||
|
||||
async function protocolSnapshot(id: string) {
|
||||
const database = await getDatabase();
|
||||
async function protocolSnapshot(id: string, suppliedDatabase?: Database) {
|
||||
const database = suppliedDatabase ?? (await getDatabase());
|
||||
const talent = await (
|
||||
await database.prepare(
|
||||
'SELECT * FROM os_protocol_talents WHERE protocol_id=?',
|
||||
)
|
||||
).get(id);
|
||||
return {
|
||||
record: await auditRecord('os_protocols', id),
|
||||
record: await auditRecord('os_protocols', id, database),
|
||||
icon: await gameDataIconMetadata(database, 'os_protocol', id),
|
||||
render: renderAuditMetadata(
|
||||
await (
|
||||
@@ -129,8 +117,8 @@ async function protocolSnapshot(id: string) {
|
||||
};
|
||||
}
|
||||
|
||||
async function profileSnapshot(id: string) {
|
||||
const database = await getDatabase();
|
||||
async function profileSnapshot(id: string, suppliedDatabase?: Database) {
|
||||
const database = suppliedDatabase ?? (await getDatabase());
|
||||
return {
|
||||
record:
|
||||
(await (
|
||||
@@ -292,6 +280,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
'attributeType',
|
||||
'primaryStatId',
|
||||
'primaryBaseValue',
|
||||
'hasTalent',
|
||||
'talentName',
|
||||
'descriptionTemplate',
|
||||
'conditionalRule',
|
||||
@@ -341,17 +330,20 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
profile.quality === protocol.quality &&
|
||||
profile.primary_attribute_type === protocol.attributeType,
|
||||
) ?? null,
|
||||
talent: {
|
||||
name: protocol.talentName,
|
||||
description_template: protocol.descriptionTemplate,
|
||||
conditionalRule: (() => {
|
||||
try {
|
||||
return JSON.parse(String(protocol.conditionalRule ?? 'null'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})(),
|
||||
},
|
||||
talent:
|
||||
protocol.hasTalent === '1'
|
||||
? {
|
||||
name: protocol.talentName,
|
||||
description_template: protocol.descriptionTemplate,
|
||||
conditionalRule: (() => {
|
||||
try {
|
||||
return JSON.parse(String(protocol.conditionalRule ?? 'null'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})(),
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
const configuredProtocols = await Promise.all(
|
||||
protocols.map(async (protocol) => {
|
||||
@@ -450,27 +442,26 @@ export const actions: Actions = {
|
||||
profileError: 'Choose a valid quality and attribute type.',
|
||||
});
|
||||
const database = await getDatabase();
|
||||
const expected = expectedSecondaryAttributes[attributeType];
|
||||
const statRows = await (
|
||||
await database.prepare(
|
||||
`SELECT id,name FROM stat_attributes WHERE active=1 AND name IN (?,?,?) COLLATE NOCASE`,
|
||||
)
|
||||
).all(expected.Core, expected.Damage, expected.Health);
|
||||
const statByName = new Map(
|
||||
statRows.map((stat) => [
|
||||
String(stat.name).toLowerCase(),
|
||||
String(stat.id),
|
||||
]),
|
||||
const attributeIds = await getOsProtocolProfileAttributeIds(
|
||||
database,
|
||||
attributeType as OsProtocolAttributeType,
|
||||
);
|
||||
if (statRows.length !== 3)
|
||||
if (Object.values(attributeIds).some((id) => !id))
|
||||
return fail(400, {
|
||||
profileError: `Create active stats named ${Object.values(expected).join(', ')} before configuring this profile.`,
|
||||
profileError:
|
||||
'Create the required active core, damage, and Health Attributes before configuring this profile.',
|
||||
});
|
||||
const effects = secondaryRoles.map((role) => ({
|
||||
role,
|
||||
statId: statByName.get(expected[role].toLowerCase()) ?? '',
|
||||
value: Number(form.get(`value${role}`)),
|
||||
}));
|
||||
const effects = secondaryRoles.map((role) => {
|
||||
const rawValue = form.get(`value${role}`);
|
||||
return {
|
||||
role,
|
||||
statId: attributeIds[role as keyof typeof attributeIds] ?? '',
|
||||
value:
|
||||
typeof rawValue === 'string' && rawValue.trim()
|
||||
? Number(rawValue)
|
||||
: Number.NaN,
|
||||
};
|
||||
});
|
||||
if (
|
||||
effects.some(
|
||||
(effect) =>
|
||||
@@ -481,47 +472,58 @@ export const actions: Actions = {
|
||||
profileError:
|
||||
'Every shared secondary attribute requires a non-negative value.',
|
||||
});
|
||||
const existing = await (
|
||||
await database.prepare(
|
||||
'SELECT id FROM os_protocol_secondary_profiles WHERE quality=? AND primary_attribute_type=?',
|
||||
)
|
||||
).get(quality, attributeType);
|
||||
const profileId = String(existing?.id ?? randomUUID()),
|
||||
now = new Date().toISOString();
|
||||
const before = existing ? await profileSnapshot(profileId) : null;
|
||||
if (existing)
|
||||
await (
|
||||
await database.prepare(
|
||||
'UPDATE os_protocol_secondary_profiles SET updated_at=? WHERE id=?',
|
||||
const saveProfile = database.transactionAsync(async (transaction) => {
|
||||
const tx = transaction as unknown as Database;
|
||||
const existing = await (
|
||||
await tx.prepare(
|
||||
'SELECT id FROM os_protocol_secondary_profiles WHERE quality=? AND primary_attribute_type=?',
|
||||
)
|
||||
).run(now, profileId);
|
||||
else
|
||||
).get(quality, attributeType);
|
||||
const profileId = String(existing?.id ?? randomUUID());
|
||||
const now = new Date().toISOString();
|
||||
const before = existing ? await profileSnapshot(profileId, tx) : null;
|
||||
if (existing)
|
||||
await (
|
||||
await tx.prepare(
|
||||
'UPDATE os_protocol_secondary_profiles SET updated_at=? WHERE id=?',
|
||||
)
|
||||
).run(now, profileId);
|
||||
else
|
||||
await (
|
||||
await tx.prepare(
|
||||
'INSERT INTO os_protocol_secondary_profiles(id,quality,primary_attribute_type,created_at,updated_at) VALUES(?,?,?,?,?)',
|
||||
)
|
||||
).run(profileId, quality, attributeType, now, now);
|
||||
await (
|
||||
await database.prepare(
|
||||
'INSERT INTO os_protocol_secondary_profiles(id,quality,primary_attribute_type,created_at,updated_at) VALUES(?,?,?,?,?)',
|
||||
await tx.prepare(
|
||||
'DELETE FROM os_protocol_secondary_effects WHERE profile_id=?',
|
||||
)
|
||||
).run(profileId, quality, attributeType, now, now);
|
||||
await (
|
||||
await database.prepare(
|
||||
'DELETE FROM os_protocol_secondary_effects WHERE profile_id=?',
|
||||
)
|
||||
).run(profileId);
|
||||
for (const effect of effects)
|
||||
await (
|
||||
await database.prepare(
|
||||
'INSERT INTO os_protocol_secondary_effects(id,profile_id,effect_role,stat_attribute_id,value) VALUES(?,?,?,?,?)',
|
||||
)
|
||||
).run(randomUUID(), profileId, effect.role, effect.statId, effect.value);
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'game_data',
|
||||
action: existing
|
||||
? 'os_protocol_profile.updated'
|
||||
: 'os_protocol_profile.created',
|
||||
entityId: profileId,
|
||||
before,
|
||||
after: await profileSnapshot(profileId),
|
||||
).run(profileId);
|
||||
for (const effect of effects)
|
||||
await (
|
||||
await tx.prepare(
|
||||
'INSERT INTO os_protocol_secondary_effects(id,profile_id,effect_role,stat_attribute_id,value) VALUES(?,?,?,?,?)',
|
||||
)
|
||||
).run(
|
||||
randomUUID(),
|
||||
profileId,
|
||||
effect.role,
|
||||
effect.statId,
|
||||
effect.value,
|
||||
);
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'game_data',
|
||||
action: existing
|
||||
? 'os_protocol_profile.updated'
|
||||
: 'os_protocol_profile.created',
|
||||
entityId: profileId,
|
||||
before,
|
||||
after: await profileSnapshot(profileId, tx),
|
||||
database: tx,
|
||||
});
|
||||
});
|
||||
await saveProfile.immediate();
|
||||
return { profileSaved: true, resultToken: randomUUID() };
|
||||
},
|
||||
save: async ({ request, cookies }) => {
|
||||
@@ -559,7 +561,12 @@ export const actions: Actions = {
|
||||
const quality = String(form.get('quality') ?? '');
|
||||
const attributeType = String(form.get('attributeType') ?? '');
|
||||
const primaryStatId = String(form.get('primaryStatId') ?? '');
|
||||
const primaryBaseValue = Number(form.get('primaryBaseValue'));
|
||||
const primaryBaseValueRaw = form.get('primaryBaseValue');
|
||||
const primaryBaseValue =
|
||||
typeof primaryBaseValueRaw === 'string' && primaryBaseValueRaw.trim()
|
||||
? Number(primaryBaseValueRaw)
|
||||
: Number.NaN;
|
||||
const hasTalent = form.get('hasTalent') === '1';
|
||||
const talentName = String(form.get('talentName') ?? '')
|
||||
.normalize('NFKC')
|
||||
.trim();
|
||||
@@ -567,22 +574,27 @@ export const actions: Actions = {
|
||||
.normalize('NFKC')
|
||||
.trim();
|
||||
const database = await getDatabase();
|
||||
let conditionalRule;
|
||||
let conditionalRule: ReturnType<typeof parseConditionalRulePayload> | null =
|
||||
null;
|
||||
try {
|
||||
const conditionalRuleRaw = form.get('conditionalRule');
|
||||
const activeStats = await (
|
||||
await database.prepare('SELECT id FROM stat_attributes WHERE active=1')
|
||||
).all();
|
||||
conditionalRule = parseConditionalRulePayload(
|
||||
conditionalRuleRaw,
|
||||
[{ key: 'base', variantKey: 'base' }],
|
||||
{
|
||||
allowedAttributeIds: new Set(
|
||||
activeStats.map((row) => String(row.id)),
|
||||
),
|
||||
allowedRecipeKeys: new Set(Object.keys(MECHANIC_RECIPE_REGISTRY)),
|
||||
},
|
||||
);
|
||||
if (hasTalent) {
|
||||
const conditionalRuleRaw = form.get('conditionalRule');
|
||||
const activeStats = await (
|
||||
await database.prepare(
|
||||
'SELECT id FROM stat_attributes WHERE active=1',
|
||||
)
|
||||
).all();
|
||||
conditionalRule = parseConditionalRulePayload(
|
||||
conditionalRuleRaw,
|
||||
[{ key: 'base', variantKey: 'base' }],
|
||||
{
|
||||
allowedAttributeIds: new Set(
|
||||
activeStats.map((row) => String(row.id)),
|
||||
),
|
||||
allowedRecipeKeys: new Set(Object.keys(MECHANIC_RECIPE_REGISTRY)),
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return fail(400, {
|
||||
protocolError:
|
||||
@@ -608,29 +620,39 @@ export const actions: Actions = {
|
||||
protocolError:
|
||||
'Choose the protocol’s unique primary stat and a non-negative level-1 base value.',
|
||||
});
|
||||
const outcomes = mechanicOutcomes(conditionalRule);
|
||||
const numberedOutcomes = descriptionOutcomes(conditionalRule);
|
||||
const outcomes = conditionalRule ? mechanicOutcomes(conditionalRule) : [];
|
||||
const numberedOutcomes = conditionalRule
|
||||
? descriptionOutcomes(conditionalRule)
|
||||
: [];
|
||||
const durationSeconds =
|
||||
conditionalRule.pools.find((pool) => pool.expiresAfterSeconds)
|
||||
conditionalRule?.pools.find((pool) => pool.expiresAfterSeconds)
|
||||
?.expiresAfterSeconds ?? null;
|
||||
const maxStacks = conditionalRule.pools.length
|
||||
const maxStacks = conditionalRule?.pools.length
|
||||
? Math.max(...conditionalRule.pools.map((pool) => pool.max ?? 1))
|
||||
: null;
|
||||
const requiresCompleteMechanic =
|
||||
hasTalent && conditionalRule?.simulationReadiness !== 'captured_only';
|
||||
if (
|
||||
talentName.length < 2 ||
|
||||
talentName.length > 80 ||
|
||||
!descriptionTemplate ||
|
||||
!conditionalRule.clauses.length ||
|
||||
!outcomes.length
|
||||
hasTalent &&
|
||||
(talentName.length < 2 || talentName.length > 80 || !descriptionTemplate)
|
||||
)
|
||||
return fail(400, {
|
||||
protocolError:
|
||||
'The unique talent requires a name, description template, mechanic branch, and at least one outcome.',
|
||||
'The assigned Talent requires a name and description template.',
|
||||
});
|
||||
if (
|
||||
requiresCompleteMechanic &&
|
||||
(!conditionalRule?.clauses.length || !outcomes.length)
|
||||
)
|
||||
return fail(400, {
|
||||
protocolError:
|
||||
'Add a mechanic branch and outcome, or mark the Talent CAPTURE ONLY.',
|
||||
});
|
||||
const placeholders = [...descriptionTemplate.matchAll(/\{(\d+)\}/g)].map(
|
||||
(match) => Number(match[1]),
|
||||
);
|
||||
if (
|
||||
requiresCompleteMechanic &&
|
||||
placeholders.some(
|
||||
(index) =>
|
||||
!numberedOutcomes.some(
|
||||
@@ -642,23 +664,49 @@ export const actions: Actions = {
|
||||
protocolError:
|
||||
'Description placeholders must reference configured numbered outcomes.',
|
||||
});
|
||||
if (descriptionTemplate.includes('{duration}') && durationSeconds === null)
|
||||
if (
|
||||
requiresCompleteMechanic &&
|
||||
descriptionTemplate.includes('{duration}') &&
|
||||
durationSeconds === null
|
||||
)
|
||||
return fail(400, {
|
||||
protocolError:
|
||||
'The description uses {duration}, but no expiring state pool is defined.',
|
||||
});
|
||||
if (descriptionTemplate.includes('{max_stacks}') && !maxStacks)
|
||||
if (
|
||||
requiresCompleteMechanic &&
|
||||
descriptionTemplate.includes('{max_stacks}') &&
|
||||
!maxStacks
|
||||
)
|
||||
return fail(400, {
|
||||
protocolError:
|
||||
'The description uses {max_stacks}, but no state pool maximum is defined.',
|
||||
});
|
||||
if (
|
||||
!(await (
|
||||
const profileAttributeIds = await getOsProtocolProfileAttributeIds(
|
||||
database,
|
||||
attributeType as OsProtocolAttributeType,
|
||||
);
|
||||
const completeProfile =
|
||||
Object.values(profileAttributeIds).every(Boolean) &&
|
||||
(await (
|
||||
await database.prepare(
|
||||
'SELECT id FROM os_protocol_secondary_profiles WHERE quality=? AND primary_attribute_type=?',
|
||||
`SELECT profile.id FROM os_protocol_secondary_profiles profile
|
||||
JOIN os_protocol_secondary_effects effect ON effect.profile_id=profile.id
|
||||
WHERE profile.quality=? AND profile.primary_attribute_type=?
|
||||
GROUP BY profile.id
|
||||
HAVING COUNT(effect.id)=3
|
||||
AND SUM(CASE WHEN effect.effect_role='Core' AND effect.stat_attribute_id=? THEN 1 ELSE 0 END)=1
|
||||
AND SUM(CASE WHEN effect.effect_role='Damage' AND effect.stat_attribute_id=? THEN 1 ELSE 0 END)=1
|
||||
AND SUM(CASE WHEN effect.effect_role='Health' AND effect.stat_attribute_id=? THEN 1 ELSE 0 END)=1`,
|
||||
)
|
||||
).get(quality, attributeType))
|
||||
)
|
||||
).get(
|
||||
quality,
|
||||
attributeType,
|
||||
profileAttributeIds.Core,
|
||||
profileAttributeIds.Damage,
|
||||
profileAttributeIds.Health,
|
||||
));
|
||||
if (!completeProfile)
|
||||
return fail(400, {
|
||||
protocolError:
|
||||
'Configure the shared secondary profile for this quality and attribute type first.',
|
||||
@@ -679,11 +727,21 @@ export const actions: Actions = {
|
||||
).get(id)
|
||||
: null;
|
||||
if (
|
||||
await (
|
||||
existingTalent &&
|
||||
!hasTalent &&
|
||||
form.get('confirmTalentRemoval') !== 'REMOVE'
|
||||
)
|
||||
return fail(400, {
|
||||
protocolError:
|
||||
'Confirm removal of the existing Talent and mechanic program.',
|
||||
});
|
||||
if (
|
||||
hasTalent &&
|
||||
(await (
|
||||
await database.prepare(
|
||||
'SELECT id FROM os_protocol_talents WHERE name=? COLLATE NOCASE AND id<>?',
|
||||
)
|
||||
).get(talentName, existingTalent?.id ?? 'new')
|
||||
).get(talentName, existingTalent?.id ?? 'new'))
|
||||
)
|
||||
return fail(409, {
|
||||
protocolError: 'That protocol talent name is already in use.',
|
||||
@@ -701,71 +759,83 @@ export const actions: Actions = {
|
||||
return fail(400, {
|
||||
protocolError: 'One or more selected attributes are unavailable.',
|
||||
});
|
||||
const protocolId = id || randomUUID(),
|
||||
const protocolId =
|
||||
id || String(form.get('_proposedEntityId') ?? '') || randomUUID(),
|
||||
talentId = String(existingTalent?.id ?? randomUUID()),
|
||||
now = new Date().toISOString();
|
||||
const before = id ? await protocolSnapshot(id) : null;
|
||||
if (id && !before?.record)
|
||||
return fail(404, { protocolError: 'OS Protocol not found.' });
|
||||
await database.exec('BEGIN');
|
||||
try {
|
||||
const saveProtocol = database.transactionAsync(async (transaction) => {
|
||||
const tx = transaction as unknown as Database;
|
||||
const before = id ? await protocolSnapshot(id, tx) : null;
|
||||
if (id && !before?.record)
|
||||
return fail(404, { protocolError: 'OS Protocol not found.' });
|
||||
if (id) {
|
||||
await (
|
||||
await database.prepare(
|
||||
await tx.prepare(
|
||||
'UPDATE os_protocols SET name=?,quality=?,primary_attribute_type=?,active=1,updated_at=? WHERE id=?',
|
||||
)
|
||||
).run(name, quality, attributeType, now, id);
|
||||
} else
|
||||
await (
|
||||
await database.prepare(
|
||||
await tx.prepare(
|
||||
'INSERT INTO os_protocols(id,name,quality,primary_attribute_type,created_at,updated_at) VALUES(?,?,?,?,?,?)',
|
||||
)
|
||||
).run(protocolId, name, quality, attributeType, now, now);
|
||||
await (
|
||||
await database.prepare(`INSERT INTO os_protocol_primary_effects(id,protocol_id,stat_attribute_id,base_value) VALUES(?,?,?,?)
|
||||
await tx.prepare(`INSERT INTO os_protocol_primary_effects(id,protocol_id,stat_attribute_id,base_value) VALUES(?,?,?,?)
|
||||
ON CONFLICT(protocol_id) DO UPDATE SET stat_attribute_id=excluded.stat_attribute_id,base_value=excluded.base_value`)
|
||||
).run(randomUUID(), protocolId, primaryStatId, primaryBaseValue);
|
||||
await (
|
||||
await database.prepare(`INSERT INTO os_protocol_talents(id,protocol_id,name,description_template,created_at,updated_at) VALUES(?,?,?,?,?,?)
|
||||
ON CONFLICT(protocol_id) DO UPDATE SET name=excluded.name,description_template=excluded.description_template,updated_at=excluded.updated_at`)
|
||||
).run(talentId, protocolId, talentName, descriptionTemplate, now, now);
|
||||
await (
|
||||
await database.prepare(
|
||||
'DELETE FROM os_protocol_talent_effects WHERE talent_id=?',
|
||||
)
|
||||
).run(talentId);
|
||||
let compatibilitySortOrder = 0;
|
||||
for (const outcome of numberedOutcomes) {
|
||||
if (outcome.kind !== 'attribute_modifier' || !outcome.attributeId)
|
||||
continue;
|
||||
compatibilitySortOrder += 1;
|
||||
if (hasTalent && conditionalRule) {
|
||||
await (
|
||||
await database.prepare(
|
||||
'INSERT INTO os_protocol_talent_effects(id,talent_id,stat_attribute_id,base_value,sort_order) VALUES(?,?,?,?,?)',
|
||||
await tx.prepare(`INSERT INTO os_protocol_talents(id,protocol_id,name,description_template,created_at,updated_at) VALUES(?,?,?,?,?,?)
|
||||
ON CONFLICT(protocol_id) DO UPDATE SET name=excluded.name,description_template=excluded.description_template,updated_at=excluded.updated_at`)
|
||||
).run(talentId, protocolId, talentName, descriptionTemplate, now, now);
|
||||
await (
|
||||
await tx.prepare(
|
||||
'DELETE FROM os_protocol_talent_effects WHERE talent_id=?',
|
||||
)
|
||||
).run(
|
||||
randomUUID(),
|
||||
).run(talentId);
|
||||
let compatibilitySortOrder = 0;
|
||||
for (const outcome of numberedOutcomes) {
|
||||
if (outcome.kind !== 'attribute_modifier' || !outcome.attributeId)
|
||||
continue;
|
||||
compatibilitySortOrder += 1;
|
||||
await (
|
||||
await tx.prepare(
|
||||
'INSERT INTO os_protocol_talent_effects(id,talent_id,stat_attribute_id,base_value,sort_order) VALUES(?,?,?,?,?)',
|
||||
)
|
||||
).run(
|
||||
randomUUID(),
|
||||
talentId,
|
||||
outcome.attributeId,
|
||||
outcome.variants.base.value,
|
||||
compatibilitySortOrder,
|
||||
);
|
||||
}
|
||||
await saveConditionalRuleInTransaction(
|
||||
tx,
|
||||
'os_protocol_talent',
|
||||
talentId,
|
||||
outcome.attributeId,
|
||||
outcome.variants.base.value,
|
||||
compatibilitySortOrder,
|
||||
conditionalRule,
|
||||
);
|
||||
} else if (existingTalent) {
|
||||
await (
|
||||
await tx.prepare(
|
||||
"DELETE FROM conditional_effect_rules WHERE source_type='os_protocol_talent' AND source_id=?",
|
||||
)
|
||||
).run(String(existingTalent.id));
|
||||
await (
|
||||
await tx.prepare('DELETE FROM os_protocol_talents WHERE id=?')
|
||||
).run(String(existingTalent.id));
|
||||
}
|
||||
await saveConditionalRuleInTransaction(
|
||||
database,
|
||||
'os_protocol_talent',
|
||||
talentId,
|
||||
conditionalRule,
|
||||
);
|
||||
await applyIconMutation(
|
||||
database,
|
||||
tx,
|
||||
'os_protocol',
|
||||
protocolId,
|
||||
iconMutation,
|
||||
operator.id,
|
||||
);
|
||||
await applyGameDataRender(
|
||||
database,
|
||||
tx,
|
||||
'os_protocol',
|
||||
protocolId,
|
||||
name,
|
||||
@@ -778,13 +848,13 @@ export const actions: Actions = {
|
||||
action: id ? 'os_protocol.updated' : 'os_protocol.created',
|
||||
entityId: protocolId,
|
||||
before,
|
||||
after: await protocolSnapshot(protocolId),
|
||||
after: await protocolSnapshot(protocolId, tx),
|
||||
database: tx,
|
||||
});
|
||||
await database.exec('COMMIT');
|
||||
} catch (error) {
|
||||
await database.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const transactionResult = await saveProtocol.immediate();
|
||||
if (transactionResult) return transactionResult;
|
||||
return {
|
||||
protocolSaved: true,
|
||||
protocolMode: id ? 'edit' : 'add',
|
||||
|
||||
Reference in New Issue
Block a user