refactor: separate React views, Redux snapshots and Three.js runtime

This commit is contained in:
2026-10-28 12:26:40 +03:30
parent 6bd9b17173
commit d0da416923
77 changed files with 4941 additions and 3723 deletions
+176
View File
@@ -0,0 +1,176 @@
import * as THREE from 'three';
import { catalog } from '../economy/gameState.js';
import { textTexture } from './textures.js';
const PRODUCT_COLORS = {
juice: '#e5ae47',
water: '#9fd3e6',
milk: '#8a5a3c',
tea: '#c7783a',
lemonade: '#f0dc5a',
biscuit: '#c9935a',
chocolate: '#5a3524',
nuts: '#b9884f',
cake: '#e58fa6',
};
// Sign panel; the shared helper redraws it once the Persian web font has loaded.
const label = (text, sub) =>
textTexture(512, 160, (c, width, height) => {
c.fillStyle = '#0f2a2a';
c.fillRect(0, 0, width, height);
c.fillStyle = '#3ee0c9';
c.fillRect(0, height - 10, width, 10);
c.textAlign = 'center';
c.direction = 'rtl';
c.fillStyle = '#e9fff8';
c.font = 'bold 56px Vazirmatn, Tahoma, sans-serif';
c.fillText(text, width / 2, 72);
c.fillStyle = '#9fe9da';
c.font = '34px Vazirmatn, Tahoma, sans-serif';
c.fillText(sub, width / 2, 126);
});
// A Robot Market machine. Local +z is the front face; the group is placed facing the path.
export function createVendingMachine(def) {
const group = new THREE.Group();
const mat = (color, extra = {}) =>
new THREE.MeshStandardMaterial({ color, roughness: 0.5, ...extra });
const box = (w, h, d, material, x, y, z, parent = group) => {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
mesh.position.set(x, y, z);
mesh.castShadow = mesh.receiveShadow = true;
parent.add(mesh);
return mesh;
};
const shell = mat('#0b8f88', {
metalness: 0.45,
roughness: 0.3,
emissive: '#0b8f88',
emissiveIntensity: 0.15,
}),
white = mat('#eef5f3', { metalness: 0.2, roughness: 0.35 });
const dark = mat('#13292b'),
steel = mat('#b7c1c0', { metalness: 0.8, roughness: 0.3 });
box(1.9, 2.4, 1.1, shell, 0, 1.3, 0);
box(1.94, 0.32, 1.14, white, 0, 2.62, 0);
box(1.94, 0.12, 1.14, dark, 0, 0.06, 0);
// The lit interior glows instead of using a point light, which would cost every pixel in the scene.
box(
1.3,
1.8,
0.06,
mat('#13292b', { emissive: '#2a7f73', emissiveIntensity: 0.5 }),
-0.2,
1.45,
0.56,
);
// Six shelves of products behind the glass, coloured by what this machine sells.
for (let row = 0; row < 6; row++) {
const y = 0.78 + row * 0.28;
box(1.2, 0.02, 0.25, steel, -0.2, y, 0.45);
for (let col = 0; col < 8; col++) {
const id = def.products[(row * 8 + col) % def.products.length];
const color = mat(PRODUCT_COLORS[id]);
if (catalog[id].type === 'drink') {
const bottle = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.04, 0.17, 8), color);
bottle.position.set(-0.73 + col * 0.15, y + 0.1, 0.47);
group.add(bottle);
} else box(0.09, 0.14, 0.06, color, -0.73 + col * 0.15, y + 0.08, 0.47);
}
}
const glass = new THREE.Mesh(
new THREE.PlaneGeometry(1.32, 1.82),
new THREE.MeshStandardMaterial({
color: '#cdeeea',
transparent: true,
opacity: 0.16,
roughness: 0.05,
metalness: 0.3,
depthWrite: false,
}),
);
glass.position.set(-0.2, 1.45, 0.6);
group.add(glass);
// Card reader panel on the right of the front.
box(0.42, 1.8, 0.05, white, 0.7, 1.45, 0.56);
const screen = box(
0.3,
0.22,
0.02,
new THREE.MeshStandardMaterial({
color: '#7ef0d6',
emissive: '#2bbfa0',
emissiveIntensity: 0.8,
}),
0.7,
1.92,
0.6,
);
box(0.3, 0.2, 0.03, dark, 0.7, 1.55, 0.6);
box(0.22, 0.03, 0.04, steel, 0.7, 1.62, 0.62);
for (let r = 0; r < 3; r++)
for (let c = 0; c < 3; c++) box(0.07, 0.05, 0.02, steel, 0.62 + c * 0.08, 1.32 - r * 0.07, 0.6);
box(0.9, 0.3, 0.08, mat('#050f10'), -0.2, 0.38, 0.56);
const flap = new THREE.Group();
flap.position.set(-0.2, 0.53, 0.61);
group.add(flap);
box(0.88, 0.28, 0.03, mat('#4f6f68', { metalness: 0.4 }), 0, -0.14, 0, flap);
const sign = new THREE.Mesh(
new THREE.PlaneGeometry(1.86, 0.3),
new THREE.MeshBasicMaterial({ map: label('روبات مارکت', def.name) }),
);
sign.position.set(0, 2.62, 0.575);
group.add(sign);
const delivery = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.2, 10), mat('#e5ae47'));
delivery.visible = false;
group.add(delivery);
const card = box(0.2, 0.13, 0.01, mat('#e2d6a8', { metalness: 0.3 }), 0.95, 1.7, 0.66);
card.visible = false;
// Objective beacon: a tall column of light visible across the city, like a mission marker.
const beaconMaterial = new THREE.MeshBasicMaterial({
color: '#3ee0c9',
transparent: true,
opacity: 0.22,
depthWrite: false,
fog: false,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
});
const beacon = new THREE.Mesh(
new THREE.CylinderGeometry(0.7, 0.7, 90, 16, 1, true),
beaconMaterial,
);
beacon.position.y = 45;
group.add(beacon);
const ring = new THREE.Mesh(
new THREE.RingGeometry(1.6, 1.9, 40),
new THREE.MeshBasicMaterial({
color: '#3ee0c9',
transparent: true,
opacity: 0.6,
depthWrite: false,
}),
);
ring.rotation.x = -Math.PI / 2;
ring.position.set(0, 0.09, 1.6);
group.add(ring);
return {
group,
flap,
delivery,
card,
screen,
beacon,
ring,
setDelivery(productId) {
delivery.material = mat(PRODUCT_COLORS[productId]);
delivery.scale.set(catalog[productId].type === 'drink' ? 1 : 1.6, 1, 1);
},
// Points in machine space, converted to world space by the caller.
readerPoint: new THREE.Vector3(0.7, 1.6, 0.62),
slotPoint: new THREE.Vector3(-0.2, 0.38, 0.66),
};
}