RayMelius Claude Opus 4.6 commited on
Commit
d3e36f7
·
1 Parent(s): 97c7b3e

Weather system, day/night cycle, agent behaviors, city expansion

Browse files

- Sun movement east→zenith→west with dynamic shadows
- Weather effects: rain (4000 particles), snow (2000 particles), thunderstorms (lightning flashes), fog, cloudy skies
- Sun brightness/size adjusts per weather (bright sunny → dim stormy)
- Storm clouds layer (18 dark clouds) for bad weather
- Night: residential buildings become transparent, agents sleep in beds
- Furniture inside buildings: tables at restaurants/bars, desks in offices/schools, seats in cinema/church
- Agent poses: walking, sitting (at tables/desks), sleeping (in beds)
- Bad weather: 70% agents stay indoors; night: all go home to sleep
- Demo mode cycles through full day/night + weather types automatically
- Moon enlarged (10), stars visible from 19:00, brighter after 21:00
- Trees halved in size for better proportion with buildings
- City expanded: WORLD_SIZE 100→130, shadow camera enlarged
- 10 new public buildings (bank, court, gallery, daycare, vet, yoga, pub, etc.)
- 4 new streets for expanded area
- Ollama default model: llama3.3:latest
- Snow/snowy weather icon added to both 3d.html and index.html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. src/soci/engine/llm.py +3 -3
  2. web/3d.html +444 -39
  3. web/index.html +35 -5
src/soci/engine/llm.py CHANGED
@@ -27,8 +27,8 @@ MODEL_SONNET = "claude-sonnet-4-5-20250929"
27
  MODEL_HAIKU = "claude-haiku-4-5-20251001"
28
 
29
  # Ollama model IDs (popular open-source models)
30
- MODEL_LLAMA = "llama3.1:8b"
31
- MODEL_LLAMA_SMALL = "llama3.1:8b"
32
  MODEL_MISTRAL = "mistral"
33
  MODEL_QWEN = "qwen2.5"
34
  MODEL_GEMMA = "gemma2"
@@ -1002,7 +1002,7 @@ def create_llm_client(
1002
  default_model = model or os.environ.get("GEMINI_MODEL", MODEL_GEMINI_FLASH)
1003
  return GeminiClient(default_model=default_model)
1004
  elif provider == PROVIDER_OLLAMA:
1005
- default_model = model or os.environ.get("OLLAMA_MODEL", MODEL_OLLAMA_SOCI)
1006
  return OllamaClient(base_url=ollama_url, default_model=default_model)
1007
  else:
1008
  raise ValueError(f"Unknown LLM provider: {provider}. Use 'nn', 'claude', 'groq', 'gemini', or 'ollama'.")
 
27
  MODEL_HAIKU = "claude-haiku-4-5-20251001"
28
 
29
  # Ollama model IDs (popular open-source models)
30
+ MODEL_LLAMA = "llama3.3:latest"
31
+ MODEL_LLAMA_SMALL = "llama3.3:latest"
32
  MODEL_MISTRAL = "mistral"
33
  MODEL_QWEN = "qwen2.5"
34
  MODEL_GEMMA = "gemma2"
 
1002
  default_model = model or os.environ.get("GEMINI_MODEL", MODEL_GEMINI_FLASH)
1003
  return GeminiClient(default_model=default_model)
1004
  elif provider == PROVIDER_OLLAMA:
1005
+ default_model = model or os.environ.get("OLLAMA_MODEL", MODEL_LLAMA)
1006
  return OllamaClient(base_url=ollama_url, default_model=default_model)
1007
  else:
1008
  raise ValueError(f"Unknown LLM provider: {provider}. Use 'nn', 'claude', 'groq', 'gemini', or 'ollama'.")
web/3d.html CHANGED
@@ -171,7 +171,7 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
171
  // ============================================================
172
  // CONSTANTS
173
  // ============================================================
174
- const WORLD_SIZE = 100;
175
  const HALF = WORLD_SIZE / 2;
176
 
177
  const PALETTE = {
@@ -324,6 +324,22 @@ const LOCATION_POSITIONS = {
324
  park_east: { x: 0.86, y: 0.58, type: 'park', label: 'Rose Garden' },
325
  park_south: { x: 0.40, y: 0.88, type: 'park', label: 'Lakeside Park' },
326
  playground: { x: 0.15, y: 0.58, type: 'park', label: 'Kids Playground' },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  };
328
 
329
  function toWorld(nx, ny) {
@@ -389,12 +405,12 @@ sunLight.position.set(30, 50, 20);
389
  sunLight.castShadow = true;
390
  sunLight.shadow.mapSize.width = 2048;
391
  sunLight.shadow.mapSize.height = 2048;
392
- sunLight.shadow.camera.left = -60;
393
- sunLight.shadow.camera.right = 60;
394
- sunLight.shadow.camera.top = 60;
395
- sunLight.shadow.camera.bottom = -60;
396
  sunLight.shadow.camera.near = 1;
397
- sunLight.shadow.camera.far = 150;
398
  sunLight.shadow.bias = -0.001;
399
  scene.add(sunLight);
400
 
@@ -469,6 +485,12 @@ createRoad(28, -HALF * 1.1, 28, HALF * 1.1, 2);
469
  createRoad(35, -HALF * 1.1, 35, HALF * 1.1, 2.5);
470
  createRoad(42, -HALF * 1.1, 42, HALF * 1.1, 2.5);
471
 
 
 
 
 
 
 
472
  // ============================================================
473
  // MATERIAL HELPERS
474
  // ============================================================
@@ -619,6 +641,7 @@ function createHouse(id, locData) {
619
  mat(BLDG_COLORS.door)
620
  );
621
  door.position.set(0, 0.6, d / 2 + 0.05);
 
622
  group.add(door);
623
 
624
  // Windows
@@ -632,6 +655,26 @@ function createHouse(id, locData) {
632
  group.add(win);
633
  }
634
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
  const label = createLabel(locData.label, group, wallH + roofH + 1);
636
  const badge = createOccupantBadge(group, wallH + roofH);
637
  group.userData = { id, type: 'house', label, badge, locData };
@@ -672,6 +715,16 @@ function createApartment(id, locData) {
672
  }
673
  }
674
 
 
 
 
 
 
 
 
 
 
 
675
  const label = createLabel(locData.label, group, wallH + 2);
676
  const badge = createOccupantBadge(group, wallH + 1);
677
  group.userData = { id, type: 'apartment', label, badge, locData };
@@ -1006,7 +1059,7 @@ function createPark(id, locData) {
1006
  const tx = (Math.random() - 0.5) * 6;
1007
  const tz = (Math.random() - 0.5) * 4;
1008
  if (Math.abs(tx) > 1) {
1009
- const t = createTree(0, 0, 0.7 + Math.random() * 0.5);
1010
  t.position.set(tx, 0, tz);
1011
  scene.remove(t);
1012
  group.add(t);
@@ -1271,6 +1324,7 @@ function createBuilding(id, locData) {
1271
  case 'market': bldg = createMarket(id, locData); break;
1272
  default: bldg = createShop(id, locData); break;
1273
  }
 
1274
  const pos = toWorld(locData.x, locData.y);
1275
  bldg.position.set(pos.x, 0, pos.z);
1276
  scene.add(bldg);
@@ -1278,6 +1332,53 @@ function createBuilding(id, locData) {
1278
  return bldg;
1279
  }
1280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1281
  // ============================================================
1282
  // PLACE ALL BUILDINGS
1283
  // ============================================================
@@ -1310,7 +1411,7 @@ for (let i = 0; i < 80; i++) {
1310
  }
1311
  if (Math.abs(tx) < 2.5 || Math.abs(tz + 7) < 2.5) tooClose = true; // avoid main roads
1312
  if (!tooClose) {
1313
- createTree(tx, tz, 0.6 + Math.random() * 0.8);
1314
  treePositions.push({ x: tx, z: tz });
1315
  }
1316
  }
@@ -1426,7 +1527,7 @@ scene.add(sunSprite);
1426
  const moonSprite = new THREE.Sprite(
1427
  new THREE.SpriteMaterial({ map: celestialTex(0xccddff), transparent: true, depthTest: false })
1428
  );
1429
- moonSprite.scale.set(7, 7, 1);
1430
  moonSprite.renderOrder = -1;
1431
  moonSprite.visible = false;
1432
  scene.add(moonSprite);
@@ -1466,6 +1567,127 @@ for (let ci = 0; ci < 7; ci++) {
1466
  clouds.push(cg);
1467
  }
1468
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1469
  // Position sun/moon based on hour
1470
  function updateCelestials(hour) {
1471
  // Sun: rises 6, peaks 12, sets 18
@@ -1487,7 +1709,8 @@ function updateCelestials(hour) {
1487
  } else {
1488
  moonSprite.visible = false;
1489
  }
1490
- starPoints.visible = hour >= 20 || hour <= 5;
 
1491
  }
1492
  updateCelestials(10);
1493
 
@@ -1543,7 +1766,7 @@ function updateTimeOfDay(phase, hour) {
1543
  scene.fog = new THREE.FogExp2(PALETTE.fog, 0.004);
1544
  }
1545
 
1546
- // Window glow
1547
  scene.traverse(o => {
1548
  if (o.userData?.isWindow && o.material) {
1549
  if (isNight) {
@@ -1557,6 +1780,26 @@ function updateTimeOfDay(phase, hour) {
1557
  }
1558
  }
1559
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1560
  }
1561
 
1562
  // Parse simulation time string → { hour, phase }
@@ -1600,6 +1843,13 @@ function createLimb(length, radius, color) {
1600
  return g;
1601
  }
1602
 
 
 
 
 
 
 
 
1603
  function createAgentMesh(agentId, agentData) {
1604
  const group = new THREE.Group();
1605
  const idx = getAgentIdx(agentId);
@@ -1607,7 +1857,9 @@ function createAgentMesh(agentId, agentData) {
1607
  const shirtColor = AGENT_COLORS[idx % AGENT_COLORS.length];
1608
  const skinColor = SKIN_COLORS[h % SKIN_COLORS.length];
1609
  const hairColor = HAIR_COLORS[(h >> 3) % HAIR_COLORS.length];
1610
- const wearsSkirt = (h >> 6) % 3 === 0;
 
 
1611
  const bottomColor = wearsSkirt
1612
  ? SKIRT_COLORS[(h >> 8) % SKIRT_COLORS.length]
1613
  : PANTS_COLORS[(h >> 8) % PANTS_COLORS.length];
@@ -1756,6 +2008,7 @@ const dynamicLocations = {};
1756
  let simState = {};
1757
  let wsConnected = false;
1758
  let ws = null;
 
1759
 
1760
  function connectWebSocket() {
1761
  const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -1793,10 +2046,13 @@ function handleStateUpdate(msg) {
1793
  const agents = simState.agents || {};
1794
  const locations = simState.locations || {};
1795
 
 
 
 
1796
  // Update time display
1797
  document.getElementById('sim-time').textContent = msg.time || '';
1798
  const weather = simState.weather || simState.clock?.weather || '';
1799
- const weatherIcons = { sunny: '☀️', clear: '☀️', cloudy: '☁️', rainy: '🌧️', stormy: '⛈️', foggy: '🌫️' };
1800
  document.getElementById('sim-weather').textContent = weatherIcons[weather] || '';
1801
  document.getElementById('sim-agents').textContent = `${Object.keys(agents).length} agents`;
1802
 
@@ -1805,9 +2061,10 @@ function handleStateUpdate(msg) {
1805
  document.getElementById('sim-cost').textContent = `$${parseFloat(cost).toFixed(4)}`;
1806
  }
1807
 
1808
- // Update sky / lighting from simulation time
1809
  const { hour, phase } = parseSimTime(msg.time || '');
1810
  updateTimeOfDay(phase, hour);
 
1811
 
1812
  // Register dynamic locations
1813
  for (const [locId, locData] of Object.entries(locations)) {
@@ -2213,20 +2470,44 @@ function animate() {
2213
  }
2214
  }
2215
 
2216
- // Walking animation — swing limbs when moving, gentle bob when idle
 
 
 
2217
  const dx2 = target ? target.x - mesh.position.x : 0;
2218
  const dz2 = target ? target.z - mesh.position.z : 0;
2219
  const dist = Math.sqrt(dx2 * dx2 + dz2 * dz2);
2220
  const moving = dist > 0.15;
2221
- const walkPhase = frameCount * 0.12 + hash(agentId) * 0.5;
2222
- const swing = moving ? Math.sin(walkPhase) * 0.7 : 0;
2223
- if (mesh.userData.leftLeg) mesh.userData.leftLeg.rotation.x = swing;
2224
- if (mesh.userData.rightLeg) mesh.userData.rightLeg.rotation.x = -swing;
2225
- if (mesh.userData.leftArm) mesh.userData.leftArm.rotation.x = -swing * 0.5;
2226
- if (mesh.userData.rightArm) mesh.userData.rightArm.rotation.x = swing * 0.5;
2227
- mesh.position.y = moving
2228
- ? Math.abs(Math.sin(walkPhase * 2)) * 0.04
2229
- : Math.sin(frameCount * 0.02 + hash(agentId) * 0.1) * 0.015;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2230
  }
2231
 
2232
  // Subtle tree sway
@@ -2238,11 +2519,65 @@ function animate() {
2238
  });
2239
  }
2240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2241
  // Cloud drift
2242
  for (const cloud of clouds) {
2243
  cloud.position.x += cloud.userData.speed;
2244
  if (cloud.position.x > 70) cloud.position.x = -70;
2245
  }
 
 
 
 
 
2246
 
2247
  renderer.render(scene, camera);
2248
  }
@@ -2289,7 +2624,6 @@ async function pollFallback() {
2289
  // ============================================================
2290
  // BOOT
2291
  // ============================================================
2292
- const isEmbedded = window.parent !== window;
2293
  if (isEmbedded) {
2294
  document.getElementById('status-bar').style.display = 'none';
2295
  document.getElementById('controls-bar').style.display = 'none';
@@ -2400,30 +2734,101 @@ function spawnDemoAgents() {
2400
  for (const d of demoNames) {
2401
  demoAgents[d.id] = { name: d.name, location: d.location, state: 'idle', needs: {} };
2402
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2403
  handleStateUpdate({
2404
- type: 'tick', time: 'Day 1, 10:30 (morning)',
2405
- state: { agents: demoAgents, locations: {}, weather: 'sunny' }
2406
  });
2407
- document.getElementById('sim-time').textContent = 'Day 1, 10:30 (morning)';
2408
- document.getElementById('sim-weather').textContent = '☀️';
2409
  document.getElementById('sim-agents').textContent = `${demoNames.length} agents (demo)`;
2410
 
2411
- // Animate demo agents moving between locations
2412
- const locIds = Object.keys(LOCATION_POSITIONS).filter(k => !k.startsWith('street'));
2413
  setInterval(() => {
2414
  if (wsConnected) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
2415
  const agents = Object.keys(demoAgents);
2416
- const moveCount = 3 + Math.floor(Math.random() * 5);
2417
- for (let i = 0; i < moveCount; i++) {
2418
- const who = agents[Math.floor(Math.random() * agents.length)];
2419
- const dest = locIds[Math.floor(Math.random() * locIds.length)];
2420
- demoAgents[who].location = dest;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2421
  }
 
2422
  handleStateUpdate({
2423
- type: 'tick', time: 'Day 1, 10:30 (morning)',
2424
- state: { agents: demoAgents, locations: {}, weather: 'sunny' }
2425
  });
2426
- }, 2000);
2427
  }
2428
 
2429
  // Hide zoom hint after a few seconds
 
171
  // ============================================================
172
  // CONSTANTS
173
  // ============================================================
174
+ const WORLD_SIZE = 130;
175
  const HALF = WORLD_SIZE / 2;
176
 
177
  const PALETTE = {
 
324
  park_east: { x: 0.86, y: 0.58, type: 'park', label: 'Rose Garden' },
325
  park_south: { x: 0.40, y: 0.88, type: 'park', label: 'Lakeside Park' },
326
  playground: { x: 0.15, y: 0.58, type: 'park', label: 'Kids Playground' },
327
+
328
+ // ── Additional public buildings ───────────────────────────
329
+ post_office: { x: 0.30, y: 0.58, type: 'office', label: 'Post Office' },
330
+ bank: { x: 0.60, y: 0.58, type: 'office', label: 'City Bank' },
331
+ court: { x: 0.44, y: 0.58, type: 'townhall', label: 'Courthouse' },
332
+ gallery: { x: 0.15, y: 0.34, type: 'museum', label: 'Art Gallery' },
333
+ daycare: { x: 0.15, y: 0.42, type: 'school', label: 'Sunny Daycare' },
334
+ vet_clinic: { x: 0.65, y: 0.58, type: 'hospital', label: 'Vet Clinic' },
335
+ yoga_studio: { x: 0.56, y: 0.58, type: 'shop', label: 'Zen Yoga' },
336
+ pub: { x: 0.70, y: 0.42, type: 'shop', label: "The Oak Pub" },
337
+ ice_cream: { x: 0.30, y: 0.42, type: 'shop', label: 'Scoop & Joy' },
338
+ taxi_stand: { x: 0.50, y: 0.42, type: 'shop', label: 'Taxi Stand' },
339
+
340
+ // ── Streets ───────────────────────────────────────────────
341
+ street_main: { x: 0.50, y: 0.28, type: 'square', label: 'Main Street' },
342
+ street_west: { x: 0.15, y: 0.50, type: 'square', label: 'West Avenue' },
343
  };
344
 
345
  function toWorld(nx, ny) {
 
405
  sunLight.castShadow = true;
406
  sunLight.shadow.mapSize.width = 2048;
407
  sunLight.shadow.mapSize.height = 2048;
408
+ sunLight.shadow.camera.left = -80;
409
+ sunLight.shadow.camera.right = 80;
410
+ sunLight.shadow.camera.top = 80;
411
+ sunLight.shadow.camera.bottom = -80;
412
  sunLight.shadow.camera.near = 1;
413
+ sunLight.shadow.camera.far = 200;
414
  sunLight.shadow.bias = -0.001;
415
  scene.add(sunLight);
416
 
 
485
  createRoad(35, -HALF * 1.1, 35, HALF * 1.1, 2.5);
486
  createRoad(42, -HALF * 1.1, 42, HALF * 1.1, 2.5);
487
 
488
+ // Extra streets for expanded city
489
+ createRoad(-HALF * 1.1, -48, HALF * 1.1, -48, 2); // far far north
490
+ createRoad(-HALF * 1.1, 48, HALF * 1.1, 48, 2); // far far south
491
+ createRoad(-52, -HALF * 1.1, -52, HALF * 1.1, 2); // far west
492
+ createRoad(52, -HALF * 1.1, 52, HALF * 1.1, 2); // far east
493
+
494
  // ============================================================
495
  // MATERIAL HELPERS
496
  // ============================================================
 
641
  mat(BLDG_COLORS.door)
642
  );
643
  door.position.set(0, 0.6, d / 2 + 0.05);
644
+ door.userData.isDoor = true;
645
  group.add(door);
646
 
647
  // Windows
 
655
  group.add(win);
656
  }
657
 
658
+ // Bed (visible when house is transparent at night)
659
+ const bed = new THREE.Mesh(
660
+ new THREE.BoxGeometry(1.2, 0.15, 0.7),
661
+ mat(0x8b6040)
662
+ );
663
+ bed.position.set(0.6, 0.08, 0);
664
+ group.add(bed);
665
+ const mattress = new THREE.Mesh(
666
+ new THREE.BoxGeometry(1.1, 0.1, 0.6),
667
+ mat(0xe8e0d0)
668
+ );
669
+ mattress.position.set(0.6, 0.18, 0);
670
+ group.add(mattress);
671
+ const pillow = new THREE.Mesh(
672
+ new THREE.BoxGeometry(0.25, 0.08, 0.35),
673
+ mat(0xf0f0f0)
674
+ );
675
+ pillow.position.set(1.1, 0.24, 0);
676
+ group.add(pillow);
677
+
678
  const label = createLabel(locData.label, group, wallH + roofH + 1);
679
  const badge = createOccupantBadge(group, wallH + roofH);
680
  group.userData = { id, type: 'house', label, badge, locData };
 
715
  }
716
  }
717
 
718
+ // Beds inside (visible at night through transparent walls)
719
+ for (let bx = -1; bx <= 1; bx += 2) {
720
+ const bed = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.12, 0.5), mat(0x8b6040));
721
+ bed.position.set(bx * 1.2, 0.06, 0);
722
+ group.add(bed);
723
+ const matt = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.08, 0.45), mat(0xe0d8c8));
724
+ matt.position.set(bx * 1.2, 0.14, 0);
725
+ group.add(matt);
726
+ }
727
+
728
  const label = createLabel(locData.label, group, wallH + 2);
729
  const badge = createOccupantBadge(group, wallH + 1);
730
  group.userData = { id, type: 'apartment', label, badge, locData };
 
1059
  const tx = (Math.random() - 0.5) * 6;
1060
  const tz = (Math.random() - 0.5) * 4;
1061
  if (Math.abs(tx) > 1) {
1062
+ const t = createTree(0, 0, 0.35 + Math.random() * 0.25);
1063
  t.position.set(tx, 0, tz);
1064
  scene.remove(t);
1065
  group.add(t);
 
1324
  case 'market': bldg = createMarket(id, locData); break;
1325
  default: bldg = createShop(id, locData); break;
1326
  }
1327
+ addFurniture(id, bldg, locData);
1328
  const pos = toWorld(locData.x, locData.y);
1329
  bldg.position.set(pos.x, 0, pos.z);
1330
  scene.add(bldg);
 
1332
  return bldg;
1333
  }
1334
 
1335
+ const FOOD_LOCS = new Set(['restaurant','bar','cafe','diner','bakery','sushi_bar','pizzeria','coffeehouse','pub','ice_cream']);
1336
+ const DESK_LOCS = new Set(['office','office_tech','office_media','school','library','townhall','post_office','bank','court','daycare']);
1337
+ const SEAT_LOCS = new Set(['cinema','church','museum']);
1338
+ const SITTING_LOCS = new Set([...FOOD_LOCS, ...DESK_LOCS, ...SEAT_LOCS]);
1339
+
1340
+ function addFurniture(id, group, locData) {
1341
+ const tableMat = mat(0x8b6040);
1342
+ const chairMat = mat(0x6a5030);
1343
+ const deskMat = mat(0xa09080);
1344
+
1345
+ if (FOOD_LOCS.has(id)) {
1346
+ for (let i = -1; i <= 1; i++) {
1347
+ const table = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.4, 0.7), tableMat);
1348
+ table.position.set(i * 1.6, 0.2, 3.0);
1349
+ group.add(table);
1350
+ for (let s of [-0.5, 0.5]) {
1351
+ const chair = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.25, 0.3), chairMat);
1352
+ chair.position.set(i * 1.6 + s, 0.125, 3.6);
1353
+ group.add(chair);
1354
+ const back = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.05), chairMat);
1355
+ back.position.set(i * 1.6 + s, 0.35, 3.75);
1356
+ group.add(back);
1357
+ }
1358
+ }
1359
+ } else if (DESK_LOCS.has(id)) {
1360
+ for (let i = -1; i <= 1; i += 2) {
1361
+ const desk = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.45, 0.6), deskMat);
1362
+ desk.position.set(i * 1.3, 0.225, 3.0);
1363
+ group.add(desk);
1364
+ const ch = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.25, 0.3), chairMat);
1365
+ ch.position.set(i * 1.3, 0.125, 3.5);
1366
+ group.add(ch);
1367
+ }
1368
+ } else if (SEAT_LOCS.has(id)) {
1369
+ for (let row = 0; row < 2; row++) {
1370
+ for (let col = -1; col <= 1; col++) {
1371
+ const seat = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.25, 0.35), chairMat);
1372
+ seat.position.set(col * 0.7, 0.125, 2.5 + row * 0.8);
1373
+ group.add(seat);
1374
+ const sb = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.35, 0.05), chairMat);
1375
+ sb.position.set(col * 0.7, 0.35, 2.5 + row * 0.8 + 0.18);
1376
+ group.add(sb);
1377
+ }
1378
+ }
1379
+ }
1380
+ }
1381
+
1382
  // ============================================================
1383
  // PLACE ALL BUILDINGS
1384
  // ============================================================
 
1411
  }
1412
  if (Math.abs(tx) < 2.5 || Math.abs(tz + 7) < 2.5) tooClose = true; // avoid main roads
1413
  if (!tooClose) {
1414
+ createTree(tx, tz, 0.3 + Math.random() * 0.4);
1415
  treePositions.push({ x: tx, z: tz });
1416
  }
1417
  }
 
1527
  const moonSprite = new THREE.Sprite(
1528
  new THREE.SpriteMaterial({ map: celestialTex(0xccddff), transparent: true, depthTest: false })
1529
  );
1530
+ moonSprite.scale.set(10, 10, 1);
1531
  moonSprite.renderOrder = -1;
1532
  moonSprite.visible = false;
1533
  scene.add(moonSprite);
 
1567
  clouds.push(cg);
1568
  }
1569
 
1570
+ // ============================================================
1571
+ // WEATHER EFFECTS — rain, snow, lightning, storm clouds
1572
+ // ============================================================
1573
+ let currentWeather = 'sunny';
1574
+
1575
+ const stormClouds = [];
1576
+ for (let ci = 0; ci < 18; ci++) {
1577
+ const cg = new THREE.Group();
1578
+ const nPuffs = 4 + Math.floor(Math.random() * 5);
1579
+ for (let j = 0; j < nPuffs; j++) {
1580
+ const pf = new THREE.Mesh(
1581
+ new THREE.SphereGeometry(2.0 + Math.random() * 2.8, 7, 5),
1582
+ new THREE.MeshStandardMaterial({ color: 0x556070, roughness: 1, metalness: 0, transparent: true, opacity: 0.85, flatShading: true })
1583
+ );
1584
+ pf.scale.y = 0.25;
1585
+ pf.position.set((Math.random() - 0.5) * 10, (Math.random() - 0.5) * 0.6, (Math.random() - 0.5) * 6);
1586
+ cg.add(pf);
1587
+ }
1588
+ cg.position.set((Math.random() - 0.5) * 140, 17 + Math.random() * 8, (Math.random() - 0.5) * 140);
1589
+ cg.userData.speed = 0.007 + Math.random() * 0.012;
1590
+ cg.visible = false;
1591
+ scene.add(cg);
1592
+ stormClouds.push(cg);
1593
+ }
1594
+
1595
+ const RAIN_COUNT = 4000;
1596
+ const rainGeo = new THREE.BufferGeometry();
1597
+ const rainPositions = new Float32Array(RAIN_COUNT * 3);
1598
+ const rainVelocities = new Float32Array(RAIN_COUNT);
1599
+ for (let i = 0; i < RAIN_COUNT; i++) {
1600
+ rainPositions[i*3] = (Math.random() - 0.5) * 120;
1601
+ rainPositions[i*3+1] = Math.random() * 40;
1602
+ rainPositions[i*3+2] = (Math.random() - 0.5) * 120;
1603
+ rainVelocities[i] = 0.45 + Math.random() * 0.35;
1604
+ }
1605
+ rainGeo.setAttribute('position', new THREE.BufferAttribute(rainPositions, 3));
1606
+ const rainMat = new THREE.PointsMaterial({ color: 0x8899cc, size: 0.1, transparent: true, opacity: 0.45 });
1607
+ const rainSystem = new THREE.Points(rainGeo, rainMat);
1608
+ rainSystem.visible = false;
1609
+ scene.add(rainSystem);
1610
+
1611
+ const SNOW_COUNT = 2000;
1612
+ const snowGeo = new THREE.BufferGeometry();
1613
+ const snowPositions = new Float32Array(SNOW_COUNT * 3);
1614
+ const snowDriftX = new Float32Array(SNOW_COUNT);
1615
+ const snowDriftZ = new Float32Array(SNOW_COUNT);
1616
+ for (let i = 0; i < SNOW_COUNT; i++) {
1617
+ snowPositions[i*3] = (Math.random() - 0.5) * 120;
1618
+ snowPositions[i*3+1] = Math.random() * 35;
1619
+ snowPositions[i*3+2] = (Math.random() - 0.5) * 120;
1620
+ snowDriftX[i] = (Math.random() - 0.5) * 0.012;
1621
+ snowDriftZ[i] = (Math.random() - 0.5) * 0.012;
1622
+ }
1623
+ snowGeo.setAttribute('position', new THREE.BufferAttribute(snowPositions, 3));
1624
+ const snowMat = new THREE.PointsMaterial({ color: 0xffffff, size: 0.2, transparent: true, opacity: 0.85 });
1625
+ const snowSystem = new THREE.Points(snowGeo, snowMat);
1626
+ snowSystem.visible = false;
1627
+ scene.add(snowSystem);
1628
+
1629
+ const lightningLight = new THREE.PointLight(0xccddff, 0, 200);
1630
+ lightningLight.position.set(0, 50, 0);
1631
+ scene.add(lightningLight);
1632
+ let lightningTimer = 0;
1633
+ let nextLightningAt = 0;
1634
+
1635
+ function updateWeather(weather) {
1636
+ currentWeather = (weather || 'sunny').toLowerCase();
1637
+ const isSunny = currentWeather === 'sunny' || currentWeather === 'clear';
1638
+ const isCloudy = currentWeather === 'cloudy';
1639
+ const isRainy = currentWeather === 'rainy';
1640
+ const isStormy = currentWeather === 'stormy';
1641
+ const isSnowy = currentWeather === 'snowy' || currentWeather === 'snow';
1642
+ const isFoggy = currentWeather === 'foggy';
1643
+ const precipitating = isRainy || isStormy || isSnowy;
1644
+
1645
+ if (sunSprite.visible) {
1646
+ sunSprite.material.opacity = isSunny ? 1.0 : isCloudy ? 0.3 : precipitating ? 0.05 : isFoggy ? 0.35 : 0.8;
1647
+ const ss = isSunny ? 16 : isCloudy ? 10 : precipitating ? 6 : 14;
1648
+ sunSprite.scale.set(ss, ss, 1);
1649
+ }
1650
+
1651
+ if (!isNight) {
1652
+ const wm = isSunny ? 1.0 : isCloudy ? 0.55 : isRainy ? 0.3 : isStormy ? 0.2 : isSnowy ? 0.4 : isFoggy ? 0.35 : 1.0;
1653
+ sunLight.intensity *= wm;
1654
+ ambientLight.intensity = Math.max(ambientLight.intensity * (0.5 + wm * 0.5), 0.08);
1655
+ hemiLight.intensity *= (0.6 + wm * 0.4);
1656
+ }
1657
+
1658
+ const fairVis = !(precipitating);
1659
+ const fairOp = isSunny ? 0.7 : isCloudy ? 0.95 : 0.5;
1660
+ clouds.forEach(c => {
1661
+ c.visible = fairVis || isCloudy;
1662
+ if (!isNight) c.children.forEach(p => { p.material.opacity = fairOp; });
1663
+ });
1664
+
1665
+ const showStorm = isCloudy || precipitating;
1666
+ const sc = isStormy ? 0x3a3a50 : isRainy ? 0x4a5560 : isSnowy ? 0x8890a0 : 0x6a7580;
1667
+ const so = isStormy ? 0.95 : isRainy ? 0.88 : isSnowy ? 0.8 : 0.7;
1668
+ stormClouds.forEach(c => {
1669
+ c.visible = showStorm;
1670
+ c.children.forEach(p => { p.material.color.set(sc); p.material.opacity = so; });
1671
+ });
1672
+
1673
+ rainSystem.visible = isRainy || isStormy;
1674
+ rainMat.opacity = isStormy ? 0.65 : 0.4;
1675
+
1676
+ snowSystem.visible = isSnowy;
1677
+
1678
+ if (isFoggy && !isNight) {
1679
+ scene.fog = new THREE.FogExp2(0xaaaaaa, 0.018);
1680
+ } else if (isStormy && !isNight) {
1681
+ scene.fog = new THREE.FogExp2(0x333340, 0.009);
1682
+ } else if (isRainy && !isNight) {
1683
+ scene.fog = new THREE.FogExp2(0x556678, 0.007);
1684
+ } else if (isSnowy && !isNight) {
1685
+ scene.fog = new THREE.FogExp2(0x99a0a8, 0.006);
1686
+ }
1687
+
1688
+ if (isSnowy && !isNight) ground.material.color.set(0xd8dce0);
1689
+ }
1690
+
1691
  // Position sun/moon based on hour
1692
  function updateCelestials(hour) {
1693
  // Sun: rises 6, peaks 12, sets 18
 
1709
  } else {
1710
  moonSprite.visible = false;
1711
  }
1712
+ starPoints.visible = hour >= 19 || hour <= 5;
1713
+ starPoints.material.opacity = (hour >= 21 || hour <= 4) ? 0.9 : 0.5;
1714
  }
1715
  updateCelestials(10);
1716
 
 
1766
  scene.fog = new THREE.FogExp2(PALETTE.fog, 0.004);
1767
  }
1768
 
1769
+ // Window glow + building transparency at night
1770
  scene.traverse(o => {
1771
  if (o.userData?.isWindow && o.material) {
1772
  if (isNight) {
 
1780
  }
1781
  }
1782
  });
1783
+
1784
+ const deepNight = phase === 'night';
1785
+ for (const [, bldg] of buildingMeshes) {
1786
+ const btype = bldg.userData?.type;
1787
+ const isResidential = btype === 'house' || btype === 'apartment';
1788
+ if (!isResidential) continue;
1789
+ bldg.traverse(child => {
1790
+ if (child.isMesh && !child.userData?.isWindow && !child.userData?.isDoor) {
1791
+ if (deepNight) {
1792
+ child.material.transparent = true;
1793
+ child.material.opacity = 0.3;
1794
+ child.material.depthWrite = false;
1795
+ } else {
1796
+ child.material.transparent = false;
1797
+ child.material.opacity = 1.0;
1798
+ child.material.depthWrite = true;
1799
+ }
1800
+ }
1801
+ });
1802
+ }
1803
  }
1804
 
1805
  // Parse simulation time string → { hour, phase }
 
1843
  return g;
1844
  }
1845
 
1846
+ const FEMALE_NAMES = new Set([
1847
+ 'elena','lila','helen','diana','priya','rosa','yuki','nina','zoe','alice',
1848
+ 'ada','mia','dara','hana','vera','nadia','petra','ling','maya','sophie',
1849
+ 'anya','clara','elsa','greta','irene','kira','marta','olga','rita','tanya',
1850
+ 'viola','xena','zara','bianca','dina','fiona','hazel','jenna',
1851
+ ]);
1852
+
1853
  function createAgentMesh(agentId, agentData) {
1854
  const group = new THREE.Group();
1855
  const idx = getAgentIdx(agentId);
 
1857
  const shirtColor = AGENT_COLORS[idx % AGENT_COLORS.length];
1858
  const skinColor = SKIN_COLORS[h % SKIN_COLORS.length];
1859
  const hairColor = HAIR_COLORS[(h >> 3) % HAIR_COLORS.length];
1860
+ const genderStr = (agentData?.gender || '').toLowerCase();
1861
+ const isFemale = genderStr === 'female' || (!genderStr && FEMALE_NAMES.has(agentId.toLowerCase()));
1862
+ const wearsSkirt = isFemale;
1863
  const bottomColor = wearsSkirt
1864
  ? SKIRT_COLORS[(h >> 8) % SKIRT_COLORS.length]
1865
  : PANTS_COLORS[(h >> 8) % PANTS_COLORS.length];
 
2008
  let simState = {};
2009
  let wsConnected = false;
2010
  let ws = null;
2011
+ const isEmbedded = window.parent !== window;
2012
 
2013
  function connectWebSocket() {
2014
  const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
 
2046
  const agents = simState.agents || {};
2047
  const locations = simState.locations || {};
2048
 
2049
+ // Forward state to parent when embedded
2050
+ if (isEmbedded) window.parent.postMessage({ type: 'state-update', state: simState, time: msg.time }, '*');
2051
+
2052
  // Update time display
2053
  document.getElementById('sim-time').textContent = msg.time || '';
2054
  const weather = simState.weather || simState.clock?.weather || '';
2055
+ const weatherIcons = { sunny: '☀️', clear: '☀️', cloudy: '☁️', rainy: '🌧️', stormy: '⛈️', foggy: '🌫️', snowy: '🌨️', snow: '🌨️' };
2056
  document.getElementById('sim-weather').textContent = weatherIcons[weather] || '';
2057
  document.getElementById('sim-agents').textContent = `${Object.keys(agents).length} agents`;
2058
 
 
2061
  document.getElementById('sim-cost').textContent = `$${parseFloat(cost).toFixed(4)}`;
2062
  }
2063
 
2064
+ // Update sky / lighting from simulation time + weather
2065
  const { hour, phase } = parseSimTime(msg.time || '');
2066
  updateTimeOfDay(phase, hour);
2067
+ updateWeather(weather);
2068
 
2069
  // Register dynamic locations
2070
  for (const [locId, locData] of Object.entries(locations)) {
 
2470
  }
2471
  }
2472
 
2473
+ // Sleeping, sitting, walking, or idle animation
2474
+ const agentState = mesh.userData.data?.state || '';
2475
+ const agentLoc = mesh.userData.data?.location || '';
2476
+ const isSleeping = agentState === 'sleeping';
2477
  const dx2 = target ? target.x - mesh.position.x : 0;
2478
  const dz2 = target ? target.z - mesh.position.z : 0;
2479
  const dist = Math.sqrt(dx2 * dx2 + dz2 * dz2);
2480
  const moving = dist > 0.15;
2481
+ const atLocation = dist < 0.3;
2482
+ const isSitting = !moving && atLocation && SITTING_LOCS.has(agentLoc);
2483
+
2484
+ if (isSleeping) {
2485
+ mesh.rotation.x = Math.PI / 2;
2486
+ mesh.rotation.z = 0;
2487
+ mesh.position.y = 0.35;
2488
+ if (mesh.userData.leftLeg) mesh.userData.leftLeg.rotation.x = 0.15;
2489
+ if (mesh.userData.rightLeg) mesh.userData.rightLeg.rotation.x = -0.08;
2490
+ if (mesh.userData.leftArm) mesh.userData.leftArm.rotation.x = 0.6;
2491
+ if (mesh.userData.rightArm) mesh.userData.rightArm.rotation.x = 0.4;
2492
+ } else if (isSitting) {
2493
+ mesh.rotation.x = 0;
2494
+ mesh.position.y = 0.0;
2495
+ if (mesh.userData.leftLeg) mesh.userData.leftLeg.rotation.x = -Math.PI / 2;
2496
+ if (mesh.userData.rightLeg) mesh.userData.rightLeg.rotation.x = -Math.PI / 2;
2497
+ if (mesh.userData.leftArm) mesh.userData.leftArm.rotation.x = -0.3;
2498
+ if (mesh.userData.rightArm) mesh.userData.rightArm.rotation.x = -0.3;
2499
+ } else {
2500
+ mesh.rotation.x = 0;
2501
+ const walkPhase = frameCount * 0.12 + hash(agentId) * 0.5;
2502
+ const swing = moving ? Math.sin(walkPhase) * 0.7 : 0;
2503
+ if (mesh.userData.leftLeg) mesh.userData.leftLeg.rotation.x = swing;
2504
+ if (mesh.userData.rightLeg) mesh.userData.rightLeg.rotation.x = -swing;
2505
+ if (mesh.userData.leftArm) mesh.userData.leftArm.rotation.x = -swing * 0.5;
2506
+ if (mesh.userData.rightArm) mesh.userData.rightArm.rotation.x = swing * 0.5;
2507
+ mesh.position.y = moving
2508
+ ? Math.abs(Math.sin(walkPhase * 2)) * 0.04
2509
+ : Math.sin(frameCount * 0.02 + hash(agentId) * 0.1) * 0.015;
2510
+ }
2511
  }
2512
 
2513
  // Subtle tree sway
 
2519
  });
2520
  }
2521
 
2522
+ // Rain animation
2523
+ if (rainSystem.visible) {
2524
+ const rp = rainGeo.attributes.position.array;
2525
+ const windX = currentWeather === 'stormy' ? 0.06 : 0.012;
2526
+ for (let i = 0; i < RAIN_COUNT; i++) {
2527
+ rp[i*3+1] -= rainVelocities[i];
2528
+ rp[i*3] += windX;
2529
+ if (rp[i*3+1] < 0) {
2530
+ rp[i*3] = (Math.random() - 0.5) * 120;
2531
+ rp[i*3+1] = 35 + Math.random() * 10;
2532
+ rp[i*3+2] = (Math.random() - 0.5) * 120;
2533
+ }
2534
+ }
2535
+ rainGeo.attributes.position.needsUpdate = true;
2536
+ }
2537
+
2538
+ // Snow animation
2539
+ if (snowSystem.visible) {
2540
+ const sp = snowGeo.attributes.position.array;
2541
+ for (let i = 0; i < SNOW_COUNT; i++) {
2542
+ sp[i*3] += snowDriftX[i] + Math.sin(frameCount * 0.008 + i) * 0.004;
2543
+ sp[i*3+1] -= 0.025 + Math.random() * 0.008;
2544
+ sp[i*3+2] += snowDriftZ[i];
2545
+ if (sp[i*3+1] < 0) {
2546
+ sp[i*3] = (Math.random() - 0.5) * 120;
2547
+ sp[i*3+1] = 28 + Math.random() * 10;
2548
+ sp[i*3+2] = (Math.random() - 0.5) * 120;
2549
+ }
2550
+ }
2551
+ snowGeo.attributes.position.needsUpdate = true;
2552
+ }
2553
+
2554
+ // Lightning flashes
2555
+ if (currentWeather === 'stormy') {
2556
+ if (frameCount >= nextLightningAt) {
2557
+ lightningLight.intensity = 10 + Math.random() * 15;
2558
+ lightningLight.position.set((Math.random() - 0.5) * 60, 45, (Math.random() - 0.5) * 60);
2559
+ lightningTimer = 2 + Math.floor(Math.random() * 5);
2560
+ nextLightningAt = frameCount + 90 + Math.floor(Math.random() * 250);
2561
+ }
2562
+ if (lightningTimer > 0) {
2563
+ lightningTimer--;
2564
+ lightningLight.intensity *= 0.45;
2565
+ if (lightningTimer <= 0) lightningLight.intensity = 0;
2566
+ }
2567
+ } else {
2568
+ lightningLight.intensity = 0;
2569
+ }
2570
+
2571
  // Cloud drift
2572
  for (const cloud of clouds) {
2573
  cloud.position.x += cloud.userData.speed;
2574
  if (cloud.position.x > 70) cloud.position.x = -70;
2575
  }
2576
+ for (const cloud of stormClouds) {
2577
+ if (!cloud.visible) continue;
2578
+ cloud.position.x += cloud.userData.speed;
2579
+ if (cloud.position.x > 70) cloud.position.x = -70;
2580
+ }
2581
 
2582
  renderer.render(scene, camera);
2583
  }
 
2624
  // ============================================================
2625
  // BOOT
2626
  // ============================================================
 
2627
  if (isEmbedded) {
2628
  document.getElementById('status-bar').style.display = 'none';
2629
  document.getElementById('controls-bar').style.display = 'none';
 
2734
  for (const d of demoNames) {
2735
  demoAgents[d.id] = { name: d.name, location: d.location, state: 'idle', needs: {} };
2736
  }
2737
+ let demoDay = 1;
2738
+ let demoMinute = 630; // 10:30
2739
+ const DEMO_WEATHER_CYCLE = ['sunny','sunny','sunny','cloudy','cloudy','rainy','stormy','rainy','cloudy','sunny','sunny','snowy','cloudy','sunny','foggy','sunny'];
2740
+ let demoWIdx = 0;
2741
+
2742
+ function demoTimeStr() {
2743
+ const hh = Math.floor(demoMinute / 60);
2744
+ const mm = demoMinute % 60;
2745
+ const phase = hh >= 5 && hh < 7 ? 'dawn' : hh >= 7 && hh < 12 ? 'morning' : hh >= 12 && hh < 17 ? 'afternoon' : hh >= 17 && hh < 20 ? 'evening' : 'night';
2746
+ return `Day ${demoDay}, ${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')} (${phase})`;
2747
+ }
2748
+
2749
+ const agentHome = {};
2750
+ for (const d of demoNames) {
2751
+ agentHome[d.id] = d.location;
2752
+ }
2753
+
2754
+ const residentialLocs = Object.entries(LOCATION_POSITIONS)
2755
+ .filter(([, v]) => v.type === 'house' || v.type === 'apartment')
2756
+ .map(([k]) => k);
2757
+ const publicLocs = Object.keys(LOCATION_POSITIONS).filter(k => !k.startsWith('street'));
2758
+
2759
  handleStateUpdate({
2760
+ type: 'tick', time: demoTimeStr(),
2761
+ state: { agents: demoAgents, locations: {}, weather: DEMO_WEATHER_CYCLE[0] }
2762
  });
 
 
2763
  document.getElementById('sim-agents').textContent = `${demoNames.length} agents (demo)`;
2764
 
 
 
2765
  setInterval(() => {
2766
  if (wsConnected) return;
2767
+
2768
+ demoMinute += 20;
2769
+ if (demoMinute >= 1440) { demoMinute -= 1440; demoDay++; }
2770
+ const hh = Math.floor(demoMinute / 60);
2771
+ if (demoMinute % 60 === 0 && hh % 3 === 0) {
2772
+ demoWIdx = (demoWIdx + 1) % DEMO_WEATHER_CYCLE.length;
2773
+ }
2774
+
2775
+ const w = DEMO_WEATHER_CYCLE[demoWIdx];
2776
+ const badW = w === 'rainy' || w === 'stormy' || w === 'snowy';
2777
+ const isNightTime = hh >= 22 || hh < 6;
2778
+ const isLateEvening = hh >= 20 && hh < 22;
2779
+
2780
  const agents = Object.keys(demoAgents);
2781
+
2782
+ if (isNightTime) {
2783
+ for (const who of agents) {
2784
+ const home = agentHome[who] || residentialLocs[hash(who) % residentialLocs.length];
2785
+ demoAgents[who].location = home;
2786
+ demoAgents[who].state = 'sleeping';
2787
+ }
2788
+ } else if (isLateEvening) {
2789
+ const goHomeCount = Math.floor(agents.length * 0.6);
2790
+ for (let i = 0; i < goHomeCount; i++) {
2791
+ const who = agents[Math.floor(Math.random() * agents.length)];
2792
+ const home = agentHome[who] || residentialLocs[hash(who) % residentialLocs.length];
2793
+ demoAgents[who].location = home;
2794
+ demoAgents[who].state = 'resting';
2795
+ }
2796
+ const moveCount = 2 + Math.floor(Math.random() * 3);
2797
+ for (let i = 0; i < moveCount; i++) {
2798
+ const who = agents[Math.floor(Math.random() * agents.length)];
2799
+ if (demoAgents[who].state !== 'resting') {
2800
+ demoAgents[who].location = publicLocs[Math.floor(Math.random() * publicLocs.length)];
2801
+ demoAgents[who].state = 'idle';
2802
+ }
2803
+ }
2804
+ } else if (badW) {
2805
+ const stayInCount = Math.floor(agents.length * 0.7);
2806
+ for (let i = 0; i < stayInCount; i++) {
2807
+ const who = agents[Math.floor(Math.random() * agents.length)];
2808
+ const home = agentHome[who] || residentialLocs[hash(who) % residentialLocs.length];
2809
+ demoAgents[who].location = home;
2810
+ demoAgents[who].state = 'sheltering';
2811
+ }
2812
+ const moveCount = 1 + Math.floor(Math.random() * 2);
2813
+ for (let i = 0; i < moveCount; i++) {
2814
+ const who = agents[Math.floor(Math.random() * agents.length)];
2815
+ demoAgents[who].location = publicLocs[Math.floor(Math.random() * publicLocs.length)];
2816
+ demoAgents[who].state = 'idle';
2817
+ }
2818
+ } else {
2819
+ const moveCount = 3 + Math.floor(Math.random() * 5);
2820
+ for (let i = 0; i < moveCount; i++) {
2821
+ const who = agents[Math.floor(Math.random() * agents.length)];
2822
+ demoAgents[who].location = publicLocs[Math.floor(Math.random() * publicLocs.length)];
2823
+ demoAgents[who].state = 'idle';
2824
+ }
2825
  }
2826
+
2827
  handleStateUpdate({
2828
+ type: 'tick', time: demoTimeStr(),
2829
+ state: { agents: demoAgents, locations: {}, weather: w }
2830
  });
2831
+ }, 2500);
2832
  }
2833
 
2834
  // Hide zoom hint after a few seconds
web/index.html CHANGED
@@ -41,7 +41,7 @@
41
  .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
42
  .dot.green { background: #4ecca3; } .dot.yellow { background: #f0c040; } .dot.red { background: #e94560; }
43
  #main { display: flex; flex-direction: row; height: calc(100vh - 50px); }
44
- #viewport-3d { flex: 7; position: relative; min-width: 0; }
45
  #viewport-3d iframe { width: 100%; height: 100%; border: none; display: block; }
46
  #canvas-container { display: none; }
47
  #cityCanvas { display: none; }
@@ -574,6 +574,7 @@ const AGENT_COLORS = [
574
  const WEATHER_ICONS = {
575
  sunny:'\u2600\uFE0F', clear:'\u2600\uFE0F', cloudy:'\u2601\uFE0F',
576
  rainy:'\uD83C\uDF27\uFE0F', stormy:'\u26C8\uFE0F', foggy:'\uD83C\uDF2B\uFE0F',
 
577
  };
578
 
579
  const SKY = {
@@ -2733,7 +2734,7 @@ function showDefaultDetail() {
2733
  const mayorBadge = a.is_mayor ? ' \uD83C\uDFDB\uFE0F' : '';
2734
  const ageStr = a.age != null ? ` (${a.age})` : '';
2735
  return `
2736
- <div class="agent-list-item" onclick="selectedAgentId='${aid}';fetchAgentDetail('${aid}');" style="${isDead ? 'opacity:0.5' : ''}">
2737
  <span class="agent-dot" style="background:${color}"></span>
2738
  <span style="font-size:13px">${gi}</span>
2739
  <div class="agent-info">
@@ -2744,12 +2745,20 @@ function showDefaultDetail() {
2744
  }).join('')}`;
2745
  }
2746
 
 
 
 
 
 
2747
  async function fetchAgentDetail(agentId) {
2748
  try {
2749
  const res=await fetch(`${API_BASE}/agents/${agentId}`);
2750
- if(!res.ok) return;
2751
  renderAgentDetail(await res.json());
2752
- } catch(e){}
 
 
 
2753
  }
2754
 
2755
  function renderAgentDetail(data) {
@@ -3904,7 +3913,28 @@ window.addEventListener('message', (e) => {
3904
  if (e.data?.type === 'agent-select' && e.data.agentId) {
3905
  selectedAgentId = e.data.agentId;
3906
  switchTab('agents');
3907
- fetchAgentDetail(e.data.agentId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3908
  }
3909
  });
3910
 
 
41
  .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
42
  .dot.green { background: #4ecca3; } .dot.yellow { background: #f0c040; } .dot.red { background: #e94560; }
43
  #main { display: flex; flex-direction: row; height: calc(100vh - 50px); }
44
+ #viewport-3d { flex: 6; position: relative; min-width: 0; }
45
  #viewport-3d iframe { width: 100%; height: 100%; border: none; display: block; }
46
  #canvas-container { display: none; }
47
  #cityCanvas { display: none; }
 
574
  const WEATHER_ICONS = {
575
  sunny:'\u2600\uFE0F', clear:'\u2600\uFE0F', cloudy:'\u2601\uFE0F',
576
  rainy:'\uD83C\uDF27\uFE0F', stormy:'\u26C8\uFE0F', foggy:'\uD83C\uDF2B\uFE0F',
577
+ snowy:'🌨️', snow:'🌨️',
578
  };
579
 
580
  const SKY = {
 
2734
  const mayorBadge = a.is_mayor ? ' \uD83C\uDFDB\uFE0F' : '';
2735
  const ageStr = a.age != null ? ` (${a.age})` : '';
2736
  return `
2737
+ <div class="agent-list-item" onclick="selectedAgentId='${aid}';selectAgentFromList('${aid}');" style="${isDead ? 'opacity:0.5' : ''}">
2738
  <span class="agent-dot" style="background:${color}"></span>
2739
  <span style="font-size:13px">${gi}</span>
2740
  <div class="agent-info">
 
2745
  }).join('')}`;
2746
  }
2747
 
2748
+ function selectAgentFromList(agentId) {
2749
+ fetchAgentDetail(agentId);
2750
+ send3d('select-agent');
2751
+ }
2752
+
2753
  async function fetchAgentDetail(agentId) {
2754
  try {
2755
  const res=await fetch(`${API_BASE}/agents/${agentId}`);
2756
+ if(!res.ok) throw 0;
2757
  renderAgentDetail(await res.json());
2758
+ } catch(e){
2759
+ const ag = agents[agentId];
2760
+ if (ag) renderAgentDetail({ ...ag, id: agentId, memories: [], relationships: [], recent_memories: [] });
2761
+ }
2762
  }
2763
 
2764
  function renderAgentDetail(data) {
 
3913
  if (e.data?.type === 'agent-select' && e.data.agentId) {
3914
  selectedAgentId = e.data.agentId;
3915
  switchTab('agents');
3916
+ // In demo mode (no server), show inline detail from iframe state
3917
+ const ag = agents[e.data.agentId];
3918
+ if (ag) {
3919
+ renderAgentDetail({ ...ag, id: e.data.agentId, memories: [], relationships: [] });
3920
+ } else {
3921
+ fetchAgentDetail(e.data.agentId);
3922
+ }
3923
+ }
3924
+ if (e.data?.type === 'state-update' && e.data.state) {
3925
+ const s = e.data.state;
3926
+ // Parse time string from iframe
3927
+ const timeMatch = (e.data.time || '').match(/Day\s+(\d+),\s*(\d+:\d+)\s*\((\w+)\)/i);
3928
+ const dayNum = timeMatch ? parseInt(timeMatch[1]) : 1;
3929
+ const timeStr = timeMatch ? timeMatch[2] : '10:30';
3930
+ const tod = timeMatch ? timeMatch[3] : 'morning';
3931
+ processStateData({
3932
+ clock: { day: dayNum, time_str: timeStr, time_of_day: tod, total_ticks: Date.now() },
3933
+ agents: s.agents || {},
3934
+ weather: s.weather || 'sunny',
3935
+ active_conversations: 0,
3936
+ llm_usage: '',
3937
+ });
3938
  }
3939
  });
3940