Mike0021's picture
Publish Kimi K3 enhanced Sunset Racing Space
af99a3b verified
Raw
History Blame Contribute Delete
6.5 kB
// ═══════════════════════════════════════════════════════
// 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);
}
}