Phase 3 of turning the game into the company landing page. - Replace the Iran-map briefing with a brand start screen over the orbiting 3D factory: logo, tagline and four ways in. - Real world: safety notice, then GPS; if location is denied or missing, a friendly dialog offers offline play instead. - Offline: start at Park Mellat beside the family car with the route to the factory, where the first purchase mission is. - About Us: start in the factory yard. Products: start at the machine samples with the showroom open. - Contact, address with map links, FAQ, careers, gallery and social links open in a sheet (bottom sheet on phones) from company config. - Restart replays the chosen mode. Layout scrolls on short screens and respects safe areas. - Add tests for the start screen, map links and theme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
2.0 KiB
JavaScript
77 lines
2.0 KiB
JavaScript
import { publishInterface } from '../store/slices/interfaceSlice.js';
|
|
import { publishProgress } from '../store/slices/gameSlice.js';
|
|
|
|
/** Bridges the high-frequency renderer and React using serializable, batched snapshots.
|
|
* @param {object} store - Application Redux store.
|
|
* @returns {object} View publication and action registration API.
|
|
*/
|
|
export function createInterfaceBridge(store) {
|
|
let nodes = {
|
|
landing: {
|
|
hidden: false,
|
|
},
|
|
hud: {
|
|
hidden: true,
|
|
},
|
|
},
|
|
actions = new Map(),
|
|
timer = null,
|
|
disposed = false;
|
|
/** Publishes at most ten interface frames per second, independently of WebGL FPS. */
|
|
function flush() {
|
|
timer = null;
|
|
if (!disposed) store.dispatch(publishInterface(structuredClone(nodes)));
|
|
}
|
|
/** Merges a node view model, scheduling only actual changes. */
|
|
function patch(id, values) {
|
|
const previous = nodes[id] ?? {};
|
|
if (Object.keys(values).every((key) => Object.is(previous[key], values[key]))) return;
|
|
nodes = {
|
|
...nodes,
|
|
[id]: {
|
|
...previous,
|
|
...values,
|
|
},
|
|
};
|
|
if (timer === null && !disposed) timer = setTimeout(flush, 100);
|
|
}
|
|
return {
|
|
activate() {
|
|
disposed = false;
|
|
},
|
|
patch,
|
|
read: (id) => nodes[id] ?? {},
|
|
open: (id) =>
|
|
patch(id, {
|
|
open: true,
|
|
}),
|
|
close: (id) =>
|
|
patch(id, {
|
|
open: false,
|
|
}),
|
|
on: (id, handler) => actions.set(id, handler),
|
|
emit: (id, value) => actions.get(id)?.(value),
|
|
style: (id, style) =>
|
|
patch(id, {
|
|
style: {
|
|
...nodes[id]?.style,
|
|
...style,
|
|
},
|
|
}),
|
|
classState: (id, name, on) =>
|
|
patch(id, {
|
|
classes: {
|
|
...nodes[id]?.classes,
|
|
[name]: on,
|
|
},
|
|
}),
|
|
publishGame: (state) => store.dispatch(publishProgress(structuredClone(state))),
|
|
clearActions: () => actions.clear(),
|
|
dispose() {
|
|
disposed = true;
|
|
clearTimeout(timer);
|
|
actions.clear();
|
|
},
|
|
};
|
|
}
|