The first ALLY, and the reputation loop that earns it — "don't shoot the biome, it helps you"
(V2_IDEAS) made mechanical.
- combat/enemies.js + balance.js: the `phage` archetype. A friendly seeker that hunts the
nearest FOE (not the player) and rams it — a bacteriophage bursts its host after `bursts`
hits. Fast (26 > every foe) so it catches things, fragile (hp 8) so it is spent, not a wall.
Entities now carry a `foe` flag (foes true; flora + phage false); kill() generalised so ANY
ally scores nothing and never combos. Fallback silhouette is a bare icosahedron = the phage's
real icosahedral capsid. Teal (EMISSIVE.ally), distinct from cyan "don't shoot" flora.
- combat/index.js: BIOME STANDING — one run-scoped float. flora:tended raises it, flora:harmed
drops it 25; at the symbiotic threshold the biome BUDS a phage near you (capped 3, 3s cooldown),
spending standing. Emits `standing {value}` for a future Lane E meter.
- levels/enemies.js: ARCHETYPES += phage; catalogue `phage`.
Verified deterministically in-engine: tending flora budded a phage, which hunted a spore_pod foe
(30hp) and rammed it dead; three-colour readability holds (amber foe / teal ally / cyan friend).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
3.8 KiB
JavaScript
75 lines
3.8 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)
|
|
ally: { core: 0x33ffbe, rim: 0xd6fff0 }, // teal — an active friendly unit (phage).
|
|
// distinct from cyan "don't shoot" flora:
|
|
// this one HELPS, and reads as its own thing.
|
|
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>
|
|
}`,
|
|
});
|
|
}
|