ape-claw 0.1.6 → 0.1.8

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.
@@ -1,22 +1,34 @@
1
1
  /**
2
- * forge-attachments.js — Scalable region builders (1000+ skills),
3
- * energy network, category-specific idle animations for ClawBot Forge.
2
+ * forge-attachments.js — 200-slot body-mounted hardpoint system
3
+ * with hyperrealistic components. Every slot is welded to a specific
4
+ * anatomical zone of the robot — no floating parts.
4
5
  *
5
- * HV-MTL integrated style: attachments mount directly onto the robot
6
- * body at armor hardpoints, joints, and expansion slots rather than
7
- * floating disconnected in space.
6
+ * Robot anatomy (Y axis):
7
+ * Crown: Y 7.5 Feet: Y -2.6
8
+ * Head: Y 6.2–7.2 Shins: Y -0.8 -1.8
9
+ * Neck: Y 5.8–6.2 Knees: Y -0.6
10
+ * Shoulders: Y 5.0–5.6 Thighs: Y 0.0 – 0.8
11
+ * Chest: Y 3.2–5.2 Waist: Y 1.2 – 1.8
12
+ * Back: Y 2.0–5.2 Hips: Y 0.2 – 1.0
13
+ * Arms: X ±2.0–2.4 Calves: Y -1.0 – -2.2
14
+ *
15
+ * Region → primaryCap mapping (sums to 200):
16
+ * Security 20 · Analytics 24 · Automation 16 · DevTools 16
17
+ * NFT 14 · Social 12 · Storage 12 · Productivity 12
18
+ * Bridge 10 · Trading 10 · Governance 10 · Wallet 10
19
+ * DeFi 10 · Development 12 · Writing 10 · Communication 12
8
20
  */
9
21
  import * as THREE from "three";
10
22
  import { RoundedBoxGeometry } from "three/addons/geometries/RoundedBoxGeometry.js";
11
23
  import { MAT } from "./forge-scene.js";
12
24
 
13
- /* ══════════════════════════════════════════════════════════
14
- Category Color hex map
15
- ══════════════════════════════════════════════════════════ */
25
+ /* ─────────────────────────────────────────────────────────────
26
+ Category colour palette
27
+ ───────────────────────────────────────────────────────────── */
16
28
  export const CATEGORY_COLORS = {
17
29
  Security: "#FFB347",
18
30
  Analytics: "#63d7ff",
19
- Automation: "#00ff00",
31
+ Automation: "#00ff88",
20
32
  DevTools: "#4169E1",
21
33
  NFT: "#b026ff",
22
34
  Social: "#FF7F50",
@@ -27,7 +39,7 @@ export const CATEGORY_COLORS = {
27
39
  Governance: "#4B0082",
28
40
  Wallet: "#50C878",
29
41
  DeFi: "#32CD32",
30
- Development: "#4169E1",
42
+ Development: "#4488FF",
31
43
  Writing: "#FFF8DC",
32
44
  Communication: "#00BFFF",
33
45
  };
@@ -40,350 +52,616 @@ for (const [k, v] of Object.entries(CATEGORY_COLORS)) {
40
52
  function matFor(cat) {
41
53
  const key = cat.toLowerCase();
42
54
  const mat = MAT[key] ? MAT[key].clone() : MAT.armor.clone();
43
- if (typeof mat.emissiveIntensity === "number") {
44
- mat.emissiveIntensity *= 0.72;
45
- }
55
+ if (typeof mat.emissiveIntensity === "number") mat.emissiveIntensity *= 0.72;
46
56
  return mat;
47
57
  }
48
58
 
49
- /* ══════════════════════════════════════════════════════════
50
- Golden-angle spiral for overflow orbital placement
51
- ══════════════════════════════════════════════════════════ */
52
- const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
53
-
54
- function overflowPosition(overflowIdx, center, baseRadius, verticalSpread) {
55
- const shell = Math.floor(overflowIdx / 24);
56
- const idxInShell = overflowIdx % 24;
57
- const r = baseRadius + shell * 0.5;
58
- const angle = idxInShell * GOLDEN_ANGLE;
59
- const yOff = (idxInShell / 24 - 0.5) * verticalSpread;
60
- return new THREE.Vector3(
61
- center.x + Math.cos(angle) * r,
62
- center.y + yOff,
63
- center.z + Math.sin(angle) * r,
64
- );
59
+ /* ─────────────────────────────────────────────────────────────
60
+ Shared geometry constants
61
+ ───────────────────────────────────────────────────────────── */
62
+ const FRAME_C = 0x1a2230;
63
+ const RIVET_C = 0xd4dce8;
64
+ const HEAT_C = 0xff4400;
65
+ const PIPE_C = 0x2a3545;
66
+
67
+ function rMat(extra = {}) {
68
+ return new THREE.MeshPhysicalMaterial({
69
+ color: RIVET_C, metalness: 0.96, roughness: 0.05,
70
+ clearcoat: 1.0, clearcoatRoughness: 0.0, ...extra,
71
+ });
65
72
  }
66
73
 
67
- /* ══════════════════════════════════════════════════════════
68
- Region configs positions mapped to robot body hardpoints
69
- Adjusted for HV-MTL proportions (feet at ~-2.5, head at ~7)
70
- ══════════════════════════════════════════════════════════ */
71
- const REGIONS = {
72
- Security: { center: [0, 4.0, -0.8], primaryCap: 14, overflowR: 1.4, vSpread: 3.0 },
73
- Analytics: { center: [0, 7.2, 0], primaryCap: 20, overflowR: 1.0, vSpread: 1.5 },
74
- Automation: { center: [-2.1, 3.0, 0], primaryCap: 12, overflowR: 1.2, vSpread: 2.5 },
75
- DevTools: { center: [2.1, 3.0, 0], primaryCap: 12, overflowR: 1.2, vSpread: 2.5 },
76
- NFT: { center: [2.3, 5.5, -0.3], primaryCap: 10, overflowR: 1.1, vSpread: 2.0 },
77
- Social: { center: [-2.1, 2.0, 0.2], primaryCap: 8, overflowR: 1.0, vSpread: 1.8 },
78
- Storage: { center: [0, 3.0, -0.9], primaryCap: 8, overflowR: 1.0, vSpread: 1.5 },
79
- Productivity: { center: [0, 4.5, 0.78], primaryCap: 9, overflowR: 0.9, vSpread: 1.5 },
80
- Bridge: { center: [-2.0, 5.5, -0.2],primaryCap: 6, overflowR: 1.0, vSpread: 2.0 },
81
- Trading: { center: [-0.55, -0.8, 0.4],primaryCap: 6, overflowR: 1.0, vSpread: 2.0 },
82
- Governance: { center: [0, 6.7, 0.4], primaryCap: 6, overflowR: 0.8, vSpread: 1.2 },
83
- Wallet: { center: [0, 2.35, 0.55], primaryCap: 6, overflowR: 0.8, vSpread: 1.5 },
84
- DeFi: { center: [0.55, -0.8, 0.4],primaryCap: 6, overflowR: 1.0, vSpread: 2.0 },
85
- Development: { center: [2.1, 4.0, 0.2], primaryCap: 8, overflowR: 1.0, vSpread: 2.0 },
86
- Writing: { center: [2.1, 2.0, 0.2], primaryCap: 6, overflowR: 0.9, vSpread: 1.5 },
87
- Communication: { center: [0, 7.5, 0], primaryCap: 6, overflowR: 0.8, vSpread: 1.2 },
88
- };
74
+ function frameMat(base) {
75
+ const m = base.clone();
76
+ m.color = new THREE.Color(FRAME_C);
77
+ m.emissiveIntensity = (m.emissiveIntensity || 0) * 0.3;
78
+ return m;
79
+ }
89
80
 
90
- /* ══════════════════════════════════════════════════════════
91
- Attachment shapes super-detailed integrated components
92
- Layered geometry with edge trim, glow lines, mounting hardware
93
- ══════════════════════════════════════════════════════════ */
94
- const FRAME_C = 0x283040;
95
- const RIVET_C = 0xd0d8e0;
81
+ function glowMat(base, mult = 2.5) {
82
+ const m = base.clone();
83
+ m.emissiveIntensity = (m.emissiveIntensity || 0.5) * mult;
84
+ m.transparent = true;
85
+ m.opacity = 0.65;
86
+ return m;
87
+ }
96
88
 
89
+ /* ─────────────────────────────────────────────────────────────
90
+ ■ SHAPE LIBRARY (11 hyperrealistic components)
91
+ ───────────────────────────────────────────────────────────── */
92
+
93
+ /** Bevelled armour slab with frame rails, glow stripe, and rivets */
97
94
  function armorPlate(mat, w, h, d) {
98
- const group = new THREE.Group();
99
- const minDim = Math.min(w, h, d);
100
- const bodyGeo = minDim > 0.08
101
- ? new RoundedBoxGeometry(w, h, d, 2, minDim * 0.12)
95
+ const g = new THREE.Group();
96
+ const minD = Math.min(w, h, d);
97
+ const bodyGeo = minD > 0.08
98
+ ? new RoundedBoxGeometry(w, h, d, 2, minD * 0.13)
102
99
  : new THREE.BoxGeometry(w, h, d);
103
- const body = new THREE.Mesh(bodyGeo, mat);
104
- group.add(body);
105
- const frameMat = mat.clone();
106
- frameMat.color = new THREE.Color(FRAME_C);
107
- frameMat.emissiveIntensity = (frameMat.emissiveIntensity || 0) * 0.4;
108
- const bw = 0.015;
109
- group.add(new THREE.Mesh(new THREE.BoxGeometry(w + bw * 2, bw, d + bw), frameMat).translateY(h / 2));
110
- group.add(new THREE.Mesh(new THREE.BoxGeometry(w + bw * 2, bw, d + bw), frameMat).translateY(-h / 2));
111
- group.add(new THREE.Mesh(new THREE.BoxGeometry(bw, h, d + bw), frameMat).translateX(w / 2));
112
- group.add(new THREE.Mesh(new THREE.BoxGeometry(bw, h, d + bw), frameMat).translateX(-w / 2));
113
- const glowMat = mat.clone();
114
- glowMat.emissiveIntensity = (glowMat.emissiveIntensity || 0.5) * 2.5;
115
- glowMat.transparent = true; glowMat.opacity = 0.7;
116
- group.add(new THREE.Mesh(new THREE.BoxGeometry(w * 0.6, 0.015, d * 0.3), glowMat).translateZ(d / 2 + 0.005));
117
- const rMat = new THREE.MeshPhysicalMaterial({ color: RIVET_C, metalness: 0.95, roughness: 0.08, clearcoat: 1.0, clearcoatRoughness: 0.0 });
118
- const rGeo = new THREE.SphereGeometry(0.018, 8, 6);
119
- for (const [rx, ry] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) {
120
- const rv = new THREE.Mesh(rGeo, rMat);
121
- rv.position.set(rx * w * 0.4, ry * h * 0.35, d / 2 + 0.01);
122
- group.add(rv);
100
+ g.add(new THREE.Mesh(bodyGeo, mat));
101
+
102
+ const fm = frameMat(mat);
103
+ const bw = 0.014;
104
+ g.add(new THREE.Mesh(new THREE.BoxGeometry(w + bw * 2, bw, d + bw), fm).translateY( h / 2));
105
+ g.add(new THREE.Mesh(new THREE.BoxGeometry(w + bw * 2, bw, d + bw), fm).translateY(-h / 2));
106
+ g.add(new THREE.Mesh(new THREE.BoxGeometry(bw, h, d + bw), fm).translateX( w / 2));
107
+ g.add(new THREE.Mesh(new THREE.BoxGeometry(bw, h, d + bw), fm).translateX(-w / 2));
108
+
109
+ const gm = glowMat(mat, 2.8);
110
+ g.add(new THREE.Mesh(new THREE.BoxGeometry(w * 0.55, 0.013, d * 0.28), gm).translateZ(d / 2 + 0.005));
111
+
112
+ const rv = new THREE.Mesh(new THREE.SphereGeometry(0.016, 8, 6), rMat());
113
+ for (const [rx, ry] of [[-1,-1],[1,-1],[-1,1],[1,1]]) {
114
+ const r2 = rv.clone();
115
+ r2.position.set(rx * w * 0.38, ry * h * 0.33, d / 2 + 0.009);
116
+ g.add(r2);
123
117
  }
124
- return group;
118
+ return g;
125
119
  }
126
120
 
121
+ /** Hexagonal tech plate with inner glow disc and apex sensor node */
127
122
  function hexPlate(mat, r, h) {
128
- const group = new THREE.Group();
129
- group.add(new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, 6), mat));
130
- const innerMat = mat.clone();
131
- innerMat.emissiveIntensity = (innerMat.emissiveIntensity || 0.5) * 1.8;
132
- innerMat.transparent = true; innerMat.opacity = 0.6;
133
- const inner = new THREE.Mesh(new THREE.CylinderGeometry(r * 0.55, r * 0.55, h + 0.01, 6), innerMat);
134
- group.add(inner);
135
- const dotMat = mat.clone();
136
- dotMat.emissiveIntensity = (dotMat.emissiveIntensity || 0.5) * 3;
137
- group.add(new THREE.Mesh(new THREE.SphereGeometry(r * 0.18, 12, 8), dotMat).translateY(h / 2 + 0.01));
138
- const edgeMat = mat.clone();
139
- edgeMat.emissiveIntensity = (edgeMat.emissiveIntensity || 0.5) * 2;
140
- edgeMat.transparent = true; edgeMat.opacity = 0.5;
141
- const edge = new THREE.Mesh(new THREE.TorusGeometry(r, r * 0.06, 10, 6), edgeMat);
142
- edge.rotation.x = Math.PI / 2; edge.position.y = h / 2 + 0.005;
143
- group.add(edge);
144
- return group;
123
+ const g = new THREE.Group();
124
+ g.add(new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, 6), mat));
125
+ const inner = new THREE.Mesh(
126
+ new THREE.CylinderGeometry(r * 0.52, r * 0.52, h + 0.01, 6),
127
+ glowMat(mat, 1.9),
128
+ );
129
+ g.add(inner);
130
+ const dot = new THREE.Mesh(new THREE.SphereGeometry(r * 0.19, 14, 9), glowMat(mat, 3.2));
131
+ dot.translateY(h / 2 + 0.01);
132
+ g.add(dot);
133
+ const edge = new THREE.Mesh(new THREE.TorusGeometry(r, r * 0.055, 10, 6), glowMat(mat, 2.1));
134
+ edge.rotation.x = Math.PI / 2; edge.position.y = h / 2 + 0.004;
135
+ g.add(edge);
136
+ return g;
145
137
  }
146
138
 
139
+ /** Spinning energy core — octahedron + dual orbiting rings + bracket */
147
140
  function energyModule(mat, r) {
148
- const group = new THREE.Group();
149
- const core = new THREE.Mesh(new THREE.OctahedronGeometry(r * 0.6, 1), mat);
150
- group.add(core);
151
- const gMat = mat.clone();
152
- gMat.emissiveIntensity = (gMat.emissiveIntensity || 1) * 3;
153
- gMat.transparent = true; gMat.opacity = 0.5;
154
- group.add(new THREE.Mesh(new THREE.SphereGeometry(r * 0.3, 16, 12), gMat));
155
- const ring = new THREE.Mesh(new THREE.TorusGeometry(r, r * 0.1, 12, 32), mat.clone());
156
- ring.material.transparent = true; ring.material.opacity = 0.6;
157
- ring.rotation.x = Math.PI / 2;
158
- group.add(ring);
159
- const ring2 = new THREE.Mesh(new THREE.TorusGeometry(r * 0.8, r * 0.06, 10, 24), mat.clone());
160
- ring2.material.transparent = true; ring2.material.opacity = 0.4;
161
- ring2.rotation.z = Math.PI / 2;
162
- group.add(ring2);
163
- const bMat = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.6, roughness: 0.4, clearcoat: 0.3, clearcoatRoughness: 0.4 });
164
- group.add(new THREE.Mesh(new THREE.BoxGeometry(0.03, r * 1.2, 0.03), bMat).translateX(-r * 0.9));
165
- return group;
141
+ const g = new THREE.Group();
142
+ g.add(new THREE.Mesh(new THREE.OctahedronGeometry(r * 0.62, 1), mat));
143
+ g.add(new THREE.Mesh(new THREE.SphereGeometry(r * 0.28, 16, 12), glowMat(mat, 3.5)));
144
+ const ring1 = new THREE.Mesh(new THREE.TorusGeometry(r, r * 0.1, 12, 32), glowMat(mat, 1.6));
145
+ ring1.rotation.x = Math.PI / 2; g.add(ring1);
146
+ const ring2 = new THREE.Mesh(new THREE.TorusGeometry(r * 0.78, r * 0.06, 10, 24), glowMat(mat, 1.3));
147
+ ring2.rotation.z = Math.PI / 2; g.add(ring2);
148
+ const bm = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.65, roughness: 0.35, clearcoat: 0.4 });
149
+ g.add(new THREE.Mesh(new THREE.BoxGeometry(0.028, r * 1.25, 0.028), bm).translateX(-r * 0.88));
150
+ return g;
166
151
  }
167
152
 
153
+ /** Swept blade fin with edge-glow wires and mounting pin */
168
154
  function bladeFin(mat, h, w) {
169
- const group = new THREE.Group();
155
+ const g = new THREE.Group();
170
156
  const shape = new THREE.Shape();
171
157
  shape.moveTo(0, h / 2);
158
+ shape.bezierCurveTo(w * 0.6, h * 0.1, w * 0.7, -h * 0.3, w / 2, -h / 2);
172
159
  shape.lineTo(-w / 2, -h / 2);
173
- shape.lineTo(w / 2, -h / 2);
160
+ shape.bezierCurveTo(-w * 0.1, -h * 0.1, 0, h * 0.2, 0, h / 2);
174
161
  shape.closePath();
175
- const geo = new THREE.ExtrudeGeometry(shape, { depth: 0.04, bevelEnabled: false });
176
- group.add(new THREE.Mesh(geo, mat));
177
- // Edge glow trim
178
- const eMat = mat.clone();
179
- eMat.emissiveIntensity = (eMat.emissiveIntensity || 0.5) * 3;
180
- eMat.transparent = true; eMat.opacity = 0.6;
162
+ const geo = new THREE.ExtrudeGeometry(shape, { depth: 0.038, bevelEnabled: true, bevelSize: 0.008, bevelThickness: 0.008, bevelSegments: 2 });
163
+ g.add(new THREE.Mesh(geo, mat));
181
164
  const pts = [
182
- new THREE.Vector3(0, h / 2, 0.02),
183
- new THREE.Vector3(-w / 2, -h / 2, 0.02),
184
- new THREE.Vector3(w / 2, -h / 2, 0.02),
185
- new THREE.Vector3(0, h / 2, 0.02),
165
+ new THREE.Vector3(0, h / 2, 0.022),
166
+ new THREE.Vector3(w * 0.45, -h / 4, 0.022),
167
+ new THREE.Vector3(w / 2, -h / 2, 0.022),
186
168
  ];
187
169
  const lineGeo = new THREE.BufferGeometry().setFromPoints(pts);
188
- group.add(new THREE.Line(lineGeo, new THREE.LineBasicMaterial({
170
+ g.add(new THREE.Line(lineGeo, new THREE.LineBasicMaterial({
189
171
  color: mat.emissive?.getHex?.() || 0xffffff,
190
- transparent: true, opacity: 0.6,
191
- blending: THREE.AdditiveBlending,
172
+ transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending,
192
173
  })));
193
- const bMat = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.6, roughness: 0.4, clearcoat: 0.3, clearcoatRoughness: 0.4 });
194
- group.add(new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.04, 0.06, 8), bMat).translateY(-h / 2 - 0.03));
195
- return group;
174
+ const bm = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.65, roughness: 0.38 });
175
+ g.add(new THREE.Mesh(new THREE.CylinderGeometry(0.024, 0.038, 0.055, 8), bm).translateY(-h / 2 - 0.028));
176
+ return g;
196
177
  }
197
178
 
179
+ /** Shoulder/turret gun barrel with housing ring and muzzle glow */
198
180
  function turretModule(mat, r) {
199
- const group = new THREE.Group();
200
- const bMat = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.7, roughness: 0.3, clearcoat: 0.4, clearcoatRoughness: 0.3 });
201
- group.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 0.18, r * 0.22, r * 2.5, 12), bMat).rotateX(Math.PI / 2).translateY(r * 0.6));
202
- group.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 0.6, r * 0.7, r * 0.4, 12), mat));
203
- const gMat = mat.clone();
204
- gMat.emissiveIntensity = (gMat.emissiveIntensity || 0.5) * 2.5;
205
- gMat.transparent = true; gMat.opacity = 0.5;
206
- const ring = new THREE.Mesh(new THREE.TorusGeometry(r * 0.65, r * 0.06, 10, 24), gMat);
207
- ring.rotation.x = Math.PI / 2;
208
- group.add(ring);
209
- return group;
181
+ const g = new THREE.Group();
182
+ const bm = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.72, roughness: 0.28, clearcoat: 0.5 });
183
+ const barrel = new THREE.Mesh(new THREE.CylinderGeometry(r * 0.17, r * 0.21, r * 2.6, 12), bm);
184
+ barrel.rotation.x = Math.PI / 2; barrel.position.y = r * 0.55;
185
+ g.add(barrel);
186
+ g.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 0.62, r * 0.72, r * 0.38, 12), mat));
187
+ const ring = new THREE.Mesh(new THREE.TorusGeometry(r * 0.66, r * 0.055, 10, 24), glowMat(mat, 2.4));
188
+ ring.rotation.x = Math.PI / 2; g.add(ring);
189
+ const muzzle = new THREE.Mesh(new THREE.TorusGeometry(r * 0.15, r * 0.04, 8, 16), glowMat(mat, 4.0));
190
+ muzzle.rotation.x = Math.PI / 2; muzzle.position.set(0, r * 0.55, r * 1.35); g.add(muzzle);
191
+ return g;
210
192
  }
211
193
 
194
+ /** Hemisphere sensor dome with rotating inner ring and antenna spike */
212
195
  function sensorDome(mat, r) {
213
- const group = new THREE.Group();
214
- group.add(new THREE.Mesh(new THREE.SphereGeometry(r, 24, 16, 0, Math.PI * 2, 0, Math.PI / 2), mat));
215
- const bMat = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.6, roughness: 0.4, clearcoat: 0.3, clearcoatRoughness: 0.4 });
216
- group.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 1.1, r * 1.1, r * 0.15, 16), bMat));
217
- group.add(new THREE.Mesh(new THREE.ConeGeometry(r * 0.08, r * 1.2, 8), mat.clone()).translateY(r * 0.9));
218
- const gMat = mat.clone();
219
- gMat.emissiveIntensity = (gMat.emissiveIntensity || 0.5) * 2.5;
220
- gMat.transparent = true; gMat.opacity = 0.5;
221
- const ring = new THREE.Mesh(new THREE.TorusGeometry(r * 0.7, r * 0.04, 10, 24), gMat);
222
- ring.rotation.x = Math.PI / 2; ring.position.y = r * 0.3;
223
- group.add(ring);
224
- return group;
196
+ const g = new THREE.Group();
197
+ g.add(new THREE.Mesh(new THREE.SphereGeometry(r, 24, 16, 0, Math.PI * 2, 0, Math.PI / 2), mat));
198
+ const bm = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.62, roughness: 0.42, clearcoat: 0.35 });
199
+ g.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 1.08, r * 1.08, r * 0.14, 16), bm));
200
+ const spike = new THREE.Mesh(new THREE.ConeGeometry(r * 0.07, r * 1.3, 8), mat.clone());
201
+ spike.translateY(r * 0.9); g.add(spike);
202
+ const scanRing = new THREE.Mesh(new THREE.TorusGeometry(r * 0.68, r * 0.035, 10, 24), glowMat(mat, 2.8));
203
+ scanRing.rotation.x = Math.PI / 2; scanRing.position.y = r * 0.3; g.add(scanRing);
204
+ return g;
205
+ }
206
+
207
+ /** Cluster of three missile/rocket tubes with blast-guard plate */
208
+ function missilePod(mat, r) {
209
+ const g = new THREE.Group();
210
+ const bm = new THREE.MeshPhysicalMaterial({ color: PIPE_C, metalness: 0.75, roughness: 0.25, clearcoat: 0.6 });
211
+ const capMat = rMat({ color: HEAT_C, emissive: new THREE.Color(HEAT_C), emissiveIntensity: 1.2 });
212
+ const offsets = [[0, r * 0.55], [-r * 0.5, -r * 0.28], [r * 0.5, -r * 0.28]];
213
+ for (const [ox, oy] of offsets) {
214
+ const tube = new THREE.Mesh(new THREE.CylinderGeometry(r * 0.22, r * 0.22, r * 1.8, 8), bm);
215
+ tube.position.set(ox, oy, 0); g.add(tube);
216
+ const cap = new THREE.Mesh(new THREE.CircleGeometry(r * 0.18, 8), capMat);
217
+ cap.position.set(ox, oy - r * 0.9, r * 0.24); cap.rotation.x = Math.PI / 2; g.add(cap);
218
+ }
219
+ const plate = new THREE.Mesh(new THREE.BoxGeometry(r * 1.6, r * 1.6, r * 0.22), mat);
220
+ plate.position.z = -r * 0.12; g.add(plate);
221
+ const ring = new THREE.Mesh(new THREE.TorusGeometry(r * 0.65, r * 0.04, 8, 18), glowMat(mat, 2.0));
222
+ ring.rotation.x = Math.PI / 2; g.add(ring);
223
+ return g;
224
+ }
225
+
226
+ /** Exhaust thruster nozzle with heat shield ribs and afterburner glow */
227
+ function thrusterNozzle(mat, r) {
228
+ const g = new THREE.Group();
229
+ const bm = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.8, roughness: 0.2, clearcoat: 0.7 });
230
+ g.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 0.55, r * 0.9, r * 1.1, 16), bm));
231
+ const heatMat = new THREE.MeshPhysicalMaterial({
232
+ color: HEAT_C, emissive: new THREE.Color(0xff6600),
233
+ emissiveIntensity: 2.0, transparent: true, opacity: 0.55, metalness: 0.4, roughness: 0.6,
234
+ });
235
+ const nozzleInner = new THREE.Mesh(new THREE.CylinderGeometry(r * 0.42, r * 0.82, r * 1.15, 16), heatMat);
236
+ g.add(nozzleInner);
237
+ for (let i = 0; i < 6; i++) {
238
+ const rib = new THREE.Mesh(new THREE.BoxGeometry(r * 0.06, r * 1.15, r * 0.12), bm);
239
+ rib.rotation.y = (i / 6) * Math.PI * 2;
240
+ rib.position.set(Math.cos((i / 6) * Math.PI * 2) * r * 0.72, 0, Math.sin((i / 6) * Math.PI * 2) * r * 0.72);
241
+ g.add(rib);
242
+ }
243
+ const glow = new THREE.Mesh(new THREE.CircleGeometry(r * 0.45, 16), glowMat(mat, 5.0));
244
+ glow.position.y = -r * 0.56; glow.rotation.x = Math.PI / 2; g.add(glow);
245
+ return g;
246
+ }
247
+
248
+ /** Data uplink spike — tapered rod with three orbiting signal rings */
249
+ function dataSpike(mat, h) {
250
+ const g = new THREE.Group();
251
+ const bm = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.78, roughness: 0.22, clearcoat: 0.8 });
252
+ g.add(new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.06, h, 8), bm));
253
+ g.add(new THREE.Mesh(new THREE.ConeGeometry(0.04, h * 0.28, 8), mat).translateY(h * 0.64));
254
+ const ringOffsets = [0.28, 0.52, 0.72];
255
+ for (const t of ringOffsets) {
256
+ const nr = 0.11 + t * 0.06;
257
+ const ring = new THREE.Mesh(new THREE.TorusGeometry(nr, 0.013, 7, 20), glowMat(mat, 3.0 - t));
258
+ ring.rotation.x = Math.PI / 2; ring.position.y = h * (t - 0.5); g.add(ring);
259
+ }
260
+ const base = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.045, 10), mat);
261
+ base.translateY(-h * 0.5 - 0.022); g.add(base);
262
+ return g;
263
+ }
264
+
265
+ /** Glowing reactor core — icosahedron with equatorial ring array */
266
+ function reactorCore(mat, r) {
267
+ const g = new THREE.Group();
268
+ const coreMat = glowMat(mat, 3.8);
269
+ coreMat.opacity = 0.9;
270
+ g.add(new THREE.Mesh(new THREE.IcosahedronGeometry(r * 0.58, 1), coreMat));
271
+ const shell = mat.clone(); shell.transparent = true; shell.opacity = 0.18; shell.side = THREE.BackSide;
272
+ g.add(new THREE.Mesh(new THREE.IcosahedronGeometry(r * 0.82, 1), shell));
273
+ for (let i = 0; i < 3; i++) {
274
+ const angle = (i / 3) * Math.PI;
275
+ const orb = new THREE.Mesh(new THREE.TorusGeometry(r, r * 0.07, 9, 28), glowMat(mat, 2.0 + i * 0.4));
276
+ orb.rotation.set(angle, angle * 0.7, 0); orb.transparent = true; orb.opacity = 0.5; g.add(orb);
277
+ }
278
+ const bm = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.7, roughness: 0.3 });
279
+ for (let i = 0; i < 4; i++) {
280
+ const a = (i / 4) * Math.PI * 2;
281
+ const strut = new THREE.Mesh(new THREE.CylinderGeometry(0.018, 0.018, r * 0.75, 6), bm);
282
+ strut.position.set(Math.cos(a) * r * 0.88, 0, Math.sin(a) * r * 0.88);
283
+ strut.rotation.z = -Math.PI / 2; strut.rotation.y = a; g.add(strut);
284
+ }
285
+ return g;
286
+ }
287
+
288
+ /** Articulated shoulder cannon with scope housing and charging rune */
289
+ function shoulderCannon(mat, scale = 1.0) {
290
+ const s = scale;
291
+ const g = new THREE.Group();
292
+ const bm = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.78, roughness: 0.22, clearcoat: 0.6 });
293
+ const housing = new THREE.Mesh(new RoundedBoxGeometry(0.55 * s, 0.3 * s, 0.4 * s, 2, 0.04 * s), mat);
294
+ g.add(housing);
295
+ const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.065 * s, 0.08 * s, 0.9 * s, 12), bm);
296
+ barrel.rotation.x = Math.PI / 2; barrel.position.set(0, 0.05 * s, 0.65 * s); g.add(barrel);
297
+ const scope = new THREE.Mesh(new THREE.CylinderGeometry(0.04 * s, 0.04 * s, 0.35 * s, 8), bm);
298
+ scope.rotation.x = Math.PI / 2; scope.position.set(0, 0.2 * s, 0.4 * s); g.add(scope);
299
+ const muzzle = new THREE.Mesh(new THREE.TorusGeometry(0.055 * s, 0.016 * s, 8, 16), glowMat(mat, 4.5));
300
+ muzzle.rotation.x = Math.PI / 2; muzzle.position.set(0, 0.05 * s, 1.12 * s); g.add(muzzle);
301
+ const rune = new THREE.Mesh(new THREE.TorusGeometry(0.18 * s, 0.012 * s, 7, 18), glowMat(mat, 3.0));
302
+ rune.rotation.x = Math.PI / 2; rune.position.set(0, 0.05 * s, 0.22 * s); g.add(rune);
303
+ return g;
304
+ }
305
+
306
+ /** Knee/joint guard — curved convex plate with sensor cluster grid */
307
+ function kneeGuard(mat, w, h) {
308
+ const g = new THREE.Group();
309
+ const shape = new THREE.Shape();
310
+ shape.moveTo(-w / 2, -h / 2);
311
+ shape.bezierCurveTo(-w / 2, h * 0.1, 0, h * 0.6, 0, h / 2);
312
+ shape.bezierCurveTo(0, h * 0.6, w / 2, h * 0.1, w / 2, -h / 2);
313
+ shape.lineTo(-w / 2, -h / 2);
314
+ const geo = new THREE.ExtrudeGeometry(shape, { depth: 0.14, bevelEnabled: true, bevelSize: 0.02, bevelThickness: 0.02, bevelSegments: 3 });
315
+ g.add(new THREE.Mesh(geo, mat));
316
+ const fm = frameMat(mat);
317
+ const ridge = new THREE.Mesh(new THREE.BoxGeometry(w * 0.85, 0.018, 0.16), fm);
318
+ ridge.position.set(0, 0, 0.16); g.add(ridge);
319
+ const dotGeo = new THREE.CircleGeometry(0.022, 8);
320
+ const dotMat = glowMat(mat, 3.5);
321
+ for (let i = -1; i <= 1; i++) {
322
+ const d = new THREE.Mesh(dotGeo, dotMat.clone());
323
+ d.position.set(i * w * 0.22, h * 0.05, 0.165); d.rotation.x = -Math.PI / 12; g.add(d);
324
+ }
325
+ return g;
326
+ }
327
+
328
+ /** Boot/foot anchor — thick platform plate with magnetic claw pads */
329
+ function footAnchor(mat, w, h) {
330
+ const g = new THREE.Group();
331
+ const bm = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.82, roughness: 0.18, clearcoat: 0.7 });
332
+ g.add(new THREE.Mesh(new RoundedBoxGeometry(w, h * 0.22, w * 0.65, 2, 0.035), mat));
333
+ const toe = new THREE.Mesh(new RoundedBoxGeometry(w * 0.72, h * 0.15, w * 0.35, 2, 0.025), bm);
334
+ toe.position.set(0, -h * 0.035, w * 0.42); g.add(toe);
335
+ const clawMat = rMat({ color: 0x1a2230 });
336
+ const cGeo = new THREE.SphereGeometry(0.045, 8, 6);
337
+ for (const cx of [-w * 0.3, 0, w * 0.3]) {
338
+ const claw = new THREE.Mesh(cGeo, clawMat);
339
+ claw.position.set(cx, -h * 0.12, w * 0.36); g.add(claw);
340
+ }
341
+ const glow = new THREE.Mesh(new THREE.PlaneGeometry(w * 0.55, w * 0.32), glowMat(mat, 2.2));
342
+ glow.position.set(0, -h * 0.1, w * 0.38); glow.rotation.x = -Math.PI / 2; g.add(glow);
343
+ return g;
225
344
  }
226
345
 
346
+ /* ─────────────────────────────────────────────────────────────
347
+ Region config — centre anchors + primaryCap = 200 total
348
+ ───────────────────────────────────────────────────────────── */
349
+ const REGIONS = {
350
+ Security: { center: [0, 3.5, -0.92], primaryCap: 20, overflowR: 1.2, vSpread: 2.5 },
351
+ Analytics: { center: [0, 7.1, 0.0 ], primaryCap: 24, overflowR: 0.9, vSpread: 1.2 },
352
+ Automation: { center: [-2.1, 3.0, 0.0 ], primaryCap: 16, overflowR: 1.0, vSpread: 2.8 },
353
+ DevTools: { center: [ 2.1, 3.0, 0.0 ], primaryCap: 16, overflowR: 1.0, vSpread: 2.8 },
354
+ NFT: { center: [ 2.3, 5.2, -0.2 ], primaryCap: 14, overflowR: 1.0, vSpread: 1.8 },
355
+ Social: { center: [-2.3, 5.2, -0.2 ], primaryCap: 12, overflowR: 1.0, vSpread: 1.8 },
356
+ Storage: { center: [0, 1.5, -0.78], primaryCap: 12, overflowR: 1.0, vSpread: 1.5 },
357
+ Productivity: { center: [0, 4.2, 0.88], primaryCap: 12, overflowR: 0.9, vSpread: 2.0 },
358
+ Bridge: { center: [-1.2, 5.6, 0.55], primaryCap: 10, overflowR: 0.9, vSpread: 1.4 },
359
+ Trading: { center: [ 1.2, 5.6, 0.55], primaryCap: 10, overflowR: 0.9, vSpread: 1.4 },
360
+ Governance: { center: [0, 6.7, 0.35], primaryCap: 10, overflowR: 0.8, vSpread: 1.0 },
361
+ Wallet: { center: [0, 1.2, 0.62], primaryCap: 10, overflowR: 0.8, vSpread: 1.2 },
362
+ DeFi: { center: [0, -0.6, 0.45], primaryCap: 10, overflowR: 0.9, vSpread: 2.0 },
363
+ Development: { center: [ 2.1, 4.5, 0.22], primaryCap: 12, overflowR: 1.0, vSpread: 2.0 },
364
+ Writing: { center: [-2.1, 2.2, 0.0 ], primaryCap: 10, overflowR: 0.9, vSpread: 1.8 },
365
+ Communication: { center: [0, 7.6, 0.0 ], primaryCap: 12, overflowR: 0.8, vSpread: 1.0 },
366
+ };
367
+
368
+ /* ─────────────────────────────────────────────────────────────
369
+ PRIMARY placement builders — body-welded hardpoints
370
+ ───────────────────────────────────────────────────────────── */
227
371
  const PRIMARY = {
228
- Security(mat, idx, count) {
229
- const cols = Math.ceil(count / 7);
230
- const col = idx % cols;
231
- const row = Math.floor(idx / cols);
232
- const mesh = (idx % 3 === 0) ? turretModule(mat, 0.15) : armorPlate(mat, 0.7, 0.3, 0.1);
233
- mesh.position.set(-0.35 * (cols - 1) / 2 + col * 0.42, 3.0 + row * 0.38, -0.88);
372
+
373
+ /* ── SECURITY (20) ── back torso armour grid 5×4 */
374
+ Security(mat, idx) {
375
+ const col = idx % 4;
376
+ const row = Math.floor(idx / 4);
377
+ let mesh;
378
+ if (idx % 5 === 0) mesh = turretModule(mat, 0.13);
379
+ else if (idx % 7 === 0) mesh = sensorDome(mat, 0.1);
380
+ else mesh = armorPlate(mat, 0.52, 0.22, 0.07);
381
+ mesh.position.set(-0.42 + col * 0.28, 2.0 + row * 0.82, -0.92);
234
382
  return mesh;
235
383
  },
236
- Analytics(mat, idx, count) {
237
- const ring = Math.floor(idx / 8);
238
- const idxInRing = idx % 8;
239
- const ringCount = Math.min(count - ring * 8, 8);
240
- const angle = (idxInRing / ringCount) * Math.PI * 2;
241
- const r = 0.45 + ring * 0.28;
242
- const mesh = (idx % 4 === 0) ? sensorDome(mat, 0.12) : bladeFin(mat, 0.4, 0.15);
243
- mesh.position.set(Math.cos(angle) * r, 7.2 + ring * 0.38, Math.sin(angle) * r);
384
+
385
+ /* ── ANALYTICS (24) ── crown halo — three concentric rings */
386
+ Analytics(mat, idx) {
387
+ const ringIdx = Math.floor(idx / 8);
388
+ const posInRing = idx % 8;
389
+ const ringCount = 8;
390
+ const angle = (posInRing / ringCount) * Math.PI * 2;
391
+ const radii = [0.38, 0.62, 0.9];
392
+ const yOffs = [0.0, 0.22, 0.48];
393
+ const r = radii[ringIdx] || 0.9;
394
+ const y = 7.1 + (yOffs[ringIdx] || 0);
395
+ let mesh;
396
+ if (idx % 4 === 0) mesh = sensorDome(mat, 0.1);
397
+ else if (idx % 6 === 0) mesh = dataSpike(mat, 0.32);
398
+ else mesh = bladeFin(mat, 0.3, 0.12);
399
+ mesh.position.set(Math.cos(angle) * r, y, Math.sin(angle) * r);
244
400
  mesh.rotation.y = -angle;
245
401
  return mesh;
246
402
  },
403
+
404
+ /* ── AUTOMATION (16) ── left arm full column — shoulder → wrist */
247
405
  Automation(mat, idx) {
248
- const row = idx % 6;
249
- const col = Math.floor(idx / 6);
250
- const mesh = hexPlate(mat, 0.2, 0.12);
251
- mesh.position.set(-2.1 - 0.18 - col * 0.48, 2.0 + row * 0.32, 0);
406
+ const col = Math.floor(idx / 8);
407
+ const row = idx % 8;
408
+ let mesh;
409
+ if (idx % 4 === 0) mesh = hexPlate(mat, 0.16, 0.09);
410
+ else if (idx % 7 === 0) mesh = missilePod(mat, 0.1);
411
+ else mesh = armorPlate(mat, 0.1, 0.34, 0.4);
412
+ mesh.position.set(-2.08 - col * 0.22, 1.1 + row * 0.5, 0.05);
252
413
  mesh.rotation.z = Math.PI / 2;
253
414
  return mesh;
254
415
  },
416
+
417
+ /* ── DEVTOOLS (16) ── right arm full column — shoulder → wrist */
255
418
  DevTools(mat, idx) {
256
- const col = idx % 3;
257
- const row = Math.floor(idx / 3);
258
- const mesh = armorPlate(mat, 0.12, 0.4, 0.45);
259
- mesh.position.set(2.1 + 0.18 + col * 0.2, 2.2 + row * 0.48, 0);
419
+ const col = Math.floor(idx / 8);
420
+ const row = idx % 8;
421
+ let mesh;
422
+ if (idx % 4 === 0) mesh = hexPlate(mat, 0.16, 0.09);
423
+ else if (idx % 7 === 0) mesh = dataSpike(mat, 0.36);
424
+ else mesh = armorPlate(mat, 0.1, 0.34, 0.4);
425
+ mesh.position.set(2.08 + col * 0.22, 1.1 + row * 0.5, 0.05);
426
+ mesh.rotation.z = Math.PI / 2;
260
427
  return mesh;
261
428
  },
429
+
430
+ /* ── NFT (14) ── right shoulder cluster — arc + stack */
262
431
  NFT(mat, idx) {
263
- const mesh = energyModule(mat, 0.22);
264
- const angle = (idx / 10) * Math.PI * 1.5 - 0.3;
265
- const r = 0.5 + idx * 0.09;
266
- mesh.position.set(2.0 + Math.cos(angle) * r, 5.5 + Math.sin(angle) * 0.3, -0.3 - idx * 0.13);
432
+ let mesh;
433
+ if (idx % 3 === 0) mesh = reactorCore(mat, 0.18);
434
+ else if (idx % 5 === 0) mesh = shoulderCannon(mat, 0.55);
435
+ else mesh = energyModule(mat, 0.19);
436
+ const arc = Math.floor(idx / 5);
437
+ const posInArc = idx % 5;
438
+ const angle = (posInArc / 5) * Math.PI * 1.1 - 0.3;
439
+ const r = 0.32 + arc * 0.25;
440
+ mesh.position.set(1.95 + Math.cos(angle) * r, 5.1 + arc * 0.38, -0.22 + Math.sin(angle) * 0.28);
267
441
  return mesh;
268
442
  },
443
+
444
+ /* ── SOCIAL (12) ── left shoulder cluster — mirrored arc */
269
445
  Social(mat, idx) {
270
- const angle = (idx / 8) * Math.PI * 2;
271
- const mesh = (idx % 3 === 0) ? sensorDome(mat, 0.1) : hexPlate(mat, 0.15, 0.08);
272
- mesh.position.set(-2.1 + Math.cos(angle) * 0.32, 2.0, Math.sin(angle) * 0.32);
273
- mesh.rotation.x = Math.PI / 2;
446
+ let mesh;
447
+ if (idx % 3 === 0) mesh = sensorDome(mat, 0.11);
448
+ else if (idx % 4 === 0) mesh = hexPlate(mat, 0.14, 0.08);
449
+ else mesh = bladeFin(mat, 0.28, 0.12);
450
+ const arc = Math.floor(idx / 4);
451
+ const posInArc = idx % 4;
452
+ const angle = (posInArc / 4) * Math.PI * 1.1 - 0.2;
453
+ const r = 0.3 + arc * 0.22;
454
+ mesh.position.set(-1.95 - Math.cos(angle) * r, 5.1 + arc * 0.38, -0.2 + Math.sin(angle) * 0.28);
274
455
  return mesh;
275
456
  },
457
+
458
+ /* ── STORAGE (12) ── waist band — full 360° drum pods */
276
459
  Storage(mat, idx) {
460
+ const angle = (idx / 12) * Math.PI * 2;
461
+ const r = 0.72;
462
+ const g = new THREE.Group();
463
+ const bm = mat.clone();
464
+ g.add(new THREE.Mesh(new THREE.CylinderGeometry(0.13, 0.13, 0.38, 8), bm));
465
+ const gm = glowMat(mat, 2.0);
466
+ const ring = new THREE.Mesh(new THREE.TorusGeometry(0.135, 0.018, 7, 14), gm);
467
+ ring.rotation.x = Math.PI / 2; ring.position.y = 0.17; g.add(ring);
468
+ g.position.set(Math.cos(angle) * r, 1.5, Math.sin(angle) * r);
469
+ g.rotation.y = -angle;
470
+ return g;
471
+ },
472
+
473
+ /* ── PRODUCTIVITY (12) ── chest front — 4×3 panel grid */
474
+ Productivity(mat, idx) {
277
475
  const col = idx % 4;
278
476
  const row = Math.floor(idx / 4);
279
- const group = new THREE.Group();
280
- group.add(new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.15, 0.4, 8), mat));
281
- const gMat = mat.clone(); gMat.emissiveIntensity = (gMat.emissiveIntensity || 0.5) * 2;
282
- gMat.transparent = true; gMat.opacity = 0.5;
283
- const ring = new THREE.Mesh(new THREE.TorusGeometry(0.15, 0.02, 6, 12), gMat);
284
- ring.rotation.x = Math.PI / 2; ring.position.y = 0.18;
285
- group.add(ring);
286
- group.position.set(-0.6 + col * 0.42, 3.0 + row * 0.48, -0.92);
287
- return group;
288
- },
289
- Productivity(mat, idx) {
290
- const ring = Math.floor(idx / 3);
291
- const triIdx = idx % 3;
292
- const offsets = [[0, 0.18, 0], [-0.16, -0.09, 0], [0.16, -0.09, 0]];
293
- const [ox, oy] = offsets[triIdx];
294
- const mesh = bladeFin(mat, 0.24, 0.2);
295
- mesh.position.set(ox, 4.5 + oy + ring * 0.48, 0.8 + ring * 0.12);
477
+ let mesh;
478
+ if (idx % 5 === 0) mesh = sensorDome(mat, 0.09);
479
+ else if (idx % 3 === 0) mesh = hexPlate(mat, 0.13, 0.07);
480
+ else mesh = armorPlate(mat, 0.34, 0.18, 0.06);
481
+ mesh.position.set(-0.5 + col * 0.34, 3.05 + row * 0.58, 0.88);
296
482
  return mesh;
297
483
  },
484
+
485
+ /* ── BRIDGE (10) ── left collar/upper chest — curved ramp */
298
486
  Bridge(mat, idx) {
299
- const mesh = energyModule(mat, 0.2);
300
- mesh.position.set(-2.0 - idx * 0.14, 5.5 + idx * 0.32, -0.2);
487
+ let mesh;
488
+ if (idx % 3 === 0) mesh = energyModule(mat, 0.17);
489
+ else if (idx % 4 === 0) mesh = thrusterNozzle(mat, 0.13);
490
+ else mesh = armorPlate(mat, 0.38, 0.16, 0.08);
491
+ const t = idx / 9;
492
+ mesh.position.set(-0.6 - t * 0.55, 5.6 - t * 0.55, 0.55 - t * 0.18);
493
+ mesh.rotation.y = t * 0.3;
301
494
  return mesh;
302
495
  },
496
+
497
+ /* ── TRADING (10) ── right collar/upper chest — mirrored ramp */
303
498
  Trading(mat, idx) {
304
- const col = idx % 2;
305
- const row = Math.floor(idx / 2);
306
- const mesh = armorPlate(mat, 0.4, 0.25, 0.06);
307
- mesh.position.set(-0.55 + col * 0.48, -0.6 + row * 0.32, 0.42);
308
- mesh.rotation.x = -0.2;
499
+ let mesh;
500
+ if (idx % 3 === 0) mesh = energyModule(mat, 0.17);
501
+ else if (idx % 4 === 0) mesh = missilePod(mat, 0.12);
502
+ else mesh = armorPlate(mat, 0.38, 0.16, 0.08);
503
+ const t = idx / 9;
504
+ mesh.position.set(0.6 + t * 0.55, 5.6 - t * 0.55, 0.55 - t * 0.18);
505
+ mesh.rotation.y = -t * 0.3;
309
506
  return mesh;
310
507
  },
508
+
509
+ /* ── GOVERNANCE (10) ── head sides + crown band */
311
510
  Governance(mat, idx) {
312
- const mesh = armorPlate(mat, 0.45, 0.06, 0.04);
313
- mesh.position.set(0, 6.7 - idx * 0.12, 0.5);
511
+ let mesh;
512
+ if (idx === 0) mesh = shoulderCannon(mat, 0.42);
513
+ else if (idx % 3 === 0) mesh = sensorDome(mat, 0.08);
514
+ else mesh = armorPlate(mat, 0.28, 0.09, 0.06);
515
+ const side = idx % 2 === 0 ? 1 : -1;
516
+ const row = Math.floor(idx / 2);
517
+ mesh.position.set(side * 0.52, 6.7 - row * 0.19, 0.3 + row * 0.04);
314
518
  return mesh;
315
519
  },
520
+
521
+ /* ── WALLET (10) ── hip-belt band — evenly spaced on front 180° */
316
522
  Wallet(mat, idx) {
317
- const angle = (idx / 6) * Math.PI * 2;
318
- const r = idx === 0 ? 0 : 0.28;
319
- const mesh = hexPlate(mat, 0.14, 0.1);
320
- mesh.position.set(Math.cos(angle) * r, 2.35, 0.55 + Math.sin(angle) * r);
523
+ const angle = ((idx / 9) - 0.5) * Math.PI;
524
+ const r = 0.58;
525
+ let mesh;
526
+ if (idx === 0) mesh = reactorCore(mat, 0.14);
527
+ else if (idx % 3 === 0) mesh = hexPlate(mat, 0.12, 0.07);
528
+ else mesh = armorPlate(mat, 0.3, 0.14, 0.07);
529
+ mesh.position.set(Math.cos(angle) * r, 1.2, 0.62 + Math.sin(angle) * r * 0.5);
530
+ mesh.rotation.y = -angle;
321
531
  return mesh;
322
532
  },
533
+
534
+ /* ── DEFI (10) ── thigh + shin armour — bilateral leg panels */
323
535
  DeFi(mat, idx) {
324
- const col = idx % 2;
325
- const row = Math.floor(idx / 2);
326
- const mesh = armorPlate(mat, 0.4, 0.25, 0.06);
327
- mesh.position.set(0.55 + col * 0.48, -0.6 + row * 0.32, 0.42);
328
- mesh.rotation.x = -0.2;
536
+ const side = idx % 2 === 0 ? -1 : 1;
537
+ const stack = Math.floor(idx / 2);
538
+ let mesh;
539
+ if (stack === 0) mesh = kneeGuard(mat, 0.38, 0.28);
540
+ else if (idx % 3 === 0) mesh = armorPlate(mat, 0.34, 0.2, 0.07);
541
+ else mesh = hexPlate(mat, 0.14, 0.08);
542
+ mesh.position.set(side * 0.56, 0.22 - stack * 0.55, 0.44);
329
543
  return mesh;
330
544
  },
545
+
546
+ /* ── DEVELOPMENT (12) ── right torso side — vertical stack */
331
547
  Development(mat, idx) {
332
- const col = idx % 2;
333
- const row = Math.floor(idx / 2);
334
- const mesh = armorPlate(mat, 0.14, 0.45, 0.5);
335
- mesh.position.set(2.1 + 0.18 + col * 0.2, 3.8 + row * 0.52, 0.2);
548
+ const row = idx % 6;
549
+ const col = Math.floor(idx / 6);
550
+ let mesh;
551
+ if (idx % 4 === 0) mesh = dataSpike(mat, 0.4);
552
+ else if (idx % 3 === 0) mesh = hexPlate(mat, 0.14, 0.08);
553
+ else mesh = armorPlate(mat, 0.12, 0.42, 0.48);
554
+ mesh.position.set(2.08 + col * 0.2, 2.5 + row * 0.52, 0.22);
336
555
  return mesh;
337
556
  },
557
+
558
+ /* ── WRITING (10) ── left forearm bracers — 2 cols × 5 rows */
338
559
  Writing(mat, idx) {
339
- const angle = (idx / 6) * Math.PI * 2;
340
- const r = 0.32;
341
- const mesh = hexPlate(mat, 0.12, 0.06);
342
- mesh.position.set(2.1 + Math.cos(angle) * r, 2.0, 0.2 + Math.sin(angle) * r);
343
- mesh.rotation.x = Math.PI / 2;
560
+ const col = idx % 2;
561
+ const row = Math.floor(idx / 2);
562
+ let mesh;
563
+ if (idx % 4 === 0) mesh = bladeFin(mat, 0.26, 0.1);
564
+ else mesh = armorPlate(mat, 0.1, 0.3, 0.38);
565
+ mesh.position.set(-2.08 - col * 0.18, 1.2 + row * 0.52, 0.0);
566
+ mesh.rotation.z = Math.PI / 2;
344
567
  return mesh;
345
568
  },
569
+
570
+ /* ── COMMUNICATION (12) ── antenna array above head */
346
571
  Communication(mat, idx) {
347
- const mesh = (idx === 0) ? sensorDome(mat, 0.15) : bladeFin(mat, 0.5 + idx * 0.1, 0.1);
348
- mesh.position.set(0, 7.5 + idx * 0.42, 0);
349
- return mesh;
572
+ if (idx === 0) {
573
+ const d = sensorDome(mat, 0.16);
574
+ d.position.set(0, 7.55, 0);
575
+ return d;
576
+ }
577
+ if (idx % 3 === 0) {
578
+ const rc = reactorCore(mat, 0.12);
579
+ rc.position.set(0, 7.55 + idx * 0.38, 0);
580
+ return rc;
581
+ }
582
+ const angle = ((idx - 1) / 11) * Math.PI * 2;
583
+ const r = 0.22 + (idx % 3) * 0.12;
584
+ const spike = dataSpike(mat, 0.38 + idx * 0.06);
585
+ spike.position.set(Math.cos(angle) * r, 7.6 + idx * 0.3, Math.sin(angle) * r);
586
+ return spike;
350
587
  },
351
588
  };
352
589
 
353
- /* ══════════════════════════════════════════════════════════
354
- Overflow builder energy module at orbital position
355
- ══════════════════════════════════════════════════════════ */
356
- function buildOverflow(mat, overflowIdx, region) {
357
- const shapes = [energyModule, hexPlate, sensorDome];
358
- const pick = overflowIdx % shapes.length;
359
- let mesh;
360
- if (pick === 0) mesh = energyModule(mat, 0.12);
361
- else if (pick === 1) { mesh = hexPlate(mat, 0.1, 0.06); mesh.rotation.x = Math.PI / 2; }
362
- else mesh = sensorDome(mat, 0.08);
590
+ /* ─────────────────────────────────────────────────────────────
591
+ Overflow: snap to nearest body-surface slab for this region
592
+ Uses a deterministic grid around the region anchor, NOT open
593
+ orbital space parts stay tightly against the robot body.
594
+ ───────────────────────────────────────────────────────────── */
595
+
596
+ /* Surface-normal hint for snapping overflow to body faces */
597
+ const REGION_SURFACE_NORMAL = {
598
+ Security: new THREE.Vector3( 0, 0, -1),
599
+ Analytics: new THREE.Vector3( 0, 1, 0),
600
+ Automation: new THREE.Vector3(-1, 0, 0),
601
+ DevTools: new THREE.Vector3( 1, 0, 0),
602
+ NFT: new THREE.Vector3( 1, 0.3, 0),
603
+ Social: new THREE.Vector3(-1, 0.3, 0),
604
+ Storage: new THREE.Vector3( 0, -0.2,-1),
605
+ Productivity: new THREE.Vector3( 0, 0, 1),
606
+ Bridge: new THREE.Vector3(-0.7,0.3, 0.5),
607
+ Trading: new THREE.Vector3( 0.7,0.3, 0.5),
608
+ Governance: new THREE.Vector3( 0, 0.6, 0.5),
609
+ Wallet: new THREE.Vector3( 0, 0, 1),
610
+ DeFi: new THREE.Vector3( 0, 0, 1),
611
+ Development: new THREE.Vector3( 1, 0, 0),
612
+ Writing: new THREE.Vector3(-1, 0, 0),
613
+ Communication: new THREE.Vector3( 0, 1, 0),
614
+ };
615
+
616
+ function buildOverflow(mat, overflowIdx, cat) {
617
+ const region = REGIONS[cat] || REGIONS.Security;
618
+ const norm = (REGION_SURFACE_NORMAL[cat] || REGION_SURFACE_NORMAL.Security).clone().normalize();
363
619
  const center = new THREE.Vector3(...region.center);
364
- const pos = overflowPosition(overflowIdx, center, region.overflowR, region.vSpread);
365
- mesh.position.copy(pos);
620
+
621
+ /* Tile overflow parts in a tight grid on the body surface */
622
+ const gridW = 4;
623
+ const gCol = overflowIdx % gridW;
624
+ const gRow = Math.floor(overflowIdx / gridW);
625
+ const step = 0.28;
626
+
627
+ /* Two tangent axes perpendicular to surface normal */
628
+ const up = Math.abs(norm.y) < 0.9 ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(1, 0, 0);
629
+ const tan1 = new THREE.Vector3().crossVectors(norm, up).normalize();
630
+ const tan2 = new THREE.Vector3().crossVectors(norm, tan1).normalize();
631
+
632
+ const offset = new THREE.Vector3()
633
+ .addScaledVector(tan1, (gCol - (gridW - 1) / 2) * step)
634
+ .addScaledVector(tan2, gRow * step);
635
+
636
+ /* Small compact parts for overflow */
637
+ const shapes = [
638
+ () => hexPlate(mat, 0.09, 0.05),
639
+ () => { const m = energyModule(mat, 0.09); m.rotation.x = Math.PI / 2; return m; },
640
+ () => armorPlate(mat, 0.2, 0.12, 0.04),
641
+ () => dataSpike(mat, 0.22),
642
+ () => sensorDome(mat, 0.07),
643
+ ];
644
+ const mesh = shapes[overflowIdx % shapes.length]();
645
+ mesh.position.copy(center).add(offset);
366
646
  return mesh;
367
647
  }
368
648
 
369
- /* ══════════════════════════════════════════════════════════
370
- Unified builder: dispatches to PRIMARY or overflow
371
- ══════════════════════════════════════════════════════════ */
649
+ /* ─────────────────────────────────────────────────────────────
650
+ Central dispatcher
651
+ ───────────────────────────────────────────────────────────── */
372
652
  function buildOne(cat, mat, idx, count) {
373
653
  const region = REGIONS[cat];
374
- if (!region) {
375
- return buildOverflow(mat, idx, REGIONS.Security);
376
- }
654
+ if (!region) return buildOverflow(mat, idx, "Security");
377
655
  const cap = region.primaryCap;
378
656
  if (idx < cap && PRIMARY[cat]) {
379
657
  return PRIMARY[cat](mat, idx, Math.min(count, cap));
380
658
  }
381
- return buildOverflow(mat, idx - cap, region);
659
+ return buildOverflow(mat, idx - cap, cat);
382
660
  }
383
661
 
384
- /* ══════════════════════════════════════════════════════════
385
- Build all attachments from skill list
386
- ══════════════════════════════════════════════════════════ */
662
+ /* ─────────────────────────────────────────────────────────────
663
+ Public API — build attachments from live skill list
664
+ ───────────────────────────────────────────────────────────── */
387
665
  export function buildAttachmentsFromSkills(skills, parentGroup) {
388
666
  const byCategory = {};
389
667
  skills.forEach(s => {
@@ -396,34 +674,34 @@ export function buildAttachmentsFromSkills(skills, parentGroup) {
396
674
 
397
675
  for (const [cat, catSkills] of Object.entries(byCategory)) {
398
676
  catSkills.forEach((skill, idx) => {
399
- const mat = matFor(cat);
677
+ const mat = matFor(cat);
400
678
  const built = buildOne(cat, mat, idx, catSkills.length);
401
679
  const group = new THREE.Group();
402
680
 
403
681
  group.userData = {
404
- skillSlug: skill.slug,
405
- skillName: skill.name,
406
- category: cat,
407
- description: skill.description,
408
- onchain: skill.onchain || !!skill.onchainTokenId,
409
- vettedOk: skill.vetted_ok || skill.vettedOk,
410
- risk_tier: skill.risk_tier || skill.riskTier,
682
+ skillSlug: skill.slug,
683
+ skillName: skill.name,
684
+ category: cat,
685
+ description: skill.description,
686
+ onchain: skill.onchain || !!skill.onchainTokenId,
687
+ vettedOk: skill.vetted_ok || skill.vettedOk,
688
+ risk_tier: skill.risk_tier || skill.riskTier,
411
689
  documentation_md: skill.documentation_md,
412
- fileName: skill.fileName,
413
- source: skill.source,
414
- categoryIndex: idx,
415
- isOverflow: idx >= (REGIONS[cat]?.primaryCap || 0),
690
+ fileName: skill.fileName,
691
+ source: skill.source,
692
+ categoryIndex: idx,
693
+ isOverflow: idx >= (REGIONS[cat]?.primaryCap || 0),
416
694
  };
417
695
 
696
+ /* Copy sub-meshes + world position from the builder group */
418
697
  if (built.isGroup) {
419
698
  built.children.forEach(c => group.add(c.clone()));
420
699
  group.position.copy(built.position);
700
+ group.rotation.copy(built.rotation);
421
701
  } else {
422
702
  group.add(built);
423
- if (built.position) {
424
- group.position.copy(built.position);
425
- built.position.set(0, 0, 0);
426
- }
703
+ if (built.position) { group.position.copy(built.position); built.position.set(0, 0, 0); }
704
+ if (built.rotation) { group.rotation.copy(built.rotation); built.rotation.set(0, 0, 0); }
427
705
  }
428
706
 
429
707
  parentGroup.add(group);
@@ -434,34 +712,28 @@ export function buildAttachmentsFromSkills(skills, parentGroup) {
434
712
  return attachments;
435
713
  }
436
714
 
437
- /* ══════════════════════════════════════════════════════════
438
- Clear and rebuild — used after install/uninstall
439
- ══════════════════════════════════════════════════════════ */
715
+ /* ─────────────────────────────────────────────────────────────
716
+ Clear helpers
717
+ ───────────────────────────────────────────────────────────── */
440
718
  export function clearAttachments(attachmentGroup, energyNetworkGroup) {
441
- while (attachmentGroup.children.length) {
442
- const child = attachmentGroup.children[0];
443
- child.traverse(c => {
444
- if (c.geometry) c.geometry.dispose();
445
- if (c.material) {
446
- if (Array.isArray(c.material)) c.material.forEach(m => m.dispose());
447
- else c.material.dispose();
448
- }
449
- });
450
- attachmentGroup.remove(child);
451
- }
452
- while (energyNetworkGroup.children.length) {
453
- const child = energyNetworkGroup.children[0];
454
- child.traverse(c => {
455
- if (c.geometry) c.geometry.dispose();
456
- if (c.material) c.material.dispose();
457
- });
458
- energyNetworkGroup.remove(child);
719
+ for (const grp of [attachmentGroup, energyNetworkGroup]) {
720
+ while (grp.children.length) {
721
+ const child = grp.children[0];
722
+ child.traverse(c => {
723
+ if (c.geometry) c.geometry.dispose();
724
+ if (c.material) {
725
+ if (Array.isArray(c.material)) c.material.forEach(m => m.dispose());
726
+ else c.material.dispose();
727
+ }
728
+ });
729
+ grp.remove(child);
730
+ }
459
731
  }
460
732
  }
461
733
 
462
- /* ══════════════════════════════════════════════════════════
463
- Energy network — lines connecting same-category attachments
464
- ══════════════════════════════════════════════════════════ */
734
+ /* ─────────────────────────────────────────────────────────────
735
+ Energy network — low-opacity lines linking category clusters
736
+ ───────────────────────────────────────────────────────────── */
465
737
  export function buildEnergyNetwork(attachments, parentGroup) {
466
738
  const byCategory = {};
467
739
  attachments.forEach(a => {
@@ -470,140 +742,107 @@ export function buildEnergyNetwork(attachments, parentGroup) {
470
742
  byCategory[cat].push(a);
471
743
  });
472
744
 
473
- const corePos = new THREE.Vector3(0, 4.5, 0.7);
745
+ const corePos = new THREE.Vector3(0, 4.2, 0.65);
474
746
 
475
747
  for (const [cat, atts] of Object.entries(byCategory)) {
476
- const color = CATEGORY_COLORS_HEX[cat] || 0xcccccc;
477
- const mat = new THREE.LineBasicMaterial({
478
- color, transparent: true, opacity: 0.15,
748
+ const color = CATEGORY_COLORS_HEX[cat] || 0xaaaaaa;
749
+ const lineMat = new THREE.LineBasicMaterial({
750
+ color, transparent: true, opacity: 0.11,
479
751
  blending: THREE.AdditiveBlending,
480
752
  });
481
753
 
482
- const maxLines = Math.min(atts.length - 1, 60);
483
- for (let i = 0; i < maxLines; i++) {
754
+ const maxChain = Math.min(atts.length - 1, 80);
755
+ for (let i = 0; i < maxChain; i++) {
484
756
  const geo = new THREE.BufferGeometry().setFromPoints([atts[i].position, atts[i + 1].position]);
485
- const line = new THREE.Line(geo, mat.clone());
757
+ const line = new THREE.Line(geo, lineMat.clone());
486
758
  line.userData.category = cat;
487
759
  parentGroup.add(line);
488
760
  }
489
761
 
490
762
  if (atts.length > 0) {
491
- const hubMat = mat.clone();
492
- hubMat.opacity = 0.1;
763
+ const hubMat = lineMat.clone(); hubMat.opacity = 0.07;
493
764
  const geo = new THREE.BufferGeometry().setFromPoints([atts[0].position, corePos]);
494
- const line = new THREE.Line(geo, hubMat);
495
- line.userData.category = cat;
496
- parentGroup.add(line);
765
+ parentGroup.add(new THREE.Line(geo, hubMat));
497
766
  }
498
767
  }
499
768
  }
500
769
 
501
- /* ══════════════════════════════════════════════════════════
502
- Idle animation factories (per-category)
503
- ══════════════════════════════════════════════════════════ */
770
+ /* ─────────────────────────────────────────────────────────────
771
+ Per-category idle animations
772
+ ───────────────────────────────────────────────────────────── */
504
773
  const IDLE = {
505
774
  Security(att, time, idx) {
506
- att.traverse(c => {
507
- if (c.isMesh) c.material.emissiveIntensity = 0.6 + 1.0 * Math.max(0, Math.sin(time * 2 + idx * 0.4));
508
- });
775
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.5 + 0.9 * Math.max(0, Math.sin(time * 2.1 + idx * 0.38)); });
509
776
  },
510
777
  Analytics(att, time, idx) {
511
778
  att.rotation.y += 0.003;
512
- att.traverse(c => {
513
- if (c.isMesh) c.material.emissiveIntensity = 0.8 + 0.6 * Math.sin(time * 3 + idx);
514
- });
779
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.7 + 0.6 * Math.sin(time * 3.2 + idx); });
515
780
  },
516
781
  Automation(att, time, idx) {
517
- att.traverse(c => {
518
- if (c.isMesh) c.material.emissiveIntensity = 0.6 + 0.8 * Math.max(0, Math.sin(time * 3 - idx * 1.2));
519
- });
782
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.5 + 0.85 * Math.max(0, Math.sin(time * 2.8 - idx * 1.1)); });
520
783
  },
521
- DevTools(att, time) {
522
- att.traverse(c => {
523
- if (c.isMesh) c.material.emissiveIntensity = 0.8 + 0.6 * Math.sin(time * 4);
524
- });
784
+ DevTools(att, time, idx) {
785
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.7 + 0.7 * Math.sin(time * 4.0 + idx * 0.2); });
525
786
  },
526
787
  NFT(att, time) {
527
- att.rotation.y += 0.008;
528
- att.traverse(c => {
529
- if (c.isMesh) c.material.emissiveIntensity = 1.0 + 0.8 * Math.sin(time * 1.5);
530
- });
788
+ att.rotation.y += 0.009;
789
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.9 + 0.85 * Math.abs(Math.sin(time * 1.4)); });
531
790
  },
532
- Social(att, time) {
533
- att.rotation.y += 0.01;
534
- att.traverse(c => {
535
- if (c.isMesh) c.material.emissiveIntensity = 0.8 + 0.4 * Math.sin(time * 2);
536
- });
791
+ Social(att, time, idx) {
792
+ att.rotation.y += 0.011;
793
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.7 + 0.45 * Math.sin(time * 2.2 + idx * 0.5); });
537
794
  },
538
- Storage(att, time) {
539
- att.traverse(c => {
540
- if (c.isMesh) c.material.emissiveIntensity = 0.5 + 0.5 * Math.sin(time * 1.2);
541
- });
795
+ Storage(att, time, idx) {
796
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.4 + 0.55 * Math.sin(time * 1.1 + idx * 0.6); });
542
797
  },
543
- Productivity(att, time) {
544
- att.rotation.z += 0.005;
545
- att.traverse(c => {
546
- if (c.isMesh) c.material.emissiveIntensity = 0.6 + 0.4 * Math.sin(time * 2.5);
547
- });
798
+ Productivity(att, time, idx) {
799
+ att.rotation.z += 0.004;
800
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.55 + 0.45 * Math.sin(time * 2.6 + idx * 0.4); });
548
801
  },
549
- Bridge(att, time) {
550
- att.rotation.y += 0.006;
551
- att.traverse(c => {
552
- if (c.isMesh) c.material.emissiveIntensity = 0.8 + 0.5 * Math.sin(time * 2.5);
553
- });
802
+ Bridge(att, time, idx) {
803
+ att.rotation.y += 0.007;
804
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.7 + 0.55 * Math.sin(time * 2.4 + idx * 0.55); });
554
805
  },
555
806
  Trading(att, time) {
556
807
  att.traverse(c => {
557
- if (c.isMesh) c.material.emissiveIntensity = 0.6 + 1.0 * (Math.random() > 0.9 ? 1 : 0.2);
808
+ if (c.isMesh) c.material.emissiveIntensity = 0.6 + 1.1 * (Math.random() > 0.92 ? 1 : 0.15);
558
809
  });
559
810
  },
560
- Governance(att, time) {
561
- att.traverse(c => {
562
- if (c.isMesh) c.material.emissiveIntensity = 0.8 + 0.4 * Math.sin(time * 1.5);
563
- });
811
+ Governance(att, time, idx) {
812
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.7 + 0.5 * Math.sin(time * 1.6 + idx * 0.7); });
564
813
  },
565
- Wallet(att, time) {
566
- att.traverse(c => {
567
- if (c.isMesh) c.material.emissiveIntensity = 0.5 + 0.7 * (0.5 + 0.5 * Math.sin(time * 4.2));
568
- });
814
+ Wallet(att, time, idx) {
815
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.45 + 0.75 * (0.5 + 0.5 * Math.sin(time * 4.4 + idx * 0.5)); });
569
816
  },
570
- DeFi(att, time) {
571
- att.traverse(c => {
572
- if (c.isMesh) c.material.emissiveIntensity = 0.6 + 0.5 * Math.sin(time * 2);
573
- });
817
+ DeFi(att, time, idx) {
818
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.55 + 0.55 * Math.sin(time * 2.1 + idx * 0.6); });
574
819
  },
575
- Development(att, time) {
576
- att.traverse(c => {
577
- if (c.isMesh) c.material.emissiveIntensity = 0.8 + 0.6 * Math.sin(time * 3);
578
- });
820
+ Development(att, time, idx) {
821
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.7 + 0.65 * Math.sin(time * 3.1 + idx * 0.35); });
579
822
  },
580
- Writing(att, time) {
581
- att.traverse(c => {
582
- if (c.isMesh) c.material.emissiveIntensity = 0.5 + 0.3 * Math.sin(time * 1.8);
583
- });
823
+ Writing(att, time, idx) {
824
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.45 + 0.35 * Math.sin(time * 1.9 + idx * 0.45); });
584
825
  },
585
- Communication(att, time) {
586
- att.traverse(c => {
587
- if (c.isMesh) c.material.emissiveIntensity = 0.8 + 0.7 * Math.abs(Math.sin(time * 3));
588
- });
826
+ Communication(att, time, idx) {
827
+ att.rotation.y += 0.005;
828
+ att.traverse(c => { if (c.isMesh) c.material.emissiveIntensity = 0.7 + 0.75 * Math.abs(Math.sin(time * 3.0 + idx * 0.3)); });
589
829
  },
590
830
  };
591
831
 
592
832
  function overflowIdle(att, time, idx) {
593
- att.rotation.y += 0.004;
833
+ att.rotation.y += 0.003;
594
834
  att.traverse(c => {
595
- if (c.isMesh) c.material.emissiveIntensity = 0.6 + 0.6 * Math.sin(time * 1.5 + idx * 0.3);
835
+ if (c.isMesh) c.material.emissiveIntensity = 0.5 + 0.55 * Math.sin(time * 1.6 + idx * 0.32);
596
836
  });
597
837
  }
598
838
 
599
839
  export function makeIdleAnimator(attachments) {
600
840
  return function idleAnimator(time) {
601
841
  for (const att of attachments) {
602
- const cat = att.userData.category;
603
842
  if (att.userData.isOverflow) {
604
843
  overflowIdle(att, time, att.userData.categoryIndex);
605
844
  } else {
606
- const fn = IDLE[cat];
845
+ const fn = IDLE[att.userData.category];
607
846
  if (fn) fn(att, time, att.userData.categoryIndex);
608
847
  }
609
848
  }