let-them-talk 4.3.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 -582
- 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 -7964
- package/dashboard.js +1071 -29
- package/office/building-interior.js +261 -0
- 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/fast-travel.js +215 -0
- package/office/hq-building.js +295 -0
- package/office/index.js +1095 -1046
- 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 -658
- 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/package.json +59 -59
- package/server.js +7190 -4685
- package/conversation-templates/managed-team.json +0 -12
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
|
3
|
+
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
|
4
|
+
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
|
5
|
+
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
|
|
6
|
+
import { S } from './state.js';
|
|
7
|
+
|
|
8
|
+
// ============================================================
|
|
9
|
+
// POST-PROCESSING — bloom, tone mapping for cinematic visuals
|
|
10
|
+
// Makes neon signs glow, street lights bloom, windows shine
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
var composer = null;
|
|
14
|
+
var bloomPass = null;
|
|
15
|
+
|
|
16
|
+
export function initPostProcessing() {
|
|
17
|
+
if (!S.renderer || !S.scene || !S.camera) return null;
|
|
18
|
+
|
|
19
|
+
var w = S.container.clientWidth;
|
|
20
|
+
var h = S.container.clientHeight;
|
|
21
|
+
|
|
22
|
+
composer = new EffectComposer(S.renderer);
|
|
23
|
+
|
|
24
|
+
// Base render
|
|
25
|
+
var renderPass = new RenderPass(S.scene, S.camera);
|
|
26
|
+
composer.addPass(renderPass);
|
|
27
|
+
|
|
28
|
+
// Bloom — makes emissive materials glow
|
|
29
|
+
bloomPass = new UnrealBloomPass(
|
|
30
|
+
new THREE.Vector2(w, h),
|
|
31
|
+
0.4, // strength (subtle, not overpowering)
|
|
32
|
+
0.6, // radius (spread of glow)
|
|
33
|
+
0.85 // threshold (only bright things bloom)
|
|
34
|
+
);
|
|
35
|
+
composer.addPass(bloomPass);
|
|
36
|
+
|
|
37
|
+
// Output — handles tone mapping + color space
|
|
38
|
+
var outputPass = new OutputPass();
|
|
39
|
+
composer.addPass(outputPass);
|
|
40
|
+
|
|
41
|
+
// Boost tone mapping for cinematic look
|
|
42
|
+
S.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
43
|
+
S.renderer.toneMappingExposure = 1.0;
|
|
44
|
+
|
|
45
|
+
// Store reference
|
|
46
|
+
S._composer = composer;
|
|
47
|
+
S._bloomPass = bloomPass;
|
|
48
|
+
|
|
49
|
+
return composer;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function renderWithPostProcessing() {
|
|
53
|
+
if (composer) {
|
|
54
|
+
composer.render();
|
|
55
|
+
} else {
|
|
56
|
+
S.renderer.render(S.scene, S.camera);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function setBloomStrength(strength) {
|
|
61
|
+
if (bloomPass) bloomPass.strength = strength;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function setBloomThreshold(threshold) {
|
|
65
|
+
if (bloomPass) bloomPass.threshold = threshold;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resizePostProcessing(w, h) {
|
|
69
|
+
if (composer) composer.setSize(w, h);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function disposePostProcessing() {
|
|
73
|
+
if (composer) {
|
|
74
|
+
composer.dispose();
|
|
75
|
+
composer = null;
|
|
76
|
+
bloomPass = null;
|
|
77
|
+
S._composer = null;
|
|
78
|
+
S._bloomPass = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getComposer() { return composer; }
|
package/office/sky.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sky system for AI City day/night cycle.
|
|
5
|
+
* - Sky dome with gradient color lerp (dawn/day/dusk/night)
|
|
6
|
+
* - Star field particle system (visible at night)
|
|
7
|
+
* - Time-of-day HUD indicator
|
|
8
|
+
* - Exports getTimeOfDay() for other modules (lights, windows, agents)
|
|
9
|
+
* Target: minimal GPU cost — single sphere + point cloud, no shaders.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
let skyDome = null;
|
|
13
|
+
let starField = null;
|
|
14
|
+
let timeHudEl = null;
|
|
15
|
+
let gameTime = 0.5; // 0-1, 0=midnight, 0.25=dawn, 0.5=noon, 0.75=dusk
|
|
16
|
+
let timeSpeed = 1 / 600; // Full cycle in 600s (10 min) by default
|
|
17
|
+
let paused = false;
|
|
18
|
+
|
|
19
|
+
// Sky color presets (interpolated based on gameTime)
|
|
20
|
+
const SKY_COLORS = {
|
|
21
|
+
midnight: new THREE.Color(0x0a0a1a),
|
|
22
|
+
dawn: new THREE.Color(0xff7744),
|
|
23
|
+
morning: new THREE.Color(0x87CEEB),
|
|
24
|
+
noon: new THREE.Color(0x4a90d9),
|
|
25
|
+
afternoon:new THREE.Color(0x6bb3e0),
|
|
26
|
+
dusk: new THREE.Color(0xff6633),
|
|
27
|
+
evening: new THREE.Color(0x1a1a3a),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Fog color follows sky
|
|
31
|
+
const FOG_COLORS = {
|
|
32
|
+
midnight: new THREE.Color(0x050510),
|
|
33
|
+
dawn: new THREE.Color(0xcc8866),
|
|
34
|
+
noon: new THREE.Color(0x8899aa),
|
|
35
|
+
dusk: new THREE.Color(0xcc6644),
|
|
36
|
+
evening: new THREE.Color(0x101020),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const TIME_LABELS = [
|
|
40
|
+
{ t: 0, label: '12:00 AM', icon: '🌙' },
|
|
41
|
+
{ t: 0.125,label: '3:00 AM', icon: '🌙' },
|
|
42
|
+
{ t: 0.25, label: '6:00 AM', icon: '🌅' },
|
|
43
|
+
{ t: 0.375,label: '9:00 AM', icon: '☀️' },
|
|
44
|
+
{ t: 0.5, label: '12:00 PM', icon: '☀️' },
|
|
45
|
+
{ t: 0.625,label: '3:00 PM', icon: '☀️' },
|
|
46
|
+
{ t: 0.75, label: '6:00 PM', icon: '🌅' },
|
|
47
|
+
{ t: 0.875,label: '9:00 PM', icon: '🌙' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const TIME_HUD_STYLES = `
|
|
51
|
+
.sky-time-hud {
|
|
52
|
+
position: fixed;
|
|
53
|
+
top: 12px;
|
|
54
|
+
left: 50%;
|
|
55
|
+
transform: translateX(-50%);
|
|
56
|
+
z-index: 90;
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
font-family: 'Segoe UI', sans-serif;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 8px;
|
|
62
|
+
background: rgba(0,0,0,0.5);
|
|
63
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
64
|
+
border-radius: 20px;
|
|
65
|
+
padding: 4px 14px;
|
|
66
|
+
backdrop-filter: blur(6px);
|
|
67
|
+
opacity: 0;
|
|
68
|
+
transition: opacity 0.3s ease;
|
|
69
|
+
}
|
|
70
|
+
.sky-time-hud.visible { opacity: 1; }
|
|
71
|
+
.sky-time-icon { font-size: 16px; }
|
|
72
|
+
.sky-time-label {
|
|
73
|
+
font-size: 12px;
|
|
74
|
+
color: #ddd;
|
|
75
|
+
font-variant-numeric: tabular-nums;
|
|
76
|
+
min-width: 60px;
|
|
77
|
+
text-align: center;
|
|
78
|
+
}
|
|
79
|
+
.sky-time-bar {
|
|
80
|
+
width: 80px;
|
|
81
|
+
height: 4px;
|
|
82
|
+
background: rgba(255,255,255,0.15);
|
|
83
|
+
border-radius: 2px;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
}
|
|
86
|
+
.sky-time-fill {
|
|
87
|
+
height: 100%;
|
|
88
|
+
background: linear-gradient(90deg, #1a1a3a 0%, #ff7744 25%, #4a90d9 50%, #ff6633 75%, #1a1a3a 100%);
|
|
89
|
+
border-radius: 2px;
|
|
90
|
+
transition: width 0.5s linear;
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create the sky dome and star field. Call once during city init.
|
|
96
|
+
* @param {THREE.Scene} scene
|
|
97
|
+
* @param {number} radius - Sky dome radius (default 500)
|
|
98
|
+
* @returns {{ skyDome: THREE.Mesh, starField: THREE.Points }}
|
|
99
|
+
*/
|
|
100
|
+
export function createSky(scene, radius) {
|
|
101
|
+
const r = radius || 500;
|
|
102
|
+
|
|
103
|
+
// Sky dome — large inverted sphere
|
|
104
|
+
const skyGeo = new THREE.SphereGeometry(r, 32, 16);
|
|
105
|
+
const skyMat = new THREE.MeshBasicMaterial({
|
|
106
|
+
color: SKY_COLORS.noon,
|
|
107
|
+
side: THREE.BackSide,
|
|
108
|
+
fog: false,
|
|
109
|
+
});
|
|
110
|
+
skyDome = new THREE.Mesh(skyGeo, skyMat);
|
|
111
|
+
skyDome.name = 'sky-dome';
|
|
112
|
+
skyDome.renderOrder = -1;
|
|
113
|
+
scene.add(skyDome);
|
|
114
|
+
|
|
115
|
+
// Star field — point cloud (only visible at night)
|
|
116
|
+
const starCount = 500;
|
|
117
|
+
const starGeo = new THREE.BufferGeometry();
|
|
118
|
+
const positions = new Float32Array(starCount * 3);
|
|
119
|
+
for (let i = 0; i < starCount; i++) {
|
|
120
|
+
const theta = Math.random() * Math.PI * 2;
|
|
121
|
+
const phi = Math.acos(Math.random() * 0.8 + 0.2); // Upper hemisphere only
|
|
122
|
+
const sr = r * 0.95;
|
|
123
|
+
positions[i * 3] = sr * Math.sin(phi) * Math.cos(theta);
|
|
124
|
+
positions[i * 3 + 1] = sr * Math.cos(phi);
|
|
125
|
+
positions[i * 3 + 2] = sr * Math.sin(phi) * Math.sin(theta);
|
|
126
|
+
}
|
|
127
|
+
starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
128
|
+
const starMat = new THREE.PointsMaterial({
|
|
129
|
+
color: 0xffffff,
|
|
130
|
+
size: 1.5,
|
|
131
|
+
transparent: true,
|
|
132
|
+
opacity: 0,
|
|
133
|
+
fog: false,
|
|
134
|
+
sizeAttenuation: false,
|
|
135
|
+
});
|
|
136
|
+
starField = new THREE.Points(starGeo, starMat);
|
|
137
|
+
starField.name = 'star-field';
|
|
138
|
+
scene.add(starField);
|
|
139
|
+
|
|
140
|
+
// Time HUD
|
|
141
|
+
initTimeHUD();
|
|
142
|
+
|
|
143
|
+
return { skyDome, starField };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Update sky colors and stars based on current game time.
|
|
148
|
+
* Call once per frame (or at 10Hz for performance).
|
|
149
|
+
* @param {number} dt - Delta time in seconds
|
|
150
|
+
* @param {THREE.Scene} scene - For fog color updates
|
|
151
|
+
*/
|
|
152
|
+
export function updateSky(dt, scene) {
|
|
153
|
+
if (paused) return;
|
|
154
|
+
gameTime = (gameTime + timeSpeed * dt) % 1;
|
|
155
|
+
|
|
156
|
+
// Interpolate sky color
|
|
157
|
+
const skyColor = getSkyColorAtTime(gameTime);
|
|
158
|
+
if (skyDome) skyDome.material.color.copy(skyColor);
|
|
159
|
+
|
|
160
|
+
// Fog
|
|
161
|
+
if (scene && scene.fog) {
|
|
162
|
+
const fogColor = getFogColorAtTime(gameTime);
|
|
163
|
+
scene.fog.color.copy(fogColor);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Stars: fade in at night (0.8-1.0 and 0.0-0.2)
|
|
167
|
+
if (starField) {
|
|
168
|
+
const nightness = getNightness(gameTime);
|
|
169
|
+
starField.material.opacity = nightness;
|
|
170
|
+
starField.visible = nightness > 0.01;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Update HUD
|
|
174
|
+
updateTimeHUD();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get current time of day (0-1). 0=midnight, 0.5=noon.
|
|
179
|
+
* @returns {number}
|
|
180
|
+
*/
|
|
181
|
+
export function getTimeOfDay() {
|
|
182
|
+
return gameTime;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Set game time directly.
|
|
187
|
+
* @param {number} t - 0-1
|
|
188
|
+
*/
|
|
189
|
+
export function setTimeOfDay(t) {
|
|
190
|
+
gameTime = ((t % 1) + 1) % 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Set the speed of the day/night cycle.
|
|
195
|
+
* @param {number} cycleDurationSeconds - Full cycle duration (default 600)
|
|
196
|
+
*/
|
|
197
|
+
export function setCycleSpeed(cycleDurationSeconds) {
|
|
198
|
+
timeSpeed = 1 / (cycleDurationSeconds || 600);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Pause/resume the cycle.
|
|
203
|
+
* @param {boolean} p
|
|
204
|
+
*/
|
|
205
|
+
export function setPaused(p) {
|
|
206
|
+
paused = p;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Is it currently night? (for street lights, windows, agent behavior)
|
|
211
|
+
* @returns {boolean}
|
|
212
|
+
*/
|
|
213
|
+
export function isNight() {
|
|
214
|
+
return gameTime < 0.22 || gameTime > 0.78;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get nightness factor (0 = full day, 1 = full night).
|
|
219
|
+
* Smooth transition during dawn/dusk.
|
|
220
|
+
* @param {number} t
|
|
221
|
+
* @returns {number}
|
|
222
|
+
*/
|
|
223
|
+
function getNightness(t) {
|
|
224
|
+
// Night: 0.85-1.0 and 0.0-0.15 = full night
|
|
225
|
+
// Dawn: 0.15-0.3, Dusk: 0.7-0.85 = transition
|
|
226
|
+
if (t < 0.15) return 1;
|
|
227
|
+
if (t < 0.3) return 1 - (t - 0.15) / 0.15;
|
|
228
|
+
if (t < 0.7) return 0;
|
|
229
|
+
if (t < 0.85) return (t - 0.7) / 0.15;
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getSkyColorAtTime(t) {
|
|
234
|
+
const c = new THREE.Color();
|
|
235
|
+
if (t < 0.2) c.lerpColors(SKY_COLORS.midnight, SKY_COLORS.dawn, t / 0.2);
|
|
236
|
+
else if (t < 0.35) c.lerpColors(SKY_COLORS.dawn, SKY_COLORS.morning, (t - 0.2) / 0.15);
|
|
237
|
+
else if (t < 0.5) c.lerpColors(SKY_COLORS.morning, SKY_COLORS.noon, (t - 0.35) / 0.15);
|
|
238
|
+
else if (t < 0.65) c.lerpColors(SKY_COLORS.noon, SKY_COLORS.afternoon, (t - 0.5) / 0.15);
|
|
239
|
+
else if (t < 0.8) c.lerpColors(SKY_COLORS.afternoon, SKY_COLORS.dusk, (t - 0.65) / 0.15);
|
|
240
|
+
else if (t < 0.9) c.lerpColors(SKY_COLORS.dusk, SKY_COLORS.evening, (t - 0.8) / 0.1);
|
|
241
|
+
else c.lerpColors(SKY_COLORS.evening, SKY_COLORS.midnight, (t - 0.9) / 0.1);
|
|
242
|
+
return c;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getFogColorAtTime(t) {
|
|
246
|
+
const c = new THREE.Color();
|
|
247
|
+
if (t < 0.25) c.lerpColors(FOG_COLORS.midnight, FOG_COLORS.dawn, t / 0.25);
|
|
248
|
+
else if (t < 0.5) c.lerpColors(FOG_COLORS.dawn, FOG_COLORS.noon, (t - 0.25) / 0.25);
|
|
249
|
+
else if (t < 0.75) c.lerpColors(FOG_COLORS.noon, FOG_COLORS.dusk, (t - 0.5) / 0.25);
|
|
250
|
+
else c.lerpColors(FOG_COLORS.dusk, FOG_COLORS.midnight, (t - 0.75) / 0.25);
|
|
251
|
+
return c;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getTimeLabel(t) {
|
|
255
|
+
const hours = Math.floor(t * 24);
|
|
256
|
+
const minutes = Math.floor((t * 24 - hours) * 60);
|
|
257
|
+
const h12 = hours % 12 || 12;
|
|
258
|
+
const ampm = hours < 12 ? 'AM' : 'PM';
|
|
259
|
+
return h12 + ':' + (minutes < 10 ? '0' : '') + minutes + ' ' + ampm;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getTimeIcon(t) {
|
|
263
|
+
if (t < 0.22 || t > 0.78) return '\u{1F319}'; // crescent moon
|
|
264
|
+
if (t < 0.3 || t > 0.7) return '\u{1F305}'; // sunrise/sunset
|
|
265
|
+
return '\u{2600}'; // sun
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function initTimeHUD() {
|
|
269
|
+
if (timeHudEl) return;
|
|
270
|
+
const style = document.createElement('style');
|
|
271
|
+
style.textContent = TIME_HUD_STYLES;
|
|
272
|
+
document.head.appendChild(style);
|
|
273
|
+
|
|
274
|
+
timeHudEl = document.createElement('div');
|
|
275
|
+
timeHudEl.className = 'sky-time-hud';
|
|
276
|
+
timeHudEl.innerHTML = `
|
|
277
|
+
<span class="sky-time-icon" id="sky-time-icon"></span>
|
|
278
|
+
<span class="sky-time-label" id="sky-time-label">12:00 PM</span>
|
|
279
|
+
<div class="sky-time-bar">
|
|
280
|
+
<div class="sky-time-fill" id="sky-time-fill" style="width:50%"></div>
|
|
281
|
+
</div>
|
|
282
|
+
`;
|
|
283
|
+
document.body.appendChild(timeHudEl);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function updateTimeHUD() {
|
|
287
|
+
if (!timeHudEl) return;
|
|
288
|
+
const iconEl = document.getElementById('sky-time-icon');
|
|
289
|
+
const labelEl = document.getElementById('sky-time-label');
|
|
290
|
+
const fillEl = document.getElementById('sky-time-fill');
|
|
291
|
+
if (iconEl) iconEl.textContent = getTimeIcon(gameTime);
|
|
292
|
+
if (labelEl) labelEl.textContent = getTimeLabel(gameTime);
|
|
293
|
+
if (fillEl) fillEl.style.width = (gameTime * 100) + '%';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Show the time HUD.
|
|
298
|
+
*/
|
|
299
|
+
export function showTimeHUD() {
|
|
300
|
+
if (!timeHudEl) initTimeHUD();
|
|
301
|
+
timeHudEl.classList.add('visible');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Hide the time HUD.
|
|
306
|
+
*/
|
|
307
|
+
export function hideTimeHUD() {
|
|
308
|
+
if (timeHudEl) timeHudEl.classList.remove('visible');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Dispose sky system.
|
|
313
|
+
* @param {THREE.Scene} scene
|
|
314
|
+
*/
|
|
315
|
+
export function disposeSky(scene) {
|
|
316
|
+
if (skyDome) { scene.remove(skyDome); skyDome.geometry.dispose(); skyDome.material.dispose(); skyDome = null; }
|
|
317
|
+
if (starField) { scene.remove(starField); starField.geometry.dispose(); starField.material.dispose(); starField = null; }
|
|
318
|
+
if (timeHudEl) { timeHudEl.remove(); timeHudEl = null; }
|
|
319
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Street furniture — small detail props that make the city feel alive.
|
|
5
|
+
* Crosswalks, benches, bus stops, trash cans, fire hydrants, street signs, awnings.
|
|
6
|
+
* All use InstancedMesh for 120fps performance.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const _dummy = new THREE.Object3D();
|
|
10
|
+
const pools = {};
|
|
11
|
+
|
|
12
|
+
// District color palettes for building variety
|
|
13
|
+
export const DISTRICT_PALETTES = {
|
|
14
|
+
downtown: [0x4a6fa5, 0x5577aa, 0x3d5f8a, 0x6688bb, 0x4477aa, 0x556699],
|
|
15
|
+
industrial: [0x8a7755, 0x997744, 0x776644, 0x887766, 0x665533, 0x998866],
|
|
16
|
+
residential: [0xcc9977, 0xddaa88, 0xbb8866, 0xeebbaa, 0xddbb99, 0xccaa88],
|
|
17
|
+
campus: [0x55aa77, 0x66bb88, 0x449966, 0x77cc99, 0x55bb77, 0x66aa88],
|
|
18
|
+
commercial: [0xaa6688, 0xbb7799, 0x995577, 0xcc88aa, 0xaa7799, 0xbb6688],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get a randomized building color from a district palette.
|
|
23
|
+
* @param {string} district
|
|
24
|
+
* @param {number} seed - Building index for deterministic randomization
|
|
25
|
+
* @returns {THREE.Color}
|
|
26
|
+
*/
|
|
27
|
+
export function getBuildingColor(district, seed) {
|
|
28
|
+
const palette = DISTRICT_PALETTES[district] || DISTRICT_PALETTES.downtown;
|
|
29
|
+
const idx = Math.abs(seed * 2654435761 | 0) % palette.length; // hash-based pick
|
|
30
|
+
return new THREE.Color(palette[idx]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a bench mesh (low-poly).
|
|
35
|
+
* @param {THREE.Scene} scene
|
|
36
|
+
* @param {THREE.Vector3} position
|
|
37
|
+
* @param {number} rotation - Y rotation in radians
|
|
38
|
+
*/
|
|
39
|
+
export function createBench(scene, position, rotation) {
|
|
40
|
+
const group = new THREE.Group();
|
|
41
|
+
// Seat
|
|
42
|
+
const seat = new THREE.Mesh(
|
|
43
|
+
new THREE.BoxGeometry(1.2, 0.08, 0.4),
|
|
44
|
+
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
|
|
45
|
+
);
|
|
46
|
+
seat.position.y = 0.4;
|
|
47
|
+
group.add(seat);
|
|
48
|
+
// Back
|
|
49
|
+
const back = new THREE.Mesh(
|
|
50
|
+
new THREE.BoxGeometry(1.2, 0.4, 0.06),
|
|
51
|
+
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
|
|
52
|
+
);
|
|
53
|
+
back.position.set(0, 0.6, -0.17);
|
|
54
|
+
group.add(back);
|
|
55
|
+
// Legs (2)
|
|
56
|
+
const legGeo = new THREE.BoxGeometry(0.06, 0.4, 0.35);
|
|
57
|
+
const legMat = new THREE.MeshLambertMaterial({ color: 0x333333 });
|
|
58
|
+
const leg1 = new THREE.Mesh(legGeo, legMat);
|
|
59
|
+
leg1.position.set(-0.45, 0.2, 0);
|
|
60
|
+
group.add(leg1);
|
|
61
|
+
const leg2 = new THREE.Mesh(legGeo, legMat);
|
|
62
|
+
leg2.position.set(0.45, 0.2, 0);
|
|
63
|
+
group.add(leg2);
|
|
64
|
+
|
|
65
|
+
group.position.copy(position);
|
|
66
|
+
group.rotation.y = rotation || 0;
|
|
67
|
+
group.matrixAutoUpdate = false;
|
|
68
|
+
group.updateMatrix();
|
|
69
|
+
scene.add(group);
|
|
70
|
+
return group;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a trash can (cylinder).
|
|
75
|
+
* @param {THREE.Scene} scene
|
|
76
|
+
* @param {THREE.Vector3} position
|
|
77
|
+
*/
|
|
78
|
+
export function createTrashCan(scene, position) {
|
|
79
|
+
const group = new THREE.Group();
|
|
80
|
+
const body = new THREE.Mesh(
|
|
81
|
+
new THREE.CylinderGeometry(0.15, 0.15, 0.5, 8),
|
|
82
|
+
new THREE.MeshLambertMaterial({ color: 0x555555 })
|
|
83
|
+
);
|
|
84
|
+
body.position.y = 0.25;
|
|
85
|
+
group.add(body);
|
|
86
|
+
// Lid
|
|
87
|
+
const lid = new THREE.Mesh(
|
|
88
|
+
new THREE.CylinderGeometry(0.17, 0.17, 0.04, 8),
|
|
89
|
+
new THREE.MeshLambertMaterial({ color: 0x666666 })
|
|
90
|
+
);
|
|
91
|
+
lid.position.y = 0.52;
|
|
92
|
+
group.add(lid);
|
|
93
|
+
|
|
94
|
+
group.position.copy(position);
|
|
95
|
+
group.matrixAutoUpdate = false;
|
|
96
|
+
group.updateMatrix();
|
|
97
|
+
scene.add(group);
|
|
98
|
+
return group;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a fire hydrant.
|
|
103
|
+
* @param {THREE.Scene} scene
|
|
104
|
+
* @param {THREE.Vector3} position
|
|
105
|
+
*/
|
|
106
|
+
export function createFireHydrant(scene, position) {
|
|
107
|
+
const group = new THREE.Group();
|
|
108
|
+
// Body
|
|
109
|
+
const body = new THREE.Mesh(
|
|
110
|
+
new THREE.CylinderGeometry(0.08, 0.1, 0.4, 8),
|
|
111
|
+
new THREE.MeshLambertMaterial({ color: 0xcc2222 })
|
|
112
|
+
);
|
|
113
|
+
body.position.y = 0.2;
|
|
114
|
+
group.add(body);
|
|
115
|
+
// Top
|
|
116
|
+
const top = new THREE.Mesh(
|
|
117
|
+
new THREE.CylinderGeometry(0.1, 0.08, 0.1, 8),
|
|
118
|
+
new THREE.MeshLambertMaterial({ color: 0xcc2222 })
|
|
119
|
+
);
|
|
120
|
+
top.position.y = 0.45;
|
|
121
|
+
group.add(top);
|
|
122
|
+
// Side nozzle
|
|
123
|
+
const nozzle = new THREE.Mesh(
|
|
124
|
+
new THREE.CylinderGeometry(0.03, 0.03, 0.12, 6),
|
|
125
|
+
new THREE.MeshLambertMaterial({ color: 0xdddd33 })
|
|
126
|
+
);
|
|
127
|
+
nozzle.rotation.z = Math.PI / 2;
|
|
128
|
+
nozzle.position.set(0.12, 0.3, 0);
|
|
129
|
+
group.add(nozzle);
|
|
130
|
+
|
|
131
|
+
group.position.copy(position);
|
|
132
|
+
group.matrixAutoUpdate = false;
|
|
133
|
+
group.updateMatrix();
|
|
134
|
+
scene.add(group);
|
|
135
|
+
return group;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a bus stop shelter.
|
|
140
|
+
* @param {THREE.Scene} scene
|
|
141
|
+
* @param {THREE.Vector3} position
|
|
142
|
+
* @param {number} rotation
|
|
143
|
+
*/
|
|
144
|
+
export function createBusStop(scene, position, rotation) {
|
|
145
|
+
const group = new THREE.Group();
|
|
146
|
+
const mat = new THREE.MeshLambertMaterial({ color: 0x4488cc });
|
|
147
|
+
// Roof
|
|
148
|
+
const roof = new THREE.Mesh(new THREE.BoxGeometry(2, 0.06, 1), mat);
|
|
149
|
+
roof.position.y = 2.2;
|
|
150
|
+
group.add(roof);
|
|
151
|
+
// Back wall (glass-like)
|
|
152
|
+
const glass = new THREE.Mesh(
|
|
153
|
+
new THREE.BoxGeometry(2, 2, 0.04),
|
|
154
|
+
new THREE.MeshLambertMaterial({ color: 0x88ccff, transparent: true, opacity: 0.3 })
|
|
155
|
+
);
|
|
156
|
+
glass.position.set(0, 1.1, -0.48);
|
|
157
|
+
group.add(glass);
|
|
158
|
+
// Posts
|
|
159
|
+
const postGeo = new THREE.BoxGeometry(0.06, 2.2, 0.06);
|
|
160
|
+
const postMat = new THREE.MeshLambertMaterial({ color: 0x333333 });
|
|
161
|
+
[-0.95, 0.95].forEach(x => {
|
|
162
|
+
const post = new THREE.Mesh(postGeo, postMat);
|
|
163
|
+
post.position.set(x, 1.1, 0.45);
|
|
164
|
+
group.add(post);
|
|
165
|
+
});
|
|
166
|
+
// Bench inside
|
|
167
|
+
const bench = new THREE.Mesh(
|
|
168
|
+
new THREE.BoxGeometry(1.6, 0.06, 0.3),
|
|
169
|
+
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
|
|
170
|
+
);
|
|
171
|
+
bench.position.set(0, 0.5, -0.25);
|
|
172
|
+
group.add(bench);
|
|
173
|
+
|
|
174
|
+
group.position.copy(position);
|
|
175
|
+
group.rotation.y = rotation || 0;
|
|
176
|
+
group.matrixAutoUpdate = false;
|
|
177
|
+
group.updateMatrix();
|
|
178
|
+
scene.add(group);
|
|
179
|
+
return group;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create a crosswalk on the road surface.
|
|
184
|
+
* @param {THREE.Scene} scene
|
|
185
|
+
* @param {THREE.Vector3} position
|
|
186
|
+
* @param {number} rotation
|
|
187
|
+
*/
|
|
188
|
+
export function createCrosswalk(scene, position, rotation) {
|
|
189
|
+
const group = new THREE.Group();
|
|
190
|
+
const stripeMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
|
191
|
+
const stripeGeo = new THREE.BoxGeometry(0.3, 0.005, 2);
|
|
192
|
+
for (let i = -3; i <= 3; i++) {
|
|
193
|
+
const stripe = new THREE.Mesh(stripeGeo, stripeMat);
|
|
194
|
+
stripe.position.set(i * 0.5, 0.01, 0);
|
|
195
|
+
group.add(stripe);
|
|
196
|
+
}
|
|
197
|
+
group.position.copy(position);
|
|
198
|
+
group.rotation.y = rotation || 0;
|
|
199
|
+
group.matrixAutoUpdate = false;
|
|
200
|
+
group.updateMatrix();
|
|
201
|
+
scene.add(group);
|
|
202
|
+
return group;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create an awning/canopy at a building entrance.
|
|
207
|
+
* @param {THREE.Scene} scene
|
|
208
|
+
* @param {THREE.Vector3} position
|
|
209
|
+
* @param {number} rotation
|
|
210
|
+
* @param {number} color - Hex color
|
|
211
|
+
*/
|
|
212
|
+
export function createAwning(scene, position, rotation, color) {
|
|
213
|
+
const group = new THREE.Group();
|
|
214
|
+
const awning = new THREE.Mesh(
|
|
215
|
+
new THREE.BoxGeometry(1.5, 0.05, 1),
|
|
216
|
+
new THREE.MeshLambertMaterial({ color: color || 0xcc4444 })
|
|
217
|
+
);
|
|
218
|
+
awning.position.set(0, 2.5, 0.5);
|
|
219
|
+
group.add(awning);
|
|
220
|
+
// Support poles
|
|
221
|
+
const poleMat = new THREE.MeshLambertMaterial({ color: 0x444444 });
|
|
222
|
+
const poleGeo = new THREE.CylinderGeometry(0.03, 0.03, 0.8, 6);
|
|
223
|
+
[-0.6, 0.6].forEach(x => {
|
|
224
|
+
const pole = new THREE.Mesh(poleGeo, poleMat);
|
|
225
|
+
pole.position.set(x, 2.1, 0.95);
|
|
226
|
+
group.add(pole);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
group.position.copy(position);
|
|
230
|
+
group.rotation.y = rotation || 0;
|
|
231
|
+
group.matrixAutoUpdate = false;
|
|
232
|
+
group.updateMatrix();
|
|
233
|
+
scene.add(group);
|
|
234
|
+
return group;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create a street name sign.
|
|
239
|
+
* @param {THREE.Scene} scene
|
|
240
|
+
* @param {THREE.Vector3} position
|
|
241
|
+
* @param {string} streetName
|
|
242
|
+
*/
|
|
243
|
+
export function createStreetSign(scene, position, streetName) {
|
|
244
|
+
const group = new THREE.Group();
|
|
245
|
+
// Pole
|
|
246
|
+
const pole = new THREE.Mesh(
|
|
247
|
+
new THREE.CylinderGeometry(0.03, 0.03, 3, 6),
|
|
248
|
+
new THREE.MeshLambertMaterial({ color: 0x666666 })
|
|
249
|
+
);
|
|
250
|
+
pole.position.y = 1.5;
|
|
251
|
+
group.add(pole);
|
|
252
|
+
// Sign plate
|
|
253
|
+
const plate = new THREE.Mesh(
|
|
254
|
+
new THREE.BoxGeometry(1.2, 0.3, 0.04),
|
|
255
|
+
new THREE.MeshLambertMaterial({ color: 0x225588 })
|
|
256
|
+
);
|
|
257
|
+
plate.position.set(0, 3, 0);
|
|
258
|
+
group.add(plate);
|
|
259
|
+
|
|
260
|
+
group.position.copy(position);
|
|
261
|
+
group.matrixAutoUpdate = false;
|
|
262
|
+
group.updateMatrix();
|
|
263
|
+
scene.add(group);
|
|
264
|
+
return group;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Populate a city block with street furniture.
|
|
269
|
+
* Call this per block to add variety.
|
|
270
|
+
* @param {THREE.Scene} scene
|
|
271
|
+
* @param {number} blockX - Block center X
|
|
272
|
+
* @param {number} blockZ - Block center Z
|
|
273
|
+
* @param {number} blockSize - Block dimension
|
|
274
|
+
* @param {number} seed - For deterministic randomization
|
|
275
|
+
*/
|
|
276
|
+
export function populateBlock(scene, blockX, blockZ, blockSize, seed) {
|
|
277
|
+
const rng = seedRng(seed);
|
|
278
|
+
const half = blockSize / 2;
|
|
279
|
+
const items = [];
|
|
280
|
+
|
|
281
|
+
// Benches on sidewalks (2 per block)
|
|
282
|
+
if (rng() > 0.3) {
|
|
283
|
+
items.push(createBench(scene, new THREE.Vector3(blockX + half - 0.5, 0, blockZ + rng() * blockSize - half), 0));
|
|
284
|
+
}
|
|
285
|
+
if (rng() > 0.3) {
|
|
286
|
+
items.push(createBench(scene, new THREE.Vector3(blockX - half + 0.5, 0, blockZ + rng() * blockSize - half), Math.PI));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Trash can (1 per block)
|
|
290
|
+
if (rng() > 0.4) {
|
|
291
|
+
items.push(createTrashCan(scene, new THREE.Vector3(blockX + half - 0.3, 0, blockZ + half - 0.3)));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Fire hydrant (every other block)
|
|
295
|
+
if (rng() > 0.5) {
|
|
296
|
+
items.push(createFireHydrant(scene, new THREE.Vector3(blockX - half + 0.3, 0, blockZ - half + 0.5)));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return items;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function seedRng(seed) {
|
|
303
|
+
let s = seed | 0;
|
|
304
|
+
return function() {
|
|
305
|
+
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
|
306
|
+
return s / 0x7fffffff;
|
|
307
|
+
};
|
|
308
|
+
}
|