docs(mystery): spec for engine prereqs (verbs, disambiguation, ending UI) #1

Merged
ejlewis merged 19 commits from feat/engine-prereqs into main 2026-05-09 15:10:20 -05:00
4 changed files with 50 additions and 6 deletions
Showing only changes of commit df50afa479 - Show all commits
+1 -1
View File
@@ -150,7 +150,7 @@ export function parseItem(raw: string, sourcePath: string): Item {
export interface ParsedEnding {
id: 'true' | 'wrong' | 'bad'
ending: { whenFlags: Record<string, string | boolean | number>; narration: string }
ending: { whenFlags: Record<string, string | boolean | number | string[]>; narration: string }
}
export function parseEnding(raw: string, _sourcePath: string): ParsedEnding {
+24
View File
@@ -79,3 +79,27 @@ describe('encounterFrontmatterSchema', () => {
expect(() => encounterFrontmatterSchema.parse(data)).not.toThrow()
})
})
describe('itemFrontmatterSchema — bible additions', () => {
it('accepts readable + lighter fields', () => {
const data = {
id: 'matches',
names: ['matches', 'matchbook'],
short: 'a matchbook',
takeable: true,
lighter: true,
lighterUses: 4,
}
expect(() => itemFrontmatterSchema.parse(data)).not.toThrow()
})
it('accepts lightable on its own', () => {
const data = { id: 'lamp', names: ['lamp'], short: 'a lamp', takeable: true, lightable: true }
expect(() => itemFrontmatterSchema.parse(data)).not.toThrow()
})
it('rejects negative lighterUses', () => {
const data = { id: 'matches', names: ['matches'], short: 'matches', takeable: true, lighter: true, lighterUses: -1 }
expect(() => itemFrontmatterSchema.parse(data)).toThrow()
})
})
+5 -1
View File
@@ -1,6 +1,6 @@
import { z } from 'zod'
const stateValueSchema = z.union([z.string(), z.boolean(), z.number()])
const stateValueSchema = z.union([z.string(), z.boolean(), z.number(), z.array(z.string())])
const stateRecordSchema = z.record(z.string(), stateValueSchema)
export const roomFrontmatterSchema = z.object({
@@ -37,6 +37,10 @@ export const itemFrontmatterSchema = z.object({
short: z.string().min(1),
takeable: z.boolean(),
initialState: stateRecordSchema.default({}),
readable: z.boolean().optional(),
lightable: z.boolean().optional(),
lighter: z.boolean().optional(),
lighterUses: z.number().int().nonnegative().optional(),
})
export type ItemFrontmatter = z.infer<typeof itemFrontmatterSchema>
+20 -4
View File
@@ -37,9 +37,25 @@ export interface Item {
/** Long description shown when examined. */
long: string
/** Initial per-instance state (e.g. `{ lit: false }`). */
initialState: Record<string, string | boolean | number>
initialState: Record<string, string | boolean | number | string[]>
/** True if the player can pick it up. */
takeable: boolean
/** True if `read X` should narrate the item's `## read` section. */
readable?: boolean
/** True if `light X` / `extinguish X` apply; toggles state.lit. */
lightable?: boolean
/** True if this item can light other items. */
lighter?: boolean
/** Optional remaining-charges counter; absent means unlimited. */
lighterUses?: number
/** Prose returned by `read X`. Required iff readable is true. */
readableText?: string
/** Prose narrated when `light X` succeeds. Falls back to "It catches." */
litText?: string
/** Prose narrated when `extinguish X` succeeds. Falls back to "The flame dies." */
extinguishedText?: string
/** Prose narrated when this item's lighterUses reaches 0. Falls back to "It is spent." */
lighterEmptyText?: string
}
export interface EncounterPhaseDef {
@@ -83,8 +99,8 @@ export interface World {
encounters: Record<EncounterId, EncounterDef>
/** Story flag definitions and the endings they unlock. */
endings: {
true: { whenFlags: Record<string, string | boolean | number>; narration: string }
wrong: { whenFlags: Record<string, string | boolean | number>; narration: string }
bad: { whenFlags: Record<string, string | boolean | number>; narration: string }
true: { whenFlags: Record<string, string | boolean | number | string[]>; narration: string }
wrong: { whenFlags: Record<string, string | boolean | number | string[]>; narration: string }
bad: { whenFlags: Record<string, string | boolean | number | string[]>; narration: string }
}
}