feat(mystery): narration() helper and encounter narration registry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parseRoom, parseItem, parseEnding, parseEncounterNarration } from './loader'
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { parseRoom, parseItem, parseEnding, parseEncounterNarration, narration, registerEncounterNarrations, _resetEncounterNarrationRegistry } from './loader'
|
||||
|
||||
const FOYER_MD = `---
|
||||
id: foyer
|
||||
@@ -286,3 +286,27 @@ initialPhase: p
|
||||
expect(() => parseEncounterNarration(md, 'encounters/x.md')).toThrow(/no narration sections/i)
|
||||
})
|
||||
})
|
||||
|
||||
describe('narration registry', () => {
|
||||
beforeEach(() => {
|
||||
_resetEncounterNarrationRegistry()
|
||||
})
|
||||
|
||||
it('returns prose for a registered encounter and key', () => {
|
||||
registerEncounterNarrations([
|
||||
{ id: 'rat', startsIn: 'cellar-stair', initialPhase: 'lurking', narrations: { lurking: 'watches' } },
|
||||
])
|
||||
expect(narration('rat', 'lurking')).toBe('watches')
|
||||
})
|
||||
|
||||
it('throws with available keys when key is missing', () => {
|
||||
registerEncounterNarrations([
|
||||
{ id: 'rat', startsIn: 'cellar-stair', initialPhase: 'lurking', narrations: { lurking: 'a', resolved: 'b' } },
|
||||
])
|
||||
expect(() => narration('rat', 'sleping')).toThrow(/no matching section.*Available: lurking, resolved/i)
|
||||
})
|
||||
|
||||
it('throws when encounter is unknown', () => {
|
||||
expect(() => narration('ghost', 'whatever')).toThrow(/unknown encounter id "ghost"/i)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -154,3 +154,48 @@ export function parseEncounterNarration(raw: string, sourcePath: string): Parsed
|
||||
narrations,
|
||||
}
|
||||
}
|
||||
|
||||
const encounterNarrationRegistry = new Map<string, Map<string, string>>()
|
||||
|
||||
export function registerEncounterNarrations(docs: ParsedEncounterNarration[]): void {
|
||||
for (const doc of docs) {
|
||||
encounterNarrationRegistry.set(doc.id, new Map(Object.entries(doc.narrations)))
|
||||
}
|
||||
}
|
||||
|
||||
export function _resetEncounterNarrationRegistry(autoReregister: boolean = false): void {
|
||||
encounterNarrationRegistry.clear()
|
||||
if (autoReregister) autoRegisterEncounters()
|
||||
}
|
||||
|
||||
export function narration(encounterId: string, key: string): string {
|
||||
const map = encounterNarrationRegistry.get(encounterId)
|
||||
if (!map) {
|
||||
throw new Error(`narration(): unknown encounter id "${encounterId}"`)
|
||||
}
|
||||
const value = map.get(key)
|
||||
if (value === undefined) {
|
||||
const available = [...map.keys()].join(', ')
|
||||
throw new Error(
|
||||
`narration(): no matching section "## ${key}" for encounter "${encounterId}". Available: ${available}`,
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Auto-register encounter narrations from co-located markdown files at module init.
|
||||
// This populates the registry BEFORE encounters.ts is evaluated (ESM evaluates dependencies first),
|
||||
// so encounters.ts can call narration() at top level without explicit ordering.
|
||||
// While src/mystery/world/encounters/ does not yet exist (Task 8 creates it), this is a no-op.
|
||||
const _encounterFiles = import.meta.glob<string>('./encounters/*.md', {
|
||||
eager: true, query: '?raw', import: 'default',
|
||||
})
|
||||
|
||||
function autoRegisterEncounters(): void {
|
||||
for (const [path, raw] of Object.entries(_encounterFiles)) {
|
||||
const doc = parseEncounterNarration(raw, path)
|
||||
encounterNarrationRegistry.set(doc.id, new Map(Object.entries(doc.narrations)))
|
||||
}
|
||||
}
|
||||
|
||||
autoRegisterEncounters()
|
||||
|
||||
Reference in New Issue
Block a user