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
package/office/campus-env.js
CHANGED
|
@@ -86,6 +86,7 @@ export function buildCampusEnvironment() {
|
|
|
86
86
|
|
|
87
87
|
// ========== BAR & CAFÉ (back left) ==========
|
|
88
88
|
buildBar(-14, -12, walnutMat, chromeMat, neonBlueMat, neonPurpleMat);
|
|
89
|
+
buildJukebox(-10, -13.5); // Right side of bar area, against the back wall
|
|
89
90
|
|
|
90
91
|
// ========== RECREATION CENTER (back center) ==========
|
|
91
92
|
buildRecCenter(0, -12, walnutMat, chromeMat, carpetMat);
|
|
@@ -595,13 +596,13 @@ function buildGamingDesk(x, z, index) {
|
|
|
595
596
|
screen.position.set(0, 1.15, -0.234);
|
|
596
597
|
group.add(screen);
|
|
597
598
|
|
|
598
|
-
// Monitor stand (V-shaped, chrome)
|
|
599
|
+
// Monitor stand (V-shaped, chrome — positioned BEHIND the screen to avoid clipping)
|
|
599
600
|
var standMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.15, metalness: 0.7 });
|
|
600
601
|
var standArm = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.25, 0.04), standMat);
|
|
601
|
-
standArm.position.set(0, 0.92, -0.
|
|
602
|
+
standArm.position.set(0, 0.92, -0.27);
|
|
602
603
|
group.add(standArm);
|
|
603
604
|
var standBase = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.02, 0.15), standMat);
|
|
604
|
-
standBase.position.set(0, 0.78, -0.
|
|
605
|
+
standBase.position.set(0, 0.78, -0.27);
|
|
605
606
|
group.add(standBase);
|
|
606
607
|
|
|
607
608
|
// PC tower under desk (with RGB glow)
|
|
@@ -898,26 +899,25 @@ function buildManagerOffice(x, z, glassMat, frameMat, walnutMat, leatherMat, chr
|
|
|
898
899
|
cablePanel.position.set(0, 0.5, 0.9);
|
|
899
900
|
group.add(cablePanel);
|
|
900
901
|
|
|
901
|
-
// ---
|
|
902
|
+
// --- Single 47" ultrawide monitor (replaces dual setup per owner request) ---
|
|
902
903
|
var monMat = new THREE.MeshStandardMaterial({ color: 0x0a0a0a, roughness: 0.2 });
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
group.add(monArm);
|
|
904
|
+
// 47" = ~1.2m wide x 0.67m tall in world units (16:9 aspect ratio, scaled to desk)
|
|
905
|
+
var bigMon = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.67, 0.025), monMat);
|
|
906
|
+
bigMon.position.set(0, 1.2, 1.05); bigMon.castShadow = true;
|
|
907
|
+
group.add(bigMon);
|
|
908
|
+
// Screen (in FRONT of bezel — z offset +0.013 so stand doesn't clip through)
|
|
909
|
+
var scrMat = new THREE.MeshStandardMaterial({ color: 0x1a2a4a, emissive: 0x58a6ff, emissiveIntensity: 0.3, roughness: 0.1 });
|
|
910
|
+
var bigScr = new THREE.Mesh(new THREE.PlaneGeometry(1.14, 0.61), scrMat);
|
|
911
|
+
bigScr.position.set(0, 1.2, 1.063);
|
|
912
|
+
group.add(bigScr);
|
|
913
|
+
// Center stand (BEHIND screen — z offset -0.02 so it doesn't poke through)
|
|
914
|
+
var stand = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 0.28, 8), chromeMat);
|
|
915
|
+
stand.position.set(0, 0.92, 1.07);
|
|
916
|
+
group.add(stand);
|
|
917
|
+
// Stand base (BEHIND screen)
|
|
918
|
+
var standBase = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.02, 0.2), chromeMat);
|
|
919
|
+
standBase.position.set(0, 0.78, 1.07);
|
|
920
|
+
group.add(standBase);
|
|
921
921
|
|
|
922
922
|
// --- Keyboard + mouse ---
|
|
923
923
|
var kbMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.5 });
|
|
@@ -1082,9 +1082,10 @@ function buildManagerOffice(x, z, glassMat, frameMat, walnutMat, leatherMat, chr
|
|
|
1082
1082
|
S._managerOfficePos = { x: x, z: z };
|
|
1083
1083
|
|
|
1084
1084
|
// Register manager desk in deskMeshes so monitor screen system works
|
|
1085
|
+
// bigScr is the 47" monitor screen — used for click detection + iframe overlay
|
|
1085
1086
|
var mgrDeskIdx = CAMPUS_DESKS.length - 1;
|
|
1086
1087
|
var mgrScreenMat = new THREE.MeshStandardMaterial({ color: 0x333333, emissive: 0x333333, emissiveIntensity: 0.1, roughness: 0.2 });
|
|
1087
|
-
S.deskMeshes[mgrDeskIdx] = { group: group, screen:
|
|
1088
|
+
S.deskMeshes[mgrDeskIdx] = { group: group, screen: bigScr, screenMat: mgrScreenMat, index: mgrDeskIdx, x: x, z: z + 1.7 };
|
|
1088
1089
|
}
|
|
1089
1090
|
|
|
1090
1091
|
// ==================== DESIGNER STUDIO ====================
|
|
@@ -1199,6 +1200,101 @@ function buildBar(x, z, walnutMat, chromeMat, neonBlueMat, neonPurpleMat) {
|
|
|
1199
1200
|
S.furnitureGroup.add(sign);
|
|
1200
1201
|
}
|
|
1201
1202
|
|
|
1203
|
+
// ==================== JUKEBOX (Wurlitzer 1015 style) ====================
|
|
1204
|
+
function buildJukebox(x, z) {
|
|
1205
|
+
var group = new THREE.Group();
|
|
1206
|
+
group.position.set(x, 0, z);
|
|
1207
|
+
|
|
1208
|
+
// Main body — rounded wooden cabinet
|
|
1209
|
+
var bodyMat = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.5, metalness: 0.05 });
|
|
1210
|
+
var body = new THREE.Mesh(new THREE.BoxGeometry(0.9, 1.5, 0.5), bodyMat);
|
|
1211
|
+
body.position.y = 0.75; body.castShadow = true;
|
|
1212
|
+
group.add(body);
|
|
1213
|
+
|
|
1214
|
+
// Chrome trim top arch
|
|
1215
|
+
var chromeMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.1, metalness: 0.8 });
|
|
1216
|
+
var topArch = new THREE.Mesh(new THREE.CylinderGeometry(0.45, 0.45, 0.06, 16, 1, false, 0, Math.PI), chromeMat);
|
|
1217
|
+
topArch.position.set(0, 1.53, 0);
|
|
1218
|
+
topArch.rotation.z = Math.PI;
|
|
1219
|
+
topArch.rotation.y = Math.PI / 2;
|
|
1220
|
+
group.add(topArch);
|
|
1221
|
+
|
|
1222
|
+
// Glass viewing panel (curved top section)
|
|
1223
|
+
var glassMat = new THREE.MeshStandardMaterial({ color: 0xaaddff, transparent: true, opacity: 0.4, roughness: 0.05 });
|
|
1224
|
+
var glassPanel = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.4, 0.08), glassMat);
|
|
1225
|
+
glassPanel.position.set(0, 1.35, 0.22);
|
|
1226
|
+
group.add(glassPanel);
|
|
1227
|
+
|
|
1228
|
+
// Neon glow strips (sides) — animated via S._jukeboxNeon
|
|
1229
|
+
var neonColors = [0xff4488, 0xff8844, 0xffdd44, 0x44ff88, 0x4488ff, 0xaa44ff];
|
|
1230
|
+
var neonMat = new THREE.MeshStandardMaterial({ color: 0xff4488, emissive: 0xff4488, emissiveIntensity: 0.8, roughness: 0.2 });
|
|
1231
|
+
// Left neon strip
|
|
1232
|
+
var neonL = new THREE.Mesh(new THREE.BoxGeometry(0.04, 1.2, 0.04), neonMat);
|
|
1233
|
+
neonL.position.set(-0.42, 0.75, 0.23);
|
|
1234
|
+
group.add(neonL);
|
|
1235
|
+
// Right neon strip
|
|
1236
|
+
var neonR = new THREE.Mesh(new THREE.BoxGeometry(0.04, 1.2, 0.04), neonMat);
|
|
1237
|
+
neonR.position.set(0.42, 0.75, 0.23);
|
|
1238
|
+
group.add(neonR);
|
|
1239
|
+
// Top neon arc
|
|
1240
|
+
var neonTop = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.04, 0.04), neonMat);
|
|
1241
|
+
neonTop.position.set(0, 1.5, 0.23);
|
|
1242
|
+
group.add(neonTop);
|
|
1243
|
+
|
|
1244
|
+
// Bubble tube columns (sides)
|
|
1245
|
+
var bubbleMat = new THREE.MeshStandardMaterial({ color: 0x66ccff, transparent: true, opacity: 0.5, emissive: 0x66ccff, emissiveIntensity: 0.3 });
|
|
1246
|
+
var bubbleL = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 1.3, 8), bubbleMat);
|
|
1247
|
+
bubbleL.position.set(-0.38, 0.75, 0.18);
|
|
1248
|
+
group.add(bubbleL);
|
|
1249
|
+
var bubbleR = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 1.3, 8), bubbleMat);
|
|
1250
|
+
bubbleR.position.set(0.38, 0.75, 0.18);
|
|
1251
|
+
group.add(bubbleR);
|
|
1252
|
+
|
|
1253
|
+
// Chrome base
|
|
1254
|
+
var baseMat = chromeMat;
|
|
1255
|
+
var base = new THREE.Mesh(new THREE.BoxGeometry(1, 0.08, 0.55), baseMat);
|
|
1256
|
+
base.position.y = 0.04;
|
|
1257
|
+
group.add(base);
|
|
1258
|
+
|
|
1259
|
+
// Chrome grille (speaker area — lower section)
|
|
1260
|
+
for (var gi = 0; gi < 6; gi++) {
|
|
1261
|
+
var grille = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.01, 0.01), chromeMat);
|
|
1262
|
+
grille.position.set(0, 0.15 + gi * 0.06, 0.26);
|
|
1263
|
+
group.add(grille);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Record selector buttons (front panel)
|
|
1267
|
+
var buttonMat = new THREE.MeshStandardMaterial({ color: 0xdddddd, roughness: 0.3, metalness: 0.5 });
|
|
1268
|
+
for (var bi = 0; bi < 3; bi++) {
|
|
1269
|
+
var btn = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.02, 8), buttonMat);
|
|
1270
|
+
btn.position.set(-0.15 + bi * 0.15, 0.85, 0.26);
|
|
1271
|
+
btn.rotation.x = Math.PI / 2;
|
|
1272
|
+
group.add(btn);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// "NOW PLAYING" CSS2D label (hidden by default, shown when music plays)
|
|
1276
|
+
var labelDiv = document.createElement('div');
|
|
1277
|
+
labelDiv.className = 'jukebox-label';
|
|
1278
|
+
labelDiv.style.cssText = 'color:#ff4488;font-size:8px;font-weight:bold;font-family:monospace;text-shadow:0 0 6px #ff4488;text-align:center;pointer-events:none;opacity:0.9;';
|
|
1279
|
+
labelDiv.innerHTML = '<div style="color:#ffdd44;font-size:10px">JUKEBOX</div><div style="font-size:7px;color:#aaa">Press E to play</div>';
|
|
1280
|
+
var label = new CSS2DObject(labelDiv);
|
|
1281
|
+
label.position.set(0, 1.8, 0);
|
|
1282
|
+
group.add(label);
|
|
1283
|
+
|
|
1284
|
+
// Store references for interaction + animation
|
|
1285
|
+
S._jukebox = {
|
|
1286
|
+
group: group,
|
|
1287
|
+
neonMat: neonMat,
|
|
1288
|
+
neonColors: neonColors,
|
|
1289
|
+
neonIndex: 0,
|
|
1290
|
+
label: labelDiv,
|
|
1291
|
+
pos: { x: x, z: z },
|
|
1292
|
+
playing: false
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
S.furnitureGroup.add(group);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1202
1298
|
// ==================== RECREATION CENTER ====================
|
|
1203
1299
|
function buildRecCenter(x, z, walnutMat, chromeMat, carpetMat) {
|
|
1204
1300
|
// Carpet area
|
|
@@ -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
|
+
}
|