Files
shd.build/src/lib/calculations/mechanic.test.ts
T

1225 lines
34 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
MECHANIC_EVENT_REGISTRY,
evaluateMechanicProgram,
formatMechanicProgram,
validateMechanicProgram,
type MechanicEvent,
type MechanicProgram
} from './mechanic.js';
const custom = (encounterSeconds = 30, inputs: Record<string, number | boolean> = {}) => ({
mode: 'custom' as const,
encounterSeconds,
inputs
});
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',
name: 'Superior Firepower',
pools: [{
key: 'relentless',
label: 'Relentless',
scope: 'agent',
initial: 0,
min: 0,
max: 10,
expiresAfterSeconds: 10
}],
clauses: [{
id: 'build-relentless',
label: 'Build Relentless',
when: 'weapon_hit',
if: { kind: 'exists', value: { source: 'event', path: 'targetId' } },
reducer: {
kind: 'sliding_count',
threshold: 9,
windowSeconds: 3,
groupBy: { source: 'event', path: 'targetId' }
},
mutations: [{ kind: 'add', pool: 'relentless', value: 1 }],
outcomes: [
{
id: 'relentless-weapon-damage',
label: 'Weapon Damage',
phase: 'while_active',
kind: 'attribute_modifier',
basis: 'flat',
scope: 'equipped_weapon',
attributeId: 'weapon-damage',
value: 3,
statePool: 'relentless',
perStack: true
},
{
id: 'relentless-critical-damage',
label: 'Critical Damage',
phase: 'while_active',
kind: 'attribute_modifier',
basis: 'flat',
scope: 'equipped_weapon',
attributeId: 'critical-damage',
value: 1,
statePool: 'relentless',
perStack: true
}
]
}]
};
const events: MechanicEvent[] = Array.from({ length: 18 }, (_, index) => ({
kind: 'weapon_hit',
at: index * 0.2,
targetId: 'enemy-a'
}));
const result = evaluateMechanicProgram(superiorFirepower, events, custom(15));
expect(result.status).toBe('OK');
expect(result.emissions[0].at).toBeCloseTo(1.6);
expect(result.emissions[1].at).toBeCloseTo(3.4);
expect(result.finalState).toContainEqual(expect.objectContaining({
pool: 'relentless',
value: 0,
expiresAt: null
}));
expect(result.outcomes.every((outcome) => outcome.attributeId !== null)).toBe(true);
expect(result.outcomes.map((outcome) => outcome.value)).toEqual([3, 1, 6, 2]);
expect(result.script).toContain('COUNT 9 MATCHES WITHIN 3s PER EVENT.targetId');
expect(result.script).toContain('ADD 1 TO relentless');
const maximum = evaluateMechanicProgram(superiorFirepower, [], {
mode: 'maximum',
encounterSeconds: 15
});
expect(maximum.finalState[0].value).toBe(10);
expect(maximum.outcomes.map((outcome) => outcome.value)).toEqual([30, 10]);
});
it('supports every-N-shot reducers without a bespoke action family', () => {
const program: MechanicProgram = {
id: 'every-three-shots',
name: 'Every Three Shots',
pools: [{ key: 'charges', label: 'Charges', scope: 'agent', initial: 0, max: 10 }],
clauses: [{
id: 'charge',
when: 'shot_fired',
reducer: { kind: 'modulo', every: 3 },
mutations: [{ kind: 'add', pool: 'charges', value: 1 }]
}]
};
const events = Array.from({ length: 7 }, (_, index): MechanicEvent => ({
kind: 'shot_fired',
at: index
}));
const result = evaluateMechanicProgram(program, events, custom(10));
expect(result.emissions.map((emission) => emission.at)).toEqual([2, 5]);
expect(result.finalState[0].value).toBe(2);
});
it('supports duration and periodic reducers over deterministic timeline snapshots', () => {
const program: MechanicProgram = {
id: 'timed-movement',
name: 'Timed Movement',
pools: [
{ key: 'settled', label: 'Settled', scope: 'agent', initial: 0, max: 1 },
{ key: 'tempo', label: 'Tempo', scope: 'agent', initial: 0, max: 10 }
],
clauses: [
{
id: 'immobile-duration',
when: 'movement_snapshot',
if: {
kind: 'compare',
left: { source: 'event', path: 'values.state' },
operator: 'eq',
right: 'immobile'
},
reducer: { kind: 'duration', seconds: 3 },
mutations: [{ kind: 'set', pool: 'settled', value: 1 }]
},
{
id: 'moving-periodic',
when: 'movement_snapshot',
if: {
kind: 'compare',
left: { source: 'event', path: 'values.state' },
operator: 'eq',
right: 'moving'
},
reducer: { kind: 'periodic', intervalSeconds: 2 },
mutations: [{ kind: 'add', pool: 'tempo', value: 1 }]
}
]
};
const result = evaluateMechanicProgram(program, [
{ kind: 'movement_snapshot', at: 0, values: { state: 'immobile' } },
{ kind: 'movement_snapshot', at: 2, values: { state: 'immobile' } },
{ kind: 'movement_snapshot', at: 3, values: { state: 'immobile' } },
{ kind: 'movement_snapshot', at: 5, values: { state: 'moving' } },
{ kind: 'movement_snapshot', at: 7, values: { state: 'moving' } },
{ kind: 'movement_snapshot', at: 9, values: { state: 'moving' } }
], custom(12));
expect(result.emissions.map(({ clauseId, at }) => ({ clauseId, at }))).toEqual([
{ clauseId: 'immobile-duration', at: 3 },
{ clauseId: 'moving-periodic', at: 7 },
{ clauseId: 'moving-periodic', at: 9 }
]);
expect(result.finalState).toEqual([
expect.objectContaining({ pool: 'settled', value: 1 }),
expect.objectContaining({ pool: 'tempo', value: 2 })
]);
});
it('emits outcomes in the declared event and state-transition phases', () => {
const program: MechanicProgram = {
id: 'phases',
name: 'Outcome Phases',
pools: [{ key: 'active', label: 'Active', scope: 'agent', initial: 0, max: 1 }],
clauses: [
{
id: 'activate',
when: 'weapon_hit',
reducer: { kind: 'none' },
mutations: [{ kind: 'set', pool: 'active', value: 1 }],
outcomes: [
{
id: 'triggering-hit',
label: 'Triggering Hit',
phase: 'on_event',
kind: 'marker',
basis: 'flat',
scope: 'equipped_weapon',
value: 1
},
{
id: 'entered',
label: 'Entered',
phase: 'on_enter',
kind: 'marker',
basis: 'flat',
scope: 'self',
statePool: 'active',
value: 1
},
{
id: 'active-modifier',
label: 'Active Modifier',
phase: 'while_active',
kind: 'attribute_modifier',
basis: 'flat',
scope: 'equipped_weapon',
attributeId: 'weapon-damage',
statePool: 'active',
value: 10
}
]
},
{
id: 'deactivate',
when: 'firing_stopped',
reducer: { kind: 'none' },
mutations: [{ kind: 'clear', pool: 'active' }],
outcomes: [{
id: 'exited',
label: 'Exited',
phase: 'on_exit',
kind: 'marker',
basis: 'flat',
scope: 'self',
statePool: 'active',
value: 1
}]
}
]
};
const result = evaluateMechanicProgram(program, [
{ kind: 'weapon_hit', at: 1, targetId: 'enemy-a' },
{ kind: 'firing_stopped', at: 2 }
], custom());
expect(result.outcomes.map((outcome) => `${outcome.outcomeId}:${outcome.phase}`)).toEqual([
'triggering-hit:on_event',
'entered:on_enter',
'active-modifier:while_active',
'exited:on_exit'
]);
});
it('emits on-exit outcomes at pool expiry and applies their resource side effects', () => {
const program: MechanicProgram = {
id: 'expiry-exit',
name: 'Expiry Exit',
pools: [{
key: 'active',
label: 'Active',
scope: 'agent',
initial: 0,
max: 1,
expiresAfterSeconds: 2
}],
clauses: [{
id: 'activate',
when: 'weapon_hit',
reducer: { kind: 'none' },
mutations: [{ kind: 'set', pool: 'active', value: 1 }],
outcomes: [{
id: 'expiry-health-cost',
label: 'Expiry Health Cost',
phase: 'on_exit',
kind: 'resource_cost',
scope: 'self',
resource: 'health',
basis: 'max_health_percent',
statePool: 'active',
value: 10
}]
}]
};
const result = evaluateMechanicProgram(
program,
[{ kind: 'weapon_hit', at: 1, targetId: 'enemy-a' }],
custom(10),
{ maxHealth: 100, currentHealth: 100 }
);
expect(result.outcomes).toMatchObject([{
outcomeId: 'expiry-health-cost',
phase: 'on_exit',
at: 3,
appliedValue: 10,
combatBefore: { currentHealth: 100 },
combatAfter: { currentHealth: 90 }
}]);
expect(result.finalCombat?.currentHealth).toBe(90);
expect(result.finalState).toContainEqual(expect.objectContaining({
pool: 'active',
value: 0,
expiresAt: null
}));
});
it('uses a custom percentage as a weighted estimate and resolves 100% in maximum mode', () => {
const program: MechanicProgram = {
id: 'full-health-hit',
name: 'Full Health Hit',
pools: [{
key: 'estimated-procs',
label: 'Estimated Procs',
scope: 'agent',
initial: 0
}],
scenarioInputs: [{
key: 'fullHealthHitPercent',
label: 'Hits received at full Health',
kind: 'slider',
defaultValue: 25,
bestCaseValue: 100,
min: 0,
max: 100
}],
clauses: [{
id: 'estimate-full-health-hits',
when: 'agent_hit',
reducer: { kind: 'none' },
weight: {
op: 'divide',
left: { ref: { source: 'scenario', key: 'fullHealthHitPercent' } },
right: 100
},
mutations: [{ kind: 'add', pool: 'estimated-procs', value: 1 }]
}]
};
const events: MechanicEvent[] = [0, 5, 10, 15].map((at) => ({ kind: 'agent_hit', at }));
const estimate = evaluateMechanicProgram(program, events, custom(20, { fullHealthHitPercent: 25 }));
expect(estimate.estimatedActivationCount).toBe(1);
expect(estimate.emissions.every((emission) => emission.weight === 0.25)).toBe(true);
expect(estimate.emissions.map((emission) => emission.mutations[0].after)).toEqual([
0.25,
0.5,
0.75,
1
]);
expect(estimate.finalState[0].value).toBe(1);
const maximum = evaluateMechanicProgram(program, events, { mode: 'maximum', encounterSeconds: 20 });
expect(maximum.estimatedActivationCount).toBe(4);
expect(maximum.scenarioInputs.fullHealthHitPercent).toBe(100);
expect(maximum.finalState[0].value).toBe(4);
});
it('derives proximity stacks directly from a builder-controlled snapshot', () => {
const program: MechanicProgram = {
id: 'nearby-allies',
name: 'Nearby Allies',
pools: [{
key: 'nearby-ally-stacks',
label: 'Nearby Ally Stacks',
scope: 'agent',
initial: 0,
min: 0,
max: 3,
bestCaseValue: 3
}],
clauses: [{
id: 'derive-nearby-allies',
when: 'proximity_snapshot',
reducer: {
kind: 'derived_scalar',
value: { ref: { source: 'event', path: 'values.alliesWithinRadius' } }
},
mutations: [{
kind: 'derive',
pool: 'nearby-ally-stacks',
value: { ref: { source: 'reducer' } }
}]
}]
};
const result = evaluateMechanicProgram(program, [{
kind: 'proximity_snapshot',
at: 0,
values: { alliesWithinRadius: 2 }
}], custom());
expect(result.finalState[0].value).toBe(2);
const maximum = evaluateMechanicProgram(program, [], { mode: 'maximum', encounterSeconds: 30 });
expect(maximum.finalState[0].value).toBe(3);
});
it('shares one movement charge pool across generation and consumption clauses', () => {
const program: MechanicProgram = {
id: 'mobile-charge',
name: 'Mobile Charge',
pools: [{
key: 'mobile-charge',
label: 'Mobile Charge',
scope: 'agent',
initial: 0,
min: 0,
max: 20
}],
clauses: [
{
id: 'gain-moving',
when: 'movement_snapshot',
if: {
kind: 'compare',
left: { source: 'event', path: 'values.state' },
operator: 'eq',
right: 'moving'
},
reducer: { kind: 'none' },
mutations: [{ kind: 'add', pool: 'mobile-charge', value: 1 }]
},
{
id: 'gain-sprinting',
when: 'movement_snapshot',
if: {
kind: 'compare',
left: { source: 'event', path: 'values.state' },
operator: 'eq',
right: 'sprinting'
},
reducer: { kind: 'none' },
mutations: [{ kind: 'add', pool: 'mobile-charge', value: 2 }]
},
{
id: 'spend-on-shot',
when: 'shot_fired',
if: {
kind: 'compare',
left: { source: 'pool', pool: 'mobile-charge' },
operator: 'gt',
right: 0
},
reducer: { kind: 'none' },
mutations: [{ kind: 'consume', pool: 'mobile-charge', value: 1 }]
},
{
id: 'spend-on-skill',
when: 'skill_deployed',
reducer: { kind: 'none' },
mutations: [{ kind: 'consume', pool: 'mobile-charge', value: 10 }]
}
]
};
const result = evaluateMechanicProgram(program, [
{ kind: 'movement_snapshot', at: 0, values: { state: 'moving' } },
{ kind: 'movement_snapshot', at: 1, values: { state: 'sprinting' } },
{ kind: 'movement_snapshot', at: 2, values: { state: 'sprinting' } },
{ kind: 'shot_fired', at: 3 },
{ kind: 'skill_deployed', at: 4 }
], custom());
expect(result.finalState[0].value).toBe(0);
expect(result.emissions.map((emission) => emission.clauseId)).toEqual([
'gain-moving',
'gain-sprinting',
'gain-sprinting',
'spend-on-shot',
'spend-on-skill'
]);
});
it('transfers a target-scoped mark to the target named by the death event', () => {
const program: MechanicProgram = {
id: 'transferring-mark',
name: 'Transferring Mark',
pools: [{
key: 'mark',
label: 'Mark',
scope: 'target',
initial: 0,
min: 0,
max: 1
}],
clauses: [
{
id: 'apply-mark',
when: 'weapon_hit',
reducer: { kind: 'none' },
mutations: [{
kind: 'set',
pool: 'mark',
entity: { kind: 'event_target' },
value: 1
}]
},
{
id: 'transfer-mark',
when: 'enemy_killed',
if: {
kind: 'compare',
left: { source: 'pool', pool: 'mark', entity: { kind: 'event_target' } },
operator: 'gt',
right: 0
},
reducer: { kind: 'none' },
mutations: [{
kind: 'transfer',
pool: 'mark',
from: { kind: 'event_target' },
to: { kind: 'event_field', path: 'values.transferTargetId' },
value: 'all'
}]
}
]
};
const result = evaluateMechanicProgram(program, [
{ kind: 'weapon_hit', at: 1, targetId: 'enemy-a' },
{
kind: 'enemy_killed',
at: 2,
targetId: 'enemy-a',
values: { transferTargetId: 'enemy-b' }
}
], custom());
expect(result.finalState).toEqual([
expect.objectContaining({ entityId: 'enemy-a', value: 0 }),
expect.objectContaining({ entityId: 'enemy-b', value: 1 })
]);
});
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,
left: 1,
right: {
op: 'divide' as const,
left: { ref: { source: 'equipment' as const, path: 'protocolTalentUpgrades' } },
right: 5
}
};
const program: MechanicProgram = {
id: 'os-protocol-health-engine',
name: 'OS Protocol Health Engine',
scenarioInputs: [{
key: 'restorationIntervalSeconds',
label: 'Restoration interval',
description: 'The game cadence is not yet verified; Custom Scenario must supply it.',
kind: 'number',
readiness: 'requires_assumption',
min: 0.01
}],
pools: [{
key: 'nearby-enemies',
label: 'Nearby Enemies',
scope: 'agent',
initial: 0,
min: 0,
max: 20
}],
clauses: [
{
id: 'sample-nearby-enemies',
when: 'proximity_snapshot',
reducer: {
kind: 'derived_scalar',
value: { ref: { source: 'event', path: 'values.enemiesWithinRadius' } }
},
mutations: [{
kind: 'derive',
pool: 'nearby-enemies',
value: { ref: { source: 'reducer' } }
}]
},
{
id: 'restore-nearby-enemies',
when: 'tick',
reducer: {
kind: 'periodic',
intervalSeconds: {
ref: {
source: 'scenario',
key: 'restorationIntervalSeconds'
}
}
},
mutations: [{ kind: 'none' }],
outcomes: [{
id: 'nearby-restoration',
label: 'Nearby Enemy Restoration',
phase: 'after_event',
kind: 'restoration',
scope: 'self',
resource: 'health',
basis: 'max_health_percent',
value: {
op: 'multiply',
left: {
op: 'clamp',
value: {
op: 'multiply',
left: { ref: { source: 'pool', pool: 'nearby-enemies' } },
right: 0.5
},
min: 0,
max: 2.5
},
right: upgradeScale
}
}]
},
{
id: 'shot-health-exchange',
when: 'shot_fired',
reducer: { kind: 'none' },
mutations: [{ kind: 'none' }],
outcomes: [
{
id: 'shot-health-cost',
label: 'Shot Health Cost',
phase: 'on_event',
kind: 'resource_cost',
scope: 'self',
resource: 'health',
basis: 'max_health_percent',
value: 2
},
{
id: 'shot-health-damage',
label: 'Current Health Bonus Damage',
phase: 'on_event',
kind: 'direct_damage',
scope: 'target',
basis: 'current_health_percent',
combatTiming: 'current',
value: {
op: 'multiply',
left: 0.6,
right: upgradeScale
}
}
]
}
]
};
const events: MechanicEvent[] = [
{ kind: 'tick', at: 0 },
{ kind: 'shot_fired', at: 1 },
{ kind: 'proximity_snapshot', at: 2, values: { enemiesWithinRadius: 9 } },
{ kind: 'tick', at: 3 }
];
const result = evaluateMechanicProgram(
program,
events,
custom(10, { restorationIntervalSeconds: 3 }),
{ maxHealth: 10_000, currentHealth: 10_000, weaponBaseDamage: 500 },
{ protocolTalentUpgrades: 5 }
);
expect(result.status).toBe('OK');
const cost = result.outcomes.find((outcome) => outcome.outcomeId === 'shot-health-cost');
const damage = result.outcomes.find((outcome) => outcome.outcomeId === 'shot-health-damage');
const restoration = result.outcomes.find((outcome) => outcome.outcomeId === 'nearby-restoration');
expect(cost).toMatchObject({
value: 2,
resolvedValue: 200,
appliedValue: 200
});
expect(damage).toMatchObject({
value: 1.2,
resolvedValue: 117.6,
appliedValue: 117.6
});
expect(restoration).toMatchObject({
value: 5,
resolvedValue: 500,
appliedValue: 200
});
expect(result.finalCombat?.currentHealth).toBe(10_000);
expect(result.equipment?.protocolTalentUpgrades).toBe(5);
const preCostProgram = structuredClone(program);
const preCostDamage = preCostProgram.clauses[2].outcomes?.find(
(outcome) => outcome.id === 'shot-health-damage'
);
if (preCostDamage) preCostDamage.combatTiming = 'pre_event';
const preCost = evaluateMechanicProgram(
preCostProgram,
[{ kind: 'shot_fired', at: 1 }],
custom(10, { restorationIntervalSeconds: 3 }),
{ maxHealth: 10_000, currentHealth: 10_000 },
{ protocolTalentUpgrades: 5 }
);
expect(preCost.outcomes.find((outcome) => outcome.outcomeId === 'shot-health-damage'))
.toMatchObject({ resolvedValue: 120 });
const unresolved = evaluateMechanicProgram(
program,
[],
{ mode: 'maximum', encounterSeconds: 10 },
{ maxHealth: 10_000, currentHealth: 10_000 },
{ protocolTalentUpgrades: 5 }
);
expect(unresolved.readiness).toEqual({
status: 'REQUIRES_ASSUMPTIONS',
missingInputs: ['restorationIntervalSeconds']
});
});
it('counts distinct values in a rolling window, resets consecutive runs, and applies named resources', () => {
const program: MechanicProgram = {
id: 'runtime-gaps',
name: 'Runtime gaps',
clauses: [
{
id: 'distinct-targets',
recipeKey: 'distinct-enemies-within-window',
when: 'weapon_hit',
if: { kind: 'exists', value: { source: 'event', path: 'targetId' } },
reducer: {
kind: 'distinct_count',
threshold: 3,
windowSeconds: 2,
distinctBy: { source: 'event', path: 'targetId' }
},
mutations: [{ kind: 'none' }],
outcomes: [{
id: 'distinct-marker',
label: 'Distinct target threshold',
phase: 'after_event',
kind: 'marker',
scope: 'all_skills',
basis: 'flat',
value: 1
}]
},
{
id: 'consecutive-targets',
when: 'shot_fired',
if: { kind: 'exists', value: { source: 'event', path: 'targetId' } },
reducer: {
kind: 'consecutive_count',
threshold: 3,
groupBy: { source: 'event', path: 'targetId' }
},
mutations: [{ kind: 'none' }],
outcomes: [{
id: 'consecutive-marker',
label: 'Consecutive target threshold',
phase: 'after_event',
kind: 'marker',
scope: 'assigned_ability',
basis: 'flat',
value: 1
}]
},
{
id: 'resources',
when: 'tick',
reducer: { kind: 'none' },
mutations: [{ kind: 'none' }],
outcomes: [
{
id: 'health-cost', label: 'Health cost', phase: 'on_event', kind: 'resource_cost',
scope: 'self', resource: 'health', basis: 'flat', value: 10
},
{
id: 'armor-restore', label: 'Armor restore', phase: 'on_event', kind: 'restoration',
scope: 'self_and_allies', resource: 'armor', basis: 'flat', value: 20
},
{
id: 'ammo-restore', label: 'Ammo restore', phase: 'on_event', kind: 'restoration',
scope: 'equipped_weapon', resource: 'ammo', basis: 'flat', value: 5
}
]
}
]
};
const result = evaluateMechanicProgram(program, [
{ kind: 'weapon_hit', at: 0, targetId: 'alpha' },
{ kind: 'weapon_hit', at: 0.2, targetId: 'alpha' },
{ kind: 'weapon_hit', at: 0.4, targetId: 'bravo' },
{ kind: 'weapon_hit', at: 0.6, targetId: 'charlie' },
{ kind: 'shot_fired', at: 1, targetId: 'alpha' },
{ kind: 'shot_fired', at: 1.1, targetId: 'alpha' },
{ kind: 'shot_fired', at: 1.2 },
{ kind: 'shot_fired', at: 1.3, targetId: 'bravo' },
{ kind: 'shot_fired', at: 1.4, targetId: 'bravo' },
{ kind: 'shot_fired', at: 1.5, targetId: 'bravo' },
{ kind: 'tick', at: 2 }
], custom(), { maxHealth: 100, currentHealth: 50, maxArmor: 50, currentArmor: 30, maxAmmo: 10, currentAmmo: 7 });
expect(result.status).toBe('OK');
expect(result.emissions.filter(({ clauseId }) => clauseId === 'distinct-targets')).toMatchObject([{ at: 0.6, reducerValue: 3 }]);
expect(result.emissions.filter(({ clauseId }) => clauseId === 'consecutive-targets')).toMatchObject([{ at: 1.5, reducerValue: 3 }]);
expect(result.outcomes.filter(({ clauseId }) => clauseId === 'resources')).toMatchObject([
{ outcomeId: 'health-cost', resource: 'health', appliedValue: 10 },
{ outcomeId: 'armor-restore', resource: 'armor', appliedValue: 20 },
{ outcomeId: 'ammo-restore', resource: 'ammo', appliedValue: 3 }
]);
expect(result.finalCombat).toMatchObject({ currentHealth: 40, currentArmor: 50, currentAmmo: 10 });
expect(result.script).toContain('COUNT 3 DISTINCT EVENT.targetId WITHIN 2s');
expect(result.script).toContain('COUNT 3 CONSECUTIVE MATCHES PER EVENT.targetId');
expect(result.script).toContain('[RECIPE distinct-enemies-within-window]');
});
it('resolves live Attribute percentages and delivers a restoration total continuously', () => {
const firepowerMultiplierId = '82a95611-283b-4fd8-a6ed-3567a316f36e';
const program: MechanicProgram = {
id: 'predatory-aura',
name: 'Predatory Aura',
clauses: [{
id: 'restore-on-kill',
when: 'enemy_killed',
reducer: { kind: 'none' },
mutations: [],
outcomes: [{
id: 'health-restoration',
label: 'Health restoration',
phase: 'after_event',
kind: 'restoration',
scope: 'self',
resource: 'health',
basis: 'flat',
delivery: 'continuous',
durationSeconds: 3,
value: {
op: 'multiply',
left: 240,
right: {
op: 'divide',
left: { ref: { source: 'attribute', attributeId: firepowerMultiplierId } },
right: 100
}
}
}]
}]
};
const result = evaluateMechanicProgram(
program,
[{ kind: 'enemy_killed', at: 0 }],
custom(3),
{
maxHealth: 1_000,
currentHealth: 500,
attributes: { [firepowerMultiplierId]: 100 }
}
);
expect(result.status).toBe('OK');
expect(result.outcomes).toHaveLength(1);
expect(result.outcomes[0]).toMatchObject({
at: 3,
delivery: 'continuous',
deliveryStart: 0,
deliveryEnd: 3,
totalResolvedValue: 240,
resolvedValue: 240,
appliedValue: 240
});
expect(result.finalCombat?.currentHealth).toBe(740);
expect(result.script).toContain(
'ATTRIBUTE[82a95611-283b-4fd8-a6ed-3567a316f36e] / 100'
);
expect(result.script).toContain('DELIVER TOTAL CONTINUOUSLY OVER 3s');
const missingAttribute = evaluateMechanicProgram(
program,
[{ kind: 'enemy_killed', at: 0 }],
custom(3),
{ maxHealth: 1_000, currentHealth: 500 }
);
expect(missingAttribute.status).toBe('INVALID');
expect(missingAttribute.errors).toContain(
`This mechanic requires authoritative Attribute ${firepowerMultiplierId}.`
);
});
it('integrates overlapping restoration streams at event boundaries without banking overheal', () => {
const program: MechanicProgram = {
id: 'continuous-restoration',
name: 'Continuous Restoration',
clauses: [{
id: 'restore',
when: 'enemy_killed',
reducer: { kind: 'none' },
mutations: [],
outcomes: [{
id: 'restore-health',
label: 'Restore Health',
phase: 'after_event',
kind: 'restoration',
scope: 'self',
resource: 'health',
basis: 'flat',
value: 240,
delivery: 'continuous',
durationSeconds: 3
}]
}, {
id: 'damage-between-streams',
when: 'agent_hit',
reducer: { kind: 'none' },
mutations: [],
outcomes: [{
id: 'health-cost',
label: 'Incoming damage proxy',
phase: 'on_event',
kind: 'resource_cost',
scope: 'self',
resource: 'health',
basis: 'flat',
value: 100
}]
}]
};
const result = evaluateMechanicProgram(
program,
[
{ kind: 'enemy_killed', at: 0 },
{ kind: 'agent_hit', at: 1 },
{ kind: 'enemy_killed', at: 1 }
],
custom(4),
{ maxHealth: 1_000, currentHealth: 500 }
);
const continuous = result.outcomes.filter((outcome) => outcome.delivery === 'continuous');
expect(continuous[0]).toMatchObject({ at: 1, resolvedValue: 80, appliedValue: 80 });
expect(continuous.reduce((total, outcome) => total + outcome.resolvedValue, 0)).toBeCloseTo(480);
expect(continuous.reduce((total, outcome) => total + outcome.appliedValue, 0)).toBeCloseTo(480);
expect(result.finalCombat?.currentHealth).toBe(880);
const capped = evaluateMechanicProgram(
program,
[{ kind: 'enemy_killed', at: 0 }],
custom(3),
{ maxHealth: 1_000, currentHealth: 950 }
);
expect(capped.outcomes.reduce((total, outcome) => total + outcome.appliedValue, 0)).toBe(50);
expect(capped.finalCombat?.currentHealth).toBe(1_000);
});
});
describe('mechanic validation and pseudo-script', () => {
it('enforces clause cooldowns without counting events received during the cooldown', () => {
const program: MechanicProgram = {
id: 'cooldown',
name: 'Cooldown',
clauses: [{
id: 'shot-proc',
when: 'shot_fired',
reducer: { kind: 'none' },
cooldownSeconds: 2,
mutations: [{ kind: 'none' }],
outcomes: [{
id: 'proc',
label: 'Proc',
phase: 'after_event',
kind: 'marker',
scope: 'self',
basis: 'flat',
value: 1
}]
}]
};
const result = evaluateMechanicProgram(program, [
{ kind: 'shot_fired', at: 0 },
{ kind: 'shot_fired', at: 1 },
{ kind: 'shot_fired', at: 2 },
{ kind: 'shot_fired', at: 3 }
], custom(4));
expect(result.emissions.map(({ at }) => at)).toEqual([0, 2]);
expect(result.outcomes.map(({ at }) => at)).toEqual([0, 2]);
expect(result.script).toContain('COOLDOWN 2s');
});
it('rejects invalid references, reducers, and target transfer scopes', () => {
const invalid: MechanicProgram = {
id: 'invalid',
name: 'Invalid',
pools: [{ key: 'agent-state', label: 'Agent State', scope: 'agent', initial: 0 }],
clauses: [{
id: 'bad',
recipeKey: '',
when: 'shot_fired',
reducer: { kind: 'modulo', every: 0 },
mutations: [{
kind: 'transfer',
pool: 'agent-state',
from: { kind: 'agent' },
to: { kind: 'event_target' }
}],
outcomes: [{
id: 'missing-attribute',
label: 'Missing',
phase: 'after_event',
kind: 'attribute_modifier',
basis: 'flat',
scope: 'self',
value: { ref: { source: 'scenario', key: 'missing-input' } }
}, {
id: 'missing-resource',
label: 'Missing resource',
phase: 'after_event',
kind: 'restoration',
scope: 'self',
basis: 'flat',
value: 1
}]
}]
};
const result = validateMechanicProgram(invalid);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.errors).toContain('Clause 1 modulo interval must be a positive integer.');
expect(result.errors).toContain('Clause 1 recipe key cannot be empty.');
expect(result.errors).toContain('Clause 1 mutation 1 can only transfer a target-scoped pool.');
expect(result.errors).toContain('Clause 1 outcome 1 requires an Attribute ID.');
expect(result.errors).toContain('Clause 1 outcome 1 value references unknown scenario input "missing-input".');
expect(result.errors).toContain('Clause 1 outcome 2 requires a resource.');
}
});
it('rejects invalid continuous-delivery combinations', () => {
const base = {
id: 'delivery-validation',
name: 'Delivery Validation',
clauses: [{
id: 'invalid-delivery',
when: 'enemy_killed' as const,
reducer: { kind: 'none' as const },
mutations: [],
outcomes: [{
id: 'damage-over-time',
label: 'Damage',
phase: 'after_event' as const,
kind: 'direct_damage' as const,
scope: 'target' as const,
basis: 'flat' as const,
value: 10,
delivery: 'continuous' as const,
durationSeconds: 3
}]
}]
};
const result = validateMechanicProgram(base);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.errors).toContain(
'Clause 1 outcome 1 can only use continuous delivery for restoration.'
);
}
});
it('formats nested conditions and state operations deterministically', () => {
const program: MechanicProgram = {
id: 'format',
name: 'Prepared',
pools: [{ key: 'prepared', label: 'Prepared', scope: 'agent', initial: 0, max: 10 }],
clauses: [{
id: 'gain',
label: 'Gain Prepared',
when: 'agent_hit',
if: {
kind: 'all',
predicates: [
{
kind: 'compare',
left: { source: 'event', path: 'values.healthPercentBefore' },
operator: 'eq',
right: 100
},
{ kind: 'not', predicate: { kind: 'exists', value: { source: 'event', path: 'values.blocked' } } }
]
},
reducer: { kind: 'none' },
mutations: [{ kind: 'add', pool: 'prepared', value: 1 }]
}]
};
expect(formatMechanicProgram(program)).toBe([
'MECHANIC PREPARED',
'STATE prepared [AGENT] 0..10',
'',
'WHEN AGENT HIT // Gain Prepared',
'IF (EVENT.values.healthPercentBefore == 100) AND (NOT (EXISTS EVENT.values.blocked))',
'ADD 1 TO prepared'
].join('\n'));
});
});