let-them-talk 3.5.1 → 3.6.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,337 @@
1
+ import * as THREE from 'three';
2
+ import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
3
+ import { S } from './state.js';
4
+ import { DESK_POSITIONS, DRESSING_ROOM_POS, DRESSING_ROOM_ENTRANCE, REST_AREA_POS, REST_AREA_ENTRANCE } from './constants.js';
5
+ import { initScene } from './scene.js';
6
+ import { buildEnvironment, updateTVScreen } from './environment.js';
7
+ import { updateAgent } from './animation.js';
8
+ import { syncAgents, processMessages, walkTo, showBubble } from './agents.js';
9
+ // Side-effect: registers window.officeGetAppearance
10
+ import './appearance.js';
11
+
12
+ // Expose createCharacter + resolveAppearance for the character designer (Phase 3)
13
+ export { createCharacter } from './character.js';
14
+ export { resolveAppearance } from './appearance.js';
15
+ export { buildHair } from './hair.js';
16
+ export { buildFaceSprite } from './face.js';
17
+ export { buildGlasses, buildHeadwear, buildNeckwear } from './accessories.js';
18
+ export { buildOutfit, removeOutfit } from './outfits.js';
19
+
20
+ // ===================== RAYCASTER + COMMAND MENU =====================
21
+ var raycaster = new THREE.Raycaster();
22
+ var mouse = new THREE.Vector2();
23
+ var activeMenu = null; // { agentName, css2dObj, div, timeout }
24
+ var clickHandlerBound = null;
25
+
26
+ function setupClickHandler() {
27
+ if (clickHandlerBound) return;
28
+ // Track mouse-down position to distinguish clicks from camera drags
29
+ var downPos = { x: 0, y: 0 };
30
+ S.renderer.domElement.addEventListener('mousedown', function(e) {
31
+ downPos.x = e.clientX; downPos.y = e.clientY;
32
+ });
33
+ clickHandlerBound = function(event) {
34
+ if (!S.running || !S.renderer) return;
35
+ // Ignore if mouse moved more than 5px (it was a drag, not a click)
36
+ var dx = event.clientX - downPos.x, dy = event.clientY - downPos.y;
37
+ if (Math.sqrt(dx * dx + dy * dy) > 5) return;
38
+
39
+ var rect = S.renderer.domElement.getBoundingClientRect();
40
+ mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
41
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
42
+
43
+ raycaster.setFromCamera(mouse, S.camera);
44
+
45
+ // Test all agent body meshes
46
+ var agentMeshes = [];
47
+ for (var name in S.agents3d) {
48
+ var agent = S.agents3d[name];
49
+ if (agent.dying) continue;
50
+ // Collect body-part meshes for intersection
51
+ agent.parts.group.traverse(function(child) {
52
+ if (child.isMesh && !child.userData.isShadow) {
53
+ child.userData._agentName = name;
54
+ agentMeshes.push(child);
55
+ }
56
+ });
57
+ }
58
+
59
+ var intersects = raycaster.intersectObjects(agentMeshes, false);
60
+ if (intersects.length > 0) {
61
+ var hitAgent = intersects[0].object.userData._agentName;
62
+ if (hitAgent && S.agents3d[hitAgent]) {
63
+ event.stopPropagation();
64
+ showCommandMenu(hitAgent);
65
+ return;
66
+ }
67
+ }
68
+
69
+ // Clicked nothing — dismiss menu
70
+ dismissCommandMenu();
71
+ };
72
+ S.renderer.domElement.addEventListener('click', clickHandlerBound);
73
+ }
74
+
75
+ function showCommandMenu(agentName) {
76
+ dismissCommandMenu();
77
+ var agent = S.agents3d[agentName];
78
+ if (!agent) return;
79
+
80
+ var loc = agent.location || 'desk';
81
+ var isWalking = agent.target !== null;
82
+
83
+ var div = document.createElement('div');
84
+ div.className = 'office3d-cmd-menu';
85
+
86
+ var commands = [
87
+ { icon: '\uD83D\uDC57', label: 'Dressing Room', action: 'dressing_room', disabled: loc === 'dressing_room' || isWalking },
88
+ { icon: '\uD83D\uDCA4', label: 'Go Rest', action: 'rest', disabled: loc === 'rest' || isWalking },
89
+ { icon: '\uD83D\uDCBB', label: 'Back to Work', action: 'desk', disabled: loc === 'desk' || isWalking },
90
+ { divider: true },
91
+ { icon: '\u270F\uFE0F', label: 'Edit Profile', action: 'edit_profile', disabled: false },
92
+ ];
93
+
94
+ commands.forEach(function(cmd) {
95
+ if (cmd.divider) {
96
+ var d = document.createElement('div');
97
+ d.className = 'office3d-cmd-divider';
98
+ div.appendChild(d);
99
+ return;
100
+ }
101
+ var btn = document.createElement('button');
102
+ btn.className = 'office3d-cmd-btn' + (cmd.disabled ? ' disabled' : '');
103
+ btn.innerHTML = '<span class="office3d-cmd-icon">' + cmd.icon + '</span>' + cmd.label;
104
+ btn.addEventListener('click', function(e) {
105
+ e.stopPropagation();
106
+ dismissCommandMenu();
107
+ executeCommand(agentName, cmd.action);
108
+ });
109
+ div.appendChild(btn);
110
+ });
111
+
112
+ var menuObj = new CSS2DObject(div);
113
+ menuObj.position.set(0, 2.1, 0);
114
+ agent.parts.group.add(menuObj);
115
+
116
+ activeMenu = {
117
+ agentName: agentName,
118
+ css2dObj: menuObj,
119
+ div: div,
120
+ timeout: setTimeout(dismissCommandMenu, 5000),
121
+ };
122
+ }
123
+
124
+ function dismissCommandMenu() {
125
+ if (!activeMenu) return;
126
+ var agent = S.agents3d[activeMenu.agentName];
127
+ if (agent) {
128
+ agent.parts.group.remove(activeMenu.css2dObj);
129
+ }
130
+ if (activeMenu.div.parentElement) activeMenu.div.remove();
131
+ clearTimeout(activeMenu.timeout);
132
+ activeMenu = null;
133
+ }
134
+
135
+ function executeCommand(agentName, action) {
136
+ var agent = S.agents3d[agentName];
137
+ if (!agent) return;
138
+
139
+ switch (action) {
140
+ case 'dressing_room':
141
+ agent.location = 'walking';
142
+ agent.isSitting = false;
143
+ showBubble(agent, 'Going to change!');
144
+ walkTo(agent, DRESSING_ROOM_ENTRANCE.x, DRESSING_ROOM_ENTRANCE.z, function() {
145
+ walkTo(agent, DRESSING_ROOM_POS.x, DRESSING_ROOM_POS.z, function() {
146
+ agent.location = 'dressing_room';
147
+ agent.isSitting = false;
148
+ showBubble(agent, 'Time for a new look!');
149
+ // Open character editor for this agent
150
+ window.editingAgent = agentName;
151
+ if (typeof window.openProfileEditor === 'function') {
152
+ window.openProfileEditor();
153
+ }
154
+ // Listen for editor close to return to desk
155
+ waitForEditorClose(agent);
156
+ });
157
+ });
158
+ break;
159
+
160
+ case 'rest':
161
+ agent.location = 'walking';
162
+ agent.isSitting = false;
163
+ showBubble(agent, 'Need a break...');
164
+ walkTo(agent, REST_AREA_ENTRANCE.x, REST_AREA_ENTRANCE.z, function() {
165
+ walkTo(agent, REST_AREA_POS.x, REST_AREA_POS.z, function() {
166
+ agent.location = 'rest';
167
+ agent.state = 'sleeping';
168
+ agent.isSitting = false;
169
+ showBubble(agent, 'Zzz...');
170
+ });
171
+ });
172
+ break;
173
+
174
+ case 'desk':
175
+ agent.location = 'walking';
176
+ agent.state = 'active';
177
+ agent.isSitting = false;
178
+ showBubble(agent, 'Back to work!');
179
+ walkTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7, function() {
180
+ agent.location = 'desk';
181
+ agent.registered = true;
182
+ });
183
+ break;
184
+
185
+ case 'edit_profile':
186
+ window.editingAgent = agentName;
187
+ if (typeof window.openProfileEditor === 'function') {
188
+ window.openProfileEditor();
189
+ }
190
+ break;
191
+ }
192
+ }
193
+
194
+ function waitForEditorClose(agent) {
195
+ // Poll for the character designer panel closing
196
+ var checkInterval = setInterval(function() {
197
+ var panel = document.getElementById('char-designer');
198
+ if (!panel || !panel.classList.contains('open')) {
199
+ clearInterval(checkInterval);
200
+ // Agent walks back to desk after editor closes
201
+ if (agent.location === 'dressing_room') {
202
+ agent.location = 'walking';
203
+ showBubble(agent, 'Looking good!');
204
+ walkTo(agent, DRESSING_ROOM_ENTRANCE.x, DRESSING_ROOM_ENTRANCE.z, function() {
205
+ walkTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7, function() {
206
+ agent.location = 'desk';
207
+ agent.registered = true;
208
+ });
209
+ });
210
+ }
211
+ }
212
+ }, 500);
213
+ // Safety: stop checking after 2 minutes
214
+ setTimeout(function() { clearInterval(checkInterval); }, 120000);
215
+ }
216
+
217
+ // ===================== ANIMATION LOOP =====================
218
+ function animate() {
219
+ if (!S.running) return;
220
+ S.animationId = requestAnimationFrame(animate);
221
+
222
+ var dt = Math.min(S.clock.getDelta(), 0.1);
223
+ var time = S.clock.getElapsedTime();
224
+
225
+ for (var name in S.agents3d) {
226
+ updateAgent(S.agents3d[name], dt, time);
227
+ }
228
+
229
+ // Update TV screen every ~0.5s for smooth ticker
230
+ if (!S._tvTimer) S._tvTimer = 0;
231
+ S._tvTimer += dt;
232
+ if (S._tvTimer >= 0.15) {
233
+ S._tvTimer = 0;
234
+ updateTVScreen(time);
235
+ }
236
+
237
+ S.controls.update(dt);
238
+ S.renderer.render(S.scene, S.camera);
239
+ S.cssRenderer.render(S.scene, S.camera);
240
+
241
+ // FPS
242
+ S.fpsCounter++;
243
+ var now = performance.now();
244
+ if (now - S.fpsTime > 1000) {
245
+ var fpsEl = document.getElementById('office-fps');
246
+ if (fpsEl && window.activeView === 'office') fpsEl.textContent = S.fpsCounter + ' fps';
247
+ S.fpsCounter = 0;
248
+ S.fpsTime = now;
249
+ }
250
+ }
251
+
252
+ // ===================== PUBLIC API =====================
253
+ window.office3dStart = function() {
254
+ if (S.running) return;
255
+ S.container = document.getElementById('office-3d-container');
256
+ if (!S.container) return;
257
+
258
+ if (!S.scene) {
259
+ if (!initScene()) return;
260
+ buildEnvironment();
261
+ }
262
+
263
+ if (!S.container.contains(S.renderer.domElement)) {
264
+ S.container.appendChild(S.renderer.domElement);
265
+ S.container.appendChild(S.cssRenderer.domElement);
266
+ }
267
+
268
+ var w = S.container.clientWidth;
269
+ var h = S.container.clientHeight;
270
+ if (w > 0 && h > 0) {
271
+ S.camera.aspect = w / h;
272
+ S.camera.updateProjectionMatrix();
273
+ S.renderer.setSize(w, h);
274
+ S.cssRenderer.setSize(w, h);
275
+ }
276
+
277
+ S.running = true;
278
+ S.clock.start();
279
+ S.lastProcessedMsg = 0;
280
+ syncAgents();
281
+ processMessages();
282
+ setupClickHandler();
283
+ animate();
284
+
285
+ if (S.syncInterval) clearInterval(S.syncInterval);
286
+ S.syncInterval = setInterval(function() {
287
+ if (S.running && window.activeView === 'office') {
288
+ syncAgents();
289
+ processMessages();
290
+ updateTVScreen(S.clock.getElapsedTime());
291
+ }
292
+ }, 2000);
293
+ };
294
+
295
+ window.office3dStop = function() {
296
+ S.running = false;
297
+ if (S.animationId) {
298
+ cancelAnimationFrame(S.animationId);
299
+ S.animationId = null;
300
+ }
301
+ if (S.syncInterval) {
302
+ clearInterval(S.syncInterval);
303
+ S.syncInterval = null;
304
+ }
305
+ dismissCommandMenu();
306
+ };
307
+
308
+ window.office3dSetEnvironment = function(env) {
309
+ if (env === S.currentEnv) return;
310
+ S.currentEnv = env;
311
+ if (S.scene) {
312
+ buildEnvironment();
313
+ var i = 0;
314
+ for (var name in S.agents3d) {
315
+ var agent = S.agents3d[name];
316
+ if (i < DESK_POSITIONS.length) {
317
+ agent.deskIdx = i;
318
+ agent.deskPos = { x: DESK_POSITIONS[i].x, z: DESK_POSITIONS[i].z };
319
+ walkTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7);
320
+ }
321
+ i++;
322
+ }
323
+ }
324
+ };
325
+
326
+ window.office3dSetCamSpeed = function(speed) {
327
+ if (S.controls) S.controls.moveSpeed = speed;
328
+ };
329
+
330
+ // Handle visibility change for 3D mode
331
+ document.addEventListener('visibilitychange', function() {
332
+ if (document.hidden && S.running) {
333
+ window.office3dStop();
334
+ } else if (!document.hidden && window.activeView === 'office' && window.officeMode === '3d') {
335
+ window.office3dStart();
336
+ }
337
+ });
@@ -0,0 +1,257 @@
1
+ // Mod loader — loads and validates GLB/GLTF community mods + built-in procedural items.
2
+ // GLB files contain ONLY geometry/textures/animations — NO executable code.
3
+ import * as THREE from 'three';
4
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
5
+
6
+ // Type limits for validation
7
+ const TYPE_LIMITS = {
8
+ accessory: { maxPolys: 500, maxBytes: 200 * 1024 },
9
+ hairstyle: { maxPolys: 800, maxBytes: 300 * 1024 },
10
+ outfit: { maxPolys: 1500, maxBytes: 500 * 1024 },
11
+ character: { maxPolys: 3000, maxBytes: 1024 * 1024 },
12
+ environment: { maxPolys: 10000, maxBytes: 2 * 1024 * 1024 },
13
+ };
14
+
15
+ // Manifest schema validation
16
+ const REQUIRED_FIELDS = ['id', 'name', 'version', 'author', 'type', 'category'];
17
+ const VALID_TYPES = ['accessory', 'hairstyle', 'outfit', 'character', 'environment'];
18
+ const ID_PATTERN = /^[a-z0-9_-]{1,40}$/;
19
+
20
+ // GLB magic bytes: 0x46546C67 ("glTF" in ASCII)
21
+ const GLB_MAGIC = 0x46546C67;
22
+
23
+ const modRegistry = {};
24
+ const loadedModels = {};
25
+ let builtInItems = [];
26
+
27
+ // Load built-in procedural item manifests
28
+ export async function loadBuiltInManifests() {
29
+ try {
30
+ var resp = await fetch('/mods/built-in-accessories.json');
31
+ if (resp.ok) {
32
+ builtInItems = await resp.json();
33
+ builtInItems.forEach(function(item) {
34
+ modRegistry[item.id] = item;
35
+ });
36
+ }
37
+ } catch (e) {
38
+ console.warn('Could not load built-in mod manifests:', e);
39
+ }
40
+ }
41
+
42
+ export function getInstalledMods() {
43
+ return Object.values(modRegistry);
44
+ }
45
+
46
+ export function getModsByCategory(category) {
47
+ return Object.values(modRegistry).filter(function(m) { return m.category === category; });
48
+ }
49
+
50
+ export function getModsByType(type) {
51
+ return Object.values(modRegistry).filter(function(m) { return m.type === type; });
52
+ }
53
+
54
+ export function isModInstalled(id) {
55
+ return !!modRegistry[id];
56
+ }
57
+
58
+ export function getMod(id) {
59
+ return modRegistry[id] || null;
60
+ }
61
+
62
+ // Validate manifest JSON
63
+ export function validateManifest(manifest) {
64
+ var errors = [];
65
+
66
+ // Required fields
67
+ for (var i = 0; i < REQUIRED_FIELDS.length; i++) {
68
+ if (!manifest[REQUIRED_FIELDS[i]]) {
69
+ errors.push('Missing required field: ' + REQUIRED_FIELDS[i]);
70
+ }
71
+ }
72
+
73
+ // ID format
74
+ if (manifest.id && !ID_PATTERN.test(manifest.id)) {
75
+ errors.push('Invalid id format: must be 1-40 chars of a-z, 0-9, _, -');
76
+ }
77
+
78
+ // Type
79
+ if (manifest.type && !VALID_TYPES.includes(manifest.type)) {
80
+ errors.push('Invalid type: must be one of ' + VALID_TYPES.join(', '));
81
+ }
82
+
83
+ // ID collision
84
+ if (manifest.id && modRegistry[manifest.id]) {
85
+ errors.push('ID collision: "' + manifest.id + '" already exists');
86
+ }
87
+
88
+ // Asset definition
89
+ if (!manifest.asset || !manifest.asset.format) {
90
+ errors.push('Missing asset definition');
91
+ } else if (!['glb', 'gltf', 'procedural'].includes(manifest.asset.format)) {
92
+ errors.push('Invalid asset format: must be glb, gltf, or procedural');
93
+ }
94
+
95
+ return { valid: errors.length === 0, errors: errors };
96
+ }
97
+
98
+ // Validate GLB binary data (client-side, called after loading)
99
+ export function validateGLBBytes(arrayBuffer) {
100
+ if (arrayBuffer.byteLength < 12) {
101
+ return { valid: false, error: 'File too small to be a valid GLB' };
102
+ }
103
+
104
+ var view = new DataView(arrayBuffer);
105
+ var magic = view.getUint32(0, true);
106
+ if (magic !== GLB_MAGIC) {
107
+ return { valid: false, error: 'Invalid GLB magic bytes (not a GLB file)' };
108
+ }
109
+
110
+ var version = view.getUint32(4, true);
111
+ if (version !== 2) {
112
+ return { valid: false, error: 'Unsupported GLB version: ' + version + ' (expected 2)' };
113
+ }
114
+
115
+ return { valid: true };
116
+ }
117
+
118
+ // Count polygons in a loaded GLTF scene
119
+ export function countPolygons(gltfScene) {
120
+ var totalPolys = 0;
121
+ gltfScene.traverse(function(child) {
122
+ if (child.isMesh && child.geometry) {
123
+ var geo = child.geometry;
124
+ if (geo.index) {
125
+ totalPolys += geo.index.count / 3;
126
+ } else if (geo.attributes.position) {
127
+ totalPolys += geo.attributes.position.count / 3;
128
+ }
129
+ }
130
+ });
131
+ return Math.floor(totalPolys);
132
+ }
133
+
134
+ // Check bounding box fits expected dimensions
135
+ export function checkBoundingBox(gltfScene, maxDim) {
136
+ var box = new THREE.Box3().setFromObject(gltfScene);
137
+ var size = box.getSize(new THREE.Vector3());
138
+ var max = Math.max(size.x, size.y, size.z);
139
+ if (max > maxDim) {
140
+ return { valid: false, error: 'Model too large: ' + max.toFixed(2) + ' > ' + maxDim + ' max dimension' };
141
+ }
142
+ return { valid: true, size: size };
143
+ }
144
+
145
+ // Sanitize materials (cap emissive, force roughness minimum)
146
+ export function sanitizeMaterials(gltfScene) {
147
+ gltfScene.traverse(function(child) {
148
+ if (child.isMesh && child.material) {
149
+ var mats = Array.isArray(child.material) ? child.material : [child.material];
150
+ mats.forEach(function(mat) {
151
+ if (mat.emissiveIntensity > 2) mat.emissiveIntensity = 2;
152
+ if (mat.roughness !== undefined && mat.roughness < 0.1) mat.roughness = 0.1;
153
+ });
154
+ }
155
+ });
156
+ }
157
+
158
+ // Load a GLB mod and validate it
159
+ export async function loadGLBMod(manifest) {
160
+ if (manifest.asset.format !== 'glb' || !manifest.asset.file) {
161
+ return { success: false, error: 'Not a GLB mod or missing file path' };
162
+ }
163
+
164
+ var limits = TYPE_LIMITS[manifest.type] || TYPE_LIMITS.accessory;
165
+ var url = '/mods/' + manifest.id + '/' + manifest.asset.file;
166
+
167
+ try {
168
+ // Fetch raw bytes for magic check
169
+ var resp = await fetch(url);
170
+ if (!resp.ok) throw new Error('Failed to fetch: ' + resp.status);
171
+ var buffer = await resp.arrayBuffer();
172
+
173
+ // Size check
174
+ if (buffer.byteLength > limits.maxBytes) {
175
+ return { success: false, error: 'File too large: ' + (buffer.byteLength / 1024).toFixed(0) + 'KB > ' + (limits.maxBytes / 1024) + 'KB limit' };
176
+ }
177
+
178
+ // Magic bytes check
179
+ var bytesCheck = validateGLBBytes(buffer);
180
+ if (!bytesCheck.valid) return { success: false, error: bytesCheck.error };
181
+
182
+ // Parse with Three.js GLTFLoader
183
+ var loader = new GLTFLoader();
184
+ var gltf = await new Promise(function(resolve, reject) {
185
+ loader.parse(buffer, '', resolve, reject);
186
+ });
187
+
188
+ // Poly count check
189
+ var polyCount = countPolygons(gltf.scene);
190
+ if (polyCount > limits.maxPolys) {
191
+ return { success: false, error: 'Too many polygons: ' + polyCount + ' > ' + limits.maxPolys + ' limit' };
192
+ }
193
+
194
+ // Bounding box check (max dimension based on type)
195
+ var maxDims = { accessory: 1, hairstyle: 1, outfit: 2, character: 3, environment: 30 };
196
+ var bbCheck = checkBoundingBox(gltf.scene, maxDims[manifest.type] || 1);
197
+ if (!bbCheck.valid) return { success: false, error: bbCheck.error };
198
+
199
+ // Sanitize materials
200
+ sanitizeMaterials(gltf.scene);
201
+
202
+ // Store loaded model
203
+ loadedModels[manifest.id] = gltf;
204
+
205
+ return { success: true, gltf: gltf, polyCount: polyCount, size: bbCheck.size };
206
+ } catch (e) {
207
+ return { success: false, error: 'Failed to load GLB: ' + e.message };
208
+ }
209
+ }
210
+
211
+ // Get a loaded GLB model's scene (clone for instancing)
212
+ export function getModModel(id) {
213
+ if (!loadedModels[id]) return null;
214
+ return loadedModels[id].scene.clone();
215
+ }
216
+
217
+ // Register a mod into the registry
218
+ export function registerMod(manifest) {
219
+ modRegistry[manifest.id] = manifest;
220
+ }
221
+
222
+ // Unregister a mod
223
+ export function unregisterMod(id) {
224
+ delete modRegistry[id];
225
+ if (loadedModels[id]) {
226
+ // Dispose loaded model
227
+ loadedModels[id].scene.traverse(function(child) {
228
+ if (child.geometry) child.geometry.dispose();
229
+ if (child.material) {
230
+ if (Array.isArray(child.material)) child.material.forEach(function(m) { m.dispose(); });
231
+ else child.material.dispose();
232
+ }
233
+ });
234
+ delete loadedModels[id];
235
+ }
236
+ }
237
+
238
+ // Initialize: load registry + built-in manifests
239
+ export async function initModSystem() {
240
+ await loadBuiltInManifests();
241
+
242
+ // Load community registry
243
+ try {
244
+ var resp = await fetch('/api/mods');
245
+ if (resp.ok) {
246
+ var data = await resp.json();
247
+ var mods = data.mods || {};
248
+ for (var id in mods) {
249
+ if (!modRegistry[id]) {
250
+ modRegistry[id] = mods[id];
251
+ }
252
+ }
253
+ }
254
+ } catch (e) {
255
+ console.warn('Could not load mod registry:', e);
256
+ }
257
+ }
@@ -0,0 +1,113 @@
1
+ import * as THREE from 'three';
2
+ import { S } from './state.js';
3
+
4
+ export function updateMonitorScreen(deskIdx, agentName, time) {
5
+ var desk = S.deskMeshes[deskIdx];
6
+ if (!desk) return;
7
+ var W = 256, H = 160;
8
+ if (!S.monitorCanvases[deskIdx]) {
9
+ var cvs = document.createElement('canvas');
10
+ cvs.width = W; cvs.height = H;
11
+ S.monitorCanvases[deskIdx] = { canvas: cvs, texture: new THREE.CanvasTexture(cvs) };
12
+ S.monitorCanvases[deskIdx].texture.minFilter = THREE.LinearFilter;
13
+ desk.screen.material = new THREE.MeshStandardMaterial({
14
+ map: S.monitorCanvases[deskIdx].texture,
15
+ emissive: 0x58a6ff, emissiveIntensity: 0.3, roughness: 0.2
16
+ });
17
+ }
18
+ var mc = S.monitorCanvases[deskIdx];
19
+ var ctx = mc.canvas.getContext('2d');
20
+
21
+ // Terminal background
22
+ ctx.fillStyle = '#0c1021';
23
+ ctx.fillRect(0, 0, W, H);
24
+
25
+ // Title bar
26
+ ctx.fillStyle = '#1a1f36';
27
+ ctx.fillRect(0, 0, W, 14);
28
+ ctx.fillStyle = '#ff5f57'; ctx.beginPath(); ctx.arc(8, 7, 3, 0, Math.PI * 2); ctx.fill();
29
+ ctx.fillStyle = '#ffbd2e'; ctx.beginPath(); ctx.arc(18, 7, 3, 0, Math.PI * 2); ctx.fill();
30
+ ctx.fillStyle = '#28c840'; ctx.beginPath(); ctx.arc(28, 7, 3, 0, Math.PI * 2); ctx.fill();
31
+ ctx.fillStyle = '#8892b0';
32
+ ctx.font = '9px monospace';
33
+ ctx.fillText(agentName + ' \u2014 terminal', 40, 11);
34
+
35
+ // Gather real data
36
+ var history = window.cachedHistory || [];
37
+ var agentInfo = (window.cachedAgents || {})[agentName] || {};
38
+ var lines = [];
39
+
40
+ var statusColor = agentInfo.status === 'active' ? '#28c840' : '#ffbd2e';
41
+ lines.push({ color: '#546178', text: '$ agent status' });
42
+ lines.push({ color: statusColor, text: ' ' + (agentInfo.status || 'unknown').toUpperCase() + (agentInfo.is_listening ? ' (listening)' : ' (working)') });
43
+ lines.push({ color: '#546178', text: '' });
44
+
45
+ var sent = 0, recv = 0;
46
+ for (var i = 0; i < history.length; i++) {
47
+ if (history[i].from === agentName) sent++;
48
+ if (history[i].to === agentName) recv++;
49
+ }
50
+ lines.push({ color: '#546178', text: '$ stats' });
51
+ lines.push({ color: '#79c0ff', text: ' sent: ' + sent + ' recv: ' + recv + ' total: ' + history.length });
52
+ lines.push({ color: '#546178', text: '' });
53
+
54
+ lines.push({ color: '#546178', text: '$ tail -f messages.jsonl' });
55
+ var agentMsgs = [];
56
+ for (var j = history.length - 1; j >= 0 && agentMsgs.length < 4; j--) {
57
+ var m = history[j];
58
+ if (m.from === agentName || m.to === agentName) agentMsgs.unshift(m);
59
+ }
60
+ for (var k = 0; k < agentMsgs.length; k++) {
61
+ var msg = agentMsgs[k];
62
+ var isSent = msg.from === agentName;
63
+ var prefix = isSent ? ' > ' : ' < ';
64
+ var peer = isSent ? (msg.to || 'all') : msg.from;
65
+ var snippet = (msg.content || msg.message || '').substring(0, 28);
66
+ if ((msg.content || msg.message || '').length > 28) snippet += '..';
67
+ lines.push({ color: isSent ? '#7ee787' : '#d2a8ff', text: prefix + peer + ': ' + snippet });
68
+ }
69
+ if (agentMsgs.length === 0) {
70
+ lines.push({ color: '#3d4663', text: ' (no messages yet)' });
71
+ }
72
+
73
+ var showCursor = Math.floor(time * 2) % 2 === 0;
74
+
75
+ ctx.font = '9px monospace';
76
+ var lineY = 24;
77
+ var maxLines = Math.floor((H - 24) / 11);
78
+ var startLine = Math.max(0, lines.length - maxLines);
79
+ for (var r = startLine; r < lines.length; r++) {
80
+ ctx.fillStyle = lines[r].color;
81
+ ctx.fillText(lines[r].text, 4, lineY);
82
+ lineY += 11;
83
+ }
84
+ if (showCursor) {
85
+ ctx.fillStyle = '#58a6ff';
86
+ ctx.fillText('$ _', 4, lineY);
87
+ } else {
88
+ ctx.fillStyle = '#58a6ff';
89
+ ctx.fillText('$', 4, lineY);
90
+ }
91
+
92
+ // Scanline effect
93
+ ctx.fillStyle = 'rgba(0,0,0,0.03)';
94
+ for (var sl = 0; sl < H; sl += 2) {
95
+ ctx.fillRect(0, sl, W, 1);
96
+ }
97
+
98
+ mc.texture.needsUpdate = true;
99
+ }
100
+
101
+ export function setMonitorDim(deskIdx) {
102
+ var desk = S.deskMeshes[deskIdx];
103
+ if (!desk) return;
104
+ if (S.monitorCanvases[deskIdx]) {
105
+ S.monitorCanvases[deskIdx].texture.dispose();
106
+ if (desk.screen.material !== desk.screenMat) desk.screen.material.dispose();
107
+ delete S.monitorCanvases[deskIdx];
108
+ }
109
+ desk.screen.material = desk.screenMat;
110
+ desk.screenMat.emissive.setHex(0x1a2744);
111
+ desk.screenMat.emissiveIntensity = 0.15;
112
+ desk.screenMat.color.setHex(0x1a2744);
113
+ }