feat: support extra health state mechanics
Validate and deploy production / validate (push) Successful in 21s
Validate and deploy staging / validate (push) Successful in 23s
Validate and deploy production / deploy (push) Successful in 44s
Validate and deploy staging / deploy (push) Successful in 30s

This commit is contained in:
jb
2026-07-31 23:55:49 -04:00
parent 9ac2fe47cf
commit 1998e87309
16 changed files with 575 additions and 47 deletions
+132
View File
@@ -62,6 +62,138 @@ describe('structured mechanic runtime', () => {
});
});
it('advertises the authoritative Extra Health state contract', () => {
expect(MECHANIC_EVENT_REGISTRY.extra_health_changed).toEqual({
key: 'extra_health_changed',
label: 'Extra Health changed',
description: expect.stringContaining('transition to zero'),
strictFields: true,
fields: [
{
path: 'values.hasExtraHealth',
label: 'Has Extra Health',
type: 'boolean',
required: true
},
{
path: 'values.currentExtraHealth',
label: 'Current Extra Health',
type: 'number',
minimum: 0
}
]
});
});
it('gains one capped stack per second with Extra Health and clears all stacks on depletion', () => {
const skillIntensityId = 'skill-intensity';
const program: MechanicProgram = {
id: 'electrical-protection',
name: 'Electrical Protection',
pools: [
{ key: 'extra-health-active', label: 'Extra Health active', scope: 'agent', initial: 0, min: 0, max: 1 },
{ key: 'stacks', label: 'Electrical Protection stacks', scope: 'agent', initial: 0, min: 0, max: 10, bestCaseValue: 10 }
],
clauses: [
{
id: 'extra-health-present',
when: 'extra_health_changed',
if: { kind: 'compare', left: { source: 'event', path: 'values.hasExtraHealth' }, operator: 'eq', right: true },
reducer: { kind: 'none' },
mutations: [{ kind: 'set', pool: 'extra-health-active', value: 1 }]
},
{
id: 'extra-health-depleted',
when: 'extra_health_changed',
if: { kind: 'compare', left: { source: 'event', path: 'values.hasExtraHealth' }, operator: 'eq', right: false },
reducer: { kind: 'none' },
mutations: [
{ kind: 'clear', pool: 'extra-health-active' },
{ kind: 'clear', pool: 'stacks' }
]
},
{
id: 'gain-stack',
when: 'tick',
if: { kind: 'compare', left: { source: 'pool', pool: 'extra-health-active' }, operator: 'eq', right: 1 },
reducer: { kind: 'periodic', intervalSeconds: 1 },
mutations: [{ kind: 'add', pool: 'stacks', value: 1 }],
outcomes: [{
id: 'skill-intensity',
label: 'Skill Intensity',
phase: 'while_active',
kind: 'attribute_modifier',
scope: 'all_skills',
basis: 'flat',
attributeId: skillIntensityId,
value: 1.8,
statePool: 'stacks',
perStack: true
}]
}
]
};
const ticks = Array.from({ length: 13 }, (_, index): MechanicEvent => ({ kind: 'tick', at: index }));
const capped = evaluateMechanicProgram(program, [
{ kind: 'extra_health_changed', at: 0, values: { hasExtraHealth: true, currentExtraHealth: 500 } },
...ticks
], custom(12), { currentHealth: 1000, maxHealth: 1000, attributes: { [skillIntensityId]: 0 } });
expect(capped.status).toBe('OK');
expect(capped.finalState).toEqual(expect.arrayContaining([
expect.objectContaining({ pool: 'stacks', value: 10 })
]));
expect(capped.outcomes.at(-1)).toMatchObject({ value: 18, stateValue: 10 });
const depleted = evaluateMechanicProgram(program, [
{ kind: 'extra_health_changed', at: 0, values: { hasExtraHealth: true, currentExtraHealth: 500 } },
{ kind: 'tick', at: 0 },
{ kind: 'tick', at: 1 },
{ kind: 'extra_health_changed', at: 1.5, values: { hasExtraHealth: true, currentExtraHealth: 100 } },
{ kind: 'tick', at: 2 },
{ kind: 'extra_health_changed', at: 2.5, values: { hasExtraHealth: false, currentExtraHealth: 0 } },
{ kind: 'tick', at: 3 }
], custom(3), { currentHealth: 1000, maxHealth: 1000, attributes: { [skillIntensityId]: 0 } });
expect(depleted.status).toBe('OK');
expect(depleted.finalState).toEqual(expect.arrayContaining([
expect.objectContaining({ pool: 'extra-health-active', value: 0 }),
expect.objectContaining({ pool: 'stacks', value: 0 })
]));
expect(depleted.emissions.filter((emission) => emission.clauseId === 'gain-stack')).toHaveLength(2);
});
it('rejects malformed Extra Health state fields and inconsistent amounts', () => {
const program: MechanicProgram = {
id: 'extra-health-state',
name: 'Extra Health state',
clauses: [{
id: 'state-change',
when: 'extra_health_changed',
if: { kind: 'compare', left: { source: 'event', path: 'values.hasExtraHealth' }, operator: 'eq', right: true },
reducer: { kind: 'none' },
mutations: [{ kind: 'none' }]
}]
};
for (const event of [
{ kind: 'extra_health_changed', at: 0, values: {} },
{ kind: 'extra_health_changed', at: 0, values: { hasExtraHealth: true, currentExtraHealth: -1 } },
{ kind: 'extra_health_changed', at: 0, values: { hasExtraHealth: false, currentExtraHealth: 1 } }
] as MechanicEvent[]) {
expect(evaluateMechanicProgram(program, [event], custom(1)).status).toBe('INVALID');
}
const unknownField = structuredClone(program);
unknownField.clauses[0].if = {
kind: 'compare',
left: { source: 'event', path: 'values.extraHealthActive' },
operator: 'eq',
right: true
};
expect(validateMechanicProgram(unknownField)).toMatchObject({
ok: false,
errors: [expect.stringContaining('is not defined for Extra Health changed')]
});
});
it('executes imported talent trigger families as first-class events', () => {
const eventKinds: MechanicEventKind[] = [
'ally_downed',