import { EVENT_TYPES, TOOLBELT, commandTranscript } from './shared.js'; import { advanceWorldState, createWorldState, getNearbyDistrict, renderMinimap, renderWorld, worldReducer } from './world-architect.js'; import { bindUi, renderHud } from './ui-reality.js'; const app = document; const worldViewport = app.getElementById('world-viewport'); const minimap = app.getElementById('minimap-viewport'); let state = createWorldState(); function assetPath(relativePath) { const root = './phase2-unreal/ReferenceSource/'; const UrlConstructor = window.URL; return new UrlConstructor(root + relativePath, window.location.href).href; } function setupBrowserScene() { const opening = app.getElementById('opening-scene'); const video = app.getElementById('opening-video'); const enter = app.querySelector('[data-action="enter-game"]'); const sound = app.querySelector('[data-action="toggle-opening-sound"]'); const status = app.getElementById('opening-status'); if (!opening || !video || !enter) return; const enterWorld = () => { opening.classList.add('is-dismissed'); opening.setAttribute('aria-hidden', 'true'); video.pause(); worldViewport?.focus(); }; try { video.src = assetPath('Opening/3099.mp4'); video.poster = assetPath('NeonReflections/3090.webp'); video.load(); const openingFallback = window.setTimeout(() => { if (video.readyState < 2) enterWorld(); }, 2500); video.play().catch(() => { if (status) status.textContent = 'Opening scene ready · press ENTER THE WORLD'; }); video.addEventListener('playing', () => { window.clearTimeout(openingFallback); if (status) status.textContent = 'Playing 3099.mp4 · opening scene'; }, { once: true }); video.addEventListener('error', () => { if (status) status.textContent = 'Opening media unavailable · entering browser scene'; window.setTimeout(enterWorld, 700); }, { once: true }); } catch { if (status) status.textContent = 'Opening media unavailable · entering browser scene'; enterWorld(); } enter.addEventListener('click', enterWorld); sound?.addEventListener('click', () => { video.muted = !video.muted; sound.textContent = video.muted ? 'SOUND OFF' : 'SOUND ON'; }); try { app.querySelectorAll('[data-reference-image]').forEach((plate) => { plate.setAttribute('poster', assetPath(plate.dataset.referenceImage || '')); }); } catch { // Reference plates are optional presentation assets; never block gameplay. } } function dispatch(event) { state = worldReducer(state, event); window.gitWorldState = state; render(); } function render() { const nearby = getNearbyDistrict(state); if (worldViewport) renderWorld(worldViewport, state, (districtId) => dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId })); if (minimap) renderMinimap(minimap, state); renderHud(app, state, nearby); } function move(dx, dy) { dispatch({ type: EVENT_TYPES.PLAYER_MOVE, dx, dy }); } function useToolByKey(key) { const tool = TOOLBELT.find((candidate) => candidate.key === key); if (!tool) return; dispatch({ type: EVENT_TYPES.USE_TOOL, toolId: tool.id, message: `${tool.name}: ${tool.verb}.` }); } function inspectNearest() { const nearby = getNearbyDistrict(state); if (nearby.inRange) { dispatch({ type: EVENT_TYPES.ENTER_DISTRICT, districtId: nearby.district.id }); } else { dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId: nearby.district.id }); } } function onKeyDown(event) { const target = event.target; const typing = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement; if (typing && event.key !== 'Escape') return; const key = event.key.toLowerCase(); const movement = { w: [0, -1], arrowup: [0, -1], s: [0, 1], arrowdown: [0, 1], a: [-1, 0], arrowleft: [-1, 0], d: [1, 0], arrowright: [1, 0] }[key]; if (movement) { event.preventDefault(); move(movement[0], movement[1]); return; } if (key === 'e' || event.key === 'Enter') { event.preventDefault(); inspectNearest(); return; } if (key === 't') { event.preventDefault(); dispatch({ type: EVENT_TYPES.OPEN_TERMINAL }); return; } if (key === 'i') { event.preventDefault(); dispatch({ type: EVENT_TYPES.OPEN_INSPECTOR }); return; } if (/^[1-8]$/.test(event.key)) { event.preventDefault(); useToolByKey(event.key); return; } if (event.key === 'Escape') { dispatch(state.terminal.open ? { type: EVENT_TYPES.CLOSE_TERMINAL } : { type: EVENT_TYPES.CLOSE_INSPECTOR }); } } bindUi(app, dispatch); app.addEventListener('keydown', onKeyDown); window.addEventListener('resize', render); window.gitWorldState = state; render(); setupBrowserScene(); // Fixed-step browser-local simulation: the world advances on a deterministic clock, // while the render layer simply reflects the latest serializable state. window.setInterval(() => { const advanced = advanceWorldState(state); state = advanced.state; window.gitWorldState = state; render(); }, 1600); // Keep the transcript helper reachable for the UI's delegated terminal buttons in // environments where the app is embedded without a module bundler. window.gitWorldCommandTranscript = commandTranscript;