; ; /* ============================================================================ MODEL LIBRARY — every unit, structure and world object as real geometry. ---------------------------------------------------------------------------- Models are welded from primitives and authored around the origin with their feet on y=0, facing +X (which matches the sim's angle 0). A model's own colours are near-neutral livery values; the per-instance tint multiplies them, so the same mesh serves both armies and still shows its own panel lines, shadow gaps and warning markings. Turreted vehicles are two meshes — hull and turret — instanced separately so the turret can track a target while the hull drives somewhere else, which is the whole reason tanks read as tanks. ============================================================================ */ const C=(r,g,b)=>[r/255,g/255,b/255]; const MET =C(158,166,176), MET_D=C(96,104,114), MET_L=C(206,214,222); const DARK =C(52,58,66), DARKER=C(32,36,42); const TREAD =C(44,46,50), RUBBER=C(60,62,68); const GLASS =C(120,200,255), LAMP =C(255,226,150); const HOT =C(255,150,70), ENERGY=C(140,230,255); const CONC =C(150,146,138), CONC_D=C(104,101,95); const RUST =C(140,102,72), BONE =C(206,198,182); /* Warm bone-amber, not leaf green. The reference material's whole character is a pale amber plate field falling to an almost-black wet seam; a mid-green base has neither the value range nor the warmth to get there, and no amount of lighting recovers it. CLAW stays near-white so the tips read at 40px. */ const CHITIN=C(196,178,124), CHIT_D=C(96,86,58), CLAW=C(232,226,206); /* Biological livery and flexible tissue never inherit vehicle plate or servo textures. BIO_TEAM stays faction-tinted on chitin; BIO_LEG/BIO_MEM use an organic relief and mark appendages for procedural secondary motion. */ const BIO_TEAM=C(198,174,220), BIO_LEG=C(148,132,88), BIO_MEM=C(150,138,96); /* TEAM_* entries are the only surfaces that take the full faction colour: hull flanks, turret cheeks, wing roots, roof panels. Keeping livery to a few deliberate panels is what makes a unit read as "blue army" without erasing its metal, glass and rubber. */ const TEAM_A=C(214,222,230), TEAM_B=C(160,170,182), TEAM_T=C(236,242,248); /* Architectural palette. Structures are not vehicles: they want a wall material and a roof material, both far calmer than hull plating. */ const WALL =C(176,180,188), WALL_D=C(124,128,136); /* Anything painted SERVO is treated as a leg by the vertex stage and animates through the walk cycle. Use it for shins, thighs, feet and hip rams only. */ const SERVO =C(132,138,148); const ROOFC =C(150,154,160), ROOF_T=C(196,202,210); /* Tower-only hard-surface palette. These references deliberately do not replace MET/DARK globally: vehicles and legacy structures keep their current finishes while defensive towers receive clearly separated armor, mechanism, trim, livery, light, foundation and bore materials. */ const TWR_ARM=C(190,198,208), TWR_ARM_D=C(118,128,140); const TWR_MACH=C(145,154,166), TWR_TRIM=C(220,226,234); const TWR_COAT=C(68,76,88), TWR_TEAM=C(218,226,236); const TWR_GLOW=C(92,214,255), TWR_PAD=C(116,120,128), TWR_BORE=C(13,15,18); /* Landmark glowOk only passes MAT.TWR_GLOW. This brighter vertex is the same tile, so HQ points read at command distance without a rail. */ const HQ_LAMP=C(236,250,255); const LIT_WIN=C(255,230,170), COOL_WIN=C(170,230,255), FLICK_WIN=C(255,200,140); const SHOP_WIN=C(255,245,220), NEON_CYAN=C(0,240,255), SOLAR_PANEL=C(35,50,80); const COPPER_PAT=C(82,132,114), BRICK_RED=C(140,76,64); const NOVA_MET=C(132,142,156), NOVA_CARB=C(44,48,56); /* Stage S1 bespoke structure semantics. These are deliberately palette-local selectors, rather than a renderer switch: HQ and Techlab can establish their own material hierarchy now while the renderer still falls back to the live V2 atlas. They are semantic-bake prototypes, NOT UV-authored map packs. */ const NOVA_HQ_ARM=C(152,158,166), NOVA_HQ_STRUCT=C(46,50,56); const NOVA_HQ_ROOF=C(116,132,152), NOVA_HQ_GLASS=C(44,130,196); const NOVA_RESEARCH_ARM=C(136,154,178), NOVA_RESEARCH_CORE=C(40,60,82); const NOVA_RESEARCH_ROOF=C(104,122,144), NOVA_RESEARCH_GLASS=C(50,148,212); /* Stage S2 keeps production landmarks as individual semantic packs. These are palette selectors for the live V2 material atlas, not a claim that custom UV-authored maps already exist. Their separation is intentional: when the authored BaseAO/NRE/mask pack lands, it can replace just one landmark's surfaces without accidentally recolouring another Nova building. */ const NOVA_FACTORY_ARM=C(132,154,180), NOVA_FACTORY_CORE=C(34,46,62); const NOVA_FACTORY_ROOF=C(100,122,148), NOVA_FACTORY_GLASS=C(48,136,202); const NOVA_REACTOR_ARM=C(124,150,176), NOVA_REACTOR_CORE=C(30,46,64); const NOVA_REACTOR_TRIM=C(156,178,202), NOVA_REACTOR_GLOW=C(72,208,255); const NOVA_AIR_DECK=C(58,76,100), NOVA_AIR_ARM=C(128,150,176); const NOVA_AIR_ROOF=C(104,126,150), NOVA_AIR_GLASS=C(48,142,210); const NOVA_FAB_ARM=C(130,152,176), NOVA_FAB_CORE=C(38,50,66); const NOVA_FAB_ROOF=C(102,124,148), NOVA_FAB_GLASS=C(50,138,204); /* Stage S3 extends the same isolated semantic treatment to real Nova economy and support structures. These are still palette selectors for the live V2 material atlas, not a claim that bespoke UV-authored maps are complete. */ const NOVA_MEX_ARM=C(126,148,174), NOVA_MEX_CORE=C(34,50,68); const NOVA_MEX_TRIM=C(160,180,202), NOVA_MEX_GLOW=C(22,70,92); /* Service-deck plating. Its own palette entry so retiring it from a pad cannot disturb TWR_COAT, which many other surfaces still use. */ const NOVA_DECK=C(111,119,131); const NOVA_GEO_ARM=C(122,146,170), NOVA_GEO_CORE=C(30,46,62); const NOVA_GEO_TRIM=C(154,178,202), NOVA_GEO_GLOW=C(76,210,255); const NOVA_SILO_ARM=C(130,150,174), NOVA_SILO_CORE=C(36,48,62); const NOVA_SILO_ROOF=C(104,124,148), NOVA_SILO_GLOW=C(64,196,248); const NOVA_UPLINK_ARM=C(128,152,178), NOVA_UPLINK_CORE=C(32,48,66); const NOVA_UPLINK_TRIM=C(162,184,208), NOVA_UPLINK_GLOW=C(70,210,255); const NOVA_HARBOR_ARM=C(124,148,174), NOVA_HARBOR_CORE=C(32,48,66); const NOVA_HARBOR_DECK=C(78,98,124), NOVA_HARBOR_GLASS=C(46,136,202); /* Stage S4 gives the common Nova defenses distinct semantic palettes. These remain live-V2 selectors, deliberately isolated per building family, so a later authored bake can replace one asset without recolouring another. */ const NOVA_SENTINEL_ARM=C(138,160,186), NOVA_SENTINEL_CORE=C(36,48,64); const NOVA_SENTINEL_TRIM=C(164,188,214), NOVA_SENTINEL_GLOW=C(72,212,255); const NOVA_BUNKER_ARM=C(132,150,174), NOVA_BUNKER_CORE=C(34,44,58); const NOVA_BUNKER_TRIM=C(156,176,200), NOVA_BUNKER_GLOW=C(68,198,246); const NOVA_RAIL_ARM=C(144,166,194), NOVA_RAIL_CORE=C(30,42,60); const NOVA_RAIL_TRIM=C(174,198,224), NOVA_RAIL_GLOW=C(84,224,255); const DROP_CANOPY_GLASS=C(34,112,176); // dark pressure glazing stays blue under the overhead world light const LEG_IRON=C(88,80,78), LEG_VENT=C(255,100,20); const SYN_NANO_C=C(120,132,148), SYN_GOLD_C=C(212,175,55), SYN_FLOW=C(0,220,240); const BROOD_MEMB=C(184,152,120); const HEADLIGHT_C=C(255,250,220), TAILLIGHT_C=C(255,34,17); /* Colour constants double as MATERIAL selectors: the builder tracks a current material, and this wrapper sets it from whichever palette entry a primitive was already using. That upgrades the entire model library to textured surfaces without touching a single call site. */ const COL_MAT=new Map(); const COL_TEAM=new Set(); // palette entries painted in team livery function bindMat(){ COL_MAT.set(MET,matResolve('hull')); COL_MAT.set(MET_D,matResolve('hull.dark')); COL_MAT.set(MET_L,matResolve('hull.trim')); COL_MAT.set(DARK,matResolve('hull.dark'));COL_MAT.set(DARKER,matResolve('hull.dark')); COL_MAT.set(TREAD,matResolve('hull.track')); COL_MAT.set(RUBBER,matResolve('hull.track')); COL_MAT.set(GLASS,matResolve('glass.canopy')); COL_MAT.set(LAMP,matResolve('emissive.light')); COL_MAT.set(HOT,matResolve('emissive.light')); COL_MAT.set(ENERGY,matResolve('emissive.energy')); COL_MAT.set(CONC,MAT.CONC); COL_MAT.set(CONC_D,MAT.CONC); COL_MAT.set(RUST,MAT.RUST); COL_MAT.set(BONE,MAT.CONC); COL_MAT.set(CHITIN,matResolve('bio.shell')); COL_MAT.set(CHIT_D,matResolve('bio.shell')); COL_MAT.set(CLAW,matResolve('hull.trim')); COL_MAT.set(BIO_TEAM,matResolve('bio.shell')); COL_MAT.set(BIO_LEG,matResolve('bio.limb')); COL_MAT.set(BIO_MEM,matResolve('bio.membrane')); COL_MAT.set(TEAM_A,matResolve('hull')); COL_MAT.set(TEAM_B,matResolve('hull')); COL_MAT.set(TEAM_T,matResolve('hull.trim')); COL_MAT.set(WALL,matResolve('structure.wall')); COL_MAT.set(WALL_D,matResolve('structure.wall')); COL_MAT.set(SERVO,MAT.SERVO); COL_MAT.set(ROOFC,matResolve('structure.roof')); COL_MAT.set(ROOF_T,matResolve('structure.roof')); COL_MAT.set(TWR_ARM,MAT.TWR_ARMOR); COL_MAT.set(TWR_ARM_D,MAT.TWR_ARMOR); COL_MAT.set(TWR_MACH,MAT.TWR_MACH); COL_MAT.set(TWR_TRIM,MAT.TWR_MACH); COL_MAT.set(TWR_COAT,MAT.TWR_COAT); COL_MAT.set(TWR_TEAM,MAT.TWR_ARMOR); COL_MAT.set(TWR_GLOW,MAT.TWR_GLOW); COL_MAT.set(HQ_LAMP,MAT.TWR_GLOW); COL_MAT.set(TWR_PAD,MAT.TWR_PAD); COL_MAT.set(TWR_BORE,MAT.TWR_BORE); COL_MAT.set(LIT_WIN,matResolve('window.warm')); COL_MAT.set(COOL_WIN,matResolve('window.cool')); COL_MAT.set(FLICK_WIN,matResolve('window.flicker')); COL_MAT.set(SHOP_WIN,matResolve('window.shop')); COL_MAT.set(NEON_CYAN,MAT.NEON_FACADE); COL_MAT.set(SOLAR_PANEL,matResolve('roof.solar')); COL_MAT.set(COPPER_PAT,matResolve('roof.copper')); COL_MAT.set(BRICK_RED,MAT.BRICK_MASONRY); COL_MAT.set(NOVA_MET,matResolve('faction.nova')); COL_MAT.set(NOVA_CARB,MAT.NOVA_CARBON); /* HQ/Techlab own semantic surface sets until authored UV packs arrive. Keeping the mappings local prevents a generic Nova-material edit from accidentally changing every production building in the faction. */ COL_MAT.set(NOVA_HQ_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_HQ_STRUCT,MAT.TWR_COAT); COL_MAT.set(NOVA_HQ_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_HQ_GLASS,matResolve('glass.canopy')); COL_MAT.set(NOVA_RESEARCH_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_RESEARCH_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_RESEARCH_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_RESEARCH_GLASS,matResolve('glass.canopy')); COL_MAT.set(NOVA_FACTORY_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_FACTORY_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_FACTORY_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_FACTORY_GLASS,matResolve('glass.canopy')); COL_MAT.set(NOVA_REACTOR_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_REACTOR_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_REACTOR_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_REACTOR_GLOW,matResolve('emissive.energy')); COL_MAT.set(NOVA_AIR_DECK,matResolve('structure.roof')); COL_MAT.set(NOVA_AIR_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_AIR_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_AIR_GLASS,matResolve('glass.canopy')); COL_MAT.set(NOVA_FAB_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_FAB_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_FAB_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_FAB_GLASS,matResolve('glass.canopy')); COL_MAT.set(NOVA_MEX_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_MEX_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_MEX_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_MEX_GLOW,MAT.NOVA_CARBON); COL_MAT.set(NOVA_DECK,MAT.DECK_PLATE); COL_MAT.set(NOVA_GEO_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_GEO_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_GEO_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_GEO_GLOW,matResolve('emissive.energy')); COL_MAT.set(NOVA_SILO_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_SILO_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_SILO_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_SILO_GLOW,matResolve('emissive.energy')); COL_MAT.set(NOVA_UPLINK_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_UPLINK_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_UPLINK_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_UPLINK_GLOW,matResolve('emissive.energy')); COL_MAT.set(NOVA_HARBOR_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_HARBOR_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_HARBOR_DECK,matResolve('structure.roof')); COL_MAT.set(NOVA_HARBOR_GLASS,matResolve('glass.canopy')); COL_MAT.set(NOVA_SENTINEL_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_SENTINEL_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_SENTINEL_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_SENTINEL_GLOW,matResolve('emissive.energy')); COL_MAT.set(NOVA_BUNKER_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_BUNKER_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_BUNKER_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_BUNKER_GLOW,matResolve('emissive.energy')); COL_MAT.set(NOVA_RAIL_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_RAIL_CORE,MAT.NOVA_CARBON); COL_MAT.set(NOVA_RAIL_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_RAIL_GLOW,matResolve('emissive.energy')); COL_MAT.set(DROP_CANOPY_GLASS,matResolve('glass.canopy')); COL_MAT.set(LEG_IRON,matResolve('faction.legion')); COL_MAT.set(LEG_VENT,MAT.LEGION_THERMITE); COL_MAT.set(SYN_NANO_C,matResolve('faction.syndicate')); COL_MAT.set(SYN_GOLD_C,MAT.SYN_GOLD); COL_MAT.set(SYN_FLOW,matResolve('emissive.energy')); COL_MAT.set(BROOD_MEMB,matResolve('bio.membrane')); COL_MAT.set(HEADLIGHT_C,MAT.UNIT_HEADLIGHT); COL_MAT.set(TAILLIGHT_C,MAT.UNIT_TAILLIGHT); } bindMat(); COL_TEAM.add(TEAM_A); COL_TEAM.add(TEAM_B); COL_TEAM.add(TEAM_T); COL_TEAM.add(TWR_TEAM); COL_TEAM.add(BIO_TEAM); /* A named role is preferred, but legacy models still pass palette colours. The fallback heuristics prevent an unregistered pale cyan face from becoming dull steel when an experimental model is added before its palette binding. */ function matDetect(col,role){ if(role) return matResolve(role); const known=COL_MAT.get(col); if(known!=null) return known; if(!Array.isArray(col)) return MAT.PLATE; const r=col[0]||0,g=col[1]||0,b=col[2]||0; if(b>r*1.35&&b>g*1.08&&b>.42) return matResolve('glass'); if(r>.82&&g>.82&&b>.82) return matResolve('hull.trim'); if(r<.24&&g<.24&&b<.24) return matResolve('hull.dark'); return matResolve('hull'); } /* Patch the builder so every legacy primitive picks its material from its palette while new factories can request semantic material roles directly. */ for(const fn of ['box','cyl','sphere','wedge','extrude','ring','bevelBox','inset','tube','greeble']){ const orig=MeshBuilder.prototype[fn]; MeshBuilder.prototype[fn]=function(...args){ /* The colour argument sits at a different index per primitive. Scan from the end: position vectors come first, while colour is always the last normalized RGB tuple before optional yaw/flags. This lets new palette colours get a sensible material fallback instead of silently inheriting the previous part's material. */ for(let k=args.length-1;k>=0;k--){ const a=args[k]; if(Array.isArray(a)&&a.length===3&&a.every(v=>typeof v==='number'&&v>=0&&v<=1)){ this.m=matDetect(a); this.tm=COL_TEAM.has(a); break; } } return orig.apply(this,args); }; } /* Tread bank: two blocks with road wheels between them. Repeated on nearly every ground vehicle, so it earns its own function. */ function treads(m,len,wid,h,gauge,wheels){ for(const s of [-1,1]){ m.box(0,0,s*gauge,len,h,wid,TREAD); m.box(0,h*0.34,s*gauge,len*1.03,h*0.34,wid*1.16,DARKER); // track links const n=wheels||4; for(let k=0;k[p[0]*k,p[1]*k]),hHigh,colTop||col); return m; } /* Angled armour plate with a lip — spaced armour, skirts, glacis add-ons. */ function armorPlate(m,x,y,z,w,h,thick,tilt,col,yaw){ const c=Math.cos(yaw||0), s=Math.sin(yaw||0); m.box(x,y,z,w,h,thick,col,yaw||0); m.box(x - Math.sin(yaw||0)*0, y+h, z, w*0.96, thick*0.7, thick*1.9, col, yaw||0); for(const o of [-0.34,0.34]){ // bolt strip down the plate const ox=w*o; m.box(x+ox*c, y+h*0.5, z+ox*s, thick*0.9, h*0.7, thick*0.9, col, yaw||0); } return m; } /* Hydraulic ram: a pair of polished pistons in a housing. Legs, elevation gear, suspension arms — anything that should look like it moves. */ function hydraulic(m,x,y,z,len,r,col,yaw){ const c=Math.cos(yaw||0), s=Math.sin(yaw||0); m.cyl(x,y,z,r,r*0.95,len*0.6,8,col); m.cyl(x,y+len*0.6,z,r*0.55,r*0.55,len*0.45,8,MET_L); // bright ram m.cyl(x,y+len*0.55,z,r*1.18,r*1.18,len*0.10,8,DARKER); // gland nut return m; } /* Louvred vent bank — engine decks, radiator faces, intake grilles. */ function ventBank(m,x,y,z,w,d,n,col,yaw){ const c=Math.cos(yaw||0), s=Math.sin(yaw||0); m.inset(x,y,z,w,d,0.36,0.10,DARKER,yaw||0); for(let k=0;k{ sd=(sd*1664525+1013904223)&0x7fffffff; return (sd>>>10)/2097152; }; const c=Math.cos(yaw||0), s=Math.sin(yaw||0); for(let k=0;k<4;k++){ const t=(k+0.5)/4-0.5, px=t*len, pz=(rr2()-0.5)*wid*0.6; const bw=len/4*(0.55+rr2()*0.35), bh=0.5+rr2()*0.7, bd=wid*(0.3+rr2()*0.3); m.bevelBox(x+px*c-pz*s, y, z+px*s+pz*c, bw,bh,bd, Math.min(bw,bd)*0.22, col, yaw||0); } return m; } /* Ammunition / equipment box with a lid lip and latches. */ function kitBox(m,x,y,z,w,h,d,col,yaw){ m.bevelBox(x,y,z,w,h,d,Math.min(w,d)*0.14,col,yaw||0); m.box(x,y+h,z,w*1.04,h*0.14,d*1.04,DARKER,yaw||0); for(const o of [-0.3,0.3]) m.box(x+w*o*Math.cos(yaw||0),y+h*0.55,z+w*o*Math.sin(yaw||0), w*0.08,h*0.5,d*1.06,MET_L,yaw||0); return m; } /* SupCom-style emissive strip: a thin glowing line along a hull edge. */ /* --------------------------------------------------------------------------- ROOF EDGE / PARAPET A roof that ends in a bare chamfer has nothing for the light to catch, so from overhead the building's outline dissolves into the ground. A raised lip with a darker reveal under it gives every roof a hard bright edge and a line of shade beneath — the single cheapest piece of architectural detail there is, and the one the eye uses to read a top-down building's extent. --------------------------------------------------------------------------- */ function roofEdge(m,x,y,z,w,d,lip,col,trim){ const hw=w/2, hd=d/2, th=lip||1.6; for(const s of [-1,1]){ m.box(x,y,z+s*(hd-th*0.5), w, th*1.5, th, col); // side parapets m.box(x,y+th*1.5,z+s*(hd-th*0.5), w*0.995, th*0.36, th*1.25, trim||col); m.box(x+s*(hw-th*0.5),y,z, th, th*1.5, d-th*2, col); // end parapets m.box(x+s*(hw-th*0.5),y+th*1.5,z, th*1.25, th*0.36, d-th*2, trim||col); } } function glowStrip(m,x,y,z,len,col,yaw){ m.box(x,y,z,len,0.22,0.34,col||ENERGY,yaw||0); return m; } /* --------------------------------------------------------------------------- DECK CROWN — roofEdge, but for vehicles. roofEdge above exists because "from overhead the building's outline dissolves into the ground". That is just as true of a tank, and nothing was doing it: this camera looks almost straight down, so a hull's DECK is the surface a player actually identifies a unit by, and an extrude cap gives it nothing to catch light on. The raised rail draws a hard outline against terrain, the nose chevrons state facing without needing a turret, and the rear kick plate stops the back edge melting into the tracks. Trim is team colour and it lands in the SAME PLACE on every chassis — that consistency is the other half of the read, and it is why this is a shared helper and not per-model geometry. --------------------------------------------------------------------------- */ function deckCrown(m,x,y,z,len,wid,col,trim){ const L=len*0.5, W=wid*0.5, th=Math.min(len,wid)*0.09; for(const sd of [-1,1]){ m.box(x,y,z+sd*(W-th*0.5),len*0.94,th*0.85,th,col||MET_D); // deck rail m.box(x,y+th*0.85,z+sd*(W-th*0.5),len*0.90,th*0.30,th*0.8,trim||MET_L); // bright reveal m.box(x+L*0.44,y+th*0.2,z+sd*W*0.46,len*0.32,th*0.45,th*1.3,trim||MET_L,sd*0.44); // nose chevron } m.box(x-L*0.88,y,z,th,th*0.85,wid*0.88,col||MET_D); // rear kick plate return m; } /* ============================================================================ HARD-SURFACE COMPONENT KIT The roster's tell was never polygon count — it was that everything was built from PRIMARY shapes: a box is a box, a cylinder is a cylinder, and the eye reads that as a logo, not a machine. Real hardware is components: a wheel is tire + rim + spokes + hub; a thruster is bell + throat + glow + gimbal; a window is frame + mullions + glass. Each helper below builds the COMPONENT, with the chamfers and material breaks already in it, so a model assembled from these starts at "machine" instead of earning it one box at a time. ============================================================================ */ /* Chamfered cylinder — the single most-used hard-surface move. A straight cylinder has a razor rim that catches no light; a 45-degree chamfer ring at each end gives every drum, hub and housing a lit edge. */ function chamferCyl(m,x,y,z,r,h,seg,col,colRim,yaw){ const rim=Math.min(h*0.22,r*0.30); m.cyl(x,y,z,r-rim,r,rim,seg,colRim||MET_L,yaw); m.cyl(x,y+rim,z,r,r,h-rim*2,seg,col,yaw); m.cyl(x,y+h-rim,z,r,r-rim,rim,seg,colRim||MET_L,yaw); return m; } /* A real wheel: treaded tire, rim dish, spokes, hub cap. Axis is Z (side mount) — the only orientation ground vehicles use. */ function wheelAsm(m,x,y,z,r,w,col){ const sd=z>=0?1:-1; m.cyl(x,y,z,r,r,w,12,TREAD,Math.PI/2); /* tread lugs around the face — reads even at 20px */ for(let k=0;k<8;k++){ const a=k/8*TAU; m.box(x+Math.cos(a)*r*0.98,y+Math.sin(a)*r*0.98,z,w*0.9,r*0.22,r*0.20,DARKER,a); } m.cyl(x,y,z+sd*w*0.5,r*0.62,r*0.62,w*0.14,10,col||MET_L,Math.PI/2); // rim dish for(let k=0;k<5;k++){ const a=k/5*TAU; m.box(x+Math.cos(a)*r*0.34,y+Math.sin(a)*r*0.34,z+sd*w*0.56,r*0.5,r*0.16,w*0.10,MET_D,a); } m.cyl(x,y,z+sd*w*0.60,r*0.22,r*0.18,w*0.16,8,MET_D,Math.PI/2); // hub cap return m; } /* Thruster: gimbal collar, bell that WIDENS toward the exit, recessed throat, and a glow disc set inside the bell so the emissive reads as heat coming from within, not paint on the back face. Points -X (exhaust rearward). */ function thrusterBell(m,x,y,z,r,len,col){ m.cyl(x,y,z,r*0.74,r*0.62,len*0.30,10,MET_D,Math.PI/2); // gimbal collar m.cyl(x-len*0.30,y,z,r*0.62,r,len*0.70,12,col||MET_L,Math.PI/2); // bell (flares out) m.tube(x-len,y,z,r*0.98,r*0.80,len*0.14,12,DARKER,Math.PI/2); // rim ring m.cyl(x-len*0.86,y,z,r*0.72,r*0.72,len*0.10,10,HOT,Math.PI/2); // glow, recessed return m; } /* Cockpit glazing: raised frame, glass panes, and mullions crossing them. Glass with no frame reads as a blue sticker; the frame is what sells it. */ function canopyGlass(m,x,y,z,len,wid,hgt,col){ m.bevelBox(x,y,z,len,hgt*0.35,wid,0.24,col||MET_L); // sill m.bevelBox(x,y+hgt*0.30,z,len*0.92,hgt*0.70,wid*0.88,0.30,GLASS); // panes m.box(x,y+hgt*0.32,z,len*0.94,hgt*0.66,wid*0.10,col||MET_L); // centre mullion m.box(x,y+hgt*0.32,z,len*0.10,hgt*0.66,wid*0.90,col||MET_L); // cross mullion m.wedge(x+len*0.55,y+hgt*0.9,z,len*0.35,hgt*0.25,wid*0.9,col||MET_L,0,true); // brow return m; } /* Recessed panel with a lip — the inner-extrusion move as a one-liner, for flat faces that need a fabricated break without restructuring the hull. */ function insetPanel(m,x,y,z,len,wid,depth,col,yaw){ m.box(x,y,z,len,depth,wid,DARKER,yaw); m.box(x,y+depth*0.55,z,len*0.86,depth*0.55,wid*0.86,col||MET_D,yaw); return m; } /* Road wheels with return rollers and a drive sprocket — a proper running gear instead of a plain slab, visible along the whole flank. WEIGHT CLASS, NOT JUST SIZE. Every ground vehicle from a size-16 Lancer to a size-26 Basilisk used the same proportions and the same wheel count, so a light and a heavy were the identical machine viewed at two zoom levels — and with nothing on screen to measure against, the player had no way to tell which one was the expensive one. `cls` (0 light / 1 medium / 2 heavy) widens the tracks, adds or removes a road wheel, and gives a heavy a second skirt tier, so weight reads at a glance from the running gear alone. Defaults to medium, which is what every existing caller was already getting. */ function runningGear(m,len,wid,h,gauge,wheels,col,cls){ const K=cls==null?1:cls; wid*=[0.82,1.0,1.26][K]; wheels=(wheels||5)+[-1,0,1][K]; for(const s of [-1,1]){ m.box(0,0,s*gauge,len,h*0.86,wid,TREAD); m.box(0,h*0.30,s*gauge,len*1.04,h*0.30,wid*1.20,DARKER); // track links const n=wheels||5; for(let k=0;k