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.
@@ -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
+ }