| 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 {
|
|
|
| }
|
| }
|
|
|
| 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();
|
|
|
|
|
|
|
| window.setInterval(() => {
|
| const advanced = advanceWorldState(state);
|
| state = advanced.state;
|
| window.gitWorldState = state;
|
| render();
|
| }, 1600);
|
|
|
|
|
|
|
| window.gitWorldCommandTranscript = commandTranscript;
|
|
|