ai-agent-session-center 2.0.2 → 2.0.3

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.
Files changed (58) hide show
  1. package/README.md +484 -429
  2. package/docs/3D/ADAPTATION_GUIDE.md +592 -0
  3. package/docs/3D/index.html +754 -0
  4. package/docs/AGENT_TEAM_TASKS.md +716 -0
  5. package/docs/CYBERDROME_V2_SPEC.md +531 -0
  6. package/docs/ERROR_185_ANALYSIS.md +263 -0
  7. package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
  8. package/docs/SESSION_DETAIL_FEATURES.md +98 -0
  9. package/docs/_3d_multimedia_features.md +1080 -0
  10. package/docs/_frontend_features.md +1057 -0
  11. package/docs/_server_features.md +1077 -0
  12. package/docs/session-duplication-fixes.md +271 -0
  13. package/docs/session-terminal-linkage.md +412 -0
  14. package/package.json +63 -5
  15. package/public/apple-touch-icon.svg +21 -0
  16. package/public/css/dashboard.css +0 -161
  17. package/public/css/detail-panel.css +25 -0
  18. package/public/css/layout.css +18 -1
  19. package/public/css/modals.css +0 -26
  20. package/public/css/settings.css +0 -150
  21. package/public/css/terminal.css +34 -0
  22. package/public/favicon.svg +18 -0
  23. package/public/index.html +6 -26
  24. package/public/js/alarmManager.js +0 -21
  25. package/public/js/app.js +21 -7
  26. package/public/js/detailPanel.js +63 -64
  27. package/public/js/historyPanel.js +61 -55
  28. package/public/js/quickActions.js +132 -48
  29. package/public/js/sessionCard.js +5 -20
  30. package/public/js/sessionControls.js +8 -0
  31. package/public/js/settingsManager.js +0 -142
  32. package/server/apiRouter.js +60 -15
  33. package/server/apiRouter.ts +774 -0
  34. package/server/approvalDetector.ts +94 -0
  35. package/server/authManager.ts +144 -0
  36. package/server/autoIdleManager.ts +110 -0
  37. package/server/config.ts +121 -0
  38. package/server/constants.ts +150 -0
  39. package/server/db.ts +475 -0
  40. package/server/hookInstaller.d.ts +3 -0
  41. package/server/hookProcessor.ts +108 -0
  42. package/server/hookRouter.ts +18 -0
  43. package/server/hookStats.ts +116 -0
  44. package/server/index.js +15 -1
  45. package/server/index.ts +230 -0
  46. package/server/logger.ts +75 -0
  47. package/server/mqReader.ts +349 -0
  48. package/server/portManager.ts +55 -0
  49. package/server/processMonitor.ts +239 -0
  50. package/server/serverConfig.ts +29 -0
  51. package/server/sessionMatcher.js +17 -6
  52. package/server/sessionMatcher.ts +403 -0
  53. package/server/sessionStore.js +109 -3
  54. package/server/sessionStore.ts +1145 -0
  55. package/server/sshManager.js +167 -24
  56. package/server/sshManager.ts +671 -0
  57. package/server/teamManager.ts +289 -0
  58. package/server/wsManager.ts +200 -0
@@ -0,0 +1,754 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CYBERDROME — Robotic Office Lab</title>
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
8
+ <link rel="apple-touch-icon" href="/apple-touch-icon.svg">
9
+ <meta name="theme-color" content="#0e0c1a">
10
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
11
+ <style>
12
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
13
+ body { overflow: hidden; background: #0e0c1a; font-family: 'Share Tech Mono', monospace; }
14
+ canvas { display: block; }
15
+
16
+ #ui {
17
+ position: fixed; inset: 0;
18
+ pointer-events: none; z-index: 10;
19
+ }
20
+ #ui::before {
21
+ content: ''; position: fixed; inset: 0; z-index: 100; pointer-events: none;
22
+ background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px);
23
+ }
24
+ #ui::after {
25
+ content: ''; position: fixed; inset: 0; pointer-events: none;
26
+ background: radial-gradient(ellipse at center, transparent 60%, rgba(6,4,14,0.4) 100%);
27
+ }
28
+
29
+ .title {
30
+ position: absolute; top: 24px; left: 28px; user-select: none;
31
+ animation: fadeIn 0.8s ease 0.2s both;
32
+ }
33
+ .title h1 {
34
+ font-family: 'Orbitron', sans-serif; font-weight: 900; font-size: 15px;
35
+ letter-spacing: 5px; color: #00f0ff; text-transform: uppercase;
36
+ text-shadow: 0 0 10px rgba(0,240,255,0.5), 0 0 30px rgba(0,240,255,0.12);
37
+ animation: flicker 6s ease-in-out infinite;
38
+ }
39
+ .title p {
40
+ font-size: 10px; letter-spacing: 3px; margin-top: 5px;
41
+ color: rgba(255,0,170,0.45); text-transform: uppercase;
42
+ text-shadow: 0 0 6px rgba(255,0,170,0.25);
43
+ }
44
+
45
+ .panel {
46
+ position: absolute; bottom: 24px; right: 28px;
47
+ background: rgba(10,6,22,0.78); backdrop-filter: blur(20px);
48
+ -webkit-backdrop-filter: blur(20px);
49
+ border: 1px solid rgba(0,240,255,0.18); border-radius: 4px;
50
+ padding: 20px 24px; pointer-events: all; min-width: 200px;
51
+ box-shadow: 0 0 12px rgba(0,240,255,0.04), inset 0 0 24px rgba(0,240,255,0.02);
52
+ animation: fadeIn 0.8s ease 0.4s both;
53
+ }
54
+ .panel::before {
55
+ content: ''; position: absolute; top: -1px; left: -1px;
56
+ width: 14px; height: 14px; border-top: 2px solid #00f0ff; border-left: 2px solid #00f0ff;
57
+ }
58
+ .panel::after {
59
+ content: ''; position: absolute; bottom: -1px; right: -1px;
60
+ width: 14px; height: 14px; border-bottom: 2px solid #ff00aa; border-right: 2px solid #ff00aa;
61
+ }
62
+
63
+ .stat-label { font-size: 10px; letter-spacing: 2px; color: rgba(0,240,255,0.4); text-transform: uppercase; }
64
+ .stat-value { font-family: 'Orbitron', sans-serif; font-size: 34px; font-weight: 700; color: #fff; margin: 3px 0 14px; line-height: 1; }
65
+ .btn {
66
+ display: block; width: 100%; font-family: 'Share Tech Mono', monospace;
67
+ font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase;
68
+ padding: 9px 14px; border-radius: 2px; cursor: pointer;
69
+ transition: all 0.15s ease; border: 1px solid;
70
+ }
71
+ .btn-primary { background: rgba(0,240,255,0.08); color: #00f0ff; border-color: rgba(0,240,255,0.28); }
72
+ .btn-primary:hover { background: rgba(0,240,255,0.16); border-color: rgba(0,240,255,0.5); box-shadow: 0 0 16px rgba(0,240,255,0.12); }
73
+ .btn-ghost { background: rgba(255,0,170,0.04); color: rgba(255,0,170,0.45); border-color: rgba(255,0,170,0.14); margin-top: 7px; }
74
+ .btn-ghost:hover { background: rgba(255,0,170,0.1); color: rgba(255,0,170,0.7); border-color: rgba(255,0,170,0.3); }
75
+ .btn:active { transform: scale(0.97); }
76
+ .hint { font-size: 9px; color: rgba(255,255,255,0.16); margin-top: 12px; line-height: 1.6; text-align: center; }
77
+ .hint kbd { display: inline-block; padding: 1px 4px; border: 1px solid rgba(0,240,255,0.18); border-radius: 2px; font-size: 9px; font-family: 'Share Tech Mono', monospace; color: rgba(0,240,255,0.3); }
78
+
79
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
80
+ @keyframes flicker { 0%,100%{opacity:1} 91%{opacity:1} 92%{opacity:.7} 93%{opacity:1} 95%{opacity:.85} 96.5%{opacity:1} }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ <div id="ui">
85
+ <div class="title">
86
+ <h1>Cyberdrome</h1>
87
+ <p>Robotic Office Lab</p>
88
+ </div>
89
+ <div class="panel">
90
+ <div class="stat-label">Units Online</div>
91
+ <div class="stat-value" id="count">0</div>
92
+ <button class="btn btn-primary" id="addBtn">&gt; Deploy Unit</button>
93
+ <button class="btn btn-ghost" id="resetBtn">// Reset Lab</button>
94
+ <div class="hint">Drag to orbit &middot; Scroll to zoom<br><kbd>Space</kbd> deploy &middot; <kbd>R</kbd> reset</div>
95
+ </div>
96
+ </div>
97
+
98
+ <script type="importmap">
99
+ { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } }
100
+ </script>
101
+
102
+ <script type="module">
103
+ import * as THREE from 'three'
104
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
105
+
106
+ // ═══════════════════════════════════════════════════
107
+ // CONSTANTS
108
+ // ═══════════════════════════════════════════════════
109
+ const GS = 30, BOUND = 13.5, WALL_H = 2.8, WALL_T = 0.12
110
+ const INITIAL = 10, MAX = 50
111
+ const PALETTE = [
112
+ '#00f0ff','#ff00aa','#a855f7','#00ff88',
113
+ '#ff4444','#ffaa00','#00aaff','#ff66ff',
114
+ '#44ff44','#ff8800','#8855ff','#00ffcc',
115
+ '#ff0066','#ccff00','#ff5577','#33ddff',
116
+ ]
117
+ const S_WALK = 0, S_GOTO = 1, S_SIT = 2
118
+
119
+ // Room bounds: NW=0, NE=1, SW=2, SE=3
120
+ const roomBounds = [
121
+ { minX:-13, maxX:-3, minZ:-13, maxZ:-3 },
122
+ { minX:3, maxX:13, minZ:-13, maxZ:-3 },
123
+ { minX:-13, maxX:-3, minZ:3, maxZ:13 },
124
+ { minX:3, maxX:13, minZ:3, maxZ:13 },
125
+ ]
126
+ // Doorway center positions (inner corner of each room)
127
+ const doorways = [
128
+ new THREE.Vector3(-4.5, 0, -4.5),
129
+ new THREE.Vector3(4.5, 0, -4.5),
130
+ new THREE.Vector3(-4.5, 0, 4.5),
131
+ new THREE.Vector3(4.5, 0, 4.5),
132
+ ]
133
+
134
+ // ═══════════════════════════════════════════════════
135
+ // RENDERER + SCENE
136
+ // ═══════════════════════════════════════════════════
137
+ const renderer = new THREE.WebGLRenderer({ antialias: true })
138
+ renderer.setSize(window.innerWidth, window.innerHeight)
139
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
140
+ renderer.shadowMap.enabled = true
141
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap
142
+ renderer.toneMapping = THREE.ACESFilmicToneMapping
143
+ renderer.toneMappingExposure = 1.2
144
+ renderer.outputColorSpace = THREE.SRGBColorSpace
145
+ document.body.appendChild(renderer.domElement)
146
+
147
+ const scene = new THREE.Scene()
148
+ scene.background = new THREE.Color('#0e0c1a')
149
+ scene.fog = new THREE.FogExp2('#0e0c1a', 0.015)
150
+
151
+ const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 150)
152
+ camera.position.set(18, 16, 18)
153
+
154
+ const controls = new OrbitControls(camera, renderer.domElement)
155
+ controls.enableDamping = true
156
+ controls.dampingFactor = 0.06
157
+ controls.maxPolarAngle = Math.PI / 2.1
158
+ controls.minDistance = 6
159
+ controls.maxDistance = 50
160
+ controls.target.set(0, 1, 0)
161
+
162
+ // ═══════════════════════════════════════════════════
163
+ // LIGHTING — brighter cyberpunk
164
+ // ═══════════════════════════════════════════════════
165
+ scene.add(new THREE.AmbientLight('#2a2040', 3))
166
+
167
+ const sun = new THREE.DirectionalLight('#c8b8e0', 2)
168
+ sun.position.set(8, 20, 6)
169
+ sun.castShadow = true
170
+ Object.assign(sun.shadow.camera, { left:-18, right:18, top:18, bottom:-18, near:1, far:50 })
171
+ sun.shadow.mapSize.set(2048, 2048)
172
+ sun.shadow.bias = -0.0004
173
+ sun.shadow.normalBias = 0.02
174
+ scene.add(sun)
175
+
176
+ const cyLight = new THREE.PointLight('#00f0ff', 4, 40, 1.5)
177
+ cyLight.position.set(-10, 8, -10)
178
+ scene.add(cyLight)
179
+ const mgLight = new THREE.PointLight('#ff00aa', 3, 40, 1.5)
180
+ mgLight.position.set(10, 7, 10)
181
+ scene.add(mgLight)
182
+ scene.add(new THREE.HemisphereLight('#2a1a3e', '#0a080e', 1))
183
+
184
+ // ═══════════════════════════════════════════════════
185
+ // GROUND
186
+ // ═══════════════════════════════════════════════════
187
+ const floorMat = new THREE.MeshStandardMaterial({ color: '#1a1830', roughness: 0.7, metalness: 0.3 })
188
+ const floor = new THREE.Mesh(new THREE.PlaneGeometry(GS, GS), floorMat)
189
+ floor.rotation.x = -Math.PI / 2
190
+ floor.receiveShadow = true
191
+ scene.add(floor)
192
+
193
+ // Room floor panels (slightly lighter)
194
+ const rmFloorGeo = new THREE.PlaneGeometry(10, 10)
195
+ const rmFloorMat = new THREE.MeshStandardMaterial({ color: '#201e38', roughness: 0.6, metalness: 0.3 })
196
+ for (const rb of roomBounds) {
197
+ const rf = new THREE.Mesh(rmFloorGeo, rmFloorMat)
198
+ rf.rotation.x = -Math.PI / 2
199
+ rf.position.set((rb.minX + rb.maxX) / 2, 0.003, (rb.minZ + rb.maxZ) / 2)
200
+ rf.receiveShadow = true
201
+ scene.add(rf)
202
+ }
203
+
204
+ // Grids
205
+ const g1 = new THREE.GridHelper(GS, 30, '#00f0ff', '#00f0ff')
206
+ g1.material.opacity = 0.06; g1.material.transparent = true; g1.position.y = 0.005
207
+ scene.add(g1)
208
+ const g2 = new THREE.GridHelper(GS, 6, '#ff00aa', '#ff00aa')
209
+ g2.material.opacity = 0.05; g2.material.transparent = true; g2.position.y = 0.008
210
+ scene.add(g2)
211
+
212
+ // Circuit traces
213
+ const traceColors = ['#00f0ff', '#ff00aa', '#a855f7']
214
+ for (let i = 0; i < 14; i++) {
215
+ const pts = []
216
+ let cx = (Math.random() - 0.5) * GS * 0.7, cz = (Math.random() - 0.5) * GS * 0.7
217
+ pts.push(new THREE.Vector3(cx, 0.011, cz))
218
+ for (let j = 0, segs = 3 + Math.floor(Math.random() * 4); j < segs; j++) {
219
+ const len = 1 + Math.random() * 3
220
+ if (j % 2 === 0) cx += (Math.random() < 0.5 ? -1 : 1) * len
221
+ else cz += (Math.random() < 0.5 ? -1 : 1) * len
222
+ cx = THREE.MathUtils.clamp(cx, -14, 14)
223
+ cz = THREE.MathUtils.clamp(cz, -14, 14)
224
+ pts.push(new THREE.Vector3(cx, 0.011, cz))
225
+ }
226
+ scene.add(new THREE.Line(
227
+ new THREE.BufferGeometry().setFromPoints(pts),
228
+ new THREE.LineBasicMaterial({ color: traceColors[i % 3], transparent: true, opacity: 0.08 + Math.random() * 0.08 })
229
+ ))
230
+ }
231
+
232
+ // ═══════════════════════════════════════════════════
233
+ // WALLS + COLLISION
234
+ // ═══════════════════════════════════════════════════
235
+ const wallRects = []
236
+ const wallMat = new THREE.MeshStandardMaterial({
237
+ color: '#282845', roughness: 0.2, metalness: 0.7,
238
+ transparent: true, opacity: 0.55, side: THREE.DoubleSide,
239
+ })
240
+ const cyStripMat = new THREE.MeshStandardMaterial({ color: '#00f0ff', emissive: '#00f0ff', emissiveIntensity: 2, roughness: 0.2 })
241
+ const mgStripMat = new THREE.MeshStandardMaterial({ color: '#ff00aa', emissive: '#ff00aa', emissiveIntensity: 2, roughness: 0.2 })
242
+
243
+ // Wall segments: [type, fixedCoord, from, to, stripColor]
244
+ // type: 'h' = horizontal (along x, z=fixed), 'v' = vertical (along z, x=fixed)
245
+ const wallDefs = [
246
+ // NW room (zone 0) — cyan strips
247
+ ['h', -13, -13, -3, 0], ['v', -13, -13, -3, 0],
248
+ ['h', -3, -13, -6, 0], ['v', -3, -13, -6, 0],
249
+ // NE room (zone 1) — magenta strips
250
+ ['h', -13, 3, 13, 1], ['v', 13, -13, -3, 1],
251
+ ['h', -3, 6, 13, 1], ['v', 3, -13, -6, 1],
252
+ // SW room (zone 2) — magenta strips
253
+ ['h', 13, -13, -3, 1], ['v', -13, 3, 13, 1],
254
+ ['h', 3, -13, -6, 1], ['v', -3, 6, 13, 1],
255
+ // SE room (zone 3) — cyan strips
256
+ ['h', 13, 3, 13, 0], ['v', 13, 3, 13, 0],
257
+ ['h', 3, 6, 13, 0], ['v', 3, 6, 13, 0],
258
+ ]
259
+
260
+ for (const [type, fixed, from, to, col] of wallDefs) {
261
+ const len = to - from
262
+ if (len <= 0) continue
263
+ const stripMat = col === 0 ? cyStripMat : mgStripMat
264
+ const mid = (from + to) / 2
265
+
266
+ if (type === 'h') {
267
+ const w = new THREE.Mesh(new THREE.BoxGeometry(len, WALL_H, WALL_T), wallMat)
268
+ w.position.set(mid, WALL_H / 2, fixed)
269
+ w.castShadow = true; w.receiveShadow = true; scene.add(w)
270
+ const s = new THREE.Mesh(new THREE.BoxGeometry(len, 0.04, WALL_T + 0.06), stripMat)
271
+ s.position.set(mid, WALL_H, fixed); scene.add(s)
272
+ wallRects.push({ minX: from, maxX: to, minZ: fixed - 0.25, maxZ: fixed + 0.25 })
273
+ } else {
274
+ const w = new THREE.Mesh(new THREE.BoxGeometry(WALL_T, WALL_H, len), wallMat)
275
+ w.position.set(fixed, WALL_H / 2, mid)
276
+ w.castShadow = true; w.receiveShadow = true; scene.add(w)
277
+ const s = new THREE.Mesh(new THREE.BoxGeometry(WALL_T + 0.06, 0.04, len), stripMat)
278
+ s.position.set(fixed, WALL_H, mid); scene.add(s)
279
+ wallRects.push({ minX: fixed - 0.25, maxX: fixed + 0.25, minZ: from, maxZ: to })
280
+ }
281
+ }
282
+
283
+ function collidesWall(x, z) {
284
+ for (const w of wallRects) {
285
+ if (x + 0.25 > w.minX && x - 0.25 < w.maxX && z + 0.25 > w.minZ && z - 0.25 < w.maxZ) return true
286
+ }
287
+ return false
288
+ }
289
+ function getZone(x, z) {
290
+ if (x < -3 && z < -3) return 0
291
+ if (x > 3 && z < -3) return 1
292
+ if (x < -3 && z > 3) return 2
293
+ if (x > 3 && z > 3) return 3
294
+ return -1
295
+ }
296
+
297
+ // ═══════════════════════════════════════════════════
298
+ // FURNITURE
299
+ // ═══════════════════════════════════════════════════
300
+ const deskMat = new THREE.MeshStandardMaterial({ color: '#1c1c30', roughness: 0.5, metalness: 0.6 })
301
+ const monFrameMat = new THREE.MeshStandardMaterial({ color: '#111120', roughness: 0.3, metalness: 0.8 })
302
+ const chairMat = new THREE.MeshStandardMaterial({ color: '#222238', roughness: 0.55, metalness: 0.5 })
303
+
304
+ // Shared furniture geometries
305
+ const fGeo = {
306
+ dTop: new THREE.BoxGeometry(1.5, 0.05, 0.65),
307
+ dPanel: new THREE.BoxGeometry(0.04, 0.66, 0.58),
308
+ monF: new THREE.BoxGeometry(0.48, 0.32, 0.025),
309
+ monS: new THREE.BoxGeometry(0.44, 0.28, 0.005),
310
+ kb: new THREE.BoxGeometry(0.32, 0.012, 0.1),
311
+ cSeat: new THREE.BoxGeometry(0.36, 0.03, 0.36),
312
+ cBack: new THREE.BoxGeometry(0.34, 0.28, 0.03),
313
+ cPed: new THREE.CylinderGeometry(0.025, 0.025, 0.36, 6),
314
+ cBase: new THREE.CylinderGeometry(0.16, 0.16, 0.025, 6),
315
+ }
316
+
317
+ const workstations = []
318
+
319
+ // Desk definitions: [x, z, rotation, zone]
320
+ const deskDefs = [
321
+ // NW room
322
+ [-10, -11.5, 0, 0], [-6, -11.5, 0, 0], [-11.5, -7, Math.PI / 2, 0],
323
+ // NE room
324
+ [6, -11.5, 0, 1], [10, -11.5, 0, 1], [11.5, -7, -Math.PI / 2, 1],
325
+ // SW room
326
+ [-10, 11.5, Math.PI, 2], [-6, 11.5, Math.PI, 2], [-11.5, 7, Math.PI / 2, 2],
327
+ // SE room
328
+ [6, 11.5, Math.PI, 3], [10, 11.5, Math.PI, 3], [11.5, 7, -Math.PI / 2, 3],
329
+ ]
330
+
331
+ for (let di = 0; di < deskDefs.length; di++) {
332
+ const [dx, dz, drot, zone] = deskDefs[di]
333
+ const g = new THREE.Group()
334
+
335
+ // Tabletop
336
+ const top = new THREE.Mesh(fGeo.dTop, deskMat)
337
+ top.position.y = 0.7; top.castShadow = true; top.receiveShadow = true; g.add(top)
338
+
339
+ // Side panels
340
+ for (const sx of [-0.72, 0.72]) {
341
+ const p = new THREE.Mesh(fGeo.dPanel, deskMat)
342
+ p.position.set(sx, 0.35, 0); p.castShadow = true; g.add(p)
343
+ }
344
+
345
+ // Monitor
346
+ const mf = new THREE.Mesh(fGeo.monF, monFrameMat)
347
+ mf.position.set(0, 0.92, -0.2); g.add(mf)
348
+ const sColor = PALETTE[(di * 3 + 1) % PALETTE.length]
349
+ const sMat = new THREE.MeshStandardMaterial({
350
+ color: sColor, emissive: sColor, emissiveIntensity: 0.6, roughness: 0.3,
351
+ })
352
+ const ms = new THREE.Mesh(fGeo.monS, sMat)
353
+ ms.position.set(0, 0.92, -0.185); g.add(ms)
354
+
355
+ // Keyboard
356
+ const kb = new THREE.Mesh(fGeo.kb, deskMat)
357
+ kb.position.set(0, 0.72, 0.12); g.add(kb)
358
+
359
+ g.position.set(dx, 0, dz)
360
+ g.rotation.y = drot
361
+ scene.add(g)
362
+
363
+ // Chair at seat position
364
+ const seatX = dx + 0.65 * Math.sin(drot)
365
+ const seatZ = dz + 0.65 * Math.cos(drot)
366
+ const faceRot = drot + Math.PI
367
+
368
+ const cg = new THREE.Group()
369
+ const seat = new THREE.Mesh(fGeo.cSeat, chairMat); seat.position.y = 0.4; cg.add(seat)
370
+ const back = new THREE.Mesh(fGeo.cBack, chairMat); back.position.set(0, 0.57, -0.155); cg.add(back)
371
+ const ped = new THREE.Mesh(fGeo.cPed, chairMat); ped.position.y = 0.19; cg.add(ped)
372
+ const base = new THREE.Mesh(fGeo.cBase, chairMat); base.position.y = 0.013; cg.add(base)
373
+ cg.position.set(seatX, 0, seatZ)
374
+ cg.rotation.y = faceRot
375
+ scene.add(cg)
376
+
377
+ workstations.push({
378
+ idx: di, zone,
379
+ seatPos: new THREE.Vector3(seatX, 0, seatZ),
380
+ faceRot,
381
+ occupant: null,
382
+ })
383
+ }
384
+
385
+ // ═══════════════════════════════════════════════════
386
+ // ENVIRONMENT
387
+ // ═══════════════════════════════════════════════════
388
+
389
+ // Central hologram
390
+ const holoMat = new THREE.MeshBasicMaterial({ color: '#00f0ff', wireframe: true, transparent: true, opacity: 0.1 })
391
+ const holo = new THREE.Mesh(new THREE.CylinderGeometry(1.2, 1.2, 3, 6, 1, true), holoMat)
392
+ holo.position.y = 1.5; scene.add(holo)
393
+ const holo2 = new THREE.Mesh(new THREE.CylinderGeometry(0.8, 0.8, 2, 6, 1, true),
394
+ new THREE.MeshBasicMaterial({ color: '#ff00aa', wireframe: true, transparent: true, opacity: 0.07 }))
395
+ holo2.position.y = 1.5; scene.add(holo2)
396
+
397
+ // Data particles
398
+ function makeStream(n, color) {
399
+ const pos = new Float32Array(n * 3), spd = new Float32Array(n)
400
+ for (let i = 0; i < n; i++) {
401
+ pos[i*3] = (Math.random()-0.5)*GS; pos[i*3+1] = Math.random()*10; pos[i*3+2] = (Math.random()-0.5)*GS
402
+ spd[i] = 0.2 + Math.random() * 0.6
403
+ }
404
+ const geo = new THREE.BufferGeometry()
405
+ geo.setAttribute('position', new THREE.BufferAttribute(pos, 3))
406
+ return { mesh: new THREE.Points(geo, new THREE.PointsMaterial({ color, size: 0.04, transparent: true, opacity: 0.4, sizeAttenuation: true })), pos, spd, n }
407
+ }
408
+ const sCy = makeStream(140, '#00f0ff'), sMg = makeStream(80, '#ff00aa')
409
+ scene.add(sCy.mesh); scene.add(sMg.mesh)
410
+
411
+ function tickStream(s, dt) {
412
+ for (let i = 0; i < s.n; i++) {
413
+ s.pos[i*3+1] += s.spd[i] * dt
414
+ if (s.pos[i*3+1] > 10) { s.pos[i*3+1] = 0; s.pos[i*3] = (Math.random()-0.5)*GS; s.pos[i*3+2] = (Math.random()-0.5)*GS }
415
+ }
416
+ s.mesh.geometry.attributes.position.needsUpdate = true
417
+ }
418
+
419
+ // Stars
420
+ const stN = 400, stP = new Float32Array(stN * 3)
421
+ for (let i = 0; i < stN; i++) { stP[i*3] = (Math.random()-0.5)*100; stP[i*3+1] = Math.random()*35+8; stP[i*3+2] = (Math.random()-0.5)*100 }
422
+ const stGeo = new THREE.BufferGeometry(); stGeo.setAttribute('position', new THREE.BufferAttribute(stP, 3))
423
+ scene.add(new THREE.Points(stGeo, new THREE.PointsMaterial({ color: '#bbaaee', size: 0.05, transparent: true, opacity: 0.4, sizeAttenuation: true })))
424
+
425
+ // ═══════════════════════════════════════════════════
426
+ // SHARED ROBOT GEOMETRY + MATERIALS
427
+ // ═══════════════════════════════════════════════════
428
+ const rGeo = {
429
+ head: new THREE.BoxGeometry(0.28, 0.24, 0.26),
430
+ visor: new THREE.BoxGeometry(0.24, 0.065, 0.02),
431
+ antenna: new THREE.CylinderGeometry(0.007, 0.007, 0.14, 4),
432
+ aTip: new THREE.SphereGeometry(0.02, 6, 6),
433
+ torso: new THREE.BoxGeometry(0.32, 0.38, 0.2),
434
+ core: new THREE.SphereGeometry(0.032, 8, 8),
435
+ joint: new THREE.SphereGeometry(0.035, 8, 8),
436
+ arm: new THREE.BoxGeometry(0.08, 0.26, 0.08),
437
+ leg: new THREE.BoxGeometry(0.09, 0.28, 0.09),
438
+ foot: new THREE.BoxGeometry(0.1, 0.045, 0.12),
439
+ }
440
+ const rEdge = {
441
+ head: new THREE.EdgesGeometry(rGeo.head),
442
+ torso: new THREE.EdgesGeometry(rGeo.torso),
443
+ arm: new THREE.EdgesGeometry(rGeo.arm),
444
+ leg: new THREE.EdgesGeometry(rGeo.leg),
445
+ }
446
+
447
+ // Material pools (shared per color)
448
+ const metalMat = new THREE.MeshStandardMaterial({ color: '#2a2a3e', roughness: 0.3, metalness: 0.85 })
449
+ const darkMat = new THREE.MeshStandardMaterial({ color: '#1c1c2c', roughness: 0.4, metalness: 0.7 })
450
+ const neonMats = PALETTE.map(h => { const c = new THREE.Color(h); return new THREE.MeshStandardMaterial({ color: c, emissive: c, emissiveIntensity: 2, roughness: 0.2, metalness: 0.3 }) })
451
+ const edgeMats = PALETTE.map(h => new THREE.LineBasicMaterial({ color: h, transparent: true, opacity: 0.3 }))
452
+
453
+ // Glow disc texture
454
+ const glowCvs = document.createElement('canvas'); glowCvs.width = 64; glowCvs.height = 64
455
+ const gx = glowCvs.getContext('2d'), gg = gx.createRadialGradient(32,32,0,32,32,32)
456
+ gg.addColorStop(0,'rgba(255,255,255,0.3)'); gg.addColorStop(0.5,'rgba(255,255,255,0.08)'); gg.addColorStop(1,'rgba(255,255,255,0)')
457
+ gx.fillStyle = gg; gx.fillRect(0,0,64,64)
458
+ const glowTex = new THREE.CanvasTexture(glowCvs)
459
+ const glowMats = PALETTE.map(h => new THREE.MeshBasicMaterial({ map: glowTex, color: h, transparent: true, opacity: 0.3, depthWrite: false, blending: THREE.AdditiveBlending }))
460
+
461
+ // ═══════════════════════════════════════════════════
462
+ // ROBOT CLASS
463
+ // ═══════════════════════════════════════════════════
464
+ class Robot {
465
+ constructor(ci, x, z) {
466
+ this.group = new THREE.Group()
467
+ this.ci = ci
468
+ this.speed = 1.0 + Math.random() * 0.7
469
+ this.target = new THREE.Vector3()
470
+ this.state = S_WALK
471
+ this.deskIdx = -1
472
+ this.sitTimer = 0
473
+ this.decisionTimer = 2 + Math.random() * 4
474
+ this.phase = Math.random() * Math.PI * 2
475
+ this.walkHz = 7 + Math.random() * 2
476
+
477
+ const s = 0.85 + Math.random() * 0.2
478
+ this.group.scale.setScalar(s)
479
+
480
+ const nm = neonMats[ci], em = edgeMats[ci]
481
+
482
+ // Head
483
+ const head = new THREE.Mesh(rGeo.head, metalMat); head.position.y = 1.32; head.castShadow = true; this.group.add(head)
484
+ const headE = new THREE.LineSegments(rEdge.head, em); headE.position.copy(head.position); this.group.add(headE)
485
+ const visor = new THREE.Mesh(rGeo.visor, nm); visor.position.set(0, 1.32, 0.13); this.group.add(visor)
486
+ const ant = new THREE.Mesh(rGeo.antenna, darkMat); ant.position.set(0.05, 1.52, 0); this.group.add(ant)
487
+ this.aTip = new THREE.Mesh(rGeo.aTip, nm); this.aTip.position.set(0.05, 1.6, 0); this.group.add(this.aTip)
488
+
489
+ // Torso
490
+ const torso = new THREE.Mesh(rGeo.torso, metalMat); torso.position.y = 0.87; torso.castShadow = true; this.group.add(torso)
491
+ this.bodyMesh = torso
492
+ this.bodyEdge = new THREE.LineSegments(rEdge.torso, em); this.bodyEdge.position.copy(torso.position)
493
+ this.group.add(this.bodyEdge)
494
+ this.core = new THREE.Mesh(rGeo.core, nm); this.core.position.set(0, 0.91, 0.105); this.group.add(this.core)
495
+
496
+ // Shoulders
497
+ for (const sx of [-1, 1]) { const j = new THREE.Mesh(rGeo.joint, nm); j.position.set(sx * 0.21, 1.07, 0); this.group.add(j) }
498
+
499
+ // Arms
500
+ this.armPivots = []
501
+ for (const sx of [-1, 1]) {
502
+ const pv = new THREE.Group(); pv.position.set(sx * 0.21, 1.07, 0)
503
+ const arm = new THREE.Mesh(rGeo.arm, darkMat); arm.position.y = -0.18; arm.castShadow = true; pv.add(arm)
504
+ const armE = new THREE.LineSegments(rEdge.arm, em); armE.position.copy(arm.position); pv.add(armE)
505
+ this.group.add(pv); this.armPivots.push(pv)
506
+ }
507
+
508
+ // Hips
509
+ for (const sx of [-1, 1]) { const j = new THREE.Mesh(rGeo.joint, nm); j.position.set(sx * 0.09, 0.54, 0); j.scale.setScalar(0.9); this.group.add(j) }
510
+
511
+ // Legs
512
+ this.legPivots = []
513
+ for (const sx of [-1, 1]) {
514
+ const pv = new THREE.Group(); pv.position.set(sx * 0.09, 0.54, 0)
515
+ const leg = new THREE.Mesh(rGeo.leg, darkMat); leg.position.y = -0.19; leg.castShadow = true; pv.add(leg)
516
+ const legE = new THREE.LineSegments(rEdge.leg, em); legE.position.copy(leg.position); pv.add(legE)
517
+ const ft = new THREE.Mesh(rGeo.foot, metalMat); ft.position.set(0, -0.36, 0.012); ft.castShadow = true; pv.add(ft)
518
+ this.group.add(pv); this.legPivots.push(pv)
519
+ }
520
+
521
+ // Ground glow
522
+ this.glow = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 1.4), glowMats[ci])
523
+ this.glow.rotation.x = -Math.PI / 2; this.glow.position.y = 0.01
524
+ this.group.add(this.glow)
525
+
526
+ this.group.position.set(x, 0, z)
527
+ this.group.rotation.y = Math.random() * Math.PI * 2
528
+ this._pickTarget()
529
+ }
530
+
531
+ // ── Target picking (zone-aware) ──
532
+ _pickTarget() {
533
+ const zone = getZone(this.group.position.x, this.group.position.z)
534
+ if (zone >= 0) {
535
+ const rb = roomBounds[zone]
536
+ if (Math.random() < 0.65) {
537
+ this.target.set(
538
+ rb.minX + 1.5 + Math.random() * (rb.maxX - rb.minX - 3), 0,
539
+ rb.minZ + 1.5 + Math.random() * (rb.maxZ - rb.minZ - 3),
540
+ )
541
+ } else {
542
+ this.target.copy(doorways[zone])
543
+ this.target.x += (Math.random() - 0.5) * 1.5
544
+ this.target.z += (Math.random() - 0.5) * 1.5
545
+ }
546
+ } else {
547
+ if (Math.random() < 0.4) {
548
+ if (Math.random() < 0.5) this.target.set((Math.random()-0.5)*4, 0, (Math.random()-0.5)*22)
549
+ else this.target.set((Math.random()-0.5)*22, 0, (Math.random()-0.5)*4)
550
+ } else {
551
+ const ri = Math.floor(Math.random() * 4)
552
+ this.target.copy(doorways[ri])
553
+ this.target.x += (Math.random()-0.5) * 1.5
554
+ this.target.z += (Math.random()-0.5) * 1.5
555
+ }
556
+ }
557
+ }
558
+
559
+ // ── Sit at desk ──
560
+ seatAt(wsIdx) {
561
+ const ws = workstations[wsIdx]
562
+ ws.occupant = this
563
+ this.deskIdx = wsIdx
564
+ this.state = S_SIT
565
+ this.sitTimer = 8 + Math.random() * 18
566
+ this.group.position.set(ws.seatPos.x, -0.12, ws.seatPos.z)
567
+ this.group.rotation.y = ws.faceRot
568
+ this.legPivots[0].rotation.x = 1.2
569
+ this.legPivots[1].rotation.x = 1.2
570
+ this.armPivots[0].rotation.x = -0.5
571
+ this.armPivots[1].rotation.x = -0.5
572
+ this.glow.position.y = 0.13
573
+ }
574
+
575
+ _standUp() {
576
+ if (this.deskIdx >= 0) workstations[this.deskIdx].occupant = null
577
+ this.deskIdx = -1
578
+ this.state = S_WALK
579
+ this.group.position.y = 0
580
+ this.glow.position.y = 0.01
581
+ this.decisionTimer = 4 + Math.random() * 6
582
+ this._pickTarget()
583
+ }
584
+
585
+ // ── Update ──
586
+ update(dt, t) {
587
+ // Antenna + core pulse
588
+ this.aTip.scale.setScalar(0.8 + ((Math.sin(t * 6 + this.phase) + 1) * 0.5) * 0.4)
589
+ this.core.scale.setScalar(0.9 + Math.sin(t * 3 + this.phase) * 0.12)
590
+
591
+ if (this.state === S_SIT) {
592
+ this.sitTimer -= dt
593
+ if (this.sitTimer <= 0) { this._standUp(); return }
594
+ const ty = Math.sin(t * 10 + this.phase) * 0.05
595
+ this.armPivots[0].rotation.x = -0.5 + ty
596
+ this.armPivots[1].rotation.x = -0.5 - ty
597
+ this.bodyMesh.rotation.z = Math.sin(t * 0.7 + this.phase) * 0.012
598
+ this.bodyEdge.rotation.z = this.bodyMesh.rotation.z
599
+ return
600
+ }
601
+
602
+ // Movement (WALK or GOTO)
603
+ const dx = this.target.x - this.group.position.x
604
+ const dz = this.target.z - this.group.position.z
605
+ const dist = Math.sqrt(dx * dx + dz * dz)
606
+
607
+ if (dist < 0.5) {
608
+ if (this.state === S_GOTO) { this.seatAt(this.deskIdx); return }
609
+ this._pickTarget()
610
+ return
611
+ }
612
+
613
+ const want = Math.atan2(dx, dz)
614
+ let diff = want - this.group.rotation.y
615
+ if (diff > Math.PI) diff -= Math.PI * 2
616
+ if (diff < -Math.PI) diff += Math.PI * 2
617
+ this.group.rotation.y += diff * Math.min(1, 5 * dt)
618
+
619
+ const step = this.speed * dt
620
+ const nx = this.group.position.x + Math.sin(this.group.rotation.y) * step
621
+ const nz = this.group.position.z + Math.cos(this.group.rotation.y) * step
622
+
623
+ if (!collidesWall(nx, nz)) {
624
+ this.group.position.x = nx; this.group.position.z = nz
625
+ } else if (!collidesWall(nx, this.group.position.z)) {
626
+ this.group.position.x = nx; if (this.state === S_WALK) this._pickTarget()
627
+ } else if (!collidesWall(this.group.position.x, nz)) {
628
+ this.group.position.z = nz; if (this.state === S_WALK) this._pickTarget()
629
+ } else {
630
+ if (this.state === S_WALK) this._pickTarget()
631
+ }
632
+
633
+ this.group.position.x = THREE.MathUtils.clamp(this.group.position.x, -BOUND, BOUND)
634
+ this.group.position.z = THREE.MathUtils.clamp(this.group.position.z, -BOUND, BOUND)
635
+
636
+ // Walk animation
637
+ const w = t * this.walkHz + this.phase
638
+ this.legPivots[0].rotation.x = Math.sin(w) * 0.4
639
+ this.legPivots[1].rotation.x = -Math.sin(w) * 0.4
640
+ this.armPivots[0].rotation.x = -Math.sin(w) * 0.25
641
+ this.armPivots[1].rotation.x = Math.sin(w) * 0.25
642
+ this.group.position.y = Math.abs(Math.sin(w * 2)) * 0.03
643
+ this.bodyMesh.rotation.z = Math.sin(w) * 0.016
644
+ this.bodyEdge.rotation.z = this.bodyMesh.rotation.z
645
+
646
+ // Decision to sit (only while walking)
647
+ if (this.state === S_WALK) {
648
+ this.decisionTimer -= dt
649
+ if (this.decisionTimer <= 0) {
650
+ this.decisionTimer = 3 + Math.random() * 5
651
+ const zone = getZone(this.group.position.x, this.group.position.z)
652
+ if (zone >= 0 && Math.random() < 0.5) {
653
+ const empty = workstations.filter(ws => ws.zone === zone && !ws.occupant)
654
+ if (empty.length > 0) {
655
+ const ws = empty[Math.floor(Math.random() * empty.length)]
656
+ ws.occupant = this
657
+ this.deskIdx = ws.idx
658
+ this.state = S_GOTO
659
+ this.target.copy(ws.seatPos)
660
+ }
661
+ }
662
+ }
663
+ }
664
+ }
665
+
666
+ dispose() {
667
+ if (this.deskIdx >= 0) workstations[this.deskIdx].occupant = null
668
+ this.group.traverse(c => { if (c.isMesh && !Object.values(neonMats).includes(c.material) && c.material !== metalMat && c.material !== darkMat) c.material.dispose() })
669
+ }
670
+ }
671
+
672
+ // ═══════════════════════════════════════════════════
673
+ // STATE MANAGEMENT
674
+ // ═══════════════════════════════════════════════════
675
+ const robots = []
676
+ let colorIdx = 0
677
+ const countEl = document.getElementById('count')
678
+
679
+ function deploy() {
680
+ if (robots.length >= MAX) return
681
+ // Spawn in corridor
682
+ let sx, sz
683
+ if (Math.random() < 0.5) { sx = (Math.random()-0.5)*4; sz = (Math.random()-0.5)*20 }
684
+ else { sx = (Math.random()-0.5)*20; sz = (Math.random()-0.5)*4 }
685
+ const r = new Robot(colorIdx++ % PALETTE.length, sx, sz)
686
+ robots.push(r); scene.add(r.group)
687
+ countEl.textContent = robots.length
688
+ }
689
+
690
+ function deployAtDesk(wsIdx) {
691
+ if (robots.length >= MAX) return
692
+ const r = new Robot(colorIdx++ % PALETTE.length, 0, 0)
693
+ r.seatAt(wsIdx)
694
+ robots.push(r); scene.add(r.group)
695
+ countEl.textContent = robots.length
696
+ }
697
+
698
+ function reset() {
699
+ for (const r of robots) { scene.remove(r.group); r.dispose() }
700
+ robots.length = 0; colorIdx = 0
701
+ for (const ws of workstations) ws.occupant = null
702
+ initPopulation()
703
+ }
704
+
705
+ function initPopulation() {
706
+ // Seat some robots at desks first
707
+ const deskOrder = [...Array(workstations.length).keys()].sort(() => Math.random() - 0.5)
708
+ const seatCount = Math.min(6, INITIAL, workstations.length)
709
+ for (let i = 0; i < seatCount; i++) deployAtDesk(deskOrder[i])
710
+ // Rest in corridors
711
+ for (let i = seatCount; i < INITIAL; i++) deploy()
712
+ }
713
+ initPopulation()
714
+
715
+ // ═══════════════════════════════════════════════════
716
+ // EVENTS
717
+ // ═══════════════════════════════════════════════════
718
+ document.getElementById('addBtn').addEventListener('click', deploy)
719
+ document.getElementById('resetBtn').addEventListener('click', reset)
720
+ window.addEventListener('keydown', e => {
721
+ if (e.target !== document.body) return
722
+ if (e.code === 'Space') { e.preventDefault(); deploy() }
723
+ if (e.code === 'KeyR') reset()
724
+ })
725
+ window.addEventListener('resize', () => {
726
+ camera.aspect = window.innerWidth / window.innerHeight
727
+ camera.updateProjectionMatrix()
728
+ renderer.setSize(window.innerWidth, window.innerHeight)
729
+ })
730
+
731
+ // ═══════════════════════════════════════════════════
732
+ // ANIMATION LOOP
733
+ // ═══════════════════════════════════════════════════
734
+ const clock = new THREE.Clock()
735
+ ;(function tick() {
736
+ requestAnimationFrame(tick)
737
+ const dt = Math.min(clock.getDelta(), 0.1)
738
+ const t = clock.elapsedTime
739
+
740
+ for (const r of robots) r.update(dt, t)
741
+ tickStream(sCy, dt); tickStream(sMg, dt)
742
+
743
+ // Hologram rotation
744
+ holo.rotation.y += dt * 0.2
745
+ holo2.rotation.y -= dt * 0.15
746
+ holo.position.y = 1.5 + Math.sin(t * 0.5) * 0.15
747
+ holo2.position.y = 1.5 + Math.sin(t * 0.5 + 1) * 0.1
748
+
749
+ controls.update()
750
+ renderer.render(scene, camera)
751
+ })()
752
+ </script>
753
+ </body>
754
+ </html>