CREATORJD's picture
Publish MASSFRONT web 1.33.33 (part 3)
3597f5a verified
Raw
History Blame Contribute Delete
9.14 kB
;
;
/* ============================================================================
MOD ATTACHMENTS β€” item 5c, "mods get random 3D visual add-ons"
----------------------------------------------------------------------------
Crafted modules changed six global scalars and showed it with a coloured
billboard orbiting the Commander and the HQ. Nothing on the army itself ever
admitted that a mod was fitted: a fleet with Reactive Plating and a Sensor
Mast looked identical to a fleet with nothing.
Now every player unit carries the hardware. Each module owns a small kit of
VARIANTS and each unit deterministically picks one from its own spawn id, so
a column reads as field-fitted rather than factory-stamped β€” the same
attachment, mounted a different way down the line β€” and the choice is stable:
a unit does not re-roll its kit every frame, every reselect, or on reload.
COST. This is the doctrine-shell pattern (render3d.js), not a new renderer:
one cached InstMesh per (module, variant), one extra instanced draw call for
the whole army per live pair, and nothing at all allocated when no module is
equipped. Geometry is deliberately tiny β€” these read at 40px on a phone, so
silhouette and one accent light is the entire budget.
COLOUR. The accent surfaces are painted TEAM_A, which is the only material
the instance colour reaches. Passing the module's own colour through that
channel is what makes a Fusion Overdraw pod read orange and a Relic Lattice
read magenta without a second shader path or a second material id.
============================================================================ */
/* Each builder returns a MeshBuilder-built geometry. Keep them under ~80 tris:
they are drawn once per unit on top of a hull that already costs more. */
function mkPlateA(){ /* reactive plating β€” bolted slabs */
const m=MB();
for(const s of [-1,1]){
m.bevelBox(0,0.35,s*1.5,2.3,0.42,0.9,0.12,MET_D);
m.box(0.1,0.62,s*1.5,1.5,0.16,0.5,TEAM_A);
}
return m.build();
}
function mkPlateB(){ /* reactive plating β€” dorsal brick */
const m=MB();
m.bevelBox(0,0.5,0,2.0,0.7,2.0,0.22,MET_D);
m.box(0,0.92,0,1.2,0.16,1.2,TEAM_A);
for(const s of [-1,1]) m.cyl(s*0.85,0.55,0,0.13,0.13,0.5,6,DARK,0);
return m.build();
}
function mkOpticA(){ /* targeting uplink β€” mast + scope */
const m=MB();
m.cyl(0,0.6,0,0.16,0.13,1.2,6,MET_D);
m.bevelBox(0.35,1.3,0,1.1,0.5,0.5,0.14,MET);
m.cyl(0.95,1.3,0,0.22,0.26,0.28,8,TEAM_A,Math.PI/2);
return m.build();
}
function mkOpticB(){ /* targeting uplink β€” cheek pod */
const m=MB();
m.bevelBox(0,0.45,0.9,0.9,0.6,0.8,0.16,MET_D);
m.cyl(0.5,0.55,0.9,0.2,0.2,0.35,8,TEAM_A,Math.PI/2);
m.box(0,0.85,0.9,0.5,0.12,0.4,MET_L);
return m.build();
}
function mkRangeA(){ /* sensor mast β€” whip antenna */
const m=MB();
m.cyl(-0.4,1.15,0,0.09,0.05,2.3,5,MET_L);
m.box(-0.4,0.22,0,0.5,0.44,0.5,MET_D);
m.cyl(-0.4,2.35,0,0.16,0.02,0.3,5,TEAM_A);
return m.build();
}
function mkRangeB(){ /* sensor mast β€” folded dish */
const m=MB();
m.cyl(0,0.5,0,0.13,0.11,1.0,6,MET_D);
m.ring(0,1.1,0,0.18,0.62,10,TEAM_A);
m.cyl(0,1.18,0,0.07,0.03,0.4,5,MET_L);
return m.build();
}
function mkTempoA(){ /* drive governors β€” stack vents */
const m=MB();
for(const s of [-1,1]){
m.cyl(-0.9,0.5,s*0.7,0.22,0.26,0.9,7,DARK);
m.tube(-0.9,0.98,s*0.7,0.27,0.15,0.18,7,TEAM_A);
}
return m.build();
}
function mkTempoB(){ /* drive governors β€” rear fins */
const m=MB();
for(const s of [-1,1]){
m.wedge(-1.1,0.4,s*1.1,1.4,0.9,0.24,MET_D,0,s<0?1:0);
m.box(-1.1,0.85,s*1.1,1.0,0.1,0.2,TEAM_A);
}
return m.build();
}
function mkReclA(){ /* reclaim servos β€” folded claw */
const m=MB();
m.box(0.6,0.35,0,1.4,0.3,0.35,MET_D);
for(const s of [-1,1]) m.wedge(1.5,0.42,s*0.28,0.9,0.28,0.2,TEAM_A,0,s<0?1:0);
m.cyl(0,0.35,0,0.24,0.24,0.5,6,SERVO,Math.PI/2);
return m.build();
}
function mkReclB(){ /* reclaim servos β€” hopper */
const m=MB();
m.bevelBox(-0.5,0.55,0,1.5,0.9,1.3,0.2,MET_D);
m.inset(-0.5,1.0,0,1.1,0.9,0.22,0.7,TEAM_A);
return m.build();
}
function mkCoreA(){ /* fusion overdraw β€” reactor pod */
const m=MB();
m.cyl(0,0.7,0,0.45,0.45,1.1,9,MET_D);
m.tube(0,0.7,0,0.52,0.38,0.34,9,TEAM_A);
m.sphere(0,1.35,0,0.28,8,HOT);
return m.build();
}
function mkCoreB(){ /* fusion overdraw β€” flank cells */
const m=MB();
for(const s of [-1,1]){
m.cyl(0,0.5,s*1.2,0.3,0.3,0.85,8,DARK);
m.tube(0,0.92,s*1.2,0.34,0.2,0.16,8,TEAM_A);
}
return m.build();
}
function mkRelicA(){ /* relic lattice β€” shard ring */
const m=MB();
for(let k=0;k<5;k++){
const a=k*(Math.PI*2/5);
m.wedge(Math.cos(a)*0.85,1.15,Math.sin(a)*0.85,0.34,0.8,0.26,TEAM_A,a,k&1);
}
m.ring(0,0.72,0,0.5,0.95,12,DARKER);
return m.build();
}
function mkRelicB(){ /* relic lattice β€” spine shard */
const m=MB();
m.wedge(0,1.0,0,0.5,1.6,0.4,TEAM_A,0,0);
m.box(0,0.3,0,0.9,0.35,0.9,DARKER);
return m.build();
}
function mkEmpA(){ /* EMP charge β€” coil emitter */
const m=MB();
m.cyl(0,0.5,0,0.3,0.24,0.7,8,MET_D);
for(let k=0;k<3;k++) m.tube(0,0.75+k*0.24,0,0.42-k*0.06,0.26,0.1,9,TEAM_A);
return m.build();
}
function mkEmpB(){ /* EMP charge β€” prong array */
const m=MB();
m.box(0,0.3,0,0.9,0.3,0.9,DARK);
for(const s of [-1,1]) for(const t of [-1,1])
m.cyl(s*0.42,0.85,t*0.42,0.08,0.05,1.0,5,TEAM_A);
return m.build();
}
/* module id -> {builders, h, s}.
h is EXTRA height in model units above the hull's own deck line, and s scales
the kit against the unit. The first cut hung these off a fraction of
TYPES.size, which is a collision radius, not a hull height β€” a Rhino mounted
its reactor pod eleven units above a hull four units tall, so the hardware
floated in the air and read as a HUD decal rather than as bolted-on kit. The
deck line the game already knows is M.turH, the turret ring. */
const MOD_ATTACH={
/* Sized against SPAN_MIN=420, the closest the camera is ever allowed to get
(mesh.js) β€” a unit is about sixty pixels there, so kit that is "correctly"
scaled against the hull is four pixels and might as well not exist. These
are deliberately oversized for the same reason the units themselves are
(sc = T.size/15 * 1.5 in render3d.js): legibility beats literal scale. */
plate:{v:[mkPlateA,mkPlateB], h:0.05, s:1.55},
optic:{v:[mkOpticA,mkOpticB], h:0.20, s:1.40},
range:{v:[mkRangeA,mkRangeB], h:0.20, s:1.35},
tempo:{v:[mkTempoA,mkTempoB], h:0.00, s:1.45},
recl: {v:[mkReclA,mkReclB], h:0.10, s:1.40},
core: {v:[mkCoreA,mkCoreB], h:0.15, s:1.40},
relic:{v:[mkRelicA,mkRelicB], h:0.90, s:1.35},
emp: {v:[mkEmpA,mkEmpB], h:0.20, s:1.35},
};
const MOD_ATTACH_MESH={}; // modId -> [InstMesh per variant]
let modAttachLive=[]; // rebuilt only when the equipped set changes
let modAttachSig='';
/* Lazy, and only for what is actually fitted: a player who never crafts a
module never pays a byte of GPU memory for this feature. */
function modAttachMeshes(id){
if(MOD_ATTACH_MESH[id]) return MOD_ATTACH_MESH[id];
const K=MOD_ATTACH[id];
if(!K||typeof gl==='undefined'||!gl) return null;
MOD_ATTACH_MESH[id]=K.v.map(fn=>new InstMesh(gl,fn(),1400));
return MOD_ATTACH_MESH[id];
}
/* The live set, refreshed from the same seam the HUD badge reads so the two can
never disagree about what is equipped. */
function modAttachSync(){
if(typeof activeModuleMarks!=='function'){ modAttachLive=[]; return modAttachLive; }
const marks=activeModuleMarks();
const sig=marks.map(m=>m.id).join(',');
if(sig===modAttachSig) return modAttachLive;
modAttachSig=sig;
modAttachLive=[];
for(const m of marks){
const K=MOD_ATTACH[m.id]; if(!K) continue;
const mesh=modAttachMeshes(m.id); if(!mesh) continue;
modAttachLive.push({id:m.id,mesh:mesh,h:K.h,s:K.s,col:m.col||[220,230,240]});
}
return modAttachLive;
}
/* Stable per unit, varied across the army. ugen is the spawn generation
counter, so it survives selection, reload and camera moves β€” a unit that
re-rolled its hardware every frame would strobe. */
function modAttachVariant(i,slot,n){
const seed=(typeof ugen!=='undefined'?ugen[i]:i)+i*31+slot*97;
return ((seed%n)+n)%n;
}
/* Instances submitted on the last frame. Read before flushing, because flush
empties the stream β€” "I cannot see it at 40px" and "it was never submitted"
are indistinguishable on screen, and this is the number that tells them
apart, both in the gate and in a live diagnosis. */
let modAttachDrawn=0;
function modAttachFlush(){
let n=0;
for(const id in MOD_ATTACH_MESH) for(const M of MOD_ATTACH_MESH[id]){ n+=M.n||0; M.flush(gl); }
modAttachDrawn=n;
}