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.
- package/CHANGELOG.md +640 -540
- package/README.md +592 -415
- package/cli.js +1089 -589
- package/conversation-templates/autonomous-feature.json +22 -0
- package/conversation-templates/code-review.json +21 -11
- package/conversation-templates/debug-squad.json +21 -11
- package/conversation-templates/feature-build.json +21 -11
- package/conversation-templates/research-write.json +21 -11
- package/dashboard.html +9250 -7771
- package/dashboard.js +1232 -29
- package/office/agents.js +148 -4
- package/office/animation.js +68 -0
- package/office/assets.js +431 -0
- package/office/builder.js +355 -0
- package/office/building-interior.js +261 -0
- package/office/campus-env.js +119 -23
- package/office/car-hud.js +368 -0
- package/office/daynight.js +221 -0
- package/office/economy-hud.js +432 -0
- package/office/economy-ui.js +238 -0
- package/office/environment.js +818 -808
- package/office/face.js +65 -0
- package/office/fast-travel.js +215 -0
- package/office/hq-building.js +295 -0
- package/office/index.js +1095 -423
- package/office/instancing.js +160 -0
- package/office/lod-manager.js +165 -0
- package/office/multiplayer-hud.js +428 -0
- package/office/net-client.js +299 -0
- package/office/particles.js +172 -0
- package/office/player.js +658 -436
- package/office/post-processing.js +82 -0
- package/office/sky.js +319 -0
- package/office/street-furniture.js +308 -0
- package/office/vehicle.js +455 -0
- package/office/world-save.js +91 -0
- package/package.json +59 -59
- package/server.js +7190 -4685
- 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
|
+
}
|