// Help & Settings overlay (integration addition, post-playtest). // Press H (or the corner badge) to toggle. First run auto-opens the HOW TO // PLAY tab. Settings persist to localStorage and apply live via onApply. export interface Settings { volume: number; // 0..1 master sensitivity: number; // multiplier on the 0.0022 rad/px base viewBob: boolean; quality: 'sharp' | 'balanced' | 'fast'; // pixelRatio cap 2 / 1.5 / 1 } export const QUALITY_PIXEL_RATIO: Record = { sharp: 2, balanced: 1.5, fast: 1, }; const DEFAULTS: Settings = { volume: 0.85, sensitivity: 1, viewBob: false, quality: 'sharp' }; const LS_SETTINGS = 'turncraft.settings'; const LS_HELP_SEEN = 'turncraft.helpSeen'; export interface MenuOpts { onApply(s: Settings): void; onOpen?(): void; // host: exit pointer lock, suppress the pause overlay onClose?(): void; // host: re-acquire pointer lock if the game has started } const HELP_HTML = `

WHAT IS THIS

You are 7 mm tall inside a DJ booth, and the booth is dead. Five things in the signal chain are broken. Fix all five, hit the power switch, and the mix comes back to life — one layer of music per repair.

CONTROLS

MOUSElook around
W A S Dmove
SPACEjump
SHIFTsprint
HOLD LEFT CLICKmine the block you're looking at
RIGHT CLICKplace the selected block
Euse — buttons, faders, plugs, sockets
1–9 / SCROLLpick a hotbar slot
Ffly mode (cheat — great for exploring)
Hthis menu

THE FIVE REPAIRS

STYLUSthe needle is missing. Climb down under the table (vent slot behind a deck, or the hatch left of Deck A) and find the chrome block spot-lit in the parts bin. Carry it up and press E on Deck A's empty headshell.
RCAbehind the gear is a canyon of cables. One gold plug lies loose on the floor — press E on it to shove it into the empty socket on the wall panel (three shoves, clunk-clunk-CLICK).
X-FADERthe crossfader slot on the front of the mixer is jammed with dust. Mine it out, then walk into the fader to slide it end-to-end.
FUSEunder the mixer is a fuse box with a blackened fuse. Break it, grab the fresh glass fuse from the parts bin, and press E on the empty box while holding it.
POWERwhen the other four glow on your tracker, the master switch on the mixer's back edge lights green. Climb up and press it.

TIPS

The records spin — stand on one and ride. Walking to the middle while it turns is a real fight (tiny legs, big physics): press the START/STOP plate to stop the deck, or embrace the spiral. The little terraces step up as you walk in. At 45 speed the rim moves faster than you can sprint — jump near the edge to get launched. Mined blocks can be placed anywhere: build bridges between the gear.

`; export class Menu { private readonly opts: MenuOpts; private settings: Settings; private el: HTMLDivElement; private panel: HTMLDivElement; private badge: HTMLDivElement; private open = false; constructor(opts: MenuOpts) { this.opts = opts; this.settings = this.load(); this.injectStyle(); this.badge = document.createElement('div'); this.badge.className = 'tcm-badge'; this.badge.textContent = 'H — help & settings'; this.badge.addEventListener('click', () => this.toggle()); document.body.appendChild(this.badge); this.el = document.createElement('div'); this.el.className = 'tcm-overlay hidden'; this.panel = document.createElement('div'); this.panel.className = 'tcm-panel'; this.el.appendChild(this.panel); this.el.addEventListener('click', (e) => { if (e.target === this.el) this.toggle(false); }); document.body.appendChild(this.el); this.build(); window.addEventListener('keydown', this.onKey); this.opts.onApply(this.settings); if (!localStorage.getItem(LS_HELP_SEEN)) { localStorage.setItem(LS_HELP_SEEN, '1'); this.toggle(true, 'help'); } } isOpen(): boolean { return this.open; } getSettings(): Settings { return { ...this.settings }; } toggle(force?: boolean, tab?: 'help' | 'settings'): void { const next = force ?? !this.open; if (next === this.open) { if (tab) this.showTab(tab); return; } this.open = next; this.el.classList.toggle('hidden', !next); if (tab) this.showTab(tab); if (next) this.opts.onOpen?.(); else this.opts.onClose?.(); } dispose(): void { window.removeEventListener('keydown', this.onKey); this.el.remove(); this.badge.remove(); } private onKey = (e: KeyboardEvent): void => { if (e.code === 'KeyH' && !e.repeat) this.toggle(); else if (e.code === 'Escape' && this.open) this.toggle(false); }; private load(): Settings { try { const raw = localStorage.getItem(LS_SETTINGS); if (raw) return { ...DEFAULTS, ...JSON.parse(raw) as Partial }; } catch { /* fall through to defaults */ } return { ...DEFAULTS }; } private save(): void { localStorage.setItem(LS_SETTINGS, JSON.stringify(this.settings)); this.opts.onApply(this.settings); } private showTab(tab: 'help' | 'settings'): void { this.panel.querySelectorAll('.tcm-tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tab)); this.panel.querySelectorAll('.tcm-page').forEach((p) => p.classList.toggle('hidden', p.dataset.tab !== tab)); } private build(): void { const tabs = document.createElement('div'); tabs.className = 'tcm-tabs'; for (const [key, label] of [['help', 'HOW TO PLAY'], ['settings', 'SETTINGS']] as const) { const t = document.createElement('button'); t.className = 'tcm-tab' + (key === 'help' ? ' active' : ''); t.dataset.tab = key; t.textContent = label; t.addEventListener('click', () => this.showTab(key)); tabs.appendChild(t); } const close = document.createElement('button'); close.className = 'tcm-close'; close.textContent = '✕'; close.addEventListener('click', () => this.toggle(false)); tabs.appendChild(close); this.panel.appendChild(tabs); const help = document.createElement('div'); help.className = 'tcm-page'; help.dataset.tab = 'help'; help.innerHTML = HELP_HTML; // static template string, no user input this.panel.appendChild(help); const st = document.createElement('div'); st.className = 'tcm-page hidden'; st.dataset.tab = 'settings'; this.panel.appendChild(st); const s = this.settings; st.appendChild(this.slider('Master volume', 0, 1, 0.01, s.volume, (v) => { this.settings.volume = v; this.save(); })); st.appendChild(this.slider('Mouse sensitivity', 0.3, 3, 0.05, s.sensitivity, (v) => { this.settings.sensitivity = v; this.save(); })); st.appendChild(this.checkbox('View bob (head sway while walking)', s.viewBob, (v) => { this.settings.viewBob = v; this.save(); })); st.appendChild(this.select('Render quality', [['sharp', 'Sharp'], ['balanced', 'Balanced'], ['fast', 'Fast']], s.quality, (v) => { this.settings.quality = v as Settings['quality']; this.save(); })); } private row(label: string): HTMLLabelElement { const row = document.createElement('label'); row.className = 'tcm-row'; const span = document.createElement('span'); span.textContent = label; row.appendChild(span); return row; } private slider(label: string, min: number, max: number, stepV: number, value: number, onChange: (v: number) => void): HTMLElement { const row = this.row(label); const input = document.createElement('input'); input.type = 'range'; input.min = String(min); input.max = String(max); input.step = String(stepV); input.value = String(value); input.addEventListener('input', () => onChange(parseFloat(input.value))); row.appendChild(input); return row; } private checkbox(label: string, value: boolean, onChange: (v: boolean) => void): HTMLElement { const row = this.row(label); const input = document.createElement('input'); input.type = 'checkbox'; input.checked = value; input.addEventListener('change', () => onChange(input.checked)); row.appendChild(input); return row; } private select(label: string, options: ReadonlyArray, value: string, onChange: (v: string) => void): HTMLElement { const row = this.row(label); const sel = document.createElement('select'); for (const [v, text] of options) { const o = document.createElement('option'); o.value = v; o.textContent = text; o.selected = v === value; sel.appendChild(o); } sel.addEventListener('change', () => onChange(sel.value)); row.appendChild(sel); return row; } private injectStyle(): void { const css = document.createElement('style'); css.textContent = ` .tcm-badge{position:fixed;top:10px;left:10px;z-index:40;font:11px ui-monospace,monospace; color:#8a8378;background:#0008;border:1px solid #3a352c;border-radius:6px; padding:4px 8px;cursor:pointer;user-select:none} .tcm-badge:hover{color:#e8dcc0;border-color:#6a6252} .tcm-overlay{position:fixed;inset:0;z-index:50;background:#000a;display:flex; align-items:center;justify-content:center} .tcm-overlay.hidden{display:none} .tcm-panel{width:min(640px,92vw);max-height:86vh;overflow-y:auto;background:#14110c; border:1px solid #4a4234;border-radius:10px;padding:18px 22px; font:13px/1.5 ui-monospace,monospace;color:#d8cdb4} .tcm-tabs{display:flex;gap:8px;margin-bottom:12px;align-items:center} .tcm-tab{font:12px ui-monospace,monospace;letter-spacing:.12em;background:none; color:#8a8378;border:1px solid #3a352c;border-radius:6px;padding:6px 12px;cursor:pointer} .tcm-tab.active{color:#ffb028;border-color:#ffb028} .tcm-close{margin-left:auto;background:none;border:none;color:#8a8378;font-size:15px;cursor:pointer} .tcm-close:hover{color:#fff} .tcm-page h3{color:#ffb028;font-size:12px;letter-spacing:.15em;margin:14px 0 6px} .tcm-page table{border-collapse:collapse;width:100%} .tcm-page td{padding:3px 10px 3px 0;vertical-align:top} .tcm-page td:first-child{color:#7ec46a;white-space:nowrap;letter-spacing:.06em} .tcm-page.hidden{display:none} .tcm-row{display:flex;align-items:center;gap:14px;padding:10px 0;border-bottom:1px solid #2a2620} .tcm-row span{flex:1} .tcm-row input[type=range]{flex:1;accent-color:#ffb028} .tcm-row input[type=checkbox]{accent-color:#ffb028;width:16px;height:16px} .tcm-row select{background:#0a0908;color:#d8cdb4;border:1px solid #3a352c;border-radius:4px; font:12px ui-monospace,monospace;padding:4px 6px}`; document.head.appendChild(css); } }