guts/web/js/flight/emissive.js
jing 0a038582a7 [lane B] Round 1: flight controller, cannon, enemy framework on the stub
Flight (js/flight/): tube-mode controller in spline space (s,x,y) — flow-locked
forward motion, throttle as a spring-back lean, boost + i-frames, banking chase
cam on the parallel-transport frame, arcade wall response (shove + graze damage,
never a hard stop). Twin-stick input: WASD/left-stick moves the ship in the disc,
mouse/right-stick aims, Shift/Ctrl is throttle. Gamepad supported.

Combat (js/combat/): lysozyme cannon (pooled, instanced, heat-limited with
hysteresis lockout) + antacid torpedo emitting level:neutralize. Enemy framework
with floater/seeker/turret and turn-rate-limited homing darts, spawned from C's
level:event. Coat/hull model and the full bus surface E builds against.

Measured (deterministic stepper): flow-lock exact (3s at flow 14 -> s=44.0);
boost peak 30.8 u/s; heat 4s fire -> 2s lockout; wall slam 13.3 coat at 11.3 u/s
with forward speed untouched; 10 combat draws at 55 live enemies (budget <=80);
0.10 ms/frame CPU; 0 leaks over 20 create/dispose cycles. fps NOT claimed — rAF
does not run in the automated pane. Evidence: docs/shots/laneB/.

Three findings escalated in LANE_B_NOTES:
- qa.sh gate #1 is a no-op: node --check silently exits 0 on ESM, so the syntax
  gate has never checked anything. I shipped a real SyntaxError past a GREEN qa.
  Fix + verification handed to F.
- Surf is structurally broken: the peristalsis wave (13.64 u/s) is slower than
  the player (19.6 u/s), so level 2's signature mechanic loses to mashing
  throttle. Not tunable from this lane; escalated to A/F with options.
- Custom ShaderMaterials need #include <colorspace_fragment>, else ART_BIBLE's
  hostile amber reaches the display as rgb(255,26,6). Fixed here; stub/walls
  inherit it.

js/flight/dev.html is a dev harness (boot.js has no player wiring yet); it
retires once F pastes the snippet in LANE_B_NOTES.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:56:32 +10:00

72 lines
3.5 KiB
JavaScript

// flight/emissive.js (Lane B) — the one material behind every glowing thing B draws.
//
// ART_BIBLE: gameplay-relevant things glow, and the scene has NO lights (walls fake theirs
// with fresnel). So our primitives are unlit: a core colour with a rim that hots up at
// grazing angles, which reads as a volume against the dark tint instead of a flat decal.
// One material per (colour, rim) pair + InstancedMesh = 1 draw call per enemy/projectile
// type, which is how the whole combat layer fits in its 80-draw budget.
//
// Lives in flight/ rather than combat/ to keep the dependency arrow one-way: combat imports
// flight (it needs the player), flight never imports combat.
import * as THREE from 'three';
// ART_BIBLE §Emissive code — the readability law. Never hand-pick a colour outside this.
export const EMISSIVE = {
hostile: { core: 0xff5a2a, rim: 0xffb488 }, // hot amber -> red core
hostileShot: { core: 0xffe9d0, rim: 0xffffff }, // white-hot
neutral: { core: 0x39e6ff, rim: 0xdff2ff }, // cyan (player + flora + interactive)
pickup: { core: 0x7dffb0, rim: 0xdaffe9 },
gate: { core: 0xb06aff, rim: 0xe6d0ff },
acid: { core: 0xc8ff3a, rim: 0xeaffb0 },
};
export function emissiveMat({ core, rim }, { additive = false, opacity = 1 } = {}) {
return new THREE.ShaderMaterial({
transparent: opacity < 1 || additive,
depthWrite: !additive,
blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending,
uniforms: {
uCore: { value: new THREE.Color(core) },
uRim: { value: new THREE.Color(rim) },
uOpacity: { value: opacity },
uFlash: { value: 0 }, // driven on hit: 0..1 blows the whole body to rim colour
},
vertexShader: /* glsl */`
varying vec3 vN; varying vec3 vV;
void main() {
// three declares the instanceMatrix attribute for us whenever the drawn object is
// an InstancedMesh (USE_INSTANCING); this same material still works on a plain Mesh.
// (Careful: no backticks in here — this is a JS template literal.)
#ifdef USE_INSTANCING
mat4 m = modelViewMatrix * instanceMatrix;
#else
mat4 m = modelViewMatrix;
#endif
vec4 mv = m * vec4(position, 1.0);
// mat3(m) is only a true normal matrix under uniform scale — every instance we
// emit is uniformly scaled, and normalize() eats the leftover constant.
vN = normalize(mat3(m) * normal);
vV = -mv.xyz;
gl_Position = projectionMatrix * mv;
}`,
fragmentShader: /* glsl */`
varying vec3 vN; varying vec3 vV;
uniform vec3 uCore; uniform vec3 uRim; uniform float uOpacity; uniform float uFlash;
void main() {
float f = pow(1.0 - abs(dot(normalize(vN), normalize(vV))), 2.0);
vec3 col = mix(uCore, uRim, f) + uRim * f * 0.6;
col = mix(col, uRim * 1.6, uFlash);
gl_FragColor = vec4(col, uOpacity);
// MANDATORY for any custom ShaderMaterial in this project. ColorManagement is on,
// so THREE.Color(0xff5a2a) is converted to LINEAR working space on the way in, but
// a hand-written fragment shader gets no automatic conversion on the way out — it
// would write linear straight into the sRGB target. Measured: hostile amber landed
// as rgb(255,26,6) blood-red instead of rgb(255,90,42), quietly breaking the
// ART_BIBLE emissive readability law. This chunk applies linear -> sRGB.
#include <colorspace_fragment>
}`,
});
}