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>
83 lines
2.6 KiB
TypeScript
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;
|
|
},
|
|
};
|
|
}
|