let-them-talk 4.2.0 → 5.2.0

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 (39) hide show
  1. package/CHANGELOG.md +640 -540
  2. package/README.md +592 -415
  3. package/cli.js +1089 -589
  4. package/conversation-templates/autonomous-feature.json +22 -0
  5. package/conversation-templates/code-review.json +21 -11
  6. package/conversation-templates/debug-squad.json +21 -11
  7. package/conversation-templates/feature-build.json +21 -11
  8. package/conversation-templates/research-write.json +21 -11
  9. package/dashboard.html +9250 -7771
  10. package/dashboard.js +1232 -29
  11. package/office/agents.js +148 -4
  12. package/office/animation.js +68 -0
  13. package/office/assets.js +431 -0
  14. package/office/builder.js +355 -0
  15. package/office/building-interior.js +261 -0
  16. package/office/campus-env.js +119 -23
  17. package/office/car-hud.js +368 -0
  18. package/office/daynight.js +221 -0
  19. package/office/economy-hud.js +432 -0
  20. package/office/economy-ui.js +238 -0
  21. package/office/environment.js +818 -808
  22. package/office/face.js +65 -0
  23. package/office/fast-travel.js +215 -0
  24. package/office/hq-building.js +295 -0
  25. package/office/index.js +1095 -423
  26. package/office/instancing.js +160 -0
  27. package/office/lod-manager.js +165 -0
  28. package/office/multiplayer-hud.js +428 -0
  29. package/office/net-client.js +299 -0
  30. package/office/particles.js +172 -0
  31. package/office/player.js +658 -436
  32. package/office/post-processing.js +82 -0
  33. package/office/sky.js +319 -0
  34. package/office/street-furniture.js +308 -0
  35. package/office/vehicle.js +455 -0
  36. package/office/world-save.js +91 -0
  37. package/package.json +59 -59
  38. package/server.js +7190 -4685
  39. package/conversation-templates/managed-team.json +0 -12
@@ -0,0 +1,355 @@
1
+ // builder.js — Real-time World Builder for the 3D Hub
2
+ // Press B to toggle builder mode. Click to place assets. R to rotate. Right-click to delete.
3
+ import * as THREE from 'three';
4
+ import { S } from './state.js';
5
+ import { ASSETS, ASSET_CATEGORIES, getAsset, getAssetsByCategory, createGhost } from './assets.js';
6
+ import { addPlacement, removePlacement, loadWorld, getPlacements } from './world-save.js';
7
+ import { isPlayerMode } from './player.js';
8
+
9
+ var _active = false; // builder mode on/off
10
+ var _panel = null; // UI panel element
11
+ var _selectedAsset = null; // currently selected asset ID
12
+ var _ghostMesh = null; // preview mesh following cursor
13
+ var _rotation = 0; // current placement rotation (0, PI/2, PI, 3PI/2)
14
+ var _placedMeshes = {}; // id → THREE.Group map for deletion
15
+ var _gridHelper = null; // grid overlay
16
+ var _raycaster = new THREE.Raycaster();
17
+ var _mouse = new THREE.Vector2();
18
+ var _floorPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // Y=0 plane
19
+ var GRID_SIZE = 0.5; // snap grid in world units
20
+ var _undoStack = []; // for undo (Ctrl+Z)
21
+
22
+ // ===================== PUBLIC API =====================
23
+
24
+ export function isBuilderActive() { return _active; }
25
+
26
+ export function toggleBuilder() {
27
+ if (_active) exitBuilder();
28
+ else enterBuilder();
29
+ }
30
+
31
+ export function enterBuilder() {
32
+ if (_active) return;
33
+ _active = true;
34
+ showPanel();
35
+ showGrid();
36
+ addListeners();
37
+ }
38
+
39
+ export function exitBuilder() {
40
+ if (!_active) return;
41
+ _active = false;
42
+ hidePanel();
43
+ hideGrid();
44
+ removeGhost();
45
+ removeListeners();
46
+ _selectedAsset = null;
47
+ }
48
+
49
+ // Load saved placements and render them in the scene
50
+ export function loadSavedWorld() {
51
+ loadWorld().then(function(placements) {
52
+ if (!placements || !Array.isArray(placements)) return;
53
+ for (var i = 0; i < placements.length; i++) {
54
+ renderPlacement(placements[i]);
55
+ }
56
+ });
57
+ }
58
+
59
+ // ===================== GRID =====================
60
+
61
+ function showGrid() {
62
+ if (_gridHelper) return;
63
+ _gridHelper = new THREE.GridHelper(50, 100, 0x444466, 0x333355); // 50 units, 0.5 unit cells
64
+ _gridHelper.position.y = 0.005;
65
+ _gridHelper.material.transparent = true;
66
+ _gridHelper.material.opacity = 0.3;
67
+ S.scene.add(_gridHelper);
68
+ }
69
+
70
+ function hideGrid() {
71
+ if (_gridHelper) {
72
+ S.scene.remove(_gridHelper);
73
+ _gridHelper.geometry.dispose();
74
+ _gridHelper.material.dispose();
75
+ _gridHelper = null;
76
+ }
77
+ }
78
+
79
+ // ===================== GHOST PREVIEW =====================
80
+
81
+ function setGhost(assetId) {
82
+ removeGhost();
83
+ if (!assetId) return;
84
+ _ghostMesh = createGhost(assetId);
85
+ if (_ghostMesh) {
86
+ S.scene.add(_ghostMesh);
87
+ }
88
+ }
89
+
90
+ function removeGhost() {
91
+ if (_ghostMesh) {
92
+ S.scene.remove(_ghostMesh);
93
+ _ghostMesh.traverse(function(c) {
94
+ if (c.geometry) c.geometry.dispose();
95
+ if (c.material) c.material.dispose();
96
+ });
97
+ _ghostMesh = null;
98
+ }
99
+ }
100
+
101
+ function updateGhostPosition(event) {
102
+ if (!_ghostMesh || !S.renderer || !S.camera) return;
103
+ var rect = S.renderer.domElement.getBoundingClientRect();
104
+ _mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
105
+ _mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
106
+ _raycaster.setFromCamera(_mouse, S.camera);
107
+
108
+ var intersect = new THREE.Vector3();
109
+ _raycaster.ray.intersectPlane(_floorPlane, intersect);
110
+ if (intersect) {
111
+ // Snap to grid
112
+ intersect.x = Math.round(intersect.x / GRID_SIZE) * GRID_SIZE;
113
+ intersect.z = Math.round(intersect.z / GRID_SIZE) * GRID_SIZE;
114
+ intersect.y = 0;
115
+ _ghostMesh.position.copy(intersect);
116
+ _ghostMesh.rotation.y = _rotation;
117
+ }
118
+ }
119
+
120
+ // ===================== PLACEMENT =====================
121
+
122
+ function placeAsset(event) {
123
+ if (!_selectedAsset || !_ghostMesh) return;
124
+
125
+ var pos = _ghostMesh.position.clone();
126
+ var entry = addPlacement(_selectedAsset, pos.x, pos.y, pos.z, _rotation, 'user');
127
+ renderPlacement(entry);
128
+ _undoStack.push(entry.id);
129
+ }
130
+
131
+ function renderPlacement(entry) {
132
+ var asset = getAsset(entry.type);
133
+ if (!asset) return;
134
+ var group = asset.factory();
135
+ group.position.set(entry.x, entry.y || 0, entry.z);
136
+ group.rotation.y = entry.rotY || 0;
137
+ group.userData.placementId = entry.id;
138
+ group.userData.isPlaced = true;
139
+ S.scene.add(group);
140
+ _placedMeshes[entry.id] = group;
141
+ }
142
+
143
+ function deleteNearestPlacement(event) {
144
+ if (!S.renderer || !S.camera) return;
145
+ var rect = S.renderer.domElement.getBoundingClientRect();
146
+ _mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
147
+ _mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
148
+ _raycaster.setFromCamera(_mouse, S.camera);
149
+
150
+ // Collect all placed meshes
151
+ var meshes = [];
152
+ for (var id in _placedMeshes) {
153
+ _placedMeshes[id].traverse(function(c) {
154
+ if (c.isMesh) { c.userData._placementId = id; meshes.push(c); }
155
+ });
156
+ }
157
+
158
+ var hits = _raycaster.intersectObjects(meshes, false);
159
+ if (hits.length > 0) {
160
+ var hitId = hits[0].object.userData._placementId;
161
+ if (hitId && _placedMeshes[hitId]) {
162
+ // Remove from scene
163
+ var group = _placedMeshes[hitId];
164
+ S.scene.remove(group);
165
+ group.traverse(function(c) {
166
+ if (c.geometry) c.geometry.dispose();
167
+ if (c.material && !c.material._shared) c.material.dispose();
168
+ });
169
+ delete _placedMeshes[hitId];
170
+ // Remove from save data
171
+ removePlacement(hitId);
172
+ }
173
+ }
174
+ }
175
+
176
+ function undoLast() {
177
+ if (_undoStack.length === 0) return;
178
+ var lastId = _undoStack.pop();
179
+ if (_placedMeshes[lastId]) {
180
+ var group = _placedMeshes[lastId];
181
+ S.scene.remove(group);
182
+ group.traverse(function(c) {
183
+ if (c.geometry) c.geometry.dispose();
184
+ if (c.material) c.material.dispose();
185
+ });
186
+ delete _placedMeshes[lastId];
187
+ removePlacement(lastId);
188
+ }
189
+ }
190
+
191
+ // ===================== UI PANEL =====================
192
+
193
+ function showPanel() {
194
+ if (_panel) return;
195
+ _panel = document.createElement('div');
196
+ _panel.id = 'builder-panel';
197
+ _panel.style.cssText = 'position:fixed;right:12px;top:80px;z-index:999999;width:180px;max-height:70vh;overflow-y:auto;background:rgba(22,27,34,0.95);border:1px solid #30363d;border-radius:10px;padding:0 8px 8px;font-family:system-ui;color:#e6edf3;backdrop-filter:blur(8px);';
198
+
199
+ // Drag handle header
200
+ var header = document.createElement('div');
201
+ header.style.cssText = 'text-align:center;font-size:13px;font-weight:bold;color:#58a6ff;padding:8px 0;border-bottom:1px solid #30363d;margin-bottom:8px;cursor:grab;user-select:none;';
202
+ header.textContent = '\u2630 World Builder';
203
+ header.title = 'Drag to move';
204
+ _panel.appendChild(header);
205
+
206
+ // Drag logic — store refs for cleanup in hidePanel()
207
+ var dragOffX = 0, dragOffY = 0, dragging = false;
208
+ header.addEventListener('mousedown', function(e) {
209
+ dragging = true;
210
+ dragOffX = e.clientX - _panel.getBoundingClientRect().left;
211
+ dragOffY = e.clientY - _panel.getBoundingClientRect().top;
212
+ header.style.cursor = 'grabbing';
213
+ e.preventDefault();
214
+ });
215
+ _panel._dragMove = function(e) {
216
+ if (!dragging || !_panel) return;
217
+ _panel.style.left = (e.clientX - dragOffX) + 'px';
218
+ _panel.style.top = (e.clientY - dragOffY) + 'px';
219
+ _panel.style.right = 'auto';
220
+ _panel.style.transform = 'none';
221
+ };
222
+ _panel._dragUp = function() {
223
+ if (dragging) { dragging = false; if (header) header.style.cursor = 'grab'; }
224
+ };
225
+ document.addEventListener('mousemove', _panel._dragMove);
226
+ document.addEventListener('mouseup', _panel._dragUp);
227
+
228
+ // Hint
229
+ var hint = document.createElement('div');
230
+ hint.style.cssText = 'font-size:9px;color:#8b949e;text-align:center;margin-bottom:8px;';
231
+ hint.textContent = 'Click=Place | R=Rotate | Right-click=Delete | Ctrl+Z=Undo | B=Close';
232
+ _panel.appendChild(hint);
233
+
234
+ // Categories + assets
235
+ for (var ci = 0; ci < ASSET_CATEGORIES.length; ci++) {
236
+ var cat = ASSET_CATEGORIES[ci];
237
+ var catAssets = getAssetsByCategory(cat.id);
238
+ if (catAssets.length === 0) continue;
239
+
240
+ var catLabel = document.createElement('div');
241
+ catLabel.style.cssText = 'font-size:10px;color:#8b949e;padding:4px 0 2px;text-transform:uppercase;letter-spacing:1px;';
242
+ catLabel.textContent = cat.icon + ' ' + cat.label;
243
+ _panel.appendChild(catLabel);
244
+
245
+ for (var ai = 0; ai < catAssets.length; ai++) {
246
+ var asset = catAssets[ai];
247
+ var btn = document.createElement('button');
248
+ btn.style.cssText = 'display:block;width:100%;padding:6px 8px;margin:2px 0;background:rgba(48,54,61,0.6);border:1px solid transparent;border-radius:6px;color:#c9d1d9;font-size:11px;cursor:pointer;text-align:left;transition:all 0.15s;';
249
+ btn.textContent = asset.icon + ' ' + asset.name;
250
+ btn.dataset.assetId = asset.id;
251
+ btn.addEventListener('mouseenter', function() { this.style.background = 'rgba(88,166,255,0.2)'; this.style.borderColor = '#58a6ff'; });
252
+ btn.addEventListener('mouseleave', function() {
253
+ if (this.dataset.assetId !== _selectedAsset) {
254
+ this.style.background = 'rgba(48,54,61,0.6)'; this.style.borderColor = 'transparent';
255
+ }
256
+ });
257
+ btn.addEventListener('click', function(e) {
258
+ e.stopPropagation();
259
+ var id = this.dataset.assetId;
260
+ _selectedAsset = id;
261
+ _rotation = 0;
262
+ setGhost(id);
263
+ // Highlight selected
264
+ var btns = _panel.querySelectorAll('button');
265
+ for (var b = 0; b < btns.length; b++) {
266
+ btns[b].style.background = 'rgba(48,54,61,0.6)'; btns[b].style.borderColor = 'transparent';
267
+ }
268
+ this.style.background = 'rgba(88,166,255,0.3)'; this.style.borderColor = '#58a6ff';
269
+ });
270
+ _panel.appendChild(btn);
271
+ }
272
+ }
273
+
274
+ // Append to fullscreen element if active, otherwise document.body
275
+ // This ensures the builder panel is visible when the 3D container is fullscreened
276
+ var target = document.fullscreenElement || document.body;
277
+ target.appendChild(_panel);
278
+ }
279
+
280
+ function hidePanel() {
281
+ if (_panel) {
282
+ // Remove drag listeners to prevent leaks
283
+ if (_panel._dragMove) document.removeEventListener('mousemove', _panel._dragMove);
284
+ if (_panel._dragUp) document.removeEventListener('mouseup', _panel._dragUp);
285
+ if (_panel.parentElement) _panel.remove();
286
+ }
287
+ _panel = null;
288
+ }
289
+
290
+ // ===================== EVENT LISTENERS =====================
291
+
292
+ var _onMouseMove = null;
293
+ var _onMouseDown = null;
294
+ var _onContextMenu = null;
295
+ var _onKeyDown = null;
296
+
297
+ function addListeners() {
298
+ _onMouseMove = function(e) { updateGhostPosition(e); };
299
+ _onMouseDown = function(e) {
300
+ if (!_active) return;
301
+ if (e.button === 0 && _selectedAsset) { // left click
302
+ // Don't place if clicking the panel
303
+ if (_panel && _panel.contains(e.target)) return;
304
+ placeAsset(e);
305
+ }
306
+ };
307
+ _onContextMenu = function(e) {
308
+ if (!_active) return;
309
+ e.preventDefault();
310
+ deleteNearestPlacement(e);
311
+ };
312
+ _onKeyDown = function(e) {
313
+ if (!_active) return;
314
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
315
+ if (e.code === 'KeyR') {
316
+ // Rotate 90 degrees
317
+ _rotation = (_rotation + Math.PI / 2) % (Math.PI * 2);
318
+ if (_ghostMesh) _ghostMesh.rotation.y = _rotation;
319
+ }
320
+ if (e.code === 'KeyZ' && (e.ctrlKey || e.metaKey)) {
321
+ e.preventDefault();
322
+ undoLast();
323
+ }
324
+ };
325
+
326
+ window.addEventListener('mousemove', _onMouseMove);
327
+ window.addEventListener('mousedown', _onMouseDown);
328
+ window.addEventListener('contextmenu', _onContextMenu);
329
+ window.addEventListener('keydown', _onKeyDown);
330
+ }
331
+
332
+ function removeListeners() {
333
+ if (_onMouseMove) window.removeEventListener('mousemove', _onMouseMove);
334
+ if (_onMouseDown) window.removeEventListener('mousedown', _onMouseDown);
335
+ if (_onContextMenu) window.removeEventListener('contextmenu', _onContextMenu);
336
+ if (_onKeyDown) window.removeEventListener('keydown', _onKeyDown);
337
+ _onMouseMove = _onMouseDown = _onContextMenu = _onKeyDown = null;
338
+ }
339
+
340
+ // ===================== CLEANUP =====================
341
+
342
+ export function cleanupBuilder() {
343
+ exitBuilder();
344
+ // Remove all placed meshes from scene
345
+ for (var id in _placedMeshes) {
346
+ var group = _placedMeshes[id];
347
+ S.scene.remove(group);
348
+ group.traverse(function(c) {
349
+ if (c.geometry) c.geometry.dispose();
350
+ if (c.material) c.material.dispose();
351
+ });
352
+ }
353
+ _placedMeshes = {};
354
+ _undoStack = [];
355
+ }
@@ -0,0 +1,261 @@
1
+ import * as THREE from 'three';
2
+ import { S } from './state.js';
3
+
4
+ // ============================================================
5
+ // BUILDING INTERIORS — enterable buildings with floors, desks
6
+ // Agents sit at desks INSIDE buildings, visible through windows
7
+ // ============================================================
8
+
9
+ var FLOOR_HEIGHT = 3.0;
10
+ var WALL_THICKNESS = 0.15;
11
+
12
+ // Shared materials (created once)
13
+ var intMats = null;
14
+
15
+ function getIntMats() {
16
+ if (intMats) return intMats;
17
+ intMats = {
18
+ floor: new THREE.MeshStandardMaterial({ color: 0x8a8a7a, roughness: 0.6, side: THREE.DoubleSide }),
19
+ carpet: new THREE.MeshStandardMaterial({ color: 0x3a3d4a, roughness: 0.9 }),
20
+ wall: new THREE.MeshStandardMaterial({ color: 0xd8d4cc, roughness: 0.7, side: THREE.DoubleSide }),
21
+ wallAccent: new THREE.MeshStandardMaterial({ color: 0x6a7a8a, roughness: 0.5 }),
22
+ desk: new THREE.MeshStandardMaterial({ color: 0x5a4a3a, roughness: 0.5 }),
23
+ deskTop: new THREE.MeshStandardMaterial({ color: 0x7a6a5a, roughness: 0.4 }),
24
+ chair: new THREE.MeshStandardMaterial({ color: 0x2a2a2a, roughness: 0.6 }),
25
+ monitor: new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.3 }),
26
+ screen: new THREE.MeshStandardMaterial({ color: 0x2244aa, emissive: 0x1133aa, emissiveIntensity: 0.8, roughness: 0.1 }),
27
+ ceiling: new THREE.MeshStandardMaterial({ color: 0xeeeeee, roughness: 0.8, side: THREE.DoubleSide }),
28
+ column: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.4, metalness: 0.2 }),
29
+ elevator: new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.3, metalness: 0.5 }),
30
+ plant: new THREE.MeshStandardMaterial({ color: 0x2a6a2a, roughness: 0.8 }),
31
+ };
32
+ return intMats;
33
+ }
34
+
35
+ // ============================================================
36
+ // BUILD FLOOR INTERIOR — desks, chairs, monitors per floor
37
+ // ============================================================
38
+
39
+ function buildFloorInterior(group, floorY, w, d, floorNum, seed) {
40
+ var m = getIntMats();
41
+ var innerW = w - WALL_THICKNESS * 2;
42
+ var innerD = d - WALL_THICKNESS * 2;
43
+
44
+ // Floor slab
45
+ var floorGeo = new THREE.BoxGeometry(innerW, 0.08, innerD);
46
+ var floor = new THREE.Mesh(floorGeo, floorNum === 0 ? m.floor : m.carpet);
47
+ floor.position.y = floorY + 0.04;
48
+ floor.receiveShadow = true;
49
+ group.add(floor);
50
+
51
+ // Ceiling
52
+ var ceilGeo = new THREE.BoxGeometry(innerW, 0.06, innerD);
53
+ var ceil = new THREE.Mesh(ceilGeo, m.ceiling);
54
+ ceil.position.y = floorY + FLOOR_HEIGHT - 0.03;
55
+ group.add(ceil);
56
+
57
+ // Ceiling light (fluorescent strip)
58
+ var lightGeo = new THREE.BoxGeometry(innerW * 0.6, 0.04, 0.15);
59
+ var lightMat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xeeeeff, emissiveIntensity: 0.6 });
60
+ var ceilLight = new THREE.Mesh(lightGeo, lightMat);
61
+ ceilLight.position.y = floorY + FLOOR_HEIGHT - 0.08;
62
+ group.add(ceilLight);
63
+
64
+ // Columns removed for performance
65
+
66
+ // Desks — limited count for performance
67
+ var deskCount = Math.min(3, Math.floor(innerW / 3));
68
+ var deskSpacing = innerW / (deskCount + 1);
69
+ var deskPositions = [];
70
+
71
+ for (var di = 0; di < deskCount; di++) {
72
+ var dx = -innerW / 2 + deskSpacing * (di + 1);
73
+ var dz = ((seed + di) % 2 === 0) ? -innerD * 0.2 : innerD * 0.2;
74
+
75
+ // Desk (table top + legs)
76
+ var topGeo = new THREE.BoxGeometry(1.2, 0.06, 0.6);
77
+ var top = new THREE.Mesh(topGeo, m.deskTop);
78
+ top.position.set(dx, floorY + 0.72, dz);
79
+ group.add(top);
80
+
81
+ // Desk legs
82
+ var legGeo = new THREE.BoxGeometry(0.05, 0.7, 0.05);
83
+ [[-0.55,-0.25],[0.55,-0.25],[0.55,0.25],[-0.55,0.25]].forEach(function(lp) {
84
+ var leg = new THREE.Mesh(legGeo, m.desk);
85
+ leg.position.set(dx + lp[0], floorY + 0.35, dz + lp[1]);
86
+ group.add(leg);
87
+ });
88
+
89
+ // Monitor
90
+ var monGeo = new THREE.BoxGeometry(0.5, 0.35, 0.03);
91
+ var mon = new THREE.Mesh(monGeo, m.monitor);
92
+ mon.position.set(dx, floorY + 1.0, dz - 0.15);
93
+ group.add(mon);
94
+
95
+ // Monitor screen (emissive — glows through windows)
96
+ var scrGeo = new THREE.PlaneGeometry(0.45, 0.3);
97
+ var scr = new THREE.Mesh(scrGeo, m.screen);
98
+ scr.position.set(dx, floorY + 1.0, dz - 0.16);
99
+ scr.rotation.y = Math.PI;
100
+ group.add(scr);
101
+
102
+ // Chair
103
+ var chairSeat = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.06, 0.4), m.chair);
104
+ chairSeat.position.set(dx, floorY + 0.45, dz + 0.45);
105
+ group.add(chairSeat);
106
+ var chairBack = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.05), m.chair);
107
+ chairBack.position.set(dx, floorY + 0.65, dz + 0.65);
108
+ group.add(chairBack);
109
+
110
+ deskPositions.push({ x: dx, y: floorY, z: dz + 0.45, floor: floorNum });
111
+ }
112
+
113
+ // Plants removed for performance
114
+
115
+ return deskPositions;
116
+ }
117
+
118
+ // ============================================================
119
+ // BUILD COMPLETE BUILDING — shell + interior per floor
120
+ // ============================================================
121
+
122
+ export function buildDetailedBuilding(cx, cz, w, d, floors, buildingType, color) {
123
+ var m = getIntMats();
124
+ var group = new THREE.Group();
125
+ var height = floors * FLOOR_HEIGHT;
126
+ var allDesks = [];
127
+
128
+ // === EXTERIOR WALLS (hollow shell, not solid box) ===
129
+ var wallMat = new THREE.MeshStandardMaterial({
130
+ color: color, roughness: 0.5, metalness: 0.15,
131
+ });
132
+
133
+ // Front wall (with window cutouts represented by glass)
134
+ var glassMat = new THREE.MeshStandardMaterial({
135
+ color: 0x99bbdd, transparent: true, opacity: 0.2,
136
+ roughness: 0.0, metalness: 0.3, side: THREE.DoubleSide,
137
+ depthWrite: false,
138
+ });
139
+
140
+ // Position group at building center — all children use local coords
141
+ group.position.set(cx, 0, cz);
142
+
143
+ // 4 walls as thin boxes (local coords, relative to group)
144
+ var wallParts = [
145
+ { sx: w, sy: height, sz: WALL_THICKNESS, px: 0, pz: d / 2 }, // front
146
+ { sx: w, sy: height, sz: WALL_THICKNESS, px: 0, pz: -d / 2 }, // back
147
+ { sx: WALL_THICKNESS, sy: height, sz: d, px: -w / 2, pz: 0 }, // left
148
+ { sx: WALL_THICKNESS, sy: height, sz: d, px: w / 2, pz: 0 }, // right
149
+ ];
150
+
151
+ wallParts.forEach(function(wp) {
152
+ var wallGeo = new THREE.BoxGeometry(wp.sx, wp.sy, wp.sz);
153
+ var wall = new THREE.Mesh(wallGeo, wallMat);
154
+ wall.position.set(wp.px, height / 2, wp.pz);
155
+ wall.castShadow = true;
156
+ wall.receiveShadow = true;
157
+ group.add(wall);
158
+ });
159
+
160
+ // Glass window panels on front and back (per floor)
161
+ for (var fi = 0; fi < floors; fi++) {
162
+ var winY = fi * FLOOR_HEIGHT + FLOOR_HEIGHT * 0.55;
163
+ var winH = FLOOR_HEIGHT * 0.5;
164
+
165
+ // Front windows
166
+ var fwGeo = new THREE.PlaneGeometry(w * 0.85, winH);
167
+ var fw = new THREE.Mesh(fwGeo, glassMat);
168
+ fw.position.set(0, winY, d / 2 + 0.01);
169
+ group.add(fw);
170
+
171
+ // Back windows
172
+ var bwGeo = new THREE.PlaneGeometry(w * 0.85, winH);
173
+ var bw = new THREE.Mesh(bwGeo, glassMat);
174
+ bw.position.set(0, winY, -d / 2 - 0.01);
175
+ bw.rotation.y = Math.PI;
176
+ group.add(bw);
177
+
178
+ // Side windows
179
+ [1, -1].forEach(function(side) {
180
+ var swGeo = new THREE.PlaneGeometry(d * 0.85, winH);
181
+ var sw = new THREE.Mesh(swGeo, glassMat);
182
+ sw.position.set(side * (w / 2 + 0.01), winY, 0);
183
+ sw.rotation.y = side * Math.PI / 2;
184
+ group.add(sw);
185
+ });
186
+ }
187
+
188
+ // Roof
189
+ var roofGeo = new THREE.BoxGeometry(w + 0.3, 0.2, d + 0.3);
190
+ var roof = new THREE.Mesh(roofGeo, wallMat);
191
+ roof.position.set(0, height + 0.1, 0);
192
+ roof.castShadow = true;
193
+ group.add(roof);
194
+
195
+ // Roof ledge (decorative)
196
+ var ledgeGeo = new THREE.BoxGeometry(w + 0.5, 0.4, 0.15);
197
+ var ledgeMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.4, metalness: 0.3 });
198
+ [1, -1].forEach(function(side) {
199
+ var ledge = new THREE.Mesh(ledgeGeo, ledgeMat);
200
+ ledge.position.set(0, height + 0.2, side * (d / 2 + 0.07));
201
+ group.add(ledge);
202
+ });
203
+
204
+ // === ENTRANCE (ground floor door) ===
205
+ var doorGeo = new THREE.BoxGeometry(1.5, 2.5, 0.1);
206
+ var doorMat = new THREE.MeshStandardMaterial({ color: 0x3a2a1a, roughness: 0.5 });
207
+ var door = new THREE.Mesh(doorGeo, doorMat);
208
+ door.position.set(0, 1.25, d / 2 + 0.08);
209
+ group.add(door);
210
+
211
+ // Glass door panels
212
+ var doorGlass = new THREE.Mesh(
213
+ new THREE.PlaneGeometry(0.55, 1.8),
214
+ glassMat
215
+ );
216
+ doorGlass.position.set(-0.3, 1.1, d / 2 + 0.09);
217
+ group.add(doorGlass);
218
+ var doorGlass2 = new THREE.Mesh(
219
+ new THREE.PlaneGeometry(0.55, 1.8),
220
+ glassMat
221
+ );
222
+ doorGlass2.position.set(0.3, 1.1, d / 2 + 0.09);
223
+ group.add(doorGlass2);
224
+
225
+ // Entrance canopy
226
+ var canopyGeo = new THREE.BoxGeometry(2.5, 0.1, 1.2);
227
+ var canopyMat = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.4, metalness: 0.3 });
228
+ var canopy = new THREE.Mesh(canopyGeo, canopyMat);
229
+ canopy.position.set(0, 2.7, d / 2 + 0.6);
230
+ canopy.castShadow = true;
231
+ group.add(canopy);
232
+
233
+ // === FLOOR INTERIORS ===
234
+ for (var floor = 0; floor < floors; floor++) {
235
+ var floorY = floor * FLOOR_HEIGHT;
236
+ var seed = Math.floor(cx * 7 + cz * 13 + floor * 31);
237
+ var desks = buildFloorInterior(group, floorY, w, d, floor, seed);
238
+
239
+ // Offset desk positions to world coords
240
+ desks.forEach(function(dp) {
241
+ allDesks.push({
242
+ x: cx + dp.x,
243
+ y: dp.y,
244
+ z: cz + dp.z,
245
+ floor: dp.floor,
246
+ building: buildingType,
247
+ });
248
+ });
249
+ }
250
+
251
+ // No point lights — use emissive ceiling lights instead (zero GPU cost)
252
+
253
+ S.furnitureGroup.add(group);
254
+
255
+ return {
256
+ group: group,
257
+ desks: allDesks,
258
+ height: height,
259
+ entrance: { x: cx, z: cz + d / 2 + 1 }, // world coords for external use
260
+ };
261
+ }