feat(world): item schema — readable, lightable, lighter, lighterUses
Optional fields used by the new read/light/extinguish dispatcher branches. Loader updates and dispatcher logic follow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -150,7 +150,7 @@ export function parseItem(raw: string, sourcePath: string): Item {
|
|||||||
|
|
||||||
export interface ParsedEnding {
|
export interface ParsedEnding {
|
||||||
id: 'true' | 'wrong' | 'bad'
|
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 {
|
export function parseEnding(raw: string, _sourcePath: string): ParsedEnding {
|
||||||
|
|||||||
@@ -79,3 +79,27 @@ describe('encounterFrontmatterSchema', () => {
|
|||||||
expect(() => encounterFrontmatterSchema.parse(data)).not.toThrow()
|
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
@@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod'
|
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)
|
const stateRecordSchema = z.record(z.string(), stateValueSchema)
|
||||||
|
|
||||||
export const roomFrontmatterSchema = z.object({
|
export const roomFrontmatterSchema = z.object({
|
||||||
@@ -37,6 +37,10 @@ export const itemFrontmatterSchema = z.object({
|
|||||||
short: z.string().min(1),
|
short: z.string().min(1),
|
||||||
takeable: z.boolean(),
|
takeable: z.boolean(),
|
||||||
initialState: stateRecordSchema.default({}),
|
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>
|
export type ItemFrontmatter = z.infer<typeof itemFrontmatterSchema>
|
||||||
|
|||||||
+20
-4
@@ -37,9 +37,25 @@ export interface Item {
|
|||||||
/** Long description shown when examined. */
|
/** Long description shown when examined. */
|
||||||
long: string
|
long: string
|
||||||
/** Initial per-instance state (e.g. `{ lit: false }`). */
|
/** 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. */
|
/** True if the player can pick it up. */
|
||||||
takeable: boolean
|
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 {
|
export interface EncounterPhaseDef {
|
||||||
@@ -83,8 +99,8 @@ export interface World {
|
|||||||
encounters: Record<EncounterId, EncounterDef>
|
encounters: Record<EncounterId, EncounterDef>
|
||||||
/** Story flag definitions and the endings they unlock. */
|
/** Story flag definitions and the endings they unlock. */
|
||||||
endings: {
|
endings: {
|
||||||
true: { 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>; narration: string }
|
wrong: { whenFlags: Record<string, string | boolean | number | string[]>; narration: string }
|
||||||
bad: { whenFlags: Record<string, string | boolean | number>; narration: string }
|
bad: { whenFlags: Record<string, string | boolean | number | string[]>; narration: string }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user