Support target-scoped trauma mechanics

This commit is contained in:
JB
2026-07-27 22:20:52 -04:00
parent 7e78d75f85
commit 0b3be3587b
20 changed files with 1032 additions and 93 deletions
@@ -54,6 +54,41 @@ const draft = (): ConditionalMechanicDraftV1 => ({
});
describe('conditional mechanic authoring payload', () => {
it('accepts an empty captured-only program for deferred modeling', () => {
const partial: ConditionalMechanicDraftV1 = {
version: 1,
simulationReadiness: 'captured_only',
scenarioInputs: [],
pools: [],
clauses: []
};
const result = parseConditionalMechanicDraft(partial, {
variantKeys: ['I', 'II']
});
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.draft).toEqual(partial);
expect(result.value.programs.I.clauses).toEqual([]);
expect(result.value.programs.II.clauses).toEqual([]);
});
it('requires a branch after a partial program is marked calculation-ready', () => {
const partial: ConditionalMechanicDraftV1 = {
version: 1,
simulationReadiness: 'ready',
scenarioInputs: [],
pools: [],
clauses: []
};
const result = parseConditionalMechanicDraft(partial, {
variantKeys: ['I', 'II']
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.errors).toContain('A simulation-ready mechanic requires at least one clause.');
}
});
it('builds one validated runtime program per exact source variant', () => {
const result = parseConditionalMechanicDraft(draft(), {
variantKeys: ['I', 'II'],
@@ -263,6 +298,98 @@ describe('conditional mechanic authoring payload', () => {
}
});
it('preserves target entity mutations and state-scaled exit outcomes', () => {
const targeted: ConditionalMechanicDraftV1 = {
version: 1,
simulationReadiness: 'ready',
scenarioInputs: [],
pools: [{
key: 'marked-target',
label: 'Marked Target',
stateKind: 'target_mark',
scope: 'target',
initial: 0,
min: 0,
max: 1
}],
clauses: [{
key: 'detonate-mark',
event: 'enemy_killed',
reducer: { kind: 'none' },
mutations: [{
kind: 'set',
pool: 'marked-target',
entity: { kind: 'event_target' },
value: 1
}],
outcomes: [{
key: 'target-damage',
label: 'Target Damage',
phase: 'on_exit',
kind: 'direct_damage',
scope: 'target',
basis: 'flat',
statePool: 'marked-target',
perStack: true,
variants: {
base: {
value: 240,
expression: {
op: 'multiply',
left: 240,
right: {
op: 'divide',
left: {
ref: {
source: 'attribute',
attributeId: 'firepower-multiplier'
}
},
right: 100
}
}
}
}
}]
}]
};
const result = parseConditionalMechanicDraft(targeted, {
variantKeys: ['base'],
allowedAttributeIds: new Set(['firepower-multiplier'])
});
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.draft).toMatchObject(targeted);
expect(result.value.programs.base).toMatchObject({
pools: [{ key: 'marked-target', scope: 'target' }],
clauses: [{
mutations: [{
kind: 'set',
pool: 'marked-target',
entity: { kind: 'event_target' },
value: 1
}],
outcomes: [{
phase: 'on_exit',
kind: 'direct_damage',
statePool: 'marked-target',
perStack: true,
value: {
right: {
left: {
ref: {
source: 'attribute',
attributeId: 'firepower-multiplier'
}
}
}
}
}]
}]
});
});
it('rejects outcome fields that are absent from the canonical capability manifest', () => {
const unsupported = draft();
(unsupported.clauses[0].outcomes[0] as unknown as Record<string, unknown>).cadence =