let-them-talk 4.2.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +640 -540
- package/README.md +592 -415
- package/cli.js +1089 -589
- package/conversation-templates/autonomous-feature.json +22 -0
- package/conversation-templates/code-review.json +21 -11
- package/conversation-templates/debug-squad.json +21 -11
- package/conversation-templates/feature-build.json +21 -11
- package/conversation-templates/research-write.json +21 -11
- package/dashboard.html +9250 -7771
- package/dashboard.js +1232 -29
- package/office/agents.js +148 -4
- package/office/animation.js +68 -0
- package/office/assets.js +431 -0
- package/office/builder.js +355 -0
- package/office/building-interior.js +261 -0
- package/office/campus-env.js +119 -23
- package/office/car-hud.js +368 -0
- package/office/daynight.js +221 -0
- package/office/economy-hud.js +432 -0
- package/office/economy-ui.js +238 -0
- package/office/environment.js +818 -808
- package/office/face.js +65 -0
- package/office/fast-travel.js +215 -0
- package/office/hq-building.js +295 -0
- package/office/index.js +1095 -423
- package/office/instancing.js +160 -0
- package/office/lod-manager.js +165 -0
- package/office/multiplayer-hud.js +428 -0
- package/office/net-client.js +299 -0
- package/office/particles.js +172 -0
- package/office/player.js +658 -436
- package/office/post-processing.js +82 -0
- package/office/sky.js +319 -0
- package/office/street-furniture.js +308 -0
- package/office/vehicle.js +455 -0
- package/office/world-save.js +91 -0
- package/package.json +59 -59
- package/server.js +7190 -4685
- package/conversation-templates/managed-team.json +0 -12
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { S } from './state.js';
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// VEHICLE SYSTEM — Drivable cars in AI City
|
|
6
|
+
// Phase 2: Enter/exit, WASD driving, camera follow, collision
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
var CAR_SPEED = 0.3;
|
|
10
|
+
var CAR_TURN_SPEED = 0.035;
|
|
11
|
+
var CAR_BRAKE = 0.92; // friction multiplier per frame
|
|
12
|
+
var CAR_ACCEL = 0.012;
|
|
13
|
+
var CAR_MAX_SPEED = 0.6;
|
|
14
|
+
var CAM_DISTANCE = 10;
|
|
15
|
+
var CAM_HEIGHT = 4;
|
|
16
|
+
var CAM_SMOOTH = 0.06;
|
|
17
|
+
var firstPersonMode = false;
|
|
18
|
+
|
|
19
|
+
var vehicles = [];
|
|
20
|
+
var activeVehicle = null; // currently driven vehicle
|
|
21
|
+
var driving = false;
|
|
22
|
+
var keys = { w: false, a: false, s: false, d: false, space: false };
|
|
23
|
+
var velocity = 0;
|
|
24
|
+
var steerAngle = 0;
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// CAR MESH — low-poly stylized car
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
function createCarMesh(color) {
|
|
31
|
+
var group = new THREE.Group();
|
|
32
|
+
var paintMat = new THREE.MeshStandardMaterial({ color: color, roughness: 0.25, metalness: 0.5 });
|
|
33
|
+
var chromeMat = new THREE.MeshStandardMaterial({ color: 0xdddddd, roughness: 0.05, metalness: 0.95 });
|
|
34
|
+
var glassMat = new THREE.MeshStandardMaterial({
|
|
35
|
+
color: 0x99ccff, transparent: true, opacity: 0.18,
|
|
36
|
+
roughness: 0.0, metalness: 0.3, side: THREE.DoubleSide,
|
|
37
|
+
depthWrite: false,
|
|
38
|
+
});
|
|
39
|
+
var darkMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.8 });
|
|
40
|
+
var tireMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.95 });
|
|
41
|
+
var rimMat = new THREE.MeshStandardMaterial({ color: 0xbbbbbb, roughness: 0.15, metalness: 0.8 });
|
|
42
|
+
var interiorMat = new THREE.MeshStandardMaterial({ color: 0x2a2a2a, roughness: 0.7 });
|
|
43
|
+
|
|
44
|
+
// === SEDAN BODY — two boxes for performance ===
|
|
45
|
+
// Lower body
|
|
46
|
+
var bodyGeo = new THREE.BoxGeometry(1.5, 0.45, 3.8);
|
|
47
|
+
var body = new THREE.Mesh(bodyGeo, paintMat);
|
|
48
|
+
body.position.set(0, 0.42, 0);
|
|
49
|
+
group.add(body);
|
|
50
|
+
|
|
51
|
+
// Cabin (narrower, shorter)
|
|
52
|
+
var cabinGeo = new THREE.BoxGeometry(1.3, 0.42, 1.7);
|
|
53
|
+
var cabin = new THREE.Mesh(cabinGeo, paintMat);
|
|
54
|
+
cabin.position.set(0, 0.86, -0.1);
|
|
55
|
+
group.add(cabin);
|
|
56
|
+
|
|
57
|
+
// Interior floor (visible through glass)
|
|
58
|
+
var intFloor = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.05, 1.5), interiorMat);
|
|
59
|
+
intFloor.position.set(0, 0.68, 0.15);
|
|
60
|
+
group.add(intFloor);
|
|
61
|
+
|
|
62
|
+
// Steering wheel
|
|
63
|
+
var steeringGeo = new THREE.TorusGeometry(0.12, 0.015, 8, 16);
|
|
64
|
+
var steering = new THREE.Mesh(steeringGeo, darkMat);
|
|
65
|
+
steering.position.set(-0.3, 0.85, 0.5);
|
|
66
|
+
steering.rotation.x = -0.5;
|
|
67
|
+
group.add(steering);
|
|
68
|
+
|
|
69
|
+
// Seats (2 front)
|
|
70
|
+
[-0.3, 0.3].forEach(function(x) {
|
|
71
|
+
var seatBase = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.08, 0.4), interiorMat);
|
|
72
|
+
seatBase.position.set(x, 0.74, 0.25);
|
|
73
|
+
group.add(seatBase);
|
|
74
|
+
var seatBack = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.35, 0.08), interiorMat);
|
|
75
|
+
seatBack.position.set(x, 0.9, 0.03);
|
|
76
|
+
seatBack.rotation.x = 0.1;
|
|
77
|
+
group.add(seatBack);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// === GLASS PANELS (truly transparent) ===
|
|
81
|
+
// Windshield (angled)
|
|
82
|
+
var wsGeo = new THREE.PlaneGeometry(1.1, 0.5);
|
|
83
|
+
var ws = new THREE.Mesh(wsGeo, glassMat);
|
|
84
|
+
ws.position.set(0, 0.9, 0.78);
|
|
85
|
+
ws.rotation.x = -0.45;
|
|
86
|
+
group.add(ws);
|
|
87
|
+
|
|
88
|
+
// Rear window
|
|
89
|
+
var rwGeo = new THREE.PlaneGeometry(1.0, 0.4);
|
|
90
|
+
var rw = new THREE.Mesh(rwGeo, glassMat);
|
|
91
|
+
rw.position.set(0, 0.9, -0.82);
|
|
92
|
+
rw.rotation.x = 0.4;
|
|
93
|
+
rw.rotation.y = Math.PI;
|
|
94
|
+
group.add(rw);
|
|
95
|
+
|
|
96
|
+
// Side windows
|
|
97
|
+
[1, -1].forEach(function(side) {
|
|
98
|
+
var swGeo = new THREE.PlaneGeometry(1.4, 0.32);
|
|
99
|
+
var sw = new THREE.Mesh(swGeo, glassMat);
|
|
100
|
+
sw.position.set(side * 0.59, 0.88, 0.0);
|
|
101
|
+
sw.rotation.y = side * Math.PI / 2;
|
|
102
|
+
group.add(sw);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// === WHEELS (realistic proportions) ===
|
|
106
|
+
var wheelPositions = [
|
|
107
|
+
{ x: -0.68, y: 0.24, z: 1.1 },
|
|
108
|
+
{ x: 0.68, y: 0.24, z: 1.1 },
|
|
109
|
+
{ x: -0.68, y: 0.24, z: -1.1 },
|
|
110
|
+
{ x: 0.68, y: 0.24, z: -1.1 },
|
|
111
|
+
];
|
|
112
|
+
var wheelMeshes = [];
|
|
113
|
+
wheelPositions.forEach(function(wp) {
|
|
114
|
+
var wheelGroup = new THREE.Group();
|
|
115
|
+
// Tire (simple cylinder for performance)
|
|
116
|
+
var tireGeo = new THREE.CylinderGeometry(0.24, 0.24, 0.16, 8);
|
|
117
|
+
var tire = new THREE.Mesh(tireGeo, tireMat);
|
|
118
|
+
tire.rotation.z = Math.PI / 2;
|
|
119
|
+
wheelGroup.add(tire);
|
|
120
|
+
// Rim
|
|
121
|
+
var rimGeo = new THREE.CylinderGeometry(0.14, 0.14, 0.17, 6);
|
|
122
|
+
var rim = new THREE.Mesh(rimGeo, rimMat);
|
|
123
|
+
rim.rotation.z = Math.PI / 2;
|
|
124
|
+
wheelGroup.add(rim);
|
|
125
|
+
|
|
126
|
+
wheelGroup.position.set(wp.x, wp.y, wp.z);
|
|
127
|
+
group.add(wheelGroup);
|
|
128
|
+
wheelMeshes.push(wheelGroup);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// === FRONT GRILLE ===
|
|
132
|
+
var grilleGeo = new THREE.BoxGeometry(0.9, 0.2, 0.05);
|
|
133
|
+
var grilleMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.3, metalness: 0.6 });
|
|
134
|
+
var grille = new THREE.Mesh(grilleGeo, grilleMat);
|
|
135
|
+
grille.position.set(0, 0.42, 1.93);
|
|
136
|
+
group.add(grille);
|
|
137
|
+
|
|
138
|
+
// === BUMPERS (body-colored, integrated) ===
|
|
139
|
+
var fBumper = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.12, 0.12), paintMat);
|
|
140
|
+
fBumper.position.set(0, 0.28, 1.94);
|
|
141
|
+
group.add(fBumper);
|
|
142
|
+
var rBumper = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.12, 0.12), paintMat);
|
|
143
|
+
rBumper.position.set(0, 0.28, -1.94);
|
|
144
|
+
group.add(rBumper);
|
|
145
|
+
|
|
146
|
+
// === HEADLIGHTS (modern LED style) ===
|
|
147
|
+
var hlMat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffffee, emissiveIntensity: 1.5 });
|
|
148
|
+
[-0.5, 0.5].forEach(function(x) {
|
|
149
|
+
var hl = new THREE.Mesh(new THREE.BoxGeometry(0.22, 0.08, 0.05), hlMat);
|
|
150
|
+
hl.position.set(x, 0.48, 1.94);
|
|
151
|
+
group.add(hl);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// === TAILLIGHTS (LED strip) ===
|
|
155
|
+
var tlMat = new THREE.MeshStandardMaterial({ color: 0xff1111, emissive: 0xff0000, emissiveIntensity: 1.0 });
|
|
156
|
+
[-0.5, 0.5].forEach(function(x) {
|
|
157
|
+
var tl = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.06, 0.05), tlMat);
|
|
158
|
+
tl.position.set(x, 0.48, -1.94);
|
|
159
|
+
group.add(tl);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// === SIDE MIRRORS ===
|
|
163
|
+
[-1, 1].forEach(function(side) {
|
|
164
|
+
var arm = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.03, 0.04), darkMat);
|
|
165
|
+
arm.position.set(side * 0.78, 0.78, 0.55);
|
|
166
|
+
group.add(arm);
|
|
167
|
+
var head = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.08, 0.1), darkMat);
|
|
168
|
+
head.position.set(side * 0.86, 0.78, 0.55);
|
|
169
|
+
group.add(head);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// === EXHAUST ===
|
|
173
|
+
var exhaust = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.04, 0.15, 8), chromeMat);
|
|
174
|
+
exhaust.position.set(0.4, 0.2, -1.98);
|
|
175
|
+
exhaust.rotation.x = Math.PI / 2;
|
|
176
|
+
group.add(exhaust);
|
|
177
|
+
|
|
178
|
+
// === LICENSE PLATES ===
|
|
179
|
+
var plateMat = new THREE.MeshStandardMaterial({ color: 0xeeeeee, roughness: 0.4 });
|
|
180
|
+
var plateGeo = new THREE.BoxGeometry(0.4, 0.1, 0.015);
|
|
181
|
+
var fPlate = new THREE.Mesh(plateGeo, plateMat);
|
|
182
|
+
fPlate.position.set(0, 0.32, 1.95);
|
|
183
|
+
group.add(fPlate);
|
|
184
|
+
var rPlate = new THREE.Mesh(plateGeo, plateMat);
|
|
185
|
+
rPlate.position.set(0, 0.32, -1.95);
|
|
186
|
+
group.add(rPlate);
|
|
187
|
+
|
|
188
|
+
group.userData.isVehicle = true;
|
|
189
|
+
group.userData.velocity = 0;
|
|
190
|
+
group.userData.wheels = wheelMeshes;
|
|
191
|
+
|
|
192
|
+
return group;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ============================================================
|
|
196
|
+
// SPAWN VEHICLES — place cars at parking spots
|
|
197
|
+
// ============================================================
|
|
198
|
+
|
|
199
|
+
export function spawnVehicles(parkingSpots) {
|
|
200
|
+
var colors = [0x3366cc, 0xcc3333, 0x33cc66, 0xcccc33, 0x9933cc, 0xcc6633];
|
|
201
|
+
|
|
202
|
+
// Default parking spots if none provided (along main roads)
|
|
203
|
+
if (!parkingSpots || parkingSpots.length === 0) {
|
|
204
|
+
parkingSpots = [
|
|
205
|
+
{ x: 26, z: 2, rot: 0 },
|
|
206
|
+
{ x: 50, z: 2, rot: 0 },
|
|
207
|
+
{ x: 74, z: 2, rot: 0 },
|
|
208
|
+
{ x: 98, z: 2, rot: 0 },
|
|
209
|
+
{ x: 122, z: 2, rot: 0 },
|
|
210
|
+
{ x: 146, z: 2, rot: 0 },
|
|
211
|
+
{ x: 2, z: 50, rot: Math.PI / 2 },
|
|
212
|
+
{ x: 2, z: 98, rot: Math.PI / 2 },
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
parkingSpots.forEach(function(spot, i) {
|
|
217
|
+
var color = colors[i % colors.length];
|
|
218
|
+
var car = createCarMesh(color);
|
|
219
|
+
car.position.set(spot.x, 0, spot.z);
|
|
220
|
+
car.rotation.y = spot.rot || 0;
|
|
221
|
+
car.userData.parkingSpot = spot;
|
|
222
|
+
car.userData.carIndex = i;
|
|
223
|
+
S.scene.add(car);
|
|
224
|
+
vehicles.push(car);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return vehicles;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================
|
|
231
|
+
// ENTER / EXIT — E key to toggle
|
|
232
|
+
// ============================================================
|
|
233
|
+
|
|
234
|
+
export function getNearestVehicle(playerPos, maxDist) {
|
|
235
|
+
maxDist = maxDist || 5;
|
|
236
|
+
var nearest = null;
|
|
237
|
+
var nearestDist = maxDist;
|
|
238
|
+
|
|
239
|
+
vehicles.forEach(function(car) {
|
|
240
|
+
var dx = car.position.x - playerPos.x;
|
|
241
|
+
var dz = car.position.z - playerPos.z;
|
|
242
|
+
var dist = Math.sqrt(dx * dx + dz * dz);
|
|
243
|
+
if (dist < nearestDist) {
|
|
244
|
+
nearestDist = dist;
|
|
245
|
+
nearest = car;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return nearest;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function enterVehicle(car) {
|
|
253
|
+
if (!car || driving) return false;
|
|
254
|
+
activeVehicle = car;
|
|
255
|
+
driving = true;
|
|
256
|
+
velocity = 0;
|
|
257
|
+
steerAngle = 0;
|
|
258
|
+
firstPersonMode = false;
|
|
259
|
+
|
|
260
|
+
// Disable spectator/player controls
|
|
261
|
+
if (S.controls && S.controls.enabled !== undefined) {
|
|
262
|
+
S.controls.enabled = false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Hide player character while in vehicle
|
|
266
|
+
if (S._player && S._player.parts && S._player.parts.group) {
|
|
267
|
+
S._player.parts.group.visible = false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Set up key listeners
|
|
271
|
+
window.addEventListener('keydown', onDriveKeyDown);
|
|
272
|
+
window.addEventListener('keyup', onDriveKeyUp);
|
|
273
|
+
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function exitVehicle() {
|
|
278
|
+
if (!driving || !activeVehicle) return false;
|
|
279
|
+
driving = false;
|
|
280
|
+
velocity = 0;
|
|
281
|
+
firstPersonMode = false;
|
|
282
|
+
|
|
283
|
+
// Re-enable spectator/player controls
|
|
284
|
+
if (S.controls && S.controls.enabled !== undefined) {
|
|
285
|
+
S.controls.enabled = true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Show player character again
|
|
289
|
+
if (S._player && S._player.parts && S._player.parts.group) {
|
|
290
|
+
S._player.parts.group.visible = true;
|
|
291
|
+
// Move player to car exit position
|
|
292
|
+
S._player.pos.x = activeVehicle.position.x + 3;
|
|
293
|
+
S._player.pos.z = activeVehicle.position.z;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Remove key listeners
|
|
297
|
+
window.removeEventListener('keydown', onDriveKeyDown);
|
|
298
|
+
window.removeEventListener('keyup', onDriveKeyUp);
|
|
299
|
+
|
|
300
|
+
// Reset keys
|
|
301
|
+
keys.w = keys.a = keys.s = keys.d = keys.space = false;
|
|
302
|
+
|
|
303
|
+
var exitPos = activeVehicle.position.clone();
|
|
304
|
+
exitPos.x += 3;
|
|
305
|
+
activeVehicle = null;
|
|
306
|
+
|
|
307
|
+
return exitPos;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function onDriveKeyDown(e) {
|
|
311
|
+
var k = e.key.toLowerCase();
|
|
312
|
+
if (k === 'w') keys.w = true;
|
|
313
|
+
if (k === 'a') keys.a = true;
|
|
314
|
+
if (k === 's') keys.s = true;
|
|
315
|
+
if (k === 'd') keys.d = true;
|
|
316
|
+
if (k === ' ') keys.space = true;
|
|
317
|
+
if (k === 'f') {
|
|
318
|
+
firstPersonMode = !firstPersonMode;
|
|
319
|
+
}
|
|
320
|
+
if (k === 'escape' || k === 'e') {
|
|
321
|
+
var pos = exitVehicle();
|
|
322
|
+
window.dispatchEvent(new CustomEvent('vehicle-exit', { detail: { position: pos } }));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function onDriveKeyUp(e) {
|
|
327
|
+
var k = e.key.toLowerCase();
|
|
328
|
+
if (k === 'w') keys.w = false;
|
|
329
|
+
if (k === 'a') keys.a = false;
|
|
330
|
+
if (k === 's') keys.s = false;
|
|
331
|
+
if (k === 'd') keys.d = false;
|
|
332
|
+
if (k === ' ') keys.space = false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ============================================================
|
|
336
|
+
// DRIVING PHYSICS — simple arcade style
|
|
337
|
+
// ============================================================
|
|
338
|
+
|
|
339
|
+
function checkBuildingCollision(pos) {
|
|
340
|
+
// Simple AABB collision against city buildings
|
|
341
|
+
if (!S._cityBuildings) return false;
|
|
342
|
+
for (var i = 0; i < S._cityBuildings.length; i++) {
|
|
343
|
+
var b = S._cityBuildings[i];
|
|
344
|
+
var halfW = b.width / 2 + 1.5; // car half-width buffer
|
|
345
|
+
var halfD = b.depth / 2 + 1.5;
|
|
346
|
+
if (pos.x > b.x - halfW && pos.x < b.x + halfW &&
|
|
347
|
+
pos.z > b.z - halfD && pos.z < b.z + halfD) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function updateVehicle(dt) {
|
|
355
|
+
if (!driving || !activeVehicle) return;
|
|
356
|
+
|
|
357
|
+
// Acceleration / braking
|
|
358
|
+
if (keys.w) velocity = Math.min(velocity + CAR_ACCEL, CAR_MAX_SPEED);
|
|
359
|
+
if (keys.s) velocity = Math.max(velocity - CAR_ACCEL, -CAR_MAX_SPEED * 0.5);
|
|
360
|
+
if (keys.space) velocity *= 0.9; // handbrake
|
|
361
|
+
if (!keys.w && !keys.s) velocity *= CAR_BRAKE; // friction
|
|
362
|
+
|
|
363
|
+
// Steering (only when moving) — drift when space + turn
|
|
364
|
+
var turnMultiplier = keys.space ? 2.2 : 1.0; // drift = sharper turns
|
|
365
|
+
if (Math.abs(velocity) > 0.01) {
|
|
366
|
+
if (keys.a) steerAngle += CAR_TURN_SPEED * turnMultiplier * (velocity > 0 ? 1 : -1);
|
|
367
|
+
if (keys.d) steerAngle -= CAR_TURN_SPEED * turnMultiplier * (velocity > 0 ? 1 : -1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Apply movement
|
|
371
|
+
var forward = new THREE.Vector3(0, 0, 1);
|
|
372
|
+
forward.applyAxisAngle(new THREE.Vector3(0, 1, 0), steerAngle);
|
|
373
|
+
|
|
374
|
+
var newPos = activeVehicle.position.clone();
|
|
375
|
+
newPos.x += forward.x * velocity;
|
|
376
|
+
newPos.z += forward.z * velocity;
|
|
377
|
+
|
|
378
|
+
// Collision check
|
|
379
|
+
if (!checkBuildingCollision(newPos)) {
|
|
380
|
+
activeVehicle.position.copy(newPos);
|
|
381
|
+
} else {
|
|
382
|
+
velocity *= -0.3; // bounce back slightly
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
activeVehicle.rotation.y = steerAngle;
|
|
386
|
+
|
|
387
|
+
// Spin wheels based on velocity
|
|
388
|
+
if (activeVehicle.userData.wheels) {
|
|
389
|
+
activeVehicle.userData.wheels.forEach(function(wheel) {
|
|
390
|
+
wheel.children.forEach(function(child) {
|
|
391
|
+
if (child.geometry && child.geometry.type === 'TorusGeometry') {
|
|
392
|
+
child.rotation.x += velocity * 3; // spin tires
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Camera: first-person or third-person
|
|
399
|
+
if (firstPersonMode) {
|
|
400
|
+
// First person — inside car, looking forward
|
|
401
|
+
var fpPos = new THREE.Vector3();
|
|
402
|
+
fpPos.x = activeVehicle.position.x + forward.x * 0.3;
|
|
403
|
+
fpPos.y = activeVehicle.position.y + 1.1;
|
|
404
|
+
fpPos.z = activeVehicle.position.z + forward.z * 0.3;
|
|
405
|
+
S.camera.position.lerp(fpPos, 0.15);
|
|
406
|
+
|
|
407
|
+
var lookTarget = new THREE.Vector3();
|
|
408
|
+
lookTarget.x = activeVehicle.position.x + forward.x * 10;
|
|
409
|
+
lookTarget.y = activeVehicle.position.y + 0.8;
|
|
410
|
+
lookTarget.z = activeVehicle.position.z + forward.z * 10;
|
|
411
|
+
S.camera.lookAt(lookTarget);
|
|
412
|
+
} else {
|
|
413
|
+
// Third person — behind and above
|
|
414
|
+
var camTarget = new THREE.Vector3();
|
|
415
|
+
camTarget.x = activeVehicle.position.x - forward.x * CAM_DISTANCE;
|
|
416
|
+
camTarget.y = activeVehicle.position.y + CAM_HEIGHT;
|
|
417
|
+
camTarget.z = activeVehicle.position.z - forward.z * CAM_DISTANCE;
|
|
418
|
+
|
|
419
|
+
S.camera.position.lerp(camTarget, CAM_SMOOTH);
|
|
420
|
+
S.camera.lookAt(activeVehicle.position.x, activeVehicle.position.y + 1, activeVehicle.position.z);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ============================================================
|
|
425
|
+
// GETTERS
|
|
426
|
+
// ============================================================
|
|
427
|
+
|
|
428
|
+
export function isDriving() { return driving; }
|
|
429
|
+
export function isFirstPerson() { return firstPersonMode; }
|
|
430
|
+
export function getActiveVehicle() { return activeVehicle; }
|
|
431
|
+
export function getVehicles() { return vehicles; }
|
|
432
|
+
export function getSpeed() { return Math.abs(velocity) * 100; } // km/h-ish
|
|
433
|
+
export function getSteerAngle() { return steerAngle; }
|
|
434
|
+
export function isDrifting() { return keys.space && Math.abs(velocity) > 0.1 && (keys.a || keys.d); }
|
|
435
|
+
|
|
436
|
+
// ============================================================
|
|
437
|
+
// CLEANUP
|
|
438
|
+
// ============================================================
|
|
439
|
+
|
|
440
|
+
export function disposeVehicles() {
|
|
441
|
+
vehicles.forEach(function(car) {
|
|
442
|
+
car.traverse(function(child) {
|
|
443
|
+
if (child.geometry) child.geometry.dispose();
|
|
444
|
+
if (child.material) {
|
|
445
|
+
if (Array.isArray(child.material)) child.material.forEach(function(m) { m.dispose(); });
|
|
446
|
+
else child.material.dispose();
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
S.scene.remove(car);
|
|
450
|
+
});
|
|
451
|
+
vehicles = [];
|
|
452
|
+
activeVehicle = null;
|
|
453
|
+
driving = false;
|
|
454
|
+
velocity = 0;
|
|
455
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// world-save.js — Persistence layer for the World Builder
|
|
2
|
+
// Saves/loads placed objects to .agent-bridge/world-layout.json via dashboard API
|
|
3
|
+
|
|
4
|
+
var _placements = []; // in-memory placement array
|
|
5
|
+
var _saveTimeout = null; // debounce timer
|
|
6
|
+
var _loaded = false;
|
|
7
|
+
|
|
8
|
+
// Generate unique placement ID
|
|
9
|
+
function generatePlacementId() {
|
|
10
|
+
return 'p_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Get project query string for API calls
|
|
14
|
+
function getProjectParam() {
|
|
15
|
+
return window.activeProject ? '?project=' + encodeURIComponent(window.activeProject) : '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// --- Load world layout from server ---
|
|
19
|
+
export async function loadWorld() {
|
|
20
|
+
try {
|
|
21
|
+
var res = await fetch('/api/world-layout' + getProjectParam());
|
|
22
|
+
if (res.ok) {
|
|
23
|
+
var data = await res.json();
|
|
24
|
+
_placements = Array.isArray(data) ? data : [];
|
|
25
|
+
_loaded = true;
|
|
26
|
+
return _placements;
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.warn('[world-save] Load failed:', e.message);
|
|
30
|
+
}
|
|
31
|
+
_placements = [];
|
|
32
|
+
_loaded = true;
|
|
33
|
+
return _placements;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Save full world layout to server (debounced) ---
|
|
37
|
+
function scheduleSave() {
|
|
38
|
+
if (_saveTimeout) clearTimeout(_saveTimeout);
|
|
39
|
+
_saveTimeout = setTimeout(function() {
|
|
40
|
+
_saveTimeout = null;
|
|
41
|
+
fetch('/api/world-save' + getProjectParam(), {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json', 'X-LTT-Request': '1' },
|
|
44
|
+
body: JSON.stringify(_placements)
|
|
45
|
+
}).catch(function(e) {
|
|
46
|
+
console.warn('[world-save] Save failed:', e.message);
|
|
47
|
+
});
|
|
48
|
+
}, 500); // debounce 500ms — batches rapid placements
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- Add a single placement ---
|
|
52
|
+
export function addPlacement(type, x, y, z, rotY, placedBy) {
|
|
53
|
+
var entry = {
|
|
54
|
+
id: generatePlacementId(),
|
|
55
|
+
type: type,
|
|
56
|
+
x: x,
|
|
57
|
+
y: y || 0,
|
|
58
|
+
z: z,
|
|
59
|
+
rotY: rotY || 0,
|
|
60
|
+
placed_by: placedBy || 'user',
|
|
61
|
+
timestamp: new Date().toISOString()
|
|
62
|
+
};
|
|
63
|
+
_placements.push(entry);
|
|
64
|
+
scheduleSave();
|
|
65
|
+
return entry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Remove a placement by ID ---
|
|
69
|
+
export function removePlacement(id) {
|
|
70
|
+
var idx = _placements.findIndex(function(p) { return p.id === id; });
|
|
71
|
+
if (idx === -1) return null;
|
|
72
|
+
var removed = _placements.splice(idx, 1)[0];
|
|
73
|
+
scheduleSave();
|
|
74
|
+
return removed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- Get all placements (read-only copy) ---
|
|
78
|
+
export function getPlacements() {
|
|
79
|
+
return _placements.slice();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Clear all placements ---
|
|
83
|
+
export function clearWorld() {
|
|
84
|
+
_placements = [];
|
|
85
|
+
scheduleSave();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Check if world has been loaded ---
|
|
89
|
+
export function isLoaded() {
|
|
90
|
+
return _loaded;
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "let-them-talk",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
|
|
5
|
-
"main": "server.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"agent-bridge": "./cli.js",
|
|
8
|
-
"let-them-talk": "./cli.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"start": "node server.js",
|
|
12
|
-
"dashboard": "node dashboard.js",
|
|
13
|
-
"test": "echo \"No tests
|
|
14
|
-
},
|
|
15
|
-
"engines": {
|
|
16
|
-
"node": ">=
|
|
17
|
-
},
|
|
18
|
-
"files": [
|
|
19
|
-
"server.js",
|
|
20
|
-
"dashboard.js",
|
|
21
|
-
"dashboard.html",
|
|
22
|
-
"office/",
|
|
23
|
-
"mods/",
|
|
24
|
-
"cli.js",
|
|
25
|
-
"templates/",
|
|
26
|
-
"conversation-templates/",
|
|
27
|
-
"logo.png",
|
|
28
|
-
"LICENSE",
|
|
29
|
-
"SECURITY.md",
|
|
30
|
-
"CHANGELOG.md"
|
|
31
|
-
],
|
|
32
|
-
"keywords": [
|
|
33
|
-
"mcp",
|
|
34
|
-
"claude",
|
|
35
|
-
"claude-code",
|
|
36
|
-
"gemini-cli",
|
|
37
|
-
"codex-cli",
|
|
38
|
-
"agent",
|
|
39
|
-
"multi-agent",
|
|
40
|
-
"communication",
|
|
41
|
-
"message-broker",
|
|
42
|
-
"ai-agents",
|
|
43
|
-
"let-them-talk"
|
|
44
|
-
],
|
|
45
|
-
"repository": {
|
|
46
|
-
"type": "git",
|
|
47
|
-
"url": "git+https://github.com/Dekelelz/let-them-talk.git"
|
|
48
|
-
},
|
|
49
|
-
"homepage": "https://talk.unrealai.studio",
|
|
50
|
-
"bugs": {
|
|
51
|
-
"url": "https://github.com/Dekelelz/let-them-talk/issues"
|
|
52
|
-
},
|
|
53
|
-
"author": "Dekelelz <contact@talk.unrealai.studio>",
|
|
54
|
-
"license": "SEE LICENSE IN LICENSE",
|
|
55
|
-
"dependencies": {
|
|
56
|
-
"@modelcontextprotocol/sdk": "1.27.1",
|
|
57
|
-
"three": "0.
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "let-them-talk",
|
|
3
|
+
"version": "5.2.0",
|
|
4
|
+
"description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-bridge": "./cli.js",
|
|
8
|
+
"let-them-talk": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node server.js",
|
|
12
|
+
"dashboard": "node dashboard.js",
|
|
13
|
+
"test": "echo \"No tests configured\""
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"server.js",
|
|
20
|
+
"dashboard.js",
|
|
21
|
+
"dashboard.html",
|
|
22
|
+
"office/",
|
|
23
|
+
"mods/",
|
|
24
|
+
"cli.js",
|
|
25
|
+
"templates/",
|
|
26
|
+
"conversation-templates/",
|
|
27
|
+
"logo.png",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"SECURITY.md",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"claude",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"gemini-cli",
|
|
37
|
+
"codex-cli",
|
|
38
|
+
"agent",
|
|
39
|
+
"multi-agent",
|
|
40
|
+
"communication",
|
|
41
|
+
"message-broker",
|
|
42
|
+
"ai-agents",
|
|
43
|
+
"let-them-talk"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/Dekelelz/let-them-talk.git"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://talk.unrealai.studio",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/Dekelelz/let-them-talk/issues"
|
|
52
|
+
},
|
|
53
|
+
"author": "Dekelelz <contact@talk.unrealai.studio>",
|
|
54
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@modelcontextprotocol/sdk": "1.27.1",
|
|
57
|
+
"three": "0.175.0"
|
|
58
|
+
}
|
|
59
|
+
}
|