Spaces:
Running
Running
| ; | |
| ; | |
| /* ============================================================================ | |
| BROOD SWARM — UNIT MODELS 30..32 | |
| ---------------------------------------------------------------------------- | |
| Load after src/engine/models.js. Everything here is built from the ORGANIC | |
| kit that already lives there — carapace(), broodLeg(), boneSeg(), | |
| chitinBlade(), tendril(), tubercles(), eyeCluster() — because the Brood's | |
| whole readability rests on the roster looking like ONE species. A Sovereign | |
| assembled from a different vocabulary than the Ravager it spawned beside is | |
| not a hero, it is a second faction. | |
| The kit covered slots 0..27. These are the three creatures that were still | |
| falling through to the base roster and rendering as somebody else's | |
| geometry: the faction's hero, its caster, and its miner. | |
| 30 The Brood Sovereign hero hp 7400 dmg 110 rng 34 melee | |
| 31 Brood Tidecaster sup hp 390 dmg 24 rng 142 caster | |
| 32 Prospector sup hp 190 dmg 0 rng 0 UNARMED miner | |
| +X is forward, +Y is up, Z is lateral, feet at y=0. Organisms have no | |
| turret, so every builder returns tur:null. | |
| ---------------------------------------------------------------------------- | |
| MATERIALS ARE THE RIG. | |
| The vertex stage keys its animation off the MATERIAL ID, not off a bone | |
| count or a name: MAT.LEAF (BIO_LEG/BIO_MEM) gets the two-oscillator spring | |
| bend, MAT.CHITIN gets the breathing pulse, MAT.SERVO gets the machine walk | |
| cycle. Nothing here may ever wear SERVO — a Brood creature stepping through | |
| a servo gait is a mech wearing a shell. | |
| The catch is that models.js only patches the material onto the PRIMITIVE | |
| builders (box/cyl/sphere/wedge/extrude/ring/bevelBox/inset/tube/greeble). | |
| sculpt(), quad() and therefore everything built out of boneSeg() — | |
| chitinBlade, tendril, broodLeg's segments, every helper below — inherit | |
| whatever material the last primitive call happened to leave on the builder. | |
| Build a carapace right after a lamp-coloured eye and the whole body animates | |
| as a lamp; build one after a leg and the body flexes like a limb. That is | |
| invisible in a screenshot and only shows up as a creature whose shell | |
| breathes wrong. | |
| So the surface is declared EXPLICITLY before every stretch of unpatched | |
| geometry, via the three helpers below. It costs one call and removes the | |
| entire class of bug. | |
| ============================================================================ */ | |
| /* Shell: hard chitin. Breathes, takes the subsurface term, does not flex. */ | |
| function brdShell(m){ return m.mat(MAT.CHITIN).team(0); } | |
| /* Limb: everything that hangs, reaches, trails or whips — legs, scythes, | |
| mandibles, spines, antennae. Boned geometry is driven by its hinge chain; | |
| unboned geometry gets the spring follow-through. Both need this id. */ | |
| function brdLimb(m){ return m.mat(MAT.LEAF).team(0); } | |
| /* Livery: faction-tinted living tissue, the organic equivalent of TEAM_A. | |
| Without a few of these patches a creature renders untinted and the player | |
| cannot tell whose army just walked over the ridge. */ | |
| function brdLivery(m){ return m.mat(MAT.CHITIN).team(1); } | |
| /* Local palette. Being absent from COL_MAT does NOT keep the primitive patch | |
| off the material we just declared: matDetect() consults COL_MAT first and | |
| then falls through to a luminance heuristic that always returns something. | |
| Anything here handed to a PATCHED primitive is silently reclassified — SOFT | |
| lands on hull, GULLET on hull.dark — which is what the slot 32 contract at | |
| the foot of this file has to undo on the feeding rasp. Only the two that are | |
| read inside a sculpt paint callback (SEAM, LUMEN) never reach a primitive | |
| and are genuinely inert. */ | |
| const BRD_GULLET=C(38,32,26); // wet dark inside the feeding mouth | |
| const BRD_SEAM =C(22,24,18); // near-black crease between shell plates | |
| const BRD_SOFT =C(216,198,152); // pale soft grub tissue — no armour value | |
| const BRD_LUMEN =C(214,190,236); // lit synaptic tissue, reads under livery | |
| /* Combat gore triples [wet, dark, highlight]. Mucus green matches | |
| MAT.BROOD_SLIME / infestation INF_MUCUS; amber is acid organs; violet is | |
| the horde FX palette (186,82,245). Not human crimson. organicfx.js reads | |
| this so a later livery retint cannot silently ship the wrong fluid. */ | |
| const BRD_ICHOR=Object.freeze([ | |
| Object.freeze([[115,177,77],[62,88,30],[185,255,72]]), | |
| Object.freeze([[214,168,40],[78,48,12],[255,220,110]]), | |
| Object.freeze([[186,82,245],[48,18,64],[220,150,255]]) | |
| ]); | |
| /* Deterministic 2D value noise, seeded, for the sculpts below. carapace() has | |
| its own private copy; duplicating four lines is cheaper than exporting a | |
| fifth global from models.js just to share it. */ | |
| function brdNoise(seed){ | |
| const sd=(seed||3)>>>0; | |
| const h=(a,b)=>{ let n=(a*374761393+b*668265263+sd)>>>0; | |
| n=(n^(n>>>13))*1274126177>>>0; return ((n^(n>>>16))>>>0)/4294967296; }; | |
| return (a,b)=>{ | |
| const ia=Math.floor(a), ib=Math.floor(b), fa=a-ia, fb=b-ib; | |
| const sa=fa*fa*(3-2*fa), sb=fb*fb*(3-2*fb); | |
| return (h(ia,ib)*(1-sa)+h(ia+1,ib)*sa)*(1-sb)+(h(ia,ib+1)*(1-sa)+h(ia+1,ib+1)*sa)*sb; | |
| }; | |
| } | |
| /* --------------------------------------------------------------------------- | |
| DORSAL CREST — the raised armoured shield the Sovereign carries over its | |
| shoulders, and the smaller collar it wears at the throat. | |
| This is its own sculpt rather than a third carapace() because carapace masks | |
| all of its relief with cos(u) — the +Z flank — so its ridges and keel run | |
| down ONE SIDE of the body, not along the spine. That is fine on a body seen | |
| from a three-quarter camera, but a crest is read entirely from above: its | |
| ribs have to fan across the top or the whole part collapses into a smooth | |
| lens. Here the rib mask keys off the lateral coordinate and the vertical one | |
| independently, so the plates fan out from the spine the way a crab's do. | |
| A lens section, not an open shell: the underside is flattened to 16% of the | |
| rise and buries itself in the carapace, which keeps the part watertight | |
| (no back faces to cull) for six extra quads. | |
| --------------------------------------------------------------------------- */ | |
| function brdCrest(m,x,y,z,len,wid,rise,ribs,col,seam,seed){ | |
| const nz=brdNoise(seed||17), lo=seam||BRD_SEAM; | |
| const plan=v=>Math.pow(Math.sin(Math.PI*Math.pow(v,0.74)),0.52); | |
| /* Lateral index across the shield, 0..ribs. Derived from cos so the rib | |
| count is the same on both flanks and the seam falls on the spine. */ | |
| /* THREE grooves, not seven, and eighteen samples around the section. | |
| u sweeps rim -> crown -> rim across the top half, so with segU=18 the top | |
| gets nine samples for `ribs` periods. The first version asked for seven | |
| ribs at segU=14 — barely one sample per groove — and a smoothstepped | |
| sawtooth sampled at its own frequency does not alias into a soft ridge, it | |
| aliases into NOTHING: the shield rendered as a bare smooth dome and every | |
| other decision about it was invisible. Three grooves is what fits, and | |
| three is also what actually reads at 40 pixels. */ | |
| const lat=ct=>(ct*0.5+0.5)*ribs; | |
| m.sculpt(x,y,z,18,8,(u,v)=>{ | |
| const th=u*TAU, ct=Math.cos(th), st=Math.sin(th); | |
| const w=plan(v); | |
| const f=lat(ct)%1, rib=f*f*(3-2*f); | |
| /* Ribs stand proudest on the crown and die out at the rim, which is what | |
| makes the edge read as a thin plate rather than a corrugated bowl. */ | |
| const crown=Math.max(0,st); | |
| let r=1+0.26*(rib-0.5)*2*crown+0.055*(nz(u*15,v*11)-0.5)*2; | |
| r+=0.12*Math.pow(crown,3); // spine ridge down the middle | |
| /* Scalloped trailing edge — a rank of shallow bays cut into the rim is | |
| the single cheapest thing that stops a shield looking machined. */ | |
| r*=1-0.09*Math.pow(v,2.6)*Math.cos(lat(ct)*TAU); | |
| return [(v-0.5)*len, st>0?st*rise*w*r:st*rise*0.16*w, ct*wid*w*r]; | |
| },(u,v)=>{ | |
| const th=u*TAU, ct=Math.cos(th), st=Math.sin(th); | |
| const f=lat(ct)%1, d=Math.min(f,1-f)*2; | |
| let k=Math.pow(d,0.44); k=k*k*(3-2*k); | |
| k*=0.58+0.42*Math.max(0,st); // underside stays dark | |
| k*=0.82+0.18*nz(u*27,v*19); // pore stipple | |
| k=0.16+0.84*k; | |
| return [lo[0]+(col[0]-lo[0])*k, lo[1]+(col[1]-lo[1])*k, lo[2]+(col[2]-lo[2])*k]; | |
| },false,false); | |
| return m; | |
| } | |
| /* --------------------------------------------------------------------------- | |
| SCYTHE ARM — the Sovereign's forward weapon. | |
| Two hinged links and an opposed pair of curved blades at the wrist. It is a | |
| RAPTORIAL arm, not a leg: the elbow rides high and outboard and the wrist | |
| drops forward and inboard, so the pair of them frame the head instead of | |
| splaying like limbs five and six. Only two joints, because the bone budget | |
| is 80 and eight seven-segment legs already spend 56 of them. | |
| --------------------------------------------------------------------------- */ | |
| function brdScythe(m,x,y,z,side,reach,th,phase){ | |
| const S=side, R=reach; | |
| /* Every offset scales with reach, and the shoulder starts a long way | |
| OUTBOARD. The first version socketed the arm at z=±0.6 — inside the | |
| thorax — so both arms ran through the body and out through the head, and | |
| the one feature that separates a Sovereign from a big Ravager rendered as | |
| a couple of stray blades near the jaw. The chain now goes out, up, then | |
| forward and back in, so the two claws close in front of the face. */ | |
| const P=[ | |
| [x, y, z+S*R*0.42], // shoulder, on the flank | |
| [x+R*0.42, y+R*0.14, z+S*R*0.64], // elbow, raised and outboard | |
| [x+R*0.98, y-R*0.28, z+S*R*0.32], // wrist, forward and drawn in | |
| ]; | |
| const j0=m.joint(P[0],null,[0,1,0],phase,0.15*S,0); // sweep about vertical | |
| const j1=m.joint(P[1],j0,[S,0,0],phase+0.62,0.22,0); // elbow flex | |
| brdLimb(m); | |
| m.bone(j0); boneSeg(m,P[0],P[1],th*1.30,th*1.02,6,BIO_LEG); | |
| m.bone(j1); boneSeg(m,P[1],P[2],th*1.00,th*0.72,6,BIO_LEG); | |
| /* Knuckles ride the bone BELOW the joint so they stay welded to the segment | |
| they drive and never tear open as the arm folds. */ | |
| m.bone(j1); m.sphere(P[1][0],P[1][1],P[1][2],th*1.18,5,BIO_LEG,0.88,false); | |
| m.bone(j1); m.sphere(P[2][0],P[2][1],P[2][2],th*0.92,5,BIO_LEG,0.90,false); | |
| /* Opposed blades: a long upper hook and a shorter lower one, curving toward | |
| each other. Two blades read as a grasping claw; one reads as a sword. */ | |
| m.bone(j1); | |
| chitinBlade(m,P[2][0],P[2][1]+th*0.30,P[2][2],R*0.62,th*0.88,-S*0.40,S*0.12,CLAW,0.22); | |
| chitinBlade(m,P[2][0],P[2][1]-th*0.55,P[2][2],R*0.48,th*0.62,-S*0.30,S*0.06,CLAW,-0.26); | |
| /* Forearm spur — the elbow blade every mantid carries, and the part that | |
| makes the arm read as armed even folded. */ | |
| chitinBlade(m,P[1][0],P[1][1],P[1][2],R*0.30,th*0.44,S*0.24,Math.PI*0.80,CLAW,0.44); | |
| m.bone(-1); | |
| return m; | |
| } | |
| /* --------------------------------------------------------------------------- | |
| SENSORY BULB — the Tidecaster's coordinating organ. | |
| Pear-shaped and lobed, heavy at the base where it meets the stalk, painted | |
| with the veins that run between its lobes. Built as livery, because this | |
| organ IS the unit's faction read: it is the largest single thing on the | |
| silhouette and it is the part that glows. | |
| --------------------------------------------------------------------------- */ | |
| function brdBulb(m,x,y,z,r,lobes,col,seed){ | |
| const nz=brdNoise(seed||29); | |
| m.sculpt(x,y,z,12,9,(u,v)=>{ | |
| const th=u*TAU, ph=v*Math.PI, st=Math.sin(ph), ct=Math.cos(ph); | |
| /* Lobes fade out at both poles so the surface stays closed and smooth | |
| where the sculpt collapses to a point. */ | |
| const lobe=1+0.15*Math.cos(th*lobes)*Math.pow(st,1.5); | |
| const pear=1+0.26*Math.pow(v,2.4); // hangs heavy at the crown | |
| const R=r*lobe*pear*(1+0.035*(nz(u*19,v*13)-0.5)*2); | |
| return [Math.cos(th)*st*R*0.88, -ct*R, Math.sin(th)*st*R]; | |
| },(u,v)=>{ | |
| const th=u*TAU, st=Math.sin(v*Math.PI); | |
| /* Bright along the lobe crests, dark in the grooves — the vein pattern. | |
| Value break, not hue: the team wash multiplies this, so a flat colour | |
| here would flatten the whole organ into one plastic ball. */ | |
| let k=0.5+0.5*Math.cos(th*lobes); | |
| k=k*k*(3-2*k); | |
| k=0.34+0.66*k*(0.55+0.45*Math.pow(st,0.7)); | |
| return [BRD_LUMEN[0]*k+col[0]*(1-k), BRD_LUMEN[1]*k+col[1]*(1-k), BRD_LUMEN[2]*k+col[2]*(1-k)]; | |
| },false,false); | |
| return m; | |
| } | |
| /* --------------------------------------------------------------------------- | |
| PROLEG — the grub's stubby walking foot. | |
| A grub does not walk on seven-jointed arachnid legs; it hauls itself on many | |
| short soft prolegs, and modelling eight of those costs a third of what six | |
| broodLeg()s would. Three links, a soft pad at the end, and deliberately NO | |
| claw: this animal is unarmed down to its feet. | |
| --------------------------------------------------------------------------- */ | |
| function brdProleg(m,x,y,z,side,reach,drop,th,phase){ | |
| const S=side, R=reach, D=drop; | |
| const P=[ | |
| [x, y, z+S*0.22*R], | |
| [x+R*0.12, y-D*0.30, z+S*0.72*R], | |
| [x-R*0.06, y-D*0.74, z+S*0.96*R], | |
| [x-R*0.22, y-D, z+S*1.02*R], | |
| ]; | |
| const RD=[th*1.30,th*1.08,th*0.76,th*0.40]; | |
| let parent=null; const bone=[]; | |
| for(let k=0;k<3;k++){ | |
| const ax=k?[S,0,0]:[0,1,0]; | |
| parent=m.joint(P[k],parent,ax,phase+k*0.36,(k?0.17:0.21*S),0); | |
| bone.push(parent); | |
| } | |
| brdLimb(m); | |
| for(let k=0;k<3;k++){ m.bone(bone[k]); boneSeg(m,P[k],P[k+1],RD[k],RD[k+1],4,BIO_LEG); } | |
| m.bone(bone[2]); | |
| m.sphere(P[3][0],P[3][1]+th*0.22,P[3][2],th*0.66,4,BIO_LEG,0.68,true); // soft pad | |
| m.bone(-1); | |
| return m; | |
| } | |
| /* --------------------------------------------------------------------------- | |
| FEEDING MOUTH — a blunt radial rasp on the +X face. | |
| The Prospector is UNARMED, and an unarmed unit that looks armed is a lie the | |
| player will act on: they will walk it into a fight because it has jaws. So | |
| nothing here protrudes past the lip. A soft everted ring, a genuinely hollow | |
| dark bore, and a circle of blunt grinding nubs set INSIDE the mouth where | |
| they can chew ore and nothing else. | |
| --------------------------------------------------------------------------- */ | |
| function brdRasp(m,x,y,z,r,col){ | |
| brdShell(m); | |
| cylX(m,x,y,z,r*0.55,r*0.86,r*1.02,8,col,false); // everted soft lip | |
| tubeX(m,x+r*0.50,y,z,r*0.42,r*0.94,r*0.52,8,BRD_GULLET); // real bore, not a decal | |
| for(let k=0;k<6;k++){ | |
| const a=k/6*TAU; | |
| m.sphere(x+r*0.30,y+Math.sin(a)*r*0.66,z+Math.cos(a)*r*0.66,r*0.24,4,BRD_SOFT,0.62,true); | |
| } | |
| return m; | |
| } | |
| /* Spiracle — a breathing vent through the shell. Hollow, so it reads as a hole | |
| in a living thing rather than a stud glued to one. */ | |
| function brdSpiracle(m,x,y,z,r,col){ | |
| brdShell(m); | |
| m.tube(x,y,z,r,r*0.52,r*0.86,6,col||CHIT_D); | |
| return m; | |
| } | |
| /* ============================================================================ | |
| 30 — THE BROOD SOVEREIGN | |
| ---------------------------------------------------------------------------- | |
| hp 7400, dmg 110, rng 34. The faction's Commander: the one creature a Brood | |
| player looks at, and a melee animal at a range shorter than a Ravager's | |
| body. It has to read at 40px as a HILL with claws in front of it. | |
| Three things carry that and nothing else does: | |
| 1. A raised armoured crest over the shoulders, wider than the body. From | |
| the game's overhead camera the plan silhouette is what the player sees, | |
| and this is the only part that is wider than everything around it. | |
| 2. Two scythe arms held forward of the head. The Alpha Ravager's jaws open | |
| sideways; the Sovereign's arms reach, which is the difference between a | |
| big animal and a commanding one. | |
| 3. Height. It stands on eight seven-jointed limbs with the body slung at | |
| 6.4 and the knees breaking above 10 — the arachnid read, where the | |
| animal hangs inside its own legs instead of sitting on them. | |
| ============================================================================ */ | |
| function mdlBrdSovereign(){ | |
| const m=MB(); | |
| const HIP=6.4, DROP=6.4; | |
| /* ONE continuous skin from the jaw to the tail. The waist, the segment | |
| plates, the tubercle field and the keel are displacement INSIDE that | |
| surface, so nothing intersects and the animal reads as grown rather than | |
| assembled. Two masses: a deep thorax forward of the waist carrying the | |
| crest and the arms, a tapering armoured abdomen behind it. */ | |
| brdShell(m); | |
| carapace(m,-1.6,7.0,0,20.4,7.0,4.5,CHITIN, | |
| {segs:7,ridge:0.28,bump:0.125,keel:0.20,nose:0.46,tail:0.16, | |
| waist:0.36,waistAt:0.56,waistW:0.062,seed:30071,u:16,v:20}); | |
| /* Head capsule proud of the front of that skin, in the darker shell so the | |
| face separates from the body mass at distance. */ | |
| brdShell(m); | |
| carapace(m,10.2,6.6,0,6.4,4.0,3.2,CHIT_D, | |
| {segs:3,ridge:0.20,bump:0.16,keel:0.14,nose:0.36,tail:0.44,seed:3011,u:11,v:8}); | |
| eyeCluster(m,11.9,7.2,0,1.30,3,HOT); | |
| /* THE CREST. Seven fanned plates on a lens, sitting down into the thorax. */ | |
| brdShell(m); | |
| /* WIDER THAN THE BODY, deliberately. At wid 5.8 against a body of 7.0 the | |
| shield sat inside the flank and the two masses fused into one smooth hump | |
| in plan — which is the only view the game camera gives. Overhanging the | |
| body is what puts a hard silhouette break between them, and the plan | |
| silhouette is the whole reason this part exists. */ | |
| brdCrest(m,2.2,10.8,0,11.4,7.4,3.4,3,CHITIN,BRD_SEAM,30071); | |
| /* Gorget at the throat, in livery. Placed forward and high on purpose: this | |
| is the faction patch that survives the overhead camera, where flank sacs | |
| are foreshortened into nothing. */ | |
| brdLivery(m); | |
| brdCrest(m,7.2,8.5,0,4.2,4.6,1.5,3,BIO_TEAM,C(96,78,120),777); | |
| m.team(0); | |
| /* Eight limbs, staggered in reach and knee height so they fan instead of | |
| marching in parallel, and phased in an alternating tripod so diagonal | |
| pairs swing in antiphase. */ | |
| for(let k=0;k<4;k++) for(const sd of [-1,1]){ | |
| brdLimb(m); | |
| /* hip = where this animal's shell actually is at that station, so the limb | |
| emerges from the body instead of floating beside it. See broodLeg. */ | |
| broodLeg(m, 4.6-k*3.1, HIP, 0, sd, 5.7+k*0.24, DROP, 4.15-k*0.19, 1.02, null, | |
| ((k+(sd<0?1:0))&1)*Math.PI, [5.25,4.05,5.30,4.75][k]); | |
| } | |
| /* The arms, in antiphase with each other so the animal never looks like it | |
| is clapping. */ | |
| for(const sd of [-1,1]) brdScythe(m,6.6,8.3,0,sd,7.4,1.06,sd>0?0:Math.PI); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| /* Mandibles under the head — opposed, curved, and small enough that the | |
| arms stay the thing the eye goes to. */ | |
| chitinBlade(m,11.6,5.7,sd*1.9,4.4,0.60,-sd*0.44,sd*0.24,CLAW,0.06); | |
| chitinBlade(m,11.4,6.6,sd*1.0,3.0,0.38,-sd*0.32,sd*0.12,CLAW,0.10); | |
| /* Sensory tendrils. Unboned would spring; these are hinged, so they lag | |
| the head and settle after it. */ | |
| tendril(m,12.2,9.0,sd*1.5,8.2,0.34,sd,3.6,BIO_LEG); | |
| tendril(m,9.6,9.7,sd*2.6,6.4,0.28,sd,3.0,BIO_LEG); | |
| /* Shoulder horns — the widest point of the animal in plan. */ | |
| chitinBlade(m,6.2,9.0,sd*3.8,5.6,0.58,sd*0.30,Math.PI*0.80,CLAW,0.46); | |
| /* Spikes raking back off the crest rim. */ | |
| for(let k=0;k<3;k++) | |
| chitinBlade(m,5.0-k*2.6,10.7-k*0.10,sd*(5.4+k*0.42),3.8-k*0.35,0.46,sd*0.34, | |
| Math.PI*0.86,CLAW,0.52); | |
| } | |
| /* Crown of spines down the abdomen, tallest just behind the crest — the | |
| hero read at 40px, and the only thing that stops the tail being a lump. */ | |
| brdLimb(m); | |
| for(let k=0;k<7;k++) | |
| chitinBlade(m,-3.5-k*1.24,9.9-k*0.34,0,3.4+Math.sin(k/6*Math.PI)*2.2,0.46,0.10, | |
| Math.PI*0.97,CLAW,1.02); | |
| /* Brood-lumen lobes: lit tissue slung along the abdomen flanks. Livery, so | |
| they carry the faction colour through the translucency term. */ | |
| for(const sd of [-1,1]){ | |
| m.sphere(-7.4,8.6,sd*3.0,2.15,7,BIO_TEAM,0.76,false); | |
| m.sphere(-4.0,9.4,sd*3.1,1.50,7,BIO_TEAM,0.76,false); | |
| } | |
| /* Spiracles along the abdomen, and the tubercle field over the tail. */ | |
| for(const sd of [-1,1]){ brdSpiracle(m,-5.8,9.6,sd*2.0,0.80,CHIT_D); | |
| brdSpiracle(m,-9.0,8.6,sd*1.5,0.66,CHIT_D); } | |
| brdShell(m); | |
| tubercles(m,-6.6,9.0,0,4.2,3.4,14,1.05,CHIT_D,30071); | |
| return {hull:m.build(),tur:null,s:1.15}; | |
| } | |
| /* ============================================================================ | |
| 31 — BROOD TIDECASTER | |
| ---------------------------------------------------------------------------- | |
| hp 390, dmg 24, rng 142. It forms where 28 broodmates gather and organises | |
| the mass into one targeted tide, which means it is not a fighter at all — | |
| it is a COORDINATING ORGAN that grew legs, and it should look like it cost | |
| the swarm something to make. | |
| So the mass is inverted relative to every other creature here: a small light | |
| body on long thin legs, carrying a raised synaptic bulb that is bigger than | |
| the body itself. The ring of fused lobes around the bulb's base is the 28 | |
| creatures that went into it, and the trailing tendrils are what it reaches | |
| the rest of the swarm with. Its only weapon is a slender ventral sting, | |
| deliberately thin: at dmg 24 it must not read as a threat next to a Ravager. | |
| ============================================================================ */ | |
| function mdlBrdTidecaster(){ | |
| const m=MB(); | |
| const HIP=3.6, DROP=3.6; | |
| /* Slight, narrow, and low — everything the Sovereign is not. */ | |
| brdShell(m); | |
| carapace(m,-0.6,4.2,0,9.4,3.0,2.0,CHITIN, | |
| {segs:6,ridge:0.18,bump:0.10,keel:0.16,nose:0.30,tail:0.34, | |
| waist:0.32,waistAt:0.50,waistW:0.080,seed:3101,u:12,v:12}); | |
| brdShell(m); | |
| carapace(m,4.4,4.1,0,2.8,1.7,1.4,CHIT_D, | |
| {segs:2,ridge:0.16,bump:0.14,keel:0.12,nose:0.38,tail:0.40,seed:311,u:8,v:6}); | |
| eyeCluster(m,5.2,4.3,0,0.52,2,LAMP); | |
| /* Six long spindly legs. Thin enough (th 0.44) that broodLeg skips its knee | |
| spur — a caster with blades on its knees is a fighter. */ | |
| for(let k=0;k<3;k++) for(const sd of [-1,1]){ | |
| brdLimb(m); | |
| broodLeg(m, 1.6-k*2.3, HIP, 0, sd, 4.3+k*0.22, DROP, 3.05-k*0.16, 0.44, null, | |
| ((k+(sd<0?1:0))&1)*Math.PI, [2.10,1.60,2.05][k]); | |
| } | |
| /* THE STALK. Three hinged links rising back off the thorax, so the bulb | |
| sways a beat behind the body instead of being welded to it. Every part | |
| above this rides the top link. */ | |
| /* Height is capped deliberately. The bulb wants to be the biggest thing on | |
| the model, but a 390hp support unit that stands as tall as the 7400hp | |
| Sovereign reads as the more important creature, and the player picks | |
| targets off silhouette. Crown tops out around 11 against the Sovereign's | |
| 15 before either is scaled. */ | |
| const NP=[[-0.9,5.0,0],[-1.3,6.5,0],[-1.7,7.7,0],[-1.9,8.5,0]]; | |
| let par=null; const nb=[]; | |
| for(let k=0;k<3;k++){ par=m.joint(NP[k],par,[0,0,1],k*0.48,0.055+k*0.030,0); nb.push(par); } | |
| brdLimb(m); | |
| for(let k=0;k<3;k++){ | |
| m.bone(nb[k]); | |
| boneSeg(m,NP[k],NP[k+1],0.82-k*0.13,0.69-k*0.13,5,BIO_LEG); | |
| } | |
| for(let k=1;k<3;k++){ m.bone(nb[k]); m.sphere(NP[k][0],NP[k][1],NP[k][2],0.76-k*0.10,4,BIO_LEG,0.88,false); } | |
| /* The organ itself, riding the top link. */ | |
| m.bone(nb[2]); | |
| brdLivery(m); | |
| brdBulb(m,-2.0,9.5,0,2.30,6,BIO_TEAM,3101); | |
| m.team(0); | |
| m.bone(nb[2]); | |
| m.sphere(-2.0,11.3,0,0.78,6,LAMP,0.90,false); // synaptic node | |
| /* The 28. A ring of fused lobes around the bulb's base — the broodmates the | |
| Tidecaster was assembled out of, still readable as separate heads. */ | |
| m.bone(nb[2]); | |
| brdLivery(m); | |
| /* Set OUTSIDE the bulb's own radius. At 1.45 out from a bulb of 2.30 the | |
| whole ring was swallowed by the surface it was supposed to fringe, and the | |
| one detail that says "this was 28 creatures" rendered as nothing. */ | |
| for(let k=0;k<6;k++){ | |
| const a=k/6*TAU; | |
| m.sphere(-2.0+Math.cos(a)*2.30,8.05,Math.sin(a)*2.55,0.72,5,BIO_TEAM,0.86,true); | |
| } | |
| m.team(0); | |
| /* Sensory crown raking up and out off the ring. */ | |
| m.bone(nb[2]); | |
| brdLimb(m); | |
| for(let k=0;k<6;k++){ | |
| const a=k/6*TAU, q=k/5-0.5; | |
| chitinBlade(m,-2.0+Math.cos(a)*2.15,8.4,Math.sin(a)*2.40, | |
| 3.0-Math.abs(q)*1.2,0.22,Math.sin(a)*0.34,Math.PI*(0.60+q*0.55),CLAW,1.00); | |
| } | |
| m.bone(-1); | |
| /* Trailing tendrils — what it actually reaches the swarm with. Long, thin, | |
| and the last thing on the model to come to rest. */ | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,-2.6,5.4,sd*1.1,7.6,0.24,sd,2.4,BIO_LEG); | |
| tendril(m,-3.8,4.6,sd*0.6,6.0,0.19,sd,1.6,BIO_LEG); | |
| } | |
| /* Ventral sting and its gland. Slender on purpose: rng 142 at dmg 24 is a | |
| needle, not a jaw. */ | |
| brdLivery(m); | |
| m.sphere(-4.6,4.0,0,1.05,6,BIO_TEAM,0.84,false); | |
| m.team(0); | |
| brdLimb(m); | |
| chitinBlade(m,-5.2,3.6,0,3.2,0.28,0,Math.PI*0.98,CLAW,-0.34); | |
| /* Flank sacs so the livery reads from the side as well as from above. */ | |
| for(const sd of [-1,1]) m.sphere(0.6,5.2,sd*1.5,0.78,6,BIO_TEAM,0.90,false); | |
| for(const sd of [-1,1]) brdSpiracle(m,-1.6,5.4,sd*1.2,0.44,CHIT_D); | |
| brdShell(m); | |
| tubercles(m,-2.4,5.2,0,2.0,1.4,5,0.46,CHIT_D,3101); | |
| return {hull:m.build(),tur:null,s:1.0}; | |
| } | |
| /* ============================================================================ | |
| 32 — PROSPECTOR | |
| ---------------------------------------------------------------------------- | |
| hp 190, dmg 0, rng 0. A mobile ore miner that chews ore where it lies, and | |
| the Brood's version of it is a GRUB. | |
| It is unarmed, and that is the whole brief. No claws, no jaws, no spines, no | |
| blade of any kind appears in this builder — not on the mouth, not on the | |
| back, not even on the feet, where every other creature in the kit ends in a | |
| CLAW-coloured point. An unarmed unit that looks armed gets walked into a | |
| fight, and the player is right to blame the model. | |
| What replaces the weapons is the other half of the job: a blunt feeding | |
| rasp at the front and a swollen carrying abdomen at the back, pinched apart | |
| by a waist so the load reads as LOAD. Soft, low-value seams and a pale skin | |
| keep it from looking armoured — the value break that makes the Sovereign's | |
| shell read as chitin is deliberately turned down here, because this animal | |
| is not wearing any. | |
| ============================================================================ */ | |
| function mdlBrdGrub(){ | |
| const m=MB(); | |
| const HIP=1.35, DROP=1.35; | |
| /* One soft body. Nine shallow segment rolls instead of six deep plates, a | |
| late waist, and almost no rear taper, so the mass piles up behind the | |
| pinch as an ore-swollen abdomen. */ | |
| brdShell(m); | |
| carapace(m,-0.6,2.05,0,10.6,3.4,2.5,CHITIN, | |
| {segs:9,ridge:0.14,bump:0.085,keel:0.08,nose:0.06,tail:0.34, | |
| waist:0.24,waistAt:0.66,waistW:0.085,seed:3207,u:13,v:14, | |
| seam:[0.26,0.24,0.18]}); | |
| /* Head: small, blunt, no capsule ridge. */ | |
| brdShell(m); | |
| /* nose (the -X end) is left almost untapered so the head's rear plugs into | |
| the body's front taper. At 0.30 both ends narrowed toward the same gap and | |
| the head floated a full unit clear of the animal. */ | |
| carapace(m,4.7,1.95,0,3.6,2.1,1.7,CHIT_D, | |
| {segs:2,ridge:0.10,bump:0.10,keel:0.06,nose:0.08,tail:0.30,seed:327,u:9,v:7, | |
| seam:[0.24,0.22,0.17]}); | |
| /* Eyespots, not an eye cluster. A grub barely sees; it feels its way. */ | |
| for(const sd of [-1,1]) m.sphere(5.45,2.52,sd*0.66,0.26,4,LAMP,0.86,false); | |
| /* Set INTO the snout, not hung off the end of it. Every carapace() tapers | |
| to a needle at both ends whatever nose/tail say, so a mouth placed at the | |
| nominal front edge floats clear of the head with daylight between them. */ | |
| brdRasp(m,5.75,1.88,0,1.30,BRD_SOFT); | |
| /* Eight short prolegs. Many small feet under a heavy body is the grub read, | |
| and it costs a third of what six arachnid limbs would. */ | |
| for(let k=0;k<4;k++) for(const sd of [-1,1]) | |
| brdProleg(m, 2.6-k*1.9, HIP, 0, sd, 1.72+k*0.05, DROP, 0.34, | |
| ((k+(sd<0?1:0))&1)*Math.PI); | |
| /* Ore sacs — swollen, translucent, and the faction patch. Sitting in the | |
| dorsal midline behind the waist, which is exactly where a full load would | |
| push the skin out. */ | |
| m.sphere(-3.3,3.45,0,1.38,6,BIO_TEAM,0.82,false); | |
| m.sphere(-1.5,3.55,0,1.02,6,BIO_TEAM,0.84,false); | |
| m.sphere(-4.9,3.22,0,0.92,6,BIO_TEAM,0.80,false); | |
| /* Short feeler antennae — the only appendages it has, and they trail rather | |
| than reach. */ | |
| brdLimb(m); | |
| for(const sd of [-1,1]) tendril(m,4.9,3.10,sd*0.60,2.6,0.13,sd,0.7,BIO_LEG); | |
| for(const sd of [-1,1]) brdSpiracle(m,-2.6,3.05,sd*1.35,0.34,CHIT_D); | |
| brdShell(m); | |
| tubercles(m,-2.8,3.05,0,2.4,1.5,6,0.40,CHIT_D,3207); | |
| return {hull:m.build(),tur:null,s:0.92}; | |
| } | |
| /* ============================================================================ | |
| THE TWELVE — SPLITTING mdlHordeSpitter AND mdlHordeBombardier | |
| ---------------------------------------------------------------------------- | |
| FAC_KIT.horde mapped 28 roster slots onto nine builders, each called with NO | |
| arguments, so twelve of those slots were not "similar", they were the SAME | |
| VERTEX BUFFER. Six units shared mdlHordeSpitter and six shared | |
| mdlHordeBombardier — a tier-1 line brawler and a tier-3 siege caster | |
| rendering as one animal. | |
| WHAT SEPARATES THEM IS PROPORTION, NOT DECORATION. | |
| The camera is overhead and a unit is ~40px. At that size a player reads | |
| three things and nothing else: the PLAN outline (long/short, wide/narrow), | |
| the HEIGHT of the mass over the ground, and ONE dominant feature breaking | |
| the outline. Greebles do not survive; a body that is twice as long as its | |
| neighbour does. So each of the twelve below picks a different corner of that | |
| space, and the detail exists only to explain the shape it is already making: | |
| slot animal plan stance dominant feature | |
| 1 Gorger short + WIDE low frontal brow shield | |
| 2 Ramparthorn long + wide TALL paired shoulder pauldrons | |
| 7 Skysting long + NARROW mid dorsal rack, raked up | |
| 10 Flakspine SHORT + narrow TALL vertical neck mast | |
| 20 Bloomsac round + FAT LOW bloated sac + burst ring | |
| 21 Emberthroat long + narrow mid dorsal vane fan, drooping gullet | |
| 3 Mortarback SHORT + deep hunched one up-and-back mortar throat | |
| 6 Lancespine VERY LONG + low low straight forward harpoon | |
| 16 Siegemound wide + squat LOW near-vertical chimney | |
| 22 Railfang long + narrow mid braced forward lance | |
| 26 Basilisk long + wide tall crowned head, single great eye | |
| 27 Harbinger short + narrow STILTS hanging spore bell + curtain | |
| Cross-checked against src/game/sim.js TYPES: every choice above is something | |
| the unit's own numbers already claim. The Vulture (10) cannot shoot ground at | |
| all and reaches 172px, so it is a mast; the Bombard (16) fires 400px at speed | |
| 14, so it is a mound that barely moves; the Longbow (6) is a 110hp single- | |
| target sniper, so it is all length and no armour. | |
| ============================================================================ */ | |
| /* --------------------------------------------------------------------------- | |
| MAW — the one weapon organ, parameterised. | |
| Every Brood gun in this file is this surface with different numbers, which | |
| is the point: a swarm whose artillery and whose brawler grew DIFFERENT | |
| weapons is two factions. A throat, built as a surface of revolution along | |
| its own axis and then pitched and yawed into place, with: | |
| * a taper, so it is an organ and not a pipe, | |
| * annular sphincter rings that die out toward the tip, | |
| * a lobed section, so the silhouette is never a circle, | |
| * a bell that only opens over the last quarter, and | |
| * a rim that folds back INWARD over the last tenth. | |
| That last one matters: leaving the end open renders the inside of the tube, | |
| and with backface culling on that is a hole straight through the animal. The | |
| fold closes the surface and still reads as a dark wet socket, because the | |
| paint callback drives the same region toward the seam colour. | |
| --------------------------------------------------------------------------- */ | |
| function brdMaw(m,x,y,z,o){ | |
| o=o||{}; | |
| const len=o.len==null?4.0:o.len, r0=o.r0==null?0.95:o.r0, r1=o.r1==null?0.55:o.r1; | |
| const flare=o.flare==null?0.85:o.flare, lobes=o.lobes==null?5:o.lobes; | |
| const rings=o.rings==null?4:o.rings, taper=o.taper==null?0.85:o.taper; | |
| const socket=o.socket===false?0:1; | |
| const pitch=o.pitch||0, yaw=o.yaw||0, col=o.col||CHIT_D; | |
| const seam=o.seam||BRD_SEAM, nz=brdNoise(o.seed||53); | |
| const cp=Math.cos(pitch), sp=Math.sin(pitch), cw=Math.cos(yaw), sw=Math.sin(yaw); | |
| const CM=(typeof COL_MAT!=='undefined'&&COL_MAT.get(col)); | |
| if(CM!=null) m.mat(CM); | |
| m.sculpt(x,y,z,o.u||9,o.v||9,(u,v)=>{ | |
| const th=u*TAU; | |
| let r=r0+(r1-r0)*Math.pow(v,taper); | |
| const b=Math.max(0,(v-0.74)/0.26); | |
| r*=1+flare*b*b; // bell, last quarter only | |
| const f=(v*rings)%1, s=f*f*(3-2*f); | |
| r*=1+0.11*(s-0.5)*2*(1-Math.pow(v,2.6)); // sphincter rings | |
| r*=1+0.085*Math.cos(th*lobes); // never a circle in section | |
| r*=1+0.05*(nz(u*17,v*13)-0.5)*2; | |
| if(socket&&v>0.90) r*=1-((v-0.90)/0.10)*0.94; // rim folds in: socket, not hole | |
| const a=len*v, by=Math.cos(th)*r, cz=Math.sin(th)*r; | |
| const px=a*cp-by*sp, py=a*sp+by*cp; // pitch about +Z | |
| return [px*cw-cz*sw, py, px*sw+cz*cw]; // then yaw about +Y | |
| },(u,v)=>{ | |
| const th=u*TAU, f=(v*rings)%1, d=Math.min(f,1-f)*2; | |
| let k=Math.pow(d,0.50); k=k*k*(3-2*k); | |
| k*=0.62+0.38*(0.5+0.5*Math.cos(th*lobes)); | |
| k*=0.80+0.20*nz(u*23,v*29); | |
| k=0.20+0.80*k; | |
| k*=1-0.60*Math.pow(Math.max(0,(v-0.78)/0.22),1.3); // aperture goes wet-dark | |
| return [seam[0]+(col[0]-seam[0])*k, seam[1]+(col[1]-seam[1])*k, | |
| seam[2]+(col[2]-seam[2])*k]; | |
| },true,true); | |
| return m; | |
| } | |
| /* --------------------------------------------------------------------------- | |
| FIN — a dorsal vane. A flattened lens standing on the spine, swept back by | |
| `rake`, with a scalloped free edge and a root that is three times the | |
| thickness of the rim. It is a MEMBRANE stretched on ribs, so it is painted | |
| bright at the root and dark at the rim with radial rib strokes across it — | |
| the same value-break logic carapace() uses, keyed to the rib angle instead | |
| of the segment index. | |
| --------------------------------------------------------------------------- */ | |
| function brdFin(m,x,y,z,len,rise,th,rake,col,seam,seed){ | |
| const nz=brdNoise(seed||71), lo=seam||BRD_SEAM; | |
| const CM=(typeof COL_MAT!=='undefined'&&COL_MAT.get(col)); | |
| if(CM!=null) m.mat(CM); | |
| m.sculpt(x,y,z,9,6,(u,v)=>{ | |
| const ang=u*TAU, ph=v*Math.PI, st=Math.sin(ph), ct=Math.cos(ph); | |
| const ex=Math.cos(ang)*st, ey=(1-ct)*0.5, ez=Math.sin(ang)*st; | |
| const scal=1-0.11*Math.cos(ang*5)*Math.pow(ey,1.6); // scalloped rim | |
| const tk=th*(0.34+0.66*Math.pow(1-ey,1.5))*(1+0.18*(nz(u*21,v*17)-0.5)*2); | |
| return [ex*len*0.5*scal+ey*rise*rake, ey*rise*scal, ez*tk]; | |
| },(u,v)=>{ | |
| const ang=u*TAU, ey=(1-Math.cos(v*Math.PI))*0.5; | |
| let k=0.5+0.5*Math.cos(ang*5); k=k*k*(3-2*k); // rib strokes | |
| k=0.30+0.70*k; | |
| k*=1-0.55*Math.pow(ey,1.2); // rim goes translucent-dark | |
| k*=0.84+0.16*nz(u*29,v*23); | |
| return [lo[0]+(col[0]-lo[0])*k, lo[1]+(col[1]-lo[1])*k, lo[2]+(col[2]-lo[2])*k]; | |
| },false,false); | |
| return m; | |
| } | |
| /* --------------------------------------------------------------------------- | |
| STALK — a hinged neck/mast. Returns the id of the TOP bone so the caller can | |
| hang the organ it carries on it; anything welded to the body instead would | |
| stay rigid while the mast swayed, and the join would visibly tear. | |
| --------------------------------------------------------------------------- */ | |
| function brdStalk(m,P,r0,r1,phase,axis){ | |
| const n=P.length-1, ax=axis||[0,0,1]; | |
| let par=null; const bone=[]; | |
| for(let k=0;k<n;k++){ par=m.joint(P[k],par,ax,phase+k*0.46,0.05+k*0.028,0); bone.push(par); } | |
| brdLimb(m); | |
| for(let k=0;k<n;k++){ | |
| m.bone(bone[k]); | |
| boneSeg(m,P[k],P[k+1],r0+(r1-r0)*(k/n),r0+(r1-r0)*((k+1)/n),5,BIO_LEG); | |
| } | |
| for(let k=1;k<n;k++){ | |
| m.bone(bone[k]); | |
| m.sphere(P[k][0],P[k][1],P[k][2],(r0+(r1-r0)*(k/n))*1.16,4,BIO_LEG,0.88,false); | |
| } | |
| m.bone(-1); | |
| return bone[n-1]; | |
| } | |
| /* A rank of walking limbs, staggered and phased into an alternating tripod. | |
| Every builder below did this by hand; twelve copies of a five-line loop is | |
| twelve chances to get the antiphase term wrong. `hips` is per-pair, because | |
| the shell's half-width changes down the body and broodLeg() needs the real | |
| number or the limb floats off the flank (see broodLeg's own note). */ | |
| function brdRank(m,x0,dx,hipY,drop,n,reach,knee,th,hips){ | |
| for(let k=0;k<n;k++) for(const sd of [-1,1]){ | |
| brdLimb(m); | |
| broodLeg(m, x0-k*dx, hipY, 0, sd, reach+k*0.20, drop, knee-k*0.16, th, null, | |
| ((k+(sd<0?1:0))&1)*Math.PI, hips[Math.min(k,hips.length-1)]); | |
| } | |
| return m; | |
| } | |
| /* WING — a membrane lobe lying in the XZ plane, which is the plan the player | |
| actually reads. Flyers and swimmers cannot share one extruded polygon the | |
| way mdlHordeFlyer did: a delta, a fat oval and a swept needle are three | |
| different silhouettes, and that difference is the whole split. `span` is | |
| lateral reach, `chord` is fore-aft, `sweep` shifts the tip aft. Tessellation | |
| is the triangle-floor: without legs a flyer has to spend its budget here. */ | |
| function brdWing(m,x,y,z,side,span,chord,th,sweep,col,seam,seed){ | |
| const nz=brdNoise(seed||41), lo=seam||BRD_SEAM, S=side; | |
| const CM=(typeof COL_MAT!=='undefined'&&COL_MAT.get(col)); | |
| if(CM!=null) m.mat(CM); | |
| m.sculpt(x,y,z,14,10,(u,v)=>{ | |
| const ang=u*TAU, ph=v*Math.PI, st=Math.sin(ph), ct=Math.cos(ph); | |
| const ex=Math.cos(ang)*st, ez=Math.abs(Math.sin(ang)*st); | |
| const scal=1-0.13*Math.cos(ang*5)*Math.pow(ez,1.45); | |
| const tk=th*(0.20+0.80*Math.pow(1-ez,1.55))*(1+0.14*(nz(u*19,v*13)-0.5)*2); | |
| return [ex*chord*0.5*scal+ez*span*sweep, ct*tk, S*ez*span*scal]; | |
| },(u,v)=>{ | |
| const ang=u*TAU, ez=Math.abs(Math.sin(ang)*Math.sin(v*Math.PI)); | |
| let k=0.5+0.5*Math.cos(ang*5); k=k*k*(3-2*k); | |
| k=0.28+0.72*k; | |
| k*=1-0.58*Math.pow(ez,1.15); | |
| k*=0.84+0.16*nz(u*29,v*23); | |
| return [lo[0]+(col[0]-lo[0])*k, lo[1]+(col[1]-lo[1])*k, lo[2]+(col[2]-lo[2])*k]; | |
| },false,false); | |
| return m; | |
| } | |
| /* ===================== 1 — GORGER (was mdlHordeSpitter) ===================== | |
| Rhino slot: tier 1, 130hp, 16dmg at 88px. The army's line brawler and the | |
| thing a player buys twenty of. So: SHORT and WIDE, carried LOW, with the | |
| armour concentrated dead ahead where it walks into fire. The brow shield is | |
| wider than the body and is the whole silhouette from above — a blunt wedge, | |
| which is the one plan shape nothing else in the twelve makes. | |
| =========================================================================== */ | |
| function mdlBrdGorger(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.5,2.55,0,10.8,4.8,2.30,CHITIN, | |
| {segs:5,ridge:0.22,bump:0.115,keel:0.14,nose:0.50,tail:0.26, | |
| waist:0.24,waistAt:0.58,waistW:0.095,seed:10101,u:13,v:13}); | |
| brdShell(m); | |
| carapace(m,4.7,2.40,0,3.8,3.0,1.55,CHIT_D, | |
| {segs:3,ridge:0.18,bump:0.15,keel:0.10,nose:0.34,tail:0.42,seed:1011,u:10,v:7}); | |
| eyeCluster(m,5.6,2.65,0,0.60,2,HOT); | |
| /* THE BROW. Low and forward, overhanging the head — at rise 1.4 it never | |
| makes the animal tall, it makes it broad, which is the read the Rhino slot | |
| needs against the Ramparthorn standing next to it. */ | |
| brdShell(m); | |
| brdCrest(m,3.2,3.55,0,6.2,5.6,1.40,3,CHITIN,BRD_SEAM,10101); | |
| brdLivery(m); | |
| brdCrest(m,5.2,3.05,0,2.6,3.2,0.80,3,BIO_TEAM,C(96,78,120),1011); | |
| m.team(0); | |
| /* Short thick throat set into the face — a spitter at 88px is a gob, not a | |
| barrel, so it is stubby, wide-mouthed and barely clears the brow. */ | |
| brdMaw(m,5.9,2.25,0,{len:3.2,r0:0.86,r1:0.62,flare:0.95,lobes:5,rings:3, | |
| pitch:-0.05,col:CHIT_D,seed:1011,u:9,v:8}); | |
| brdRank(m,2.0,2.4,2.35,2.35,3,2.95,2.05,0.56,[2.55,2.15,2.35]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| chitinBlade(m,5.0,1.95,sd*1.5,2.6,0.40,-sd*0.34,sd*0.26,CLAW,0.02); // opposed jaws | |
| chitinBlade(m,2.6,3.30,sd*2.5,3.0,0.36,sd*0.30,Math.PI*0.80,CLAW,0.34); | |
| tendril(m,5.2,3.30,sd*0.9,3.0,0.16,sd,1.3,BIO_LEG); | |
| m.sphere(-2.6,3.30,sd*1.9,0.92,6,BIO_TEAM,0.82,false); | |
| brdSpiracle(m,-0.8,3.45,sd*1.6,0.46,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-2.4,3.10,0,2.4,1.8,6,0.48,CHIT_D,10101); | |
| return {hull:m.build(),tur:null,s:1.0}; | |
| } | |
| /* ================== 2 — RAMPARTHORN (was mdlHordeSpitter) ================== | |
| Goliath slot: tier 2, 450hp, 42dmg, splash 10. The heavy assault chassis, | |
| and the only unit in this group that should read as ARMOUR. It stands a | |
| full body-height above the Gorger on longer limbs, and carries two crests | |
| angled off the flanks like pauldrons rather than one flat brow — so from | |
| above its plan is a broad H, not a wedge. | |
| =========================================================================== */ | |
| function mdlBrdRamparthorn(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-1.0,4.9,0,14.0,5.4,3.30,CHITIN, | |
| {segs:6,ridge:0.27,bump:0.12,keel:0.26,nose:0.52,tail:0.20, | |
| waist:0.32,waistAt:0.54,waistW:0.072,seed:20202,u:14,v:16}); | |
| brdShell(m); | |
| carapace(m,6.6,4.6,0,4.4,3.0,2.10,CHIT_D, | |
| {segs:3,ridge:0.20,bump:0.15,keel:0.16,nose:0.36,tail:0.44,seed:2022,u:10,v:8}); | |
| eyeCluster(m,7.9,5.0,0,0.82,3,HOT); | |
| /* PAULDRONS. Two crests set out on the flanks and rolled outboard, so the | |
| shoulder line breaks the plan silhouette twice instead of once. This is | |
| the difference between "big brawler" and "heavy" at 40px. */ | |
| for(const sd of [-1,1]){ | |
| brdShell(m); | |
| brdCrest(m,1.6,7.4,sd*3.1,7.6,3.4,2.40,3,CHITIN,BRD_SEAM,20202+sd); | |
| brdLimb(m); | |
| for(let k=0;k<3;k++) | |
| chitinBlade(m,3.6-k*2.4,7.9,sd*(4.0+k*0.30),3.4-k*0.30,0.44,sd*0.32, | |
| Math.PI*0.84,CLAW,0.50); | |
| } | |
| brdLivery(m); | |
| brdCrest(m,5.4,6.2,0,3.6,3.4,1.20,3,BIO_TEAM,C(96,78,120),2022); | |
| m.team(0); | |
| /* Twin throats, splayed. Two weapons is the tier-2 read, and splaying them | |
| keeps the head from becoming one lump. */ | |
| for(const sd of [-1,1]) | |
| brdMaw(m,7.4,4.30,sd*1.15,{len:4.6,r0:0.80,r1:0.54,flare:0.80,lobes:5,rings:4, | |
| pitch:-0.03,yaw:sd*0.16,col:CHIT_D,seed:2022+sd,u:9,v:9}); | |
| brdRank(m,3.2,2.9,4.70,4.70,3,4.35,3.35,0.86,[3.15,2.55,2.95]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| chitinBlade(m,7.2,3.9,sd*1.9,3.8,0.52,-sd*0.40,sd*0.24,CLAW,0.04); | |
| tendril(m,7.8,6.2,sd*1.2,4.6,0.22,sd,2.1,BIO_LEG); | |
| m.sphere(-4.6,6.0,sd*2.3,1.35,7,BIO_TEAM,0.80,false); | |
| brdSpiracle(m,-2.2,6.3,sd*1.9,0.60,CHIT_D); | |
| } | |
| for(let k=0;k<4;k++) | |
| chitinBlade(m,-2.6-k*1.9,6.6-k*0.22,0,2.6-k*0.24,0.38,0.10,Math.PI*0.96,CLAW,0.86); | |
| brdShell(m); | |
| tubercles(m,-4.2,6.2,0,3.2,2.2,8,0.62,CHIT_D,20202); | |
| return {hull:m.build(),tur:null,s:1.06}; | |
| } | |
| /* ==================== 7 — SKYSTING (was mdlHordeSpitter) =================== | |
| Hornet slot: tier 2, 175px, splash 24, and the only unit in this group that | |
| can engage AIRCRAFT as well as ground. So the weapon does not point forward | |
| at all — it is a rack of five slim spine-throats raked UP and back off the | |
| spine, fanned in plan. Narrow body underneath, because everything this unit | |
| has is in that rack. | |
| =========================================================================== */ | |
| function mdlBrdSkysting(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.6,3.25,0,11.2,3.25,2.05,CHITIN, | |
| {segs:6,ridge:0.20,bump:0.10,keel:0.20,nose:0.42,tail:0.30, | |
| waist:0.30,waistAt:0.52,waistW:0.078,seed:70707,u:12,v:14}); | |
| brdShell(m); | |
| carapace(m,5.2,3.15,0,3.2,1.9,1.45,CHIT_D, | |
| {segs:2,ridge:0.16,bump:0.14,keel:0.12,nose:0.34,tail:0.42,seed:707,u:9,v:6}); | |
| eyeCluster(m,6.1,3.35,0,0.52,2,HOT); | |
| /* THE RACK. Five throats, fanned in yaw and staggered in pitch so the tips | |
| describe an arc instead of a comb — a comb reads as machinery. The two | |
| outermost are shortest, which puts the tallest point on the centreline. */ | |
| for(let k=0;k<5;k++){ | |
| const q=k/4-0.5; | |
| brdMaw(m,-1.4+Math.abs(q)*1.2,4.35,q*3.4, | |
| {len:5.4-Math.abs(q)*2.0,r0:0.44,r1:0.20,flare:0.30,lobes:4,rings:5, | |
| taper:0.62,pitch:0.86-Math.abs(q)*0.10,yaw:q*0.62,col:CHITIN, | |
| seed:700+k,u:7,v:8}); | |
| } | |
| brdLivery(m); | |
| for(const sd of [-1,1]) m.sphere(-2.4,4.15,sd*1.5,0.90,6,BIO_TEAM,0.84,false); | |
| brdCrest(m,3.2,4.10,0,3.4,2.4,0.95,3,BIO_TEAM,C(96,78,120),707); | |
| m.team(0); | |
| brdRank(m,2.2,2.5,3.10,3.10,3,3.35,2.30,0.54,[1.90,1.55,1.80]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,6.0,4.10,sd*0.9,4.4,0.18,sd,2.0,BIO_LEG); | |
| chitinBlade(m,5.6,2.75,sd*1.1,2.2,0.26,-sd*0.28,sd*0.20,CLAW,0.04); | |
| brdSpiracle(m,-3.8,4.05,sd*1.2,0.42,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-4.2,3.90,0,2.2,1.2,5,0.42,CHIT_D,70707); | |
| return {hull:m.build(),tur:null,s:1.0}; | |
| } | |
| /* ==================== 10 — FLAKSPINE (was mdlHordeSpitter) ================= | |
| Vulture slot: tier 1, 172px, and it CANNOT SHOOT GROUND AT ALL. That is the | |
| most specific fact about any unit in this group and the silhouette should | |
| say it before the player reads a card: a short stubby body on four splayed | |
| legs carrying a near-vertical neck mast with a flak bell on top. It is the | |
| tallest thing in the group at less than half the Ramparthorn's body length. | |
| =========================================================================== */ | |
| function mdlBrdFlakspine(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.4,4.35,0,7.6,3.15,2.20,CHITIN, | |
| {segs:4,ridge:0.20,bump:0.11,keel:0.16,nose:0.40,tail:0.34, | |
| waist:0.22,waistAt:0.56,waistW:0.10,seed:10010,u:16,v:16}); | |
| brdShell(m); | |
| carapace(m,3.3,4.25,0,2.4,1.7,1.30,CHIT_D, | |
| {segs:2,ridge:0.16,bump:0.14,keel:0.10,nose:0.34,tail:0.40,seed:100,u:12,v:8}); | |
| eyeCluster(m,4.0,4.45,0,0.46,2,HOT); | |
| /* THE MAST. Four hinged links rising off the thorax, so the bell sways a | |
| beat behind the body — and everything above rides the top bone, or the | |
| join tears open the first time it leans. */ | |
| const NP=[[-0.9,5.5,0],[-0.9,7.2,0],[-0.7,8.7,0],[-0.5,9.9,0]]; | |
| const top=brdStalk(m,NP,0.62,0.42,0.20,[0,0,1]); | |
| m.bone(top); | |
| brdMaw(m,-0.5,9.9,0,{len:3.6,r0:0.66,r1:0.40,flare:1.35,lobes:6,rings:3, | |
| pitch:1.30,col:CHIT_D,seed:1001,u:10,v:8}); | |
| m.bone(top); | |
| brdLimb(m); | |
| for(let k=0;k<4;k++){ | |
| const a=k/4*TAU+0.4; | |
| chitinBlade(m,-0.5+Math.cos(a)*0.75,10.6,Math.sin(a)*0.75, | |
| 2.4,0.22,Math.sin(a)*0.30,a,CLAW,0.92); // ranging spines | |
| } | |
| m.bone(top); | |
| brdLivery(m); | |
| m.sphere(-0.9,8.9,0,0.78,6,BIO_TEAM,0.86,false); | |
| m.bone(-1); | |
| m.team(0); | |
| /* FOUR legs, splayed wide and long — a tripod-tall stance, and the only | |
| four-legged animal in the twelve. */ | |
| brdRank(m,1.5,3.0,4.20,4.20,2,4.30,3.30,0.60,[1.95,1.70]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,3.8,5.1,sd*0.8,4.0,0.17,sd,1.9,BIO_LEG); | |
| m.sphere(-2.2,5.1,sd*1.3,0.72,6,BIO_TEAM,0.84,false); | |
| brdSpiracle(m,-1.2,5.3,sd*1.1,0.38,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-2.4,4.95,0,1.6,1.1,4,0.38,CHIT_D,10010); | |
| return {hull:m.build(),tur:null,s:1.0}; | |
| } | |
| /* ===================== 20 — BLOOMSAC (was mdlHordeSpitter) ================= | |
| Reaper slot: tier 2, splash 52 — the largest area of effect on any unit in | |
| either group, ground only. It is a delivery system for one enormous burst, | |
| so the animal is mostly ABDOMEN: a bloated livery sac slung low between | |
| short legs, ringed with the spines that will go outward when it fires. | |
| Round in plan, low to the ground, and the only silhouette here with no | |
| forward weapon at all. | |
| =========================================================================== */ | |
| function mdlBrdBloomsac(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,0.6,2.65,0,9.6,4.30,2.60,CHITIN, | |
| {segs:4,ridge:0.20,bump:0.12,keel:0.10,nose:0.44,tail:0.44, | |
| waist:0.40,waistAt:0.40,waistW:0.075,seed:20020,u:14,v:14}); | |
| brdShell(m); | |
| carapace(m,4.6,2.45,0,2.8,2.1,1.40,CHIT_D, | |
| {segs:2,ridge:0.16,bump:0.15,keel:0.08,nose:0.34,tail:0.42,seed:2002,u:12,v:8}); | |
| eyeCluster(m,5.3,2.65,0,0.50,2,HOT); | |
| /* THE SAC. Bigger than the thorax carrying it, pear-heavy, and painted from | |
| the same vein function the Tidecaster's organ uses so the two read as the | |
| same tissue. Livery, because at this size it IS the faction patch. */ | |
| brdLivery(m); | |
| brdBulb(m,-3.7,5.35,0,3.35,7,BIO_TEAM,20020); | |
| m.team(0); | |
| /* The burst ring — eight spines set OUTSIDE the sac's radius and raked flat, | |
| so in plan they make a starburst nothing else in the twelve makes. */ | |
| brdLimb(m); | |
| for(let k=0;k<8;k++){ | |
| const a=k/8*TAU; | |
| chitinBlade(m,-3.7+Math.cos(a)*3.05,3.15,Math.sin(a)*3.35, | |
| 2.8,0.30,Math.sin(a)*0.26,a,CLAW,0.26); | |
| } | |
| brdRank(m,2.4,2.3,2.45,2.45,3,2.75,1.95,0.52,[2.30,2.05,2.15]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| chitinBlade(m,4.9,2.10,sd*1.2,2.2,0.32,-sd*0.32,sd*0.24,CLAW,0.02); | |
| tendril(m,5.1,3.20,sd*0.8,2.6,0.15,sd,1.1,BIO_LEG); | |
| brdSpiracle(m,1.6,3.55,sd*1.5,0.44,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,1.4,3.30,0,2.0,1.6,5,0.44,CHIT_D,20020); | |
| return {hull:m.build(),tur:null,s:1.05}; | |
| } | |
| /* ==================== 21 — EMBERTHROAT (was mdlHordeSpitter) =============== | |
| Cinder slot: tier 2, incendiary, splash 46 at 96px. A thing that carries | |
| burning chemistry has to shed heat, so the spine wears a fan of five | |
| membrane vanes — tall, thin, translucent at the rim — and the weapon hangs | |
| UNDER the head as a drooping gullet rather than sitting on it. Long narrow | |
| body, vertical fan: the plan is a fish, and no other silhouette here is. | |
| =========================================================================== */ | |
| function mdlBrdEmberthroat(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.6,3.05,0,11.6,3.60,2.35,CHITIN, | |
| {segs:7,ridge:0.22,bump:0.105,keel:0.12,nose:0.46,tail:0.28, | |
| waist:0.28,waistAt:0.54,waistW:0.080,seed:21021,u:13,v:15}); | |
| brdShell(m); | |
| carapace(m,5.4,2.95,0,3.4,2.1,1.55,CHIT_D, | |
| {segs:2,ridge:0.16,bump:0.15,keel:0.10,nose:0.34,tail:0.42,seed:2101,u:9,v:7}); | |
| eyeCluster(m,6.3,3.20,0,0.54,2,HOT); | |
| /* THE FAN. Five vanes, tallest over the shoulders, raked back. Membrane, so | |
| they take the translucency term instead of shading as plate. */ | |
| for(let k=0;k<5;k++){ | |
| const q=k/4; | |
| brdFin(m,2.6-k*2.15,4.05,0,3.4-q*0.9,3.4-Math.abs(q-0.25)*2.2,0.28, | |
| -0.34,BIO_MEM,C(46,44,30),2100+k); | |
| } | |
| /* THE GULLET. Under the jaw, drooping, wide-belled: a flamethrower's read. */ | |
| brdMaw(m,6.2,2.35,0,{len:4.2,r0:0.92,r1:0.70,flare:1.15,lobes:6,rings:3, | |
| pitch:-0.52,col:CHIT_D,seed:2101,u:10,v:8}); | |
| brdLivery(m); | |
| for(const sd of [-1,1]) m.sphere(-1.4,3.95,sd*1.9,1.05,6,BIO_TEAM,0.84,false); | |
| m.team(0); | |
| brdRank(m,2.4,2.6,2.90,2.90,3,3.30,2.25,0.56,[2.05,1.70,1.95]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| chitinBlade(m,6.2,3.55,sd*1.2,2.4,0.28,-sd*0.30,sd*0.20,CLAW,0.16); | |
| tendril(m,6.4,3.90,sd*0.8,3.6,0.17,sd,1.6,BIO_LEG); | |
| brdSpiracle(m,-3.4,3.80,sd*1.3,0.42,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-4.0,3.65,0,2.2,1.4,5,0.42,CHIT_D,21021); | |
| return {hull:m.build(),tur:null,s:1.0}; | |
| } | |
| /* ================= 3 — MORTARBACK (was mdlHordeBombardier) ================= | |
| Thumper slot: tier 2 artillery, 265px, and a MINIMUM range of 80 — it | |
| cannot defend itself up close, and the model should look like it is aiming | |
| over things. Short deep body with the back arched high, one heavy throat | |
| pitched up and rearward off the hump. Shortest body of the six bombardiers. | |
| =========================================================================== */ | |
| function mdlBrdMortarback(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.5,3.20,0,9.0,4.05,3.20,CHITIN, | |
| {segs:5,ridge:0.25,bump:0.115,keel:0.34,nose:0.46,tail:0.32, | |
| waist:0.26,waistAt:0.60,waistW:0.085,seed:30303,u:12,v:13}); | |
| brdShell(m); | |
| carapace(m,4.2,2.85,0,3.0,2.1,1.50,CHIT_D, | |
| {segs:2,ridge:0.16,bump:0.15,keel:0.10,nose:0.34,tail:0.42,seed:303,u:9,v:6}); | |
| eyeCluster(m,5.0,3.05,0,0.50,2,HOT); | |
| /* THE HUMP. A crest laid ALONG the back rather than across the shoulders, | |
| which is what turns the profile into an arch. */ | |
| brdShell(m); | |
| brdCrest(m,-1.2,5.15,0,7.2,3.1,1.55,3,CHITIN,BRD_SEAM,30303); | |
| /* The mortar. Pitched 55 degrees and yawed a full half-turn so it fires back | |
| over the animal's own abdomen — the one weapon here that does not point | |
| where the head does. */ | |
| brdMaw(m,0.4,5.70,0,{len:5.6,r0:1.15,r1:0.86,flare:0.62,lobes:6,rings:4, | |
| pitch:0.96,yaw:Math.PI,col:CHIT_D,seed:303,u:10,v:9}); | |
| brdLivery(m); | |
| brdCrest(m,3.4,4.30,0,2.8,2.6,0.90,3,BIO_TEAM,C(96,78,120),3033); | |
| m.team(0); | |
| brdRank(m,2.2,2.4,3.05,3.05,3,3.30,2.35,0.62,[2.35,1.95,2.20]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| chitinBlade(m,4.8,2.45,sd*1.2,2.0,0.28,-sd*0.30,sd*0.22,CLAW,0.02); | |
| tendril(m,5.0,3.70,sd*0.8,3.0,0.16,sd,1.4,BIO_LEG); | |
| m.sphere(-3.2,3.95,sd*1.8,1.00,6,BIO_TEAM,0.82,false); | |
| brdSpiracle(m,-1.6,4.15,sd*1.6,0.44,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-3.0,4.00,0,2.2,1.6,6,0.46,CHIT_D,30303); | |
| return {hull:m.build(),tur:null,s:1.0}; | |
| } | |
| /* ================= 6 — LANCESPINE (was mdlHordeBombardier) ================= | |
| Longbow slot: 110hp, 95dmg, 205px, NO splash. The most fragile thing in | |
| either group and a pure single-target shot, so it is all reach and no | |
| armour: the longest, lowest, narrowest body of the twelve, with one rigid | |
| harpoon running forward almost as far again as the body behind it. Length | |
| is the entire read; there is deliberately nothing tall on it. | |
| =========================================================================== */ | |
| function mdlBrdLancespine(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-1.4,2.30,0,13.6,2.65,1.75,CHITIN, | |
| {segs:8,ridge:0.18,bump:0.095,keel:0.10,nose:0.36,tail:0.30, | |
| waist:0.26,waistAt:0.50,waistW:0.070,seed:60606,u:14,v:16}); | |
| brdShell(m); | |
| carapace(m,5.9,2.30,0,3.2,1.65,1.25,CHIT_D, | |
| {segs:2,ridge:0.14,bump:0.13,keel:0.08,nose:0.32,tail:0.40,seed:606,u:12,v:8}); | |
| eyeCluster(m,6.7,2.45,0,0.44,2,HOT); | |
| /* THE HARPOON. Straight, unbelled, tapering to a needle over nine units. | |
| `socket:false` because this one is not a throat — nothing opens at the | |
| end of it, and a rolled rim would blunt the only line on the model. */ | |
| brdMaw(m,6.6,2.30,0,{len:9.0,r0:0.52,r1:0.055,flare:0,lobes:4,rings:7, | |
| taper:0.50,socket:false,pitch:0.015,col:CLAW, | |
| seam:C(56,54,44),seed:606,u:7,v:10}); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| /* Two launch rails bracing the harpoon's root — the part that says the | |
| spine is fired rather than bitten with. */ | |
| chitinBlade(m,6.4,2.30,sd*0.78,4.2,0.20,-sd*0.05,sd*0.045,CLAW,0.02); | |
| tendril(m,6.9,3.05,sd*0.7,3.4,0.15,sd,1.2,BIO_LEG); | |
| m.sphere(-4.4,3.05,sd*1.2,0.80,6,BIO_TEAM,0.84,false); | |
| brdSpiracle(m,-1.8,3.10,sd*1.1,0.36,CHIT_D); | |
| } | |
| brdRank(m,2.6,3.0,2.20,2.20,3,3.55,2.25,0.46,[1.60,1.35,1.50]); | |
| brdShell(m); | |
| tubercles(m,-4.8,2.95,0,2.6,1.0,5,0.34,CHIT_D,60606); | |
| return {hull:m.build(),tur:null,s:1.0}; | |
| } | |
| /* ================= 16 — SIEGEMOUND (was mdlHordeBombardier) ================ | |
| Bombard slot: 400px range at speed 14 — the slowest and longest-ranged unit | |
| in the game's ground roster. It should look like it barely walks: a squat | |
| broad mound carried LOW on eight short thick legs, with a near-vertical | |
| chimney throat as the only tall thing on it. Widest plan of the twelve. | |
| =========================================================================== */ | |
| function mdlBrdSiegemound(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.6,2.90,0,12.4,6.30,3.30,CHITIN, | |
| {segs:5,ridge:0.28,bump:0.13,keel:0.20,nose:0.50,tail:0.34, | |
| waist:0.20,waistAt:0.62,waistW:0.10,seed:16016,u:14,v:13}); | |
| brdShell(m); | |
| carapace(m,5.8,2.70,0,3.4,2.5,1.60,CHIT_D, | |
| {segs:2,ridge:0.18,bump:0.15,keel:0.10,nose:0.34,tail:0.42,seed:1601,u:9,v:6}); | |
| eyeCluster(m,6.6,2.90,0,0.54,2,HOT); | |
| /* THE CHIMNEY. Nine units of near-vertical throat with a heavy bell, set | |
| back over the animal's centre of mass. Everything else on this model is | |
| deliberately short so that this is the only vertical line. */ | |
| brdMaw(m,-1.0,4.60,0,{len:8.6,r0:1.70,r1:1.05,flare:0.75,lobes:7,rings:5, | |
| pitch:1.40,col:CHIT_D,seed:1601,u:11,v:10}); | |
| /* A buttress collar where the chimney leaves the shell — a nine-unit organ | |
| growing straight out of a smooth back reads as bolted on. */ | |
| brdShell(m); | |
| brdCrest(m,-1.0,4.15,0,5.4,4.6,1.15,3,CHITIN,BRD_SEAM,16016); | |
| brdLivery(m); | |
| brdCrest(m,4.0,3.90,0,3.2,3.4,0.95,3,BIO_TEAM,C(96,78,120),1601); | |
| m.team(0); | |
| /* EIGHT short thick legs. A mound needs many small supports, not four long | |
| ones; the count is also what stops it reading as a building. */ | |
| brdRank(m,3.4,2.3,2.70,2.70,4,2.85,1.95,0.66,[3.30,2.85,2.60,2.90]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| chitinBlade(m,6.2,2.35,sd*1.4,2.2,0.34,-sd*0.30,sd*0.24,CLAW,0.02); | |
| tendril(m,6.6,3.55,sd*0.9,2.8,0.16,sd,1.2,BIO_LEG); | |
| m.sphere(-5.0,3.85,sd*2.0,1.15,6,BIO_TEAM,0.80,false); | |
| brdSpiracle(m,2.0,3.95,sd*2.1,0.52,CHIT_D); | |
| for(let k=0;k<2;k++) | |
| chitinBlade(m,1.4-k*2.6,3.95,sd*(3.0+k*0.2),2.6,0.34,sd*0.28,Math.PI*0.84,CLAW,0.30); | |
| } | |
| brdShell(m); | |
| tubercles(m,1.8,3.70,0,3.0,2.4,8,0.54,CHIT_D,16016); | |
| return {hull:m.build(),tur:null,s:1.10}; | |
| } | |
| /* ================== 22 — RAILFANG (was mdlHordeBombardier) ================= | |
| Lancer slot: 150 damage at 230px on a 900-unit projectile — a hypervelocity | |
| anti-armour shot. The organ that throws it is braced: a long forward lance | |
| with four struts fanning back from its root into the shoulders, and the | |
| whole body raked nose-down along the shot line. Slim, forward-leaning, and | |
| the only silhouette in the twelve with a visible frame around its weapon. | |
| =========================================================================== */ | |
| function mdlBrdRailfang(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.8,3.35,0,11.8,2.95,2.05,CHITIN, | |
| {segs:6,ridge:0.20,bump:0.10,keel:0.22,nose:0.44,tail:0.28, | |
| waist:0.30,waistAt:0.52,waistW:0.072,seed:22022,u:12,v:14}); | |
| brdShell(m); | |
| carapace(m,5.4,3.05,0,3.2,1.9,1.40,CHIT_D, | |
| {segs:2,ridge:0.16,bump:0.14,keel:0.10,nose:0.34,tail:0.42,seed:2202,u:9,v:6}); | |
| eyeCluster(m,6.2,3.25,0,0.48,2,HOT); | |
| /* THE LANCE. Long, slim, barely belled, raked slightly down along the shot | |
| line so the model leans into its own shot. */ | |
| brdMaw(m,5.8,3.35,0,{len:7.4,r0:0.66,r1:0.30,flare:0.35,lobes:5,rings:6, | |
| taper:0.66,pitch:-0.10,col:CHIT_D,seed:2202,u:9,v:10}); | |
| /* THE BRACE. Four struts from the lance root back into the shoulders. They | |
| are the difference between "a spike" and "a weapon that recoils". */ | |
| brdLimb(m); | |
| for(let k=0;k<4;k++){ | |
| const a=k/4*TAU+Math.PI/4; | |
| chitinBlade(m,5.6+Math.cos(a)*0.0,3.35+Math.sin(a)*0.90,Math.cos(a)*1.05, | |
| 4.4,0.24,0,Math.PI*(a>Math.PI/2&&a<Math.PI*1.5?1.02:0.98),CLAW, | |
| -Math.sin(a)*0.30); | |
| } | |
| brdLivery(m); | |
| brdCrest(m,1.2,4.55,0,4.6,2.6,1.05,3,BIO_TEAM,C(96,78,120),2202); | |
| m.team(0); | |
| brdRank(m,2.6,2.7,3.20,3.20,3,3.55,2.45,0.58,[1.85,1.55,1.75]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,6.2,4.10,sd*0.8,3.8,0.17,sd,1.7,BIO_LEG); | |
| m.sphere(-4.0,4.15,sd*1.4,0.88,6,BIO_TEAM,0.84,false); | |
| brdSpiracle(m,-2.0,4.25,sd*1.2,0.40,CHIT_D); | |
| } | |
| for(let k=0;k<3;k++) | |
| chitinBlade(m,-2.0-k*1.9,4.35-k*0.16,0,2.2-k*0.22,0.30,0.10,Math.PI*0.96,CLAW,0.80); | |
| brdShell(m); | |
| tubercles(m,-3.6,4.05,0,2.4,1.2,5,0.38,CHIT_D,22022); | |
| return {hull:m.build(),tur:null,s:1.0}; | |
| } | |
| /* ================== 26 — BASILISK (was mdlHordeBombardier) ================= | |
| Tier 3, 1100hp, 120 damage at 190px — the heaviest thing either group | |
| fields. It is the only unit in the twelve that gets a HEAD as its dominant | |
| feature: a crowned skull-plate wider than the body, with one great eye at | |
| its centre instead of a cluster. Eight limbs, tall stance, long body. | |
| =========================================================================== */ | |
| function mdlBrdBasilisk(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-1.2,4.65,0,16.4,5.90,3.60,CHITIN, | |
| {segs:7,ridge:0.28,bump:0.125,keel:0.30,nose:0.50,tail:0.20, | |
| waist:0.34,waistAt:0.56,waistW:0.068,seed:26026,u:15,v:18}); | |
| brdShell(m); | |
| carapace(m,8.0,4.40,0,5.4,3.5,2.30,CHIT_D, | |
| {segs:3,ridge:0.20,bump:0.16,keel:0.14,nose:0.36,tail:0.44,seed:2602,u:11,v:8}); | |
| /* THE CROWN. Wider than the head AND wider than the body, so the plan | |
| silhouette is a spade — a shape nothing else in the twelve makes. */ | |
| brdShell(m); | |
| brdCrest(m,7.2,6.15,0,6.6,5.4,1.90,3,CHITIN,BRD_SEAM,26026); | |
| /* ONE eye, not a cluster. At tier 3 the face has to be a face. */ | |
| m.sphere(9.9,4.85,0,1.30,8,HOT,0.92,false); | |
| brdLimb(m); | |
| for(let k=0;k<5;k++){ | |
| const q=k/4-0.5; | |
| chitinBlade(m,7.6-Math.abs(q)*1.0,7.05,q*4.4,3.6-Math.abs(q)*1.1,0.40,q*0.36, | |
| Math.PI*(0.5+q*0.9),CLAW,0.96); // crown horns | |
| } | |
| /* Twin flank throats set BACK under the crown, so the head stays the read. */ | |
| for(const sd of [-1,1]) | |
| brdMaw(m,8.6,3.70,sd*1.85,{len:4.4,r0:0.80,r1:0.52,flare:0.85,lobes:5,rings:4, | |
| pitch:-0.10,yaw:sd*0.22,col:CHIT_D,seed:2602+sd,u:9,v:8}); | |
| brdLivery(m); | |
| brdCrest(m,3.0,6.35,0,4.6,3.8,1.30,3,BIO_TEAM,C(96,78,120),2602); | |
| m.team(0); | |
| brdRank(m,4.4,2.9,4.50,4.50,4,4.55,3.45,0.90,[3.55,2.95,3.40,3.10]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| chitinBlade(m,9.2,3.35,sd*2.1,4.4,0.56,-sd*0.42,sd*0.26,CLAW,0.04); | |
| tendril(m,9.6,5.85,sd*1.4,5.6,0.24,sd,2.5,BIO_LEG); | |
| m.sphere(-5.4,6.00,sd*2.5,1.45,7,BIO_TEAM,0.80,false); | |
| brdSpiracle(m,-2.4,6.25,sd*2.0,0.62,CHIT_D); | |
| chitinBlade(m,1.6,6.10,sd*3.2,4.4,0.46,sd*0.30,Math.PI*0.84,CLAW,0.48); | |
| } | |
| for(let k=0;k<5;k++) | |
| chitinBlade(m,-2.4-k*1.9,6.55-k*0.24,0,2.8+Math.sin(k/4*Math.PI)*1.2,0.40,0.10, | |
| Math.PI*0.96,CLAW,0.90); | |
| brdShell(m); | |
| tubercles(m,-5.0,6.10,0,3.6,2.4,10,0.66,CHIT_D,26026); | |
| return {hull:m.build(),tur:null,s:1.10}; | |
| } | |
| /* ================= 27 — HARBINGER (was mdlHordeBombardier) ================= | |
| Tier 3, 210px, ground only, 760hp on a 24-size chassis — a lightly built | |
| long-range area caster. It is the inverse of the Siegemound: almost no body, | |
| carried absurdly high on six very long limbs, with a spore bell HANGING | |
| under a forward-reaching stalk and a curtain of tendrils trailing beneath | |
| it. Tallest of the twelve, and the only one whose mass is above its legs' | |
| knees rather than slung between them. | |
| =========================================================================== */ | |
| function mdlBrdHarbinger(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.6,6.40,0,10.2,3.30,2.30,CHITIN, | |
| {segs:6,ridge:0.20,bump:0.10,keel:0.24,nose:0.44,tail:0.30, | |
| waist:0.32,waistAt:0.52,waistW:0.075,seed:27027,u:12,v:14}); | |
| brdShell(m); | |
| carapace(m,4.6,6.30,0,2.8,1.8,1.35,CHIT_D, | |
| {segs:2,ridge:0.16,bump:0.14,keel:0.10,nose:0.34,tail:0.42,seed:2702,u:9,v:6}); | |
| eyeCluster(m,5.3,6.50,0,0.50,2,LAMP); | |
| /* THE GANTRY. A stalk reaching forward and up off the shoulders; the bell | |
| hangs from its far end, so the organ swings out beyond the head instead of | |
| sitting on the back. Everything below the top bone rides it. */ | |
| const NP=[[0.4,7.30,0],[2.4,8.60,0],[4.4,9.30,0],[6.0,9.40,0]]; | |
| const top=brdStalk(m,NP,0.60,0.40,0.30,[0,0,1]); | |
| m.bone(top); | |
| brdLivery(m); | |
| brdBulb(m,6.0,8.90,0,2.05,6,BIO_TEAM,27027); // hangs DOWN off the stalk | |
| m.team(0); | |
| m.bone(top); | |
| m.sphere(6.0,9.65,0,0.62,6,LAMP,0.90,false); | |
| m.bone(top); | |
| brdLimb(m); | |
| for(let k=0;k<6;k++){ | |
| const a=k/6*TAU; | |
| tendril(m,6.0+Math.cos(a)*1.55,6.90,Math.sin(a)*1.70,4.6,0.16, | |
| Math.sin(a)>0?1:-1,-1.5,BIO_LEG); // the curtain | |
| } | |
| m.bone(-1); | |
| /* Six very long limbs. The body rides at 6.4 while the knees break above | |
| 10 — the animal hangs inside its own legs, and that stilt read is the | |
| whole silhouette from any angle. */ | |
| brdRank(m,2.0,2.9,6.30,6.30,3,5.30,4.30,0.52,[1.95,1.60,1.85]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,-4.8,6.90,sd*0.9,6.4,0.18,sd,1.4,BIO_LEG); | |
| m.sphere(-2.6,7.35,sd*1.4,0.92,6,BIO_TEAM,0.84,false); | |
| brdSpiracle(m,-0.6,7.45,sd*1.2,0.40,CHIT_D); | |
| } | |
| for(let k=0;k<3;k++) | |
| chitinBlade(m,-1.6-k*1.9,7.55-k*0.18,0,2.4-k*0.24,0.30,0.10,Math.PI*0.96,CLAW,0.86); | |
| brdShell(m); | |
| tubercles(m,-3.4,7.20,0,2.2,1.2,5,0.38,CHIT_D,27027); | |
| return {hull:m.build(),tur:null,s:1.12}; | |
| } | |
| /* ============================================================================ | |
| THE FOURTEEN — SPLITTING THE FIVE REMAINING SHARED BUILDERS | |
| ---------------------------------------------------------------------------- | |
| After the Spitter/Bombardier split, FAC_KIT.horde still mapped fourteen | |
| slots onto five builders called with NO arguments: | |
| mdlHordeBeast 0, 9 | |
| mdlHordeLeviathan 4, 8, 18 | |
| mdlHordeFlyer 5, 17, 25 | |
| mdlHordeSupport 11, 19, 23, 24 | |
| mdlHordeSwimmer 14, 15 | |
| Same failure mode as the twelve: a 40hp skirmisher and a 210hp flamethrower | |
| were one vertex buffer; a shield, a builder, a sonic gun and a medic were | |
| one vertex buffer; three aircraft were one pair of wings. | |
| Same rule: proportion, not decoration. Plan / height / one dominant feature. | |
| Unarmed slots (11, 19, 24) get no maw — a builder that looks armed is a lie | |
| the player will walk into a fight. Air slots return air:1 so the renderer | |
| lifts them. Swimmers have no walking legs. | |
| slot animal was plan stance dominant feature | |
| 0 Skitterling Beast SHORT + NARROW low dart mandibles | |
| 9 Brandmaw Beast SHORT + WIDE low open furnace mouth | |
| 4 Crownbeast Leviathan long + WIDE TALL ring of sensory stalks | |
| 8 Worldshell Leviathan VERY LONG+WIDE TALL stacked dorsal plates | |
| 18 Furnaceback Leviathan wide + squat LOW twin flank heat sacs | |
| 5 Stingwing Flyer compact DELTA air triangular wings + sting | |
| 17 Sacfly Flyer round + FAT air hanging bomb sac | |
| 25 Needlewren Flyer long + NARROW air swept needle wings | |
| 11 Bastioncrab Support SHORT + WIDE low disc shield, NO weapon | |
| 19 Weaver Support mid + arms mid reaching spinnerets | |
| 23 Drumback Support round mid paired tympanic plates | |
| 24 Ichorleech Support NARROW low palps + flank sacs | |
| 14 Razorfinn Swimmer long + NARROW LOW one tall dorsal keel | |
| 15 Keelback Swimmer VERY LONG+WIDE LOW row of chimney spines | |
| ============================================================================ */ | |
| /* ===================== 0 — SKITTERLING (was mdlHordeBeast) ================= | |
| Striker slot: tier 1, 40hp, 5.4dmg at 62px, spd 38. Fastest ground unit and | |
| the first thing that dies. SHORT and NARROW, carried LOW, with the only | |
| mass that breaks the plan being a pair of dart mandibles past the head — | |
| a needle, which is the one plan nothing else in this fourteen makes. | |
| =========================================================================== */ | |
| function mdlBrdSkitterling(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.2,2.05,0,7.4,2.35,1.55,CHITIN, | |
| {segs:5,ridge:0.18,bump:0.10,keel:0.12,nose:0.38,tail:0.30, | |
| waist:0.22,waistAt:0.52,waistW:0.090,seed:101,u:16,v:16}); | |
| brdShell(m); | |
| carapace(m,3.4,1.95,0,2.2,1.45,1.05,CHIT_D, | |
| {segs:2,ridge:0.14,bump:0.12,keel:0.08,nose:0.32,tail:0.40,seed:10,u:12,v:8}); | |
| eyeCluster(m,4.0,2.15,0,0.38,2,HOT); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| chitinBlade(m,4.2,1.70,sd*0.55,3.2,0.22,-sd*0.18,sd*0.10,CLAW,0.02); // dart mandibles | |
| tendril(m,4.1,2.55,sd*0.55,2.4,0.12,sd,1.6,BIO_LEG); | |
| brdSpiracle(m,-1.6,2.55,sd*0.85,0.28,CHIT_D); | |
| } | |
| brdLivery(m); | |
| brdCrest(m,1.6,2.55,0,2.4,1.8,0.55,3,BIO_TEAM,C(96,78,120),101); | |
| m.team(0); | |
| brdRank(m,1.6,1.7,1.95,1.95,3,2.15,1.45,0.38,[1.25,1.05,1.15]); | |
| brdShell(m); | |
| tubercles(m,-2.0,2.40,0,1.4,0.9,4,0.28,CHIT_D,101); | |
| return {hull:m.build(),tur:null,s:0.92}; | |
| } | |
| /* ====================== 9 — BRANDMAW (was mdlHordeBeast) =================== | |
| Pyro slot: tier 1, 210hp, 11dmg at 58px, splash 18, melee-typed incendiary. | |
| SHORT and WIDE, LOW, with one huge open furnace mouth filling the front of | |
| the plan. Emberthroat (21) already owns the vane-fan + drooping gullet; | |
| this is the squat ground-level maw that slot is not. | |
| =========================================================================== */ | |
| function mdlBrdBrandmaw(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.4,2.45,0,8.6,4.70,2.25,CHITIN, | |
| {segs:4,ridge:0.22,bump:0.12,keel:0.10,nose:0.48,tail:0.34, | |
| waist:0.20,waistAt:0.58,waistW:0.10,seed:909,u:16,v:14}); | |
| brdShell(m); | |
| carapace(m,3.8,2.30,0,2.6,2.4,1.35,CHIT_D, | |
| {segs:2,ridge:0.16,bump:0.14,keel:0.08,nose:0.36,tail:0.40,seed:90,u:12,v:8}); | |
| eyeCluster(m,4.4,2.55,0,0.48,2,HOT); | |
| brdMaw(m,4.6,2.05,0,{len:4.0,r0:1.18,r1:0.92,flare:1.35,lobes:6,rings:3, | |
| pitch:-0.12,col:CHIT_D,seed:90,u:12,v:10}); | |
| brdLivery(m); | |
| for(const sd of [-1,1]) m.sphere(-1.8,3.15,sd*1.7,0.95,6,BIO_TEAM,0.84,false); | |
| m.team(0); | |
| brdRank(m,1.8,2.2,2.30,2.30,3,2.70,1.90,0.54,[2.45,2.15,2.25]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| chitinBlade(m,4.4,1.85,sd*1.5,2.0,0.32,-sd*0.28,sd*0.22,CLAW,0.04); | |
| tendril(m,4.6,3.05,sd*1.0,2.6,0.15,sd,1.1,BIO_LEG); | |
| brdSpiracle(m,-0.6,3.25,sd*1.7,0.40,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-2.4,3.05,0,2.0,1.6,5,0.42,CHIT_D,909); | |
| return {hull:m.build(),tur:null,s:1.0}; | |
| } | |
| /* ==================== 4 — CROWNBEAST (was mdlHordeLeviathan) =============== | |
| Commander chassis fallback: size 32, 5200hp. Brood's live hero is the | |
| Sovereign at slot 30; this is what type 4 becomes if the horde kit ever | |
| fields it. Long + WIDE + TALL, with a RING of sensory stalks on the thorax | |
| — a crown, not scythes (those are the Sovereign) and not stacked plates | |
| (those are the Worldshell). | |
| =========================================================================== */ | |
| function mdlBrdCrownbeast(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-1.2,5.10,0,16.4,6.20,3.70,CHITIN, | |
| {segs:6,ridge:0.26,bump:0.12,keel:0.22,nose:0.48,tail:0.22, | |
| waist:0.30,waistAt:0.54,waistW:0.072,seed:404,u:14,v:16}); | |
| brdShell(m); | |
| carapace(m,7.0,4.80,0,4.2,3.0,2.05,CHIT_D, | |
| {segs:3,ridge:0.18,bump:0.14,keel:0.14,nose:0.36,tail:0.42,seed:40,u:10,v:8}); | |
| eyeCluster(m,8.2,5.15,0,0.78,3,HOT); | |
| /* THE CROWN. Five short stalks in a ring, each capped with a livery bulb. | |
| From above that is a pentagon of lights sitting on a wide thorax — the | |
| command read, and the only plan in this group with a hole in the middle. */ | |
| for(let k=0;k<5;k++){ | |
| const a=k/5*TAU+0.2, cx=Math.cos(a)*1.7, cz=Math.sin(a)*1.7; | |
| const NP=[[0.4+cx,6.6,cz],[0.4+cx*1.15,8.0,cz*1.15]]; | |
| const top=brdStalk(m,NP,0.42,0.28,0.18+k*0.4,[0,0,1]); | |
| m.bone(top); | |
| brdLivery(m); | |
| brdBulb(m,NP[1][0],NP[1][1]+0.15,NP[1][2],0.72,5,BIO_TEAM,404+k); | |
| m.bone(-1); | |
| m.team(0); | |
| } | |
| brdMaw(m,8.0,4.40,0,{len:4.4,r0:0.86,r1:0.52,flare:0.70,lobes:5,rings:4, | |
| pitch:-0.04,col:CHIT_D,seed:40,u:9,v:8}); | |
| brdRank(m,3.2,3.1,4.90,4.90,4,4.40,3.40,0.62,[3.20,2.70,2.90,3.10]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,8.4,6.10,sd*1.1,4.8,0.18,sd,1.8,BIO_LEG); | |
| m.sphere(-4.2,6.35,sd*2.4,1.20,6,BIO_TEAM,0.82,false); | |
| brdSpiracle(m,-1.2,6.55,sd*2.2,0.50,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-5.0,6.20,0,3.2,2.2,8,0.58,CHIT_D,404); | |
| return {hull:m.build(),tur:null,s:1.12}; | |
| } | |
| /* ==================== 8 — WORLDSHELL (was mdlHordeLeviathan) =============== | |
| TITAN slot: tier 3, 14000hp, size 46, spd 15, beam. The heaviest thing the | |
| kit can field. VERY LONG and VERY WIDE, carried TALL on four thick pairs, | |
| with three overlapping dorsal plates that make the plan a fortress oval. | |
| The head is small on purpose: this animal is a walking hill, not a face. | |
| =========================================================================== */ | |
| function mdlBrdWorldshell(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-1.6,6.60,0,22.0,8.40,5.10,CHITIN, | |
| {segs:8,ridge:0.30,bump:0.13,keel:0.28,nose:0.50,tail:0.18, | |
| waist:0.26,waistAt:0.56,waistW:0.065,seed:808,u:16,v:18}); | |
| brdShell(m); | |
| carapace(m,9.4,6.10,0,4.6,3.2,2.20,CHIT_D, | |
| {segs:3,ridge:0.18,bump:0.14,keel:0.14,nose:0.34,tail:0.44,seed:80,u:10,v:8}); | |
| eyeCluster(m,10.6,6.50,0,0.70,2,HOT); | |
| brdShell(m); | |
| brdCrest(m,2.4,10.4,0,10.4,6.6,2.60,3,CHITIN,BRD_SEAM,808); | |
| brdCrest(m,-4.2,9.6,0,8.8,7.2,2.20,3,CHITIN,BRD_SEAM,818); | |
| brdCrest(m,-9.0,8.6,0,6.4,5.8,1.70,3,CHIT_D,BRD_SEAM,828); | |
| brdMaw(m,10.4,5.70,0,{len:5.6,r0:0.92,r1:0.48,flare:0.40,lobes:4,rings:5, | |
| pitch:0.04,col:CHIT_D,seed:80,u:9,v:9}); | |
| brdLivery(m); | |
| brdCrest(m,6.6,8.4,0,4.2,3.6,1.20,3,BIO_TEAM,C(96,78,120),80); | |
| m.team(0); | |
| brdRank(m,4.4,3.6,6.40,6.40,4,5.20,4.10,0.78,[4.20,3.60,3.80,4.00]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| chitinBlade(m,10.0,5.40,sd*1.4,3.2,0.44,-sd*0.26,sd*0.18,CLAW,0.06); | |
| tendril(m,10.6,7.40,sd*1.2,5.6,0.20,sd,1.6,BIO_LEG); | |
| m.sphere(-6.4,8.20,sd*3.2,1.55,6,BIO_TEAM,0.82,false); | |
| brdSpiracle(m,-2.0,8.55,sd*3.0,0.58,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-7.2,8.10,0,4.2,3.0,10,0.72,CHIT_D,808); | |
| return {hull:m.build(),tur:null,s:1.18}; | |
| } | |
| /* =================== 18 — FURNACEBACK (was mdlHordeLeviathan) ============== | |
| Scorcher slot: tier 2, incendiary, 640hp, 13dmg at 80px, splash 38. Wide | |
| and squat, LOW, with twin heat sacs sitting OUTBOARD of the thorax so the | |
| plan is a body with two bulbs — not Brandmaw's one mouth, not Emberthroat's | |
| dorsal fan. | |
| =========================================================================== */ | |
| function mdlBrdFurnaceback(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.6,3.15,0,12.4,5.60,2.85,CHITIN, | |
| {segs:5,ridge:0.22,bump:0.12,keel:0.14,nose:0.46,tail:0.30, | |
| waist:0.24,waistAt:0.56,waistW:0.080,seed:1818,u:13,v:14}); | |
| brdShell(m); | |
| carapace(m,5.6,2.95,0,3.4,2.4,1.55,CHIT_D, | |
| {segs:2,ridge:0.16,bump:0.14,keel:0.10,nose:0.34,tail:0.42,seed:181,u:9,v:7}); | |
| eyeCluster(m,6.5,3.20,0,0.52,2,HOT); | |
| brdLivery(m); | |
| for(const sd of [-1,1]) brdBulb(m,-1.2,4.55,sd*3.15,1.85,6,BIO_TEAM,1818+sd); | |
| m.team(0); | |
| for(const sd of [-1,1]) | |
| brdMaw(m,6.2,2.45,sd*0.95,{len:3.8,r0:0.78,r1:0.58,flare:1.10,lobes:6,rings:3, | |
| pitch:-0.42,yaw:sd*0.18,col:CHIT_D,seed:181+sd,u:9,v:8}); | |
| brdRank(m,2.4,2.7,3.00,3.00,3,3.20,2.20,0.58,[2.90,2.45,2.70]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,6.4,3.90,sd*1.0,3.2,0.16,sd,1.3,BIO_LEG); | |
| brdSpiracle(m,1.4,4.05,sd*2.0,0.44,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-4.0,3.90,0,2.6,1.8,6,0.48,CHIT_D,1818); | |
| return {hull:m.build(),tur:null,s:1.06}; | |
| } | |
| /* ====================== 5 — STINGWING (was mdlHordeFlyer) ================== | |
| Wasp slot: tier 1 air, 135hp, 13dmg at 78px, spd 74. Compact DELTA — the | |
| only triangular plan in the kit. Short sting on the nose, two swept | |
| forewings, tiny hind lobes. No legs; air:1 so the renderer lifts it. | |
| =========================================================================== */ | |
| function mdlBrdStingwing(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.4,1.15,0,8.2,2.55,1.70,CHITIN, | |
| {segs:5,ridge:0.18,bump:0.10,keel:0.16,nose:0.36,tail:0.28, | |
| waist:0.20,waistAt:0.50,waistW:0.090,seed:505,u:16,v:14}); | |
| brdShell(m); | |
| carapace(m,3.6,1.05,0,2.4,1.5,1.05,CHIT_D, | |
| {segs:2,ridge:0.14,bump:0.12,keel:0.08,nose:0.32,tail:0.40,seed:50,u:10,v:7}); | |
| eyeCluster(m,4.2,1.25,0,0.40,2,HOT); | |
| brdMaw(m,4.4,0.95,0,{len:3.0,r0:0.42,r1:0.18,flare:0.25,lobes:4,rings:4, | |
| pitch:0.02,col:CHIT_D,seed:50,u:8,v:8}); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| brdWing(m,-0.2,1.05,0,sd,7.6,5.4,0.38,0.38,BIO_MEM,C(46,44,30),505+sd); | |
| brdWing(m,-2.8,0.85,0,sd,4.6,3.6,0.28,0.22,BIO_MEM,C(46,44,30),515+sd); | |
| for(let k=0;k<4;k++) | |
| chitinBlade(m,-0.6-k*0.7,1.15,sd*(1.4+k*1.45),2.6-k*0.3,0.16,sd*0.08,sd*1.05,CLAW,0.04); | |
| tendril(m,4.2,1.55,sd*0.5,2.8,0.12,sd,1.8,BIO_LEG); | |
| m.sphere(-1.6,1.75,sd*1.0,0.55,6,BIO_TEAM,0.86,false); | |
| } | |
| brdLivery(m); | |
| brdCrest(m,1.4,1.85,0,2.6,1.6,0.50,3,BIO_TEAM,C(96,78,120),50); | |
| m.team(0); | |
| brdShell(m); | |
| tubercles(m,-2.4,1.65,0,1.6,0.9,4,0.30,CHIT_D,505); | |
| return {hull:m.build(),tur:null,s:0.96,air:1}; | |
| } | |
| /* ======================= 17 — SACFLY (was mdlHordeFlyer) =================== | |
| Raptor slot: tier 1 air, 280hp, 85dmg at 52px, splash 32. A bomber. Round | |
| and FAT in plan, with a gravid sac hanging UNDER the abdomen — the only | |
| flyer whose mass is below the wing line. Broad rounded wings, almost no | |
| sweep: a disc, not a dart. | |
| =========================================================================== */ | |
| function mdlBrdSacfly(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.2,1.35,0,10.2,4.20,2.40,CHITIN, | |
| {segs:4,ridge:0.18,bump:0.11,keel:0.10,nose:0.44,tail:0.40, | |
| waist:0.28,waistAt:0.42,waistW:0.085,seed:1717,u:16,v:14}); | |
| brdShell(m); | |
| carapace(m,4.6,1.20,0,2.8,2.0,1.25,CHIT_D, | |
| {segs:2,ridge:0.14,bump:0.13,keel:0.08,nose:0.34,tail:0.42,seed:171,u:10,v:7}); | |
| eyeCluster(m,5.3,1.40,0,0.46,2,HOT); | |
| brdLivery(m); | |
| brdBulb(m,-3.2,0.15,0,2.45,7,BIO_TEAM,1717); // hangs DOWN | |
| m.team(0); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| brdWing(m,-0.4,1.20,0,sd,9.6,6.2,0.42,0.10,BIO_MEM,C(46,44,30),1717+sd); | |
| brdWing(m,-3.2,1.00,0,sd,6.4,4.4,0.32,0.06,BIO_MEM,C(46,44,30),1727+sd); | |
| chitinBlade(m,5.0,1.00,sd*1.0,2.0,0.24,-sd*0.22,sd*0.16,CLAW,0.02); | |
| tendril(m,-3.2,-1.6,sd*1.4,2.8,0.14,sd,-0.8,BIO_LEG); | |
| m.sphere(-1.2,2.15,sd*1.5,0.72,6,BIO_TEAM,0.84,false); | |
| } | |
| brdMaw(m,5.4,1.05,0,{len:2.6,r0:0.55,r1:0.32,flare:0.50,lobes:5,rings:3, | |
| pitch:-0.18,col:CHIT_D,seed:171,u:8,v:7}); | |
| brdShell(m); | |
| tubercles(m,1.2,2.05,0,2.0,1.4,5,0.38,CHIT_D,1717); | |
| return {hull:m.build(),tur:null,s:1.04,air:1}; | |
| } | |
| /* ===================== 25 — NEEDLEWREN (was mdlHordeFlyer) ================= | |
| Kestrel slot: tier 1 air scout, 120hp, 14dmg at 150px, spd 96. Long and | |
| NARROW, the fastest thing in the sky. Swept needle wings whose span is | |
| three times the body width, plus a pair of antennae — a dragonfly, which | |
| is the one flyer plan that is not a triangle and not a disc. | |
| =========================================================================== */ | |
| function mdlBrdNeedlewren(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.6,1.05,0,9.6,1.90,1.35,CHITIN, | |
| {segs:6,ridge:0.16,bump:0.09,keel:0.18,nose:0.32,tail:0.24, | |
| waist:0.26,waistAt:0.50,waistW:0.075,seed:2525,u:16,v:14}); | |
| brdShell(m); | |
| carapace(m,4.2,0.95,0,2.0,1.15,0.90,CHIT_D, | |
| {segs:2,ridge:0.12,bump:0.11,keel:0.08,nose:0.30,tail:0.40,seed:252,u:9,v:6}); | |
| eyeCluster(m,4.7,1.15,0,0.34,2,LAMP); | |
| brdMaw(m,5.0,0.90,0,{len:2.4,r0:0.28,r1:0.12,flare:0.18,lobes:4,rings:4, | |
| pitch:0.06,col:CHIT_D,seed:252,u:7,v:7}); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| brdWing(m,0.6,0.95,0,sd,9.2,3.4,0.26,0.72,BIO_MEM,C(46,44,30),2525+sd); | |
| brdWing(m,-2.8,0.80,0,sd,7.4,2.8,0.22,0.55,BIO_MEM,C(46,44,30),2535+sd); | |
| tendril(m,4.8,1.35,sd*0.35,4.2,0.10,sd,2.4,BIO_LEG); // antennae | |
| m.sphere(-1.8,1.45,sd*0.7,0.42,6,BIO_TEAM,0.88,false); | |
| brdSpiracle(m,-3.2,1.40,sd*0.55,0.22,CHIT_D); | |
| } | |
| brdLivery(m); | |
| brdCrest(m,1.2,1.55,0,2.2,1.2,0.40,3,BIO_TEAM,C(96,78,120),252); | |
| m.team(0); | |
| brdShell(m); | |
| tubercles(m,-3.0,1.35,0,1.4,0.6,4,0.24,CHIT_D,2525); | |
| return {hull:m.build(),tur:null,s:0.94,air:1}; | |
| } | |
| /* ==================== 11 — BASTIONCRAB (was mdlHordeSupport) =============== | |
| Bulwark slot: tier 2, 950hp, UNARMED, size 21. A walking shield. SHORT and | |
| VERY WIDE, LOW, with one disc crest wider than the body and nothing that | |
| looks like a gun. An unarmed unit that looks armed is a lie the player | |
| will walk into a fight. | |
| =========================================================================== */ | |
| function mdlBrdBastioncrab(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.2,2.55,0,8.4,5.40,2.35,CHITIN, | |
| {segs:4,ridge:0.22,bump:0.12,keel:0.10,nose:0.48,tail:0.40, | |
| waist:0.18,waistAt:0.55,waistW:0.10,seed:1111,u:12,v:12}); | |
| brdShell(m); | |
| carapace(m,3.6,2.40,0,2.4,2.2,1.30,CHIT_D, | |
| {segs:2,ridge:0.14,bump:0.13,keel:0.08,nose:0.34,tail:0.42,seed:111,u:8,v:6}); | |
| eyeCluster(m,4.2,2.60,0,0.42,2,HOT); | |
| /* THE DISC. Wider than the body in both axes, low enough that it never | |
| makes the animal tall — from above this IS the silhouette. */ | |
| brdShell(m); | |
| brdCrest(m,1.4,3.85,0,8.8,8.2,1.70,3,CHITIN,BRD_SEAM,1111); | |
| brdLivery(m); | |
| brdCrest(m,3.8,3.25,0,3.2,3.6,0.70,3,BIO_TEAM,C(96,78,120),111); | |
| m.team(0); | |
| brdRank(m,2.0,2.3,2.40,2.40,3,2.85,1.95,0.56,[2.70,2.40,2.55]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,4.0,3.15,sd*1.0,2.4,0.14,sd,1.0,BIO_LEG); | |
| m.sphere(-2.2,3.25,sd*1.9,0.85,6,BIO_TEAM,0.84,false); | |
| brdSpiracle(m,-0.4,3.45,sd*1.8,0.42,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-2.0,3.20,0,2.2,1.8,6,0.44,CHIT_D,1111); | |
| return {hull:m.build(),tur:null,s:1.04}; | |
| } | |
| /* ====================== 19 — WEAVER (was mdlHordeSupport) ================== | |
| Constructor slot: tier 1 builder, 220hp, UNARMED, spd 44. Mid-length with | |
| two long spinneret arms reaching past the head and a silk bulb on the | |
| back. The rasp is blunt and recessed — same contract as the Grub: nothing | |
| here protrudes like a weapon. | |
| =========================================================================== */ | |
| function mdlBrdWeaver(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.4,2.85,0,9.6,3.20,2.10,CHITIN, | |
| {segs:5,ridge:0.18,bump:0.10,keel:0.14,nose:0.40,tail:0.32, | |
| waist:0.24,waistAt:0.52,waistW:0.085,seed:1919,u:15,v:15}); | |
| brdShell(m); | |
| carapace(m,4.4,2.70,0,2.6,1.8,1.20,CHIT_D, | |
| {segs:2,ridge:0.14,bump:0.12,keel:0.08,nose:0.32,tail:0.40,seed:191,u:12,v:8}); | |
| eyeCluster(m,5.1,2.90,0,0.44,2,HOT); | |
| brdRasp(m,5.4,2.55,0,0.85,BRD_SOFT); | |
| brdLivery(m); | |
| brdBulb(m,-3.4,4.35,0,1.55,6,BIO_TEAM,1919); | |
| m.team(0); | |
| /* Spinneret arms — long, forward, NO blades. Two reaching tendrils are | |
| the builder read at 40px; a claw here would look armed. */ | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,4.2,3.05,sd*1.35,5.2,0.22,sd,0.6,BIO_LEG); | |
| tendril(m,4.6,2.70,sd*0.85,4.4,0.16,sd,0.2,BIO_LEG); | |
| } | |
| brdRank(m,2.0,2.4,2.70,2.70,3,3.00,2.10,0.50,[1.75,1.45,1.65]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,5.0,3.50,sd*0.7,3.2,0.14,sd,1.5,BIO_LEG); | |
| brdSpiracle(m,-1.0,3.55,sd*1.2,0.36,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-2.6,3.45,0,1.8,1.2,5,0.36,CHIT_D,1919); | |
| return {hull:m.build(),tur:null,s:0.98}; | |
| } | |
| /* ===================== 23 — DRUMBACK (was mdlHordeSupport) ================= | |
| Resonator slot: tier 2, sonic, 340hp, 34dmg at 130px, splash 18. Round in | |
| plan, with a paired tympanic plate standing off each flank — two vertical | |
| discs that make an H from above. That is the sonic read, and nothing else | |
| in the fourteen makes it. | |
| =========================================================================== */ | |
| function mdlBrdDrumback(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.4,3.05,0,10.2,4.40,2.55,CHITIN, | |
| {segs:5,ridge:0.20,bump:0.11,keel:0.12,nose:0.44,tail:0.34, | |
| waist:0.22,waistAt:0.54,waistW:0.085,seed:2323,u:13,v:13}); | |
| brdShell(m); | |
| carapace(m,4.6,2.90,0,2.8,2.0,1.40,CHIT_D, | |
| {segs:2,ridge:0.14,bump:0.13,keel:0.08,nose:0.34,tail:0.42,seed:232,u:9,v:6}); | |
| eyeCluster(m,5.3,3.10,0,0.48,2,HOT); | |
| /* THE DRUMS. Two crests stood on the flanks and rolled outboard, so the | |
| plan silhouette breaks twice — an H, not a wedge and not a disc. */ | |
| for(const sd of [-1,1]){ | |
| brdShell(m); | |
| brdCrest(m,-0.4,4.55,sd*2.85,5.6,3.2,2.10,3,CHITIN,BRD_SEAM,2323+sd); | |
| } | |
| brdMaw(m,5.4,2.70,0,{len:3.4,r0:0.72,r1:0.50,flare:0.85,lobes:6,rings:3, | |
| pitch:0.08,col:CHIT_D,seed:232,u:9,v:8}); | |
| brdLivery(m); | |
| brdCrest(m,3.2,4.10,0,2.8,2.4,0.80,3,BIO_TEAM,C(96,78,120),232); | |
| m.team(0); | |
| brdRank(m,2.2,2.5,2.90,2.90,3,3.15,2.20,0.54,[2.30,1.95,2.15]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,5.2,3.70,sd*0.9,3.0,0.15,sd,1.3,BIO_LEG); | |
| m.sphere(-2.8,3.85,sd*1.7,0.90,6,BIO_TEAM,0.84,false); | |
| brdSpiracle(m,-0.8,4.05,sd*1.6,0.40,CHIT_D); | |
| } | |
| brdShell(m); | |
| tubercles(m,-3.2,3.75,0,2.2,1.6,5,0.42,CHIT_D,2323); | |
| return {hull:m.build(),tur:null,s:1.02}; | |
| } | |
| /* ==================== 24 — ICHORLEECH (was mdlHordeSupport) ================ | |
| Warden slot: tier 2 medic, 420hp, UNARMED. NARROW body, LOW, with a pair | |
| of palps reaching past the head and a rank of small healing sacs along | |
| each flank. Slim + palps: the only support plan that is neither a shield, | |
| nor reaching arms, nor an H. | |
| =========================================================================== */ | |
| function mdlBrdIchorleech(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.4,2.55,0,10.0,2.70,1.95,CHITIN, | |
| {segs:6,ridge:0.16,bump:0.10,keel:0.14,nose:0.36,tail:0.28, | |
| waist:0.26,waistAt:0.50,waistW:0.080,seed:2424,u:13,v:13}); | |
| brdShell(m); | |
| carapace(m,4.6,2.40,0,2.4,1.6,1.10,CHIT_D, | |
| {segs:2,ridge:0.12,bump:0.12,keel:0.08,nose:0.32,tail:0.40,seed:242,u:8,v:6}); | |
| eyeCluster(m,5.2,2.60,0,0.40,2,LAMP); | |
| brdRasp(m,5.5,2.30,0,0.70,BRD_SOFT); | |
| brdLivery(m); | |
| for(const sd of [-1,1]){ | |
| for(let k=0;k<4;k++) | |
| brdBulb(m,1.6-k*1.7,3.35,sd*1.55,0.62,5,BIO_TEAM,2424+k+sd); | |
| } | |
| m.team(0); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| tendril(m,5.0,2.70,sd*0.55,4.6,0.16,sd,0.4,BIO_LEG); // palps | |
| tendril(m,5.2,2.90,sd*0.35,3.8,0.12,sd,0.8,BIO_LEG); | |
| } | |
| brdRank(m,2.0,2.3,2.40,2.40,3,2.80,1.95,0.46,[1.45,1.20,1.35]); | |
| brdLimb(m); | |
| for(const sd of [-1,1]) brdSpiracle(m,-1.4,3.15,sd*1.0,0.32,CHIT_D); | |
| brdShell(m); | |
| tubercles(m,-3.0,3.05,0,1.6,0.9,4,0.30,CHIT_D,2424); | |
| return {hull:m.build(),tur:null,s:0.96}; | |
| } | |
| /* ==================== 14 — RAZORFINN (was mdlHordeSwimmer) ================= | |
| Corvette slot: tier 1 naval, 320hp, 24dmg at 115px, spd 42. Long and | |
| NARROW, LOW in the water, with one tall dorsal keel. No walking legs — a | |
| knife, which is the one naval plan the Dreadnought is not. | |
| =========================================================================== */ | |
| function mdlBrdRazorfinn(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-0.8,1.25,0,14.4,3.05,1.75,CHITIN, | |
| {segs:7,ridge:0.20,bump:0.10,keel:0.22,nose:0.34,tail:0.22, | |
| waist:0.18,waistAt:0.52,waistW:0.080,seed:1414,u:16,v:12}); | |
| brdShell(m); | |
| carapace(m,6.4,1.15,0,3.0,1.8,1.15,CHIT_D, | |
| {segs:2,ridge:0.14,bump:0.12,keel:0.10,nose:0.30,tail:0.40,seed:141,u:10,v:7}); | |
| eyeCluster(m,7.2,1.40,0,0.44,2,HOT); | |
| brdFin(m,-0.6,2.15,0,8.4,4.6,0.42,-0.22,CHITIN,BRD_SEAM,1414); | |
| brdMaw(m,7.4,1.05,0,{len:3.6,r0:0.58,r1:0.32,flare:0.45,lobes:4,rings:4, | |
| pitch:0.04,col:CHIT_D,seed:141,u:8,v:8}); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| brdWing(m,-0.4,0.85,0,sd,4.8,6.4,0.28,0.18,BIO_MEM,C(46,44,30),1414+sd); | |
| tendril(m,7.0,1.65,sd*0.7,3.2,0.14,sd,1.2,BIO_LEG); | |
| m.sphere(-3.6,1.85,sd*1.1,0.62,6,BIO_TEAM,0.84,false); | |
| brdSpiracle(m,-1.2,2.00,sd*1.0,0.34,CHIT_D); | |
| } | |
| brdLivery(m); | |
| brdCrest(m,3.2,2.05,0,2.8,1.8,0.55,3,BIO_TEAM,C(96,78,120),141); | |
| m.team(0); | |
| brdShell(m); | |
| tubercles(m,-4.4,1.85,0,2.4,1.2,6,0.36,CHIT_D,1414); | |
| return {hull:m.build(),tur:null,s:1.02}; | |
| } | |
| /* ==================== 15 — KEELBACK (was mdlHordeSwimmer) ================== | |
| Dreadnought slot: tier 2 naval, 1300hp, 88dmg at 290px, splash 34, spd 20. | |
| VERY LONG and WIDE, LOW, with a row of five chimney throats along the | |
| spine — a barge that fires, which is the one naval plan the knife is not. | |
| =========================================================================== */ | |
| function mdlBrdKeelback(){ | |
| const m=MB(); | |
| brdShell(m); | |
| carapace(m,-1.2,1.45,0,20.4,5.60,2.45,CHITIN, | |
| {segs:8,ridge:0.24,bump:0.11,keel:0.18,nose:0.42,tail:0.20, | |
| waist:0.16,waistAt:0.54,waistW:0.070,seed:1515,u:18,v:14}); | |
| brdShell(m); | |
| carapace(m,8.8,1.30,0,3.6,2.4,1.40,CHIT_D, | |
| {segs:2,ridge:0.14,bump:0.13,keel:0.10,nose:0.32,tail:0.42,seed:151,u:10,v:7}); | |
| eyeCluster(m,9.8,1.55,0,0.52,2,HOT); | |
| for(let k=0;k<5;k++) | |
| brdMaw(m,4.4-k*2.6,2.85,0,{len:3.8,r0:0.62-k*0.04,r1:0.36,flare:0.55,lobes:5,rings:3, | |
| pitch:1.12,col:CHIT_D,seed:151+k,u:8,v:8}); | |
| brdLimb(m); | |
| for(const sd of [-1,1]){ | |
| brdWing(m,-0.6,0.95,0,sd,6.8,9.2,0.36,0.08,BIO_MEM,C(46,44,30),1515+sd); | |
| brdWing(m,-6.4,0.80,0,sd,5.2,6.4,0.28,0.04,BIO_MEM,C(46,44,30),1525+sd); | |
| tendril(m,9.6,1.90,sd*0.9,3.6,0.16,sd,1.1,BIO_LEG); | |
| m.sphere(-5.6,2.25,sd*1.9,0.95,6,BIO_TEAM,0.82,false); | |
| brdSpiracle(m,-2.0,2.45,sd*1.8,0.42,CHIT_D); | |
| } | |
| brdLivery(m); | |
| brdCrest(m,6.4,2.55,0,3.4,2.6,0.70,3,BIO_TEAM,C(96,78,120),151); | |
| m.team(0); | |
| brdShell(m); | |
| tubercles(m,-6.8,2.15,0,3.4,2.0,8,0.50,CHIT_D,1515); | |
| return {hull:m.build(),tur:null,s:1.10}; | |
| } | |
| /* The exports. Wire into FAC_KIT.horde; nothing here takes over a global. | |
| SURFACE CONTRACTS. Keys are RAW pre-remap ids — the id the builder actually | |
| emitted — and for the Brood that set is small and mostly untouchable. | |
| MAT.CHITIN and MAT.LEAF are the organic equivalent of MAT.SERVO. mesh.js | |
| substitutes them into the shader as CHITIN_CONST and BIOLEG_CONST, where the | |
| vertex stage reads them for the breathing pulse and the spring bend and the | |
| fragment stage reads them AGAIN for `surfaceOrganic`, the subsurface term. | |
| Remap either and the animation is stranded on the geometry that carries it: | |
| a Sovereign is CHITIN from jaw to tail, so one line would stop it breathing | |
| and turn its shell mechanical in the same pass. Body, crest, limbs and every | |
| blade are therefore off the table, which leaves the emissive id — and, on | |
| the Prospector, the two hull ids its mouth leaked. */ | |
| const BRD_BESPOKE_PACKS=Object.freeze({ | |
| 12:Object.freeze({id:'brood-ravager-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({ | |
| /* Chaff, and MAT.LAMP is not only the eyes here: models.js builds the | |
| first leg while the lamp material eyeCluster() left current is still | |
| set, so seven segments of it render at full emissive. Filament glow on | |
| dull tissue is the right answer for an eye AND for a leg. */ | |
| [MAT.LAMP]:MAT.BROOD_VEIN | |
| }) | |
| }), | |
| 13:Object.freeze({id:'brood-alpha-ravager-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({ | |
| /* The Alpha's exposed brood core shares the eyes' id, and SLIME is the | |
| one tile whose emissive is wet blobs rather than thin strokes. Against | |
| the drone's BROOD_VEIN that is the only separation this contract can | |
| draw — every other id the two builders emit is a shader marker. */ | |
| [MAT.LAMP]:MAT.BROOD_SLIME | |
| }) | |
| }), | |
| 30:Object.freeze({id:'brood-sovereign-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({ | |
| /* Eyes at r 1.30 over three rows: the largest light organ in the kit, | |
| and the hero should look wet rather than switched on. */ | |
| [MAT.LAMP]:MAT.BROOD_SLIME | |
| }) | |
| }), | |
| 31:Object.freeze({id:'brood-tidecaster-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({ | |
| /* The synaptic node capping the stalk. VEIN, not SLIME: its emissive is | |
| linear filaments, which carries on the vein pattern brdBulb() already | |
| paints into the organ underneath instead of arguing with it. */ | |
| [MAT.LAMP]:MAT.BROOD_VEIN | |
| }) | |
| }), | |
| 32:Object.freeze({id:'brood-grub-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({ | |
| /* The rasp is the only part of the kit painted from the local palette, | |
| and those colours are deliberately absent from COL_MAT — so matDetect | |
| fell through to its luminance heuristic and handed the one unarmed | |
| animal in the game a hull-plate lip and a hull-dark machined bore. | |
| BRD_SOFT's pale skin is BROOD_MEMBRANE. The gullet takes BROOD_VEIN | |
| because that tile is a flat fill: the normal map is derived from tile | |
| luminance, so a flat tile keeps the bore smooth and the hole a hole. */ | |
| [MAT.PLATE]:MAT.BROOD_MEMBRANE, | |
| [MAT.GREEBLE]:MAT.BROOD_VEIN | |
| }) | |
| }), | |
| /* ------------------------------------------------------------------------ | |
| THE TWELVE. Twenty-three Brood slots had no per-asset contract at all — | |
| brdOrganicSurfacePass falls back to IDENTITY for this kit (there is no | |
| faction-wide remap table), so those units were running the shared atlas | |
| unmodified. These twelve now declare a SEMANTIC contract. | |
| Their dedicated geometry is real, but their own BaseAO/NRE/mask triplets | |
| have not been baked yet — except the Gorger (slot 1), which is the first | |
| live in-engine Brood bake, analogous to the Nova Rhino. Do not advertise | |
| map names that do not exist: `?assetskin=1` otherwise makes avoidable | |
| image requests before safely falling back to the shared atlas. `maps:null` | |
| keeps that fallback deliberate while retaining every organic material-role | |
| correction below. The Gorger pack names a published triplet and an | |
| `assetSkin` token so the query cannot silently unwrap the rest of the kit. | |
| Two ids are worth claiming and the rest are off the table: | |
| MAT.LAMP — eyes, lumen nodes, and (as on slot 12) any stretch of | |
| geometry that inherited the eye material. SLIME for wet | |
| blobby organs, VEIN for thin filament glow. Chosen per | |
| animal by what the emissive part actually is. | |
| MAT.TRIM — every claw, mandible, spine and horn. CLAW resolves through | |
| COL_MAT to 'hull.trim' (models.js:123), which is a MACHINED | |
| METAL TRIM tile: the Brood's blades have been rendering on | |
| the same surface as a tank's edge chrome. BROOD_CHITIN is | |
| the faction's own shell tile and is what a grown blade is | |
| made of. This is the single highest-value remap available | |
| to this kit and it costs one line per asset. | |
| MAT.CHITIN and MAT.LEAF stay put on all twelve: they are the vertex-stage | |
| markers for the breathing pulse and the limb spring (see the note above), | |
| so remapping either would strand the animation on the geometry carrying | |
| it. MAT.BROOD_MEMBRANE (BIO_MEM, the Emberthroat's vanes) is already the | |
| right tile and needs no override. | |
| ------------------------------------------------------------------------ */ | |
| 1:Object.freeze({id:'brood-gorger-v2', source:'authored', maps:'brood-gorger-v2', | |
| /* Live mdlBrdGorger hull rebaked in-engine. No turret. Opt-in via | |
| ?assetskin=1 or ?assetskin=gorger — not a global Brood material switch. */ | |
| assetSkin:'gorger', | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 2:Object.freeze({id:'brood-ramparthorn-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| /* Launcher throats, not a face: the rack is what glows, and a rack of five | |
| apertures wants the wet read rather than filaments. */ | |
| 7:Object.freeze({id:'brood-skysting-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 10:Object.freeze({id:'brood-flakspine-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| /* The burst sac is the unit. SLIME is the one tile whose emissive is wet | |
| blobs, which is exactly what a gravid abdomen about to rupture is. */ | |
| 20:Object.freeze({id:'brood-bloomsac-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 21:Object.freeze({id:'brood-emberthroat-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 3:Object.freeze({id:'brood-mortarback-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| /* The harpoon is painted CLAW along its whole nine units, so on this slot | |
| the TRIM remap is not trim at all — it is the unit's primary surface. */ | |
| 6:Object.freeze({id:'brood-lancespine-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 16:Object.freeze({id:'brood-siegemound-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 22:Object.freeze({id:'brood-railfang-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| /* One great eye at r 1.30 — the largest single light organ outside the | |
| Sovereign, and it should look wet rather than switched on. */ | |
| 26:Object.freeze({id:'brood-basilisk-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 27:Object.freeze({id:'brood-harbinger-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| /* THE FOURTEEN. Same contract as the twelve: dedicated geometry is real, | |
| unique BaseAO/NRE/mask triplets are not baked yet, so maps:null. Weaver | |
| and Ichorleech reuse the Grub's rasp, so they inherit its PLATE/GREEBLE | |
| remap or the unarmed mouth becomes machined hull again. */ | |
| 0:Object.freeze({id:'brood-skitterling-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 9:Object.freeze({id:'brood-brandmaw-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 4:Object.freeze({id:'brood-crownbeast-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 8:Object.freeze({id:'brood-worldshell-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 18:Object.freeze({id:'brood-furnaceback-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 5:Object.freeze({id:'brood-stingwing-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| /* The bomb sac is the unit. SLIME is the wet-blob tile. */ | |
| 17:Object.freeze({id:'brood-sacfly-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 25:Object.freeze({id:'brood-needlewren-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 11:Object.freeze({id:'brood-bastioncrab-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 19:Object.freeze({id:'brood-weaver-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN, | |
| [MAT.PLATE]:MAT.BROOD_MEMBRANE, [MAT.GREEBLE]:MAT.BROOD_VEIN})}), | |
| 23:Object.freeze({id:'brood-drumback-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 24:Object.freeze({id:'brood-ichorleech-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN, | |
| [MAT.PLATE]:MAT.BROOD_MEMBRANE, [MAT.GREEBLE]:MAT.BROOD_VEIN})}), | |
| 14:Object.freeze({id:'brood-razorfinn-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}), | |
| 15:Object.freeze({id:'brood-keelback-v2', source:'semantic-bake', maps:null, | |
| surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}) | |
| }); | |
| function brdOrganicSurfacePass(geo,pack){ | |
| if(!geo||!geo.v)return geo; | |
| const v=geo.v; | |
| for(let o=11;o<v.length;o+=VFLOATS){ | |
| const raw=v[o],sgn=raw<0?-1:1,packed=Math.abs(raw); | |
| const whole=Math.floor(packed),src=whole-1; | |
| const dst=pack&&pack.surfaces&&pack.surfaces[src]!==undefined?pack.surfaces[src]:src; | |
| if(dst!==undefined)v[o]=sgn*((dst+1)+(packed-whole)); | |
| } | |
| return geo; | |
| } | |
| const BRD_FACTORY_CACHE=new Map(); | |
| function brdBroodFactory(fn,slot){ | |
| const key=slot+':'+fn.name; | |
| if(BRD_FACTORY_CACHE.has(key))return BRD_FACTORY_CACHE.get(key); | |
| const wrapped=function(){ | |
| const g=fn(); | |
| const pack=BRD_BESPOKE_PACKS[slot]||null; | |
| if(g && typeof g==='object'){ | |
| if(g.hull)brdOrganicSurfacePass(g.hull,pack); | |
| if(g.tur)brdOrganicSurfacePass(g.tur,pack); | |
| if(g.v)brdOrganicSurfacePass(g,pack); | |
| } | |
| return g; | |
| }; | |
| Object.defineProperty(wrapped,'name',{value:'brdPurple'+slot+'_'+fn.name}); | |
| /* WHAT DIES HERE HAS TO BLEED. | |
| sim.js asks unitModelOrganic(kit,type) what a corpse is made of, and that | |
| answer is `ORGANIC_MDL.has(FAC_KIT[kit][type])` — the function actually | |
| registered in the kit, which for every Brood slot is THIS WRAPPER, never | |
| the builder inside it. models.js can only ever have listed the builders, | |
| so wrapping a slot silently reclassified the animal as machinery and its | |
| death dropped scrap instead of biomass. | |
| That was already true of slots 12 and 13 before this line existed; adding | |
| twelve more wrapped slots would have made it true of half the roster. The | |
| wrapper is a Brood creature by construction, so it registers itself. */ | |
| if(typeof ORGANIC_MDL!=='undefined'&&ORGANIC_MDL&&ORGANIC_MDL.add) ORGANIC_MDL.add(wrapped); | |
| BRD_FACTORY_CACHE.set(key,wrapped); | |
| return wrapped; | |
| } | |
| /* The registry. Merged over FAC_KIT.horde by mergeFactionUnitKits(), so THIS | |
| is the live mapping for every slot named here — the entries left in | |
| FAC_KIT.horde are a dead fallback that only fires if this file fails to | |
| load at all. | |
| The twelve below are the Spitter and Bombardier splits. The fourteen after | |
| them are the remaining shared Beast / Leviathan / Flyer / Support / Swimmer | |
| slots. Before this file, FAC_KIT.horde mapped 28 roster slots onto nine | |
| builders called with no arguments. */ | |
| const UNIT_MDL_BROOD={ | |
| 12:brdBroodFactory(typeof mdlRavager==='function'?mdlRavager:mdlBroodmother,12), | |
| 13:brdBroodFactory(typeof mdlAlphaRavager==='function'?mdlAlphaRavager:mdlBroodmother,13), | |
| 30:brdBroodFactory(mdlBrdSovereign,30), | |
| 31:brdBroodFactory(mdlBrdTidecaster,31), | |
| 32:brdBroodFactory(mdlBrdGrub,32), | |
| /* was mdlHordeSpitter x6 */ | |
| 1:brdBroodFactory(mdlBrdGorger,1), | |
| 2:brdBroodFactory(mdlBrdRamparthorn,2), | |
| 7:brdBroodFactory(mdlBrdSkysting,7), | |
| 10:brdBroodFactory(mdlBrdFlakspine,10), | |
| 20:brdBroodFactory(mdlBrdBloomsac,20), | |
| 21:brdBroodFactory(mdlBrdEmberthroat,21), | |
| /* was mdlHordeBombardier x6 */ | |
| 3:brdBroodFactory(mdlBrdMortarback,3), | |
| 6:brdBroodFactory(mdlBrdLancespine,6), | |
| 16:brdBroodFactory(mdlBrdSiegemound,16), | |
| 22:brdBroodFactory(mdlBrdRailfang,22), | |
| 26:brdBroodFactory(mdlBrdBasilisk,26), | |
| 27:brdBroodFactory(mdlBrdHarbinger,27), | |
| /* was mdlHordeBeast x2 */ | |
| 0:brdBroodFactory(mdlBrdSkitterling,0), | |
| 9:brdBroodFactory(mdlBrdBrandmaw,9), | |
| /* was mdlHordeLeviathan x3 */ | |
| 4:brdBroodFactory(mdlBrdCrownbeast,4), | |
| 8:brdBroodFactory(mdlBrdWorldshell,8), | |
| 18:brdBroodFactory(mdlBrdFurnaceback,18), | |
| /* was mdlHordeFlyer x3 */ | |
| 5:brdBroodFactory(mdlBrdStingwing,5), | |
| 17:brdBroodFactory(mdlBrdSacfly,17), | |
| 25:brdBroodFactory(mdlBrdNeedlewren,25), | |
| /* was mdlHordeSupport x4 */ | |
| 11:brdBroodFactory(mdlBrdBastioncrab,11), | |
| 19:brdBroodFactory(mdlBrdWeaver,19), | |
| 23:brdBroodFactory(mdlBrdDrumback,23), | |
| 24:brdBroodFactory(mdlBrdIchorleech,24), | |
| /* was mdlHordeSwimmer x2 */ | |
| 14:brdBroodFactory(mdlBrdRazorfinn,14), | |
| 15:brdBroodFactory(mdlBrdKeelback,15) | |
| }; | |
| /* Hull skins. initFactionKits already calls mfAssetSkin on hull when the pack | |
| names a triplet AND mfAssetSkinEnabled() is on. mesh.js only tests | |
| ?assetskin=1; the Nova file extends that for `rhino`. This file must not | |
| edit either owner, so we take over after both return. ?assetskin=gorger | |
| is the pack token mfPackMaps already honours. Gorger has no turret. */ | |
| if(typeof mfAssetSkinEnabled==='function'){ | |
| const _brdSkinOn=mfAssetSkinEnabled; | |
| mfAssetSkinEnabled=function(){ | |
| try{ if((new URLSearchParams(location.search).get('assetskin')||'')==='gorger') return true; }catch(e){} | |
| return _brdSkinOn(); | |
| }; | |
| } | |