feat(mystery): add opening and main-floor content
This commit is contained in:
+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
|
||||
|
||||
Reference in New Issue
Block a user