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.
- package/README.md +484 -429
- package/docs/3D/ADAPTATION_GUIDE.md +592 -0
- package/docs/3D/index.html +754 -0
- package/docs/AGENT_TEAM_TASKS.md +716 -0
- package/docs/CYBERDROME_V2_SPEC.md +531 -0
- package/docs/ERROR_185_ANALYSIS.md +263 -0
- package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
- package/docs/SESSION_DETAIL_FEATURES.md +98 -0
- package/docs/_3d_multimedia_features.md +1080 -0
- package/docs/_frontend_features.md +1057 -0
- package/docs/_server_features.md +1077 -0
- package/docs/session-duplication-fixes.md +271 -0
- package/docs/session-terminal-linkage.md +412 -0
- package/package.json +63 -5
- package/public/apple-touch-icon.svg +21 -0
- package/public/css/dashboard.css +0 -161
- package/public/css/detail-panel.css +25 -0
- package/public/css/layout.css +18 -1
- package/public/css/modals.css +0 -26
- package/public/css/settings.css +0 -150
- package/public/css/terminal.css +34 -0
- package/public/favicon.svg +18 -0
- package/public/index.html +6 -26
- package/public/js/alarmManager.js +0 -21
- package/public/js/app.js +21 -7
- package/public/js/detailPanel.js +63 -64
- package/public/js/historyPanel.js +61 -55
- package/public/js/quickActions.js +132 -48
- package/public/js/sessionCard.js +5 -20
- package/public/js/sessionControls.js +8 -0
- package/public/js/settingsManager.js +0 -142
- package/server/apiRouter.js +60 -15
- package/server/apiRouter.ts +774 -0
- package/server/approvalDetector.ts +94 -0
- package/server/authManager.ts +144 -0
- package/server/autoIdleManager.ts +110 -0
- package/server/config.ts +121 -0
- package/server/constants.ts +150 -0
- package/server/db.ts +475 -0
- package/server/hookInstaller.d.ts +3 -0
- package/server/hookProcessor.ts +108 -0
- package/server/hookRouter.ts +18 -0
- package/server/hookStats.ts +116 -0
- package/server/index.js +15 -1
- package/server/index.ts +230 -0
- package/server/logger.ts +75 -0
- package/server/mqReader.ts +349 -0
- package/server/portManager.ts +55 -0
- package/server/processMonitor.ts +239 -0
- package/server/serverConfig.ts +29 -0
- package/server/sessionMatcher.js +17 -6
- package/server/sessionMatcher.ts +403 -0
- package/server/sessionStore.js +109 -3
- package/server/sessionStore.ts +1145 -0
- package/server/sshManager.js +167 -24
- package/server/sshManager.ts +671 -0
- package/server/teamManager.ts +289 -0
- 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">> Deploy Unit</button>
|
|
93
|
+
<button class="btn btn-ghost" id="resetBtn">// Reset Lab</button>
|
|
94
|
+
<div class="hint">Drag to orbit · Scroll to zoom<br><kbd>Space</kbd> deploy · <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>
|