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,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
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Car HUD — In-vehicle dashboard overlay for AI City.
|
|
3
|
+
* HTML overlay on top of the Three.js canvas.
|
|
4
|
+
* Shows: speedometer, minimap, agent activity radio feed.
|
|
5
|
+
* Target: zero impact on 120fps (pure HTML/CSS, no canvas rendering).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
let hudEl = null;
|
|
9
|
+
let radioInterval = null;
|
|
10
|
+
let visible = false;
|
|
11
|
+
|
|
12
|
+
const HUD_STYLES = `
|
|
13
|
+
.car-hud {
|
|
14
|
+
position: fixed;
|
|
15
|
+
bottom: 0;
|
|
16
|
+
left: 0;
|
|
17
|
+
right: 0;
|
|
18
|
+
height: 120px;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
z-index: 100;
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: flex-end;
|
|
23
|
+
justify-content: space-between;
|
|
24
|
+
padding: 0 20px 12px;
|
|
25
|
+
background: linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 60%, transparent 100%);
|
|
26
|
+
font-family: 'Segoe UI', sans-serif;
|
|
27
|
+
color: #fff;
|
|
28
|
+
opacity: 0;
|
|
29
|
+
transition: opacity 0.3s ease;
|
|
30
|
+
}
|
|
31
|
+
.car-hud.visible { opacity: 1; }
|
|
32
|
+
|
|
33
|
+
.car-hud-speedo {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
align-items: center;
|
|
37
|
+
min-width: 100px;
|
|
38
|
+
}
|
|
39
|
+
.car-hud-speed-value {
|
|
40
|
+
font-size: 36px;
|
|
41
|
+
font-weight: 700;
|
|
42
|
+
font-variant-numeric: tabular-nums;
|
|
43
|
+
text-shadow: 0 0 8px rgba(212,175,55,0.6);
|
|
44
|
+
color: #FFD700;
|
|
45
|
+
}
|
|
46
|
+
.car-hud-speed-label {
|
|
47
|
+
font-size: 10px;
|
|
48
|
+
text-transform: uppercase;
|
|
49
|
+
letter-spacing: 2px;
|
|
50
|
+
color: #aaa;
|
|
51
|
+
margin-top: -2px;
|
|
52
|
+
}
|
|
53
|
+
.car-hud-gear {
|
|
54
|
+
font-size: 14px;
|
|
55
|
+
color: #D4AF37;
|
|
56
|
+
margin-top: 4px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.car-hud-radio {
|
|
60
|
+
flex: 1;
|
|
61
|
+
max-width: 400px;
|
|
62
|
+
margin: 0 24px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
}
|
|
65
|
+
.car-hud-radio-title {
|
|
66
|
+
font-size: 10px;
|
|
67
|
+
text-transform: uppercase;
|
|
68
|
+
letter-spacing: 2px;
|
|
69
|
+
color: #D4AF37;
|
|
70
|
+
margin-bottom: 4px;
|
|
71
|
+
}
|
|
72
|
+
.car-hud-radio-feed {
|
|
73
|
+
font-size: 12px;
|
|
74
|
+
line-height: 1.4;
|
|
75
|
+
color: #ccc;
|
|
76
|
+
max-height: 70px;
|
|
77
|
+
overflow-y: hidden;
|
|
78
|
+
}
|
|
79
|
+
.car-hud-radio-item {
|
|
80
|
+
padding: 2px 0;
|
|
81
|
+
opacity: 0;
|
|
82
|
+
animation: radioFadeIn 0.3s forwards;
|
|
83
|
+
}
|
|
84
|
+
.car-hud-radio-item .agent-name {
|
|
85
|
+
color: #FFD700;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
}
|
|
88
|
+
.car-hud-radio-item .action {
|
|
89
|
+
color: #aaa;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.car-hud-minimap {
|
|
93
|
+
width: 100px;
|
|
94
|
+
height: 100px;
|
|
95
|
+
border: 1px solid rgba(212,175,55,0.4);
|
|
96
|
+
border-radius: 4px;
|
|
97
|
+
background: rgba(0,0,0,0.5);
|
|
98
|
+
position: relative;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
}
|
|
101
|
+
.car-hud-minimap-dot {
|
|
102
|
+
position: absolute;
|
|
103
|
+
width: 6px;
|
|
104
|
+
height: 6px;
|
|
105
|
+
border-radius: 50%;
|
|
106
|
+
background: #FFD700;
|
|
107
|
+
transform: translate(-50%, -50%);
|
|
108
|
+
box-shadow: 0 0 4px rgba(212,175,55,0.8);
|
|
109
|
+
}
|
|
110
|
+
.car-hud-minimap-dot.agent {
|
|
111
|
+
width: 4px;
|
|
112
|
+
height: 4px;
|
|
113
|
+
background: #3fb950;
|
|
114
|
+
box-shadow: 0 0 3px rgba(63,185,80,0.6);
|
|
115
|
+
}
|
|
116
|
+
.car-hud-minimap-dot.building {
|
|
117
|
+
width: 8px;
|
|
118
|
+
height: 8px;
|
|
119
|
+
border-radius: 1px;
|
|
120
|
+
background: rgba(255,255,255,0.15);
|
|
121
|
+
box-shadow: none;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.car-hud-controls {
|
|
125
|
+
position: fixed;
|
|
126
|
+
bottom: 130px;
|
|
127
|
+
left: 50%;
|
|
128
|
+
transform: translateX(-50%);
|
|
129
|
+
font-size: 11px;
|
|
130
|
+
color: #888;
|
|
131
|
+
text-align: center;
|
|
132
|
+
pointer-events: none;
|
|
133
|
+
z-index: 100;
|
|
134
|
+
opacity: 0;
|
|
135
|
+
transition: opacity 0.3s ease;
|
|
136
|
+
}
|
|
137
|
+
.car-hud-controls.visible { opacity: 1; }
|
|
138
|
+
.car-hud-controls kbd {
|
|
139
|
+
background: rgba(255,255,255,0.1);
|
|
140
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
141
|
+
border-radius: 3px;
|
|
142
|
+
padding: 1px 5px;
|
|
143
|
+
font-family: monospace;
|
|
144
|
+
color: #ccc;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@keyframes radioFadeIn {
|
|
148
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
149
|
+
to { opacity: 1; transform: translateY(0); }
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Initialize the car HUD. Call once on page load.
|
|
155
|
+
* Creates DOM elements but keeps them hidden until showHUD() is called.
|
|
156
|
+
*/
|
|
157
|
+
export function initCarHUD() {
|
|
158
|
+
if (hudEl) return;
|
|
159
|
+
|
|
160
|
+
// Inject styles
|
|
161
|
+
const style = document.createElement('style');
|
|
162
|
+
style.textContent = HUD_STYLES;
|
|
163
|
+
document.head.appendChild(style);
|
|
164
|
+
|
|
165
|
+
// Create HUD container
|
|
166
|
+
hudEl = document.createElement('div');
|
|
167
|
+
hudEl.className = 'car-hud';
|
|
168
|
+
hudEl.innerHTML = `
|
|
169
|
+
<div class="car-hud-speedo">
|
|
170
|
+
<div class="car-hud-speed-value" id="car-speed">0</div>
|
|
171
|
+
<div class="car-hud-speed-label">km/h</div>
|
|
172
|
+
<div class="car-hud-gear" id="car-gear">P</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="car-hud-radio">
|
|
175
|
+
<div class="car-hud-radio-title">Agent Radio</div>
|
|
176
|
+
<div class="car-hud-radio-feed" id="car-radio-feed"></div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="car-hud-minimap" id="car-minimap">
|
|
179
|
+
<div class="car-hud-minimap-dot" id="car-minimap-player" style="left:50%;top:50%"></div>
|
|
180
|
+
</div>
|
|
181
|
+
`;
|
|
182
|
+
document.body.appendChild(hudEl);
|
|
183
|
+
|
|
184
|
+
// Controls hint
|
|
185
|
+
const controls = document.createElement('div');
|
|
186
|
+
controls.className = 'car-hud-controls';
|
|
187
|
+
controls.id = 'car-hud-controls';
|
|
188
|
+
controls.innerHTML = '<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> Drive <kbd>Space</kbd> Brake <kbd>E</kbd> Exit vehicle';
|
|
189
|
+
document.body.appendChild(controls);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Show the car HUD (when entering a vehicle).
|
|
194
|
+
*/
|
|
195
|
+
export function showHUD() {
|
|
196
|
+
if (!hudEl) initCarHUD();
|
|
197
|
+
hudEl.classList.add('visible');
|
|
198
|
+
document.getElementById('car-hud-controls').classList.add('visible');
|
|
199
|
+
visible = true;
|
|
200
|
+
startRadioFeed();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Hide the car HUD (when exiting a vehicle).
|
|
205
|
+
*/
|
|
206
|
+
export function hideHUD() {
|
|
207
|
+
if (hudEl) hudEl.classList.remove('visible');
|
|
208
|
+
const ctrl = document.getElementById('car-hud-controls');
|
|
209
|
+
if (ctrl) ctrl.classList.remove('visible');
|
|
210
|
+
visible = false;
|
|
211
|
+
stopRadioFeed();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Update speedometer display.
|
|
216
|
+
* Call every frame from the vehicle update loop.
|
|
217
|
+
* @param {number} speed - Current speed (units/sec)
|
|
218
|
+
* @param {string} gear - Gear indicator ('D', 'R', 'P')
|
|
219
|
+
*/
|
|
220
|
+
export function updateSpeed(speed, gear) {
|
|
221
|
+
if (!visible) return;
|
|
222
|
+
const kmh = Math.round(speed * 3.6); // Convert units/s to km/h display
|
|
223
|
+
const el = document.getElementById('car-speed');
|
|
224
|
+
const gearEl = document.getElementById('car-gear');
|
|
225
|
+
if (el) el.textContent = kmh;
|
|
226
|
+
if (gearEl) gearEl.textContent = gear || 'D';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Update minimap with player and agent positions.
|
|
231
|
+
* Call at 1Hz (not every frame — DOM updates are expensive).
|
|
232
|
+
* @param {Object} playerPos - { x, z } player world position
|
|
233
|
+
* @param {Array<{ name: string, x: number, z: number, alive: boolean }>} agents - Agent positions
|
|
234
|
+
* @param {Array<{ x: number, z: number, w: number, h: number }>} buildings - Building footprints
|
|
235
|
+
* @param {number} mapScale - World units per minimap pixel (default 5)
|
|
236
|
+
*/
|
|
237
|
+
export function updateMinimap(playerPos, agents, buildings, mapScale) {
|
|
238
|
+
if (!visible) return;
|
|
239
|
+
const minimap = document.getElementById('car-minimap');
|
|
240
|
+
if (!minimap) return;
|
|
241
|
+
|
|
242
|
+
const scale = mapScale || 5;
|
|
243
|
+
const mapW = 100;
|
|
244
|
+
const mapH = 100;
|
|
245
|
+
const cx = mapW / 2;
|
|
246
|
+
const cy = mapH / 2;
|
|
247
|
+
|
|
248
|
+
// Clear existing dots (except player)
|
|
249
|
+
const existingDots = minimap.querySelectorAll('.agent, .building');
|
|
250
|
+
existingDots.forEach(d => d.remove());
|
|
251
|
+
|
|
252
|
+
// Buildings (static, relative to player)
|
|
253
|
+
if (buildings) {
|
|
254
|
+
for (const b of buildings) {
|
|
255
|
+
const dx = (b.x - playerPos.x) / scale + cx;
|
|
256
|
+
const dy = (b.z - playerPos.z) / scale + cy;
|
|
257
|
+
if (dx < -5 || dx > mapW + 5 || dy < -5 || dy > mapH + 5) continue;
|
|
258
|
+
const dot = document.createElement('div');
|
|
259
|
+
dot.className = 'car-hud-minimap-dot building';
|
|
260
|
+
dot.style.left = dx + 'px';
|
|
261
|
+
dot.style.top = dy + 'px';
|
|
262
|
+
minimap.appendChild(dot);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Agents
|
|
267
|
+
if (agents) {
|
|
268
|
+
for (const a of agents) {
|
|
269
|
+
if (!a.alive) continue;
|
|
270
|
+
const dx = (a.x - playerPos.x) / scale + cx;
|
|
271
|
+
const dy = (a.z - playerPos.z) / scale + cy;
|
|
272
|
+
if (dx < 0 || dx > mapW || dy < 0 || dy > mapH) continue;
|
|
273
|
+
const dot = document.createElement('div');
|
|
274
|
+
dot.className = 'car-hud-minimap-dot agent';
|
|
275
|
+
dot.style.left = dx + 'px';
|
|
276
|
+
dot.style.top = dy + 'px';
|
|
277
|
+
dot.title = a.name;
|
|
278
|
+
minimap.appendChild(dot);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Add a radio feed item (agent activity announcement).
|
|
285
|
+
* @param {string} agentName
|
|
286
|
+
* @param {string} action - e.g. "completed task #12", "pushed code", "joined #general"
|
|
287
|
+
*/
|
|
288
|
+
export function addRadioItem(agentName, action) {
|
|
289
|
+
if (!visible) return;
|
|
290
|
+
const feed = document.getElementById('car-radio-feed');
|
|
291
|
+
if (!feed) return;
|
|
292
|
+
|
|
293
|
+
const item = document.createElement('div');
|
|
294
|
+
item.className = 'car-hud-radio-item';
|
|
295
|
+
item.innerHTML = '<span class="agent-name">' + escapeHtml(agentName) + '</span> <span class="action">' + escapeHtml(action) + '</span>';
|
|
296
|
+
feed.insertBefore(item, feed.firstChild);
|
|
297
|
+
|
|
298
|
+
// Keep max 5 items
|
|
299
|
+
while (feed.children.length > 5) {
|
|
300
|
+
feed.removeChild(feed.lastChild);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Start polling the radio feed API.
|
|
306
|
+
*/
|
|
307
|
+
function startRadioFeed() {
|
|
308
|
+
if (radioInterval) return;
|
|
309
|
+
fetchRadio(); // Initial fetch
|
|
310
|
+
radioInterval = setInterval(fetchRadio, 5000); // Poll every 5s
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Stop polling the radio feed.
|
|
315
|
+
*/
|
|
316
|
+
function stopRadioFeed() {
|
|
317
|
+
if (radioInterval) {
|
|
318
|
+
clearInterval(radioInterval);
|
|
319
|
+
radioInterval = null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Fetch agent activity from the radio API.
|
|
325
|
+
*/
|
|
326
|
+
function fetchRadio() {
|
|
327
|
+
fetch('/api/city/radio')
|
|
328
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
329
|
+
.then(function(data) {
|
|
330
|
+
if (!data || !data.feed) return;
|
|
331
|
+
const feedItems = data.feed;
|
|
332
|
+
const feed = document.getElementById('car-radio-feed');
|
|
333
|
+
if (!feed) return;
|
|
334
|
+
// Only add new items
|
|
335
|
+
for (let i = feedItems.length - 1; i >= 0; i--) {
|
|
336
|
+
const item = feedItems[i];
|
|
337
|
+
addRadioItem(item.from, item.preview || 'active');
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
.catch(function() { /* silently fail — radio is non-critical */ });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Check if the HUD is currently visible.
|
|
345
|
+
* @returns {boolean}
|
|
346
|
+
*/
|
|
347
|
+
export function isHUDVisible() {
|
|
348
|
+
return visible;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Dispose the car HUD (cleanup).
|
|
353
|
+
*/
|
|
354
|
+
export function disposeCarHUD() {
|
|
355
|
+
hideHUD();
|
|
356
|
+
if (hudEl) {
|
|
357
|
+
hudEl.remove();
|
|
358
|
+
hudEl = null;
|
|
359
|
+
}
|
|
360
|
+
const ctrl = document.getElementById('car-hud-controls');
|
|
361
|
+
if (ctrl) ctrl.remove();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function escapeHtml(str) {
|
|
365
|
+
const div = document.createElement('div');
|
|
366
|
+
div.textContent = str;
|
|
367
|
+
return div.innerHTML;
|
|
368
|
+
}
|