177 lines
5.5 KiB
JavaScript
177 lines
5.5 KiB
JavaScript
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),
|
|
};
|
|
}
|