feat(mystery): add opening and main-floor content
This commit is contained in:
@@ -3,6 +3,14 @@ import type { GameState, ParsedCommand, DispatchResult, ItemInstance, Transcript
|
||||
import { SCHEMA_VERSION, TRANSCRIPT_CAP } from './types'
|
||||
import { applyVerbToEncounter, maybeTriggerEncounter } from './encounters'
|
||||
|
||||
const HALFSTREET_ASCII = String.raw`
|
||||
_ _ _ __ ____ _ _
|
||||
| | | | __ _| |/ _| / ___|| |_ _ __ ___ ___| |_
|
||||
| |_| |/ _\` | | |_ \___ \| __| '__/ _ \/ _ \ __|
|
||||
| _ | (_| | | _| ___) | |_| | | __/ __/ |_
|
||||
|_| |_|\__,_|_|_| |____/ \__|_| \___|\___|\__|
|
||||
`.trim()
|
||||
|
||||
export function initialStateFor(world: World): GameState {
|
||||
const startingRoom = world.rooms[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[] = [
|
||||
{ kind: 'system', text: HALFSTREET_ASCII },
|
||||
{ kind: 'system', text: startingRoom.title },
|
||||
{ kind: 'narration', text: startingRoom.descriptions.firstVisit },
|
||||
]
|
||||
@@ -134,6 +143,10 @@ export function dispatch(state: GameState, command: ParsedCommand, world: World)
|
||||
}
|
||||
|
||||
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 === 'inventory') return withEndingCheck(handleInventory(state, world), 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', () => {
|
||||
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', () => {
|
||||
const ctx: ParserContext = {
|
||||
knownItems: ['torch'],
|
||||
|
||||
@@ -32,6 +32,9 @@ const VERB_SYNONYMS: Record<string, Verb> = {
|
||||
hold: 'hold', show: 'hold',
|
||||
push: 'push', press: 'push',
|
||||
pull: 'pull',
|
||||
cut: 'cut', trim: 'cut',
|
||||
play: 'play',
|
||||
uncover: 'open',
|
||||
wait: 'wait', z: 'wait',
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,11 @@ function ctxFor(state: GameState): ParserContext {
|
||||
if (item) visibleNouns.push({ id: inst.id, aliases: item.names })
|
||||
}
|
||||
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 {
|
||||
knownItems: Object.keys(world.items),
|
||||
@@ -41,8 +45,8 @@ function play(commands: string[]): GameState {
|
||||
describe('playthrough — sample world', () => {
|
||||
it('reaches the rat-gone flag via the canonical command sequence', () => {
|
||||
const state = play([
|
||||
'take letter',
|
||||
'read letter', // verb is recognized but encounter takes priority elsewhere; here it's a no-op
|
||||
'n', // gate → foyer
|
||||
'n', // foyer → hallway
|
||||
'take lamp',
|
||||
'e', // hallway → cellar-stair (triggers rat encounter)
|
||||
@@ -55,11 +59,52 @@ describe('playthrough — sample world', () => {
|
||||
|
||||
it('handles invalid moves gracefully', () => {
|
||||
const state = play([
|
||||
'go up', // foyer has no up exit
|
||||
'go up', // gate has no up exit
|
||||
'n',
|
||||
's',
|
||||
'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 =
|
||||
| 'go' | 'look' | 'examine' | 'take' | 'drop' | 'use' | 'open' | 'close'
|
||||
| 'read' | 'light' | 'extinguish' | 'attack' | 'inventory' | 'wait'
|
||||
| 'hold' | 'push' | 'pull'
|
||||
| 'hold' | 'push' | 'pull' | 'cut' | 'play'
|
||||
|
||||
export type MetaVerb = 'restart' | 'undo' | 'hint' | 'save' | 'quit' | 'theme'
|
||||
|
||||
|
||||
@@ -32,6 +32,13 @@ import '../ui/crt.css'
|
||||
/>
|
||||
</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>
|
||||
<script>
|
||||
// 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', () => {
|
||||
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)
|
||||
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', () => {
|
||||
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)
|
||||
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', () => {
|
||||
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: 'e' }, world).state
|
||||
const chips = computeChips(s, world)
|
||||
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 chips = computeChips(s, world)
|
||||
expect(chips.find((c) => c.command === 'look')).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.
|
||||
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: 'HELP', command: 'help', disabled: false })
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
+38
-6
@@ -46,6 +46,25 @@
|
||||
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 {
|
||||
/* scanlines overlay */
|
||||
content: '';
|
||||
@@ -104,6 +123,14 @@
|
||||
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 {
|
||||
color: var(--m-accent-2);
|
||||
}
|
||||
@@ -140,13 +167,12 @@
|
||||
}
|
||||
|
||||
.mystery-chips {
|
||||
display: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 6px 0 4px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px var(--m-divider-style) var(--m-dim);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@@ -166,10 +192,6 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.mystery-chips { display: flex; }
|
||||
}
|
||||
|
||||
.mystery-transcript .ending {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1em;
|
||||
@@ -179,6 +201,16 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.mystery-root {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mystery-bezel {
|
||||
padding: 18px 14px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-mystery-input].ended {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
+47
-1
@@ -11,12 +11,32 @@ import { renderChips } from './chip-render'
|
||||
const transcriptEl = document.querySelector<HTMLDivElement>('[data-mystery-transcript]')
|
||||
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) {
|
||||
console.error('[halfstreet] terminal mount points missing')
|
||||
} else {
|
||||
const restored = loadState()
|
||||
let state: GameState = restored ?? initialStateFor(world)
|
||||
let lastState: GameState | null = null // for one-step undo
|
||||
let transientHelpEl: HTMLDivElement | null = null
|
||||
|
||||
if (!restored) {
|
||||
// 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 (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) {
|
||||
@@ -78,6 +102,23 @@ if (!transcriptEl || !inputEl) {
|
||||
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
|
||||
// notices). Pushes into state.transcript so they survive reload, then renders.
|
||||
// Engine-originated lines (from dispatch) are already in state.transcript;
|
||||
@@ -98,6 +139,7 @@ if (!transcriptEl || !inputEl) {
|
||||
const raw = inputEl.value
|
||||
inputEl.value = ''
|
||||
if (!raw.trim()) return
|
||||
clearTransientHelp()
|
||||
appendLines([{ kind: 'player', text: raw }])
|
||||
|
||||
// Once the game has ended, only restart and undo are allowed.
|
||||
@@ -126,6 +168,10 @@ if (!transcriptEl || !inputEl) {
|
||||
syncEndedUI()
|
||||
return
|
||||
}
|
||||
if (trimmed === 'help') {
|
||||
renderTransientHelp()
|
||||
return
|
||||
}
|
||||
if (trimmed === 'undo') {
|
||||
if (lastState) {
|
||||
state = lastState
|
||||
|
||||
Vendored
+2
-1
@@ -2,5 +2,6 @@
|
||||
"alwaysUpdateLinks": true,
|
||||
"newLinkFormat": "shortest",
|
||||
"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,
|
||||
"showOrphans": true,
|
||||
"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,
|
||||
"showArrow": false,
|
||||
"textFadeMultiplier": 0,
|
||||
"nodeSizeMultiplier": 1.2,
|
||||
"lineSizeMultiplier": 1,
|
||||
"nodeSizeMultiplier": 1.82265625,
|
||||
"lineSizeMultiplier": 5,
|
||||
"collapse-forces": false,
|
||||
"centerStrength": 0.518713248970312,
|
||||
"repelStrength": 10,
|
||||
"linkStrength": 1,
|
||||
"linkDistance": 250,
|
||||
"scale": 2.25,
|
||||
"scale": 0.8332185563593782,
|
||||
"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'
|
||||
|
||||
describe('assembled world', () => {
|
||||
it('contains all three rooms', () => {
|
||||
expect(Object.keys(world.rooms).sort()).toEqual(['cellar-stair', 'foyer', 'hallway'])
|
||||
it('contains the authored opening and main-floor rooms', () => {
|
||||
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', () => {
|
||||
expect(Object.keys(world.items).sort()).toEqual(['lamp', 'letter', 'matches'])
|
||||
it('contains the authored opening and main-floor items', () => {
|
||||
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', () => {
|
||||
|
||||
@@ -27,4 +27,142 @@ export const encounters: Record<string, EncounterDef> = {
|
||||
onResolved: { setFlags: { ratGone: true } },
|
||||
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]]"
|
||||
initialPhase: lurking
|
||||
---
|
||||
|
||||
![[Pasted image 20260509213136.png]]
|
||||
## lurking
|
||||
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.
|
||||
@@ -0,0 +1,454 @@
|
||||
---
|
||||
test: "[[halfstreet-followon-notes]]"
|
||||
---
|
||||
# Halfstreet — Content Bible
|
||||
|
||||
**For:** the mystery text adventure shipped at `/mystery`
|
||||
|
||||
**Style anchors:**
|
||||
|
||||
- Le Fanu's _Carmilla_
|
||||
|
||||
- Shirley Jackson's _The Haunting of Hill House_
|
||||
|
||||
- M.R. James ghost stories
|
||||
|
||||
|
||||
**Perspective:** second person, present tense
|
||||
|
||||
**Target length:** 25–28 rooms, 45–90 minute first playthrough
|
||||
|
||||
---
|
||||
|
||||
# Voice Rules
|
||||
|
||||
1. Second person, present tense.
|
||||
|
||||
2. Sentences are short; silence does most of the work.
|
||||
|
||||
3. Never explain the supernatural.
|
||||
|
||||
4. Never address the player as a player.
|
||||
|
||||
5. No metafiction.
|
||||
|
||||
6. No named villains.
|
||||
|
||||
7. Violence usually worsens outcomes.
|
||||
|
||||
8. Many frightening things should prove harmless.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Core Themes
|
||||
|
||||
- mistaken identity
|
||||
|
||||
- unresolved grief
|
||||
|
||||
- memory behaving incorrectly
|
||||
|
||||
- repetition
|
||||
|
||||
- domestic spaces becoming ritual spaces
|
||||
|
||||
- familiar objects recurring in altered forms
|
||||
|
||||
- the house remembering itself imperfectly
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Opening Scene
|
||||
|
||||
```txt
|
||||
[ The Gate ]
|
||||
|
||||
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.
|
||||
|
||||
>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Existing House Rooms
|
||||
|
||||
| id | title | summary |
|
||||
| -------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
| `hallway` | [ Hallway ] | A hallway longer than the house should allow. |
|
||||
| `parlor` | [ Parlor ] | Chairs arranged for absent company. |
|
||||
| `study` | [ Study ] | Books left open at impossible pages. |
|
||||
| `stair-up` | [ Upper Stair ] | A stair arriving at the wrong landing. |
|
||||
| `bedroom` | [ Bedroom ] | Prepared for another sleeper. |
|
||||
| `nursery` | [ Nursery ] | Toys arranged tonight. |
|
||||
| `kitchen` | [ Kitchen ] | Something recently warm. |
|
||||
| `back-door` | [ Back Door ] | Opens onto the grounds. |
|
||||
| `garden` | [ Garden ] | Overgrown and listening. |
|
||||
| `well` | [ The Well ] | Dry deeper than it should be. |
|
||||
| `well-shaft` | [ Well Shaft ] | Descending below the water line. |
|
||||
| `tunnel` | [ Tunnel ] | A tunnel aware of your presence. |
|
||||
| `chamber` | [ Antechamber ] | Locked threshold before the vault. |
|
||||
| `vault` | [ Vault ] | Holds what was buried at Halfstreet. |
|
||||
| `chapel` | [ Chapel ] | Deconsecrated and occupied. |
|
||||
| `attic` | [ Attic ] | Reached by a staircase that was not there before. |
|
||||
| `cistern` | [ Cistern ] | Beneath the kitchen. |
|
||||
|
||||
---
|
||||
|
||||
## Structural Revision
|
||||
|
||||
The house should begin behaving differently after several story flags are set. Certain rooms subtly alter on revisit, some exits change, and a secondary loop through the house opens. The player should slowly realize the house is less a physical location than an accumulation of grief, repetition, and mistaken memory.
|
||||
|
||||
Themes reinforced by the new rooms:
|
||||
|
||||
- mistaken identity
|
||||
|
||||
- repetition
|
||||
|
||||
- domestic memory
|
||||
|
||||
- grief fossilized into architecture
|
||||
|
||||
- things mistaken for people
|
||||
|
||||
- familiar objects recurring in different forms
|
||||
|
||||
- the sense that the player may have belonged here once
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Additional Rooms
|
||||
|
||||
## Main House Expansion
|
||||
|
||||
|id|title|first-visit summary|exits|items|encounter|safe|
|
||||
|---|---|---|---|---|---|---|
|
||||
|`dining-room`|[ Dining Room ]|A dining room laid for supper long after supper ended.|w: hallway, n: conservatory|candlestick|window-guest|—|
|
||||
|`conservatory`|[ Conservatory ]|The glass ceiling has gone blind with moss and rain.|s: dining-room, e: garden|pruning-shears|ivy-figure|—|
|
||||
|`smoking-room`|[ Smoking Room ]|The room smells faintly of ash, velvet, and bitter almonds.|e: hallway|silver-lighter|covered-cage|—|
|
||||
|`music-room`|[ Music Room ]|A piano stands open with one key held silently down.|w: hallway, n: servants-passage|music-box-key|piano-echo|—|
|
||||
|`servants-passage`|[ Servants' Passage ]|The walls here are unfinished and smell of wet wood.|s: music-room, e: laundry|—|breathing-wall|—|
|
||||
|`laundry`|[ Laundry ]|Sheets hang unmoving in the still air.|w: servants-passage|damp-sheet|linen-shape|—|
|
||||
|
||||
---
|
||||
|
||||
## Lower-Level Expansion
|
||||
|
||||
|id|title|first-visit summary|exits|items|encounter|safe|
|
||||
|---|---|---|---|---|---|---|
|
||||
|`ossuary`|[ Ossuary ]|The bones have been arranged with more care than devotion.|s: tunnel, e: flooded-passage|burial-ring|bone-keeper|—|
|
||||
|`flooded-passage`|[ Flooded Passage ]|Black water moves softly across the stone floor.|w: ossuary, n: root-chamber|toy-boat|reflection|—|
|
||||
|`root-chamber`|[ Root Chamber ]|Roots have entered through the ceiling and never stopped growing.|s: flooded-passage, e: burial-gallery|—|root-movement|—|
|
||||
|`burial-gallery`|[ Burial Gallery ]|Portraits line the walls, though none survive intact.|w: root-chamber, e: vault|family-register|portrait-woman|—|
|
||||
|
||||
---
|
||||
|
||||
## Altered / Conditional Rooms
|
||||
|
||||
These rooms appear only after certain flags are set.
|
||||
|
||||
| id | title | first-visit summary | exits | items | encounter | safe |
|
||||
| ------------------ | ------------- | ----------------------------------------------------- | ------------------------- | ---------- | --------------- | ---- |
|
||||
| `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 |
|
||||
| `rain-room` | [ Rain Room ] | Rain falls steadily inside the room and nowhere else. | unknown | rusted-key | rainwater-basin | — |
|
||||
|
||||
---
|
||||
|
||||
# Core House Rooms Revisions
|
||||
|
||||
|id|revised summary|
|
||||
|---|---|
|
||||
|`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.|
|
||||
|
||||
---
|
||||
|
||||
# Inventory Items
|
||||
|
||||
|id|names|purpose|state|
|
||||
|---|---|---|---|
|
||||
|`candlestick`|candlestick, candle holder|Provides temporary light without using the lamp.|`lit: false`|
|
||||
|`pruning-shears`|shears, pruning shears|Cuts through overgrowth in the conservatory and garden.|—|
|
||||
|`silver-lighter`|lighter, silver lighter|Can light the broken cigarette or lamp.|—|
|
||||
|`music-box-key`|tiny key, music box key|Unlocks the music box in the nursery.|—|
|
||||
|`damp-sheet`|sheet, damp sheet|Used to obscure reflections in flooded areas.|—|
|
||||
|`burial-ring`|ring, burial ring|Matches the crest found in the family register.|—|
|
||||
|`toy-boat`|toy boat, wooden boat|Floats briefly against the current.|—|
|
||||
|`family-register`|register, ledger|Reveals names, dates, and repeated burials tied to Halfstreet.|—|
|
||||
|`rusted-key`|rusted key|Opens an optional locked room near the gate.|—|
|
||||
|
||||
---
|
||||
|
||||
# Non-Key Items
|
||||
|
||||
Non-key items are objects the player meaningfully interacts with but cannot permanently carry.
|
||||
|
||||
These objects should feel physically inconvenient, emotionally difficult, fragile, attached to the house itself, or impossible to remove.
|
||||
|
||||
|id|description|interaction|
|
||||
|---|---|---|
|
||||
|`grandfather-clock`|A stopped clock in the parlor whose pendulum occasionally resumes on its own.|Setting the correct time alters hallway geometry temporarily.|
|
||||
|`music-box`|A nursery music box bolted to the shelf.|Unlocking and winding it reveals hidden sounds elsewhere in the house.|
|
||||
|`dinner-place-setting`|A single untouched plate in the dining room.|Sitting before it triggers memory prose and a hidden passage.|
|
||||
|`veil`|A funeral veil draped over a portrait in the burial gallery.|Lifting it reveals the player's resemblance to the family.|
|
||||
|`rainwater-basin`|A basin collecting rain inside the rain room.|Looking into it reveals altered room descriptions.|
|
||||
|`covered-cage`|A cloth-covered birdcage in the smoking room.|Removing the cloth changes ambient sounds throughout the house.|
|
||||
|`altar-stone`|A cracked altar slab in the chapel.|Pouring the silver vial here weakens the basilisk without violence.|
|
||||
|`childs-drawing`|A crayon drawing pinned to the nursery wall.|Reveals the existence of the wrong hallway before it appears.|
|
||||
|`portrait-frame`|A shattered portrait frame underground.|Rotating it exposes hidden writing beneath the backing.|
|
||||
|`well-rope-wheel`|The old iron wheel beside the well.|Turning it changes what is heard below the house.|
|
||||
|
||||
---
|
||||
|
||||
# Revised Encounter Notes
|
||||
|
||||
## The Child Beneath the Well
|
||||
|
||||
If the player attempts to descend the well for the first time:
|
||||
|
||||
> Something moves below before you do.
|
||||
>
|
||||
> A child emerges from the tunnel beneath the well, barefoot and breathless, one hand against the stone wall as it climbs past you. It does not stop. A draft of cold air follows after it.
|
||||
>
|
||||
> Then it is gone upward toward the garden.
|
||||
|
||||
If carrying the toy dog:
|
||||
|
||||
> The child pauses at the garden gate.
|
||||
>
|
||||
> “You found Woof.”
|
||||
>
|
||||
> Or perhaps: “Wolf.”
|
||||
>
|
||||
> The child takes the toy carefully and disappears into the overgrowth.
|
||||
>
|
||||
> The garden grows quieter afterward.
|
||||
|
||||
The child is harmless.
|
||||
|
||||
---
|
||||
|
||||
## Basilisk Encounter Revision
|
||||
|
||||
Replace the chapel-watcher entirely.
|
||||
|
||||
|id|room|initial phase|resolution path|failure path|
|
||||
|---|---|---|---|---|
|
||||
|`basilisk`|chapel|sleeping|`pour silver-vial on basilisk` → resolved. The creature withdraws beneath the chapel stones.|direct eye contact costs resolve; forced exit|
|
||||
|
||||
Suggested prose:
|
||||
|
||||
> Something large is coiled beneath the altar.
|
||||
>
|
||||
> You become aware of the eye first.
|
||||
>
|
||||
> Not glowing. Merely open.
|
||||
|
||||
---
|
||||
|
||||
# Expanded Encounters
|
||||
|
||||
The expanded house should contain more quiet encounters than overtly hostile ones.
|
||||
|
||||
|id|room|initial phase|resolution path|failure path|
|
||||
|---|---|---|---|---|
|
||||
|`ivy-figure`|conservatory|hidden|`cut vines with shears` → reveals garden shortcut|entangled; lose resolve|
|
||||
|`piano-echo`|music-room|listening|`play note` matching distant melody → hidden passage opens|wrong notes cost resolve|
|
||||
|`linen-shape`|laundry|hanging|`wait` → shape proves empty|panic returns player to passage|
|
||||
|`bone-keeper`|ossuary|arranging|`leave burial-ring` → passage opens|disturbing bones costs resolve|
|
||||
|`reflection`|flooded-passage|following|obscure water with damp-sheet|reflected figure approaches|
|
||||
|`portrait-woman`|burial-gallery|watching|`examine portraits` reveals hidden family resemblance|retreat|
|
||||
|`distant-steps`|wrong-hallway|approaching|stand still until steps pass|hallway lengthens|
|
||||
|`garden-procession`|garden|passing|remain silent while lantern lights move beyond hedges|encounter follows player indoors|
|
||||
|`bell-sounder`|chapel|ringing|follow the sound without directly approaching|resolve loss|
|
||||
|`stair-sleeper`|upper stair|seated|leave without speaking|staircase changes|
|
||||
|`window-guest`|dining-room|standing outside|close curtains before approaching glass|face appears at window|
|
||||
|`breathing-wall`|servants-passage|audible|remain still until the sound passes|walls feel narrower|
|
||||
|`root-movement`|root-chamber|shifting|extinguish light and listen|roots block exits temporarily|
|
||||
|`rainwater-basin`|rain-room|reflecting|look into basin after major story flags|altered room descriptions persist|
|
||||
|`covered-cage`|smoking-room|rustling|uncover cage briefly|distant sounds intensify|
|
||||
|
||||
---
|
||||
|
||||
# Toy Dog
|
||||
|
||||
short: a stuffed dog
|
||||
|
||||
long:
|
||||
|
||||
The toy dog is several shades darker than it used to be.
|
||||
His fur is matted down in areas and worn from years of handling.
|
||||
|
||||
A name is stitched into one paw in faded blue thread:
|
||||
|
||||
WOOF
|
||||
|
||||
Or perhaps:
|
||||
|
||||
WOLF
|
||||
|
||||
The stitching has begun to loosen.
|
||||
|
||||
---
|
||||
|
||||
# Expanded Story Flags
|
||||
|
||||
- `woofReturned`
|
||||
|
||||
- `hallwayShifted`
|
||||
|
||||
- `musicSolved`
|
||||
|
||||
- `basiliskSpared`
|
||||
|
||||
- `familyNameLearned`
|
||||
|
||||
- `rainRoomEntered`
|
||||
|
||||
- `houseAcceptedYou`
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Expanded Endings
|
||||
|
||||
## True Ending
|
||||
|
||||
> You stand in the vault. What is buried at Halfstreet is buried because it was loved, and grieved, and finally let go.
|
||||
>
|
||||
> You set the lamp beside it.
|
||||
>
|
||||
> You speak the name aloud.
|
||||
>
|
||||
> The house settles around you like a long exhalation.
|
||||
>
|
||||
> Outside, the road exists again.
|
||||
|
||||
---
|
||||
|
||||
## Wrong Ending
|
||||
|
||||
> You disturb what should have remained untouched.
|
||||
>
|
||||
> The house rearranges itself around your leaving.
|
||||
>
|
||||
> Corridors repeat.
|
||||
>
|
||||
> Doors return you to themselves.
|
||||
>
|
||||
> The hallway grows longer.
|
||||
|
||||
---
|
||||
|
||||
## Bad Ending
|
||||
|
||||
> You take the photograph.
|
||||
>
|
||||
> The child in it is you.
|
||||
>
|
||||
> Behind you, on the stairs, someone has come up to meet you.
|
||||
>
|
||||
> You do not go down again.
|
||||
|
||||
---
|
||||
|
||||
## Replacement Ending
|
||||
|
||||
Unlocked if the player restores too much order to the house, lingers too long, or repeatedly revisits altered rooms.
|
||||
|
||||
> You open the front door.
|
||||
>
|
||||
> Someone is standing at the gate outside.
|
||||
>
|
||||
> They look tired. Rain-darkened. Lost.
|
||||
>
|
||||
> After a moment, you understand they have arrived for you.
|
||||
|
||||
---
|
||||
|
||||
## Mercy Ending
|
||||
|
||||
Unlocked if:
|
||||
|
||||
- Woof is returned
|
||||
|
||||
- the basilisk is spared
|
||||
|
||||
- the thing is not disturbed
|
||||
|
||||
- all burial clues are learned
|
||||
|
||||
- the revenant is laid to rest
|
||||
|
||||
|
||||
> You remain in the vault until the lamp goes out.
|
||||
>
|
||||
> The house is quiet.
|
||||
>
|
||||
> Somewhere above, floorboards settle one final time.
|
||||
>
|
||||
> By morning, Halfstreet contains one fewer restless thing.
|
||||
|
||||
---
|
||||
|
||||
# Spatial Notes
|
||||
|
||||
The house should increasingly violate geography.
|
||||
|
||||
Examples:
|
||||
|
||||
- The servants' passage should emerge somewhere impossible.
|
||||
|
||||
- The wrong hallway can replace the normal hallway after enough resolve loss.
|
||||
|
||||
- The rain room should not fit within the visible structure of the house.
|
||||
|
||||
- The burial gallery should feel emotionally adjacent to the dining room despite being underground.
|
||||
|
||||
- Certain rooms should inherit sounds from distant locations.
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
- nursery laughter faintly audible in the flooded passage
|
||||
|
||||
- piano note heard from the attic
|
||||
|
||||
- chapel bell heard underwater
|
||||
|
||||
- footsteps overhead while underground
|
||||
|
||||
|
||||
The player should slowly suspect the house is remembering itself incorrectly.
|
||||
|
||||
---
|
||||
|
||||
# Additional Tone Guidance
|
||||
|
||||
The horror works best when:
|
||||
|
||||
- encounters are quiet
|
||||
|
||||
- threats are ambiguous
|
||||
|
||||
- many frightening things prove harmless
|
||||
|
||||
- harmless things prove emotionally devastating
|
||||
|
||||
- monsters are treated with restraint
|
||||
|
||||
- violence worsens outcomes
|
||||
|
||||
|
||||
The game should feel less like surviving a haunted house and more like wandering through unresolved mourning.
|
||||
+2
-2
@@ -121,8 +121,8 @@ for (const enc of Object.values(encounters)) {
|
||||
}
|
||||
|
||||
export const world: World = {
|
||||
startingRoom: 'foyer',
|
||||
startingInventory: ['matches'],
|
||||
startingRoom: 'outside-gate',
|
||||
startingInventory: ['letter', 'matches', 'broken-cigarette'],
|
||||
rooms,
|
||||
items,
|
||||
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
|
||||
names: ["matches", "safety matches", "box"]
|
||||
short: "a box of safety matches"
|
||||
names: ["matches", "safety matches", "matchbook", "box"]
|
||||
short: "a matchbook"
|
||||
takeable: true
|
||||
lighter: true
|
||||
lighterUses: 4
|
||||
@@ -9,7 +9,7 @@ initialState:
|
||||
uses: 4
|
||||
---
|
||||
|
||||
A small cardboard box of safety matches. Half-full.
|
||||
A damp matchbook with four matches left inside.
|
||||
|
||||
## lighter-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
|
||||
title: "[ Foyer ]"
|
||||
exitN: "[[hallway]]"
|
||||
exitS: null
|
||||
exitS: "[[outside-gate]]"
|
||||
exitE: null
|
||||
exitW: null
|
||||
exitU: null
|
||||
exitD: null
|
||||
items:
|
||||
- "[[letter]]"
|
||||
items: []
|
||||
encounter: null
|
||||
safe: true
|
||||
---
|
||||
|
||||
## 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
|
||||
The foyer. The door behind you is still closed.
|
||||
|
||||
## 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
|
||||
title: "[ Hallway ]"
|
||||
exitN: null
|
||||
exitN: "[[dining-room]]"
|
||||
exitS: "[[foyer]]"
|
||||
exitE: "[[cellar-stair]]"
|
||||
exitW: null
|
||||
exitU: null
|
||||
exitD: null
|
||||
exitW: "[[smoking-room]]"
|
||||
exitU: "[[parlor]]"
|
||||
exitD: "[[music-room]]"
|
||||
items:
|
||||
- "[[lamp]]"
|
||||
encounter: null
|
||||
---
|
||||
|
||||
## 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
|
||||
The long hallway.
|
||||
The long hallway. It has not shortened.
|
||||
|
||||
## 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 {
|
||||
id: EncounterId
|
||||
/** Optional parser aliases for the encounter target while it is active. */
|
||||
aliases?: string[]
|
||||
startsIn: RoomId
|
||||
initialPhase: EncounterPhase
|
||||
phases: Record<EncounterPhase, EncounterPhaseDef>
|
||||
|
||||
Reference in New Issue
Block a user