Phase 3: regional control, sketch minimap, and fix the base hut
Three things, all tangled together enough to land at once. Regions. A single directional front cuts the map into liberated, contested, occupied and frontier. Heat accrual behind your own lines is exactly zero, not merely small — a trickle would eventually fortify your own back garden, and would mean the safe area quietly punished you for using it. Decay is judged by whoever holds the road, not whoever holds the ground the car is standing on. Modelled as one front rather than blobs of territory because the brief wants the line to move in both directions, and Phase 4 can then just slide the offsets. Territory is signalled by tone, never by the world announcing itself: crossing a border eases the fog, light and palette between four looks. Phase 3's gate is telling occupied from liberated without being told, so the HUD line naming the band is debug scaffolding like the heat numbers. Minimap. There was no way to tell where a target was. The map is drawn from Intel, not from the world: the road network's shape is always faintly visible, but only roads actually seen carry remembered heat, only ground actually driven through is filled in, and the front line is dashed in only where both sides of it have been explored. Adds a coarse fog-of-war grid to Intel, plus an eight-point compass arrow in the HUD. Base huts. The building was centred on the junction, so the starting hut sat directly over the car and hid it. Huts now stand off the junction, in whichever direction is furthest from tarmac, and are solid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,11 +26,14 @@ export function createMarkers(scene: THREE.Scene, bases: Base[]) {
|
||||
new THREE.BoxGeometry(7, 3.4, 7),
|
||||
new THREE.MeshStandardMaterial({ color: 0x3f5a46, roughness: 0.85 }),
|
||||
);
|
||||
hut.position.set(base.x, 1.7, base.z);
|
||||
// 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, 5);
|
||||
light.position.set(base.x, BEACON_HEIGHT / 2, base.z);
|
||||
scene.add(light);
|
||||
|
||||
+38
-1
@@ -1,5 +1,6 @@
|
||||
import * as THREE from 'three';
|
||||
import type { WorldModel } from '../sim/world';
|
||||
import type { Control } from '../sim/regions';
|
||||
import { CAR, WHEELS } from '../carSpec';
|
||||
|
||||
export interface SceneView {
|
||||
@@ -11,9 +12,26 @@ export interface SceneView {
|
||||
obstacles: THREE.Mesh[];
|
||||
/** Keeps the shadow frustum centred on the car. */
|
||||
followSun(): void;
|
||||
/** Eases the world's colour and visibility toward the current territory. */
|
||||
setTone(control: Control, dt: number): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* How each band of control looks.
|
||||
*
|
||||
* Phase 3's gate is that you can tell occupied ground from liberated within a
|
||||
* couple of minutes *without being told*. Nothing here is a label: your own
|
||||
* territory is open and green, contested ground goes flat and hazy, occupied
|
||||
* turns dusty and closes in, and the frontier is cold and short-sighted.
|
||||
*/
|
||||
const TONES: Record<Control, { sky: number; ground: number; sun: number; fog: [number, number] }> = {
|
||||
liberated: { sky: 0x9fc0b4, ground: 0x2f3830, sun: 0xfff0dc, fog: [110, 380] },
|
||||
contested: { sky: 0x9aa3a8, ground: 0x30302a, sun: 0xf6ecd8, fog: [80, 300] },
|
||||
occupied: { sky: 0xb09274, ground: 0x352c24, sun: 0xffdcb0, fog: [55, 210] },
|
||||
frontier: { sky: 0x7f8ba8, ground: 0x272a33, sun: 0xd8ddf0, fog: [35, 150] },
|
||||
};
|
||||
|
||||
const SKY = 0x11161c;
|
||||
|
||||
export function createScene(model: WorldModel): SceneView {
|
||||
@@ -31,7 +49,8 @@ export function createScene(model: WorldModel): SceneView {
|
||||
const camera = new THREE.PerspectiveCamera(62, innerWidth / innerHeight, 0.2, 900);
|
||||
camera.position.set(0, 6, -12);
|
||||
|
||||
scene.add(new THREE.HemisphereLight(0x9fb4c7, 0x2a2823, 1.1));
|
||||
const hemi = new THREE.HemisphereLight(0x9fb4c7, 0x2a2823, 1.1);
|
||||
scene.add(hemi);
|
||||
const sun = new THREE.DirectionalLight(0xfff0dc, 2.1);
|
||||
sun.castShadow = true;
|
||||
sun.shadow.mapSize.set(1024, 1024);
|
||||
@@ -157,6 +176,21 @@ export function createScene(model: WorldModel): SceneView {
|
||||
sun.position.set(car.position.x + 45, 70, car.position.z + 25);
|
||||
sun.target.position.copy(car.position);
|
||||
},
|
||||
|
||||
setTone(control, dt) {
|
||||
const tone = TONES[control];
|
||||
// Ease rather than snap, so a border is something you notice having
|
||||
// crossed rather than a switch being thrown in your face.
|
||||
const t = 1 - Math.exp(-0.7 * dt);
|
||||
hemi.color.lerp(toneColour.set(tone.sky), t);
|
||||
hemi.groundColor.lerp(toneColour.set(tone.ground), t);
|
||||
sun.color.lerp(toneColour.set(tone.sun), t);
|
||||
(scene.background as THREE.Color).lerp(toneColour.set(tone.sky).multiplyScalar(0.22), t);
|
||||
const fog = scene.fog as THREE.Fog;
|
||||
fog.color.copy(scene.background as THREE.Color);
|
||||
fog.near += (tone.fog[0] - fog.near) * t;
|
||||
fog.far += (tone.fog[1] - fog.far) * t;
|
||||
},
|
||||
dispose() {
|
||||
removeEventListener('resize', onResize);
|
||||
renderer.dispose();
|
||||
@@ -165,6 +199,9 @@ export function createScene(model: WorldModel): SceneView {
|
||||
};
|
||||
}
|
||||
|
||||
/** Scratch colour, so tone easing allocates nothing per frame. */
|
||||
const toneColour = new THREE.Color();
|
||||
|
||||
const camTarget = new THREE.Vector3();
|
||||
const camDesired = new THREE.Vector3();
|
||||
const CHASE_OFFSET = new THREE.Vector3(0, 3.4, -8.5);
|
||||
|
||||
Reference in New Issue
Block a user