import { describe, expect, it } from 'vitest'; import { TRIGGER_ACTION_KEYS, validateTriggerConfiguration, type TriggerActionKey, type TriggerConfiguration } from './conditional.js'; import { createMechanicRecipeForTrigger, MECHANIC_RECIPE_REGISTRY, type MechanicRecipeSeed } from './mechanic-recipes.js'; import { validateMechanicProgram } from './mechanic.js'; const paramsByAction: Record = { same_target_hits_within_window: { hitsRequired: 3, windowSeconds: 2 }, distinct_enemies_within_window: { enemiesRequired: 3, windowSeconds: 4 }, consecutive_same_target_hits: { hitsRequired: 4 }, continuous_firing_duration: { seconds: 2.5 }, while_firing: {}, health_threshold_bands: { bands: [ { thresholdPercent: 25, outcomeKey: 'critical' }, { thresholdPercent: 50, outcomeKey: 'damaged' } ] }, headshot: {}, body_shot: {}, headshot_kill: {}, weapon_kill: {}, target_hit_within_distance: { meters: 15 }, damage_taken: {}, weapon_swap: {}, reload: {}, within_distance: { meters: 10 }, extra_health_active: {} }; function triggerFor(action: TriggerActionKey): TriggerConfiguration { const trigger = validateTriggerConfiguration(action, paramsByAction[action]); if (!trigger.ok) throw new Error(trigger.errors.join(' ')); return trigger.value; } function seedFor(action: TriggerActionKey): MechanicRecipeSeed { const trigger = triggerFor(action); const result = createMechanicRecipeForTrigger(trigger); if (result.status !== 'supported') throw new Error(result.reason); return result.seed; } describe('mechanic recipe registry', () => { it('covers every trigger action with a supported seed or an explicit reason', () => { expect(Object.keys(MECHANIC_RECIPE_REGISTRY).sort()).toEqual([...TRIGGER_ACTION_KEYS].sort()); for (const action of TRIGGER_ACTION_KEYS) { const result = createMechanicRecipeForTrigger(triggerFor(action)); expect(['supported', 'unsupported']).toContain(result.status); if (result.status === 'unsupported') expect(result.reason.trim()).not.toBe(''); else expect(result.seed.clauses.every((clause) => clause.recipeKey === action)).toBe(true); } }); it('produces a valid editable program for every supported recipe without hidden placeholders', () => { for (const action of TRIGGER_ACTION_KEYS) { const seed = seedFor(action); const validation = validateMechanicProgram({ id: `recipe-${action}`, name: `Recipe ${action}`, ...seed }); expect(validation, action).toMatchObject({ ok: true }); } }); it('maps hit-family actions to same-target, distinct, and consecutive reducers', () => { expect(seedFor('same_target_hits_within_window').clauses[0]).toMatchObject({ when: 'weapon_hit', if: { kind: 'exists', value: { source: 'event', path: 'targetId' } }, reducer: { kind: 'sliding_count', threshold: 3, windowSeconds: 2, reset: 'zero', groupBy: { source: 'event', path: 'targetId' } } }); expect(seedFor('distinct_enemies_within_window').clauses[0]).toMatchObject({ when: 'weapon_hit', reducer: { kind: 'distinct_count', threshold: 3, windowSeconds: 4, distinctBy: { source: 'event', path: 'targetId' } } }); expect(seedFor('consecutive_same_target_hits').clauses[0]).toMatchObject({ when: 'weapon_hit', reducer: { kind: 'consecutive_count', threshold: 4, groupBy: { source: 'event', path: 'targetId' } } }); }); it('maps head and body shots to authoritative weapon-hit predicates', () => { for (const [action, headshot] of [['headshot', true], ['body_shot', false]] as const) { expect(seedFor(action).clauses[0]).toMatchObject({ when: 'weapon_hit', if: { kind: 'all', predicates: [ { kind: 'exists', value: { source: 'event', path: 'targetId' } }, { kind: 'exists', value: { source: 'event', path: 'values.headshot' } }, { kind: 'compare', left: { source: 'event', path: 'values.headshot' }, operator: 'eq', right: headshot } ] } }); } }); it('models firing triggers as explicit lifecycle branches', () => { const continuous = seedFor('continuous_firing_duration'); expect(continuous.pools).toEqual([expect.objectContaining({ key: 'firing-active', max: 1 })]); expect(continuous.clauses.map((clause) => clause.when)).toEqual([ 'firing_started', 'firing_stopped', 'reload_started', 'tick' ]); expect(continuous.clauses.at(-1)).toMatchObject({ reducer: { kind: 'duration', seconds: 2.5 }, if: { kind: 'compare', left: { source: 'pool', pool: 'firing-active' }, operator: 'gt', right: 0 } }); expect(seedFor('while_firing').clauses.map((clause) => clause.when)).toEqual([ 'firing_started', 'firing_stopped', 'reload_started' ]); }); it('models health bands and persistent distance and Extra Health state as clearable branches', () => { const health = seedFor('health_threshold_bands'); expect(health.pools?.map((pool) => pool.key)).toEqual(['health-band-1', 'health-band-2']); expect(health.clauses.map((clause) => clause.id)).toEqual([ 'select-health-band-1', 'select-health-band-2', 'clear-health-bands' ]); expect(health.clauses[1].if).toMatchObject({ kind: 'all', predicates: expect.arrayContaining([ { kind: 'compare', left: { source: 'combat', path: 'currentHealthPercent', timing: 'current' }, operator: 'lt', right: 50 }, { kind: 'compare', left: { source: 'combat', path: 'currentHealthPercent', timing: 'current' }, operator: 'gte', right: 25 } ]) }); expect(health.clauses.every((clause) => clause.when === 'tick')).toBe(true); expect(health.clauses[2].if).toEqual({ kind: 'compare', left: { source: 'combat', path: 'currentHealthPercent', timing: 'current' }, operator: 'gte', right: 50 }); expect(seedFor('target_hit_within_distance').clauses[0]).toMatchObject({ when: 'weapon_hit', if: { kind: 'all', predicates: expect.arrayContaining([ { kind: 'exists', value: { source: 'event', path: 'targetId' } }, { kind: 'compare', left: { source: 'event', path: 'values.distanceMeters' }, operator: 'lte', right: 15 } ]) } }); const withinDistance = seedFor('within_distance'); expect(withinDistance.clauses.map((clause) => clause.id)).toEqual([ 'enter-target-within-distance', 'exit-target-within-distance' ]); expect(withinDistance.clauses[0].if).toEqual({ kind: 'compare', left: { source: 'event', path: 'values.distanceMeters' }, operator: 'lte', right: 10 }); expect(withinDistance.clauses[1].if).toMatchObject({ kind: 'not' }); const extraHealth = seedFor('extra_health_active'); expect(extraHealth.clauses.map((clause) => clause.id)).toEqual([ 'enter-extra-health-active', 'exit-extra-health-active' ]); expect(extraHealth.clauses[0].if).toEqual({ kind: 'compare', left: { source: 'event', path: 'values.extraHealthActive' }, operator: 'eq', right: true }); }); });