Audit item 12.
THE WHOLE COHORT WAS BLINKING. Enemy flash was ONE uFlash uniform per type, and the comment
above it said so and deferred the fix: shoot one bolus chunk in a room of eight and all eight
blinked white. That is not weak feedback, it is WRONG feedback — the single signal that tells
you which body absorbed your shot was being broadcast to every body that did not, in a fogged
tube where telling them apart is the entire skill. Per-instance now: an `aFlash`
InstancedBufferAttribute written at the same compacted draw index as the matrix (they must
match, or the flash lands on somebody else's body once anything in the pool dies). Verified
with five floaters drawn and the middle one shot: aFlash reads [0, 0, 0.9, 0, 0].
emissive.js declares the attribute unconditionally, so a plain Mesh or a pool that never
attaches one gets WebGL's default 0 and renders exactly as before. uFlash stays for the one
effect that IS per-cohort — the boss's open-node tell, where every node opens at once.
THE RETICLE ONLY KNEW ABOUT THE TRIGGER. setHot was driven by `intent.fire`, so the instrument
in the middle of the screen reported whether the player's own finger was down and said nothing
about whether the shot connected. In a game where a foe can be hit-and-unharmed (armoured
plate), hit-and-shielded (the Guardian's shut iris) or simply missed in the fog, that is the
most useful thing it could have been telling you — and the shut-iris case is exactly the one
where a player without feedback concludes the gun is broken. enemies.js now announces
`enemy:hit {dmg, blocked, dead}` and the reticle pops for it: 0.35 blocked, 0.7 landed, 1.0
kill. The tell is a SIZE pop, not brightness alone, because firing already brightens it and one
signal cannot do two jobs — firing is dimmed to 0.55 to make the room.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
3.9 KiB
JavaScript
100 lines
3.9 KiB
JavaScript
// flight/ship.js (Lane B) — ENDO-1, the probe you fly, and the reticle you aim with.
|
||
//
|
||
// Procedural primitives merged into ONE geometry: assets are optional by law (TECH §manifest),
|
||
// so this is the shipping look until Lane D's GLB lands, and the permanent fallback after.
|
||
// Convention: the hull is built nose-toward −Z so Object3D.lookAt() aims it with no fudge.
|
||
|
||
import * as THREE from 'three';
|
||
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
||
import { emissiveMat, EMISSIVE } from './emissive.js';
|
||
|
||
// D's GLB swaps in here when it exists; logic above never learns which one it got.
|
||
export function buildShip(assets = null) {
|
||
const group = new THREE.Group();
|
||
group.name = 'endo1';
|
||
|
||
const glb = assets?.get?.('models', 'endo1') ?? null;
|
||
const hull = glb ? glb.scene ?? glb : proceduralHull();
|
||
group.add(hull);
|
||
|
||
// engine bloom at the tail: additive, unlit, reads as thrust without a light
|
||
const glowGeo = new THREE.SphereGeometry(0.34, 12, 8);
|
||
const glowMat = emissiveMat(EMISSIVE.neutral, { additive: true, opacity: 0.9 });
|
||
const glow = new THREE.Mesh(glowGeo, glowMat);
|
||
glow.position.z = 0.95;
|
||
group.add(glow);
|
||
|
||
const disposables = [glowGeo, glowMat];
|
||
if (!glb) { disposables.push(hull.geometry, hull.material); }
|
||
|
||
return {
|
||
group,
|
||
glow,
|
||
setThrust(t) { glow.scale.setScalar(0.7 + t * 0.9); glowMat.uniforms.uOpacity.value = 0.5 + t * 0.5; },
|
||
setFlash(v) { if (hull.material?.uniforms?.uFlash) hull.material.uniforms.uFlash.value = v; },
|
||
dispose() { for (const d of disposables) d?.dispose?.(); },
|
||
};
|
||
}
|
||
|
||
function proceduralHull() {
|
||
const parts = [];
|
||
|
||
const body = new THREE.CapsuleGeometry(0.4, 0.9, 4, 12);
|
||
body.rotateX(Math.PI / 2); // capsule is +Y by default; we fly down −Z
|
||
parts.push(body);
|
||
|
||
const nose = new THREE.ConeGeometry(0.4, 0.85, 12);
|
||
nose.rotateX(-Math.PI / 2);
|
||
nose.translate(0, 0, -1.08);
|
||
parts.push(nose);
|
||
|
||
for (const side of [-1, 1]) {
|
||
const fin = new THREE.BoxGeometry(0.62, 0.07, 0.44);
|
||
fin.translate(side * 0.52, -0.04, 0.36);
|
||
fin.rotateZ(side * -0.22);
|
||
parts.push(fin);
|
||
}
|
||
|
||
const collar = new THREE.TorusGeometry(0.42, 0.06, 6, 14);
|
||
collar.translate(0, 0, -0.28);
|
||
parts.push(collar);
|
||
|
||
const geo = mergeGeometries(parts, false);
|
||
for (const p of parts) p.dispose();
|
||
return new THREE.Mesh(geo, emissiveMat(EMISSIVE.neutral));
|
||
}
|
||
|
||
// The reticle floats TUNING.aim.distance ahead in the disc plane. Depth-test off: it is an
|
||
// instrument reading, not an object in the gut — it must never be swallowed by a fold.
|
||
export function buildReticle() {
|
||
const geo = new THREE.RingGeometry(0.55, 0.72, 20);
|
||
const mat = emissiveMat(EMISSIVE.neutral, { additive: true, opacity: 0.85 });
|
||
mat.depthTest = false;
|
||
const mesh = new THREE.Mesh(geo, mat);
|
||
mesh.renderOrder = 10;
|
||
|
||
const dotGeo = new THREE.CircleGeometry(0.12, 10);
|
||
const dot = new THREE.Mesh(dotGeo, mat);
|
||
dot.renderOrder = 10;
|
||
mesh.add(dot);
|
||
|
||
// A HITMARKER. The reticle only ever knew whether the trigger was DOWN — information the
|
||
// player already had, from their own finger — and said nothing about whether the shot
|
||
// connected. In a tube where a foe can be hit and unharmed (armour), hit and shielded
|
||
// (the Guardian's shut iris), or simply missed in the fog, that is the single most useful
|
||
// thing this instrument could be telling you.
|
||
// The tell is a SIZE POP, not just brightness: firing already brightens it, so brightness
|
||
// alone would be one signal doing two jobs. Firing is dimmed to make room.
|
||
let hot = 0, hit = 0;
|
||
const apply = () => {
|
||
mat.uniforms.uFlash.value = Math.max(hot * 0.55, hit);
|
||
mesh.scale.setScalar(1 + hit * 0.5);
|
||
};
|
||
return {
|
||
mesh,
|
||
setHot(v) { hot = v; apply(); }, // hot while the cannon is firing
|
||
setHit(v) { hit = v; apply(); }, // 0..1, decayed by player.js
|
||
dispose() { geo.dispose(); dotGeo.dispose(); mat.dispose(); },
|
||
};
|
||
}
|