[lane B] The phage gets its wings: an iconic capsid-tail-legs silhouette, built from primitives

#3 of the microbe pass. The phage ally was a bare icosahedron; now it's the recognizable T4
"lander" — icosahedral capsid + tail sheath + six splayed leg fibres.

I ran the requested hunyuan3d_mlx path first (flux concept -> hunyuan): it gave a great FLUX
reference but, exactly as ART_BIBLE predicts for thin features, the mesh WEBBED the legs into a
near-cubic 1.27x1.41x0.94 blob — no better than the icosahedron, at 217KB. So the phage is built
PROCEDURALLY (phageGeo): merged icosahedron + cylinders, ~150 tris, deterministic, bold enough to
read as a spidery phage at gameplay scale. The one microbe where geometry beats photogrammetry —
its shape IS a platonic solid plus cylinders.

(mergeGeometries needs all-or-none indexed; icosahedron is non-indexed and cylinders indexed, so
each part is toNonIndexed()'d before the merge.)

Verified in-engine: three phages read clearly as capsid+legs from head-on, side, and away.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-19 22:07:28 +10:00
parent b2d9b46c30
commit 8706387ff6

View File

@ -12,6 +12,7 @@
// Determinism: every random here comes from the seeded rng('enemies') stream.
import * as THREE from 'three';
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
import { BALANCE as B } from './balance.js';
import { emissiveMat, EMISSIVE } from '../flight/emissive.js';
@ -20,6 +21,37 @@ const _dir = new THREE.Vector3(), _v = new THREE.Vector3();
const FORWARD = new THREE.Vector3(0, 0, 1);
const BUCKET = 20; // u of s per broad-phase bucket
// The bacteriophage silhouette, built from primitives. The phage is the ONE microbe that is
// geometric, not blobby — an icosahedral capsid on a tail sheath with splayed leg fibres — so
// image->3D (Trellis/Hunyuan) webs its thin legs into a lump (measured: a 1.27x1.41x0.94 blob),
// while merged cylinders nail the iconic "lander" at ~140 tris. Authored along +Z (travel dir):
// legs reach forward, capsid trails.
const _up = new THREE.Vector3(0, 1, 0);
function phageGeo(r) {
const parts = [];
const head = new THREE.IcosahedronGeometry(r * 0.82, 0);
head.translate(0, 0, -r * 0.75);
parts.push(head);
const tail = new THREE.CylinderGeometry(r * 0.22, r * 0.22, r * 0.9, 6); // BOLD sheath
tail.rotateX(Math.PI / 2); // Y-axis -> Z-axis
tail.translate(0, 0, -r * 0.1);
parts.push(tail);
const N = 6;
for (let i = 0; i < N; i++) {
const a = (i / N) * Math.PI * 2;
const leg = new THREE.CylinderGeometry(r * 0.1, r * 0.06, r * 1.0, 4); // thick, tapered
leg.translate(0, r * 0.5, 0); // base at origin, extends +Y
// splay WIDE (small z) so a head-on phage reads as a 6-point star, not a dot
const dir = new THREE.Vector3(Math.cos(a), Math.sin(a), 0.55).normalize();
leg.applyQuaternion(new THREE.Quaternion().setFromUnitVectors(_up, dir));
leg.translate(0, 0, r * 0.35); // shift to the +Z baseplate
parts.push(leg);
}
// Icosahedron is non-indexed, cylinders are indexed — mergeGeometries needs all-or-none, so
// drop every index before merging.
return mergeGeometries(parts.map((p) => (p.index ? p.toNonIndexed() : p)), false);
}
// Round-1 roster. `geo` is the procedural fallback; D's model swaps in via assets.get.
const ARCHETYPES = {
floater: {
@ -54,12 +86,12 @@ const ARCHETYPES = {
color: EMISSIVE.hostile,
},
// phage — the ALLY. A friendly seeker that hunts the nearest FOE and rams it (a bacteriophage
// bursts its host). Teal, distinct from cyan flora. Budded by high BIOME STANDING. Fallback
// silhouette is a bare icosahedron = the phage's real icosahedral capsid (a Hunyuan mesh with
// tail fibres is the eventual upgrade).
// bursts its host). Teal, distinct from cyan flora. Budded by high BIOME STANDING. Silhouette
// is the iconic capsid+tail+legs, built procedurally (phageGeo) — image->3D can't hold the
// thin legs, and the shape is geometric anyway.
phage: {
cfg: B.enemies.phage,
geo: () => new THREE.IcosahedronGeometry(B.enemies.phage.radius, 0),
geo: () => phageGeo(B.enemies.phage.radius),
color: EMISSIVE.ally,
foe: false,
},