diff --git a/src/pages/mystery.astro b/src/pages/mystery.astro index 3af1f10..236b8f5 100644 --- a/src/pages/mystery.astro +++ b/src/pages/mystery.astro @@ -43,6 +43,7 @@ import '../mystery/ui/crt.css' diff --git a/src/ui/terminal.ts b/src/ui/terminal.ts index bf54265..b10a30c 100644 --- a/src/ui/terminal.ts +++ b/src/ui/terminal.ts @@ -23,6 +23,47 @@ if (!transcriptEl || !inputEl) { state = initialStateFor(world) } + const buildParserContext = (s: GameState): ParserContext => { + const room = world.rooms[s.location] + const visibleNouns: { id: string; aliases: string[] }[] = [] + if (room) { + for (const id of room.items) { + const it = world.items[id] + if (it) visibleNouns.push({ id, aliases: it.names }) + } + if (room.encounter && s.encounterState[room.encounter]) { + visibleNouns.push({ id: room.encounter, aliases: [room.encounter] }) + } + } + for (const inst of s.inventory) { + const it = world.items[inst.id] + if (it) visibleNouns.push({ id: inst.id, aliases: it.names }) + } + return { + knownItems: Object.keys(world.items), + knownEncounters: Object.keys(world.encounters), + visibleNouns, + inventoryItemIds: s.inventory.map((i) => i.id), + lastNoun: s.lastNoun, + awaitingDisambiguation: s.pendingDisambiguation, + } + } + + const renderAll = (lines: TranscriptLine[]): void => { + if (!transcriptEl) return + for (const line of lines) { + const el = document.createElement('div') + el.className = line.kind + el.textContent = line.text + transcriptEl.appendChild(el) + } + transcriptEl.scrollTop = transcriptEl.scrollHeight + } + + const appendLines = (lines: TranscriptLine[]): void => { + renderAll(lines) + } + renderAll(state.transcript) inputEl.focus() @@ -76,6 +117,9 @@ if (!transcriptEl || !inputEl) { appendLines(result.appended) saveState(state) transcriptEl.scrollTop = transcriptEl.scrollHeight + if (raw.trim().toLowerCase() === 'theme') { + document.dispatchEvent(new CustomEvent('halfstreet-toggle-theme')) + } } catch (err) { console.error('[halfstreet] dispatch error', err) appendLines([{ kind: 'system', text: '[ The terminal hums and resets. ]' }]) @@ -88,45 +132,4 @@ if (!transcriptEl || !inputEl) { window.location.href = '/' } }) - - function buildParserContext(s: GameState): ParserContext { - const room = world.rooms[s.location] - const visibleNouns: { id: string; aliases: string[] }[] = [] - if (room) { - for (const id of room.items) { - const it = world.items[id] - if (it) visibleNouns.push({ id, aliases: it.names }) - } - if (room.encounter && s.encounterState[room.encounter]) { - visibleNouns.push({ id: room.encounter, aliases: [room.encounter] }) - } - } - for (const inst of s.inventory) { - const it = world.items[inst.id] - if (it) visibleNouns.push({ id: inst.id, aliases: it.names }) - } - return { - knownItems: Object.keys(world.items), - knownEncounters: Object.keys(world.encounters), - visibleNouns, - inventoryItemIds: s.inventory.map((i) => i.id), - lastNoun: s.lastNoun, - awaitingDisambiguation: s.pendingDisambiguation, - } - } - - function renderAll(lines: TranscriptLine[]): void { - if (!transcriptEl) return - for (const line of lines) { - const el = document.createElement('div') - el.className = line.kind - el.textContent = line.text - transcriptEl.appendChild(el) - } - transcriptEl.scrollTop = transcriptEl.scrollHeight - } - - function appendLines(lines: TranscriptLine[]): void { - renderAll(lines) - } } diff --git a/src/ui/theme.ts b/src/ui/theme.ts new file mode 100644 index 0000000..57a27f3 --- /dev/null +++ b/src/ui/theme.ts @@ -0,0 +1,42 @@ +const STORAGE_KEY = 'halfstreet:theme:v1' + +type Theme = 'amber' | 'ansi' + +function getStored(): Theme { + try { + return (localStorage.getItem(STORAGE_KEY) as Theme | null) === 'ansi' ? 'ansi' : 'amber' + } catch { + return 'amber' + } +} + +function setTheme(theme: Theme): void { + document.documentElement.setAttribute('data-mystery-theme', theme) + try { + localStorage.setItem(STORAGE_KEY, theme) + } catch { + // ignore + } + for (const btn of document.querySelectorAll('[data-theme-choice]')) { + btn.setAttribute('aria-pressed', btn.dataset['themeChoice'] === theme ? 'true' : 'false') + } +} + +const initial = getStored() +setTheme(initial) + +document.querySelectorAll('[data-theme-choice]').forEach((btn) => { + btn.addEventListener('click', () => { + const next = (btn.dataset['themeChoice'] as Theme | undefined) ?? 'amber' + setTheme(next) + }) +}) + +// Allow the engine's `theme` meta-command (handled in terminal.ts) to flip +// without going through the button by listening for a custom event. +document.addEventListener('halfstreet-toggle-theme', () => { + const current = (document.documentElement.getAttribute('data-mystery-theme') as Theme | null) ?? 'amber' + setTheme(current === 'amber' ? 'ansi' : 'amber') +}) + +export {}