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
+156
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
MECHANIC_EVENT_REGISTRY,
evaluateMechanicProgram,
formatMechanicProgram,
validateMechanicProgram,
@@ -14,6 +15,16 @@ const custom = (encounterSeconds = 30, inputs: Record<string, number | boolean>
});
describe('structured mechanic runtime', () => {
it('advertises the authoritative weapon-hit target and headshot fields', () => {
expect(MECHANIC_EVENT_REGISTRY.weapon_hit).toMatchObject({
description: expect.stringContaining('headshot boolean'),
fields: [
{ path: 'targetId', label: 'Target ID', type: 'string' },
{ path: 'values.headshot', label: 'Headshot', type: 'boolean' }
]
});
});
it('models Superior Firepower as same-target sliding hits and one shared expiring stack pool', () => {
const superiorFirepower: MechanicProgram = {
id: 'superior-firepower',
@@ -512,6 +523,151 @@ describe('structured mechanic runtime', () => {
]);
});
it('caps and refreshes target-scoped stacks independently', () => {
const program: MechanicProgram = {
id: 'target-trauma',
name: 'Target Trauma',
pools: [{
key: 'trauma',
label: 'Trauma',
scope: 'target',
initial: 0,
min: 0,
max: 10,
expiresAfterSeconds: 1
}],
clauses: [
{
id: 'body-stack',
when: 'weapon_hit',
if: {
kind: 'compare',
left: { source: 'event', path: 'values.headshot' },
operator: 'eq',
right: false
},
reducer: { kind: 'none' },
mutations: [{
kind: 'add',
pool: 'trauma',
entity: { kind: 'event_target' },
value: 1
}]
},
{
id: 'head-stack',
when: 'weapon_hit',
if: {
kind: 'compare',
left: { source: 'event', path: 'values.headshot' },
operator: 'eq',
right: true
},
reducer: { kind: 'none' },
mutations: [{
kind: 'add',
pool: 'trauma',
entity: { kind: 'event_target' },
value: 2
}]
}
]
};
const result = evaluateMechanicProgram(program, [
{ kind: 'weapon_hit', at: 0, targetId: 'enemy-a', values: { headshot: false } },
{ kind: 'weapon_hit', at: 0.25, targetId: 'enemy-b', values: { headshot: false } },
{ kind: 'weapon_hit', at: 0.5, targetId: 'enemy-a', values: { headshot: true } },
...Array.from({ length: 5 }, (_, index): MechanicEvent => ({
kind: 'weapon_hit',
at: 0.55 + index * 0.05,
targetId: 'enemy-a',
values: { headshot: true }
}))
], custom(0.9));
expect(result.status).toBe('OK');
expect(result.finalState).toEqual([
expect.objectContaining({
pool: 'trauma',
entityId: 'enemy-a',
value: 10,
expiresAt: 1.75
}),
expect.objectContaining({
pool: 'trauma',
entityId: 'enemy-b',
value: 1,
expiresAt: 1.25
})
]);
});
it('uses the prior target stack count for live-Attribute damage on pool exit', () => {
const skillDamageId = 'skill-damage';
const program: MechanicProgram = {
id: 'precise-trauma',
name: 'Precise Trauma',
pools: [{
key: 'trauma',
label: 'Trauma',
scope: 'target',
initial: 0,
min: 0,
max: 10,
expiresAfterSeconds: 1
}],
clauses: [{
id: 'apply-trauma',
when: 'weapon_hit',
reducer: { kind: 'none' },
mutations: [{
kind: 'add',
pool: 'trauma',
entity: { kind: 'event_target' },
value: 2
}],
outcomes: [{
id: 'trauma-damage',
label: 'Trauma Damage',
phase: 'on_exit',
kind: 'direct_damage',
scope: 'target',
basis: 'flat',
value: { ref: { source: 'attribute', attributeId: skillDamageId } },
statePool: 'trauma',
perStack: true,
minStacks: 2
}]
}]
};
const result = evaluateMechanicProgram(
program,
[{ kind: 'weapon_hit', at: 0, targetId: 'enemy-a', values: { headshot: true } }],
custom(2),
{
maxHealth: 100,
currentHealth: 100,
attributes: { [skillDamageId]: 75 }
}
);
expect(result.status).toBe('OK');
expect(result.outcomes).toMatchObject([{
outcomeId: 'trauma-damage',
targetId: 'enemy-a',
at: 1,
phase: 'on_exit',
value: 150,
resolvedValue: 150,
appliedValue: 150,
stateValue: 2
}]);
expect(result.finalState).toContainEqual(expect.objectContaining({
pool: 'trauma',
entityId: 'enemy-a',
value: 0,
expiresAt: null
}));
});
it('models scaled OS Protocol restoration, an unscaled Health cost, and post-cost bonus damage', () => {
const upgradeScale = {
op: 'add' as const,