ape-claw 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,51 +10,224 @@ import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
10
10
  import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
11
11
  import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
12
12
  import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js";
13
+ import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
14
+ import { SSAOPass } from "three/addons/postprocessing/SSAOPass.js";
15
+ import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
16
+ import { RoundedBoxGeometry } from "three/addons/geometries/RoundedBoxGeometry.js";
13
17
 
14
18
  /* ══════════════════════════════════════════════════════════
15
19
  Material palette — HV-MTL inspired chrome + neon
16
20
  ══════════════════════════════════════════════════════════ */
17
21
  const M = THREE.MeshStandardMaterial;
22
+ const P = THREE.MeshPhysicalMaterial;
18
23
 
19
- const GLOW = 0xcfff04;
20
- const CYAN = 0x63d7ff;
24
+ const GLOW = 0x4488ff;
25
+ const CYAN = 0x5599ff;
21
26
  const HOT = 0xff3344;
27
+ const RED = 0xcc2233;
28
+ const BLUE = 0x1a3a88;
22
29
 
23
30
  export const MAT = {
24
- armor: new M({ color: 0x556070, roughness: 0.45, metalness: 0.4, emissive: 0x1a2030, emissiveIntensity: 0.5 }),
25
- armorLt: new M({ color: 0x6a7585, roughness: 0.5, metalness: 0.35, emissive: 0x1e2535, emissiveIntensity: 0.5 }),
26
- chrome: new M({ color: 0x8090a0, roughness: 0.3, metalness: 0.5, emissive: 0x2a3545, emissiveIntensity: 0.6 }),
27
- joint: new M({ color: 0x404550, roughness: 0.6, metalness: 0.3, emissive: GLOW, emissiveIntensity: 0.25 }),
28
- visor: new M({ color: CYAN, transparent: true, opacity: 0.6, emissive: CYAN, emissiveIntensity: 2.5, roughness: 0.1, metalness: 0.2 }),
29
- core: new M({ color: GLOW, emissive: GLOW, emissiveIntensity: 3.0, roughness: 0.0, metalness: 0.0 }),
30
- coreInner: new M({ color: 0xffffff, emissive: GLOW, emissiveIntensity: 4.0, transparent: true, opacity: 0.9 }),
31
- vent: new M({ color: GLOW, emissive: GLOW, emissiveIntensity: 2.5, transparent: true, opacity: 0.8 }),
32
- ventHot: new M({ color: HOT, emissive: HOT, emissiveIntensity: 3.0, transparent: true, opacity: 0.7 }),
33
- panelLine: new M({ color: GLOW, emissive: GLOW, emissiveIntensity: 2.0, transparent: true, opacity: 0.7 }),
34
- spineGlow: new M({ color: GLOW, emissive: GLOW, emissiveIntensity: 2.5 }),
35
- hardpoint: new M({ color: 0x505560, roughness: 0.4, metalness: 0.4, emissive: CYAN, emissiveIntensity: 0.5 }),
36
- security: new M({ color: 0xFFB347, emissive: 0xFFB347, emissiveIntensity: 1.2, roughness: 0.3, metalness: 0.7 }),
37
- analytics: new M({ color: 0x63d7ff, emissive: 0x63d7ff, emissiveIntensity: 1.2, roughness: 0.3, metalness: 0.6 }),
38
- automation: new M({ color: 0x00ff00, emissive: 0x00ff00, emissiveIntensity: 1.2, roughness: 0.3, metalness: 0.6 }),
39
- devtools: new M({ color: 0x4169E1, emissive: 0x4169E1, emissiveIntensity: 1.2, roughness: 0.3, metalness: 0.6 }),
40
- nft: new M({ color: 0xb026ff, emissive: 0xb026ff, emissiveIntensity: 1.5, roughness: 0.3, metalness: 0.7 }),
41
- social: new M({ color: 0xFF7F50, emissive: 0xFF7F50, emissiveIntensity: 1.2, roughness: 0.3, metalness: 0.5 }),
42
- storage: new M({ color: 0x6A5ACD, emissive: 0x6A5ACD, emissiveIntensity: 1.0, roughness: 0.4, metalness: 0.5 }),
43
- productivity: new M({ color: 0xE6F3FF, emissive: 0xE6F3FF, emissiveIntensity: 0.8, roughness: 0.3, metalness: 0.4 }),
44
- bridge: new M({ color: 0xFF8C00, emissive: 0xFF8C00, emissiveIntensity: 1.2, roughness: 0.3, metalness: 0.6 }),
45
- trading: new M({ color: 0xFFD700, emissive: 0xFFD700, emissiveIntensity: 1.0, roughness: 0.3, metalness: 0.7 }),
46
- governance: new M({ color: 0x4B0082, emissive: 0x4B0082, emissiveIntensity: 1.2, roughness: 0.3, metalness: 0.6 }),
47
- wallet: new M({ color: 0x50C878, emissive: 0x50C878, emissiveIntensity: 1.0, roughness: 0.4, metalness: 0.5 }),
48
- defi: new M({ color: 0x32CD32, emissive: 0x32CD32, emissiveIntensity: 1.2, roughness: 0.3, metalness: 0.6 }),
49
- development: new M({ color: 0x4169E1, emissive: 0x4169E1, emissiveIntensity: 1.2, roughness: 0.3, metalness: 0.6 }),
50
- writing: new M({ color: 0xFFF8DC, emissive: 0xFFF8DC, emissiveIntensity: 0.7, roughness: 0.4, metalness: 0.4 }),
51
- communication: new M({ color: 0x00BFFF, emissive: 0x00BFFF, emissiveIntensity: 1.2, roughness: 0.3, metalness: 0.6 }),
52
- undersuit: new M({ color: 0x1a1e28, roughness: 0.75, metalness: 0.2, emissive: 0x060810, emissiveIntensity: 0.1 }),
53
- pistonMat: new M({ color: 0xb08040, roughness: 0.3, metalness: 0.85, emissive: 0x302010, emissiveIntensity: 0.15 }),
54
- cableMat: new M({ color: 0x181822, roughness: 0.9, metalness: 0.1 }),
55
- rivetMat: new M({ color: 0xd0d8e0, roughness: 0.15, metalness: 0.95, emissive: 0x506080, emissiveIntensity: 0.25 }),
56
- frameMat: new M({ color: 0x283040, roughness: 0.6, metalness: 0.45, emissive: 0x0a1020, emissiveIntensity: 0.15 }),
57
- darkChrome: new M({ color: 0x3a4555, roughness: 0.25, metalness: 0.7, emissive: 0x151e2a, emissiveIntensity: 0.3 }),
31
+ // Primary armor deep Optimus-blue with mirror clearcoat
32
+ armor: new P({ color: 0x1a2e6a, roughness: 0.15, metalness: 0.85, emissive: 0x080e28, emissiveIntensity: 0.3, clearcoat: 1.0, clearcoatRoughness: 0.05 }),
33
+ armorLt: new P({ color: 0x283d7a, roughness: 0.18, metalness: 0.8, emissive: 0x0a1230, emissiveIntensity: 0.3, clearcoat: 0.9, clearcoatRoughness: 0.08 }),
34
+ // Heroic red accent plates (chest, shoulders)
35
+ armorRed: new P({ color: 0xaa1825, roughness: 0.12, metalness: 0.85, emissive: 0x330808, emissiveIntensity: 0.4, clearcoat: 1.0, clearcoatRoughness: 0.04 }),
36
+ // Chrome high-shine polished silver
37
+ chrome: new P({ color: 0xc0ccdd, roughness: 0.04, metalness: 0.98, emissive: 0x3a4555, emissiveIntensity: 0.5, clearcoat: 1.0, clearcoatRoughness: 0.02 }),
38
+ joint: new P({ color: 0x2a2e38, roughness: 0.25, metalness: 0.7, emissive: GLOW, emissiveIntensity: 0.2, clearcoat: 0.8, clearcoatRoughness: 0.15, sheen: 0.3, sheenColor: new THREE.Color(GLOW), sheenRoughness: 0.5 }),
39
+ visor: new P({ color: 0xaaddff, emissive: CYAN, emissiveIntensity: 2.2, roughness: 0.05, metalness: 0.0, transmission: 0.85, thickness: 0.4, clearcoat: 1.0, clearcoatRoughness: 0.0, transparent: true, opacity: 0.85, iridescence: 0.6, iridescenceIOR: 1.3, iridescenceThicknessRange: [100, 400] }),
40
+ core: new P({ color: 0xaaddff, emissive: GLOW, emissiveIntensity: 3.5, roughness: 0.05, metalness: 0.0, transmission: 0.7, thickness: 0.6, clearcoat: 1.0, clearcoatRoughness: 0.0 }),
41
+ coreInner: new P({ color: 0xffffff, emissive: GLOW, emissiveIntensity: 5.0, transparent: true, opacity: 0.95, clearcoat: 1.0, clearcoatRoughness: 0.0 }),
42
+ vent: new P({ color: GLOW, emissive: GLOW, emissiveIntensity: 2.5, transparent: true, opacity: 0.8, clearcoat: 0.5, clearcoatRoughness: 0.2 }),
43
+ ventHot: new P({ color: HOT, emissive: HOT, emissiveIntensity: 3.0, transparent: true, opacity: 0.7, clearcoat: 0.3, clearcoatRoughness: 0.3 }),
44
+ panelLine: new P({ color: GLOW, emissive: GLOW, emissiveIntensity: 1.8, transparent: true, opacity: 0.7 }),
45
+ spineGlow: new P({ color: GLOW, emissive: GLOW, emissiveIntensity: 2.5, clearcoat: 0.4, clearcoatRoughness: 0.3 }),
46
+ hardpoint: new P({ color: 0x404858, roughness: 0.15, metalness: 0.8, emissive: CYAN, emissiveIntensity: 0.4, clearcoat: 0.8, clearcoatRoughness: 0.1 }),
47
+ security: new P({ color: 0xFFB347, emissive: 0xFFB347, emissiveIntensity: 1.2, roughness: 0.15, metalness: 0.8, clearcoat: 0.6, clearcoatRoughness: 0.1 }),
48
+ analytics: new P({ color: 0x63d7ff, emissive: 0x63d7ff, emissiveIntensity: 1.2, roughness: 0.15, metalness: 0.7, clearcoat: 0.6, clearcoatRoughness: 0.1 }),
49
+ automation: new P({ color: 0x00ff00, emissive: 0x00ff00, emissiveIntensity: 1.2, roughness: 0.15, metalness: 0.7, clearcoat: 0.5, clearcoatRoughness: 0.15 }),
50
+ devtools: new P({ color: 0x4169E1, emissive: 0x4169E1, emissiveIntensity: 1.2, roughness: 0.15, metalness: 0.7, clearcoat: 0.5, clearcoatRoughness: 0.15 }),
51
+ nft: new P({ color: 0xb026ff, emissive: 0xb026ff, emissiveIntensity: 1.5, roughness: 0.12, metalness: 0.8, clearcoat: 0.7, clearcoatRoughness: 0.08, iridescence: 0.4, iridescenceIOR: 1.5 }),
52
+ social: new P({ color: 0xFF7F50, emissive: 0xFF7F50, emissiveIntensity: 1.2, roughness: 0.15, metalness: 0.6, clearcoat: 0.5, clearcoatRoughness: 0.15 }),
53
+ storage: new P({ color: 0x6A5ACD, emissive: 0x6A5ACD, emissiveIntensity: 1.0, roughness: 0.2, metalness: 0.6, clearcoat: 0.4, clearcoatRoughness: 0.2 }),
54
+ productivity: new P({ color: 0xE6F3FF, emissive: 0xE6F3FF, emissiveIntensity: 0.8, roughness: 0.15, metalness: 0.5, clearcoat: 0.5, clearcoatRoughness: 0.15 }),
55
+ bridge: new P({ color: 0xFF8C00, emissive: 0xFF8C00, emissiveIntensity: 1.2, roughness: 0.15, metalness: 0.7, clearcoat: 0.5, clearcoatRoughness: 0.15 }),
56
+ trading: new P({ color: 0xFFD700, emissive: 0xFFD700, emissiveIntensity: 1.0, roughness: 0.1, metalness: 0.9, clearcoat: 0.8, clearcoatRoughness: 0.05 }),
57
+ governance: new P({ color: 0x4B0082, emissive: 0x4B0082, emissiveIntensity: 1.2, roughness: 0.15, metalness: 0.7, clearcoat: 0.5, clearcoatRoughness: 0.15 }),
58
+ wallet: new P({ color: 0x50C878, emissive: 0x50C878, emissiveIntensity: 1.0, roughness: 0.15, metalness: 0.6, clearcoat: 0.5, clearcoatRoughness: 0.15 }),
59
+ defi: new P({ color: 0x32CD32, emissive: 0x32CD32, emissiveIntensity: 1.2, roughness: 0.15, metalness: 0.7, clearcoat: 0.5, clearcoatRoughness: 0.15 }),
60
+ development: new P({ color: 0x4169E1, emissive: 0x4169E1, emissiveIntensity: 1.2, roughness: 0.15, metalness: 0.7, clearcoat: 0.5, clearcoatRoughness: 0.15 }),
61
+ writing: new P({ color: 0xFFF8DC, emissive: 0xFFF8DC, emissiveIntensity: 0.7, roughness: 0.2, metalness: 0.5, clearcoat: 0.4, clearcoatRoughness: 0.2 }),
62
+ communication: new P({ color: 0x00BFFF, emissive: 0x00BFFF, emissiveIntensity: 1.2, roughness: 0.15, metalness: 0.7, clearcoat: 0.6, clearcoatRoughness: 0.1 }),
63
+ undersuit: new P({ color: 0x0a0e18, roughness: 0.6, metalness: 0.3, emissive: 0x020408, emissiveIntensity: 0.05, clearcoat: 0.2 }),
64
+ pistonMat: new P({ color: 0xc8c8d0, roughness: 0.06, metalness: 0.95, emissive: 0x404050, emissiveIntensity: 0.2, clearcoat: 1.0, clearcoatRoughness: 0.02 }),
65
+ cableMat: new M({ color: 0x101018, roughness: 0.85, metalness: 0.15 }),
66
+ rivetMat: new P({ color: 0xe0e8f0, roughness: 0.04, metalness: 0.98, emissive: 0x607088, emissiveIntensity: 0.3, clearcoat: 1.0, clearcoatRoughness: 0.0 }),
67
+ frameMat: new P({ color: 0x1a2030, roughness: 0.35, metalness: 0.6, emissive: 0x050810, emissiveIntensity: 0.1, clearcoat: 0.4, clearcoatRoughness: 0.2 }),
68
+ darkChrome: new P({ color: 0x2a3040, roughness: 0.08, metalness: 0.92, emissive: 0x101520, emissiveIntensity: 0.25, clearcoat: 1.0, clearcoatRoughness: 0.04 }),
69
+ // Mechanical internals
70
+ servo: new P({ color: 0x2a2a32, roughness: 0.35, metalness: 0.7, emissive: 0x080810, emissiveIntensity: 0.1, clearcoat: 0.3, clearcoatRoughness: 0.3 }),
71
+ hydraulic: new P({ color: 0x886622, roughness: 0.2, metalness: 0.85, emissive: 0x221100, emissiveIntensity: 0.15, clearcoat: 0.7, clearcoatRoughness: 0.1 }),
72
+ rubber: new P({ color: 0x0c0c10, roughness: 0.9, metalness: 0.05, emissive: 0x000000, emissiveIntensity: 0.0 }),
73
+ coolingFin:new P({ color: 0x1e2838, roughness: 0.25, metalness: 0.75, emissive: 0x040810, emissiveIntensity: 0.08, clearcoat: 0.5, clearcoatRoughness: 0.15 }),
74
+ weldSeam: new P({ color: 0x505868, roughness: 0.5, metalness: 0.6, emissive: 0x101418, emissiveIntensity: 0.05, clearcoat: 0.2 }),
75
+ oilStain: new P({ color: 0x181410, roughness: 0.7, metalness: 0.2, emissive: 0x000000, emissiveIntensity: 0.0, clearcoat: 0.6, clearcoatRoughness: 0.4 }),
76
+ lens: new P({ color: 0x112244, emissive: CYAN, emissiveIntensity: 1.5, roughness: 0.02, metalness: 0.0, transmission: 0.6, thickness: 0.2, clearcoat: 1.0, clearcoatRoughness: 0.0, transparent: true, opacity: 0.9 }),
77
+ };
78
+
79
+ /* ══════════════════════════════════════════════════════════
80
+ Procedural surface detail — bump + roughness maps
81
+ Adds panel grooves, bolt heads, micro-scratches, and
82
+ worn-metal roughness variation to chassis materials.
83
+ ══════════════════════════════════════════════════════════ */
84
+ function _detailCanvas(size, fn) {
85
+ const c = document.createElement("canvas");
86
+ c.width = c.height = size;
87
+ fn(c.getContext("2d"), size);
88
+ const t = new THREE.CanvasTexture(c);
89
+ t.wrapS = t.wrapT = THREE.RepeatWrapping;
90
+ return t;
91
+ }
92
+
93
+ const _armorBump = _detailCanvas(256, (ctx, s) => {
94
+ ctx.fillStyle = "#808080"; ctx.fillRect(0, 0, s, s);
95
+ ctx.strokeStyle = "#5e5e5e"; ctx.lineWidth = 1.5;
96
+ for (let y = 0; y < s; y += 28) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(s, y); ctx.stroke(); }
97
+ for (let x = 0; x < s; x += 42) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, s); ctx.stroke(); }
98
+ ctx.fillStyle = "#949494";
99
+ for (let x = 21; x < s; x += 42) for (let y = 14; y < s; y += 28) {
100
+ ctx.beginPath(); ctx.arc(x, y, 2.5, 0, Math.PI * 2); ctx.fill();
101
+ }
102
+ ctx.strokeStyle = "#727272"; ctx.lineWidth = 0.4; ctx.globalAlpha = 0.7;
103
+ for (let i = 0; i < 50; i++) {
104
+ const sx = Math.random() * s, sy = Math.random() * s;
105
+ ctx.beginPath(); ctx.moveTo(sx, sy);
106
+ ctx.lineTo(sx + (Math.random() - 0.5) * 35, sy + (Math.random() - 0.5) * 4);
107
+ ctx.stroke();
108
+ }
109
+ });
110
+ _armorBump.repeat.set(3, 3);
111
+
112
+ const _chromeBump = _detailCanvas(256, (ctx, s) => {
113
+ ctx.fillStyle = "#808080"; ctx.fillRect(0, 0, s, s);
114
+ ctx.strokeStyle = "#747474"; ctx.lineWidth = 0.3; ctx.globalAlpha = 0.6;
115
+ for (let i = 0; i < 400; i++) {
116
+ const y = Math.random() * s, x = Math.random() * s * 0.15;
117
+ ctx.beginPath(); ctx.moveTo(x, y);
118
+ ctx.lineTo(x + 25 + Math.random() * 70, y + (Math.random() - 0.5) * 1);
119
+ ctx.stroke();
120
+ }
121
+ });
122
+ _chromeBump.repeat.set(4, 4);
123
+
124
+ const _roughVar = _detailCanvas(256, (ctx, s) => {
125
+ // Base matte
126
+ ctx.fillStyle = "#909090"; ctx.fillRect(0, 0, s, s);
127
+
128
+ // Large grunge patches (darker = shinier/oily)
129
+ ctx.globalCompositeOperation = "multiply";
130
+ for (let i = 0; i < 8; i++) {
131
+ const cx = Math.random() * s, cy = Math.random() * s, r = 30 + Math.random() * 50;
132
+ const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
133
+ g.addColorStop(0, "rgba(100,100,100,0.6)");
134
+ g.addColorStop(1, "rgba(140,140,140,0)");
135
+ ctx.fillStyle = g;
136
+ ctx.fillRect(0, 0, s, s);
137
+ }
138
+ ctx.globalCompositeOperation = "source-over";
139
+
140
+ // Scratches (lighter = rougher exposed metal/primer)
141
+ ctx.strokeStyle = "#d0d0d0"; ctx.lineWidth = 0.6; ctx.globalAlpha = 0.5;
142
+ for (let i = 0; i < 120; i++) {
143
+ const sx = Math.random() * s, sy = Math.random() * s;
144
+ ctx.beginPath(); ctx.moveTo(sx, sy);
145
+ ctx.lineTo(sx + (Math.random() - 0.5) * 30, sy + (Math.random() - 0.5) * 8);
146
+ ctx.stroke();
147
+ }
148
+
149
+ // Worn panel edges (darker = polished wear)
150
+ ctx.strokeStyle = "#606060"; ctx.lineWidth = 1.5; ctx.globalAlpha = 0.4;
151
+ for (let y = 0; y < s; y += 28) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(s, y); ctx.stroke(); }
152
+ for (let x = 0; x < s; x += 42) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, s); ctx.stroke(); }
153
+ });
154
+ _roughVar.repeat.set(3, 3);
155
+
156
+ // Procedural normal map for directional lighting on panel seams
157
+ const _armorNormal = _detailCanvas(256, (ctx, s) => {
158
+ ctx.fillStyle = "#8080ff"; ctx.fillRect(0, 0, s, s);
159
+ ctx.strokeStyle = "#6060ff"; ctx.lineWidth = 2;
160
+ for (let y = 0; y < s; y += 28) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(s, y); ctx.stroke(); }
161
+ for (let x = 0; x < s; x += 42) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, s); ctx.stroke(); }
162
+ ctx.fillStyle = "#a0a0ff";
163
+ for (let x = 21; x < s; x += 42) for (let y = 14; y < s; y += 28) {
164
+ ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fill();
165
+ }
166
+ });
167
+ _armorNormal.repeat.set(3, 3);
168
+
169
+ // Anisotropic brushed-metal direction for chrome
170
+ const _chromeAniso = _detailCanvas(256, (ctx, s) => {
171
+ ctx.fillStyle = "#8080ff"; ctx.fillRect(0, 0, s, s);
172
+ ctx.strokeStyle = "#9060ff"; ctx.lineWidth = 0.5; ctx.globalAlpha = 0.7;
173
+ for (let i = 0; i < 600; i++) {
174
+ const y = Math.random() * s, x = Math.random() * s * 0.1;
175
+ ctx.beginPath(); ctx.moveTo(x, y);
176
+ ctx.lineTo(x + 20 + Math.random() * 80, y + (Math.random() - 0.5) * 0.5);
177
+ ctx.stroke();
178
+ }
179
+ });
180
+ _chromeAniso.repeat.set(4, 4);
181
+
182
+ for (const k of ["armor", "armorLt", "armorRed"]) {
183
+ MAT[k].bumpMap = _armorBump; MAT[k].bumpScale = 0.018;
184
+ MAT[k].normalMap = _armorNormal; MAT[k].normalScale = new THREE.Vector2(0.15, 0.15);
185
+ MAT[k].roughnessMap = _roughVar;
186
+ }
187
+ for (const k of ["chrome", "darkChrome"]) {
188
+ MAT[k].bumpMap = _chromeBump; MAT[k].bumpScale = 0.01;
189
+ MAT[k].normalMap = _chromeAniso; MAT[k].normalScale = new THREE.Vector2(0.12, 0.12);
190
+ MAT[k].roughnessMap = _roughVar;
191
+ }
192
+ MAT.hardpoint.bumpMap = _armorBump; MAT.hardpoint.bumpScale = 0.012;
193
+ MAT.hardpoint.normalMap = _armorNormal; MAT.hardpoint.normalScale = new THREE.Vector2(0.1, 0.1);
194
+ MAT.pistonMat.bumpMap = _chromeBump; MAT.pistonMat.bumpScale = 0.008;
195
+ MAT.pistonMat.normalMap = _chromeAniso; MAT.pistonMat.normalScale = new THREE.Vector2(0.1, 0.1);
196
+ MAT.joint.bumpMap = _chromeBump; MAT.joint.bumpScale = 0.006;
197
+ MAT.undersuit.bumpMap = _armorBump; MAT.undersuit.bumpScale = 0.005;
198
+ MAT.frameMat.bumpMap = _armorBump; MAT.frameMat.bumpScale = 0.008;
199
+ MAT.servo.bumpMap = _armorBump; MAT.servo.bumpScale = 0.01;
200
+ MAT.servo.roughnessMap = _roughVar;
201
+ MAT.hydraulic.bumpMap = _chromeBump; MAT.hydraulic.bumpScale = 0.006;
202
+ MAT.hydraulic.normalMap = _chromeAniso; MAT.hydraulic.normalScale = new THREE.Vector2(0.08, 0.08);
203
+ MAT.coolingFin.bumpMap = _armorBump; MAT.coolingFin.bumpScale = 0.008;
204
+ MAT.weldSeam.bumpMap = _roughVar; MAT.weldSeam.bumpScale = 0.02;
205
+
206
+ /* ══════════════════════════════════════════════════════════
207
+ Film grain + vignette shader
208
+ ══════════════════════════════════════════════════════════ */
209
+ const FilmGrainShader = {
210
+ uniforms: {
211
+ tDiffuse: { value: null },
212
+ time: { value: 0 },
213
+ intensity: { value: 0.03 },
214
+ },
215
+ vertexShader: /* glsl */ `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
216
+ fragmentShader: /* glsl */ `
217
+ uniform sampler2D tDiffuse;
218
+ uniform float time;
219
+ uniform float intensity;
220
+ varying vec2 vUv;
221
+ float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
222
+ void main() {
223
+ vec4 c = texture2D(tDiffuse, vUv);
224
+ float grain = (hash(vUv * 1000.0 + time) - 0.5) * intensity;
225
+ vec2 uv = vUv * 2.0 - 1.0;
226
+ float vig = 1.0 - dot(uv * 0.55, uv * 0.55);
227
+ c.rgb = c.rgb * vig + grain;
228
+ gl_FragColor = c;
229
+ }
230
+ `,
58
231
  };
59
232
 
60
233
  /* ══════════════════════════════════════════════════════════
@@ -63,7 +236,7 @@ export const MAT = {
63
236
  export let scene, camera, renderer, composer, controls, outlinePass, css2dRenderer;
64
237
  export let robotGroup, attachmentGroup, energyNetworkGroup, platformGroup;
65
238
  export let coreMesh, visorMesh, spineSegments = [];
66
- let ambientParticles, energyStreams, bloomPass;
239
+ let ambientParticles, energyStreams, bloomPass, ssaoPass, filmGrainPass;
67
240
  let autoRotateTimer = null;
68
241
  let bloomEnabled = true;
69
242
  let selectedAttachment = null;
@@ -72,6 +245,15 @@ let exhaustFlames = [];
72
245
  let initialized = false;
73
246
  let renderPaused = false;
74
247
  let hoverHudEl = null;
248
+ let headPivotRef = null, leftArmPivotRef = null, rightArmPivotRef = null;
249
+ let agentSpeaking = false;
250
+ let heatShimmerMeshes = [];
251
+ let robotPowered = false;
252
+ let powerLevel = 0;
253
+ let powerDownTimer = null;
254
+ const POWER_UP_SPEED = 2.5;
255
+ const POWER_DOWN_SPEED = 1.2;
256
+ const POWER_DOWN_DELAY = 4000;
75
257
 
76
258
  const viewportEl = () => document.getElementById("forgeViewport");
77
259
  const canvasEl = () => document.getElementById("forgeCanvas");
@@ -114,8 +296,40 @@ export function tweenVector3(obj, from, to, durationMs, easeFn, onDone) {
114
296
  /* ══════════════════════════════════════════════════════════
115
297
  Geometry helpers
116
298
  ══════════════════════════════════════════════════════════ */
299
+ /* ══════════════════════════════════════════════════════════
300
+ Geometry cache — reuse identical geometries across all parts.
301
+ Keys encode shape + dimensions to share GPU buffers.
302
+ ══════════════════════════════════════════════════════════ */
303
+ const _geoCache = new Map();
304
+ function _cachedGeo(key, factory) {
305
+ if (!_geoCache.has(key)) _geoCache.set(key, factory());
306
+ return _geoCache.get(key);
307
+ }
308
+ function _boxGeo(w, h, d) {
309
+ const minDim = Math.min(w, h, d);
310
+ const useRounded = minDim > 0.08;
311
+ const k = `b${w.toFixed(3)}_${h.toFixed(3)}_${d.toFixed(3)}_${useRounded ? 1 : 0}`;
312
+ return _cachedGeo(k, () => useRounded
313
+ ? new RoundedBoxGeometry(w, h, d, 2, minDim * 0.12)
314
+ : new THREE.BoxGeometry(w, h, d));
315
+ }
316
+ function _cylGeo(rT, rB, h, segs) {
317
+ const k = `c${rT.toFixed(3)}_${rB.toFixed(3)}_${h.toFixed(3)}_${segs}`;
318
+ return _cachedGeo(k, () => new THREE.CylinderGeometry(rT, rB, h, segs));
319
+ }
320
+ function _sphereGeo(r, ws, hs) {
321
+ const k = `s${r.toFixed(4)}_${ws}_${hs}`;
322
+ return _cachedGeo(k, () => new THREE.SphereGeometry(r, ws, hs));
323
+ }
324
+ function _torusGeo(r, tube, rs, ts) {
325
+ const k = `t${r.toFixed(4)}_${tube.toFixed(4)}_${rs}_${ts}`;
326
+ return _cachedGeo(k, () => new THREE.TorusGeometry(r, tube, rs, ts));
327
+ }
328
+
329
+ const _DETAIL_THRESHOLD = 0.06;
330
+
117
331
  function box(g, w, h, d, mat, x, y, z, rx, ry, rz) {
118
- const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
332
+ const m = new THREE.Mesh(_boxGeo(w, h, d), mat);
119
333
  m.position.set(x, y, z);
120
334
  if (rx) m.rotation.x = rx;
121
335
  if (ry) m.rotation.y = ry;
@@ -124,41 +338,95 @@ function box(g, w, h, d, mat, x, y, z, rx, ry, rz) {
124
338
  return m;
125
339
  }
126
340
  function cyl(g, rT, rB, h, mat, x, y, z, segs) {
127
- const m = new THREE.Mesh(new THREE.CylinderGeometry(rT, rB, h, segs || 16), mat);
341
+ const s = segs || (Math.max(rT, rB) < _DETAIL_THRESHOLD ? 8 : 16);
342
+ const m = new THREE.Mesh(_cylGeo(rT, rB, h, s), mat);
128
343
  m.position.set(x, y, z);
129
344
  g.add(m);
130
345
  return m;
131
346
  }
132
347
  function sphere(g, r, mat, x, y, z) {
133
- const m = new THREE.Mesh(new THREE.SphereGeometry(r, 24, 16), mat);
348
+ const lod = r < 0.02 ? [8, 6] : r < _DETAIL_THRESHOLD ? [12, 8] : [20, 14];
349
+ const m = new THREE.Mesh(_sphereGeo(r, lod[0], lod[1]), mat);
134
350
  m.position.set(x, y, z);
135
351
  g.add(m);
136
352
  return m;
137
353
  }
138
354
  function torus(g, r, tube, mat, x, y, z) {
139
- const m = new THREE.Mesh(new THREE.TorusGeometry(r, tube, 16, 48), mat);
355
+ const rs = tube < 0.015 ? 6 : 10;
356
+ const ts = r < _DETAIL_THRESHOLD ? 16 : 28;
357
+ const m = new THREE.Mesh(_torusGeo(r, tube, rs, ts), mat);
140
358
  m.position.set(x, y, z);
141
359
  m.rotation.x = Math.PI / 2;
142
360
  g.add(m);
143
361
  return m;
144
362
  }
145
363
  function pistonGeo(g, r, len, mat, x, y, z, rx, ry, rz) {
146
- const shaft = cyl(g, r, r, len, mat, x, y, z, 8);
364
+ const shaft = cyl(g, r, r, len, mat, x, y, z, 6);
147
365
  if (rx) shaft.rotation.x = rx; if (ry) shaft.rotation.y = ry; if (rz) shaft.rotation.z = rz;
148
- const capG = new THREE.CylinderGeometry(r * 1.7, r * 1.7, len * 0.09, 8);
366
+ const capG = _cylGeo(r * 1.7, r * 1.7, len * 0.09, 6);
149
367
  const c1 = new THREE.Mesh(capG, MAT.rivetMat); c1.position.y = len / 2; shaft.add(c1);
150
368
  const c2 = new THREE.Mesh(capG, MAT.rivetMat); c2.position.y = -len / 2; shaft.add(c2);
151
369
  return shaft;
152
370
  }
153
- function rivet(g, x, y, z) { return sphere(g, 0.028, MAT.rivetMat, x, y, z); }
371
+ function rivet(g, x, y, z) {
372
+ const m = new THREE.Mesh(_sphereGeo(0.028, 6, 4), MAT.rivetMat);
373
+ m.position.set(x, y, z); m.userData._detail = true; g.add(m); return m;
374
+ }
154
375
  function ventSlits(g, w, n, sp, mat, x, y, z) {
155
376
  for (let i = 0; i < n; i++) box(g, w, 0.02, 0.05, mat, x, y + i * sp, z);
156
377
  }
157
378
  function cableRun(g, r, len, x, y, z, rx, ry, rz) {
158
- const m = cyl(g, r, r, len, MAT.cableMat, x, y, z, 6);
379
+ const m = cyl(g, r, r, len, MAT.cableMat, x, y, z, 4);
380
+ m.userData._detail = true;
159
381
  if (rx) m.rotation.x = rx; if (ry) m.rotation.y = ry; if (rz) m.rotation.z = rz;
160
382
  return m;
161
383
  }
384
+ function servoDrum(g, r, w, x, y, z, rz) {
385
+ const drum = cyl(g, r, r, w, MAT.servo, x, y, z, 10);
386
+ if (rz) drum.rotation.z = rz; else drum.rotation.z = Math.PI / 2;
387
+ const cap1 = cyl(g, r * 0.5, r * 0.5, w * 0.15, MAT.chrome, x, y, z, 6);
388
+ cap1.rotation.z = drum.rotation.z;
389
+ torus(g, r * 0.85, r * 0.08, MAT.darkChrome, x, y, z);
390
+ return drum;
391
+ }
392
+ function hydraulicRam(g, r, len, x, y, z, rx, ry, rz) {
393
+ const outer = cyl(g, r, r, len * 0.55, MAT.hydraulic, x, y, z, 8);
394
+ if (rx) outer.rotation.x = rx; if (ry) outer.rotation.y = ry; if (rz) outer.rotation.z = rz;
395
+ const inner = cyl(g, r * 0.6, r * 0.6, len * 0.65, MAT.pistonMat, x, y, z, 6);
396
+ inner.rotation.x = outer.rotation.x; inner.rotation.y = outer.rotation.y; inner.rotation.z = outer.rotation.z;
397
+ const seal = cyl(g, r * 1.2, r * 1.2, len * 0.06, MAT.rubber, x, y, z, 8);
398
+ seal.rotation.x = outer.rotation.x; seal.rotation.y = outer.rotation.y; seal.rotation.z = outer.rotation.z;
399
+ return outer;
400
+ }
401
+ function coolingFins(g, count, w, h, spacing, x, y, z, vertical) {
402
+ for (let i = 0; i < count; i++) {
403
+ const offset = (i - (count - 1) / 2) * spacing;
404
+ if (vertical) {
405
+ box(g, 0.015, h, w, MAT.coolingFin, x + offset, y, z);
406
+ } else {
407
+ box(g, w, 0.015, h, MAT.coolingFin, x, y + offset, z);
408
+ }
409
+ }
410
+ }
411
+ function boltCluster(g, count, radius, x, y, z) {
412
+ for (let i = 0; i < count; i++) {
413
+ const a = (i / count) * Math.PI * 2;
414
+ rivet(g, x + Math.cos(a) * radius, y, z + Math.sin(a) * radius);
415
+ }
416
+ }
417
+ function rubberSeal(g, r, tube, x, y, z) {
418
+ const m = new THREE.Mesh(_torusGeo(r, tube, 6, 16), MAT.rubber);
419
+ m.position.set(x, y, z); m.rotation.x = Math.PI / 2;
420
+ m.userData._detail = true; g.add(m); return m;
421
+ }
422
+ function cableBundle(g, count, r, len, x, y, z, rx, ry, rz) {
423
+ for (let i = 0; i < count; i++) {
424
+ const a = (i / count) * Math.PI * 2;
425
+ const ox = Math.cos(a) * r * 2.5;
426
+ const oz = Math.sin(a) * r * 2.5;
427
+ cableRun(g, r, len, x + ox, y, z + oz, rx, ry, rz);
428
+ }
429
+ }
162
430
 
163
431
  /* ══════════════════════════════════════════════════════════
164
432
  Build chassis — HV-MTL Activated inspired mecha
@@ -168,100 +436,329 @@ function buildChassis() {
168
436
  const g = new THREE.Group();
169
437
  glowRings = [];
170
438
  exhaustFlames = [];
439
+ heatShimmerMeshes = [];
440
+
441
+ // ── Animatable pivot groups (head + arms) ──
442
+ const _hp = new THREE.Group(); _hp.position.set(0, 5.85, 0);
443
+ const hd = new THREE.Group(); hd.position.set(0, -5.85, 0); _hp.add(hd);
444
+ const _lp = new THREE.Group(); _lp.position.set(-1.75, 5.25, 0);
445
+ const la = new THREE.Group(); la.position.set(1.75, -5.25, 0); _lp.add(la);
446
+ const _rp = new THREE.Group(); _rp.position.set(1.75, 5.25, 0);
447
+ const ra = new THREE.Group(); ra.position.set(-1.75, -5.25, 0); _rp.add(ra);
448
+
449
+ // ══════════════════════════════════════════════════════════
450
+ // HEAD — Refined helmet with advanced multi-element optical array
451
+ // ══════════════════════════════════════════════════════════
452
+
453
+ // ── Inner cranium (processor housing visible through panel gaps) ──
454
+ box(hd, 0.82, 0.72, 0.72, MAT.servo, 0, 6.4, 0);
455
+ box(hd, 0.74, 0.66, 0.66, MAT.frameMat, 0, 6.4, 0);
456
+ // Processor board detail (visible through top gap)
457
+ box(hd, 0.4, 0.02, 0.35, MAT.vent, 0, 6.72, 0);
458
+ for (let i = 0; i < 3; i++)
459
+ box(hd, 0.06, 0.04, 0.06, MAT.coreInner, -0.12 + i * 0.12, 6.73, 0.08);
460
+
461
+ // ── Outer helmet (split halves with layered sub-panels) ──
462
+ box(hd, 0.58, 0.98, 0.98, MAT.armor, -0.3, 6.44, 0);
463
+ box(hd, 0.58, 0.98, 0.98, MAT.armor, 0.3, 6.44, 0);
464
+ // Central seam
465
+ box(hd, 0.03, 0.98, 0.06, MAT.panelLine, 0, 6.44, 0.47);
466
+ box(hd, 0.02, 0.75, 0.04, MAT.weldSeam, -0.16, 6.52, 0.47);
467
+ box(hd, 0.02, 0.75, 0.04, MAT.weldSeam, 0.16, 6.52, 0.47);
468
+ // Sub-panel chrome inlays
469
+ for (let side = -1; side <= 1; side += 2) {
470
+ box(hd, 0.05, 0.55, 0.48, MAT.darkChrome, side * 0.32, 6.64, 0.22);
471
+ box(hd, 0.03, 0.35, 0.28, MAT.servo, side * 0.3, 6.74, 0.32);
472
+ // Temple status indicator lights (3 per side)
473
+ for (let i = 0; i < 3; i++)
474
+ sphere(hd, 0.015, MAT.vent, side * 0.58, 6.62 + i * 0.1, 0.3);
475
+ }
171
476
 
172
- // ── HEAD ──
173
- box(g, 0.85, 0.72, 0.72, MAT.undersuit, 0, 6.4, 0);
174
- // Helmet halves with center seam
175
- box(g, 0.54, 0.88, 0.92, MAT.armor, -0.29, 6.42, 0);
176
- box(g, 0.54, 0.88, 0.92, MAT.armor, 0.29, 6.42, 0);
177
- box(g, 0.03, 0.9, 0.06, MAT.panelLine, 0, 6.42, 0.44);
178
- // Crown ridge (layered)
179
- box(g, 0.35, 0.14, 0.8, MAT.chrome, 0, 6.9, -0.04);
180
- box(g, 0.22, 0.06, 0.6, MAT.darkChrome, 0, 6.98, -0.04);
181
- // Brow plate
182
- box(g, 1.12, 0.18, 0.35, MAT.armorLt, 0, 6.72, 0.28, -0.15, 0, 0);
183
- box(g, 0.8, 0.06, 0.25, MAT.darkChrome, 0, 6.66, 0.32, -0.12, 0, 0);
184
- // Chin guard + vent slits
185
- box(g, 0.7, 0.22, 0.45, MAT.chrome, 0, 5.98, 0.15);
186
- box(g, 0.5, 0.08, 0.35, MAT.darkChrome, 0, 5.9, 0.2);
187
- ventSlits(g, 0.35, 3, 0.055, MAT.vent, 0, 5.88, 0.38);
188
- // Cheek guards (inner + outer layer)
477
+ // ── Crest (taller, multi-layered Prime fin) ──
478
+ box(hd, 0.2, 0.55, 0.75, MAT.chrome, 0, 7.16, -0.04);
479
+ box(hd, 0.12, 0.32, 0.55, MAT.armorRed, 0, 7.34, -0.04);
480
+ box(hd, 0.07, 0.18, 0.4, MAT.chrome, 0, 7.44, -0.04);
481
+ box(hd, 0.04, 0.1, 0.25, MAT.coreInner, 0, 7.5, -0.04);
482
+ for (let i = 0; i < 5; i++)
483
+ box(hd, 0.14, 0.025, 0.52, MAT.frameMat, 0, 6.98 + i * 0.11, -0.04);
484
+
485
+ // ── Brow plate (heavy chrome visor shield, bolted) ──
486
+ box(hd, 1.28, 0.18, 0.42, MAT.chrome, 0, 6.72, 0.3, -0.1, 0, 0);
487
+ box(hd, 1.0, 0.08, 0.32, MAT.darkChrome, 0, 6.66, 0.34, -0.08, 0, 0);
488
+ // Underside of brow (recessed shadow area above visor)
489
+ box(hd, 1.2, 0.06, 0.12, MAT.servo, 0, 6.62, 0.42);
490
+ boltCluster(hd, 5, 0.1, -0.42, 6.72, 0.46);
491
+ boltCluster(hd, 5, 0.1, 0.42, 6.72, 0.46);
492
+
493
+ // ══════════════════════════════════════════════════════════
494
+ // VISION SYSTEM — Recessed optical housing with multi-element array
495
+ // ══════════════════════════════════════════════════════════
496
+
497
+ // Visor housing recess (dark channel cut into helmet face)
498
+ box(hd, 1.24, 0.3, 0.12, MAT.servo, 0, 6.44, 0.44);
499
+ box(hd, 1.18, 0.26, 0.06, MAT.frameMat, 0, 6.44, 0.46);
500
+
501
+ // Main visor glass (curved band sitting in the recess)
502
+ visorMesh = box(hd, 1.2, 0.24, 0.05, MAT.visor, 0, 6.44, 0.5);
503
+
504
+ // Chrome visor frame (top rail, bottom rail, side brackets)
505
+ box(hd, 1.26, 0.03, 0.1, MAT.chrome, 0, 6.57, 0.48);
506
+ box(hd, 1.26, 0.03, 0.1, MAT.chrome, 0, 6.31, 0.48);
507
+ box(hd, 0.05, 0.3, 0.1, MAT.chrome, -0.62, 6.44, 0.48);
508
+ box(hd, 0.05, 0.3, 0.1, MAT.chrome, 0.62, 6.44, 0.48);
509
+ // Corner brackets
510
+ for (let sx = -1; sx <= 1; sx += 2)
511
+ for (let sy = -1; sy <= 1; sy += 2)
512
+ rivet(hd, sx * 0.58, 6.44 + sy * 0.12, 0.52);
513
+
514
+ // Inner visor glow plate (HUD backlight)
515
+ box(hd, 1.14, 0.2, 0.015, MAT.coreInner, 0, 6.44, 0.43);
516
+
517
+ // ── Primary optics (2 main camera barrels — left and right of center) ──
189
518
  for (let side = -1; side <= 1; side += 2) {
190
- box(g, 0.18, 0.45, 0.7, MAT.armorLt, side * 0.54, 6.3, 0.05, 0, 0, side * 0.06);
191
- box(g, 0.06, 0.25, 0.55, MAT.darkChrome, side * 0.58, 6.22, 0.02, 0, 0, side * 0.06);
192
- rivet(g, side * 0.46, 6.7, 0.36); rivet(g, side * 0.46, 6.15, 0.36);
519
+ const ox = side * 0.22;
520
+ // Outer barrel housing (chrome)
521
+ cyl(hd, 0.075, 0.075, 0.1, MAT.darkChrome, ox, 6.44, 0.42, 16);
522
+ // Lens element 1 (outer glass)
523
+ cyl(hd, 0.06, 0.055, 0.03, MAT.lens, ox, 6.44, 0.46, 16);
524
+ // Lens element 2 (inner, brighter)
525
+ cyl(hd, 0.04, 0.035, 0.02, MAT.core, ox, 6.44, 0.47, 16);
526
+ // Pupil (bright emissive center)
527
+ sphere(hd, 0.022, MAT.coreInner, ox, 6.44, 0.48);
528
+ // Iris ring
529
+ torus(hd, 0.05, 0.008, MAT.vent, ox, 6.44, 0.46);
530
+ // Focus ring (chrome)
531
+ torus(hd, 0.065, 0.006, MAT.chrome, ox, 6.44, 0.44);
532
+ // Barrel mount bolts
533
+ boltCluster(hd, 6, 0.07, ox, 6.44, 0.42);
193
534
  }
194
- // Ear vent arrays (3 glowing slits per side + housing)
535
+
536
+ // ── Central targeting sensor (between main optics) ──
537
+ cyl(hd, 0.03, 0.03, 0.08, MAT.darkChrome, 0, 6.44, 0.44, 12);
538
+ sphere(hd, 0.018, MAT.ventHot, 0, 6.44, 0.48);
539
+ torus(hd, 0.025, 0.004, MAT.chrome, 0, 6.44, 0.46);
540
+
541
+ // ── Peripheral sensors (small wide-angle lenses at visor edges) ──
195
542
  for (let side = -1; side <= 1; side += 2) {
196
- box(g, 0.08, 0.35, 0.18, MAT.frameMat, side * 0.63, 6.35, -0.12);
197
- for (let i = 0; i < 3; i++)
198
- box(g, 0.04, 0.055, 0.14, MAT.vent, side * 0.65, 6.32 + i * 0.1, -0.12);
199
- }
200
- // Visor — 3-segment with chrome frame dividers + inner glow layer
201
- box(g, 0.42, 0.2, 0.05, MAT.visor, -0.32, 6.42, 0.47);
202
- visorMesh = box(g, 0.38, 0.22, 0.06, MAT.visor, 0, 6.42, 0.48);
203
- box(g, 0.42, 0.2, 0.05, MAT.visor, 0.32, 6.42, 0.47);
204
- box(g, 0.03, 0.25, 0.08, MAT.chrome, -0.13, 6.42, 0.46);
205
- box(g, 0.03, 0.25, 0.08, MAT.chrome, 0.13, 6.42, 0.46);
206
- box(g, 1.0, 0.12, 0.02, MAT.coreInner, 0, 6.42, 0.42);
207
- // Forehead glow
208
- box(g, 0.6, 0.04, 0.05, MAT.panelLine, 0, 6.74, 0.38);
209
- // Antenna fins (mount base + fin + glow strip)
543
+ const px = side * 0.48;
544
+ cyl(hd, 0.025, 0.025, 0.05, MAT.frameMat, px, 6.44, 0.44, 10);
545
+ cyl(hd, 0.018, 0.015, 0.02, MAT.lens, px, 6.44, 0.47, 10);
546
+ sphere(hd, 0.01, MAT.coreInner, px, 6.44, 0.48);
547
+ }
548
+
549
+ // ── Scanning emitter bar (thin line below visor that sweeps) ──
550
+ box(hd, 1.1, 0.02, 0.04, MAT.vent, 0, 6.3, 0.49);
551
+ box(hd, 0.8, 0.012, 0.02, MAT.coreInner, 0, 6.3, 0.5);
552
+
553
+ // ── Infrared/thermal sensors (above visor, recessed) ──
210
554
  for (let side = -1; side <= 1; side += 2) {
211
- box(g, 0.06, 0.08, 0.14, MAT.frameMat, side * 0.5, 6.78, -0.1);
212
- box(g, 0.05, 0.55, 0.2, MAT.chrome, side * 0.55, 6.88, -0.1, 0, 0, side * 0.2);
213
- box(g, 0.02, 0.35, 0.04, MAT.panelLine, side * 0.56, 6.95, -0.05, 0, 0, side * 0.2);
214
- rivet(g, side * 0.51, 6.78, -0.02);
555
+ cyl(hd, 0.02, 0.02, 0.04, MAT.darkChrome, side * 0.35, 6.6, 0.44, 8);
556
+ sphere(hd, 0.012, MAT.ventHot, side * 0.35, 6.6, 0.46);
215
557
  }
216
- // Back data ports (glowing connectors)
217
- box(g, 0.8, 0.5, 0.1, MAT.darkChrome, 0, 6.4, -0.42);
218
- for (let i = 0; i < 4; i++) {
219
- cyl(g, 0.055, 0.055, 0.08, MAT.frameMat, -0.2 + i * 0.13, 6.4, -0.46, 8);
220
- sphere(g, 0.022, MAT.vent, -0.2 + i * 0.13, 6.4, -0.5);
558
+
559
+ // ── Under-visor data strip (status readout line) ──
560
+ box(hd, 0.9, 0.025, 0.03, MAT.panelLine, 0, 6.28, 0.48);
561
+
562
+ // ══════════════════════════════════════════════════════════
563
+ // FACEPLATE / JAW (segmented with jaw actuators)
564
+ // ══════════════════════════════════════════════════════════
565
+
566
+ // Upper faceplate
567
+ box(hd, 0.82, 0.2, 0.54, MAT.chrome, 0, 6.12, 0.15);
568
+ box(hd, 0.64, 0.08, 0.44, MAT.darkChrome, 0, 6.04, 0.2);
569
+ // Faceplate vent grille (articulated slats)
570
+ for (let i = 0; i < 6; i++) {
571
+ box(hd, 0.5, 0.015, 0.06, MAT.vent, 0, 6.16 - i * 0.032, 0.42);
572
+ box(hd, 0.48, 0.008, 0.04, MAT.frameMat, 0, 6.16 - i * 0.032, 0.44);
573
+ }
574
+ // Lower jaw / chin guard
575
+ box(hd, 0.72, 0.18, 0.48, MAT.chrome, 0, 5.88, 0.16);
576
+ box(hd, 0.52, 0.06, 0.36, MAT.servo, 0, 5.82, 0.2);
577
+ // Jaw actuator pistons (visible, one per side)
578
+ for (let side = -1; side <= 1; side += 2) {
579
+ hydraulicRam(hd, 0.015, 0.2, side * 0.32, 5.95, 0.08);
580
+ rivet(hd, side * 0.34, 6.04, 0.38);
581
+ rivet(hd, side * 0.3, 5.86, 0.38);
582
+ }
583
+
584
+ // ══════════════════════════════════════════════════════════
585
+ // CHEEK GUARDS (angular, with exposed internals)
586
+ // ══════════════════════════════════════════════════════════
587
+ for (let side = -1; side <= 1; side += 2) {
588
+ box(hd, 0.24, 0.56, 0.82, MAT.armor, side * 0.58, 6.34, 0.04, 0, 0, side * 0.08);
589
+ box(hd, 0.12, 0.38, 0.66, MAT.darkChrome, side * 0.62, 6.26, 0.01, 0, 0, side * 0.08);
590
+ // Cheek inset panel (servo exposed)
591
+ box(hd, 0.06, 0.24, 0.38, MAT.servo, side * 0.6, 6.18, -0.08, 0, 0, side * 0.08);
592
+ cyl(hd, 0.04, 0.04, 0.06, MAT.hydraulic, side * 0.6, 6.08, 0.02, 8);
593
+ // Cheek surface detail
594
+ box(hd, 0.02, 0.35, 0.04, MAT.panelLine, side * 0.52, 6.32, 0.38);
595
+ rivet(hd, side * 0.5, 6.72, 0.4); rivet(hd, side * 0.5, 6.18, 0.4);
596
+ rivet(hd, side * 0.52, 6.46, 0.44); rivet(hd, side * 0.54, 6.02, 0.36);
597
+ }
598
+
599
+ // ══════════════════════════════════════════════════════════
600
+ // EAR MODULES (sensor arrays + cooling + comms antenna)
601
+ // ══════════════════════════════════════════════════════════
602
+ for (let side = -1; side <= 1; side += 2) {
603
+ // Main housing
604
+ box(hd, 0.14, 0.46, 0.24, MAT.frameMat, side * 0.7, 6.38, -0.12);
605
+ box(hd, 0.08, 0.34, 0.2, MAT.servo, side * 0.72, 6.38, -0.14);
606
+ // Vent grille (5 slats)
607
+ for (let i = 0; i < 5; i++)
608
+ box(hd, 0.07, 0.05, 0.2, MAT.vent, side * 0.72, 6.26 + i * 0.065, -0.12);
609
+ // Cooling radiator
610
+ coolingFins(hd, 6, 0.18, 0.12, 0.032, side * 0.74, 6.38, -0.24, true);
611
+ // Side-mounted sensor lens
612
+ sphere(hd, 0.025, MAT.lens, side * 0.7, 6.54, -0.02);
613
+ // Comms antenna nub
614
+ cyl(hd, 0.012, 0.008, 0.12, MAT.chrome, side * 0.72, 6.64, -0.14, 6);
615
+ sphere(hd, 0.015, MAT.vent, side * 0.72, 6.7, -0.14);
616
+ }
617
+
618
+ box(hd, 0.74, 0.04, 0.05, MAT.panelLine, 0, 6.8, 0.42);
619
+
620
+ // ══════════════════════════════════════════════════════════
621
+ // ANTENNA FINS (taller, with wiring and tip lights)
622
+ // ══════════════════════════════════════════════════════════
623
+ for (let side = -1; side <= 1; side += 2) {
624
+ box(hd, 0.09, 0.14, 0.2, MAT.frameMat, side * 0.55, 6.84, -0.1);
625
+ box(hd, 0.08, 0.8, 0.26, MAT.chrome, side * 0.6, 7.06, -0.1, 0, 0, side * 0.16);
626
+ box(hd, 0.04, 0.6, 0.07, MAT.panelLine, side * 0.61, 7.12, -0.04, 0, 0, side * 0.16);
627
+ cableRun(hd, 0.01, 0.5, side * 0.57, 6.98, -0.2, 0, 0, side * 0.16);
628
+ rivet(hd, side * 0.56, 6.84, -0.02);
629
+ // Tip light
630
+ sphere(hd, 0.022, MAT.coreInner, side * 0.62, 7.42, -0.1);
631
+ // Secondary micro-antenna
632
+ cyl(hd, 0.006, 0.006, 0.15, MAT.chrome, side * 0.56, 7.4, -0.18, 6);
633
+ sphere(hd, 0.01, MAT.ventHot, side * 0.56, 7.48, -0.18);
221
634
  }
222
635
 
223
- // ── NECK ──
224
- cyl(g, 0.22, 0.32, 0.5, MAT.joint, 0, 5.85, 0);
636
+ // ══════════════════════════════════════════════════════════
637
+ // BACK OF HEAD (data ports, cooling radiator, cable harness)
638
+ // ══════════════════════════════════════════════════════════
639
+ box(hd, 0.92, 0.62, 0.16, MAT.darkChrome, 0, 6.42, -0.44);
640
+ // Data port connectors (6 total)
641
+ for (let i = 0; i < 6; i++) {
642
+ const px = -0.3 + i * 0.12;
643
+ cyl(hd, 0.05, 0.05, 0.1, MAT.frameMat, px, 6.42, -0.5, 8);
644
+ sphere(hd, 0.02, MAT.vent, px, 6.42, -0.54);
645
+ cyl(hd, 0.06, 0.06, 0.02, MAT.darkChrome, px, 6.42, -0.48, 8);
646
+ }
647
+ // Cooling radiator array
648
+ coolingFins(hd, 10, 0.6, 0.07, 0.042, 0, 6.18, -0.52, false);
649
+ // Cable harness exiting skull
650
+ cableBundle(hd, 4, 0.014, 0.4, 0, 6.08, -0.5);
651
+ cableBundle(hd, 2, 0.01, 0.3, 0.2, 6.08, -0.48);
652
+ cableBundle(hd, 2, 0.01, 0.3, -0.2, 6.08, -0.48);
653
+
654
+ // ── NECK (exposed servos, hydraulic actuators, cable harness) ──
655
+ // Central neck column
656
+ cyl(g, 0.2, 0.32, 0.55, MAT.servo, 0, 5.85, 0);
657
+ // Rubber gaskets top and bottom
658
+ rubberSeal(g, 0.25, 0.02, 0, 6.08, 0);
659
+ rubberSeal(g, 0.34, 0.025, 0, 5.62, 0);
660
+ // Two neck servos (yaw + pitch)
661
+ servoDrum(g, 0.12, 0.18, 0.22, 5.95, 0);
662
+ servoDrum(g, 0.1, 0.14, -0.2, 5.78, 0);
663
+ // Hydraulic actuators (4 around neck for head tilt)
225
664
  for (let i = 0; i < 4; i++) {
226
665
  const a = (i / 4) * Math.PI * 2 + 0.4;
227
- cableRun(g, 0.025, 0.45, Math.cos(a) * 0.28, 5.85, Math.sin(a) * 0.28);
666
+ const cx = Math.cos(a) * 0.26, cz = Math.sin(a) * 0.26;
667
+ hydraulicRam(g, 0.022, 0.45, cx, 5.85, cz);
228
668
  }
229
- const neckRing = torus(g, 0.32, 0.04, MAT.vent, 0, 5.85, 0);
669
+ // Cable harness running through neck
670
+ cableBundle(g, 5, 0.014, 0.5, 0, 5.85, 0.22);
671
+ cableBundle(g, 3, 0.012, 0.5, 0, 5.85, -0.22);
672
+ const neckRing = torus(g, 0.35, 0.05, MAT.vent, 0, 5.85, 0);
230
673
  glowRings.push(neckRing);
231
- const neckRing2 = torus(g, 0.28, 0.025, MAT.panelLine, 0, 5.72, 0);
674
+ const neckRing2 = torus(g, 0.3, 0.03, MAT.panelLine, 0, 5.72, 0);
232
675
  glowRings.push(neckRing2);
233
- box(g, 1.6, 0.15, 0.8, MAT.darkChrome, 0, 5.6, 0);
234
-
235
- // ── TORSO ──
236
- // Endoskeleton (visible ribs between armor gaps)
237
- box(g, 0.35, 5.0, 0.35, MAT.frameMat, 0, 3.6, -0.15);
238
- for (let i = 0; i < 5; i++)
239
- box(g, 1.4, 0.05, 0.22, MAT.frameMat, 0, 3.0 + i * 0.5, 0);
240
- // Chest plates (L/R split with visible seam)
241
- box(g, 1.08, 2.6, 1.3, MAT.armor, -0.55, 4.0, 0);
242
- box(g, 1.08, 2.6, 1.3, MAT.armor, 0.55, 4.0, 0);
243
- box(g, 0.04, 2.4, 0.06, MAT.panelLine, 0, 4.0, 0.65);
244
- // Collar / gorget
245
- box(g, 1.7, 0.2, 0.9, MAT.darkChrome, 0, 5.35, 0);
246
- box(g, 1.4, 0.06, 0.5, MAT.panelLine, 0, 5.45, 0.35);
247
- // Upper chest chevron accent
248
- box(g, 2.0, 0.6, 0.12, MAT.chrome, 0, 5.0, 0.66);
249
- box(g, 1.6, 0.35, 0.06, MAT.darkChrome, 0, 5.15, 0.7);
250
- // Lower chest plate
251
- box(g, 1.8, 0.4, 0.12, MAT.armorLt, 0, 4.4, 0.7);
252
- // Side flanks (angled outward)
676
+ const neckRing3 = torus(g, 0.28, 0.02, MAT.darkChrome, 0, 5.98, 0);
677
+ // Collar plate
678
+ box(g, 1.75, 0.2, 0.88, MAT.darkChrome, 0, 5.6, 0);
679
+ boltCluster(g, 6, 0.12, 0, 5.6, 0.42);
680
+
681
+ // ── TORSO (visible internal frame, hydraulic rams, servo mounts, radiator) ──
682
+ // Central spine column (visible endoskeleton)
683
+ box(g, 0.4, 5.0, 0.4, MAT.frameMat, 0, 3.6, -0.15);
684
+ // Internal cross-ribs (exposed structural skeleton)
685
+ for (let i = 0; i < 6; i++) {
686
+ box(g, 1.6, 0.05, 0.22, MAT.frameMat, 0, 2.85 + i * 0.45, 0);
687
+ // Weld seam on each rib
688
+ box(g, 1.2, 0.02, 0.02, MAT.weldSeam, 0, 2.87 + i * 0.45, 0.11);
689
+ }
690
+ // Internal servo mounts (visible in torso gap)
253
691
  for (let side = -1; side <= 1; side += 2) {
254
- box(g, 0.28, 2.0, 1.1, MAT.armorLt, side * 1.15, 4.1, 0, 0, 0, side * -0.08);
255
- box(g, 0.08, 1.6, 0.85, MAT.darkChrome, side * 1.22, 4.1, 0, 0, 0, side * -0.08);
256
- rivet(g, side * 1.05, 5.0, 0.55); rivet(g, side * 1.05, 3.2, 0.55);
257
- }
258
- // Back plate (layered)
259
- box(g, 1.8, 2.2, 0.18, MAT.chrome, 0, 4.1, -0.7);
260
- box(g, 1.3, 1.6, 0.08, MAT.darkChrome, 0, 4.2, -0.76);
261
- // Ab plates (3 per side)
692
+ servoDrum(g, 0.08, 0.12, side * 0.4, 4.6, -0.1);
693
+ servoDrum(g, 0.07, 0.1, side * 0.35, 3.4, -0.08);
694
+ }
695
+ // Hydraulic rams connecting chest to waist (visible in side gaps)
696
+ for (let side = -1; side <= 1; side += 2) {
697
+ hydraulicRam(g, 0.035, 1.4, side * 0.95, 3.5, -0.35);
698
+ hydraulicRam(g, 0.028, 1.0, side * 0.85, 3.8, 0.35);
699
+ }
700
+ // Main chest plates — RED L/R split
701
+ box(g, 1.15, 2.8, 1.35, MAT.armorRed, -0.58, 4.0, 0);
702
+ box(g, 1.15, 2.8, 1.35, MAT.armorRed, 0.58, 4.0, 0);
703
+ // Central seam with weld detail
704
+ box(g, 0.04, 2.6, 0.06, MAT.panelLine, 0, 4.0, 0.68);
705
+ box(g, 0.02, 2.4, 0.03, MAT.weldSeam, 0, 4.0, 0.71);
706
+ // Windshield-style chest windows
707
+ for (let side = -1; side <= 1; side += 2) {
708
+ box(g, 0.55, 0.7, 0.06, MAT.visor, side * 0.42, 4.6, 0.7);
709
+ box(g, 0.04, 0.76, 0.08, MAT.chrome, side * 0.14, 4.6, 0.7);
710
+ // Window frame bolts
711
+ rivet(g, side * 0.62, 4.88, 0.72); rivet(g, side * 0.62, 4.32, 0.72);
712
+ rivet(g, side * 0.22, 4.88, 0.72); rivet(g, side * 0.22, 4.32, 0.72);
713
+ }
714
+ // Collar / gorget — heavy chrome, bolted
715
+ box(g, 1.88, 0.26, 1.02, MAT.chrome, 0, 5.38, 0);
716
+ box(g, 1.52, 0.08, 0.56, MAT.panelLine, 0, 5.48, 0.38);
717
+ boltCluster(g, 6, 0.16, 0, 5.38, 0.5);
718
+ // Upper chest chevron — chrome accent
719
+ box(g, 2.22, 0.66, 0.14, MAT.chrome, 0, 5.02, 0.68);
720
+ box(g, 1.72, 0.4, 0.07, MAT.darkChrome, 0, 5.18, 0.72);
721
+ // Lower chest
722
+ box(g, 1.92, 0.44, 0.14, MAT.armor, 0, 4.4, 0.72);
723
+ // Side flanks — blue armor with exposed mechanical sub-layer
724
+ for (let side = -1; side <= 1; side += 2) {
725
+ box(g, 0.34, 2.2, 1.18, MAT.armor, side * 1.22, 4.1, 0, 0, 0, side * -0.08);
726
+ box(g, 0.12, 1.8, 0.92, MAT.darkChrome, side * 1.3, 4.1, 0, 0, 0, side * -0.08);
727
+ // Visible servo + cable through flank gap
728
+ box(g, 0.06, 0.8, 0.3, MAT.servo, side * 1.08, 4.5, -0.2, 0, 0, side * -0.08);
729
+ cableBundle(g, 2, 0.01, 1.4, side * 1.05, 4.1, 0.3);
730
+ rivet(g, side * 1.12, 5.0, 0.58); rivet(g, side * 1.12, 3.2, 0.58);
731
+ rivet(g, side * 1.12, 4.3, 0.58); rivet(g, side * 1.12, 3.8, 0.58);
732
+ }
733
+ // Back plate — layered armor with exposed mechanical sub-structure
734
+ box(g, 1.94, 2.44, 0.24, MAT.chrome, 0, 4.1, -0.72);
735
+ box(g, 1.44, 1.84, 0.12, MAT.darkChrome, 0, 4.2, -0.78);
736
+ // Central spine channel recess
737
+ box(g, 0.26, 2.0, 0.06, MAT.servo, 0, 4.1, -0.84);
738
+ // Upper + lower horizontal reinforcement ribs
739
+ box(g, 1.6, 0.06, 0.08, MAT.frameMat, 0, 5.1, -0.78);
740
+ box(g, 1.6, 0.06, 0.08, MAT.frameMat, 0, 3.1, -0.78);
741
+ box(g, 1.3, 0.06, 0.06, MAT.weldSeam, 0, 4.1, -0.82);
742
+ // Back armor panel lines (vertical + cross)
743
+ for (let side = -1; side <= 1; side += 2) {
744
+ box(g, 0.04, 2.2, 0.06, MAT.panelLine, side * 0.62, 4.1, -0.78);
745
+ box(g, 0.04, 1.4, 0.06, MAT.panelLine, side * 0.3, 4.1, -0.78);
746
+ // Exposed servo blocks visible through back gaps
747
+ box(g, 0.14, 0.4, 0.08, MAT.servo, side * 0.48, 4.5, -0.82);
748
+ box(g, 0.14, 0.4, 0.08, MAT.servo, side * 0.48, 3.7, -0.82);
749
+ // Back plate rivets
750
+ rivet(g, side * 0.7, 5.0, -0.78); rivet(g, side * 0.7, 3.2, -0.78);
751
+ rivet(g, side * 0.4, 4.8, -0.78); rivet(g, side * 0.4, 3.4, -0.78);
752
+ }
753
+ // Primary radiator cooling array
754
+ coolingFins(g, 14, 1.3, 0.09, 0.055, 0, 4.2, -0.88, false);
755
+ // Secondary lower radiator
756
+ coolingFins(g, 8, 0.8, 0.06, 0.045, 0, 3.2, -0.86, false);
757
+ // Ab plates — chrome segments with bolts
262
758
  for (let side = -1; side <= 1; side += 2) {
263
759
  for (let i = 0; i < 3; i++) {
264
- box(g, 0.42, 0.28, 0.12, MAT.armorLt, side * 0.35, 3.0 + i * 0.33, 0.68);
760
+ box(g, 0.44, 0.3, 0.13, MAT.chrome, side * 0.36, 3.0 + i * 0.35, 0.7);
761
+ rivet(g, side * 0.36, 3.1 + i * 0.35, 0.78);
265
762
  }
266
763
  }
267
764
  // Panel line accents
@@ -270,6 +767,53 @@ function buildChassis() {
270
767
  box(g, 1.4, 0.04, 0.06, MAT.panelLine, 0, 3.2, 0.68);
271
768
  box(g, 1.4, 0.04, 0.06, MAT.panelLine, 0, 4.8, 0.68);
272
769
 
770
+ // ── DECALS (unit markings, hazard stripes, caution labels) ──
771
+ function decalTex(w, h, fn) {
772
+ const c = document.createElement("canvas"); c.width = w; c.height = h;
773
+ fn(c.getContext("2d"), w, h);
774
+ const t = new THREE.CanvasTexture(c);
775
+ t.minFilter = THREE.LinearFilter; t.generateMipmaps = false;
776
+ return new P({ map: t, transparent: true, depthWrite: false, roughness: 0.5, metalness: 0.3 });
777
+ }
778
+ // Left chest: unit designation
779
+ const unitMat = decalTex(256, 64, (ctx, w, h) => {
780
+ ctx.clearRect(0, 0, w, h);
781
+ ctx.font = "bold 36px monospace"; ctx.fillStyle = "#e0e8ff";
782
+ ctx.globalAlpha = 0.65; ctx.fillText("AC-01", 12, 40);
783
+ ctx.font = "11px monospace"; ctx.fillStyle = "#6688cc";
784
+ ctx.globalAlpha = 0.5; ctx.fillText("APECLAW PRIME", 12, 56);
785
+ });
786
+ const unitDecal = new THREE.Mesh(new THREE.PlaneGeometry(0.65, 0.17), unitMat);
787
+ unitDecal.position.set(-0.58, 4.68, 0.74); unitDecal.renderOrder = 3;
788
+ g.add(unitDecal);
789
+ // Right chest: faction badge (blue/chrome stripe)
790
+ const hazMat = decalTex(256, 128, (ctx, w, h) => {
791
+ ctx.clearRect(0, 0, w, h);
792
+ ctx.globalAlpha = 0.5;
793
+ for (let i = 0; i < 8; i++) {
794
+ ctx.fillStyle = i % 2 === 0 ? "#3355aa" : "#c0ccdd";
795
+ ctx.beginPath();
796
+ ctx.moveTo(i * 32, 0); ctx.lineTo(i * 32 + 32, 0);
797
+ ctx.lineTo(i * 32 + 16, h); ctx.lineTo(i * 32 - 16, h);
798
+ ctx.closePath(); ctx.fill();
799
+ }
800
+ });
801
+ const hazDecal = new THREE.Mesh(new THREE.PlaneGeometry(0.5, 0.25), hazMat);
802
+ hazDecal.position.set(0.58, 4.22, 0.74); hazDecal.renderOrder = 3;
803
+ g.add(hazDecal);
804
+ // Waist caution strip
805
+ const cautionMat = decalTex(192, 32, (ctx, w, h) => {
806
+ ctx.clearRect(0, 0, w, h);
807
+ ctx.globalAlpha = 0.3;
808
+ for (let x = 0; x < w; x += 16) {
809
+ ctx.fillStyle = (x / 16) % 2 === 0 ? "#cc2233" : "#1a2030";
810
+ ctx.fillRect(x, 0, 16, h);
811
+ }
812
+ });
813
+ const cautionDecal = new THREE.Mesh(new THREE.PlaneGeometry(1.1, 0.06), cautionMat);
814
+ cautionDecal.position.set(0, 2.74, 0.56); cautionDecal.renderOrder = 3;
815
+ g.add(cautionDecal);
816
+
273
817
  // ── CORE REACTOR (multi-ring housing) ──
274
818
  coreMesh = new THREE.Mesh(new THREE.OctahedronGeometry(0.3), MAT.coreInner);
275
819
  coreMesh.position.set(0, 4.5, 0.7);
@@ -290,195 +834,828 @@ function buildChassis() {
290
834
  coreGlow.layers.enable(1);
291
835
  sphere(g, 0.08, MAT.coreInner, 0, 4.5, 0.7);
292
836
 
293
- // ── WAIST / PELVIS ──
294
- box(g, 1.6, 0.7, 1.0, MAT.armorLt, 0, 2.35, 0);
295
- box(g, 1.2, 0.5, 0.8, MAT.undersuit, 0, 2.35, 0);
296
- box(g, 1.8, 0.2, 0.15, MAT.chrome, 0, 2.7, 0.52);
297
- // Hip guard skirts
837
+ // ── WAIST / PELVIS (servo mounts, hydraulic cross-links, bolted hip skirts) ──
838
+ box(g, 1.72, 0.76, 1.06, MAT.armor, 0, 2.35, 0);
839
+ box(g, 1.32, 0.56, 0.86, MAT.undersuit, 0, 2.35, 0);
840
+ // Internal waist servos (visible in gaps)
841
+ servoDrum(g, 0.1, 0.16, 0, 2.35, 0);
842
+ for (let side = -1; side <= 1; side += 2)
843
+ hydraulicRam(g, 0.025, 0.5, side * 0.55, 2.35, -0.3, 0, 0, side * 0.3);
844
+ // Chrome belt buckle
845
+ box(g, 1.92, 0.23, 0.17, MAT.chrome, 0, 2.72, 0.54);
846
+ box(g, 0.62, 0.17, 0.11, MAT.armorRed, 0, 2.72, 0.58);
847
+ boltCluster(g, 6, 0.14, 0, 2.72, 0.62);
848
+ // Hip guard skirts with bolt detail
298
849
  for (let side = -1; side <= 1; side += 2) {
299
- box(g, 0.45, 0.5, 0.7, MAT.armor, side * 0.7, 2.15, 0, 0, 0, side * -0.12);
300
- box(g, 0.35, 0.12, 0.55, MAT.darkChrome, side * 0.72, 2.3, 0, 0, 0, side * -0.12);
301
- rivet(g, side * 0.6, 2.5, 0.38);
850
+ box(g, 0.52, 0.56, 0.76, MAT.armor, side * 0.73, 2.15, 0, 0, 0, side * -0.12);
851
+ box(g, 0.4, 0.15, 0.62, MAT.darkChrome, side * 0.75, 2.32, 0, 0, 0, side * -0.12);
852
+ rivet(g, side * 0.63, 2.52, 0.42); rivet(g, side * 0.63, 2.12, 0.42);
853
+ // Visible mechanical substructure in hip gap
854
+ box(g, 0.08, 0.3, 0.2, MAT.servo, side * 0.55, 2.1, -0.15, 0, 0, side * -0.12);
302
855
  }
303
- const waistRing = torus(g, 0.9, 0.04, MAT.vent, 0, 2.7, 0);
856
+ const waistRing = torus(g, 0.96, 0.055, MAT.vent, 0, 2.72, 0);
304
857
  glowRings.push(waistRing);
305
- box(g, 0.8, 0.04, 0.06, MAT.panelLine, 0, 2.0, 0.52);
306
- // Cable conduit across waist
307
- cableRun(g, 0.02, 1.2, 0, 2.55, -0.48, 0, 0, Math.PI / 2);
308
-
309
- // ── SHOULDERS ──
310
- function buildShoulder(side) {
311
- const sx = side * 1.65;
312
- sphere(g, 0.4, MAT.joint, sx, 5.25, 0);
313
- const shoulderRing = torus(g, 0.45, 0.04, MAT.vent, sx, 5.25, 0);
858
+ box(g, 0.86, 0.05, 0.07, MAT.panelLine, 0, 2.0, 0.55);
859
+ cableBundle(g, 4, 0.012, 1.3, 0, 2.58, -0.5);
860
+
861
+ // ── SHOULDERS (massive pauldrons, exposed servo + actuators) ──
862
+ function buildShoulder(tgt, side) {
863
+ const sx = side * 1.75;
864
+ // Ball joint with rubber gasket
865
+ sphere(tgt, 0.45, MAT.joint, sx, 5.25, 0);
866
+ rubberSeal(tgt, 0.42, 0.025, sx, 5.25, 0);
867
+ const shoulderRing = torus(tgt, 0.5, 0.05, MAT.vent, sx, 5.25, 0);
314
868
  glowRings.push(shoulderRing);
315
- // 2-layer pauldron (inner mount + outer plate)
316
- box(g, 0.65, 0.35, 0.65, MAT.frameMat, sx + side * 0.2, 5.35, 0, 0, 0, side * -0.12);
317
- box(g, 0.95, 0.5, 0.85, MAT.armor, sx + side * 0.3, 5.4, 0, 0, 0, side * -0.15);
318
- box(g, 0.8, 0.15, 0.7, MAT.chrome, sx + side * 0.35, 5.65, 0, 0, 0, side * -0.15);
319
- box(g, 0.6, 0.04, 0.6, MAT.darkChrome, sx + side * 0.3, 5.55, 0, 0, 0, side * -0.15);
320
- // Vent slit array
321
- ventSlits(g, 0.4, 2, 0.08, MAT.vent, sx + side * 0.35, 5.26, 0);
322
- // Shoulder piston (connects torso to pauldron)
323
- pistonGeo(g, 0.04, 0.55, MAT.pistonMat, sx * 0.65, 5.3, -0.35, 0, 0, side * 0.6);
324
- rivet(g, sx + side * 0.15, 5.6, 0.38); rivet(g, sx + side * 0.55, 5.6, -0.3);
325
- }
326
- buildShoulder(-1);
327
- buildShoulder(1);
328
-
329
- // ── ARMS ──
330
- function buildArm(side) {
331
- const sx = side * 2.1;
332
- // Upper arm (inner visible + outer plates)
333
- box(g, 0.35, 1.3, 0.35, MAT.undersuit, sx, 4.2, 0);
334
- box(g, 0.55, 1.4, 0.5, MAT.armorLt, sx, 4.2, 0);
335
- box(g, 0.42, 0.45, 0.35, MAT.darkChrome, sx, 4.65, 0.18);
336
- box(g, 0.4, 0.06, 0.35, MAT.panelLine, sx, 4.5, 0.26);
337
- // Bicep piston
338
- pistonGeo(g, 0.03, 0.6, MAT.pistonMat, sx + side * 0.22, 4.2, -0.2);
339
- cableRun(g, 0.018, 1.0, sx - side * 0.2, 4.2, 0.18);
340
- // Elbow joint + ring + piston
341
- sphere(g, 0.28, MAT.joint, sx, 3.4, 0);
342
- const elbowRing = torus(g, 0.32, 0.03, MAT.vent, sx, 3.4, 0);
869
+ // Rotational servo drum (visible)
870
+ servoDrum(tgt, 0.15, 0.2, sx + side * 0.08, 5.25, -0.15);
871
+ // Inner structural mount
872
+ box(tgt, 0.72, 0.42, 0.72, MAT.frameMat, sx + side * 0.2, 5.38, 0, 0, 0, side * -0.1);
873
+ box(tgt, 0.5, 0.3, 0.5, MAT.servo, sx + side * 0.2, 5.38, 0, 0, 0, side * -0.1);
874
+ // Pauldron red with chrome layers
875
+ box(tgt, 1.18, 0.62, 1.02, MAT.armorRed, sx + side * 0.35, 5.46, 0, 0, 0, side * -0.12);
876
+ box(tgt, 1.02, 0.2, 0.88, MAT.chrome, sx + side * 0.4, 5.74, 0, 0, 0, side * -0.12);
877
+ box(tgt, 0.72, 0.07, 0.72, MAT.darkChrome, sx + side * 0.35, 5.63, 0, 0, 0, side * -0.12);
878
+ // Top cap with bolts
879
+ box(tgt, 0.88, 0.12, 0.78, MAT.chrome, sx + side * 0.38, 5.8, 0, 0, 0, side * -0.12);
880
+ boltCluster(tgt, 4, 0.18, sx + side * 0.38, 5.82, 0);
881
+ // Hydraulic actuator under pauldron
882
+ hydraulicRam(tgt, 0.03, 0.5, sx * 0.65, 5.32, -0.35, 0, 0, side * 0.55);
883
+ // Vent grilles
884
+ ventSlits(tgt, 0.48, 3, 0.07, MAT.vent, sx + side * 0.42, 5.26, 0);
885
+ // Cable harness from shoulder to arm
886
+ cableBundle(tgt, 3, 0.012, 0.5, sx + side * 0.1, 5.0, 0.25);
887
+ rivet(tgt, sx + side * 0.15, 5.67, 0.44); rivet(tgt, sx + side * 0.62, 5.67, -0.38);
888
+ rivet(tgt, sx + side * 0.35, 5.44, 0.5); rivet(tgt, sx + side * 0.35, 5.44, -0.5);
889
+ }
890
+ buildShoulder(la, -1);
891
+ buildShoulder(ra, 1);
892
+
893
+ // ══════════════════════════════════════════════════════════
894
+ // ARMS layered armor, exposed actuators, articulated hands
895
+ // ══════════════════════════════════════════════════════════
896
+ function buildArm(tgt, side) {
897
+ const sx = side * 2.2;
898
+
899
+ // ── UPPER ARM ──
900
+ // Inner skeleton
901
+ box(tgt, 0.3, 1.35, 0.3, MAT.undersuit, sx, 4.2, 0);
902
+ cyl(tgt, 0.1, 0.1, 1.3, MAT.frameMat, sx, 4.2, 0, 8);
903
+ // Outer armor shell (front + back plates)
904
+ box(tgt, 0.64, 1.48, 0.58, MAT.armor, sx, 4.2, 0);
905
+ // Chrome accent band at top
906
+ box(tgt, 0.52, 0.54, 0.44, MAT.darkChrome, sx, 4.72, 0.2);
907
+ box(tgt, 0.48, 0.08, 0.42, MAT.panelLine, sx, 4.56, 0.28);
908
+ // Inner armor plate (visible in gap)
909
+ box(tgt, 0.3, 0.8, 0.2, MAT.servo, sx - side * 0.15, 4.2, -0.2);
910
+ // Bicep piston + hydraulic ram
911
+ pistonGeo(tgt, 0.036, 0.72, MAT.pistonMat, sx + side * 0.26, 4.2, -0.24);
912
+ hydraulicRam(tgt, 0.03, 0.65, sx - side * 0.22, 4.32, -0.2);
913
+ // Cable harness (inner channel)
914
+ cableBundle(tgt, 5, 0.009, 1.3, sx - side * 0.18, 4.2, 0.24);
915
+ // Bicep servo
916
+ servoDrum(tgt, 0.07, 0.12, sx + side * 0.2, 4.62, -0.08);
917
+ // Panel lines + rivets
918
+ box(tgt, 0.04, 1.2, 0.04, MAT.panelLine, sx + side * 0.3, 4.2, 0.26);
919
+ rivet(tgt, sx + side * 0.28, 4.8, 0.28); rivet(tgt, sx + side * 0.28, 3.65, 0.28);
920
+ rivet(tgt, sx - side * 0.28, 4.5, 0.28); rivet(tgt, sx - side * 0.28, 3.9, 0.28);
921
+
922
+ // ── ELBOW (multi-component joint) ──
923
+ // Ball joint core
924
+ sphere(tgt, 0.36, MAT.joint, sx, 3.4, 0);
925
+ rubberSeal(tgt, 0.34, 0.022, sx, 3.4, 0);
926
+ // Two servos (flexion + rotation)
927
+ servoDrum(tgt, 0.11, 0.16, sx, 3.4, 0.2);
928
+ servoDrum(tgt, 0.08, 0.1, sx + side * 0.18, 3.4, -0.1);
929
+ const elbowRing = torus(tgt, 0.4, 0.045, MAT.vent, sx, 3.4, 0);
343
930
  glowRings.push(elbowRing);
344
- pistonGeo(g, 0.025, 0.4, MAT.pistonMat, sx + side * 0.18, 3.4, -0.18, 0.3, 0, 0);
345
- // Forearm (armor + rail mount + wrist band)
346
- box(g, 0.3, 1.2, 0.3, MAT.undersuit, sx, 2.4, 0);
347
- box(g, 0.5, 1.3, 0.45, MAT.armor, sx, 2.4, 0);
348
- box(g, 0.55, 0.3, 0.5, MAT.chrome, sx, 2.8, 0);
349
- box(g, 0.12, 0.9, 0.12, MAT.darkChrome, sx + side * 0.28, 2.4, 0);
350
- box(g, 0.06, 0.8, 0.06, MAT.panelLine, sx + side * 0.26, 2.4, 0.22);
351
- // Wrist tech band
352
- torus(g, 0.22, 0.03, MAT.vent, sx, 1.85, 0);
353
- box(g, 0.35, 0.08, 0.35, MAT.darkChrome, sx, 1.82, 0);
354
- // Hand (palm + 3 finger stubs + thumb)
355
- box(g, 0.3, 0.35, 0.3, MAT.joint, sx, 1.55, 0);
356
- for (let f = -1; f <= 1; f++) {
357
- box(g, 0.06, 0.22, 0.08, MAT.frameMat, sx + f * 0.1, 1.32, 0.08);
931
+ // Triple actuator assembly
932
+ pistonGeo(tgt, 0.03, 0.44, MAT.pistonMat, sx + side * 0.24, 3.4, -0.22, 0.3, 0, 0);
933
+ pistonGeo(tgt, 0.028, 0.38, MAT.pistonMat, sx - side * 0.2, 3.4, -0.2, -0.25, 0, 0);
934
+ hydraulicRam(tgt, 0.022, 0.35, sx, 3.4, -0.28, 0, 0, side * 0.2);
935
+ // Elbow guard plate
936
+ box(tgt, 0.45, 0.22, 0.15, MAT.darkChrome, sx - side * 0.05, 3.4, -0.22);
937
+
938
+ // ── FOREARM (chrome gauntlet with layered plating) ──
939
+ // Inner skeleton
940
+ box(tgt, 0.32, 1.34, 0.32, MAT.undersuit, sx, 2.4, 0);
941
+ cyl(tgt, 0.08, 0.08, 1.2, MAT.frameMat, sx, 2.4, 0, 8);
942
+ // Main chrome shell
943
+ box(tgt, 0.68, 1.44, 0.64, MAT.chrome, sx, 2.4, 0);
944
+ // Red accent band (upper forearm)
945
+ box(tgt, 0.74, 0.38, 0.68, MAT.armorRed, sx, 2.88, 0);
946
+ box(tgt, 0.56, 0.12, 0.5, MAT.darkChrome, sx, 3.02, 0.1);
947
+ // Outer ridge (running down arm)
948
+ box(tgt, 0.18, 1.06, 0.18, MAT.darkChrome, sx + side * 0.35, 2.4, 0);
949
+ box(tgt, 0.08, 0.96, 0.08, MAT.panelLine, sx + side * 0.33, 2.4, 0.32);
950
+ // Inner conduit channel (exposed cables)
951
+ box(tgt, 0.12, 0.9, 0.08, MAT.servo, sx - side * 0.28, 2.4, -0.24);
952
+ cableBundle(tgt, 4, 0.008, 1.0, sx - side * 0.26, 2.4, -0.24);
953
+ // Cooling vents (lateral)
954
+ ventSlits(tgt, 0.28, 4, 0.065, MAT.vent, sx + side * 0.36, 2.15, 0.18);
955
+ // Forearm hydraulic ram
956
+ hydraulicRam(tgt, 0.022, 0.8, sx - side * 0.1, 2.4, -0.28);
957
+ // Panel rivets
958
+ rivet(tgt, sx - side * 0.26, 2.88, 0.34); rivet(tgt, sx + side * 0.26, 2.06, 0.32);
959
+ rivet(tgt, sx, 2.68, 0.35); rivet(tgt, sx, 2.18, 0.35);
960
+ rivet(tgt, sx + side * 0.34, 2.68, 0); rivet(tgt, sx + side * 0.34, 2.12, 0);
961
+
962
+ // ── WRIST (rotation assembly) ──
963
+ torus(tgt, 0.32, 0.05, MAT.vent, sx, 1.78, 0);
964
+ rubberSeal(tgt, 0.3, 0.02, sx, 1.74, 0);
965
+ rubberSeal(tgt, 0.3, 0.02, sx, 1.82, 0);
966
+ servoDrum(tgt, 0.09, 0.14, sx, 1.78, 0.14);
967
+ box(tgt, 0.46, 0.14, 0.46, MAT.darkChrome, sx, 1.78, 0);
968
+ boltCluster(tgt, 6, 0.18, sx, 1.78, 0);
969
+
970
+ // ══════════════════════════════════════════════════════════
971
+ // HAND — full mechanical assembly with armored fingers
972
+ // ══════════════════════════════════════════════════════════
973
+ // Palm chassis (layered)
974
+ box(tgt, 0.42, 0.18, 0.42, MAT.chrome, sx, 1.6, 0);
975
+ box(tgt, 0.36, 0.14, 0.36, MAT.joint, sx, 1.58, 0);
976
+ box(tgt, 0.3, 0.04, 0.3, MAT.servo, sx, 1.67, 0);
977
+ box(tgt, 0.3, 0.04, 0.3, MAT.frameMat, sx, 1.49, 0);
978
+ // Palm armor plate (top)
979
+ box(tgt, 0.38, 0.03, 0.34, MAT.darkChrome, sx, 1.685, 0);
980
+ // Grip pads on underside (3 rubber strips)
981
+ for (let p = -1; p <= 1; p++) {
982
+ box(tgt, 0.08, 0.02, 0.28, MAT.rubber, sx + p * 0.11, 1.49, 0);
983
+ }
984
+ // Tendon servo bank (5 micro servos across knuckle line)
985
+ for (let f = 0; f < 5; f++) {
986
+ const fz = -0.14 + f * 0.07;
987
+ servoDrum(tgt, 0.018, 0.05, sx, 1.66, fz);
358
988
  }
359
- box(g, 0.06, 0.16, 0.08, MAT.frameMat, sx + side * 0.16, 1.42, -0.08);
360
- rivet(g, sx - side * 0.2, 2.8, 0.25); rivet(g, sx + side * 0.2, 2.0, 0.22);
989
+ // Hydraulic manifold (distributes pressure to fingers)
990
+ box(tgt, 0.06, 0.08, 0.3, MAT.hydraulic, sx - side * 0.14, 1.58, 0);
991
+ cableBundle(tgt, 3, 0.005, 0.2, sx - side * 0.12, 1.58, 0.14);
992
+ // Knuckle guard plate (chrome bar across all 4 fingers)
993
+ box(tgt, 0.06, 0.08, 0.32, MAT.chrome, sx, 1.46, 0.02);
994
+ boltCluster(tgt, 4, 0.12, sx, 1.46, 0.04);
995
+
996
+ // 4 fingers: knuckle + 3 armored phalanges + sensor tip
997
+ const fingerSpread = [-0.13, -0.05, 0.03, 0.11];
998
+ const fingerLen = [1.0, 1.1, 1.05, 0.9];
999
+ for (let f = 0; f < 4; f++) {
1000
+ const fz = fingerSpread[f];
1001
+ const scale = fingerLen[f];
1002
+ const fy = 1.44;
1003
+
1004
+ // Knuckle ball joint + rubber gasket
1005
+ sphere(tgt, 0.032, MAT.joint, sx, fy, fz);
1006
+ torus(tgt, 0.028, 0.006, MAT.rubber, sx, fy, fz);
1007
+
1008
+ // Proximal phalanx (armored)
1009
+ const p1y = fy - 0.08 * scale;
1010
+ box(tgt, 0.052, 0.14 * scale, 0.055, MAT.chrome, sx, p1y, fz);
1011
+ box(tgt, 0.04, 0.12 * scale, 0.042, MAT.servo, sx, p1y, fz);
1012
+ // Piston actuator along proximal
1013
+ pistonGeo(tgt, 0.006, 0.1 * scale, MAT.pistonMat, sx - side * 0.022, p1y, fz);
1014
+
1015
+ // PIP joint (mid knuckle)
1016
+ const pipy = fy - 0.17 * scale;
1017
+ sphere(tgt, 0.022, MAT.joint, sx, pipy, fz);
1018
+
1019
+ // Middle phalanx (armored)
1020
+ const p2y = pipy - 0.06 * scale;
1021
+ box(tgt, 0.046, 0.1 * scale, 0.05, MAT.frameMat, sx, p2y, fz);
1022
+ box(tgt, 0.038, 0.08 * scale, 0.04, MAT.darkChrome, sx, p2y, fz);
1023
+
1024
+ // DIP joint
1025
+ const dipy = pipy - 0.12 * scale;
1026
+ sphere(tgt, 0.018, MAT.joint, sx, dipy, fz);
1027
+
1028
+ // Distal phalanx (fingertip armor)
1029
+ const p3y = dipy - 0.045 * scale;
1030
+ box(tgt, 0.042, 0.075 * scale, 0.046, MAT.chrome, sx, p3y, fz);
1031
+
1032
+ // Fingertip: rubber grip pad + pressure sensor
1033
+ const tipy = dipy - 0.08 * scale;
1034
+ sphere(tgt, 0.02, MAT.rubber, sx, tipy, fz);
1035
+ sphere(tgt, 0.008, MAT.lens, sx, tipy - 0.01, fz);
1036
+
1037
+ // Tendon cable (runs full finger length)
1038
+ cableRun(tgt, 0.004, 0.35 * scale, sx - side * 0.018, p1y, fz);
1039
+ }
1040
+
1041
+ // Thumb — 3-segment, opposable (angled outward and rotated)
1042
+ const tx = sx + side * 0.2, tz = -0.18;
1043
+ // Carpometacarpal joint (base — swivel)
1044
+ sphere(tgt, 0.035, MAT.joint, tx, 1.5, tz);
1045
+ torus(tgt, 0.03, 0.007, MAT.rubber, tx, 1.5, tz);
1046
+ servoDrum(tgt, 0.015, 0.04, tx, 1.5, tz - 0.03);
1047
+ // Metacarpal (thick base segment)
1048
+ box(tgt, 0.058, 0.1, 0.062, MAT.chrome, tx + side * 0.02, 1.42, tz);
1049
+ box(tgt, 0.046, 0.08, 0.05, MAT.servo, tx + side * 0.02, 1.42, tz);
1050
+ pistonGeo(tgt, 0.006, 0.08, MAT.pistonMat, tx + side * 0.03, 1.42, tz + 0.025);
1051
+ // MCP joint
1052
+ sphere(tgt, 0.025, MAT.joint, tx + side * 0.03, 1.36, tz);
1053
+ // Proximal phalanx
1054
+ box(tgt, 0.052, 0.09, 0.056, MAT.frameMat, tx + side * 0.04, 1.3, tz);
1055
+ box(tgt, 0.042, 0.07, 0.044, MAT.darkChrome, tx + side * 0.04, 1.3, tz);
1056
+ // IP joint
1057
+ sphere(tgt, 0.02, MAT.joint, tx + side * 0.04, 1.25, tz);
1058
+ // Distal phalanx
1059
+ box(tgt, 0.048, 0.07, 0.052, MAT.chrome, tx + side * 0.05, 1.2, tz);
1060
+ // Thumb tip
1061
+ sphere(tgt, 0.022, MAT.rubber, tx + side * 0.05, 1.165, tz);
1062
+ sphere(tgt, 0.009, MAT.lens, tx + side * 0.05, 1.155, tz);
1063
+ cableRun(tgt, 0.005, 0.28, tx, 1.38, tz);
1064
+
1065
+ // Wrist-to-palm data connector
1066
+ cyl(tgt, 0.03, 0.03, 0.04, MAT.frameMat, sx - side * 0.16, 1.7, -0.12, 8);
1067
+ sphere(tgt, 0.012, MAT.vent, sx - side * 0.16, 1.72, -0.12);
361
1068
  }
362
- buildArm(-1);
363
- buildArm(1);
1069
+ buildArm(la, -1);
1070
+ buildArm(ra, 1);
364
1071
 
365
- // ── LEGS ──
1072
+ // ══════════════════════════════════════════════════════════
1073
+ // LEGS — layered armor, shock absorber knee, articulated feet
1074
+ // ══════════════════════════════════════════════════════════
366
1075
  function buildLeg(side) {
367
- const sx = side * 0.55;
368
- // Hip joint + ring
369
- sphere(g, 0.32, MAT.joint, sx, 1.95, 0);
370
- const hipRing = torus(g, 0.36, 0.03, MAT.vent, sx, 1.95, 0);
1076
+ const sx = side * 0.6;
1077
+
1078
+ // ── HIP JOINT (multi-axis) ──
1079
+ sphere(g, 0.38, MAT.joint, sx, 1.95, 0);
1080
+ rubberSeal(g, 0.36, 0.022, sx, 1.95, 0);
1081
+ servoDrum(g, 0.12, 0.16, sx + side * 0.1, 1.95, -0.14);
1082
+ servoDrum(g, 0.08, 0.1, sx - side * 0.06, 1.95, 0.14);
1083
+ const hipRing = torus(g, 0.44, 0.05, MAT.vent, sx, 1.95, 0);
371
1084
  glowRings.push(hipRing);
372
- // Hip guard (angled skirt armor)
373
- box(g, 0.55, 0.35, 0.55, MAT.armor, sx + side * 0.12, 1.82, 0.12, 0, 0, side * -0.1);
374
- box(g, 0.4, 0.1, 0.4, MAT.darkChrome, sx + side * 0.12, 1.9, 0.15, 0, 0, side * -0.1);
375
- // Thigh (inner undersuit + outer plates front/back)
376
- box(g, 0.4, 1.5, 0.4, MAT.undersuit, sx, 0.8, 0);
377
- box(g, 0.65, 1.6, 0.65, MAT.armorLt, sx, 0.8, 0);
378
- box(g, 0.5, 0.12, 0.45, MAT.chrome, sx, 1.3, 0.33);
379
- box(g, 0.45, 0.12, 0.4, MAT.darkChrome, sx, 0.5, 0.33);
380
- box(g, 0.06, 1.0, 0.06, MAT.panelLine, sx + side * 0.3, 0.8, 0.33);
381
- // Thigh cable conduit
382
- cableRun(g, 0.018, 1.2, sx - side * 0.28, 0.8, -0.2);
383
- rivet(g, sx + side * 0.25, 1.5, 0.3); rivet(g, sx + side * 0.25, 0.1, 0.3);
384
- // Knee joint + ring + pistons (inner/outer)
385
- sphere(g, 0.26, MAT.joint, sx, -0.1, 0.1);
386
- const kneeRing = torus(g, 0.3, 0.03, MAT.vent, sx, -0.1, 0.1);
1085
+
1086
+ // ── HIP SKIRT ARMOR ──
1087
+ box(g, 0.64, 0.44, 0.64, MAT.armor, sx + side * 0.12, 1.82, 0.12, 0, 0, side * -0.1);
1088
+ box(g, 0.48, 0.14, 0.48, MAT.darkChrome, sx + side * 0.12, 1.94, 0.15, 0, 0, side * -0.1);
1089
+ box(g, 0.38, 0.08, 0.38, MAT.servo, sx + side * 0.12, 1.72, 0.12, 0, 0, side * -0.1);
1090
+ boltCluster(g, 5, 0.16, sx + side * 0.12, 1.82, 0.4);
1091
+ // Hip panel line
1092
+ box(g, 0.03, 0.4, 0.04, MAT.panelLine, sx + side * 0.35, 1.82, 0.35);
1093
+
1094
+ // ── THIGH (blue plates, layered armor, exposed hydraulics) ──
1095
+ // Inner skeleton + undersuit
1096
+ box(g, 0.4, 1.55, 0.4, MAT.undersuit, sx, 0.8, 0);
1097
+ cyl(g, 0.1, 0.1, 1.45, MAT.frameMat, sx, 0.8, 0, 8);
1098
+ // Outer armor (front + back + sides)
1099
+ box(g, 0.74, 1.68, 0.74, MAT.armor, sx, 0.8, 0);
1100
+ // Chrome accent plates (top & bottom of thigh)
1101
+ box(g, 0.58, 0.16, 0.54, MAT.chrome, sx, 1.38, 0.38);
1102
+ box(g, 0.54, 0.16, 0.5, MAT.darkChrome, sx, 0.48, 0.38);
1103
+ // Lateral panel lines
1104
+ box(g, 0.07, 1.14, 0.07, MAT.panelLine, sx + side * 0.36, 0.8, 0.38);
1105
+ box(g, 0.07, 1.14, 0.07, MAT.panelLine, sx - side * 0.36, 0.8, 0.38);
1106
+ // Thigh hydraulic ram (front, through armor gap)
1107
+ hydraulicRam(g, 0.032, 1.25, sx, 0.8, 0.4);
1108
+ // Secondary inner piston
1109
+ pistonGeo(g, 0.025, 0.8, MAT.pistonMat, sx + side * 0.15, 0.8, -0.28);
1110
+ // Rear cable conduit
1111
+ cableBundle(g, 4, 0.01, 1.4, sx - side * 0.3, 0.8, -0.28);
1112
+ // Inner mech visible in side gap
1113
+ box(g, 0.06, 0.6, 0.2, MAT.servo, sx + side * 0.32, 0.9, -0.12);
1114
+ // Rivets (8 total per thigh)
1115
+ rivet(g, sx + side * 0.32, 1.54, 0.37); rivet(g, sx + side * 0.32, 0.06, 0.37);
1116
+ rivet(g, sx - side * 0.32, 1.22, 0.37); rivet(g, sx - side * 0.32, 0.38, 0.37);
1117
+ rivet(g, sx + side * 0.32, 1.0, -0.37); rivet(g, sx + side * 0.32, 0.6, -0.37);
1118
+ rivet(g, sx, 1.48, 0.37); rivet(g, sx, 0.12, 0.37);
1119
+
1120
+ // ── KNEE (shock absorber + multi-actuator + guard) ──
1121
+ // Ball joint
1122
+ sphere(g, 0.34, MAT.joint, sx, -0.1, 0.1);
1123
+ rubberSeal(g, 0.32, 0.022, sx, -0.1, 0.1);
1124
+ const kneeRing = torus(g, 0.38, 0.05, MAT.vent, sx, -0.1, 0.1);
387
1125
  glowRings.push(kneeRing);
388
- pistonGeo(g, 0.025, 0.35, MAT.pistonMat, sx + side * 0.22, -0.1, -0.15, 0.25, 0, 0);
389
- pistonGeo(g, 0.025, 0.3, MAT.pistonMat, sx - side * 0.22, -0.1, 0.2, -0.2, 0, 0);
390
- // Shin (layered guard + inner + calf vents)
391
- box(g, 0.35, 1.4, 0.35, MAT.undersuit, sx, -1.2, 0.08);
392
- box(g, 0.55, 1.5, 0.55, MAT.armor, sx, -1.2, 0.08);
393
- box(g, 0.6, 0.6, 0.15, MAT.chrome, sx, -0.6, 0.35);
394
- box(g, 0.5, 0.35, 0.08, MAT.darkChrome, sx, -0.85, 0.36);
395
- box(g, 0.06, 0.9, 0.06, MAT.panelLine, sx, -1.2, 0.36);
396
- // Calf vent slits
397
- ventSlits(g, 0.2, 3, 0.08, MAT.vent, sx - side * 0.28, -1.4, 0);
398
- cableRun(g, 0.015, 1.1, sx + side * 0.22, -1.2, -0.18);
399
- // Ankle (joint + twin pistons)
400
- cyl(g, 0.15, 0.2, 0.3, MAT.joint, sx, -2.1, 0.08);
401
- pistonGeo(g, 0.02, 0.25, MAT.pistonMat, sx + side * 0.12, -2.0, 0.22);
402
- pistonGeo(g, 0.02, 0.25, MAT.pistonMat, sx - side * 0.12, -2.0, -0.08);
403
- // Foot (toe segment + heel + mid plate + thruster)
404
- box(g, 0.65, 0.22, 0.6, MAT.armorLt, sx, -2.35, 0.38);
405
- box(g, 0.55, 0.18, 0.35, MAT.armorLt, sx, -2.35, -0.05);
406
- box(g, 0.6, 0.08, 0.55, MAT.chrome, sx, -2.22, 0.4);
407
- box(g, 0.45, 0.04, 0.35, MAT.darkChrome, sx, -2.18, 0.4);
408
- box(g, 0.3, 0.08, 0.3, MAT.ventHot, sx, -2.48, 0.15);
409
- rivet(g, sx + side * 0.25, -2.2, 0.6); rivet(g, sx - side * 0.25, -2.2, 0.6);
1126
+ // Triple actuator assembly
1127
+ pistonGeo(g, 0.034, 0.44, MAT.pistonMat, sx + side * 0.28, -0.1, -0.22, 0.25, 0, 0);
1128
+ pistonGeo(g, 0.034, 0.38, MAT.pistonMat, sx - side * 0.28, -0.1, 0.26, -0.2, 0, 0);
1129
+ hydraulicRam(g, 0.025, 0.35, sx, -0.1, -0.3, 0.15, 0, 0);
1130
+ // Shock absorber (dual-tube behind knee)
1131
+ cyl(g, 0.055, 0.055, 0.55, MAT.pistonMat, sx + side * 0.08, -0.1, -0.28, 12);
1132
+ cyl(g, 0.04, 0.04, 0.55, MAT.chrome, sx - side * 0.08, -0.1, -0.28, 12);
1133
+ cyl(g, 0.07, 0.07, 0.22, MAT.hydraulic, sx + side * 0.08, -0.22, -0.28, 12);
1134
+ cyl(g, 0.055, 0.055, 0.18, MAT.hydraulic, sx - side * 0.08, -0.22, -0.28, 12);
1135
+ rubberSeal(g, 0.065, 0.014, sx + side * 0.08, 0.06, -0.28);
1136
+ rubberSeal(g, 0.05, 0.012, sx - side * 0.08, 0.06, -0.28);
1137
+ // Caliper brake disc (front of knee)
1138
+ cyl(g, 0.18, 0.18, 0.03, MAT.darkChrome, sx, -0.1, 0.26, 24);
1139
+ torus(g, 0.14, 0.012, MAT.chrome, sx, -0.1, 0.26);
1140
+ // Knee guard (layered: chrome + red + dark)
1141
+ box(g, 0.76, 0.58, 0.26, MAT.chrome, sx, -0.1, 0.42);
1142
+ box(g, 0.62, 0.4, 0.14, MAT.armorRed, sx, -0.08, 0.52);
1143
+ box(g, 0.52, 0.1, 0.08, MAT.darkChrome, sx, -0.3, 0.48);
1144
+ box(g, 0.52, 0.1, 0.08, MAT.darkChrome, sx, 0.1, 0.48);
1145
+ boltCluster(g, 6, 0.12, sx, -0.1, 0.54);
1146
+ // Knee panel line
1147
+ box(g, 0.04, 0.5, 0.04, MAT.panelLine, sx + side * 0.36, -0.1, 0.38);
1148
+
1149
+ // ── SHIN (blue armor, exposed cable runs, cooling vents) ──
1150
+ // Inner skeleton
1151
+ box(g, 0.38, 1.48, 0.38, MAT.undersuit, sx, -1.2, 0.08);
1152
+ cyl(g, 0.09, 0.09, 1.4, MAT.frameMat, sx, -1.2, 0.08, 8);
1153
+ // Outer armor
1154
+ box(g, 0.64, 1.58, 0.64, MAT.armor, sx, -1.2, 0.08);
1155
+ // Chrome shin guard (front plate)
1156
+ box(g, 0.69, 0.68, 0.2, MAT.chrome, sx, -0.58, 0.42);
1157
+ box(g, 0.59, 0.42, 0.14, MAT.darkChrome, sx, -0.84, 0.44);
1158
+ // Panel line down shin
1159
+ box(g, 0.07, 1.04, 0.07, MAT.panelLine, sx, -1.2, 0.44);
1160
+ box(g, 0.07, 1.04, 0.07, MAT.panelLine, sx + side * 0.3, -1.2, 0.28);
1161
+ // Shin coolant lines (rear)
1162
+ cableBundle(g, 3, 0.009, 1.1, sx + side * 0.28, -1.2, -0.24);
1163
+ cableBundle(g, 2, 0.007, 0.8, sx - side * 0.2, -1.2, -0.22);
1164
+ // Cooling vents (lateral)
1165
+ ventSlits(g, 0.26, 5, 0.065, MAT.vent, sx - side * 0.34, -1.35, 0);
1166
+ // Inner mech visible
1167
+ box(g, 0.04, 0.5, 0.12, MAT.servo, sx + side * 0.28, -1.0, -0.08);
1168
+ // Shin rivets
1169
+ rivet(g, sx + side * 0.28, -0.55, 0.38); rivet(g, sx + side * 0.28, -1.85, 0.38);
1170
+ rivet(g, sx - side * 0.28, -0.75, 0.38); rivet(g, sx - side * 0.28, -1.65, 0.38);
1171
+ rivet(g, sx, -0.55, -0.32); rivet(g, sx, -1.85, -0.32);
1172
+
1173
+ // ── ANKLE (servo + dual axis) ──
1174
+ cyl(g, 0.22, 0.26, 0.36, MAT.joint, sx, -2.1, 0.08);
1175
+ rubberSeal(g, 0.24, 0.016, sx, -1.96, 0.08);
1176
+ rubberSeal(g, 0.24, 0.016, sx, -2.24, 0.08);
1177
+ servoDrum(g, 0.075, 0.12, sx + side * 0.12, -2.1, 0.22);
1178
+ servoDrum(g, 0.06, 0.08, sx - side * 0.1, -2.1, -0.06);
1179
+ // Ankle actuators
1180
+ pistonGeo(g, 0.028, 0.32, MAT.pistonMat, sx + side * 0.16, -2.0, 0.28);
1181
+ pistonGeo(g, 0.028, 0.32, MAT.pistonMat, sx - side * 0.16, -2.0, -0.14);
1182
+ hydraulicRam(g, 0.02, 0.25, sx, -2.04, -0.16);
1183
+ // Ankle guard
1184
+ box(g, 0.35, 0.18, 0.3, MAT.darkChrome, sx, -2.02, 0.26);
1185
+
1186
+ // ══════════════════════════════════════════════════════════
1187
+ // FOOT — armored multi-segment with arch, tread, and sensors
1188
+ // ══════════════════════════════════════════════════════════
1189
+
1190
+ // ── TOE SECTION (4 armored toe segments) ──
1191
+ // Toe plate (main armored shell)
1192
+ box(g, 0.76, 0.18, 0.28, MAT.chrome, sx, -2.38, 0.62);
1193
+ box(g, 0.62, 0.05, 0.22, MAT.darkChrome, sx, -2.3, 0.64);
1194
+ // Individual toe caps (4 articulated digits)
1195
+ for (let t = 0; t < 4; t++) {
1196
+ const tz = 0.66 + (t === 0 || t === 3 ? 0 : 0.04);
1197
+ const tx = sx + (-0.24 + t * 0.16);
1198
+ // MTP joint
1199
+ sphere(g, 0.024, MAT.joint, tx, -2.38, tz - 0.04);
1200
+ // Proximal toe segment (armored)
1201
+ box(g, 0.1, 0.1, 0.1, MAT.chrome, tx, -2.42, tz);
1202
+ box(g, 0.07, 0.06, 0.07, MAT.servo, tx, -2.42, tz);
1203
+ // IP joint
1204
+ sphere(g, 0.016, MAT.joint, tx, -2.42, tz + 0.06);
1205
+ // Distal toe (claw-like tip)
1206
+ box(g, 0.08, 0.08, 0.08, MAT.darkChrome, tx, -2.44, tz + 0.1);
1207
+ // Toe grip pad (rubber)
1208
+ box(g, 0.06, 0.02, 0.06, MAT.rubber, tx, -2.48, tz + 0.1);
1209
+ // Toe pressure sensor
1210
+ sphere(g, 0.008, MAT.lens, tx, -2.48, tz + 0.12);
1211
+ }
1212
+ // Toe guard plate (protective front chrome bar)
1213
+ box(g, 0.72, 0.1, 0.04, MAT.chrome, sx, -2.36, 0.8);
1214
+ box(g, 0.56, 0.06, 0.03, MAT.armorRed, sx, -2.36, 0.82);
1215
+ boltCluster(g, 4, 0.14, sx, -2.36, 0.82);
1216
+
1217
+ // ── METATARSAL BRIDGE (visible arch structure) ──
1218
+ // Metatarsal hinge axle (connects toe plate to midfoot)
1219
+ cyl(g, 0.05, 0.05, 0.65, MAT.joint, sx, -2.38, 0.5, 12);
1220
+ rubberSeal(g, 0.045, 0.012, sx, -2.38, 0.5);
1221
+ // Arch springs (visible coil springs under arch)
1222
+ for (let s = -1; s <= 1; s += 2) {
1223
+ cyl(g, 0.025, 0.025, 0.15, MAT.pistonMat, sx + s * 0.14, -2.42, 0.38, 8);
1224
+ torus(g, 0.022, 0.005, MAT.chrome, sx + s * 0.14, -2.38, 0.38);
1225
+ torus(g, 0.022, 0.005, MAT.chrome, sx + s * 0.14, -2.46, 0.38);
1226
+ }
1227
+
1228
+ // ── MIDFOOT PLATE (armored bridge with internal servo) ──
1229
+ box(g, 0.72, 0.16, 0.28, MAT.armor, sx, -2.38, 0.3);
1230
+ box(g, 0.6, 0.04, 0.22, MAT.servo, sx, -2.32, 0.3);
1231
+ box(g, 0.6, 0.04, 0.22, MAT.frameMat, sx, -2.44, 0.3);
1232
+ // Midfoot lateral stabilizer wings
1233
+ for (let s = -1; s <= 1; s += 2) {
1234
+ box(g, 0.06, 0.12, 0.2, MAT.darkChrome, sx + s * 0.38, -2.36, 0.32);
1235
+ rivet(g, sx + s * 0.38, -2.3, 0.38);
1236
+ }
1237
+ // Arch servo (flexion control)
1238
+ servoDrum(g, 0.03, 0.16, sx, -2.36, 0.42);
1239
+ // Panel lines
1240
+ box(g, 0.04, 0.14, 0.24, MAT.panelLine, sx + side * 0.28, -2.38, 0.3);
1241
+
1242
+ // ── HEEL SECTION (shock-absorbing, armored) ──
1243
+ box(g, 0.68, 0.18, 0.36, MAT.armor, sx, -2.38, 0.02);
1244
+ box(g, 0.56, 0.05, 0.28, MAT.chrome, sx, -2.3, 0.02);
1245
+ box(g, 0.56, 0.05, 0.28, MAT.darkChrome, sx, -2.46, 0.02);
1246
+ // Heel shock absorber (visible damper cylinder)
1247
+ cyl(g, 0.04, 0.04, 0.16, MAT.pistonMat, sx, -2.36, -0.1, 10);
1248
+ cyl(g, 0.055, 0.055, 0.08, MAT.hydraulic, sx, -2.42, -0.1, 10);
1249
+ rubberSeal(g, 0.05, 0.01, sx, -2.3, -0.1);
1250
+ // Lateral dampers
1251
+ for (let s = -1; s <= 1; s += 2) {
1252
+ cyl(g, 0.025, 0.025, 0.12, MAT.pistonMat, sx + s * 0.2, -2.38, -0.06, 8);
1253
+ rubberSeal(g, 0.022, 0.006, sx + s * 0.2, -2.32, -0.06);
1254
+ }
1255
+ // Heel stabilizer fins (twin vertical fins)
1256
+ for (let s = -1; s <= 1; s += 2) {
1257
+ box(g, 0.04, 0.22, 0.12, MAT.darkChrome, sx + s * 0.18, -2.32, -0.2);
1258
+ box(g, 0.02, 0.16, 0.08, MAT.panelLine, sx + s * 0.18, -2.34, -0.24);
1259
+ }
1260
+ // Central heel ridge
1261
+ box(g, 0.14, 0.04, 0.18, MAT.chrome, sx, -2.28, -0.12);
1262
+ rivet(g, sx, -2.28, -0.2);
1263
+
1264
+ // ── SOLE (tread pattern + thrusters + ground sensors) ──
1265
+ // Sole plate
1266
+ box(g, 0.7, 0.04, 0.8, MAT.darkChrome, sx, -2.5, 0.2);
1267
+ // Tread pads (rubber grip strips)
1268
+ for (let i = 0; i < 6; i++) {
1269
+ const pz = -0.08 + i * 0.14;
1270
+ box(g, 0.56, 0.02, 0.06, MAT.rubber, sx, -2.52, pz);
1271
+ }
1272
+ // Cross-hatched tread (lateral ribs)
1273
+ for (let s = -1; s <= 1; s += 2) {
1274
+ for (let i = 0; i < 3; i++) {
1275
+ box(g, 0.04, 0.02, 0.5, MAT.rubber, sx + s * (0.2 + i * 0.06), -2.52, 0.2);
1276
+ }
1277
+ }
1278
+ // Primary thruster (central)
1279
+ box(g, 0.3, 0.08, 0.3, MAT.ventHot, sx, -2.54, 0.22);
1280
+ torus(g, 0.1, 0.015, MAT.vent, sx, -2.54, 0.22);
1281
+ // Secondary micro-thrusters (heel + toe)
1282
+ cyl(g, 0.06, 0.06, 0.04, MAT.ventHot, sx, -2.54, -0.05, 8);
1283
+ cyl(g, 0.06, 0.06, 0.04, MAT.ventHot, sx, -2.54, 0.55, 8);
1284
+ // Ground proximity sensors (5-sensor array)
1285
+ sphere(g, 0.02, MAT.lens, sx, -2.52, 0.7);
1286
+ sphere(g, 0.02, MAT.lens, sx + side * 0.24, -2.52, 0.5);
1287
+ sphere(g, 0.018, MAT.lens, sx, -2.52, 0.3);
1288
+ sphere(g, 0.018, MAT.lens, sx - side * 0.24, -2.52, 0.1);
1289
+ sphere(g, 0.02, MAT.lens, sx, -2.52, -0.1);
1290
+ // Foot rivets
1291
+ rivet(g, sx + side * 0.34, -2.28, 0.64); rivet(g, sx - side * 0.34, -2.28, 0.64);
1292
+ rivet(g, sx + side * 0.3, -2.28, 0.02); rivet(g, sx - side * 0.3, -2.28, 0.02);
1293
+ rivet(g, sx + side * 0.3, -2.28, -0.14); rivet(g, sx - side * 0.3, -2.28, -0.14);
1294
+ rivet(g, sx, -2.28, 0.46);
410
1295
  }
411
1296
  buildLeg(-1);
412
1297
  buildLeg(1);
413
1298
 
414
- // ── SPINE (vertebrae with cross-struts) ──
1299
+ // ══════════════════════════════════════════════════════════
1300
+ // SPINE — articulated vertebrae, coolant, data conduits, cross-bracing
1301
+ // ══════════════════════════════════════════════════════════
415
1302
  spineSegments = [];
416
- for (let i = 0; i < 8; i++) {
1303
+ for (let i = 0; i < 10; i++) {
1304
+ const yOff = 2.5 + i * 0.35;
417
1305
  const mat = MAT.spineGlow.clone();
418
- const seg = box(g, 0.12, 0.2, 0.12, mat, 0, 2.7 + i * 0.4, -0.72);
1306
+ // Vertebra body (tapered wider in lumbar, narrower in thoracic)
1307
+ const vw = 0.16 - i * 0.004;
1308
+ const seg = box(g, vw, 0.2, vw, mat, 0, yOff, -0.72);
419
1309
  spineSegments.push(seg);
1310
+ // Spinous process (rear protrusion)
1311
+ box(g, 0.04, 0.14, 0.06, MAT.frameMat, 0, yOff, -0.78);
1312
+ // Inter-vertebral disc (rubber gasket + fluid gap)
420
1313
  if (i > 0) {
421
- box(g, 0.22, 0.04, 0.06, MAT.frameMat, 0, 2.7 + i * 0.4 - 0.2, -0.72);
1314
+ box(g, vw - 0.02, 0.03, vw - 0.02, MAT.rubber, 0, yOff - 0.175, -0.72);
1315
+ box(g, vw + 0.08, 0.03, 0.06, MAT.frameMat, 0, yOff - 0.175, -0.72);
1316
+ // Micro-servo for inter-vertebral articulation
1317
+ servoDrum(g, 0.015, 0.04, 0, yOff - 0.175, -0.68);
422
1318
  }
423
- // Cross-struts to torso
424
- if (i >= 2 && i <= 6) {
425
- box(g, 0.04, 0.04, 0.2, MAT.frameMat, 0, 2.7 + i * 0.4, -0.6);
1319
+ // Lateral transverse processes (structural wings + cable anchors)
1320
+ for (let side = -1; side <= 1; side += 2) {
1321
+ box(g, 0.14, 0.05, 0.04, MAT.frameMat, side * 0.14, yOff, -0.72);
1322
+ // Ligament cable anchor
1323
+ sphere(g, 0.008, MAT.joint, side * 0.2, yOff, -0.72);
1324
+ }
1325
+ // Cross-struts to torso (thoracic region)
1326
+ if (i >= 2 && i <= 7) {
1327
+ box(g, 0.04, 0.04, 0.24, MAT.frameMat, 0, yOff, -0.58);
1328
+ for (let side = -1; side <= 1; side += 2) {
1329
+ box(g, 0.02, 0.04, 0.2, MAT.frameMat, side * 0.07, yOff, -0.56, 0, side * 0.15, 0);
1330
+ // Diagonal tension cables
1331
+ cableRun(g, 0.004, 0.22, side * 0.08, yOff, -0.52, 0, side * 0.2, 0);
1332
+ }
426
1333
  }
1334
+ // Rivet at each vertebra top
1335
+ if (i % 2 === 0) rivet(g, 0, yOff + 0.08, -0.74);
1336
+ }
1337
+ // Coolant lines (dual, running full spine length)
1338
+ for (let side = -1; side <= 1; side += 2) {
1339
+ cableRun(g, 0.016, 3.4, side * 0.13, 4.0, -0.78);
1340
+ cableRun(g, 0.008, 3.4, side * 0.2, 4.0, -0.76);
1341
+ }
1342
+ // Central data conduit (heavy gauge)
1343
+ cableRun(g, 0.022, 3.6, 0, 4.0, -0.84);
1344
+ // Spinal junction boxes (3 intermediate nodes)
1345
+ for (let j = 0; j < 3; j++) {
1346
+ const jy = 3.0 + j * 1.0;
1347
+ box(g, 0.08, 0.08, 0.06, MAT.darkChrome, 0, jy, -0.86);
1348
+ sphere(g, 0.012, MAT.vent, 0, jy, -0.9);
427
1349
  }
428
1350
 
429
- // ── BACK THRUSTERS / EXHAUST (detailed nozzles) ──
1351
+ // ══════════════════════════════════════════════════════════
1352
+ // JETPACK — full backpack assembly with engines, fuel, RCS, wings
1353
+ // ══════════════════════════════════════════════════════════
1354
+
1355
+ // ── CENTRAL BACKPACK FRAME ──
1356
+ // Main housing (bolted to back plate)
1357
+ box(g, 1.4, 2.2, 0.44, MAT.armor, 0, 4.0, -0.94);
1358
+ box(g, 1.26, 2.0, 0.36, MAT.darkChrome, 0, 4.0, -0.98);
1359
+ box(g, 1.1, 1.8, 0.1, MAT.servo, 0, 4.0, -1.02);
1360
+ // Mounting rail (horizontal bar across upper back)
1361
+ box(g, 1.6, 0.1, 0.2, MAT.chrome, 0, 5.0, -0.9);
1362
+ boltCluster(g, 8, 0.32, 0, 5.0, -0.84);
1363
+ // Lower mounting rail
1364
+ box(g, 1.2, 0.08, 0.16, MAT.chrome, 0, 3.0, -0.9);
1365
+ boltCluster(g, 6, 0.24, 0, 3.0, -0.84);
1366
+ // Vertical structural ribs
430
1367
  for (let side = -1; side <= 1; side += 2) {
431
- // Pylon connecting to back
432
- box(g, 0.15, 0.6, 0.25, MAT.frameMat, side * 0.6, 3.9, -0.78);
433
- // Nozzle housing (outer shell)
434
- cyl(g, 0.28, 0.38, 0.85, MAT.chrome, side * 0.6, 3.8, -0.95, 12);
435
- // Nozzle inner cone
436
- cyl(g, 0.12, 0.22, 0.35, MAT.darkChrome, side * 0.6, 3.45, -0.95, 12);
1368
+ box(g, 0.06, 2.0, 0.12, MAT.frameMat, side * 0.52, 4.0, -0.96);
1369
+ box(g, 0.03, 1.8, 0.06, MAT.weldSeam, side * 0.52, 4.0, -1.02);
1370
+ }
1371
+ // Backpack panel lines
1372
+ box(g, 0.04, 2.0, 0.06, MAT.panelLine, 0, 4.0, -1.14);
1373
+ for (let side = -1; side <= 1; side += 2) {
1374
+ box(g, 0.04, 1.6, 0.06, MAT.panelLine, side * 0.3, 4.0, -1.14);
1375
+ box(g, 0.6, 0.04, 0.06, MAT.panelLine, side * 0.3, 4.8, -1.14);
1376
+ box(g, 0.6, 0.04, 0.06, MAT.panelLine, side * 0.3, 3.2, -1.14);
1377
+ }
1378
+
1379
+ // ── FUEL TANKS (cylindrical, flanking center) ──
1380
+ for (let side = -1; side <= 1; side += 2) {
1381
+ const ftx = side * 0.42;
1382
+ // Tank body
1383
+ cyl(g, 0.14, 0.14, 1.5, MAT.chrome, ftx, 3.9, -1.14, 16);
1384
+ // Tank end caps (hemispherical)
1385
+ sphere(g, 0.14, MAT.chrome, ftx, 4.65, -1.14);
1386
+ sphere(g, 0.14, MAT.chrome, ftx, 3.15, -1.14);
1387
+ // Tank bands (reinforcement straps)
1388
+ torus(g, 0.155, 0.015, MAT.darkChrome, ftx, 4.3, -1.14);
1389
+ torus(g, 0.155, 0.015, MAT.darkChrome, ftx, 3.9, -1.14);
1390
+ torus(g, 0.155, 0.015, MAT.darkChrome, ftx, 3.5, -1.14);
1391
+ // Fuel gauge (small window)
1392
+ box(g, 0.03, 0.2, 0.02, MAT.visor, ftx + side * 0.13, 3.9, -1.14);
1393
+ // Filler cap
1394
+ cyl(g, 0.04, 0.04, 0.03, MAT.darkChrome, ftx, 4.66, -1.14, 8);
1395
+ rivet(g, ftx, 4.68, -1.14);
1396
+ // Feed line from tank to engine
1397
+ cableRun(g, 0.016, 0.8, ftx, 3.5, -1.08);
1398
+ cableRun(g, 0.01, 0.6, ftx + side * 0.08, 3.6, -1.04);
1399
+ }
1400
+
1401
+ // ── MAIN ENGINE PODS (twin turbines) ──
1402
+ for (let side = -1; side <= 1; side += 2) {
1403
+ const px = side * 0.72;
1404
+ // Engine pylon (connecting to backpack frame)
1405
+ box(g, 0.14, 0.55, 0.22, MAT.frameMat, px, 3.95, -0.98);
1406
+ box(g, 0.18, 0.1, 0.26, MAT.darkChrome, px, 4.18, -0.98);
1407
+ boltCluster(g, 4, 0.08, px, 4.18, -0.94);
1408
+ // Gimbal mount (thrust vectoring ball joint)
1409
+ sphere(g, 0.12, MAT.joint, px, 3.58, -1.06);
1410
+ rubberSeal(g, 0.11, 0.012, px, 3.58, -1.06);
1411
+ servoDrum(g, 0.04, 0.08, px, 3.58, -0.96);
1412
+ // Fuel manifold block
1413
+ box(g, 0.08, 0.12, 0.08, MAT.hydraulic, px - side * 0.14, 3.72, -1.0);
1414
+ cableBundle(g, 2, 0.008, 0.3, px - side * 0.1, 3.72, -0.94);
1415
+
1416
+ // Outer nacelle (3-layer cowling)
1417
+ cyl(g, 0.32, 0.42, 0.95, MAT.chrome, px, 3.78, -1.16, 18);
1418
+ cyl(g, 0.28, 0.36, 0.75, MAT.darkChrome, px, 3.7, -1.16, 18);
1419
+ cyl(g, 0.24, 0.3, 0.5, MAT.armor, px, 3.68, -1.16, 16);
1420
+ // Intake grille (top of nacelle)
1421
+ torus(g, 0.34, 0.028, MAT.chrome, px, 4.22, -1.16);
1422
+ torus(g, 0.3, 0.012, MAT.vent, px, 4.2, -1.16);
1423
+ // Intake vane blades (visible fan)
1424
+ for (let v = 0; v < 6; v++) {
1425
+ const va = (v / 6) * Math.PI * 2;
1426
+ const vx = Math.cos(va) * 0.18, vy = Math.sin(va) * 0.18;
1427
+ box(g, 0.04, 0.16, 0.015, MAT.chrome, px + vx, 4.18 + vy * 0.3, -1.16, va, 0, 0);
1428
+ }
1429
+ // Combustion chamber (inner glow tube)
1430
+ cyl(g, 0.2, 0.2, 0.55, MAT.servo, px, 3.65, -1.16, 14);
1431
+ cyl(g, 0.12, 0.12, 0.4, MAT.ventHot, px, 3.6, -1.16, 12);
1432
+ // Turbine exhaust stages
1433
+ cyl(g, 0.16, 0.26, 0.2, MAT.darkChrome, px, 3.42, -1.16, 16);
1434
+ cyl(g, 0.2, 0.3, 0.15, MAT.chrome, px, 3.35, -1.16, 16);
1435
+ // Nozzle (convergent-divergent)
1436
+ cyl(g, 0.15, 0.28, 0.35, MAT.darkChrome, px, 3.22, -1.16, 16);
1437
+ // Nozzle exit ring (polished)
1438
+ torus(g, 0.3, 0.025, MAT.chrome, px, 3.08, -1.16);
437
1439
  // Exhaust glow
438
- const exhaust = cyl(g, 0.2, 0.3, 0.15, MAT.ventHot, side * 0.6, 3.32, -0.95, 12);
1440
+ const exhaust = cyl(g, 0.24, 0.34, 0.12, MAT.ventHot, px, 3.08, -1.16, 16);
439
1441
  exhaustFlames.push(exhaust);
440
- // Multi-ring nozzle
441
- const thrustRing = torus(g, 0.32, 0.03, MAT.vent, side * 0.6, 3.32, -0.95);
1442
+ // Nozzle glow rings (4 concentric)
1443
+ const thrustRing = torus(g, 0.36, 0.032, MAT.vent, px, 3.08, -1.16);
442
1444
  glowRings.push(thrustRing);
443
- const thrustRing2 = torus(g, 0.25, 0.02, MAT.panelLine, side * 0.6, 3.42, -0.95);
1445
+ const thrustRing2 = torus(g, 0.3, 0.02, MAT.panelLine, px, 3.18, -1.16);
444
1446
  glowRings.push(thrustRing2);
445
- // Fuel line
446
- cableRun(g, 0.02, 0.7, side * 0.42, 3.8, -0.82);
447
- rivet(g, side * 0.6 + side * 0.22, 4.15, -0.85);
1447
+ torus(g, 0.22, 0.014, MAT.vent, px, 3.14, -1.16);
1448
+ torus(g, 0.16, 0.01, MAT.coreInner, px, 3.1, -1.16);
1449
+
1450
+ // Engine cowling panel lines + rivets
1451
+ box(g, 0.04, 0.8, 0.04, MAT.panelLine, px + side * 0.3, 3.78, -1.16);
1452
+ box(g, 0.04, 0.8, 0.04, MAT.panelLine, px, 3.78, -1.44);
1453
+ rivet(g, px + side * 0.26, 4.12, -1.06); rivet(g, px + side * 0.26, 3.44, -1.06);
1454
+ rivet(g, px - side * 0.14, 4.12, -1.3); rivet(g, px - side * 0.14, 3.44, -1.3);
1455
+ rivet(g, px + side * 0.3, 3.78, -1.06); rivet(g, px - side * 0.14, 3.78, -1.3);
1456
+ // Cooling vents on nacelle
1457
+ ventSlits(g, 0.2, 4, 0.055, MAT.vent, px + side * 0.34, 3.85, -1.16);
1458
+ coolingFins(g, 6, 0.18, 0.04, 0.035, px - side * 0.3, 3.5, -1.16, true);
1459
+
1460
+ // Heat shimmer plume (larger, more dramatic)
1461
+ const shimGeo = new THREE.ConeGeometry(0.32, 1.6, 12, 1, true);
1462
+ const shimMat = new P({
1463
+ color: 0xff6600, emissive: 0xff4400, emissiveIntensity: 0.4,
1464
+ transparent: true, opacity: 0.045, side: THREE.DoubleSide,
1465
+ depthWrite: false, blending: THREE.AdditiveBlending,
1466
+ });
1467
+ const shim = new THREE.Mesh(shimGeo, shimMat);
1468
+ shim.position.set(px, 2.2, -1.16);
1469
+ g.add(shim);
1470
+ heatShimmerMeshes.push(shim);
1471
+ }
1472
+
1473
+ // ── WING PANELS (folded flight surfaces) ──
1474
+ for (let side = -1; side <= 1; side += 2) {
1475
+ const wx = side * 0.9;
1476
+ // Wing root hinge
1477
+ sphere(g, 0.06, MAT.joint, wx, 4.2, -1.04);
1478
+ servoDrum(g, 0.03, 0.08, wx, 4.2, -0.98);
1479
+ // Main wing panel (angled slightly outward)
1480
+ box(g, 0.7, 0.04, 0.35, MAT.armor, wx + side * 0.3, 4.2, -1.14, 0, 0, side * -0.15);
1481
+ box(g, 0.6, 0.02, 0.28, MAT.darkChrome, wx + side * 0.32, 4.2, -1.14, 0, 0, side * -0.15);
1482
+ // Aileron (trailing edge)
1483
+ box(g, 0.55, 0.025, 0.1, MAT.chrome, wx + side * 0.34, 4.2, -1.32, 0, 0, side * -0.15);
1484
+ // Wing tip light
1485
+ sphere(g, 0.02, side === -1 ? MAT.armorRed : MAT.coreInner, wx + side * 0.64, 4.2, -1.14);
1486
+ // Wing panel lines
1487
+ box(g, 0.3, 0.01, 0.03, MAT.panelLine, wx + side * 0.22, 4.22, -1.08, 0, 0, side * -0.15);
1488
+ box(g, 0.3, 0.01, 0.03, MAT.panelLine, wx + side * 0.22, 4.18, -1.22, 0, 0, side * -0.15);
1489
+ rivet(g, wx + side * 0.08, 4.22, -1.08); rivet(g, wx + side * 0.54, 4.22, -1.08);
448
1490
  }
449
1491
 
450
- // ── SHOULDER HARDPOINTS (detailed weapon mounts) ──
1492
+ // ── RCS ATTITUDE THRUSTERS (6 micro-thrusters for maneuvering) ──
1493
+ // Upper pair (yaw control)
1494
+ for (let side = -1; side <= 1; side += 2) {
1495
+ cyl(g, 0.06, 0.08, 0.12, MAT.chrome, side * 0.62, 4.9, -1.08, 10);
1496
+ cyl(g, 0.04, 0.06, 0.06, MAT.darkChrome, side * 0.62, 4.84, -1.08, 10);
1497
+ cyl(g, 0.05, 0.07, 0.04, MAT.ventHot, side * 0.62, 4.82, -1.08, 10);
1498
+ torus(g, 0.065, 0.008, MAT.vent, side * 0.62, 4.82, -1.08);
1499
+ }
1500
+ // Lower pair (pitch control)
1501
+ for (let side = -1; side <= 1; side += 2) {
1502
+ cyl(g, 0.055, 0.07, 0.1, MAT.chrome, side * 0.58, 3.1, -1.08, 10);
1503
+ cyl(g, 0.035, 0.055, 0.05, MAT.darkChrome, side * 0.58, 3.06, -1.08, 10);
1504
+ cyl(g, 0.045, 0.06, 0.035, MAT.ventHot, side * 0.58, 3.04, -1.08, 10);
1505
+ torus(g, 0.06, 0.007, MAT.vent, side * 0.58, 3.04, -1.08);
1506
+ }
1507
+ // Dorsal pair (roll control — on wing roots)
451
1508
  for (let side = -1; side <= 1; side += 2) {
452
- const sx = side * 2.0;
453
- // Mount pylon
454
- box(g, 0.12, 0.2, 0.12, MAT.frameMat, sx + side * 0.4, 5.5, -0.2);
455
- cyl(g, 0.12, 0.12, 0.7, MAT.hardpoint, sx + side * 0.4, 5.6, -0.2);
456
- box(g, 0.22, 0.22, 0.65, MAT.hardpoint, sx + side * 0.4, 5.95, -0.2);
457
- box(g, 0.16, 0.08, 0.55, MAT.darkChrome, sx + side * 0.4, 6.05, -0.2);
458
- box(g, 0.08, 0.06, 0.5, MAT.vent, sx + side * 0.4, 5.85, -0.2);
459
- rivet(g, sx + side * 0.4, 5.95, 0.1);
1509
+ cyl(g, 0.04, 0.05, 0.08, MAT.chrome, side * 0.88, 4.35, -1.3, 8);
1510
+ cyl(g, 0.03, 0.04, 0.04, MAT.ventHot, side * 0.88, 4.35, -1.34, 8);
460
1511
  }
461
1512
 
462
- // ── CHEST ENERGY VENTS (4 per side with housing) ──
1513
+ // ── BACKPACK EXTERIOR DETAIL ──
1514
+ // Hazard stripe band across top
1515
+ box(g, 1.36, 0.06, 0.06, MAT.armorRed, 0, 5.06, -1.0);
1516
+ // Warning chevrons (bottom)
1517
+ box(g, 1.16, 0.06, 0.06, MAT.armorRed, 0, 2.94, -1.0);
1518
+ // Grab handles (chrome rungs on sides)
1519
+ for (let side = -1; side <= 1; side += 2) {
1520
+ cyl(g, 0.02, 0.02, 0.22, MAT.chrome, side * 0.68, 4.4, -0.98, 6);
1521
+ cyl(g, 0.02, 0.02, 0.22, MAT.chrome, side * 0.68, 3.6, -0.98, 6);
1522
+ sphere(g, 0.025, MAT.joint, side * 0.68, 4.4, -0.88);
1523
+ sphere(g, 0.025, MAT.joint, side * 0.68, 3.6, -0.88);
1524
+ sphere(g, 0.025, MAT.joint, side * 0.68, 4.4, -1.08);
1525
+ sphere(g, 0.025, MAT.joint, side * 0.68, 3.6, -1.08);
1526
+ }
1527
+ // Central exhaust port (waste heat vent)
1528
+ cyl(g, 0.1, 0.1, 0.06, MAT.darkChrome, 0, 3.5, -1.16, 12);
1529
+ cyl(g, 0.06, 0.06, 0.04, MAT.ventHot, 0, 3.5, -1.18, 10);
1530
+ torus(g, 0.08, 0.01, MAT.vent, 0, 3.5, -1.16);
1531
+
1532
+ // ══════════════════════════════════════════════════════════
1533
+ // SHOULDER HARDPOINTS — weapon/tool mounts on arm pivots
1534
+ // ══════════════════════════════════════════════════════════
463
1535
  for (let side = -1; side <= 1; side += 2) {
464
- box(g, 0.45, 0.85, 0.06, MAT.frameMat, side * 0.55, 4.45, 0.67);
465
- for (let i = 0; i < 4; i++) {
466
- box(g, 0.35, 0.04, 0.08, MAT.vent, side * 0.55, 4.65 - i * 0.2, 0.7);
1536
+ const tgt = side === -1 ? la : ra;
1537
+ const sx = side * 2.1;
1538
+ const hx = sx + side * 0.45;
1539
+ // Mounting bracket (bolted to pauldron)
1540
+ box(tgt, 0.16, 0.24, 0.16, MAT.frameMat, hx, 5.52, -0.22);
1541
+ boltCluster(tgt, 4, 0.08, hx, 5.52, -0.14);
1542
+ // Hardpoint pylon (chrome cylinder)
1543
+ cyl(tgt, 0.15, 0.15, 0.8, MAT.hardpoint, hx, 5.68, -0.22);
1544
+ // Servo rotation drum at base
1545
+ servoDrum(tgt, 0.06, 0.1, hx, 5.52, -0.28);
1546
+ // Weapon rail housing (layered)
1547
+ box(tgt, 0.26, 0.26, 0.74, MAT.hardpoint, hx, 6.02, -0.22);
1548
+ box(tgt, 0.2, 0.12, 0.66, MAT.darkChrome, hx, 6.12, -0.22);
1549
+ box(tgt, 0.12, 0.08, 0.58, MAT.vent, hx, 5.92, -0.22);
1550
+ // Rail guides (top grooves for tool attachment)
1551
+ for (let r = -1; r <= 1; r += 2) {
1552
+ box(tgt, 0.02, 0.2, 0.6, MAT.panelLine, hx + r * 0.1, 6.14, -0.22);
467
1553
  }
1554
+ // Status indicator
1555
+ sphere(tgt, 0.015, MAT.coreInner, hx, 6.16, 0.1);
1556
+ // Hardpoint cable feed
1557
+ cableBundle(tgt, 2, 0.008, 0.4, hx - side * 0.08, 5.72, -0.12);
1558
+ rivet(tgt, hx, 6.0, 0.14); rivet(tgt, hx, 6.0, -0.56);
468
1559
  }
469
1560
 
470
- // Add subtle edge highlights so the full mecha silhouette is always readable.
1561
+ // ══════════════════════════════════════════════════════════
1562
+ // CHEST ENERGY VENTS — intake/exhaust grilles with glow
1563
+ // ══════════════════════════════════════════════════════════
1564
+ for (let side = -1; side <= 1; side += 2) {
1565
+ // Vent housing recess
1566
+ box(g, 0.48, 0.9, 0.06, MAT.frameMat, side * 0.55, 4.45, 0.67);
1567
+ box(g, 0.42, 0.82, 0.03, MAT.servo, side * 0.55, 4.45, 0.68);
1568
+ // Vent slats (6 per side with inner glow backing)
1569
+ for (let i = 0; i < 6; i++) {
1570
+ box(g, 0.36, 0.035, 0.07, MAT.vent, side * 0.55, 4.72 - i * 0.14, 0.7);
1571
+ box(g, 0.3, 0.015, 0.04, MAT.ventHot, side * 0.55, 4.72 - i * 0.14, 0.68);
1572
+ }
1573
+ // Vent frame bolts
1574
+ rivet(g, side * 0.72, 4.82, 0.68); rivet(g, side * 0.72, 4.08, 0.68);
1575
+ rivet(g, side * 0.38, 4.82, 0.68); rivet(g, side * 0.38, 4.08, 0.68);
1576
+ }
1577
+
1578
+ // ══════════════════════════════════════════════════════════
1579
+ // JETPACK INTERNAL SYSTEMS — reactor, flight computer, power bus
1580
+ // ══════════════════════════════════════════════════════════
1581
+ // Central reactor housing (visible through backpack center)
1582
+ box(g, 0.54, 0.64, 0.24, MAT.darkChrome, 0, 3.9, -1.0);
1583
+ box(g, 0.44, 0.54, 0.16, MAT.servo, 0, 3.9, -1.0);
1584
+ // Reactor glow core (larger, multi-ring)
1585
+ sphere(g, 0.1, MAT.coreInner, 0, 3.9, -1.06);
1586
+ torus(g, 0.14, 0.018, MAT.vent, 0, 3.9, -1.06);
1587
+ torus(g, 0.1, 0.01, MAT.coreInner, 0, 3.9, -1.08);
1588
+ // Reactor coolant manifold (feeds engines)
1589
+ for (let side = -1; side <= 1; side += 2) {
1590
+ cyl(g, 0.035, 0.035, 0.2, MAT.hydraulic, side * 0.2, 3.9, -1.1, 8);
1591
+ cableRun(g, 0.012, 0.6, side * 0.24, 3.9, -1.06);
1592
+ // Coolant to engine feed
1593
+ cableRun(g, 0.014, 0.4, side * 0.36, 3.9, -1.06);
1594
+ }
1595
+ boltCluster(g, 6, 0.2, 0, 3.9, -0.94);
1596
+
1597
+ // Flight computer module (upper backpack)
1598
+ box(g, 0.4, 0.3, 0.18, MAT.darkChrome, 0, 4.72, -1.0);
1599
+ box(g, 0.32, 0.22, 0.12, MAT.servo, 0, 4.72, -1.0);
1600
+ // Data port array (6 connectors)
1601
+ for (let i = 0; i < 6; i++) {
1602
+ const dx = -0.14 + i * 0.056;
1603
+ cyl(g, 0.022, 0.022, 0.04, MAT.frameMat, dx, 4.72, -1.1, 6);
1604
+ sphere(g, 0.009, MAT.vent, dx, 4.72, -1.12);
1605
+ }
1606
+ // Status LEDs
1607
+ sphere(g, 0.012, MAT.coreInner, 0.14, 4.84, -1.0);
1608
+ sphere(g, 0.012, MAT.ventHot, -0.14, 4.84, -1.0);
1609
+ sphere(g, 0.01, MAT.vent, 0, 4.84, -1.0);
1610
+
1611
+ // Dorsal stabilizer fin (tall, between shoulders)
1612
+ box(g, 0.07, 0.6, 0.28, MAT.chrome, 0, 5.4, -0.88);
1613
+ box(g, 0.04, 0.5, 0.22, MAT.panelLine, 0, 5.4, -0.92);
1614
+ box(g, 0.04, 0.35, 0.14, MAT.darkChrome, 0, 5.5, -0.96);
1615
+ // Fin tip beacon
1616
+ sphere(g, 0.022, MAT.coreInner, 0, 5.68, -0.88);
1617
+ // Fin base bolts
1618
+ rivet(g, 0.03, 5.12, -0.86); rivet(g, -0.03, 5.12, -0.86);
1619
+
1620
+ // Power distribution bus (horizontal bar, lower backpack)
1621
+ box(g, 0.9, 0.08, 0.1, MAT.frameMat, 0, 3.2, -1.04);
1622
+ for (let i = 0; i < 5; i++) {
1623
+ const bx = -0.3 + i * 0.15;
1624
+ cyl(g, 0.018, 0.018, 0.06, MAT.hydraulic, bx, 3.2, -1.1, 6);
1625
+ sphere(g, 0.008, MAT.vent, bx, 3.2, -1.14);
1626
+ }
1627
+
1628
+ // Lumbar support braces (lower back reinforcement, connecting to jetpack)
1629
+ for (let side = -1; side <= 1; side += 2) {
1630
+ box(g, 0.16, 0.85, 0.14, MAT.frameMat, side * 0.36, 2.9, -0.76);
1631
+ hydraulicRam(g, 0.022, 0.65, side * 0.36, 2.9, -0.7);
1632
+ box(g, 0.06, 0.6, 0.06, MAT.panelLine, side * 0.36, 2.9, -0.82);
1633
+ rivet(g, side * 0.36, 3.28, -0.72); rivet(g, side * 0.36, 2.52, -0.72);
1634
+ }
1635
+
1636
+ // ── Attach pivot groups and store refs ──
1637
+ g.add(_hp); g.add(_lp); g.add(_rp);
1638
+ headPivotRef = _hp; leftArmPivotRef = _lp; rightArmPivotRef = _rp;
1639
+
1640
+ // Edge highlights on major armor pieces only (skip rivets, cables, small detail).
471
1641
  const edgeMat = new THREE.LineBasicMaterial({
472
- color: 0x8fb6ff,
1642
+ color: 0x3355aa,
473
1643
  transparent: true,
474
- opacity: 0.22,
1644
+ opacity: 0.15,
475
1645
  blending: THREE.AdditiveBlending,
476
1646
  depthWrite: false,
477
1647
  });
1648
+ const _edgeCache = new Map();
478
1649
  g.traverse((node) => {
479
1650
  if (!node.isMesh || !node.geometry) return;
480
- const edges = new THREE.EdgesGeometry(node.geometry, 28);
481
- const lines = new THREE.LineSegments(edges, edgeMat.clone());
1651
+ if (node.userData._detail) return;
1652
+ const s = node.geometry.boundingSphere;
1653
+ if (!s) node.geometry.computeBoundingSphere();
1654
+ if (node.geometry.boundingSphere && node.geometry.boundingSphere.radius < 0.04) return;
1655
+ const geoId = node.geometry.uuid;
1656
+ let edgeGeo = _edgeCache.get(geoId);
1657
+ if (!edgeGeo) { edgeGeo = new THREE.EdgesGeometry(node.geometry, 28); _edgeCache.set(geoId, edgeGeo); }
1658
+ const lines = new THREE.LineSegments(edgeGeo, edgeMat);
482
1659
  lines.renderOrder = 2;
483
1660
  node.add(lines);
484
1661
  });
@@ -508,63 +1685,136 @@ function buildChassis() {
508
1685
  function buildPlatform() {
509
1686
  const platform = new THREE.Group();
510
1687
 
511
- // Main pad (dark hexagonal)
512
- const padGeo = new THREE.CylinderGeometry(5, 5, 0.12, 6);
513
- const padMat = new M({ color: 0x080808, roughness: 0.9, metalness: 0.2 });
1688
+ // ── BASE STRUCTURE (multi-layer industrial hex pad) ──
1689
+ // Lower foundation ring (heavy, dark)
1690
+ const foundGeo = new THREE.CylinderGeometry(5.6, 5.8, 0.18, 6);
1691
+ const foundMat = new P({ color: 0x040408, roughness: 0.7, metalness: 0.6, clearcoat: 0.2 });
1692
+ const foundation = new THREE.Mesh(foundGeo, foundMat);
1693
+ foundation.position.y = -2.9;
1694
+ platform.add(foundation);
1695
+
1696
+ // Main platform (polished dark surface)
1697
+ const padGeo = new THREE.CylinderGeometry(5.2, 5.2, 0.14, 6);
1698
+ const padMat = new P({ color: 0x0a0a10, roughness: 0.25, metalness: 0.7, clearcoat: 0.6, clearcoatRoughness: 0.15 });
514
1699
  const pad = new THREE.Mesh(padGeo, padMat);
515
- pad.position.y = -2.7;
1700
+ pad.position.y = -2.72;
516
1701
  platform.add(pad);
517
1702
 
518
- // Glowing concentric hex rings
519
- for (let i = 1; i <= 5; i++) {
520
- const ringGeo = new THREE.RingGeometry(i * 0.9 - 0.02, i * 0.9 + 0.02, 6);
1703
+ // Upper deck (thin chrome trim ring)
1704
+ const deckGeo = new THREE.CylinderGeometry(4.8, 4.9, 0.04, 6);
1705
+ const deckMat = new P({ color: 0x1a1a24, roughness: 0.15, metalness: 0.85, clearcoat: 0.8 });
1706
+ const deck = new THREE.Mesh(deckGeo, deckMat);
1707
+ deck.position.y = -2.64;
1708
+ platform.add(deck);
1709
+
1710
+ // ── CONCENTRIC HEX RINGS (glowing, animated) ──
1711
+ for (let i = 1; i <= 6; i++) {
1712
+ const r = i * 0.82;
1713
+ const ringGeo = new THREE.RingGeometry(r - 0.015, r + 0.015, 6);
1714
+ const isPrimary = i === 3 || i === 5;
521
1715
  const ringMat = new THREE.MeshBasicMaterial({
522
- color: GLOW, transparent: true, opacity: 0.06 + (i === 3 ? 0.06 : 0),
523
- side: THREE.DoubleSide, blending: THREE.AdditiveBlending,
1716
+ color: isPrimary ? CYAN : GLOW,
1717
+ transparent: true,
1718
+ opacity: isPrimary ? 0.12 : 0.05,
1719
+ side: THREE.DoubleSide,
1720
+ blending: THREE.AdditiveBlending,
524
1721
  });
525
1722
  const ring = new THREE.Mesh(ringGeo, ringMat);
526
1723
  ring.rotation.x = -Math.PI / 2;
527
- ring.position.y = -2.63;
1724
+ ring.position.y = -2.64;
528
1725
  ring.userData.ringIndex = i;
529
1726
  platform.add(ring);
530
1727
  }
531
1728
 
532
- // Hex edge glow lines
533
- for (let i = 0; i < 6; i++) {
534
- const a = (i / 6) * Math.PI * 2;
535
- const b = ((i + 1) / 6) * Math.PI * 2;
536
- const r = 5;
537
- const pts = [
538
- new THREE.Vector3(Math.cos(a) * r, -2.64, Math.sin(a) * r),
539
- new THREE.Vector3(Math.cos(b) * r, -2.64, Math.sin(b) * r),
540
- ];
541
- const edgeGeo = new THREE.BufferGeometry().setFromPoints(pts);
542
- const edgeMat = new THREE.LineBasicMaterial({
543
- color: GLOW, transparent: true, opacity: 0.3,
544
- blending: THREE.AdditiveBlending,
545
- });
546
- platform.add(new THREE.Line(edgeGeo, edgeMat));
1729
+ // ── HEX EDGE GLOW LINES (outer boundary, double-width for primary) ──
1730
+ for (let layer = 0; layer < 2; layer++) {
1731
+ const r = layer === 0 ? 5.2 : 4.0;
1732
+ const opacity = layer === 0 ? 0.35 : 0.12;
1733
+ for (let i = 0; i < 6; i++) {
1734
+ const a = (i / 6) * Math.PI * 2;
1735
+ const b = ((i + 1) / 6) * Math.PI * 2;
1736
+ const pts = [
1737
+ new THREE.Vector3(Math.cos(a) * r, -2.65, Math.sin(a) * r),
1738
+ new THREE.Vector3(Math.cos(b) * r, -2.65, Math.sin(b) * r),
1739
+ ];
1740
+ const geo = new THREE.BufferGeometry().setFromPoints(pts);
1741
+ const mat = new THREE.LineBasicMaterial({
1742
+ color: layer === 0 ? GLOW : CYAN,
1743
+ transparent: true, opacity,
1744
+ blending: THREE.AdditiveBlending,
1745
+ });
1746
+ platform.add(new THREE.Line(geo, mat));
1747
+ }
547
1748
  }
548
1749
 
549
- // Radial spokes
550
- for (let i = 0; i < 12; i++) {
551
- const a = (i / 12) * Math.PI * 2;
1750
+ // ── RADIAL SPOKES (24 fine lines + 6 thick primary) ──
1751
+ for (let i = 0; i < 24; i++) {
1752
+ const a = (i / 24) * Math.PI * 2;
1753
+ const isPrimary = i % 4 === 0;
1754
+ const len = isPrimary ? 5.0 : 4.2;
552
1755
  const pts = [
553
- new THREE.Vector3(0, -2.64, 0),
554
- new THREE.Vector3(Math.cos(a) * 4.5, -2.64, Math.sin(a) * 4.5),
1756
+ new THREE.Vector3(Math.cos(a) * 0.5, -2.65, Math.sin(a) * 0.5),
1757
+ new THREE.Vector3(Math.cos(a) * len, -2.65, Math.sin(a) * len),
555
1758
  ];
556
1759
  const geo = new THREE.BufferGeometry().setFromPoints(pts);
557
1760
  const mat = new THREE.LineBasicMaterial({
558
- color: GLOW, transparent: true, opacity: 0.04,
1761
+ color: isPrimary ? CYAN : GLOW,
1762
+ transparent: true, opacity: isPrimary ? 0.08 : 0.03,
559
1763
  blending: THREE.AdditiveBlending,
560
1764
  });
561
1765
  platform.add(new THREE.Line(geo, mat));
562
1766
  }
563
1767
 
564
- // Holographic scanline cylinder
565
- const scanGeo = new THREE.CylinderGeometry(4.8, 4.8, 0.02, 64, 1, true);
1768
+ // ── CORNER PYLONS (6 hex vertices, industrial bollards) ──
1769
+ for (let i = 0; i < 6; i++) {
1770
+ const a = (i / 6) * Math.PI * 2;
1771
+ const px = Math.cos(a) * 5.0, pz = Math.sin(a) * 5.0;
1772
+ // Pylon body
1773
+ const pylonGeo = new THREE.CylinderGeometry(0.12, 0.14, 0.6, 8);
1774
+ const pylon = new THREE.Mesh(pylonGeo, new P({ color: 0x101018, roughness: 0.3, metalness: 0.8, clearcoat: 0.4 }));
1775
+ pylon.position.set(px, -2.5, pz);
1776
+ platform.add(pylon);
1777
+ // Pylon top cap (chrome)
1778
+ const capGeo = new THREE.CylinderGeometry(0.15, 0.12, 0.06, 8);
1779
+ const cap = new THREE.Mesh(capGeo, new P({ color: 0x2a2a34, roughness: 0.12, metalness: 0.9 }));
1780
+ cap.position.set(px, -2.22, pz);
1781
+ platform.add(cap);
1782
+ // Pylon glow ring
1783
+ const glowRing = new THREE.Mesh(
1784
+ new THREE.TorusGeometry(0.14, 0.012, 6, 16),
1785
+ new THREE.MeshBasicMaterial({ color: GLOW, transparent: true, opacity: 0.2, blending: THREE.AdditiveBlending })
1786
+ );
1787
+ glowRing.position.set(px, -2.35, pz);
1788
+ glowRing.rotation.x = Math.PI / 2;
1789
+ glowRing.userData.pylonGlow = true;
1790
+ platform.add(glowRing);
1791
+ // Pylon status LED
1792
+ const led = new THREE.Mesh(
1793
+ new THREE.SphereGeometry(0.03, 6, 4),
1794
+ new THREE.MeshBasicMaterial({ color: CYAN, transparent: true, opacity: 0.6, blending: THREE.AdditiveBlending })
1795
+ );
1796
+ led.position.set(px, -2.2, pz);
1797
+ led.userData.pylonLed = i;
1798
+ platform.add(led);
1799
+ }
1800
+
1801
+ // ── HOLOGRAPHIC GRID (subtle floor markings inside hex) ──
1802
+ const gridMat = new THREE.LineBasicMaterial({
1803
+ color: GLOW, transparent: true, opacity: 0.02, blending: THREE.AdditiveBlending,
1804
+ });
1805
+ for (let gx = -4; gx <= 4; gx++) {
1806
+ const pts = [new THREE.Vector3(gx, -2.64, -4.5), new THREE.Vector3(gx, -2.64, 4.5)];
1807
+ platform.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), gridMat));
1808
+ }
1809
+ for (let gz = -4; gz <= 4; gz++) {
1810
+ const pts = [new THREE.Vector3(-4.5, -2.64, gz), new THREE.Vector3(4.5, -2.64, gz)];
1811
+ platform.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), gridMat));
1812
+ }
1813
+
1814
+ // ── HOLOGRAPHIC SCANLINE CYLINDER ──
1815
+ const scanGeo = new THREE.CylinderGeometry(4.8, 4.8, 0.02, 48, 1, true);
566
1816
  const scanMat = new THREE.MeshBasicMaterial({
567
- color: GLOW, transparent: true, opacity: 0.03,
1817
+ color: CYAN, transparent: true, opacity: 0.04,
568
1818
  side: THREE.DoubleSide, blending: THREE.AdditiveBlending,
569
1819
  });
570
1820
  const scan = new THREE.Mesh(scanGeo, scanMat);
@@ -572,6 +1822,39 @@ function buildPlatform() {
572
1822
  scan.userData.isScanline = true;
573
1823
  platform.add(scan);
574
1824
 
1825
+ // ── CENTRAL GLOW DISC (under the robot — volumetric uplight) ──
1826
+ const uplightGeo = new THREE.CylinderGeometry(1.8, 2.2, 0.01, 32);
1827
+ const uplightMat = new THREE.MeshBasicMaterial({
1828
+ color: GLOW, transparent: true, opacity: 0.06,
1829
+ blending: THREE.AdditiveBlending, depthWrite: false,
1830
+ });
1831
+ const uplight = new THREE.Mesh(uplightGeo, uplightMat);
1832
+ uplight.position.y = -2.63;
1833
+ uplight.userData.uplight = true;
1834
+ platform.add(uplight);
1835
+
1836
+ // ── HOLOGRAPHIC FIELD BOUNDARY (vertical light curtain at hex edges) ──
1837
+ for (let i = 0; i < 6; i++) {
1838
+ const a = (i / 6) * Math.PI * 2;
1839
+ const b = ((i + 1) / 6) * Math.PI * 2;
1840
+ const r = 5.15;
1841
+ const curtainGeo = new THREE.PlaneGeometry(
1842
+ Math.sqrt(Math.pow(Math.cos(b) * r - Math.cos(a) * r, 2) + Math.pow(Math.sin(b) * r - Math.sin(a) * r, 2)),
1843
+ 3.0
1844
+ );
1845
+ const curtainMat = new THREE.MeshBasicMaterial({
1846
+ color: GLOW, transparent: true, opacity: 0.008,
1847
+ side: THREE.DoubleSide, blending: THREE.AdditiveBlending, depthWrite: false,
1848
+ });
1849
+ const curtain = new THREE.Mesh(curtainGeo, curtainMat);
1850
+ const mx = (Math.cos(a) + Math.cos(b)) / 2 * r;
1851
+ const mz = (Math.sin(a) + Math.sin(b)) / 2 * r;
1852
+ curtain.position.set(mx, -1.2, mz);
1853
+ curtain.lookAt(0, -1.2, 0);
1854
+ curtain.userData.curtain = true;
1855
+ platform.add(curtain);
1856
+ }
1857
+
575
1858
  return platform;
576
1859
  }
577
1860
 
@@ -579,44 +1862,81 @@ function buildPlatform() {
579
1862
  Ambient particles + energy streams
580
1863
  ══════════════════════════════════════════════════════════ */
581
1864
  function createAmbientParticles() {
582
- const count = 300;
1865
+ const count = 800;
583
1866
  const positions = new Float32Array(count * 3);
584
- const sizes = new Float32Array(count);
1867
+ const colors = new Float32Array(count * 3);
1868
+ const palette = [
1869
+ new THREE.Color(GLOW),
1870
+ new THREE.Color(CYAN),
1871
+ new THREE.Color(0x8fbfff),
1872
+ new THREE.Color(0xb026ff),
1873
+ new THREE.Color(0x6699cc),
1874
+ ];
585
1875
  for (let i = 0; i < count; i++) {
586
- positions[i * 3] = (Math.random() - 0.5) * 20;
587
- positions[i * 3 + 1] = Math.random() * 16 - 3;
588
- positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
589
- sizes[i] = 0.02 + Math.random() * 0.06;
1876
+ const isNear = i < count * 0.3;
1877
+ const spread = isNear ? 8 : 28;
1878
+ positions[i * 3] = (Math.random() - 0.5) * spread;
1879
+ positions[i * 3 + 1] = Math.random() * 20 - 4;
1880
+ positions[i * 3 + 2] = (Math.random() - 0.5) * spread;
1881
+ const c = palette[Math.floor(Math.random() * palette.length)];
1882
+ colors[i * 3] = c.r; colors[i * 3 + 1] = c.g; colors[i * 3 + 2] = c.b;
590
1883
  }
591
1884
  const geo = new THREE.BufferGeometry();
592
1885
  geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
1886
+ geo.setAttribute("color", new THREE.BufferAttribute(colors, 3));
593
1887
  return new THREE.Points(geo, new THREE.PointsMaterial({
594
- color: GLOW, size: 0.05, transparent: true, opacity: 0.25,
595
- blending: THREE.AdditiveBlending, depthWrite: false,
1888
+ size: 0.04, transparent: true, opacity: 0.35, vertexColors: true,
1889
+ blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true,
596
1890
  }));
597
1891
  }
598
1892
 
599
1893
  function createEnergyStreams() {
600
1894
  const group = new THREE.Group();
1895
+ const streamColors = [GLOW, CYAN, 0x8fbfff, GLOW, GLOW, CYAN, 0xb026ff, GLOW, CYAN, GLOW, GLOW, 0x8fbfff, 0x6699cc, CYAN, GLOW, 0xb026ff];
1896
+ // Rising helical streams (16 total, denser)
1897
+ for (let i = 0; i < 16; i++) {
1898
+ const pts = [];
1899
+ const angle = (i / 16) * Math.PI * 2;
1900
+ const r = 1.8 + Math.random() * 2.5;
1901
+ const twist = 0.4 + Math.random() * 0.5;
1902
+ const segments = 36;
1903
+ for (let j = 0; j <= segments; j++) {
1904
+ const t = j / segments;
1905
+ pts.push(new THREE.Vector3(
1906
+ Math.cos(angle + t * Math.PI * twist) * r * (1 - t * 0.4),
1907
+ -2.8 + t * 16,
1908
+ Math.sin(angle + t * Math.PI * twist) * r * (1 - t * 0.4),
1909
+ ));
1910
+ }
1911
+ const geo = new THREE.BufferGeometry().setFromPoints(pts);
1912
+ const mat = new THREE.LineBasicMaterial({
1913
+ color: streamColors[i % streamColors.length], transparent: true, opacity: 0.035,
1914
+ blending: THREE.AdditiveBlending,
1915
+ });
1916
+ const line = new THREE.Line(geo, mat);
1917
+ line.userData.streamAngle = angle;
1918
+ group.add(line);
1919
+ }
1920
+ // Descending data cascade (8 vertical streams falling through platform)
601
1921
  for (let i = 0; i < 8; i++) {
602
1922
  const pts = [];
603
- const angle = (i / 8) * Math.PI * 2;
604
- const r = 2.5 + Math.random() * 1.5;
1923
+ const angle = (i / 8) * Math.PI * 2 + 0.3;
1924
+ const r = 3.2 + Math.random() * 1.5;
605
1925
  for (let j = 0; j <= 20; j++) {
606
1926
  const t = j / 20;
607
1927
  pts.push(new THREE.Vector3(
608
- Math.cos(angle + t * Math.PI * 0.5) * r * (1 - t * 0.3),
609
- -2.5 + t * 12,
610
- Math.sin(angle + t * Math.PI * 0.5) * r * (1 - t * 0.3),
1928
+ Math.cos(angle) * r + Math.sin(t * 6) * 0.15,
1929
+ 12 - t * 16,
1930
+ Math.sin(angle) * r + Math.cos(t * 6) * 0.15,
611
1931
  ));
612
1932
  }
613
1933
  const geo = new THREE.BufferGeometry().setFromPoints(pts);
614
1934
  const mat = new THREE.LineBasicMaterial({
615
- color: GLOW, transparent: true, opacity: 0.04,
1935
+ color: 0x2244aa, transparent: true, opacity: 0.02,
616
1936
  blending: THREE.AdditiveBlending,
617
1937
  });
618
1938
  const line = new THREE.Line(geo, mat);
619
- line.userData.streamAngle = angle;
1939
+ line.userData.cascade = true;
620
1940
  group.add(line);
621
1941
  }
622
1942
  return group;
@@ -624,11 +1944,15 @@ function createEnergyStreams() {
624
1944
 
625
1945
  function updateAmbientParticles(pts, time) {
626
1946
  const pos = pts.geometry.attributes.position.array;
627
- for (let i = 0; i < pos.length; i += 3) {
628
- pos[i + 1] += 0.004;
629
- pos[i] += Math.sin(time * 0.7 + i) * 0.0015;
630
- pos[i + 2] += Math.cos(time * 0.5 + i) * 0.001;
631
- if (pos[i + 1] > 12) pos[i + 1] = -3;
1947
+ const count = pos.length / 3;
1948
+ const nearCount = Math.floor(count * 0.3);
1949
+ for (let i = 0; i < count; i++) {
1950
+ const idx = i * 3;
1951
+ const speed = i < nearCount ? 0.006 : 0.003;
1952
+ pos[idx + 1] += speed;
1953
+ pos[idx] += Math.sin(time * 0.7 + i * 0.3) * 0.001;
1954
+ pos[idx + 2] += Math.cos(time * 0.5 + i * 0.2) * 0.0008;
1955
+ if (pos[idx + 1] > 14) pos[idx + 1] = -4;
632
1956
  }
633
1957
  pts.geometry.attributes.position.needsUpdate = true;
634
1958
  }
@@ -641,11 +1965,23 @@ function updatePlatformRings(time) {
641
1965
  platformGroup.children.forEach(child => {
642
1966
  if (child.userData.ringIndex) {
643
1967
  const i = child.userData.ringIndex;
644
- child.material.opacity = 0.03 + 0.1 * Math.max(0, Math.sin(time * 1.5 - i * 0.6));
1968
+ child.material.opacity = 0.03 + 0.12 * Math.max(0, Math.sin(time * 1.5 - i * 0.55));
645
1969
  }
646
1970
  if (child.userData.isScanline) {
647
- child.position.y = -2.5 + Math.sin(time * 0.8) * 6 + 6;
648
- child.material.opacity = 0.02 + 0.02 * Math.sin(time * 2);
1971
+ child.position.y = -2.5 + Math.sin(time * 0.6) * 7 + 7;
1972
+ child.material.opacity = 0.025 + 0.025 * Math.sin(time * 2.2);
1973
+ }
1974
+ if (child.userData.pylonGlow) {
1975
+ child.material.opacity = 0.12 + 0.12 * Math.sin(time * 2.0 + child.position.x * 2);
1976
+ }
1977
+ if (child.userData.pylonLed !== undefined) {
1978
+ child.material.opacity = 0.3 + 0.4 * Math.max(0, Math.sin(time * 3 + child.userData.pylonLed * 1.05));
1979
+ }
1980
+ if (child.userData.uplight) {
1981
+ child.material.opacity = 0.04 + 0.03 * Math.sin(time * 1.2);
1982
+ }
1983
+ if (child.userData.curtain) {
1984
+ child.material.opacity = 0.005 + 0.005 * Math.sin(time * 0.8 + child.position.x);
649
1985
  }
650
1986
  });
651
1987
  }
@@ -676,7 +2012,9 @@ function playCinematicIntro(onComplete) {
676
2012
  const startTarget = new THREE.Vector3(0, 1, 0);
677
2013
  const endTarget = new THREE.Vector3(0, 2.5, 0);
678
2014
  controls.enabled = false;
679
- scene.fog = new THREE.FogExp2(0x0a0a0a, 0.06);
2015
+ const AMBIENT_FOG_DENSITY = scene.fog ? scene.fog.density : 0.018;
2016
+ if (scene.fog) scene.fog.density = 0.07;
2017
+ else scene.fog = new THREE.FogExp2(0x020206, 0.07);
680
2018
  camera.position.copy(startPos);
681
2019
  controls.target.copy(startTarget);
682
2020
 
@@ -685,7 +2023,7 @@ function playCinematicIntro(onComplete) {
685
2023
 
686
2024
  function tick() {
687
2025
  if (introSkipped) {
688
- scene.fog = null;
2026
+ if (scene.fog) scene.fog.density = AMBIENT_FOG_DENSITY;
689
2027
  camera.position.copy(endPos);
690
2028
  controls.target.copy(endTarget);
691
2029
  controls.enabled = true;
@@ -702,11 +2040,11 @@ function playCinematicIntro(onComplete) {
702
2040
  camera.position.lerpVectors(startPos, endPos, eased);
703
2041
  controls.target.lerpVectors(startTarget, endTarget, eased);
704
2042
  const fogT = Math.min(elapsed / 2500, 1);
705
- scene.fog.density = 0.06 * (1 - fogT);
2043
+ if (scene.fog) scene.fog.density = 0.07 + (AMBIENT_FOG_DENSITY - 0.07) * fogT;
706
2044
  if (t < 1) {
707
2045
  requestAnimationFrame(tick);
708
2046
  } else {
709
- scene.fog = null;
2047
+ if (scene.fog) scene.fog.density = AMBIENT_FOG_DENSITY;
710
2048
  controls.enabled = true;
711
2049
  introPlaying = false;
712
2050
  sessionStorage.setItem("forge_intro_done", "1");
@@ -815,7 +2153,23 @@ function findNearestAttachment(ndc) {
815
2153
  function onCanvasClick(e) {
816
2154
  const ndc = screenToNDC(e);
817
2155
  const target = findNearestAttachment(ndc);
818
- selectAttachment(target);
2156
+ if (target) {
2157
+ selectAttachment(target);
2158
+ return;
2159
+ }
2160
+ // Check if click hit the robot body
2161
+ if (robotGroup) {
2162
+ mouse.copy(ndc);
2163
+ raycaster.setFromCamera(mouse, camera);
2164
+ const bodyMeshes = [];
2165
+ robotGroup.traverse(c => { if (c.isMesh) bodyMeshes.push(c); });
2166
+ const hits = raycaster.intersectObjects(bodyMeshes, false);
2167
+ if (hits.length > 0) {
2168
+ toggleRobotPower();
2169
+ return;
2170
+ }
2171
+ }
2172
+ selectAttachment(null);
819
2173
  }
820
2174
 
821
2175
  function onCanvasHover(e) {
@@ -886,7 +2240,7 @@ function initHUD() {
886
2240
  });
887
2241
  document.getElementById("forgeToggleBloom")?.addEventListener("click", (e) => {
888
2242
  bloomEnabled = !bloomEnabled;
889
- bloomPass.strength = bloomEnabled ? 1.2 : 0;
2243
+ bloomPass.strength = bloomEnabled ? 0.55 : 0;
890
2244
  e.currentTarget.classList.toggle("active", bloomEnabled);
891
2245
  });
892
2246
  }
@@ -954,47 +2308,107 @@ export function captureScreenshot(agentName, skillCount, onchainCount, categoryC
954
2308
  ══════════════════════════════════════════════════════════ */
955
2309
  let idleAnimatorFn = null;
956
2310
  export function setIdleAnimator(fn) { idleAnimatorFn = fn; }
2311
+ export function setAgentSpeaking(v) {
2312
+ agentSpeaking = !!v;
2313
+ if (agentSpeaking) {
2314
+ powerOn();
2315
+ } else {
2316
+ schedulePowerDown();
2317
+ }
2318
+ }
2319
+
2320
+ function powerOn() {
2321
+ if (powerDownTimer) { clearTimeout(powerDownTimer); powerDownTimer = null; }
2322
+ robotPowered = true;
2323
+ }
2324
+
2325
+ function schedulePowerDown() {
2326
+ if (powerDownTimer) clearTimeout(powerDownTimer);
2327
+ powerDownTimer = setTimeout(() => {
2328
+ robotPowered = false;
2329
+ powerDownTimer = null;
2330
+ }, POWER_DOWN_DELAY);
2331
+ }
2332
+
2333
+ export function toggleRobotPower() {
2334
+ if (robotPowered) {
2335
+ robotPowered = false;
2336
+ if (powerDownTimer) { clearTimeout(powerDownTimer); powerDownTimer = null; }
2337
+ } else {
2338
+ powerOn();
2339
+ schedulePowerDown();
2340
+ }
2341
+ }
957
2342
 
958
2343
  function animate() {
959
2344
  requestAnimationFrame(animate);
960
2345
  if (renderPaused) return;
961
2346
  const time = performance.now() * 0.001;
2347
+ const dt = 1 / 60;
962
2348
 
963
2349
  controls.update();
964
2350
 
2351
+ // ── Power level ramp (smooth on/off transition) ──
2352
+ const targetPower = robotPowered ? 1.0 : 0.0;
2353
+ if (powerLevel < targetPower) {
2354
+ powerLevel = Math.min(powerLevel + dt * POWER_UP_SPEED, 1.0);
2355
+ } else if (powerLevel > targetPower) {
2356
+ powerLevel = Math.max(powerLevel - dt * POWER_DOWN_SPEED, 0.0);
2357
+ }
2358
+ const pw = powerLevel;
2359
+
965
2360
  // Core reactor pulse
966
2361
  if (coreMesh) {
967
- coreMesh.material.emissiveIntensity = 1.55 + 0.45 * Math.sin(time * 2.5);
968
- coreMesh.rotation.y += 0.02;
969
- coreMesh.rotation.x = Math.sin(time * 1.5) * 0.2;
2362
+ coreMesh.material.emissiveIntensity = (1.55 + 0.45 * Math.sin(time * 2.5)) * pw;
2363
+ coreMesh.rotation.y += 0.02 * pw;
2364
+ coreMesh.rotation.x = Math.sin(time * 1.5) * 0.2 * pw;
970
2365
  }
971
2366
 
972
2367
  // Spine glow cascade
973
2368
  spineSegments.forEach((seg, i) => {
974
- seg.material.emissiveIntensity = 1.0 + 1.5 * Math.max(0, Math.sin(time * 3.5 - i * 0.5));
2369
+ seg.material.emissiveIntensity = (1.0 + 1.5 * Math.max(0, Math.sin(time * 3.5 - i * 0.5))) * pw;
975
2370
  });
976
2371
 
977
- // Visor flicker
2372
+ // Visor — off when powered down, full optical system when powered up
978
2373
  if (visorMesh) {
979
- visorMesh.material.opacity = 0.42 + 0.06 * Math.sin(time * 4) + Math.random() * 0.02;
980
- visorMesh.material.emissiveIntensity = 1.8 + 0.4 * Math.sin(time * 3);
2374
+ const sp = agentSpeaking ? 1.6 : 1.0;
2375
+ const vBreath = Math.sin(time * 1.8) * 0.02;
2376
+ const vNoise = (Math.random() - 0.5) * 0.015;
2377
+ const scanPhase = (time * 0.7) % 1.0;
2378
+ const scanPulse = Math.exp(-30 * (scanPhase - 0.5) * (scanPhase - 0.5)) * 0.12;
2379
+ const speakBurst = agentSpeaking ? Math.sin(time * 8) * 0.04 + Math.random() * 0.03 : 0;
2380
+
2381
+ visorMesh.material.transmission = 0.4 + 0.42 * pw + (vBreath + vNoise + scanPulse * 0.3) * pw;
2382
+ visorMesh.material.emissiveIntensity = ((2.0 + 0.4 * Math.sin(time * 2.5) + scanPulse * 2.0 + speakBurst) * sp) * pw;
2383
+ visorMesh.material.opacity = 0.5 + 0.34 * pw + (vBreath * 0.5 + scanPulse * 0.1) * pw;
2384
+ visorMesh.material.iridescence = (0.5 + 0.15 * Math.sin(time * 1.2)) * pw;
981
2385
  }
982
2386
 
983
2387
  // Joint glow rings pulse
984
2388
  glowRings.forEach((ring, i) => {
985
2389
  if (ring.material) {
986
- ring.material.opacity = 0.3 + 0.2 * Math.sin(time * 2 + i * 0.7);
987
- ring.material.emissiveIntensity = 1.1 + 0.6 * Math.sin(time * 2.5 + i * 0.5);
2390
+ ring.material.opacity = (0.3 + 0.2 * Math.sin(time * 2 + i * 0.7)) * pw;
2391
+ ring.material.emissiveIntensity = (1.1 + 0.6 * Math.sin(time * 2.5 + i * 0.5)) * pw;
988
2392
  }
989
2393
  });
990
2394
 
991
2395
  // Exhaust flames flicker
992
2396
  exhaustFlames.forEach((flame, i) => {
993
2397
  if (flame.material) {
994
- flame.material.emissiveIntensity = 1.4 + 0.8 * Math.random();
995
- flame.material.opacity = 0.32 + 0.2 * Math.random();
2398
+ flame.material.emissiveIntensity = (1.4 + 0.8 * Math.random()) * pw;
2399
+ flame.material.opacity = (0.32 + 0.2 * Math.random()) * pw;
996
2400
  }
997
- flame.scale.y = 0.8 + 0.4 * Math.random();
2401
+ flame.scale.y = 0.8 + 0.4 * Math.random() * pw;
2402
+ });
2403
+
2404
+ // Heat shimmer above exhausts
2405
+ heatShimmerMeshes.forEach((shim, i) => {
2406
+ const phase = time * 3 + i * 1.5;
2407
+ shim.material.opacity = (0.025 + 0.02 * Math.sin(phase)) * pw;
2408
+ shim.scale.x = 1.0 + 0.15 * Math.sin(phase * 1.3) * pw;
2409
+ shim.scale.z = 1.0 + 0.15 * Math.sin(phase * 1.3 + 0.5) * pw;
2410
+ shim.scale.y = 0.9 + 0.2 * Math.sin(phase * 0.7) * pw;
2411
+ shim.rotation.y += 0.02 * pw;
998
2412
  });
999
2413
 
1000
2414
  // Energy streams rotation
@@ -1016,6 +2430,36 @@ function animate() {
1016
2430
 
1017
2431
  if (idleAnimatorFn) idleAnimatorFn(time);
1018
2432
 
2433
+ // ── Procedural idle motion ──
2434
+ const sp = agentSpeaking ? 1.8 : 1.0;
2435
+ if (robotGroup) {
2436
+ robotGroup.position.y = Math.sin(time * 1.2) * 0.025 * sp;
2437
+ robotGroup.rotation.x = Math.sin(time * 0.7) * 0.006 * sp;
2438
+ robotGroup.position.x = Math.sin(time * 0.35) * 0.012 * sp;
2439
+ }
2440
+ if (headPivotRef) {
2441
+ headPivotRef.rotation.y = Math.sin(time * 0.3) * 0.12 * sp;
2442
+ headPivotRef.rotation.x = Math.sin(time * 0.5 + 0.3) * 0.04 * sp;
2443
+ headPivotRef.rotation.z = Math.sin(time * 0.22) * 0.018 * sp;
2444
+ }
2445
+ if (leftArmPivotRef) {
2446
+ leftArmPivotRef.rotation.x = Math.sin(time * 0.8) * 0.04 * sp;
2447
+ leftArmPivotRef.rotation.z = Math.sin(time * 0.55) * 0.025 * sp;
2448
+ }
2449
+ if (rightArmPivotRef) {
2450
+ rightArmPivotRef.rotation.x = Math.sin(time * 0.8 + 1.5) * 0.04 * sp;
2451
+ rightArmPivotRef.rotation.z = -Math.sin(time * 0.55 + 0.4) * 0.025 * sp;
2452
+ }
2453
+
2454
+ // Handheld camera sway (applied to scene root to avoid fighting OrbitControls)
2455
+ // Subtle "breathing" of the camera operator
2456
+ const camSwayY = Math.sin(time * 0.4) * 0.03 + Math.sin(time * 0.15) * 0.04;
2457
+ const camSwayX = Math.sin(time * 0.25) * 0.02;
2458
+ scene.position.y = camSwayY * 0.5;
2459
+ scene.position.x = camSwayX * 0.5;
2460
+
2461
+ if (filmGrainPass) filmGrainPass.uniforms.time.value = time;
2462
+
1019
2463
  composer.render();
1020
2464
  css2dRenderer.render(scene, camera);
1021
2465
  }
@@ -1032,6 +2476,7 @@ function onResize() {
1032
2476
  camera.updateProjectionMatrix();
1033
2477
  renderer.setSize(w, h);
1034
2478
  composer.setSize(w, h);
2479
+ if (ssaoPass) { ssaoPass.width = w; ssaoPass.height = h; }
1035
2480
  css2dRenderer.setSize(w, h);
1036
2481
  }
1037
2482
 
@@ -1048,7 +2493,7 @@ export function initForgeScene() {
1048
2493
  const h = vp.clientHeight;
1049
2494
 
1050
2495
  scene = new THREE.Scene();
1051
- scene.background = new THREE.Color(0x080810);
2496
+ scene.background = new THREE.Color(0x030308);
1052
2497
 
1053
2498
  camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 100);
1054
2499
  camera.position.set(6, 3, 10);
@@ -1064,7 +2509,7 @@ export function initForgeScene() {
1064
2509
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
1065
2510
  renderer.setSize(w, h);
1066
2511
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
1067
- renderer.toneMappingExposure = 1.1;
2512
+ renderer.toneMappingExposure = 0.95;
1068
2513
 
1069
2514
  css2dRenderer = new CSS2DRenderer({ element: document.getElementById("forgeLabelLayer") });
1070
2515
  css2dRenderer.setSize(w, h);
@@ -1079,78 +2524,156 @@ export function initForgeScene() {
1079
2524
  controls.autoRotate = true;
1080
2525
  controls.autoRotateSpeed = 0.5;
1081
2526
 
1082
- // ── Environment map for metallic surface reflections ──
2527
+ // ── Rich procedural environment map for PBR reflections ──
1083
2528
  const pmremGenerator = new THREE.PMREMGenerator(renderer);
1084
2529
  const envScene = new THREE.Scene();
1085
- envScene.background = new THREE.Color(0x1a2030);
1086
- const envGlow = new THREE.Mesh(
1087
- new THREE.SphereGeometry(10, 16, 8),
1088
- new THREE.MeshBasicMaterial({ color: 0x3a5060, side: THREE.BackSide })
1089
- );
1090
- envScene.add(envGlow);
1091
- const envTop = new THREE.PointLight(0x7090b0, 2, 20);
1092
- envTop.position.set(0, 8, 0);
1093
- envScene.add(envTop);
1094
- const envSide = new THREE.PointLight(GLOW, 0.5, 20);
1095
- envSide.position.set(5, 0, 5);
1096
- envScene.add(envSide);
1097
- const envMap = pmremGenerator.fromScene(envScene).texture;
2530
+ envScene.background = new THREE.Color(0x020408);
2531
+
2532
+ // Gradient sky dome — deep dark-blue gradient for chrome contrast
2533
+ const skyGeo = new THREE.SphereGeometry(12, 32, 16);
2534
+ const skyPos = skyGeo.attributes.position;
2535
+ const skyCol = new Float32Array(skyPos.count * 3);
2536
+ const _cTop = new THREE.Color(0x101830);
2537
+ const _cMid = new THREE.Color(0x050c18);
2538
+ const _cBot = new THREE.Color(0x01020a);
2539
+ const _cTmp = new THREE.Color();
2540
+ for (let i = 0; i < skyPos.count; i++) {
2541
+ const ny = skyPos.getY(i) / 12;
2542
+ const t = ny * 0.5 + 0.5;
2543
+ _cTmp.copy(t > 0.5 ? _cTop : _cMid).lerp(t > 0.5 ? _cMid : _cBot, t > 0.5 ? (1 - t) * 2 : (0.5 - t) * 2);
2544
+ skyCol[i * 3] = _cTmp.r; skyCol[i * 3 + 1] = _cTmp.g; skyCol[i * 3 + 2] = _cTmp.b;
2545
+ }
2546
+ skyGeo.setAttribute("color", new THREE.BufferAttribute(skyCol, 3));
2547
+ envScene.add(new THREE.Mesh(skyGeo, new THREE.MeshBasicMaterial({ side: THREE.BackSide, vertexColors: true })));
2548
+
2549
+ // Studio-style reflection panels — multiple for richer reflections
2550
+ const panelGeo = new THREE.PlaneGeometry(7, 12);
2551
+ // Key (right — bright white for strong specular highlight)
2552
+ const brightPanel = new THREE.Mesh(panelGeo, new THREE.MeshBasicMaterial({ color: 0xd8e0f0 }));
2553
+ brightPanel.position.set(8, 4, 0); brightPanel.rotation.y = -Math.PI / 2;
2554
+ envScene.add(brightPanel);
2555
+ // Fill (left — cool blue)
2556
+ const fillPanel = new THREE.Mesh(panelGeo.clone(), new THREE.MeshBasicMaterial({ color: 0x4a6080 }));
2557
+ fillPanel.position.set(-8, 3, 2); fillPanel.rotation.y = Math.PI / 2;
2558
+ envScene.add(fillPanel);
2559
+ // Top softbox
2560
+ const topPanel = new THREE.Mesh(new THREE.PlaneGeometry(10, 10), new THREE.MeshBasicMaterial({ color: 0x8898b0 }));
2561
+ topPanel.position.set(0, 10, 0); topPanel.rotation.x = Math.PI / 2;
2562
+ envScene.add(topPanel);
2563
+ // Rear accent (cool purple backlight)
2564
+ const rearPanel = new THREE.Mesh(new THREE.PlaneGeometry(8, 10), new THREE.MeshBasicMaterial({ color: 0x303050 }));
2565
+ rearPanel.position.set(0, 4, -8);
2566
+ envScene.add(rearPanel);
2567
+ // Front subtle warm (gives depth to front surfaces)
2568
+ const frontPanel = new THREE.Mesh(new THREE.PlaneGeometry(6, 8), new THREE.MeshBasicMaterial({ color: 0x181420 }));
2569
+ frontPanel.position.set(0, 2, 8); frontPanel.rotation.y = Math.PI;
2570
+ envScene.add(frontPanel);
2571
+ // Floor bounce
2572
+ const floorPanel = new THREE.Mesh(new THREE.PlaneGeometry(12, 12), new THREE.MeshBasicMaterial({ color: 0x101420 }));
2573
+ floorPanel.position.set(0, -6, 0); floorPanel.rotation.x = -Math.PI / 2;
2574
+ envScene.add(floorPanel);
2575
+
2576
+ // Env lights — richer multi-source for detailed chrome reflections
2577
+ const envLights = [
2578
+ [0xffffff, 14, [5, 9, 3]],
2579
+ [0x4488ff, 9, [-4, 4, 5]],
2580
+ [0xffffff, 7, [0, 11, 0]],
2581
+ [CYAN, 5, [-6, 5, -4]],
2582
+ [0x6688bb, 3, [3, -3, 6]],
2583
+ [0x9966cc, 3, [-3, 6, -6]],
2584
+ [0xffffff, 4, [6, 2, -4]],
2585
+ ];
2586
+ for (const [c, intens, pos] of envLights) {
2587
+ const l = new THREE.PointLight(c, intens, 25);
2588
+ l.position.set(...pos);
2589
+ envScene.add(l);
2590
+ }
2591
+
2592
+ const envMap = pmremGenerator.fromScene(envScene, 0.04).texture;
1098
2593
  scene.environment = envMap;
1099
2594
  pmremGenerator.dispose();
1100
2595
 
1101
- // ── Dramatic lighting (bright enough to read chrome armor) ──
1102
- scene.add(new THREE.AmbientLight(0x404060, 0.9));
1103
- const hemi = new THREE.HemisphereLight(0xb8ccff, 0x12161f, 0.65);
2596
+ // ── Cinematic Lighting (dark studio with chrome highlights) ──
2597
+ scene.add(new THREE.AmbientLight(0x060610, 0.15));
2598
+ const hemi = new THREE.HemisphereLight(0x8899bb, 0x020208, 0.3);
1104
2599
  scene.add(hemi);
1105
2600
 
1106
- const keyLight = new THREE.DirectionalLight(0xffffff, 2.1);
1107
- keyLight.position.set(5, 10, 5);
2601
+ // Key light hard white for chrome specular
2602
+ const keyLight = new THREE.DirectionalLight(0xffffff, 3.0);
2603
+ keyLight.position.set(4, 10, 6);
1108
2604
  scene.add(keyLight);
1109
2605
 
1110
- const fillLight = new THREE.PointLight(CYAN, 1.0, 25);
1111
- fillLight.position.set(-5, 5, 3);
2606
+ // Fill light cool blue, very dim
2607
+ const fillLight = new THREE.DirectionalLight(0x2244aa, 0.8);
2608
+ fillLight.position.set(-6, 3, 4);
1112
2609
  scene.add(fillLight);
1113
2610
 
1114
- const rimLight = new THREE.PointLight(GLOW, 0.8, 20);
1115
- rimLight.position.set(0, 8, -8);
2611
+ // Rim light strong cold backlight for silhouette pop
2612
+ const rimLight = new THREE.SpotLight(0x4488ff, 8.0, 25, 0.5, 0.4, 1);
2613
+ rimLight.position.set(0, 7, -10);
2614
+ rimLight.target.position.set(0, 3, 0);
1116
2615
  scene.add(rimLight);
2616
+ scene.add(rimLight.target);
1117
2617
 
1118
- const underGlow = new THREE.PointLight(GLOW, 0.5, 12);
2618
+ // Accent kicker from below-right for chrome underside reflections
2619
+ const kickLight = new THREE.PointLight(0x4466aa, 1.5, 15);
2620
+ kickLight.position.set(4, -1, 3);
2621
+ scene.add(kickLight);
2622
+
2623
+ // Subtle glow from below for dramatic underlight
2624
+ const underGlow = new THREE.PointLight(GLOW, 0.3, 10);
1119
2625
  underGlow.position.set(0, -2, 0);
1120
2626
  scene.add(underGlow);
1121
2627
 
1122
- const frontFill = new THREE.PointLight(0xccccff, 0.6, 18);
1123
- frontFill.position.set(0, 4, 8);
1124
- scene.add(frontFill);
1125
-
1126
- const bodySpot = new THREE.SpotLight(0xaec6ff, 1.1, 26, Math.PI * 0.36, 0.45, 1.1);
1127
- bodySpot.position.set(0, 9.5, 5.5);
1128
- bodySpot.target.position.set(0, 2.6, 0);
2628
+ // Body spot focused on torso
2629
+ const bodySpot = new THREE.SpotLight(0xdde8ff, 1.5, 28, Math.PI * 0.35, 0.4, 1.0);
2630
+ bodySpot.position.set(0, 10, 5);
2631
+ bodySpot.target.position.set(0, 3, 0);
1129
2632
  scene.add(bodySpot);
1130
2633
  scene.add(bodySpot.target);
1131
2634
 
1132
- const sideAccent = new THREE.PointLight(0xb026ff, 0.4, 15);
1133
- sideAccent.position.set(6, 3, -3);
1134
- scene.add(sideAccent);
1135
-
1136
2635
  const sideAccent2 = new THREE.PointLight(CYAN, 0.3, 15);
1137
2636
  sideAccent2.position.set(-6, 3, -3);
1138
2637
  scene.add(sideAccent2);
1139
2638
 
1140
- // Ground
1141
- const ground = new THREE.Mesh(
1142
- new THREE.PlaneGeometry(40, 40),
1143
- new M({ color: 0x050505, roughness: 0.95, metalness: 0.0 })
1144
- );
2639
+ // Ground — large reflective floor with subtle texture
2640
+ const groundGeo = new THREE.PlaneGeometry(80, 80);
2641
+ const groundCanvas = document.createElement("canvas");
2642
+ groundCanvas.width = 512; groundCanvas.height = 512;
2643
+ const gCtx = groundCanvas.getContext("2d");
2644
+ gCtx.fillStyle = "#020204";
2645
+ gCtx.fillRect(0, 0, 512, 512);
2646
+ // Subtle grid lines on the floor
2647
+ gCtx.strokeStyle = "rgba(40,50,80,0.08)";
2648
+ gCtx.lineWidth = 1;
2649
+ for (let i = 0; i <= 512; i += 32) {
2650
+ gCtx.beginPath(); gCtx.moveTo(i, 0); gCtx.lineTo(i, 512); gCtx.stroke();
2651
+ gCtx.beginPath(); gCtx.moveTo(0, i); gCtx.lineTo(512, i); gCtx.stroke();
2652
+ }
2653
+ const groundTex = new THREE.CanvasTexture(groundCanvas);
2654
+ groundTex.wrapS = groundTex.wrapT = THREE.RepeatWrapping;
2655
+ groundTex.repeat.set(8, 8);
2656
+ const groundMat = new P({
2657
+ map: groundTex,
2658
+ color: 0x030306,
2659
+ roughness: 0.22,
2660
+ metalness: 0.55,
2661
+ clearcoat: 0.65,
2662
+ clearcoatRoughness: 0.12,
2663
+ envMapIntensity: 1.2,
2664
+ });
2665
+ const ground = new THREE.Mesh(groundGeo, groundMat);
1145
2666
  ground.rotation.x = -Math.PI / 2;
1146
- ground.position.y = -2.8;
2667
+ ground.position.y = -2.82;
1147
2668
  scene.add(ground);
1148
2669
 
2670
+ // Fog gradient — subtle depth fade for the far floor edges
2671
+ scene.fog = new THREE.FogExp2(0x020206, 0.018);
2672
+
1149
2673
  platformGroup = buildPlatform();
1150
2674
  scene.add(platformGroup);
1151
2675
 
1152
2676
  robotGroup = new THREE.Group();
1153
- robotGroup.add(buildChassis());
1154
2677
  scene.add(robotGroup);
1155
2678
 
1156
2679
  attachmentGroup = new THREE.Group();
@@ -1165,11 +2688,30 @@ export function initForgeScene() {
1165
2688
  energyStreams = createEnergyStreams();
1166
2689
  scene.add(energyStreams);
1167
2690
 
1168
- // ── Post-processing (heavy bloom) ──
2691
+ // Build chassis asynchronously so the browser can paint a frame first
2692
+ const _chassisReady = new Promise(resolve => {
2693
+ requestAnimationFrame(() => {
2694
+ const t0 = performance.now();
2695
+ robotGroup.add(buildChassis());
2696
+ console.log(`[forge] chassis built in ${(performance.now() - t0).toFixed(0)}ms, geo cache: ${_geoCache.size} entries`);
2697
+ resolve();
2698
+ });
2699
+ });
2700
+
2701
+ // ── Post-processing: SSAO → bloom → outline → film grain → output ──
1169
2702
  composer = new EffectComposer(renderer);
1170
2703
  composer.addPass(new RenderPass(scene, camera));
1171
2704
 
1172
- bloomPass = new UnrealBloomPass(new THREE.Vector2(w, h), 0.7, 0.32, 0.88);
2705
+ try {
2706
+ ssaoPass = new SSAOPass(scene, camera, w, h);
2707
+ ssaoPass.kernelRadius = 12;
2708
+ ssaoPass.minDistance = 0.003;
2709
+ ssaoPass.maxDistance = 0.12;
2710
+ ssaoPass.output = SSAOPass.OUTPUT.Default;
2711
+ composer.addPass(ssaoPass);
2712
+ } catch { /* SSAO unavailable — continue without it */ }
2713
+
2714
+ bloomPass = new UnrealBloomPass(new THREE.Vector2(w, h), 0.7, 0.35, 0.85);
1173
2715
  composer.addPass(bloomPass);
1174
2716
 
1175
2717
  outlinePass = new OutlinePass(new THREE.Vector2(w, h), scene, camera);
@@ -1180,6 +2722,11 @@ export function initForgeScene() {
1180
2722
  outlinePass.hiddenEdgeColor.set(GLOW);
1181
2723
  composer.addPass(outlinePass);
1182
2724
 
2725
+ filmGrainPass = new ShaderPass(FilmGrainShader);
2726
+ composer.addPass(filmGrainPass);
2727
+
2728
+ composer.addPass(new OutputPass());
2729
+
1183
2730
  viewportEl().addEventListener("click", onCanvasClick);
1184
2731
  viewportEl().addEventListener("pointermove", onCanvasHover);
1185
2732
  viewportEl().addEventListener("pointerleave", () => {
@@ -1205,8 +2752,13 @@ export function initForgeScene() {
1205
2752
  setupAutoRotatePause();
1206
2753
  animate();
1207
2754
 
1208
- playCinematicIntro(() => {
1209
- window.dispatchEvent(new CustomEvent("forge:ready"));
2755
+ window.__forgeSetSpeaking = setAgentSpeaking;
2756
+
2757
+ _chassisReady.then(() => {
2758
+ playCinematicIntro(() => {
2759
+ window.__forgeSceneReady = true;
2760
+ window.dispatchEvent(new CustomEvent("forge:ready"));
2761
+ });
1210
2762
  });
1211
2763
 
1212
2764
  initialized = true;