From e167979fa71e6321ba34921806aee339551fd1ab Mon Sep 17 00:00:00 2001 From: Ethan J Lewis Date: Sat, 9 May 2026 14:57:52 -0500 Subject: [PATCH] feat(ui): render ending lines distinctly and lock input on end-state Ending-kind lines get a separator and italic styling. Once endedWith is set, the terminal disables the input and rejects all commands except restart and undo. Co-Authored-By: Claude Opus 4.7 --- src/ui/crt.css | 14 ++++++++++++++ src/ui/terminal.ts | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/ui/crt.css b/src/ui/crt.css index d6c5159..4c06e91 100644 --- a/src/ui/crt.css +++ b/src/ui/crt.css @@ -169,3 +169,17 @@ @media (pointer: coarse) { .mystery-chips { display: flex; } } + +.mystery-transcript .ending { + margin-top: 2em; + margin-bottom: 1em; + padding-top: 1em; + border-top: 1px solid currentColor; + font-style: italic; + white-space: pre-wrap; +} + +[data-mystery-input]:disabled { + opacity: 0.4; + cursor: not-allowed; +} diff --git a/src/ui/terminal.ts b/src/ui/terminal.ts index 0716c1c..349828a 100644 --- a/src/ui/terminal.ts +++ b/src/ui/terminal.ts @@ -33,6 +33,10 @@ if (!transcriptEl || !inputEl) { }) } + const syncEndedUI = (): void => { + inputEl!.disabled = state.endedWith !== null + } + const buildParserContext = (s: GameState): ParserContext => { const room = world.rooms[s.location] const visibleNouns: { id: string; aliases: string[] }[] = [] @@ -81,6 +85,7 @@ if (!transcriptEl || !inputEl) { renderAll(state.transcript) refreshChips() + syncEndedUI() inputEl.focus() inputEl.addEventListener('keydown', (e) => { @@ -91,6 +96,15 @@ if (!transcriptEl || !inputEl) { if (!raw.trim()) return appendLines([{ kind: 'player', text: raw }]) + // Once the game has ended, only restart and undo are allowed. + if (state.endedWith !== null) { + const lower = raw.trim().toLowerCase() + if (lower !== 'restart' && lower !== 'undo') { + appendLines([{ kind: 'system', text: 'The story has ended. Type `restart` or `undo`.' }]) + return + } + } + // Engine-level meta-commands handled here so the engine stays pure. const trimmed = raw.trim().toLowerCase() if (trimmed === 'restart') { @@ -105,6 +119,7 @@ if (!transcriptEl || !inputEl) { renderAll(state.transcript) saveState(state) refreshChips() + syncEndedUI() return } if (trimmed === 'undo') { @@ -114,6 +129,7 @@ if (!transcriptEl || !inputEl) { appendLines([{ kind: 'system', text: '(undone)' }]) saveState(state) refreshChips() + syncEndedUI() } else { appendLines([{ kind: 'system', text: 'There is no further back.' }]) } @@ -139,6 +155,7 @@ if (!transcriptEl || !inputEl) { document.dispatchEvent(new CustomEvent('halfstreet-toggle-theme')) } refreshChips() + syncEndedUI() } catch (err) { console.error('[halfstreet] dispatch error', err) appendLines([{ kind: 'system', text: '[ The terminal hums and resets. ]' }])