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>
72 lines
3.5 KiB
JavaScript
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>
|
|
}`,
|
|
});
|
|
}
|