import { describe, expect, it } from 'vitest'; import { MECHANIC_EVENT_REGISTRY, evaluateMechanicProgram, formatMechanicProgram, validateMechanicProgram, type MechanicEvent, type MechanicEventKind, type MechanicProgram, type NumericExpression, } from './mechanic.js'; const custom = ( encounterSeconds = 30, inputs: Record = {}, ) => ({ mode: 'custom' as const, encounterSeconds, inputs, }); const closeAndPersonalProgram = ( resolvedVariantValue: number, ): MechanicProgram => ({ id: 'close-and-personal-engineering', name: 'Close and Personal Engineering', pools: [ { key: 'eligible-allies', label: 'Eligible Allies', scope: 'agent', initial: 0, min: 0, max: 20, }, ], clauses: [ { id: 'sample-eligible-allies', when: 'proximity_snapshot', reducer: { kind: 'derived_scalar', value: { ref: { source: 'event', path: 'values.alliesWithin30MetersWithAgentGrantedBuff', }, }, }, mutations: [ { kind: 'derive', pool: 'eligible-allies', value: { ref: { source: 'reducer' } }, }, ], }, { id: 'resolve-engineering', when: 'tick', reducer: { kind: 'none' }, mutations: [{ kind: 'none' }], outcomes: [ { id: 'engineering', label: 'Engineering', phase: 'on_event', kind: 'attribute_modifier', scope: 'self', basis: 'flat', attributeId: 'engineering', value: { op: 'multiply', left: { op: 'multiply', left: { ref: { source: 'pool', pool: 'eligible-allies' } }, right: resolvedVariantValue, }, right: { op: 'add', left: 1, right: { op: 'divide', left: { ref: { source: 'equipment', path: 'protocolTalentUpgrades', }, }, right: 5, }, }, }, }, ], }, ], }); describe('deterministic mechanic probabilities and assigned abilities', () => { it('rolls outcome probability independently with a reproducible seed and preserves a refreshed window', () => { const program: MechanicProgram = { id: 'ammo-refill', name: 'Ammo Refill', pools: [ { key: 'tactical-link-window', label: 'Tactical Link window', scope: 'agent', initial: 0, min: 0, max: 1, expiresAfterSeconds: 6, }, ], clauses: [ { id: 'start-window', when: 'signature_ability_deployed', reducer: { kind: 'none' }, mutations: [{ kind: 'set', pool: 'tactical-link-window', value: 1 }], }, { id: 'refresh-window', when: 'enemy_killed', reducer: { kind: 'none' }, mutations: [{ kind: 'set', pool: 'tactical-link-window', value: 1 }], }, { id: 'recover-ammo', when: 'shot_fired', if: { kind: 'compare', left: { source: 'pool', pool: 'tactical-link-window' }, operator: 'eq', right: 1, }, reducer: { kind: 'none' }, mutations: [], outcomes: [ { id: 'recover-one-ammo', label: 'Recover one ammo', phase: 'after_event', kind: 'restoration', scope: 'self', basis: 'flat', resource: 'ammo', value: 1, probabilityPercent: 29, }, ], }, ], }; const events = [ { kind: 'signature_ability_deployed' as const, at: 0, values: { assignedAbilityDurationSeconds: 6 }, }, { kind: 'shot_fired' as const, at: 1 }, { kind: 'shot_fired' as const, at: 2 }, { kind: 'enemy_killed' as const, at: 5 }, { kind: 'shot_fired' as const, at: 6 }, { kind: 'shot_fired' as const, at: 11 }, ]; const first = evaluateMechanicProgram( program, events, { mode: 'custom', encounterSeconds: 12, randomSeed: 1 }, { maxHealth: 100, currentHealth: 100, maxAmmo: 20, currentAmmo: 5 }, ); const second = evaluateMechanicProgram( program, events, { mode: 'custom', encounterSeconds: 12, randomSeed: 1 }, { maxHealth: 100, currentHealth: 100, maxAmmo: 20, currentAmmo: 5 }, ); expect(first.status).toBe('OK'); expect(first.randomSeed).toBe(1); expect(first.outcomes.map((outcome) => outcome.at)).toEqual([2]); expect(first.outcomes[0]).toMatchObject({ probabilityPercent: 29, probabilityRoll: expect.any(Number), appliedValue: 1, }); expect(first.finalCombat?.currentAmmo).toBe(6); expect(second.outcomes).toEqual(first.outcomes); }); it('gates a proc before mutations, extends a live assigned ability, and starts cooldown on the eighth stack', () => { const program: MechanicProgram = { id: 'tactical-loop', name: 'Tactical Loop', pools: [ { key: 'extension-stacks', label: 'Extension stacks', scope: 'agent', initial: 7, min: 0, max: 8, }, { key: 'extension-cooldown', label: 'Extension cooldown', scope: 'agent', initial: 0, min: 0, max: 1, expiresAfterSeconds: 15, }, ], clauses: [ { id: 'extend-tactical-link', when: 'skill_hit', if: { kind: 'compare', left: { source: 'pool', pool: 'extension-cooldown' }, operator: 'eq', right: 0, }, requiresAssignedAbilityActive: true, procChancePercent: 10, reducer: { kind: 'none' }, mutations: [ { kind: 'add', pool: 'extension-stacks', value: 1 }, { kind: 'extend_duration', target: 'assigned_ability', seconds: 0.3, }, { kind: 'start_cooldown_on_max', stackPool: 'extension-stacks', cooldownPool: 'extension-cooldown', }, ], }, ], }; const success = evaluateMechanicProgram( program, [ { kind: 'signature_ability_deployed', at: 0, values: { assignedAbilityDurationSeconds: 6 }, }, { kind: 'skill_hit', at: 5.9 }, { kind: 'skill_hit', at: 6.1 }, ], { mode: 'custom', encounterSeconds: 7, randomSeed: 7 }, ); const failed = evaluateMechanicProgram( program, [ { kind: 'signature_ability_deployed', at: 0, values: { assignedAbilityDurationSeconds: 6 }, }, { kind: 'skill_hit', at: 5.9 }, ], { mode: 'custom', encounterSeconds: 7, randomSeed: 1 }, ); expect(success.status).toBe('OK'); expect(success.emissions).toHaveLength(1); const extension = success.emissions[0].mutations.find( (mutation) => mutation.kind === 'extend_duration', ); expect(extension).toMatchObject({ kind: 'extend_duration' }); expect(extension?.before).toBeCloseTo(0.1); expect(extension?.after).toBeCloseTo(0.4); expect(success.emissions[0].mutations).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: 'start_cooldown_on_max', pool: 'extension-cooldown', after: 1, }), ]), ); expect(success.finalState).toEqual( expect.arrayContaining([ expect.objectContaining({ pool: 'extension-stacks', value: 8, }), expect.objectContaining({ pool: 'extension-cooldown', value: 1, expiresAt: 20.9, }), ]), ); expect(failed.emissions).toEqual([]); }); it('does not extend an assigned ability at its exact expiry and rejects invalid chance data', () => { const boundaryProgram: MechanicProgram = { id: 'exact-expiry', name: 'Exact expiry', clauses: [ { id: 'extend', when: 'skill_hit', requiresAssignedAbilityActive: true, procChancePercent: 100, reducer: { kind: 'none' }, mutations: [ { kind: 'extend_duration', target: 'assigned_ability', seconds: 0.3, }, ], }, ], }; expect( evaluateMechanicProgram( boundaryProgram, [ { kind: 'signature_ability_deployed', at: 0, values: { assignedAbilityDurationSeconds: 1 }, }, { kind: 'skill_hit', at: 1 }, ], { mode: 'custom', encounterSeconds: 2, randomSeed: 7 }, ).emissions, ).toEqual([]); const invalid = structuredClone(boundaryProgram); invalid.clauses[0].procChancePercent = 101; expect(validateMechanicProgram(invalid)).toMatchObject({ ok: false, errors: [ expect.stringContaining('proc chance must be from 0 through 100'), ], }); const invalidOutcome = structuredClone(boundaryProgram); invalidOutcome.clauses[0].outcomes = [ { id: 'bad-chance', label: 'Bad chance', phase: 'after_event', kind: 'marker', scope: 'self', basis: 'flat', value: 1, probabilityPercent: -1, }, ]; expect(validateMechanicProgram(invalidOutcome)).toMatchObject({ ok: false, errors: [ expect.stringContaining('probability must be from 0 through 100'), ], }); }); }); describe('structured mechanic runtime', () => { it('advertises the authoritative weapon-hit target and headshot fields', () => { expect(MECHANIC_EVENT_REGISTRY.weapon_hit).toMatchObject({ description: expect.stringContaining('previous target'), fields: [ { path: 'targetId', label: 'Target ID', type: 'string' }, { path: 'values.previousTargetId', label: 'Previous target ID', type: 'string', }, { path: 'values.headshot', label: 'Headshot', type: 'boolean' }, { path: 'values.critical', label: 'Critical hit', type: 'boolean' }, { path: 'values.aiming', label: 'Aiming down sights', type: 'boolean', }, { path: 'values.behindCover', label: 'Agent behind cover', type: 'boolean', }, { path: 'values.distanceMeters', label: 'Target distance', type: 'number', }, ], }); }); it('advertises the optional non-negative integer proximity count without making snapshots strict', () => { const definition = MECHANIC_EVENT_REGISTRY.proximity_snapshot; const countField = definition.fields?.find( ({ path }) => path === 'values.alliesWithin30MetersWithAgentGrantedBuff', ); expect(definition.strictFields).not.toBe(true); expect(countField).toEqual({ path: 'values.alliesWithin30MetersWithAgentGrantedBuff', label: 'Allies within 30 meters with an Agent-granted buff', type: 'number', minimum: 0, integer: true, }); expect(countField?.required).not.toBe(true); }); it('advertises imported support and equipment-use trigger events', () => { expect(MECHANIC_EVENT_REGISTRY.ally_downed).toMatchObject({ key: 'ally_downed', label: 'Ally downed', }); expect(MECHANIC_EVENT_REGISTRY.agent_downed).toMatchObject({ key: 'agent_downed', label: 'Agent downed', }); expect(MECHANIC_EVENT_REGISTRY.ally_rescued).toMatchObject({ key: 'ally_rescued', label: 'Ally rescued', }); expect(MECHANIC_EVENT_REGISTRY.grenade_thrown).toMatchObject({ key: 'grenade_thrown', label: 'Grenade thrown', }); expect(MECHANIC_EVENT_REGISTRY.medical_kit_used).toMatchObject({ key: 'medical_kit_used', label: 'Medical Kit used', }); expect(MECHANIC_EVENT_REGISTRY.signature_ability_deployed).toMatchObject({ key: 'signature_ability_deployed', label: 'Signature Ability deployed', }); expect(MECHANIC_EVENT_REGISTRY.skill_multishot_triggered).toMatchObject({ key: 'skill_multishot_triggered', label: 'Skill Multi-Shot triggered', }); }); 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('matches kills and assists only while the assigned ability is active', () => { const weaponCriticalHitChance = 'weapon-critical-hit-chance'; const activeOutcome = { id: 'tactical-link-critical-chance', label: 'Weapon Critical Hit Chance', phase: 'while_active' as const, kind: 'attribute_modifier' as const, scope: 'self' as const, basis: 'flat' as const, value: 10.2, attributeId: weaponCriticalHitChance, statePool: 'tactical-link-kill-bonus', }; const activePredicate = { kind: 'compare' as const, left: { source: 'event' as const, path: 'values.assignedAbilityActive', }, operator: 'eq' as const, right: true, }; const program: MechanicProgram = { id: 'tactical-link-kill-bonus', name: 'Tactical Link Kill Bonus', pools: [ { key: 'tactical-link-kill-bonus', label: 'Tactical Link kill bonus', scope: 'agent', initial: 0, min: 0, max: 1, expiresAfterSeconds: 10, }, ], clauses: [ { id: 'kill-during-tactical-link', when: 'enemy_killed', if: activePredicate, reducer: { kind: 'none' }, mutations: [ { kind: 'set', pool: 'tactical-link-kill-bonus', value: 1 }, ], outcomes: [activeOutcome], }, { id: 'assist-during-tactical-link', when: 'assist', if: activePredicate, reducer: { kind: 'none' }, mutations: [ { kind: 'set', pool: 'tactical-link-kill-bonus', value: 1 }, ], outcomes: [{ ...activeOutcome }], }, ], }; expect(validateMechanicProgram(program)).toMatchObject({ ok: true }); const result = evaluateMechanicProgram( program, [ { kind: 'enemy_killed', at: 0, values: { assignedAbilityActive: true }, }, { kind: 'assist', at: 4, values: { assignedAbilityActive: false }, }, { kind: 'enemy_killed', at: 6 }, { kind: 'assist', at: 10, values: { assignedAbilityActive: true }, }, ], custom(19), { maxHealth: 1_000, currentHealth: 1_000, attributes: { [weaponCriticalHitChance]: 0 }, }, ); expect(result.status).toBe('OK'); expect(result.outcomes.map((outcome) => outcome.at)).toEqual([0, 10]); expect(result.finalState).toContainEqual( expect.objectContaining({ pool: 'tactical-link-kill-bonus', value: 1, expiresAt: 20, }), ); }); it('rejects invalid assigned-ability activity predicates and event values', () => { const program: MechanicProgram = { id: 'assigned-ability-activity', name: 'Assigned ability activity', clauses: [ { id: 'kill', when: 'enemy_killed', if: { kind: 'compare', left: { source: 'event', path: 'values.assignedAbilityActive', }, operator: 'eq', right: true, }, reducer: { kind: 'none' }, mutations: [], }, ], }; const invalidPredicate = structuredClone(program); invalidPredicate.clauses[0].if = { kind: 'compare', left: { source: 'event', path: 'values.assignedAbilityActive' }, operator: 'eq', right: 'yes', }; expect(validateMechanicProgram(invalidPredicate)).toMatchObject({ ok: false, errors: [ expect.stringContaining('Assigned ability active with a boolean value'), ], }); expect( evaluateMechanicProgram( program, [ { kind: 'enemy_killed', at: 0, values: { assignedAbilityActive: 'yes' }, } as unknown as MechanicEvent, ], custom(1), ), ).toMatchObject({ status: 'INVALID', errors: [ expect.stringContaining('Assigned ability active must be a boolean'), ], }); }); it('executes imported talent trigger families as first-class events', () => { const eventKinds: MechanicEventKind[] = [ 'ally_downed', 'agent_downed', 'ally_rescued', 'grenade_thrown', 'medical_kit_used', 'signature_ability_deployed', 'skill_multishot_triggered', ]; const program: MechanicProgram = { id: 'imported-talent-events', name: 'Imported talent events', clauses: eventKinds.map((when) => ({ id: when, when, reducer: { kind: 'none' }, mutations: [{ kind: 'none' }], outcomes: [ { id: `${when}-marker`, label: `${when} marker`, phase: 'after_event', kind: 'marker', scope: 'self', basis: 'flat', value: 1, }, ], })), }; expect(validateMechanicProgram(program)).toMatchObject({ ok: true }); const events: MechanicEvent[] = eventKinds.map((kind, index) => ({ kind, at: index + 1, })); expect( evaluateMechanicProgram(program, events, custom()).outcomes.map( (outcome) => outcome.outcomeId, ), ).toEqual(eventKinds.map((kind) => `${kind}-marker`)); }); it('derives the previous weapon-hit target for target-switch mechanics', () => { const program: MechanicProgram = { id: 'target-switch-stacks', name: 'Target Switch Stacks', pools: [ { key: 'stacks', label: 'Stacks', scope: 'agent', initial: 0, min: 0, max: 100, expiresAfterSeconds: 5, }, ], clauses: [ { id: 'halve-on-switch', when: 'weapon_hit', if: { kind: 'all', predicates: [ { kind: 'exists', value: { source: 'event', path: 'values.previousTargetId' }, }, { kind: 'compare', left: { source: 'event', path: 'values.previousTargetId' }, operator: 'ne', right: { source: 'event', path: 'targetId' }, }, ], }, reducer: { kind: 'none' }, mutations: [ { kind: 'set', pool: 'stacks', value: { op: 'multiply', left: { ref: { source: 'pool', pool: 'stacks' } }, right: 0.5, }, }, ], }, { id: 'build-stacks', when: 'weapon_hit', if: { kind: 'exists', value: { source: 'event', path: 'targetId' } }, reducer: { kind: 'none' }, mutations: [{ kind: 'add', pool: 'stacks', value: 1 }], }, ], }; const result = evaluateMechanicProgram( program, [ { kind: 'weapon_hit', at: 0, targetId: 'alpha' }, { kind: 'weapon_hit', at: 1, targetId: 'alpha' }, { kind: 'weapon_hit', at: 2, targetId: 'bravo' }, ], custom(3), ); expect(result.status).toBe('OK'); expect(result.finalState).toContainEqual( expect.objectContaining({ pool: 'stacks', value: 2, }), ); expect(result.emissions.map((emission) => emission.clauseId)).toEqual([ 'build-stacks', 'build-stacks', 'halve-on-switch', 'build-stacks', ]); }); 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('preserves an explicit zero clause weight', () => { const result = evaluateMechanicProgram( { id: 'zero-weight', name: 'Zero weight', clauses: [ { id: 'zero-weight-clause', when: 'agent_hit', reducer: { kind: 'none' }, weight: 0, mutations: [], }, ], }, [{ kind: 'agent_hit', at: 0 }], custom(1), ); expect(result.emissions).toHaveLength(1); expect(result.emissions[0].weight).toBe(0); expect(result.estimatedActivationCount).toBe(0); }); it.each([0, 0.5])( 'keeps a weighted reload delay intact at weight %s', (weight) => { const result = evaluateMechanicProgram( { id: 'weighted-reload', name: 'Weighted reload', clauses: [ { id: 'weighted-reload-clause', when: 'weapon_hit', reducer: { kind: 'none' }, weight, mutations: [], outcomes: [ { id: 'reload', label: 'Reload', phase: 'on_event', kind: 'weapon_reload', scope: 'equipped_weapon', value: 5, basis: 'flat', }, ], }, ], }, [{ kind: 'weapon_hit', at: 1 }], custom(10), ); expect(result.outcomes).toHaveLength(1); expect(result.outcomes[0]).toMatchObject({ at: 6, weight, resolvedValue: 5, appliedValue: 5, }); }, ); it('preserves a zero weight for maximum-mode passive outcomes', () => { const result = evaluateMechanicProgram( { id: 'zero-weight-maximum', name: 'Zero weight maximum', clauses: [ { id: 'zero-weight-maximum-clause', label: 'Maximum passive', when: 'weapon_hit', reducer: { kind: 'none' }, weight: 0, mutations: [], outcomes: [ { id: 'marker', label: 'Passive marker', phase: 'while_active', kind: 'marker', scope: 'self', value: 1, basis: 'flat', }, ], }, ], }, [], { mode: 'maximum', encounterSeconds: 10 }, ); expect(result.outcomes).toHaveLength(1); expect(result.outcomes[0]).toMatchObject({ weight: 0, appliedValue: 0 }); }); 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.status).toBe('OK'); expect(result.finalState[0].value).toBe(2); const maximum = evaluateMechanicProgram(program, [], { mode: 'maximum', encounterSeconds: 30, }); expect(maximum.finalState[0].value).toBe(3); }); it.each([ { count: 0, upgrades: 0, multiplier: 0 }, { count: 3, upgrades: 0, multiplier: 3 }, { count: 3, upgrades: 5, multiplier: 6 }, ])( 'resolves Close-and-Personal-style Engineering for count $count and $upgrades OS upgrades', ({ count, upgrades, multiplier }) => { const resolvedVariantValue = 7.25; const result = evaluateMechanicProgram( closeAndPersonalProgram(resolvedVariantValue), [ { kind: 'proximity_snapshot', at: 0, values: { alliesWithin30MetersWithAgentGrantedBuff: count, }, }, { kind: 'tick', at: 1 }, ], custom(), undefined, { protocolTalentUpgrades: upgrades }, ); expect(result.status).toBe('OK'); expect(result.finalState).toContainEqual( expect.objectContaining({ pool: 'eligible-allies', value: count }), ); expect( result.outcomes.find(({ outcomeId }) => outcomeId === 'engineering'), ).toMatchObject({ value: resolvedVariantValue * multiplier, resolvedValue: resolvedVariantValue * multiplier, }); }, ); it.each([ { count: '2', error: 'Allies within 30 meters with an Agent-granted buff must be a number.', }, { count: -1, error: 'Allies within 30 meters with an Agent-granted buff must be 0 or greater.', }, { count: 1.5, error: 'Allies within 30 meters with an Agent-granted buff must be an integer.', }, ])('rejects invalid proximity ally count $count', ({ count, error }) => { const result = evaluateMechanicProgram( closeAndPersonalProgram(7.25), [ { kind: 'proximity_snapshot', at: 0, values: { alliesWithin30MetersWithAgentGrantedBuff: count, }, }, ], custom(), undefined, { protocolTalentUpgrades: 0 }, ); expect(result.status).toBe('INVALID'); expect(result.errors).toContain(`Event 1 (Proximity snapshot) ${error}`); }); 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('shares an identical timed modifier outcome across kill and assist refresh clauses', () => { const weaponDamage = { id: 'pummel-weapon-damage', label: 'Weapon Damage', phase: 'while_active' as const, kind: 'attribute_modifier' as const, scope: 'equipped_weapon' as const, basis: 'flat' as const, value: 10, attributeId: 'weapon-damage', statePool: 'pummel-active', }; const program: MechanicProgram = { id: 'pummel-refresh', name: 'Pummel Refresh', pools: [ { key: 'pummel-active', label: 'Pummel active', scope: 'agent', initial: 0, min: 0, max: 1, expiresAfterSeconds: 12, }, ], clauses: [ { id: 'activate-on-kill', when: 'enemy_killed', reducer: { kind: 'none' }, mutations: [{ kind: 'set', pool: 'pummel-active', value: 1 }], outcomes: [weaponDamage], }, { id: 'refresh-on-assist', when: 'assist', reducer: { kind: 'none' }, mutations: [{ kind: 'set', pool: 'pummel-active', value: 1 }], outcomes: [{ ...weaponDamage }], }, ], }; expect(validateMechanicProgram(program)).toMatchObject({ ok: true }); const result = evaluateMechanicProgram( program, [ { kind: 'enemy_killed', at: 1 }, { kind: 'assist', at: 10 }, ], custom(20), ); expect(result.status).toBe('OK'); expect( result.outcomes.map(({ outcomeId, at, appliedValue }) => ({ outcomeId, at, appliedValue, })), ).toEqual([ { outcomeId: 'pummel-weapon-damage', at: 1, appliedValue: 10 }, { outcomeId: 'pummel-weapon-damage', at: 10, appliedValue: 10 }, ]); expect(result.finalState).toContainEqual( expect.objectContaining({ pool: 'pummel-active', value: 1, expiresAt: 22, }), ); }); it('rejects a shared outcome ID when its mechanical definitions conflict', () => { const outcome = { id: 'shared-damage', label: 'Weapon Damage', phase: 'on_event' as const, kind: 'attribute_modifier' as const, scope: 'equipped_weapon' as const, basis: 'flat' as const, value: 10, attributeId: 'weapon-damage', }; const program: MechanicProgram = { id: 'conflicting-shared-outcome', name: 'Conflicting Shared Outcome', clauses: [ { id: 'kill', when: 'enemy_killed', reducer: { kind: 'none' }, mutations: [{ kind: 'none' }], outcomes: [outcome], }, { id: 'assist', when: 'assist', reducer: { kind: 'none' }, mutations: [{ kind: 'none' }], outcomes: [{ ...outcome, value: 14 }], }, ], }; expect(validateMechanicProgram(program)).toEqual({ ok: false, errors: ['Outcome "shared-damage" has conflicting definitions.'], }); }); it('applies distance bonuses in full five-meter steps and caps the result', () => { const distanceBonus = ( base: number, increment: number, maximum: number, ): NumericExpression => ({ op: 'min', left: maximum, right: { op: 'add', left: base, right: { op: 'multiply', left: increment, right: { op: 'floor', value: { op: 'divide', left: { op: 'max', left: { ref: { source: 'event', path: 'values.distanceMeters' }, }, right: 0, }, right: 5, }, }, }, }, }); const program: MechanicProgram = { id: 'long-distance-marking', name: 'Long-Distance Marking', clauses: [ { id: 'headshot', when: 'weapon_hit', reducer: { kind: 'none' }, mutations: [], outcomes: [ { id: 'skill-damage', label: 'Skill Damage', phase: 'after_event', kind: 'attribute_modifier', scope: 'self', attributeId: 'skill-damage', basis: 'flat', value: distanceBonus(7.5, 1.5, 18), }, ], }, ], }; expect(validateMechanicProgram(program)).toMatchObject({ ok: true }); const result = evaluateMechanicProgram( program, [-1, 0, 4.99, 5, 9.99, 10, 34.99, 35, 100].map((distanceMeters, at) => ({ kind: 'weapon_hit' as const, at, values: { distanceMeters }, })), custom(10), ); expect(result.status).toBe('OK'); expect(result.outcomes.map((outcome) => outcome.resolvedValue)).toEqual([ 7.5, 7.5, 7.5, 9, 9, 10.5, 16.5, 18, 18, ]); expect(result.script).toContain( 'MIN(18, (7.5 + (1.5 * FLOOR((MAX(EVENT.values.distanceMeters, 0) / 5)))))', ); }); it('supports an immediate equipped-weapon magazine refill', () => { const program: MechanicProgram = { id: 'equipped-magazine', name: 'Equipped Magazine', clauses: [ { id: 'refill-equipped-weapon', when: 'enemy_killed', reducer: { kind: 'none' }, mutations: [{ kind: 'none' }], outcomes: [ { id: 'equipped-weapon-reload', label: 'Refill equipped weapon', phase: 'after_event', kind: 'weapon_reload', scope: 'equipped_weapon', basis: 'flat', value: 0, }, ], }, ], }; const result = evaluateMechanicProgram( program, [{ kind: 'enemy_killed', at: 1 }], custom(10), ); expect(result.status).toBe('OK'); expect(result.outcomes).toEqual([ expect.objectContaining({ kind: 'weapon_reload', scope: 'equipped_weapon', at: 1, value: 0, }), ]); expect(result.script).toContain('RELOAD EQUIPPED WEAPON AFTER 0s'); }); it('schedules an inactive-weapon reload and refreshes a pending delay', () => { const program: MechanicProgram = { id: 'backup-magazine', name: 'Backup Magazine', clauses: [ { id: 'reload-inactive-weapon', when: 'weapon_swapped', reducer: { kind: 'none' }, mutations: [{ kind: 'none' }], outcomes: [ { id: 'inactive-weapon-reload', label: 'Reload inactive weapon', phase: 'after_event', kind: 'weapon_reload', scope: 'inactive_weapon', basis: 'flat', value: 4, }, ], }, ], }; const refreshed = evaluateMechanicProgram( program, [ { kind: 'weapon_swapped', at: 1 }, { kind: 'weapon_swapped', at: 3 }, ], custom(10), ); expect(refreshed.status).toBe('OK'); expect(refreshed.emissions.map(({ at }) => at)).toEqual([1, 3]); expect(refreshed.outcomes).toEqual([ expect.objectContaining({ kind: 'weapon_reload', scope: 'inactive_weapon', at: 7, value: 4, }), ]); expect(refreshed.script).toContain('RELOAD INACTIVE WEAPON AFTER 4s'); const beyondEncounter = evaluateMechanicProgram( program, [{ kind: 'weapon_swapped', at: 8 }], custom(10), ); expect(beyondEncounter.outcomes).toEqual([]); }); 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('caps Signature Ability Charge at 1000 and scales grants by charge efficiency', () => { const efficiencyAttributeId = 'signature-charge-efficiency'; const program: MechanicProgram = { id: 'signature-charge', name: 'Signature Charge', clauses: [ { id: 'grant-charge', when: 'weapon_hit', reducer: { kind: 'none' }, mutations: [], outcomes: [ { id: 'charge', label: 'Signature Ability Charge', phase: 'after_event', kind: 'restoration', scope: 'self', resource: 'signature_ability_charge', basis: 'flat', value: { op: 'multiply', left: 70, right: { op: 'add', left: 1, right: { op: 'divide', left: { ref: { source: 'attribute', attributeId: efficiencyAttributeId, }, }, right: 100, }, }, }, }, ], }, ], }; const result = evaluateMechanicProgram( program, [ { kind: 'weapon_hit', at: 1 }, { kind: 'weapon_hit', at: 2 }, ], custom(), { maxHealth: 100, currentHealth: 100, currentSignatureAbilityCharge: 900, attributes: { [efficiencyAttributeId]: 50 }, }, ); expect(result.status).toBe('OK'); expect(result.outcomes.map((outcome) => outcome.appliedValue)).toEqual([ 100, 0, ]); expect(result.finalCombat?.currentSignatureAbilityCharge).toBe(1000); expect(result.finalCombat?.maxSignatureAbilityCharge).toBeUndefined(); }); it('evaluates mutually exclusive current Health percentage bands at strict boundaries', () => { const band = ( id: string, minimum: number | undefined, maximum: number, value: number, ): MechanicProgram['clauses'][number] => ({ id, when: 'tick', if: minimum === undefined ? { kind: 'compare', left: { source: 'combat', path: 'currentHealthPercent', timing: 'current', }, operator: 'lt', right: maximum, } : { kind: 'all', predicates: [ { kind: 'compare', left: { source: 'combat', path: 'currentHealthPercent', timing: 'current', }, operator: 'gte', right: minimum, }, { kind: 'compare', left: { source: 'combat', path: 'currentHealthPercent', timing: 'current', }, operator: 'lt', right: maximum, }, ], }, reducer: { kind: 'none' }, mutations: [], outcomes: [ { id: `${id}-damage-reduction`, label: 'Damage Reduction', phase: 'while_active', kind: 'attribute_modifier', scope: 'self', basis: 'flat', attributeId: 'damage-reduction', value, }, ], }); const spirited: MechanicProgram = { id: 'spirited', name: 'Spirited', clauses: [ band('critical', undefined, 25, 15), band('damaged', 25, 50, 12), band('wounded', 50, 75, 9), ], }; const evaluateAt = ( healthPercent: number, mode: 'custom' | 'maximum' = 'custom', ) => evaluateMechanicProgram( spirited, [{ kind: 'tick', at: 0 }], mode === 'maximum' ? { mode: 'maximum', encounterSeconds: 1 } : custom(1), { maxHealth: 1_000, currentHealth: healthPercent * 10 }, ); const valuesAt = ( healthPercent: number, mode: 'custom' | 'maximum' = 'custom', ) => evaluateAt(healthPercent, mode).outcomes.map( (outcome) => outcome.appliedValue, ); expect(valuesAt(0)).toEqual([15]); expect(valuesAt(24.99)).toEqual([15]); expect(valuesAt(25)).toEqual([12]); expect(valuesAt(49.99)).toEqual([12]); expect(valuesAt(50)).toEqual([9]); expect(valuesAt(74.99)).toEqual([9]); expect(valuesAt(75)).toEqual([]); expect(valuesAt(100)).toEqual([]); expect(valuesAt(24, 'maximum')).toEqual([15]); }); it('re-evaluates current Health percentage after deterministic combat-state updates', () => { const program: MechanicProgram = { id: 'live-health', name: 'Live Health', clauses: [ { id: 'health-cost', when: 'tick', reducer: { kind: 'none' }, mutations: [], outcomes: [ { id: 'cost', label: 'Health cost', phase: 'on_event', kind: 'resource_cost', scope: 'self', resource: 'health', basis: 'flat', value: 30, }, ], }, { id: 'below-seventy-five', when: 'tick', if: { kind: 'compare', left: { source: 'combat', path: 'currentHealthPercent', timing: 'current', }, operator: 'lt', right: 75, }, reducer: { kind: 'none' }, mutations: [], outcomes: [ { id: 'reduction', label: 'Damage Reduction', phase: 'while_active', kind: 'attribute_modifier', scope: 'self', attributeId: 'damage-reduction', basis: 'flat', value: 9, }, ], }, ], }; const result = evaluateMechanicProgram( program, [{ kind: 'tick', at: 0 }], custom(1), { maxHealth: 100, currentHealth: 100 }, ); expect(result.finalCombat?.currentHealth).toBe(70); expect(result.outcomes.map((outcome) => outcome.outcomeId)).toEqual([ 'cost', 'reduction', ]); }); it('evaluates pre-event timing for the derived current Health percentage', () => { const program: MechanicProgram = { id: 'pre-event-health-percent', name: 'Pre-event Health Percent', clauses: [ { id: 'full-health-before-event', when: 'tick', if: { kind: 'compare', left: { source: 'combat', path: 'currentHealthPercent', timing: 'pre_event', }, operator: 'gte', right: 100, }, reducer: { kind: 'none' }, mutations: [], outcomes: [ { id: 'matched', label: 'Matched', phase: 'on_event', kind: 'marker', scope: 'self', basis: 'flat', value: 1, }, ], }, ], }; expect(validateMechanicProgram(program)).toMatchObject({ ok: true }); expect( evaluateMechanicProgram(program, [{ kind: 'tick', at: 0 }], custom(1), { maxHealth: 100, currentHealth: 100, }).outcomes.map((outcome) => outcome.outcomeId), ).toEqual(['matched']); }); 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'), ); }); it('preserves radius targeting on ally modifiers and enemy markers', () => { const program: MechanicProgram = { id: 'radius-targeting', name: 'Radius Targeting', clauses: [ { id: 'kill', when: 'enemy_killed', reducer: { kind: 'none' }, mutations: [{ kind: 'none' }], outcomes: [ { id: 'healing', label: 'Incoming Healing', phase: 'on_event', kind: 'attribute_modifier', scope: 'allies', attributeId: 'incoming-healing', basis: 'flat', value: 7, targeting: { kind: 'within_radius', population: 'allies', radiusMeters: 40, }, }, { id: 'taunt', label: 'Taunt', phase: 'on_event', kind: 'marker', scope: 'target', basis: 'flat', value: 1, targeting: { kind: 'within_radius', population: 'enemies', radiusMeters: 20, }, }, ], }, ], }; const result = evaluateMechanicProgram( program, [{ kind: 'enemy_killed', at: 1 }], custom(5), ); expect(result.status).toBe('OK'); expect(result.outcomes.map((outcome) => outcome.targeting)).toEqual([ { kind: 'within_radius', population: 'allies', radiusMeters: 40 }, { kind: 'within_radius', population: 'enemies', radiusMeters: 20 }, ]); expect(result.script).toContain('TO ALLIES WITHIN 40m'); expect(result.script).toContain('TO ENEMIES WITHIN 20m'); }); it('applies aimed critical-hit area damage once to each valid enemy at or inside the radius', () => { const program: MechanicProgram = { id: 'aimed-critical-explosion', name: 'Aimed Critical Explosion', clauses: [ { id: 'aimed-critical-hit', when: 'weapon_hit', if: { kind: 'all', predicates: [ { kind: 'compare', left: { source: 'event', path: 'values.critical' }, operator: 'eq', right: true, }, { kind: 'compare', left: { source: 'event', path: 'values.aiming' }, operator: 'eq', right: true, }, ], }, reducer: { kind: 'none' }, mutations: [], outcomes: [ { id: 'explosion', label: 'Explosion', phase: 'after_event', kind: 'direct_damage', scope: 'target', basis: 'flat', value: { op: 'multiply', left: 20, right: { ref: { source: 'attribute', attributeId: 'firepower' }, }, }, targeting: { kind: 'event_target_area', center: 'event_target', validTargets: 'enemies', radiusMeters: 5, }, probabilityPercent: 100, }, ], }, ], }; const result = evaluateMechanicProgram( program, [ { kind: 'weapon_hit', at: 1, targetId: 'enemy-b', values: { critical: true, aiming: true }, areaTargetSample: { center: 'event_target', validTargets: [ { entityId: 'enemy-c', population: 'enemy', distanceMeters: 5.001, }, { entityId: 'enemy-b', population: 'enemy', distanceMeters: 0 }, { entityId: 'ally-a', population: 'ally', distanceMeters: 2 }, { entityId: 'enemy-a', population: 'enemy', distanceMeters: 5 }, ], }, }, ], custom(5), { maxHealth: 1000, currentHealth: 1000, attributes: { firepower: 100 } }, ); expect(result.status).toBe('OK'); expect(result.outcomes.map((outcome) => outcome.targetId)).toEqual([ 'enemy-a', 'enemy-b', ]); expect(result.outcomes.map((outcome) => outcome.appliedValue)).toEqual([ 2000, 2000, ]); expect( new Set(result.outcomes.map((outcome) => outcome.probabilityRoll)).size, ).toBe(1); expect(result.outcomes[0].probabilityRoll).not.toBeNull(); expect(result.script).toContain( 'EXPLOSION CENTERED ON EVENT TARGET; APPLY TO VALID ENEMIES WITHIN 5m', ); const maximum = evaluateMechanicProgram( program, [ { kind: 'weapon_hit', at: 1, targetId: 'enemy-b', values: { critical: true, aiming: true }, areaTargetSample: { center: 'event_target', validTargets: [ { entityId: 'enemy-b', population: 'enemy', distanceMeters: 0 }, { entityId: 'enemy-a', population: 'enemy', distanceMeters: 5 }, ], }, }, ], { mode: 'maximum', encounterSeconds: 5 }, { maxHealth: 1000, currentHealth: 1000, attributes: { firepower: 100 } }, ); expect(maximum.outcomes.map((outcome) => outcome.targetId)).toEqual([ 'enemy-a', 'enemy-b', ]); expect( maximum.outcomes.every((outcome) => outcome.probabilityRoll === 0), ).toBe(true); const skillProgram: MechanicProgram = { ...program, id: 'skill-hit-explosion', clauses: program.clauses.map((clause) => ({ ...clause, when: 'skill_hit', if: undefined, })), }; expect(validateMechanicProgram(skillProgram)).toMatchObject({ ok: true }); expect( evaluateMechanicProgram( skillProgram, [ { kind: 'skill_hit', at: 1, targetId: 'enemy-b', areaTargetSample: { center: 'event_target', validTargets: [ { entityId: 'enemy-b', population: 'enemy', distanceMeters: 0 }, { entityId: 'enemy-a', population: 'enemy', distanceMeters: 5 }, ], }, }, ], custom(5), { maxHealth: 1000, currentHealth: 1000, attributes: { firepower: 100 }, }, ).outcomes.map((outcome) => outcome.targetId), ).toEqual(['enemy-a', 'enemy-b']); }); it('requires authoritative area evidence only when an area outcome activates', () => { const program: MechanicProgram = { id: 'area-evidence', name: 'Area Evidence', clauses: [ { id: 'critical-hit', when: 'weapon_hit', if: { kind: 'compare', left: { source: 'event', path: 'values.critical' }, operator: 'eq', right: true, }, reducer: { kind: 'none' }, mutations: [], outcomes: [ { id: 'explosion', label: 'Explosion', phase: 'after_event', kind: 'direct_damage', scope: 'target', basis: 'flat', value: 1, targeting: { kind: 'event_target_area', center: 'event_target', validTargets: 'enemies', radiusMeters: 5, }, }, ], }, ], }; expect( evaluateMechanicProgram( program, [ { kind: 'weapon_hit', at: 1, targetId: 'enemy', values: { critical: false }, }, ], custom(5), ), ).toMatchObject({ status: 'OK', outcomes: [] }); expect( evaluateMechanicProgram( program, [ { kind: 'weapon_hit', at: 1, targetId: 'enemy', values: { critical: true }, }, ], custom(5), ), ).toMatchObject({ status: 'INVALID', errors: [ expect.stringContaining('authoritative event-target area sample'), ], }); }); it('rejects malformed and duplicate area-target samples', () => { const result = evaluateMechanicProgram( { id: 'sample-validation', name: 'Sample Validation', clauses: [] }, [ { kind: 'weapon_hit', at: 1, targetId: 'enemy', areaTargetSample: { center: 'event_target', validTargets: [ { entityId: 'enemy', population: 'enemy', distanceMeters: 0 }, { entityId: 'enemy', population: 'enemy', distanceMeters: 1 }, ], }, }, ], custom(5), ); expect(result).toMatchObject({ status: 'INVALID', errors: [expect.stringContaining('duplicates entity "enemy"')], }); const oversizedTargets = Array.from({ length: 257 }, (_, index) => ({ entityId: index === 0 ? 'enemy' : `enemy-${index}`, population: 'enemy' as const, distanceMeters: index === 0 ? 0 : 1, })); expect( evaluateMechanicProgram( { id: 'sample-bound', name: 'Sample Bound', clauses: [] }, [ { kind: 'weapon_hit', at: 1, targetId: 'enemy', areaTargetSample: { center: 'event_target', validTargets: oversizedTargets, }, }, ], custom(5), ), ).toMatchObject({ status: 'INVALID', errors: [expect.stringContaining('cannot contain more than 256')], }); }); });