Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29fd371b89 | |||
| 2a9b6155ef |
@@ -59,7 +59,7 @@ Phase 2 is large enough that I recommend slicing by *region*, not by *kind*. A f
|
|||||||
|
|
||||||
Suggested slices:
|
Suggested slices:
|
||||||
|
|
||||||
1. **Rewrite the existing 3 rooms** in the bible's voice. Foyer, Hallway, Cellar Stair — already wired up, fastest route to "the new tone is on screen." This is the smallest possible PR and de-risks the voice direction before scaling up.
|
1. **Rewrite the opening slice** in the bible's voice. The Gate, Foyer, Hallway, Cellar Stair — The Gate is the opening room, and the player should begin there carrying the folded letter, matchbook, and broken cigarette. This is the smallest possible PR and de-risks the voice direction before scaling up.
|
||||||
2. **Main-floor expansion** — Parlor, Study, Dining Room, Conservatory, Smoking Room, Music Room, Servants' Passage, Laundry. These connect to the existing Hallway. Add the items each room references (candlestick, pruning-shears, silver-lighter, music-box-key, damp-sheet) and their encounters (window-guest, ivy-figure, covered-cage, piano-echo, breathing-wall, linen-shape).
|
2. **Main-floor expansion** — Parlor, Study, Dining Room, Conservatory, Smoking Room, Music Room, Servants' Passage, Laundry. These connect to the existing Hallway. Add the items each room references (candlestick, pruning-shears, silver-lighter, music-box-key, damp-sheet) and their encounters (window-guest, ivy-figure, covered-cage, piano-echo, breathing-wall, linen-shape).
|
||||||
3. **Upper floor** — Stair, Bedroom, Nursery, Attic. Items: child's drawing, music-box (non-key), toy dog. Encounters: stair-sleeper.
|
3. **Upper floor** — Stair, Bedroom, Nursery, Attic. Items: child's drawing, music-box (non-key), toy dog. Encounters: stair-sleeper.
|
||||||
4. **Garden + grounds** — Back Door, Garden, Well, Well Shaft. Encounter: garden-procession, child-beneath-the-well (verbatim prose in bible).
|
4. **Garden + grounds** — Back Door, Garden, Well, Well Shaft. Encounter: garden-procession, child-beneath-the-well (verbatim prose in bible).
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import type { GameState, ParsedCommand, DispatchResult, ItemInstance, Transcript
|
|||||||
import { SCHEMA_VERSION, TRANSCRIPT_CAP } from './types'
|
import { SCHEMA_VERSION, TRANSCRIPT_CAP } from './types'
|
||||||
import { applyVerbToEncounter, maybeTriggerEncounter } from './encounters'
|
import { applyVerbToEncounter, maybeTriggerEncounter } from './encounters'
|
||||||
|
|
||||||
|
const HALFSTREET_ASCII = String.raw`
|
||||||
|
_ _ _ __ ____ _ _
|
||||||
|
| | | | __ _| |/ _| / ___|| |_ _ __ ___ ___| |_
|
||||||
|
| |_| |/ _\` | | |_ \___ \| __| '__/ _ \/ _ \ __|
|
||||||
|
| _ | (_| | | _| ___) | |_| | | __/ __/ |_
|
||||||
|
|_| |_|\__,_|_|_| |____/ \__|_| \___|\___|\__|
|
||||||
|
`.trim()
|
||||||
|
|
||||||
export function initialStateFor(world: World): GameState {
|
export function initialStateFor(world: World): GameState {
|
||||||
const startingRoom = world.rooms[world.startingRoom]
|
const startingRoom = world.rooms[world.startingRoom]
|
||||||
if (!startingRoom) throw new Error(`World has invalid startingRoom: ${world.startingRoom}`)
|
if (!startingRoom) throw new Error(`World has invalid startingRoom: ${world.startingRoom}`)
|
||||||
@@ -14,6 +22,7 @@ export function initialStateFor(world: World): GameState {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const opening: TranscriptLine[] = [
|
const opening: TranscriptLine[] = [
|
||||||
|
{ kind: 'system', text: HALFSTREET_ASCII },
|
||||||
{ kind: 'system', text: startingRoom.title },
|
{ kind: 'system', text: startingRoom.title },
|
||||||
{ kind: 'narration', text: startingRoom.descriptions.firstVisit },
|
{ kind: 'narration', text: startingRoom.descriptions.firstVisit },
|
||||||
]
|
]
|
||||||
@@ -134,6 +143,10 @@ export function dispatch(state: GameState, command: ParsedCommand, world: World)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (command.kind === 'verb-only') {
|
if (command.kind === 'verb-only') {
|
||||||
|
const encResult = applyVerbToEncounter(state, command, world)
|
||||||
|
if (encResult?.consumed) {
|
||||||
|
return withEndingCheck({ state: encResult.state, appended: encResult.lines }, world)
|
||||||
|
}
|
||||||
if (command.verb === 'look') return withEndingCheck(handleLook(state, world), world)
|
if (command.verb === 'look') return withEndingCheck(handleLook(state, world), world)
|
||||||
if (command.verb === 'inventory') return withEndingCheck(handleInventory(state, world), world)
|
if (command.verb === 'inventory') return withEndingCheck(handleInventory(state, world), world)
|
||||||
if (command.verb === 'wait') return withEndingCheck(narrate(state, [{ kind: 'narration', text: 'Time passes.' }]), world)
|
if (command.verb === 'wait') return withEndingCheck(narrate(state, [{ kind: 'narration', text: 'Time passes.' }]), world)
|
||||||
|
|||||||
@@ -76,6 +76,30 @@ describe('parser — unknown input', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('parser — verb + target', () => {
|
describe('parser — verb + target', () => {
|
||||||
|
it('recognizes slice-two encounter verbs', () => {
|
||||||
|
const ctx: ParserContext = {
|
||||||
|
knownItems: [],
|
||||||
|
knownEncounters: ['piano-echo', 'covered-cage'],
|
||||||
|
visibleNouns: [
|
||||||
|
{ id: 'piano-echo', aliases: ['piano', 'note'] },
|
||||||
|
{ id: 'covered-cage', aliases: ['cage'] },
|
||||||
|
],
|
||||||
|
inventoryItemIds: [],
|
||||||
|
lastNoun: null,
|
||||||
|
awaitingDisambiguation: null,
|
||||||
|
}
|
||||||
|
expect(parse('play note', ctx)).toEqual({
|
||||||
|
kind: 'verb-target',
|
||||||
|
verb: 'play',
|
||||||
|
target: { canonical: 'piano-echo', raw: 'note' },
|
||||||
|
})
|
||||||
|
expect(parse('uncover cage', ctx)).toEqual({
|
||||||
|
kind: 'verb-target',
|
||||||
|
verb: 'open',
|
||||||
|
target: { canonical: 'covered-cage', raw: 'cage' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('resolves a single visible noun', () => {
|
it('resolves a single visible noun', () => {
|
||||||
const ctx: ParserContext = {
|
const ctx: ParserContext = {
|
||||||
knownItems: ['torch'],
|
knownItems: ['torch'],
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ const VERB_SYNONYMS: Record<string, Verb> = {
|
|||||||
hold: 'hold', show: 'hold',
|
hold: 'hold', show: 'hold',
|
||||||
push: 'push', press: 'push',
|
push: 'push', press: 'push',
|
||||||
pull: 'pull',
|
pull: 'pull',
|
||||||
|
cut: 'cut', trim: 'cut',
|
||||||
|
play: 'play',
|
||||||
|
uncover: 'open',
|
||||||
wait: 'wait', z: 'wait',
|
wait: 'wait', z: 'wait',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ function ctxFor(state: GameState): ParserContext {
|
|||||||
if (item) visibleNouns.push({ id: inst.id, aliases: item.names })
|
if (item) visibleNouns.push({ id: inst.id, aliases: item.names })
|
||||||
}
|
}
|
||||||
if (room?.encounter) {
|
if (room?.encounter) {
|
||||||
visibleNouns.push({ id: room.encounter, aliases: [room.encounter] })
|
const encounter = world.encounters[room.encounter]
|
||||||
|
visibleNouns.push({
|
||||||
|
id: room.encounter,
|
||||||
|
aliases: [room.encounter, room.encounter.replace(/-/g, ' '), ...(encounter?.aliases ?? [])],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
knownItems: Object.keys(world.items),
|
knownItems: Object.keys(world.items),
|
||||||
@@ -41,8 +45,8 @@ function play(commands: string[]): GameState {
|
|||||||
describe('playthrough — sample world', () => {
|
describe('playthrough — sample world', () => {
|
||||||
it('reaches the rat-gone flag via the canonical command sequence', () => {
|
it('reaches the rat-gone flag via the canonical command sequence', () => {
|
||||||
const state = play([
|
const state = play([
|
||||||
'take letter',
|
|
||||||
'read letter', // verb is recognized but encounter takes priority elsewhere; here it's a no-op
|
'read letter', // verb is recognized but encounter takes priority elsewhere; here it's a no-op
|
||||||
|
'n', // gate → foyer
|
||||||
'n', // foyer → hallway
|
'n', // foyer → hallway
|
||||||
'take lamp',
|
'take lamp',
|
||||||
'e', // hallway → cellar-stair (triggers rat encounter)
|
'e', // hallway → cellar-stair (triggers rat encounter)
|
||||||
@@ -55,11 +59,52 @@ describe('playthrough — sample world', () => {
|
|||||||
|
|
||||||
it('handles invalid moves gracefully', () => {
|
it('handles invalid moves gracefully', () => {
|
||||||
const state = play([
|
const state = play([
|
||||||
'go up', // foyer has no up exit
|
'go up', // gate has no up exit
|
||||||
'n',
|
'n',
|
||||||
's',
|
's',
|
||||||
'flibbertigibbet', // unknown verb
|
'flibbertigibbet', // unknown verb
|
||||||
])
|
])
|
||||||
expect(state.location).toBe('foyer')
|
expect(state.location).toBe('outside-gate')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('plays through the main-floor slice encounters', () => {
|
||||||
|
const state = play([
|
||||||
|
'n', // gate → foyer
|
||||||
|
'n', // foyer → hallway
|
||||||
|
'n', // hallway → dining-room
|
||||||
|
'close curtains',
|
||||||
|
'take candlestick',
|
||||||
|
'n', // dining-room → conservatory
|
||||||
|
'take shears',
|
||||||
|
'cut vines with shears',
|
||||||
|
's',
|
||||||
|
'w', // dining-room → hallway
|
||||||
|
'w', // hallway → smoking-room
|
||||||
|
'take lighter',
|
||||||
|
'uncover cage',
|
||||||
|
'e',
|
||||||
|
'd', // hallway → music-room
|
||||||
|
'play note',
|
||||||
|
'take tiny key',
|
||||||
|
'n', // music-room → servants-passage
|
||||||
|
'wait',
|
||||||
|
'e', // servants-passage → laundry
|
||||||
|
'wait',
|
||||||
|
'take damp sheet',
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(state.flags['window-guest.resolved']).toBe(true)
|
||||||
|
expect(state.flags['ivy-figure.resolved']).toBe(true)
|
||||||
|
expect(state.flags['covered-cage.resolved']).toBe(true)
|
||||||
|
expect(state.flags['piano-echo.resolved']).toBe(true)
|
||||||
|
expect(state.flags['breathing-wall.resolved']).toBe(true)
|
||||||
|
expect(state.flags['linen-shape.resolved']).toBe(true)
|
||||||
|
expect(state.inventory.map((i) => i.id)).toEqual(expect.arrayContaining([
|
||||||
|
'candlestick',
|
||||||
|
'pruning-shears',
|
||||||
|
'silver-lighter',
|
||||||
|
'music-box-key',
|
||||||
|
'damp-sheet',
|
||||||
|
]))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ export type Direction = 'n' | 's' | 'e' | 'w' | 'u' | 'd'
|
|||||||
export type Verb =
|
export type Verb =
|
||||||
| 'go' | 'look' | 'examine' | 'take' | 'drop' | 'use' | 'open' | 'close'
|
| 'go' | 'look' | 'examine' | 'take' | 'drop' | 'use' | 'open' | 'close'
|
||||||
| 'read' | 'light' | 'extinguish' | 'attack' | 'inventory' | 'wait'
|
| 'read' | 'light' | 'extinguish' | 'attack' | 'inventory' | 'wait'
|
||||||
| 'hold' | 'push' | 'pull'
|
| 'hold' | 'push' | 'pull' | 'cut' | 'play'
|
||||||
|
|
||||||
export type MetaVerb = 'restart' | 'undo' | 'hint' | 'save' | 'quit' | 'theme'
|
export type MetaVerb = 'restart' | 'undo' | 'hint' | 'save' | 'quit' | 'theme'
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ import '../ui/crt.css'
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<footer class="mystery-footer">
|
||||||
|
By <a href="https://ethanjlewis.com">Ethan J Lewis</a>
|
||||||
|
<span aria-hidden="true">|</span>
|
||||||
|
<a href="https://half.st/ejlewis/halfstreet">Source Code</a>
|
||||||
|
<span aria-hidden="true">|</span>
|
||||||
|
<a href="https://half.st/ejlewis/halfstreet/src/branch/main/LICENSE">GNU General Public License v3.0</a>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// Theme attribute is set on :root before any rendering to avoid a flash
|
// Theme attribute is set on :root before any rendering to avoid a flash
|
||||||
|
|||||||
+11
-5
@@ -14,30 +14,36 @@ describe('computeChips — sample world', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('adds TAKE chips for visible takeable items', () => {
|
it('adds TAKE chips for visible takeable items', () => {
|
||||||
const s = initialStateFor(world)
|
let s = initialStateFor(world)
|
||||||
|
s = dispatch(s, { kind: 'go', direction: 'n' }, world).state
|
||||||
|
s = dispatch(s, { kind: 'go', direction: 'n' }, world).state
|
||||||
const chips = computeChips(s, world)
|
const chips = computeChips(s, world)
|
||||||
expect(chips.find((c) => c.kind === 'item' && c.command === 'take letter')).toBeTruthy()
|
expect(chips.find((c) => c.kind === 'item' && c.command === 'take lamp')).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes TAKE chip after item is taken', () => {
|
it('removes TAKE chip after item is taken', () => {
|
||||||
let s = initialStateFor(world)
|
let s = initialStateFor(world)
|
||||||
s = dispatch(s, { kind: 'verb-target', verb: 'take', target: { canonical: 'letter', raw: 'letter' } }, world).state
|
s = dispatch(s, { kind: 'go', direction: 'n' }, world).state
|
||||||
|
s = dispatch(s, { kind: 'go', direction: 'n' }, world).state
|
||||||
|
s = dispatch(s, { kind: 'verb-target', verb: 'take', target: { canonical: 'lamp', raw: 'lamp' } }, world).state
|
||||||
const chips = computeChips(s, world)
|
const chips = computeChips(s, world)
|
||||||
expect(chips.find((c) => c.command === 'take letter')).toBeUndefined()
|
expect(chips.find((c) => c.command === 'take lamp')).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('adds an encounter verb chip when an encounter is active', () => {
|
it('adds an encounter verb chip when an encounter is active', () => {
|
||||||
let s = initialStateFor(world)
|
let s = initialStateFor(world)
|
||||||
s = dispatch(s, { kind: 'go', direction: 'n' }, world).state
|
s = dispatch(s, { kind: 'go', direction: 'n' }, world).state
|
||||||
|
s = dispatch(s, { kind: 'go', direction: 'n' }, world).state
|
||||||
s = dispatch(s, { kind: 'go', direction: 'e' }, world).state
|
s = dispatch(s, { kind: 'go', direction: 'e' }, world).state
|
||||||
const chips = computeChips(s, world)
|
const chips = computeChips(s, world)
|
||||||
expect(chips.find((c) => c.kind === 'encounter' && c.command.includes('rat'))).toBeTruthy()
|
expect(chips.find((c) => c.kind === 'encounter' && c.command.includes('rat'))).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('always includes LOOK and INV', () => {
|
it('always includes LOOK, INV, and HELP', () => {
|
||||||
const s = initialStateFor(world)
|
const s = initialStateFor(world)
|
||||||
const chips = computeChips(s, world)
|
const chips = computeChips(s, world)
|
||||||
expect(chips.find((c) => c.command === 'look')).toBeTruthy()
|
expect(chips.find((c) => c.command === 'look')).toBeTruthy()
|
||||||
expect(chips.find((c) => c.command === 'inventory')).toBeTruthy()
|
expect(chips.find((c) => c.command === 'inventory')).toBeTruthy()
|
||||||
|
expect(chips.find((c) => c.command === 'help')).toBeTruthy()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export function computeChips(state: GameState, world: World): Chip[] {
|
|||||||
// Persistent meta chips.
|
// Persistent meta chips.
|
||||||
out.push({ kind: 'meta', label: 'LOOK', command: 'look', disabled: false })
|
out.push({ kind: 'meta', label: 'LOOK', command: 'look', disabled: false })
|
||||||
out.push({ kind: 'meta', label: 'INV', command: 'inventory', disabled: false })
|
out.push({ kind: 'meta', label: 'INV', command: 'inventory', disabled: false })
|
||||||
|
out.push({ kind: 'meta', label: 'HELP', command: 'help', disabled: false })
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-6
@@ -46,6 +46,25 @@
|
|||||||
box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.55);
|
box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mystery-footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 8px 4px 0;
|
||||||
|
color: var(--m-dim);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystery-footer a {
|
||||||
|
color: var(--m-fg);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystery-footer span {
|
||||||
|
margin: 0 0.5ch;
|
||||||
|
}
|
||||||
|
|
||||||
.mystery-bezel::before {
|
.mystery-bezel::before {
|
||||||
/* scanlines overlay */
|
/* scanlines overlay */
|
||||||
content: '';
|
content: '';
|
||||||
@@ -104,6 +123,14 @@
|
|||||||
margin-top: 0.6em;
|
margin-top: 0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mystery-transcript .help {
|
||||||
|
color: var(--m-fg);
|
||||||
|
font-weight: normal;
|
||||||
|
border: 1px var(--m-divider-style) var(--m-dim);
|
||||||
|
padding: 0.75em;
|
||||||
|
margin: 0.75em 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mystery-transcript .player {
|
.mystery-transcript .player {
|
||||||
color: var(--m-accent-2);
|
color: var(--m-accent-2);
|
||||||
}
|
}
|
||||||
@@ -140,13 +167,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mystery-chips {
|
.mystery-chips {
|
||||||
display: none;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 6px 0 4px;
|
padding: 6px 0 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
border-top: 1px var(--m-divider-style) var(--m-dim);
|
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,10 +192,6 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (pointer: coarse) {
|
|
||||||
.mystery-chips { display: flex; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.mystery-transcript .ending {
|
.mystery-transcript .ending {
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
@@ -179,6 +201,16 @@
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.mystery-root {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystery-bezel {
|
||||||
|
padding: 18px 14px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[data-mystery-input].ended {
|
[data-mystery-input].ended {
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-1
@@ -11,12 +11,32 @@ import { renderChips } from './chip-render'
|
|||||||
const transcriptEl = document.querySelector<HTMLDivElement>('[data-mystery-transcript]')
|
const transcriptEl = document.querySelector<HTMLDivElement>('[data-mystery-transcript]')
|
||||||
const inputEl = document.querySelector<HTMLInputElement>('[data-mystery-input]')
|
const inputEl = document.querySelector<HTMLInputElement>('[data-mystery-input]')
|
||||||
|
|
||||||
|
const HELP_TEXT = `You arrive at the address, but you do not remember what has happened. The road behind you is gone...
|
||||||
|
|
||||||
|
This is a text adventure. Type short commands to act in the house.
|
||||||
|
|
||||||
|
Common commands:
|
||||||
|
look describe the room again
|
||||||
|
n, s, e, w, u, d move by direction
|
||||||
|
take lamp pick something up
|
||||||
|
examine letter inspect something nearby or held
|
||||||
|
read letter read a readable object
|
||||||
|
inventory see what you carry
|
||||||
|
light lamp with matches use one thing with another
|
||||||
|
wait let the room continue
|
||||||
|
undo step back once
|
||||||
|
restart begin again
|
||||||
|
theme change the terminal colors
|
||||||
|
|
||||||
|
Most commands are verb first, then the thing: examine gate, take lamp, use key on door.`
|
||||||
|
|
||||||
if (!transcriptEl || !inputEl) {
|
if (!transcriptEl || !inputEl) {
|
||||||
console.error('[halfstreet] terminal mount points missing')
|
console.error('[halfstreet] terminal mount points missing')
|
||||||
} else {
|
} else {
|
||||||
const restored = loadState()
|
const restored = loadState()
|
||||||
let state: GameState = restored ?? initialStateFor(world)
|
let state: GameState = restored ?? initialStateFor(world)
|
||||||
let lastState: GameState | null = null // for one-step undo
|
let lastState: GameState | null = null // for one-step undo
|
||||||
|
let transientHelpEl: HTMLDivElement | null = null
|
||||||
|
|
||||||
if (!restored) {
|
if (!restored) {
|
||||||
// Fresh state already includes the opening narration in its transcript.
|
// Fresh state already includes the opening narration in its transcript.
|
||||||
@@ -50,7 +70,11 @@ if (!transcriptEl || !inputEl) {
|
|||||||
if (it) visibleNouns.push({ id, aliases: it.names })
|
if (it) visibleNouns.push({ id, aliases: it.names })
|
||||||
}
|
}
|
||||||
if (room.encounter && s.encounterState[room.encounter]) {
|
if (room.encounter && s.encounterState[room.encounter]) {
|
||||||
visibleNouns.push({ id: room.encounter, aliases: [room.encounter] })
|
const encounter = world.encounters[room.encounter]
|
||||||
|
visibleNouns.push({
|
||||||
|
id: room.encounter,
|
||||||
|
aliases: [room.encounter, room.encounter.replace(/-/g, ' '), ...(encounter?.aliases ?? [])],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const inst of s.inventory) {
|
for (const inst of s.inventory) {
|
||||||
@@ -78,6 +102,23 @@ if (!transcriptEl || !inputEl) {
|
|||||||
transcriptEl.scrollTop = transcriptEl.scrollHeight
|
transcriptEl.scrollTop = transcriptEl.scrollHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearTransientHelp = (): void => {
|
||||||
|
transientHelpEl?.remove()
|
||||||
|
transientHelpEl = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTransientHelp = (): void => {
|
||||||
|
if (!transcriptEl) return
|
||||||
|
clearTransientHelp()
|
||||||
|
const el = document.createElement('div')
|
||||||
|
el.className = 'system help'
|
||||||
|
el.dataset.transientHelp = 'true'
|
||||||
|
el.textContent = HELP_TEXT
|
||||||
|
transcriptEl.appendChild(el)
|
||||||
|
transientHelpEl = el
|
||||||
|
transcriptEl.scrollTop = transcriptEl.scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
// For UI-originated lines (player input, restart/undo/quit messages, error
|
// For UI-originated lines (player input, restart/undo/quit messages, error
|
||||||
// notices). Pushes into state.transcript so they survive reload, then renders.
|
// notices). Pushes into state.transcript so they survive reload, then renders.
|
||||||
// Engine-originated lines (from dispatch) are already in state.transcript;
|
// Engine-originated lines (from dispatch) are already in state.transcript;
|
||||||
@@ -98,6 +139,7 @@ if (!transcriptEl || !inputEl) {
|
|||||||
const raw = inputEl.value
|
const raw = inputEl.value
|
||||||
inputEl.value = ''
|
inputEl.value = ''
|
||||||
if (!raw.trim()) return
|
if (!raw.trim()) return
|
||||||
|
clearTransientHelp()
|
||||||
appendLines([{ kind: 'player', text: raw }])
|
appendLines([{ kind: 'player', text: raw }])
|
||||||
|
|
||||||
// Once the game has ended, only restart and undo are allowed.
|
// Once the game has ended, only restart and undo are allowed.
|
||||||
@@ -126,6 +168,10 @@ if (!transcriptEl || !inputEl) {
|
|||||||
syncEndedUI()
|
syncEndedUI()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (trimmed === 'help') {
|
||||||
|
renderTransientHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (trimmed === 'undo') {
|
if (trimmed === 'undo') {
|
||||||
if (lastState) {
|
if (lastState) {
|
||||||
state = lastState
|
state = lastState
|
||||||
|
|||||||
Vendored
+2
-1
@@ -2,5 +2,6 @@
|
|||||||
"alwaysUpdateLinks": true,
|
"alwaysUpdateLinks": true,
|
||||||
"newLinkFormat": "shortest",
|
"newLinkFormat": "shortest",
|
||||||
"useMarkdownLinks": false,
|
"useMarkdownLinks": false,
|
||||||
"attachmentFolderPath": "_attachments"
|
"attachmentFolderPath": "_attachments",
|
||||||
|
"propertiesInDocument": "hidden"
|
||||||
}
|
}
|
||||||
Vendored
+5
-1
@@ -1 +1,5 @@
|
|||||||
{}
|
{
|
||||||
|
"theme": "obsidian",
|
||||||
|
"accentColor": "#50609f",
|
||||||
|
"cssTheme": "Primary"
|
||||||
|
}
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
[
|
||||||
|
"obsidian-minimal-settings",
|
||||||
|
"obsidian-style-settings"
|
||||||
|
]
|
||||||
Vendored
+33
-4
@@ -6,17 +6,46 @@
|
|||||||
"hideUnresolved": false,
|
"hideUnresolved": false,
|
||||||
"showOrphans": true,
|
"showOrphans": true,
|
||||||
"collapse-color-groups": false,
|
"collapse-color-groups": false,
|
||||||
"colorGroups": [],
|
"colorGroups": [
|
||||||
|
{
|
||||||
|
"query": "path:rooms ",
|
||||||
|
"color": {
|
||||||
|
"a": 1,
|
||||||
|
"rgb": 14701138
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "path:items ",
|
||||||
|
"color": {
|
||||||
|
"a": 1,
|
||||||
|
"rgb": 14725458
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "path:encounters ",
|
||||||
|
"color": {
|
||||||
|
"a": 1,
|
||||||
|
"rgb": 11657298
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "path:endings ",
|
||||||
|
"color": {
|
||||||
|
"a": 1,
|
||||||
|
"rgb": 5431378
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"collapse-display": false,
|
"collapse-display": false,
|
||||||
"showArrow": false,
|
"showArrow": false,
|
||||||
"textFadeMultiplier": 0,
|
"textFadeMultiplier": 0,
|
||||||
"nodeSizeMultiplier": 1.2,
|
"nodeSizeMultiplier": 1.82265625,
|
||||||
"lineSizeMultiplier": 1,
|
"lineSizeMultiplier": 5,
|
||||||
"collapse-forces": false,
|
"collapse-forces": false,
|
||||||
"centerStrength": 0.518713248970312,
|
"centerStrength": 0.518713248970312,
|
||||||
"repelStrength": 10,
|
"repelStrength": 10,
|
||||||
"linkStrength": 1,
|
"linkStrength": 1,
|
||||||
"linkDistance": 250,
|
"linkDistance": 250,
|
||||||
"scale": 2.25,
|
"scale": 0.8332185563593782,
|
||||||
"close": true
|
"close": true
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"lightStyle": "minimal-light",
|
||||||
|
"darkStyle": "minimal-dark",
|
||||||
|
"lightScheme": "minimal-default-light",
|
||||||
|
"darkScheme": "minimal-default-dark",
|
||||||
|
"editorFont": "",
|
||||||
|
"lineHeight": 1.5,
|
||||||
|
"lineWidth": 40,
|
||||||
|
"lineWidthWide": 50,
|
||||||
|
"maxWidth": 88,
|
||||||
|
"textNormal": 16,
|
||||||
|
"textSmall": 13,
|
||||||
|
"imgGrid": false,
|
||||||
|
"imgWidth": "img-default-width",
|
||||||
|
"tableWidth": "table-default-width",
|
||||||
|
"iframeWidth": "iframe-default-width",
|
||||||
|
"mapWidth": "map-default-width",
|
||||||
|
"chartWidth": "chart-default-width",
|
||||||
|
"colorfulHeadings": false,
|
||||||
|
"colorfulFrame": false,
|
||||||
|
"colorfulActiveStates": false,
|
||||||
|
"trimNames": true,
|
||||||
|
"labeledNav": false,
|
||||||
|
"fullWidthMedia": true,
|
||||||
|
"bordersToggle": true,
|
||||||
|
"minimalStatus": true,
|
||||||
|
"focusMode": false,
|
||||||
|
"underlineInternal": true,
|
||||||
|
"underlineExternal": true,
|
||||||
|
"folding": true,
|
||||||
|
"lineNumbers": false,
|
||||||
|
"readableLineLength": true
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "obsidian-minimal-settings",
|
||||||
|
"name": "Minimal Theme Settings",
|
||||||
|
"version": "8.2.2",
|
||||||
|
"minAppVersion": "1.11.1",
|
||||||
|
"description": "Change the colors, fonts and features of Minimal Theme.",
|
||||||
|
"author": "@kepano",
|
||||||
|
"authorUrl": "https://www.twitter.com/kepano",
|
||||||
|
"fundingUrl": "https://www.buymeacoffee.com/kepano",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.setting-group > .setting-item-heading .setting-item-description {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"primary-theme@@alt-folder-icons": true,
|
||||||
|
"primary-theme@@colorful-folders_text": true,
|
||||||
|
"primary-theme@@colorful-folders_collapse-indicator": true,
|
||||||
|
"primary-theme@@colorful-folders_background": false,
|
||||||
|
"primary-theme@@colorful-folders_indentation-guide": true,
|
||||||
|
"primary-theme@@colorful-folders_inherit-color": true
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "obsidian-style-settings",
|
||||||
|
"name": "Style Settings",
|
||||||
|
"version": "1.0.9",
|
||||||
|
"minAppVersion": "0.11.5",
|
||||||
|
"description": "Offers controls for adjusting theme, plugin, and snippet CSS variables.",
|
||||||
|
"author": "mgmeyers",
|
||||||
|
"authorUrl": "https://github.com/mgmeyers/obsidian-style-settings",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "Minimal",
|
||||||
|
"version": "8.1.7",
|
||||||
|
"minAppVersion": "1.9.0",
|
||||||
|
"author": "@kepano",
|
||||||
|
"authorUrl": "https://twitter.com/kepano",
|
||||||
|
"fundingUrl": "https://www.buymeacoffee.com/kepano"
|
||||||
|
}
|
||||||
+2246
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Primary",
|
||||||
|
"version": "2.10.0",
|
||||||
|
"minAppVersion": "1.4.0",
|
||||||
|
"author": "Cecilia May",
|
||||||
|
"fundingUrl": {
|
||||||
|
"Ko-fi": "https://ko-fi.com/ceciliamay"
|
||||||
|
}
|
||||||
|
}
|
||||||
+3878
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
short:
|
||||||
|
readable:
|
||||||
|
---
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
formulas:
|
||||||
|
Room: |-
|
||||||
|
file.backlinks
|
||||||
|
.filter(value.asFile().inFolder("rooms"))
|
||||||
|
.map(value.asFile())
|
||||||
|
views:
|
||||||
|
- type: cards
|
||||||
|
name: Rooms - Exits
|
||||||
|
filters:
|
||||||
|
and:
|
||||||
|
- file.inFolder("rooms")
|
||||||
|
order:
|
||||||
|
- file.name
|
||||||
|
- exitN
|
||||||
|
- exitS
|
||||||
|
- exitE
|
||||||
|
- exitW
|
||||||
|
- exitU
|
||||||
|
- exitD
|
||||||
|
- type: cards
|
||||||
|
name: Items
|
||||||
|
filters:
|
||||||
|
and:
|
||||||
|
- file.inFolder("items")
|
||||||
|
order:
|
||||||
|
- file.name
|
||||||
|
- short
|
||||||
|
- readable
|
||||||
|
- takeable
|
||||||
|
- names
|
||||||
|
- formula.Room
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
- [x] Need to add help text when user types "help". This should give them some common commands and explain the concepts behind text adventure games. It should also include an exceprt at the beginning from the opening text. "You arrive at the address, but you do not remember what has happened. The road behind you is gone...". The help text should disappear after the user types a new prompt (i.e., it's not persistent).
|
||||||
|
- [x] Need to add the tiles from mobile to desktop view.
|
||||||
|
- [ ] Enhance tiles with contextual awareness, enabling tiles to appear in rooms when appropriate (e.g. "attack rat").
|
||||||
|
- [ ] Create a mechanic that asks "Are you sure?" before taking critical actions like attacking or other game-changing mechanics that might affect the final ending.
|
||||||
|
- [ ] Add a tile for USE
|
||||||
|
- [ ] Add contextual awareness and autocomplete. For example a popup that appears above the USE text or when a user types "use". The user is able to autofill the rest of the thing by using the keyboard to toggle through the inv list and tab-complete the option, (or tap on mobile) "e.g. use (matches, light, letter) *on* (lamp)" - the word "on" there being suggested. Suggestions for autocomplete are in italics. This is one modern design element we're going to add.
|
||||||
|
- [x] Add a footer with "By [Ethan J Lewis](https://ethanjlewis.com) | [Source Code](https://half.st/ejlewis/halfstreet) | [GNU General Public License v3.0](https://half.st/ejlewis/halfstreet/src/branch/main/LICENSE)"
|
||||||
|
- [x] Add "Half Street" as ASCII Art to the intro text.
|
||||||
|
- [ ] Add logic to make the last sentence in the examined description conditional. This is where we'll list items in the room. (e.g., "The hallway runs further than the house should be wide. The dust on the floor is undisturbed except where you have walked. *The oil lamp is on the side table.*")
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@@ -2,12 +2,38 @@ import { describe, it, expect } from 'vitest'
|
|||||||
import { world } from './index'
|
import { world } from './index'
|
||||||
|
|
||||||
describe('assembled world', () => {
|
describe('assembled world', () => {
|
||||||
it('contains all three rooms', () => {
|
it('contains the authored opening and main-floor rooms', () => {
|
||||||
expect(Object.keys(world.rooms).sort()).toEqual(['cellar-stair', 'foyer', 'hallway'])
|
expect(Object.keys(world.rooms)).toEqual(expect.arrayContaining([
|
||||||
|
'outside-gate',
|
||||||
|
'foyer',
|
||||||
|
'hallway',
|
||||||
|
'cellar-stair',
|
||||||
|
'parlor',
|
||||||
|
'study',
|
||||||
|
'dining-room',
|
||||||
|
'conservatory',
|
||||||
|
'smoking-room',
|
||||||
|
'music-room',
|
||||||
|
'servants-passage',
|
||||||
|
'laundry',
|
||||||
|
]))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('contains all three items', () => {
|
it('contains the authored opening and main-floor items', () => {
|
||||||
expect(Object.keys(world.items).sort()).toEqual(['lamp', 'letter', 'matches'])
|
expect(Object.keys(world.items)).toEqual(expect.arrayContaining([
|
||||||
|
'broken-cigarette',
|
||||||
|
'lamp',
|
||||||
|
'letter',
|
||||||
|
'matches',
|
||||||
|
'candlestick',
|
||||||
|
'pruning-shears',
|
||||||
|
'silver-lighter',
|
||||||
|
'music-box-key',
|
||||||
|
'damp-sheet',
|
||||||
|
'grandfather-clock',
|
||||||
|
'dinner-place-setting',
|
||||||
|
'covered-cage',
|
||||||
|
]))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('all room exits resolve to known rooms', () => {
|
it('all room exits resolve to known rooms', () => {
|
||||||
|
|||||||
@@ -27,4 +27,142 @@ export const encounters: Record<string, EncounterDef> = {
|
|||||||
onResolved: { setFlags: { ratGone: true } },
|
onResolved: { setFlags: { ratGone: true } },
|
||||||
defaultWrongVerbNarration: 'The rat watches.',
|
defaultWrongVerbNarration: 'The rat watches.',
|
||||||
},
|
},
|
||||||
|
'window-guest': {
|
||||||
|
id: 'window-guest',
|
||||||
|
aliases: ['guest', 'window guest', 'curtains', 'curtain', 'window'],
|
||||||
|
startsIn: 'dining-room',
|
||||||
|
initialPhase: 'standing-outside',
|
||||||
|
phases: {
|
||||||
|
'standing-outside': {
|
||||||
|
description: narration('window-guest', 'standing-outside'),
|
||||||
|
transitions: [
|
||||||
|
{
|
||||||
|
verb: 'close',
|
||||||
|
target: 'window-guest',
|
||||||
|
narration: narration('window-guest', 'close-window-guest-resolved'),
|
||||||
|
to: 'resolved',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onResolved: { setFlags: { curtainsClosed: true } },
|
||||||
|
onFailed: { narration: narration('window-guest', 'failed'), retreatTo: 'hallway' },
|
||||||
|
defaultWrongVerbNarration: narration('window-guest', 'wrong-verb'),
|
||||||
|
},
|
||||||
|
'ivy-figure': {
|
||||||
|
id: 'ivy-figure',
|
||||||
|
aliases: ['ivy figure', 'figure', 'ivy', 'vines', 'vine'],
|
||||||
|
startsIn: 'conservatory',
|
||||||
|
initialPhase: 'hidden',
|
||||||
|
phases: {
|
||||||
|
hidden: {
|
||||||
|
description: narration('ivy-figure', 'hidden'),
|
||||||
|
transitions: [
|
||||||
|
{
|
||||||
|
verb: 'cut',
|
||||||
|
target: 'ivy-figure',
|
||||||
|
requires: { item: 'pruning-shears' },
|
||||||
|
narration: narration('ivy-figure', 'cut-ivy-figure-resolved'),
|
||||||
|
to: 'resolved',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
verb: 'use',
|
||||||
|
target: 'ivy-figure',
|
||||||
|
requires: { item: 'pruning-shears' },
|
||||||
|
narration: narration('ivy-figure', 'cut-ivy-figure-resolved'),
|
||||||
|
to: 'resolved',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onResolved: { setFlags: { conservatoryVinesCut: true } },
|
||||||
|
onFailed: { narration: narration('ivy-figure', 'failed'), retreatTo: 'dining-room' },
|
||||||
|
defaultWrongVerbNarration: narration('ivy-figure', 'wrong-verb'),
|
||||||
|
},
|
||||||
|
'covered-cage': {
|
||||||
|
id: 'covered-cage',
|
||||||
|
aliases: ['covered cage', 'cage', 'birdcage', 'cloth'],
|
||||||
|
startsIn: 'smoking-room',
|
||||||
|
initialPhase: 'rustling',
|
||||||
|
phases: {
|
||||||
|
rustling: {
|
||||||
|
description: narration('covered-cage', 'rustling'),
|
||||||
|
transitions: [
|
||||||
|
{
|
||||||
|
verb: 'open',
|
||||||
|
target: 'covered-cage',
|
||||||
|
narration: narration('covered-cage', 'open-covered-cage-resolved'),
|
||||||
|
to: 'resolved',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onResolved: { setFlags: { cageUncovered: true } },
|
||||||
|
onFailed: { narration: narration('covered-cage', 'failed'), retreatTo: 'hallway' },
|
||||||
|
defaultWrongVerbNarration: narration('covered-cage', 'wrong-verb'),
|
||||||
|
},
|
||||||
|
'piano-echo': {
|
||||||
|
id: 'piano-echo',
|
||||||
|
aliases: ['piano echo', 'piano', 'note', 'key'],
|
||||||
|
startsIn: 'music-room',
|
||||||
|
initialPhase: 'listening',
|
||||||
|
phases: {
|
||||||
|
listening: {
|
||||||
|
description: narration('piano-echo', 'listening'),
|
||||||
|
transitions: [
|
||||||
|
{
|
||||||
|
verb: 'play',
|
||||||
|
target: 'piano-echo',
|
||||||
|
narration: narration('piano-echo', 'play-piano-echo-resolved'),
|
||||||
|
to: 'resolved',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onResolved: { setFlags: { musicSolved: true } },
|
||||||
|
onFailed: { narration: narration('piano-echo', 'failed'), retreatTo: 'hallway' },
|
||||||
|
defaultWrongVerbNarration: narration('piano-echo', 'wrong-verb'),
|
||||||
|
},
|
||||||
|
'breathing-wall': {
|
||||||
|
id: 'breathing-wall',
|
||||||
|
aliases: ['breathing wall', 'wall', 'walls', 'breathing'],
|
||||||
|
startsIn: 'servants-passage',
|
||||||
|
initialPhase: 'audible',
|
||||||
|
phases: {
|
||||||
|
audible: {
|
||||||
|
description: narration('breathing-wall', 'audible'),
|
||||||
|
transitions: [
|
||||||
|
{
|
||||||
|
verb: 'wait',
|
||||||
|
narration: narration('breathing-wall', 'wait-resolved'),
|
||||||
|
to: 'resolved',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onResolved: { setFlags: { breathingWallPassed: true } },
|
||||||
|
onFailed: { narration: narration('breathing-wall', 'failed'), retreatTo: 'music-room' },
|
||||||
|
defaultWrongVerbNarration: narration('breathing-wall', 'wrong-verb'),
|
||||||
|
},
|
||||||
|
'linen-shape': {
|
||||||
|
id: 'linen-shape',
|
||||||
|
aliases: ['linen shape', 'shape', 'sheet', 'sheets', 'linen'],
|
||||||
|
startsIn: 'laundry',
|
||||||
|
initialPhase: 'hanging',
|
||||||
|
phases: {
|
||||||
|
hanging: {
|
||||||
|
description: narration('linen-shape', 'hanging'),
|
||||||
|
transitions: [
|
||||||
|
{
|
||||||
|
verb: 'wait',
|
||||||
|
narration: narration('linen-shape', 'wait-resolved'),
|
||||||
|
to: 'resolved',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onResolved: { setFlags: { linenShapeEmpty: true } },
|
||||||
|
onFailed: { narration: narration('linen-shape', 'failed'), retreatTo: 'servants-passage' },
|
||||||
|
defaultWrongVerbNarration: narration('linen-shape', 'wrong-verb'),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
id: breathing-wall
|
||||||
|
startsIn: "[[servants-passage]]"
|
||||||
|
initialPhase: audible
|
||||||
|
---
|
||||||
|
|
||||||
|
## audible
|
||||||
|
The wall beside your shoulder breathes in. The opposite wall answers.
|
||||||
|
|
||||||
|
## wait-resolved
|
||||||
|
You stand still.
|
||||||
|
|
||||||
|
The passage narrows, then forgets to. The breathing passes on ahead of you.
|
||||||
|
|
||||||
|
## wrong-verb
|
||||||
|
The walls take a slower breath.
|
||||||
|
|
||||||
|
## failed
|
||||||
|
The boards lean close enough to touch your sleeves. You retreat into the music room.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
id: covered-cage
|
||||||
|
startsIn: "[[smoking-room]]"
|
||||||
|
initialPhase: rustling
|
||||||
|
---
|
||||||
|
|
||||||
|
## rustling
|
||||||
|
The covered cage rustles once. Then again, softer, as if whatever is inside has learned restraint.
|
||||||
|
|
||||||
|
## open-covered-cage-resolved
|
||||||
|
You lift the cloth.
|
||||||
|
|
||||||
|
The cage is empty. A few pale feathers cling to the wire, though no bird could have passed through it.
|
||||||
|
|
||||||
|
Somewhere far above you, wings beat once inside a wall.
|
||||||
|
|
||||||
|
## wrong-verb
|
||||||
|
The cloth trembles.
|
||||||
|
|
||||||
|
## failed
|
||||||
|
The rustling grows too close to your ear. You leave the room before you decide to.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
id: ivy-figure
|
||||||
|
startsIn: "[[conservatory]]"
|
||||||
|
initialPhase: hidden
|
||||||
|
---
|
||||||
|
|
||||||
|
## hidden
|
||||||
|
The ivy has gathered itself into the suggestion of a person. Leaves cling where eyes should be.
|
||||||
|
|
||||||
|
## cut-ivy-figure-resolved
|
||||||
|
The shears close with a sound like teeth.
|
||||||
|
|
||||||
|
The figure falls apart leaf by leaf. Behind it, the glass is only glass.
|
||||||
|
|
||||||
|
## wrong-verb
|
||||||
|
The vines tighten without moving.
|
||||||
|
|
||||||
|
## failed
|
||||||
|
The ivy catches at your wrists. When you pull free, you are back among the cold plates of the dining room.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
id: linen-shape
|
||||||
|
startsIn: "[[laundry]]"
|
||||||
|
initialPhase: hanging
|
||||||
|
---
|
||||||
|
|
||||||
|
## hanging
|
||||||
|
One hanging sheet has the weight and outline of a person standing behind it.
|
||||||
|
|
||||||
|
## wait-resolved
|
||||||
|
You wait.
|
||||||
|
|
||||||
|
The sheet stirs. Nothing stands behind it. Nothing had stood behind it.
|
||||||
|
|
||||||
|
## wrong-verb
|
||||||
|
The shape seems to lean toward you.
|
||||||
|
|
||||||
|
## failed
|
||||||
|
You push through the hanging sheets and come out in the servants' passage, breathing hard.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
id: piano-echo
|
||||||
|
startsIn: "[[music-room]]"
|
||||||
|
initialPhase: listening
|
||||||
|
---
|
||||||
|
|
||||||
|
## listening
|
||||||
|
The held piano key waits under your eye. A second note answers from a room you have not found.
|
||||||
|
|
||||||
|
## play-piano-echo-resolved
|
||||||
|
You play the waiting note.
|
||||||
|
|
||||||
|
The answer comes at once, nearer than before. A narrow part of the wall settles back into shadow.
|
||||||
|
|
||||||
|
## wrong-verb
|
||||||
|
The answering note repeats, patient and exact.
|
||||||
|
|
||||||
|
## failed
|
||||||
|
The wrong chord goes through the floorboards. You climb back to the hallway before the echo finishes.
|
||||||
@@ -3,7 +3,7 @@ id: rat
|
|||||||
startsIn: "[[cellar-stair]]"
|
startsIn: "[[cellar-stair]]"
|
||||||
initialPhase: lurking
|
initialPhase: lurking
|
||||||
---
|
---
|
||||||
|
![[Pasted image 20260509213136.png]]
|
||||||
## lurking
|
## lurking
|
||||||
A heavy rat watches you from the third step. Its eyes catch the light.
|
A heavy rat watches you from the third step. Its eyes catch the light.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
id: window-guest
|
||||||
|
startsIn: "[[dining-room]]"
|
||||||
|
initialPhase: standing-outside
|
||||||
|
---
|
||||||
|
|
||||||
|
## standing-outside
|
||||||
|
Rain touches the dining-room window from the wrong side. Someone stands beyond the glass with their head bowed.
|
||||||
|
|
||||||
|
## close-window-guest-resolved
|
||||||
|
You draw the curtains together before you look closely.
|
||||||
|
|
||||||
|
For a moment, cloth and rain hold the same shape. Then there is only the table behind you.
|
||||||
|
|
||||||
|
## wrong-verb
|
||||||
|
The figure outside lifts its face a little.
|
||||||
|
|
||||||
|
## failed
|
||||||
|
The glass shows too much. You find yourself back in the hallway with the taste of rain in your mouth.
|
||||||
@@ -82,7 +82,7 @@ You are carrying: a folded letter, a matchbook, and a broken cigarette.
|
|||||||
# Existing House Rooms
|
# Existing House Rooms
|
||||||
|
|
||||||
| id | title | summary |
|
| id | title | summary |
|
||||||
|---|---|---|
|
| -------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `outside-gate` | [ The Gate ] | The road behind you is gone. |
|
| `outside-gate` | [ The Gate ] | The road behind you is gone. |
|
||||||
| `foyer` | [ Foyer ] | A foyer that doesn't feel welcoming. Beneath the dust is another scent: sweet at first, then medicinal, like crushed almond pits. A hallway runs impossibly far to the north. |
|
| `foyer` | [ Foyer ] | A foyer that doesn't feel welcoming. Beneath the dust is another scent: sweet at first, then medicinal, like crushed almond pits. A hallway runs impossibly far to the north. |
|
||||||
| `hallway` | [ Hallway ] | A hallway longer than the house should allow. |
|
| `hallway` | [ Hallway ] | A hallway longer than the house should allow. |
|
||||||
@@ -159,7 +159,7 @@ Themes reinforced by the new rooms:
|
|||||||
These rooms appear only after certain flags are set.
|
These rooms appear only after certain flags are set.
|
||||||
|
|
||||||
| id | title | first-visit summary | exits | items | encounter | safe |
|
| id | title | first-visit summary | exits | items | encounter | safe |
|
||||||
|---|---|---|---|---|---|---|
|
| ------------------ | ------------- | ----------------------------------------------------- | ------------------------- | ---------- | --------------- | ---- |
|
||||||
| `wrong-hallway` | [ Hallway ] | The hallway is longer now. | impossible-changing exits | — | distant-steps | — |
|
| `wrong-hallway` | [ Hallway ] | The hallway is longer now. | impossible-changing exits | — | distant-steps | — |
|
||||||
| `returned-nursery` | [ Nursery ] | The toys no longer appear arranged. | w: bedroom | — | — | yes |
|
| `returned-nursery` | [ Nursery ] | The toys no longer appear arranged. | w: bedroom | — | — | yes |
|
||||||
| `rain-room` | [ Rain Room ] | Rain falls steadily inside the room and nowhere else. | unknown | rusted-key | rainwater-basin | — |
|
| `rain-room` | [ Rain Room ] | Rain falls steadily inside the room and nowhere else. | unknown | rusted-key | rainwater-basin | — |
|
||||||
+2
-2
@@ -121,8 +121,8 @@ for (const enc of Object.values(encounters)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const world: World = {
|
export const world: World = {
|
||||||
startingRoom: 'foyer',
|
startingRoom: 'outside-gate',
|
||||||
startingInventory: ['matches'],
|
startingInventory: ['letter', 'matches', 'broken-cigarette'],
|
||||||
rooms,
|
rooms,
|
||||||
items,
|
items,
|
||||||
encounters,
|
encounters,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
id: broken-cigarette
|
||||||
|
names: ["cigarette", "broken cigarette"]
|
||||||
|
short: "a broken cigarette"
|
||||||
|
takeable: true
|
||||||
|
lightable: true
|
||||||
|
initialState:
|
||||||
|
lit: false
|
||||||
|
---
|
||||||
|
|
||||||
|
A cigarette broken near the middle. The paper is creased where someone held it too tightly.
|
||||||
|
|
||||||
|
## lit
|
||||||
|
The end glows once, then steadies. The smoke is bitter.
|
||||||
|
|
||||||
|
## extinguished
|
||||||
|
You pinch out the ember. The smell remains.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
id: candlestick
|
||||||
|
names: ["candlestick", "candle holder", "candle"]
|
||||||
|
short: "a brass candlestick"
|
||||||
|
takeable: true
|
||||||
|
lightable: true
|
||||||
|
initialState:
|
||||||
|
lit: false
|
||||||
|
---
|
||||||
|
|
||||||
|
A brass candlestick, heavy at the base. The candle inside it is burned low but not spent.
|
||||||
|
|
||||||
|
## lit
|
||||||
|
The wick takes. The flame bends toward the nearest doorway.
|
||||||
|
|
||||||
|
## extinguished
|
||||||
|
The candle gutters out. For a moment the smoke leans against your hand.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
id: covered-cage
|
||||||
|
names: ["cage", "covered cage", "birdcage", "covered-cage"]
|
||||||
|
short: "a cloth-covered cage"
|
||||||
|
takeable: false
|
||||||
|
initialState: {}
|
||||||
|
---
|
||||||
|
|
||||||
|
A small cage beneath a dark cloth. Its wire frame is warm where your fingers touch it.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
id: damp-sheet
|
||||||
|
names: ["sheet", "damp sheet", "damp-sheet"]
|
||||||
|
short: "a damp sheet"
|
||||||
|
takeable: true
|
||||||
|
initialState: {}
|
||||||
|
---
|
||||||
|
|
||||||
|
A sheet still wet from some old washing. It smells of rainwater and closed rooms.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
id: dinner-place-setting
|
||||||
|
names: ["place setting", "plate", "setting", "dinner-place-setting"]
|
||||||
|
short: "an untouched place setting"
|
||||||
|
takeable: false
|
||||||
|
initialState: {}
|
||||||
|
---
|
||||||
|
|
||||||
|
The plate is untouched. The knife and fork have been set for someone left-handed.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
id: grandfather-clock
|
||||||
|
names: ["clock", "grandfather clock", "grandfather-clock"]
|
||||||
|
short: "a stopped grandfather clock"
|
||||||
|
takeable: false
|
||||||
|
initialState: {}
|
||||||
|
---
|
||||||
|
|
||||||
|
The clock has stopped between minutes. Its pendulum hangs a little crooked, as if it stopped while fleeing.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: matches
|
id: matches
|
||||||
names: ["matches", "safety matches", "box"]
|
names: ["matches", "safety matches", "matchbook", "box"]
|
||||||
short: "a box of safety matches"
|
short: "a matchbook"
|
||||||
takeable: true
|
takeable: true
|
||||||
lighter: true
|
lighter: true
|
||||||
lighterUses: 4
|
lighterUses: 4
|
||||||
@@ -9,7 +9,7 @@ initialState:
|
|||||||
uses: 4
|
uses: 4
|
||||||
---
|
---
|
||||||
|
|
||||||
A small cardboard box of safety matches. Half-full.
|
A damp matchbook with four matches left inside.
|
||||||
|
|
||||||
## lighter-empty
|
## lighter-empty
|
||||||
The last match flares, burns down, and goes out. The book is empty.
|
The last match flares, burns down, and goes out. The book is empty.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
id: music-box-key
|
||||||
|
names: ["tiny key", "music box key", "music-box-key", "key"]
|
||||||
|
short: "a tiny key"
|
||||||
|
takeable: true
|
||||||
|
initialState: {}
|
||||||
|
---
|
||||||
|
|
||||||
|
A tiny key on a black ribbon. It is too small for any door in the house.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
id: pruning-shears
|
||||||
|
names: ["shears", "pruning shears", "pruning-shears"]
|
||||||
|
short: "a pair of pruning shears"
|
||||||
|
takeable: true
|
||||||
|
initialState: {}
|
||||||
|
---
|
||||||
|
|
||||||
|
Iron pruning shears with dark soil in the hinge. The handles are cold through your palm.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
id: silver-lighter
|
||||||
|
names: ["lighter", "silver lighter", "silver-lighter"]
|
||||||
|
short: "a silver lighter"
|
||||||
|
takeable: true
|
||||||
|
lighter: true
|
||||||
|
initialState: {}
|
||||||
|
---
|
||||||
|
|
||||||
|
A silver lighter with a worn crest on one side. It opens with a small reluctant click.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
id: conservatory
|
||||||
|
title: "[ Conservatory ]"
|
||||||
|
exitN: null
|
||||||
|
exitS: "[[dining-room]]"
|
||||||
|
exitE: null
|
||||||
|
exitW: null
|
||||||
|
exitU: null
|
||||||
|
exitD: null
|
||||||
|
items:
|
||||||
|
- "[[pruning-shears]]"
|
||||||
|
encounter: "[[ivy-figure]]"
|
||||||
|
---
|
||||||
|
|
||||||
|
## first-visit
|
||||||
|
The conservatory roof has gone blind with moss and old rain. Vines press against the glass from both sides.
|
||||||
|
|
||||||
|
Something human-shaped hangs among the ivy. The dining room is south.
|
||||||
|
|
||||||
|
## revisit
|
||||||
|
The conservatory sweats in the dark.
|
||||||
|
|
||||||
|
## examined
|
||||||
|
The tiled floor is split by roots. The air smells of wet soil and cold iron. Pruning shears lie half-buried beneath a bench.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
id: dining-room
|
||||||
|
title: "[ Dining Room ]"
|
||||||
|
exitN: "[[conservatory]]"
|
||||||
|
exitS: null
|
||||||
|
exitE: null
|
||||||
|
exitW: "[[hallway]]"
|
||||||
|
exitU: null
|
||||||
|
exitD: null
|
||||||
|
items:
|
||||||
|
- "[[candlestick]]"
|
||||||
|
- "[[dinner-place-setting]]"
|
||||||
|
encounter: "[[window-guest]]"
|
||||||
|
---
|
||||||
|
|
||||||
|
## first-visit
|
||||||
|
The dining room is laid for supper long after supper ended. Plates dull the long table. One place has not been touched.
|
||||||
|
|
||||||
|
The hallway is west. A greenish light presses through glass to the north.
|
||||||
|
|
||||||
|
## revisit
|
||||||
|
The dining room holds its place at the table.
|
||||||
|
|
||||||
|
## examined
|
||||||
|
The silver has tarnished black at the edges. The untouched plate is clean enough to show a dim shape behind you, though there is no one there. A candlestick rests beside it.
|
||||||
@@ -2,22 +2,21 @@
|
|||||||
id: foyer
|
id: foyer
|
||||||
title: "[ Foyer ]"
|
title: "[ Foyer ]"
|
||||||
exitN: "[[hallway]]"
|
exitN: "[[hallway]]"
|
||||||
exitS: null
|
exitS: "[[outside-gate]]"
|
||||||
exitE: null
|
exitE: null
|
||||||
exitW: null
|
exitW: null
|
||||||
exitU: null
|
exitU: null
|
||||||
exitD: null
|
exitD: null
|
||||||
items:
|
items: []
|
||||||
- "[[letter]]"
|
|
||||||
encounter: null
|
encounter: null
|
||||||
safe: true
|
safe: true
|
||||||
---
|
---
|
||||||
|
|
||||||
## first-visit
|
## first-visit
|
||||||
You stand in the foyer of a house you do not remember entering. The door behind you has closed without sound. A folded letter lies on a small table. A hallway leads north.
|
You stand in the foyer of a house you do not remember entering. The door behind you has closed without sound. A hallway leads north.
|
||||||
|
|
||||||
## revisit
|
## revisit
|
||||||
The foyer. The door behind you is still closed.
|
The foyer. The door behind you is still closed.
|
||||||
|
|
||||||
## examined
|
## examined
|
||||||
A foyer with peeling paper. A small table holds nothing but the letter. The air smells of cold stone. A hallway leads north.
|
A foyer with peeling paper. The air smells of cold stone. A hallway leads north. The gate is south, though it feels less certain from this side.
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
---
|
---
|
||||||
id: hallway
|
id: hallway
|
||||||
title: "[ Hallway ]"
|
title: "[ Hallway ]"
|
||||||
exitN: null
|
exitN: "[[dining-room]]"
|
||||||
exitS: "[[foyer]]"
|
exitS: "[[foyer]]"
|
||||||
exitE: "[[cellar-stair]]"
|
exitE: "[[cellar-stair]]"
|
||||||
exitW: null
|
exitW: "[[smoking-room]]"
|
||||||
exitU: null
|
exitU: "[[parlor]]"
|
||||||
exitD: null
|
exitD: "[[music-room]]"
|
||||||
items:
|
items:
|
||||||
- "[[lamp]]"
|
- "[[lamp]]"
|
||||||
encounter: null
|
encounter: null
|
||||||
---
|
---
|
||||||
|
|
||||||
## first-visit
|
## first-visit
|
||||||
A long hallway, lit by nothing. An iron oil lamp sits on a side table. The foyer is south. A stair descends east.
|
A long hallway, lit by nothing. It runs further than the house should allow. An iron oil lamp sits on a side table. The foyer is south. A stair descends east.
|
||||||
|
|
||||||
## revisit
|
## revisit
|
||||||
The long hallway.
|
The long hallway. It has not shortened.
|
||||||
|
|
||||||
## examined
|
## examined
|
||||||
The hallway runs further than the house should be wide. The dust on the floor is undisturbed except where you have walked. The oil lamp is on the side table.
|
The hallway runs further than the house should be wide. The dust on the floor is undisturbed except where you have walked. The oil lamp is on the side table. Doors wait north and west. Two more thresholds sit where the wall should be solid.
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
id: laundry
|
||||||
|
title: "[ Laundry ]"
|
||||||
|
exitN: null
|
||||||
|
exitS: null
|
||||||
|
exitE: null
|
||||||
|
exitW: "[[servants-passage]]"
|
||||||
|
exitU: null
|
||||||
|
exitD: null
|
||||||
|
items:
|
||||||
|
- "[[damp-sheet]]"
|
||||||
|
encounter: "[[linen-shape]]"
|
||||||
|
safe: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## first-visit
|
||||||
|
Sheets hang unmoving in the still air. They divide the room into narrow aisles. One of them has the shape of a person behind it.
|
||||||
|
|
||||||
|
The servants' passage is west.
|
||||||
|
|
||||||
|
## revisit
|
||||||
|
The laundry hangs white and silent.
|
||||||
|
|
||||||
|
## examined
|
||||||
|
Water darkens the floor beneath the hanging sheets, though none of them drip. A damp sheet has fallen into an empty basket.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
id: music-room
|
||||||
|
title: "[ Music Room ]"
|
||||||
|
exitN: "[[servants-passage]]"
|
||||||
|
exitS: null
|
||||||
|
exitE: null
|
||||||
|
exitW: null
|
||||||
|
exitU: "[[hallway]]"
|
||||||
|
exitD: null
|
||||||
|
items:
|
||||||
|
- "[[music-box-key]]"
|
||||||
|
encounter: "[[piano-echo]]"
|
||||||
|
safe: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## first-visit
|
||||||
|
A piano stands open with one key held silently down. The strings inside are furred with dust.
|
||||||
|
|
||||||
|
Something answers from elsewhere in the house, too soft to be music. A servants' passage opens north. The hallway is above.
|
||||||
|
|
||||||
|
## revisit
|
||||||
|
The music room waits on the same note.
|
||||||
|
|
||||||
|
## examined
|
||||||
|
The depressed piano key does not rise when touched. A tiny key rests on the music stand where sheet music should be.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
id: outside-gate
|
||||||
|
title: "[ The Gate ]"
|
||||||
|
exitN: "[[foyer]]"
|
||||||
|
exitS: null
|
||||||
|
exitE: null
|
||||||
|
exitW: null
|
||||||
|
exitU: null
|
||||||
|
exitD: null
|
||||||
|
items: []
|
||||||
|
encounter: null
|
||||||
|
safe: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## first-visit
|
||||||
|
You arrive at the address, but you do not remember what has happened.
|
||||||
|
There is no sign, no number on the gate. The fence is overgrown with grape vines. The road behind you is gone.
|
||||||
|
|
||||||
|
The air is cool and still around you, but a quiet rustling and a damp breeze seem to emanate from beneath the house.
|
||||||
|
|
||||||
|
You are carrying: a folded letter, a matchbook, and a broken cigarette.
|
||||||
|
|
||||||
|
## revisit
|
||||||
|
The gate waits behind you. The road does not return.
|
||||||
|
|
||||||
|
## examined
|
||||||
|
The ironwork has gone black with rain. Grape vines pull at the fence and knot themselves through the bars. Beyond the gate, the house stands without light.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
id: parlor
|
||||||
|
title: "[ Parlor ]"
|
||||||
|
exitN: "[[study]]"
|
||||||
|
exitS: null
|
||||||
|
exitE: null
|
||||||
|
exitW: null
|
||||||
|
exitU: null
|
||||||
|
exitD: "[[hallway]]"
|
||||||
|
items:
|
||||||
|
- "[[grandfather-clock]]"
|
||||||
|
encounter: null
|
||||||
|
safe: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## first-visit
|
||||||
|
The parlor has been arranged for company. No chair faces another. Each one waits at a slight angle, as if the guests stood suddenly and left the room in silence.
|
||||||
|
|
||||||
|
A stopped clock stands against the north wall. A narrow study opens beyond it. The hallway is below.
|
||||||
|
|
||||||
|
## revisit
|
||||||
|
The parlor waits with its empty chairs.
|
||||||
|
|
||||||
|
## examined
|
||||||
|
Dust gathers on the chair arms, but not on the seats. Someone has sat here recently, or the room remembers being occupied. The grandfather clock shows no hour you can name.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: servants-passage
|
||||||
|
title: "[ Servants' Passage ]"
|
||||||
|
exitN: null
|
||||||
|
exitS: "[[music-room]]"
|
||||||
|
exitE: "[[laundry]]"
|
||||||
|
exitW: null
|
||||||
|
exitU: null
|
||||||
|
exitD: null
|
||||||
|
items: []
|
||||||
|
encounter: "[[breathing-wall]]"
|
||||||
|
---
|
||||||
|
|
||||||
|
## first-visit
|
||||||
|
The walls here are unfinished and smell of wet wood. The passage is too narrow for two people to pass.
|
||||||
|
|
||||||
|
The music room is south. A laundry lies east.
|
||||||
|
|
||||||
|
## revisit
|
||||||
|
The servants' passage breathes less loudly now, or you have learned when not to listen.
|
||||||
|
|
||||||
|
## examined
|
||||||
|
The boards show old finger marks in the grain. They run at shoulder height along both walls, as if someone felt their way through in the dark.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
id: smoking-room
|
||||||
|
title: "[ Smoking Room ]"
|
||||||
|
exitN: null
|
||||||
|
exitS: null
|
||||||
|
exitE: "[[hallway]]"
|
||||||
|
exitW: null
|
||||||
|
exitU: null
|
||||||
|
exitD: null
|
||||||
|
items:
|
||||||
|
- "[[silver-lighter]]"
|
||||||
|
- "[[covered-cage]]"
|
||||||
|
encounter: "[[covered-cage]]"
|
||||||
|
---
|
||||||
|
|
||||||
|
## first-visit
|
||||||
|
The smoking room smells faintly of ash, velvet, and bitter almonds. The chairs are turned toward the cold hearth.
|
||||||
|
|
||||||
|
A cloth-covered cage hangs from a stand. The hallway is east.
|
||||||
|
|
||||||
|
## revisit
|
||||||
|
The smoking room keeps its breath.
|
||||||
|
|
||||||
|
## examined
|
||||||
|
Cigar ash lies in a clean saucer. A silver lighter has been placed beside it with care. The covered cage moves once, though the room is still.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
id: study
|
||||||
|
title: "[ Study ]"
|
||||||
|
exitN: null
|
||||||
|
exitS: "[[parlor]]"
|
||||||
|
exitE: null
|
||||||
|
exitW: null
|
||||||
|
exitU: null
|
||||||
|
exitD: null
|
||||||
|
items: []
|
||||||
|
encounter: null
|
||||||
|
safe: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## first-visit
|
||||||
|
Books lie open across the desk and floor. Their pages are swollen with damp. Several have been opened past their final leaves.
|
||||||
|
|
||||||
|
The parlor is south.
|
||||||
|
|
||||||
|
## revisit
|
||||||
|
The study remains open to the wrong pages.
|
||||||
|
|
||||||
|
## examined
|
||||||
|
The handwriting in the margins changes from book to book, then becomes the same hand. Dates repeat. Names are crossed out and written again beneath themselves.
|
||||||
@@ -80,6 +80,8 @@ export interface EncounterTransition {
|
|||||||
|
|
||||||
export interface EncounterDef {
|
export interface EncounterDef {
|
||||||
id: EncounterId
|
id: EncounterId
|
||||||
|
/** Optional parser aliases for the encounter target while it is active. */
|
||||||
|
aliases?: string[]
|
||||||
startsIn: RoomId
|
startsIn: RoomId
|
||||||
initialPhase: EncounterPhase
|
initialPhase: EncounterPhase
|
||||||
phases: Record<EncounterPhase, EncounterPhaseDef>
|
phases: Record<EncounterPhase, EncounterPhaseDef>
|
||||||
|
|||||||
Reference in New Issue
Block a user