import type { World, Room, Item } from './types' import { parseRoom, parseItem, parseEnding, parseEncounterNarration, } from './loader' // Importing loader (above) triggers auto-registration of encounter narrations. // ESM evaluates dependencies first, so by the time encounters.ts is evaluated below, // narration() can resolve all keys. import { encounters } from './encounters' const roomFiles = import.meta.glob('./rooms/*.md', { eager: true, query: '?raw', import: 'default', }) const itemFiles = import.meta.glob('./items/*.md', { eager: true, query: '?raw', import: 'default', }) const endingFiles = import.meta.glob('./endings/*.md', { eager: true, query: '?raw', import: 'default', }) const encounterFiles = import.meta.glob('./encounters/*.md', { eager: true, query: '?raw', import: 'default', }) // Re-parse encounter docs here so we can validate startsIn / initialPhase against encounters.ts. // (The loader already auto-registered narrations from these same files at module init.) const encounterDocs = Object.entries(encounterFiles).map(([path, raw]) => parseEncounterNarration(raw, path), ) // Build rooms map. const rooms: Record = {} for (const [path, raw] of Object.entries(roomFiles)) { const room = parseRoom(raw, path) if (rooms[room.id]) throw new Error(`${path}: duplicate room id "${room.id}"`) rooms[room.id] = room } // Build items map. const items: Record = {} for (const [path, raw] of Object.entries(itemFiles)) { const item = parseItem(raw, path) if (items[item.id]) throw new Error(`${path}: duplicate item id "${item.id}"`) items[item.id] = item } // Build endings. const endings: World['endings'] = { true: { whenFlags: {}, narration: '' }, wrong: { whenFlags: {}, narration: '' }, bad: { whenFlags: {}, narration: '' }, } const seenEndings = new Set() for (const [path, raw] of Object.entries(endingFiles)) { const { id, ending } = parseEnding(raw, path) endings[id] = ending seenEndings.add(id) } const requiredEndings = ['true', 'wrong', 'bad'] as const for (const id of requiredEndings) { if (!seenEndings.has(id)) { throw new Error(`endings/${id}.md is missing — every ending id must have a markdown file.`) } } // Cross-reference validation. // Build set of all known flag names from encounter setFlags and ending whenFlags. const knownFlags = new Set() for (const enc of Object.values(encounters)) { if (enc.onResolved?.setFlags) { for (const flagName of Object.keys(enc.onResolved.setFlags)) knownFlags.add(flagName) } } for (const ending of Object.values(endings)) { for (const flagName of Object.keys(ending.whenFlags)) knownFlags.add(flagName) } for (const room of Object.values(rooms)) { for (const [dir, dest] of Object.entries(room.exits)) { if (!rooms[dest!]) { throw new Error(`rooms/${room.id}.md: exit${dir.toUpperCase()} references "${dest}" but no such room exists.`) } } for (const itemId of room.items) { if (!items[itemId]) { throw new Error(`rooms/${room.id}.md: items[] references unknown item "${itemId}"`) } } if (room.encounter && !encounters[room.encounter]) { throw new Error(`rooms/${room.id}.md: encounter "${room.encounter}" is not defined`) } if (room.lockedExits) { for (const [dir, lock] of Object.entries(room.lockedExits)) { const isItem = items[lock.requires] !== undefined const isFlag = knownFlags.has(lock.requires) if (!isItem && !isFlag) { const knownItemList = Object.keys(items).join(', ') || '(none)' const knownFlagList = [...knownFlags].join(', ') || '(none)' throw new Error( `rooms/${room.id}.md: exit${dir.toUpperCase()}Requires "${lock.requires}" matches no known item or flag. ` + `Known items: ${knownItemList}. Known flags: ${knownFlagList}.`, ) } } } } // Validate encounter narration registry: every encounter in TS has a markdown doc. for (const enc of Object.values(encounters)) { const doc = encounterDocs.find(d => d.id === enc.id) if (!doc) { throw new Error(`encounters/${enc.id}.md: missing narration markdown for encounter "${enc.id}"`) } if (doc.startsIn !== enc.startsIn) { throw new Error(`encounters/${enc.id}.md: startsIn "${doc.startsIn}" does not match encounters.ts "${enc.startsIn}"`) } if (doc.initialPhase !== enc.initialPhase) { throw new Error(`encounters/${enc.id}.md: initialPhase "${doc.initialPhase}" does not match encounters.ts "${enc.initialPhase}"`) } } export const world: World = { startingRoom: 'outside-gate', startingInventory: ['letter', 'matches', 'broken-cigarette'], rooms, items, encounters, endings, }