drive-between-the-lines/src/render/markers.ts
dejvino 405f1b0dfe Make the running game inspectable, and stop the beacons swallowing the view
The pane this build runs in does not composite, so requestAnimationFrame never
fires, the loop stalls, and nothing can be screenshotted from outside. Adds a
`?debug=1` handle that steps the simulation by hand and renders on demand, plus
a dev-only endpoint that takes a POSTed frame and writes it to shots/. Moving
the image as bytes rather than a pasted data URL matters: a truncated base64
string decodes to nothing.

Looking at the result immediately found a real problem. Base beacons were wide
translucent cylinders — legible from a distance, a solid green wall from inside,
and the car parks in the middle of one. Now a slim pillar plus a ring on the
ground that marks where to stop. Also raised the sky and fog brightness, which
was dark enough to read as night.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:38:27 +02:00

83 lines
2.6 KiB
TypeScript

import * as THREE from 'three';
import type { Base } from '../sim/bases';
/**
* Bases and the active objective, as things you can see from a distance.
* The player navigates by looking, so both need to be visible over scenery.
*/
const BEACON_HEIGHT = 34;
/**
* A slim pillar plus a ring on the ground.
*
* The first version was a wide translucent cylinder, which was legible from a
* distance and a solid green wall from inside — and since the car parks in the
* middle of one, that is exactly where you spend your time. Narrow enough to see
* past, tall enough to see over buildings.
*/
function beacon(colour: number, radius: number): THREE.Group {
const group = new THREE.Group();
const material = new THREE.MeshBasicMaterial({
color: colour,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide,
depthWrite: false,
// Additive keeps it reading as light rather than as a pane of glass.
blending: THREE.AdditiveBlending,
});
const pillar = new THREE.Mesh(
new THREE.CylinderGeometry(0.9, 0.9, BEACON_HEIGHT, 8, 1, true),
material,
);
pillar.position.y = BEACON_HEIGHT / 2;
group.add(pillar);
// The ring is what tells you where to actually stop.
const ring = new THREE.Mesh(
new THREE.RingGeometry(radius - 0.7, radius, 32).rotateX(-Math.PI / 2),
material,
);
ring.position.y = 0.08;
group.add(ring);
return group;
}
export function createMarkers(scene: THREE.Scene, bases: Base[]) {
for (const base of bases) {
const hut = new THREE.Mesh(
new THREE.BoxGeometry(7, 3.4, 7),
new THREE.MeshStandardMaterial({ color: 0x3f5a46, roughness: 0.85 }),
);
// The building sits beside the junction, not on it — on it, it blocks the
// road, and at the starting base it sits directly over the car.
hut.position.set(base.hutX, 1.7, base.hutZ);
hut.castShadow = true;
hut.receiveShadow = true;
scene.add(hut);
// The beacon stays over the junction, since that is where you park.
const light = beacon(0x5fd08a, 9);
light.position.set(base.x, 0, base.z);
scene.add(light);
}
const objective = beacon(0xffc247, 11);
objective.visible = false;
scene.add(objective);
return {
/** Point the objective beacon at a target, or hide it when idle. */
setObjective(target: { x: number; z: number } | null) {
objective.visible = target !== null;
if (target) objective.position.set(target.x, 0, target.z);
},
/** Slow spin, so a beacon reads as a marker rather than scenery. */
update(elapsed: number) {
objective.rotation.y = elapsed * 0.6;
},
};
}