import { describe, it, expect } from 'vitest' import { dispatch, initialStateFor } from './dispatcher' import type { World } from '../world/types' const world: World = { startingRoom: 'foyer', startingInventory: ['mirror'], rooms: { foyer: { id: 'foyer', title: '[ Foyer ]', descriptions: { firstVisit: 'Foyer.', revisit: 'Foyer.', examined: 'Foyer.' }, exits: { n: 'stair', e: 'chapel' }, items: [], safe: true, }, stair: { id: 'stair', title: '[ Cellar Stair ]', descriptions: { firstVisit: 'Stair.', revisit: 'Stair.', examined: 'Stair.' }, exits: { s: 'foyer', d: 'cellar' }, items: [], encounter: 'revenant', }, cellar: { id: 'cellar', title: '[ Cellar ]', descriptions: { firstVisit: 'Cellar.', revisit: 'Cellar.', examined: 'Cellar.' }, exits: { u: 'stair' }, items: [], }, chapel: { id: 'chapel', title: '[ Chapel ]', descriptions: { firstVisit: 'Chapel.', revisit: 'Chapel.', examined: 'Chapel.' }, exits: { s: 'foyer' }, items: ['vial'], encounter: 'basilisk', }, }, items: { mirror: { id: 'mirror', names: ['mirror', 'tarnished mirror'], short: 'a tarnished mirror', long: 'A small mirror, tarnished black.', initialState: {}, takeable: true }, sword: { id: 'sword', names: ['sword', 'cane sword'], short: 'a cane sword', long: 'A slim cane sword.', initialState: {}, takeable: true }, vial: { id: 'vial', names: ['vial'], short: 'a vial', long: 'A small vial.', initialState: {}, takeable: true }, }, encounters: { revenant: { id: 'revenant', startsIn: 'stair', initialPhase: 'wary', phases: { wary: { description: 'A revenant rises from the wet stone.', transitions: [ { verb: 'attack', target: 'revenant', narration: 'Your blade passes through.', to: 'shaken', resolveCost: 1 }, { verb: 'examine', target: 'revenant', narration: 'There is a tarnish around its eyes.', to: 'wary' }, { verb: 'hold', target: 'revenant', requires: { item: 'mirror' }, narration: 'It looks into the silver.', to: 'resolved' }, ], }, shaken: { description: 'The revenant comes closer.', transitions: [ { verb: 'hold', target: 'revenant', requires: { item: 'mirror' }, narration: 'It looks. It remembers.', to: 'resolved' }, ], }, }, onResolved: { setFlags: { revenantLaid: true } }, onFailed: { narration: 'You stagger back.', retreatTo: 'foyer' }, defaultWrongVerbNarration: 'The revenant does not seem to notice.', }, basilisk: { id: 'basilisk', aliases: ['basilisk'], startsIn: 'chapel', initialPhase: 'sleeping', phases: { sleeping: { description: 'An eye opens beneath the altar.', transitions: [ { verb: 'pour', target: 'vial', requires: { item: 'vial' }, narration: 'The eye closes.', to: 'resolved' }, ], }, }, onResolved: { setFlags: { basiliskSpared: true } }, defaultWrongVerbNarration: 'The eye watches.', }, }, endings: { true: { whenFlags: { _never: true }, narration: '' }, wrong: { whenFlags: { _never: true }, narration: '' }, bad: { whenFlags: { _never: true }, narration: '' }, }, } describe('encounters — phase advancement', () => { it('triggers an encounter on entering its room', () => { let s = initialStateFor(world) const r = dispatch(s, { kind: 'go', direction: 'n' }, world) expect(r.state.encounterState['revenant']).toBe('wary') expect(r.appended.some((l) => l.text.includes('revenant rises'))).toBe(true) }) it('right verb resolves the encounter', () => { let s = initialStateFor(world) s = dispatch(s, { kind: 'go', direction: 'n' }, world).state const r = dispatch(s, { kind: 'verb-target', verb: 'hold', target: { canonical: 'revenant', raw: 'revenant' } }, world) expect(r.state.encounterState['revenant']).toBeUndefined() expect(r.state.flags['revenantLaid']).toBe(true) expect(r.appended.some((l) => l.text.includes('looks into the silver'))).toBe(true) }) it('wrong verb costs resolve and surfaces a clue', () => { let s = initialStateFor(world) s = dispatch(s, { kind: 'go', direction: 'n' }, world).state const prompt = dispatch(s, { kind: 'verb-target', verb: 'attack', target: { canonical: 'revenant', raw: 'revenant' } }, world) expect(prompt.state.pendingConfirmation).toBeDefined() expect(prompt.appended.at(-1)?.text).toContain('Are you sure') const r = dispatch(prompt.state, { kind: 'confirmation', confirmed: true }, world) expect(r.state.resolveLevel).toBe('shaken') expect(r.state.encounterState['revenant']).toBe('shaken') }) it('cancels a confirmed attack when the player says no', () => { let s = initialStateFor(world) s = dispatch(s, { kind: 'go', direction: 'n' }, world).state const prompt = dispatch(s, { kind: 'verb-target', verb: 'attack', target: { canonical: 'revenant', raw: 'revenant' } }, world) const r = dispatch(prompt.state, { kind: 'confirmation', confirmed: false }, world) expect(r.state.pendingConfirmation).toBeNull() expect(r.state.resolveLevel).toBe('steady') expect(r.state.encounterState['revenant']).toBe('wary') expect(r.appended.at(-1)?.text).toBe('Cancelled.') }) it('falls back to defaultWrongVerbNarration for unrecognized verbs', () => { let s = initialStateFor(world) s = dispatch(s, { kind: 'go', direction: 'n' }, world).state const r = dispatch(s, { kind: 'verb-target', verb: 'push', target: { canonical: 'revenant', raw: 'revenant' } }, world) expect(r.appended.some((l) => l.text.includes('does not seem to notice'))).toBe(true) }) it('retreats to safe room when resolve runs out', () => { let s = initialStateFor(world) s = dispatch(s, { kind: 'go', direction: 'n' }, world).state // Force resolve to 'returning' so the next failure retreats. s = { ...s, resolveLevel: 'returning' } s = dispatch(s, { kind: 'verb-target', verb: 'attack', target: { canonical: 'revenant', raw: 'revenant' } }, world).state const r = dispatch(s, { kind: 'confirmation', confirmed: true }, world) expect(r.state.location).toBe('foyer') expect(r.appended.some((l) => l.text.includes('stagger back'))).toBe(true) }) it('safe room entry regenerates resolve', () => { let s = initialStateFor(world) s = { ...s, resolveLevel: 'shaken' } s = dispatch(s, { kind: 'go', direction: 'n' }, world).state s = dispatch(s, { kind: 'go', direction: 's' }, world).state expect(s.resolveLevel).toBe('steady') }) it('allows a required item to be the direct target in a target-preposition encounter command', () => { let s = initialStateFor(world) s = { ...s, inventory: [...s.inventory, { id: 'vial', state: {} }], roomState: { ...s.roomState, chapel: { takenItems: ['vial'] } }, } s = dispatch(s, { kind: 'go', direction: 'e' }, world).state const r = dispatch( s, { kind: 'verb-target-prep', verb: 'pour', target: { canonical: 'vial', raw: 'vial' }, preposition: 'on', indirect: { canonical: 'basilisk', raw: 'basilisk' }, }, world, ) expect(r.state.flags['basiliskSpared']).toBe(true) expect(r.appended.some((l) => l.text.includes('eye closes'))).toBe(true) }) })