File size: 6,500 Bytes
af99a3b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | // βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// ITEMS β decorative floating "?" bonus boxes
// Purely ambient trackside dressing (no gameplay effect):
// they hover just beyond the road edge, spin and bob, and
// glow warmly in the sunset light. The original build gave
// every box its own real PointLight (10 extra lights!); this
// version uses emissive materials + one additive glow sprite
// per box instead β zero additional lights.
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
import * as THREE from 'three';
import { TRACK_WIDTH, CURB_W } from './config.js';
import { frame } from './track.js';
// ββ Shared assets (built once) ββ
let _shared = null;
function getShared() {
if (_shared) return _shared;
// "?" decal texture β warm amber tile with a bold question mark
const qCanvas = document.createElement('canvas');
qCanvas.width = qCanvas.height = 128;
const qctx = qCanvas.getContext('2d');
const grad = qctx.createLinearGradient(0, 0, 0, 128);
grad.addColorStop(0, '#ffc85e');
grad.addColorStop(1, '#f09120');
qctx.fillStyle = grad;
qctx.fillRect(0, 0, 128, 128);
qctx.strokeStyle = 'rgba(60,30,5,0.9)';
qctx.lineWidth = 8;
qctx.strokeRect(4, 4, 120, 120);
// Corner rivets
qctx.fillStyle = 'rgba(60,30,5,0.8)';
for (const [rx, ry] of [[16, 16], [112, 16], [16, 112], [112, 112]]) {
qctx.beginPath(); qctx.arc(rx, ry, 5, 0, Math.PI * 2); qctx.fill();
}
qctx.fillStyle = '#fff7e8';
qctx.strokeStyle = 'rgba(60,30,5,0.85)';
qctx.lineWidth = 6;
qctx.font = '900 84px Arial';
qctx.textAlign = 'center';
qctx.textBaseline = 'middle';
qctx.strokeText('?', 64, 70);
qctx.fillText('?', 64, 70);
const decalTex = new THREE.CanvasTexture(qCanvas);
decalTex.anisotropy = 4;
// Soft radial glow sprite (additive, replaces PointLights)
const gCanvas = document.createElement('canvas');
gCanvas.width = gCanvas.height = 128;
const gctx = gCanvas.getContext('2d');
const rg = gctx.createRadialGradient(64, 64, 4, 64, 64, 64);
rg.addColorStop(0, 'rgba(255,190,90,0.55)');
rg.addColorStop(0.4, 'rgba(255,150,50,0.22)');
rg.addColorStop(1, 'rgba(255,120,30,0)');
gctx.fillStyle = rg;
gctx.fillRect(0, 0, 128, 128);
const glowTex = new THREE.CanvasTexture(gCanvas);
_shared = {
decalTex,
glowTex,
boxGeo: new THREE.BoxGeometry(1.3, 1.3, 1.3),
lidGeo: new THREE.BoxGeometry(1.36, 0.18, 1.36),
decalGeo: new THREE.PlaneGeometry(1.05, 1.05),
glowGeo: new THREE.PlaneGeometry(3.6, 3.6),
boxMat: new THREE.MeshPhongMaterial({
color: 0xffa832, emissive: 0x7a4a00, emissiveIntensity: 0.55,
shininess: 90, specular: 0xffeecc,
}),
lidMat: new THREE.MeshPhongMaterial({
color: 0x8a5a18, emissive: 0x3a2404, emissiveIntensity: 0.4,
shininess: 60,
}),
glowMat: new THREE.MeshBasicMaterial({
map: glowTex, transparent: true, depthWrite: false,
blending: THREE.AdditiveBlending, side: THREE.DoubleSide,
}),
};
return _shared;
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// createItemBoxes(scene) β place decorative boxes along the
// track, alternating sides, just beyond the curb so they
// never interfere with racing. Returns an array of boxes
// whose userData carries animation state.
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function createItemBoxes(scene) {
const S = getShared();
const boxes = [];
const COUNT = 10;
for (let i = 0; i < COUNT; i++) {
const t = (i + 0.5) / COUNT; // offset from start line & sector marks
const { point, side } = frame(t);
const sideSign = i % 2 === 0 ? 1 : -1;
const lateral = sideSign * (TRACK_WIDTH / 2 + CURB_W + 2.2);
const box = new THREE.Group();
const body = new THREE.Mesh(S.boxGeo, S.boxMat);
body.castShadow = true;
box.add(body);
// Lid seams top & bottom give the cube a "crate" read
for (const y of [0.62, -0.62]) {
const lid = new THREE.Mesh(S.lidGeo, S.lidMat);
lid.position.y = y;
box.add(lid);
}
// "?" decals on the four vertical faces
const decalMat = new THREE.MeshBasicMaterial({ map: S.decalTex, transparent: true });
for (const [pos, rotY] of [
[[0, 0, 0.66], 0], [[0, 0, -0.66], Math.PI],
[[0.66, 0, 0], Math.PI / 2], [[-0.66, 0, 0], -Math.PI / 2],
]) {
const q = new THREE.Mesh(S.decalGeo, decalMat);
q.position.set(...pos);
q.rotation.y = rotY;
box.add(q);
}
// Additive glow halo (billboarded cheaply by facing up-track)
const glow = new THREE.Mesh(S.glowGeo, S.glowMat);
glow.renderOrder = 5;
box.add(glow);
box.userData.glow = glow;
box.position.set(point.x + side.x * lateral, 1.7, point.z + side.z * lateral);
box.userData.baseY = 1.7;
box.userData.phase = i * 1.37; // desync bobbing
box.userData.spinRate = 0.6 + (i % 3) * 0.25;
scene.add(box);
boxes.push(box);
}
return boxes;
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// updateItemBoxes(boxes, elapsed, camera) β bob, spin, pulse.
// Call once per frame from the game loop. Cheap: no allocs.
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function updateItemBoxes(boxes, elapsed, camera) {
if (!boxes.length) return;
// Pulse the shared box material once (all boxes pulse together)
const S = getShared();
S.boxMat.emissiveIntensity = 0.45 + 0.25 * Math.sin(elapsed * 2.2);
for (const box of boxes) {
const ph = box.userData.phase;
box.position.y = box.userData.baseY + Math.sin(elapsed * 1.6 + ph) * 0.25;
box.rotation.y = elapsed * box.userData.spinRate + ph;
// Billboard the glow halo toward the camera when available
if (camera && box.userData.glow) box.userData.glow.lookAt(camera.position);
}
}
|