RayMelius Claude Opus 4.6 commited on
Commit
3dd76ce
·
1 Parent(s): d0c28ad

Street lamps, moon/stars fix, 100 agents with lifecycle, resizable panel

Browse files

3D Visual:
- Street lamps along roads with PointLights that glow at night
- Solid 3D moon with glow halo, 500 brighter stars
- Clouds hidden when sunny weather
- Night scene brighter (ambient 0.35, hemi 0.45)
- Furniture placed inside buildings (not in front)
- Agents positioned inside buildings, visible only on interior click
- Cemetery location with gravestones

Agent Lifecycle (demo mode):
- 100 agents with age, gender, occupation, life phase
- Marriage system (18+), pregnancy (9 ticks), births increase population
- Divorce (rare random), aging with career progression
- Illness at 70+, hospitalization, death, burial at cemetery
- Age-based daily routines: kindergarten/school/university/work/retired
- Long-term memory recording life events
- Life plan displayed in info panel

UI:
- Resizable right sidebar panel (drag handle)
- Life plan and long-term memory sections in agent info

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

Files changed (2) hide show
  1. web/3d.html +433 -162
  2. web/index.html +42 -4
web/3d.html CHANGED
@@ -233,7 +233,7 @@ const SKY_PHASES = {
233
  morning: { top:'#4a90c8', mid:'#88b8d8', bot:'#c8e0f0' },
234
  afternoon: { top:'#2878b8', mid:'#60a0d0', bot:'#88c8e8' },
235
  evening: { top:'#1a1040', mid:'#884060', bot:'#e07840' },
236
- night: { top:'#0a0e22', mid:'#101830', bot:'#182040' },
237
  };
238
 
239
  const LOCATION_POSITIONS = {
@@ -337,6 +337,11 @@ const LOCATION_POSITIONS = {
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' },
@@ -1298,6 +1303,45 @@ function createSquare(id, locData) {
1298
  return group;
1299
  }
1300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1301
  // Building factory dispatch
1302
  function createBuilding(id, locData) {
1303
  const type = locData.type;
@@ -1322,6 +1366,7 @@ function createBuilding(id, locData) {
1322
  case 'mall': bldg = createMall(id, locData); break;
1323
  case 'townhall': bldg = createTownHall(id, locData); break;
1324
  case 'market': bldg = createMarket(id, locData); break;
 
1325
  default: bldg = createShop(id, locData); break;
1326
  }
1327
  addFurniture(id, bldg, locData);
@@ -1333,7 +1378,7 @@ function createBuilding(id, locData) {
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
 
@@ -1345,34 +1390,34 @@ function addFurniture(id, group, locData) {
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
  }
@@ -1386,7 +1431,7 @@ const BLDG_SIZES = {
1386
  house: [4,4], apartment: [6,5], shop: [5,4], office: [6,6], tower: [5,5],
1387
  hospital: [8,6], church: [5,7], school: [7,6], factory: [8,6], cinema: [6,6],
1388
  park: [8,8], sports: [10,8], square: [8,8], police: [6,5], fire: [6,6],
1389
- museum: [8,7], mall: [9,7], townhall: [8,7], market: [9,7],
1390
  };
1391
  const obstacles = [];
1392
 
@@ -1416,6 +1461,59 @@ for (let i = 0; i < 80; i++) {
1416
  }
1417
  }
1418
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1419
  // ============================================================
1420
  // SKY ENVIRONMENT — mountains, sun, moon, clouds, stars
1421
  // ============================================================
@@ -1524,28 +1622,40 @@ sunSprite.renderOrder = -1;
1524
  scene.add(sunSprite);
1525
 
1526
  // Moon
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);
1534
 
1535
  // Stars
1536
- const starCount = 250;
1537
  const starPos = new Float32Array(starCount * 3);
 
1538
  for (let i = 0; i < starCount; i++) {
1539
  const th = Math.random() * Math.PI * 2;
1540
- const ph = Math.random() * Math.PI * 0.45;
1541
- const r = 95;
1542
  starPos[i*3] = Math.cos(th) * Math.sin(ph) * r;
1543
- starPos[i*3+1] = Math.cos(ph) * r + 10;
1544
  starPos[i*3+2] = Math.sin(th) * Math.sin(ph) * r;
 
1545
  }
1546
  const starGeo = new THREE.BufferGeometry();
1547
  starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
1548
- const starPoints = new THREE.Points(starGeo, new THREE.PointsMaterial({ color: 0xffffff, size: 0.5, transparent: true, opacity: 0.85 }));
 
 
 
 
1549
  starPoints.visible = false;
1550
  scene.add(starPoints);
1551
 
@@ -1655,8 +1765,8 @@ function updateWeather(weather) {
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; });
@@ -1704,13 +1814,19 @@ function updateCelestials(hour) {
1704
  const mh = ((hour - 19 + 24) % 24);
1705
  if (hour >= 18 || hour <= 6) {
1706
  const ma = (mh / 11) * Math.PI;
1707
- moonSprite.position.set(-Math.cos(ma) * 65, Math.sin(ma) * 45 + 5, -60);
1708
- moonSprite.visible = true;
 
 
 
 
 
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
 
@@ -1737,12 +1853,12 @@ function updateTimeOfDay(phase, hour) {
1737
 
1738
  // Lighting
1739
  if (isNight) {
1740
- hemiLight.intensity = 0.35; hemiLight.color.set(0x334488);
1741
- ambientLight.intensity = 0.25;
1742
- sunLight.intensity = 0.2; sunLight.color.set(0x5577aa);
1743
- ground.material.color.set(0x1a3a1a);
1744
- gridHelper.material.opacity = 0.06;
1745
- scene.fog = new THREE.FogExp2(0x0c0c20, 0.004);
1746
  } else if (phase === 'evening') {
1747
  hemiLight.intensity = 0.35; hemiLight.color.set(0xcc8855);
1748
  ambientLight.intensity = 0.2;
@@ -1766,6 +1882,13 @@ function updateTimeOfDay(phase, hour) {
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) {
@@ -1980,25 +2103,31 @@ function createAgentMesh(agentId, agentData) {
1980
  return group;
1981
  }
1982
 
 
 
 
1983
  function getAgentScenePosition(agentId, locationId, agents) {
1984
  const loc = LOCATION_POSITIONS[locationId] || dynamicLocations[locationId];
1985
  if (!loc) return { x: 0, y: 0, z: 0 };
1986
 
1987
  const pos = toWorld(loc.x, loc.y);
1988
- // Offset agents at same location in a circle
1989
  const agentsHere = Object.entries(agents).filter(([, a]) => a.location === locationId);
1990
  const myIdx = agentsHere.findIndex(([id]) => id === agentId);
1991
  const count = agentsHere.length;
 
1992
 
1993
  let ox = 0, oz = 0;
1994
  if (count > 1 && myIdx >= 0) {
1995
  const angle = (myIdx / count) * Math.PI * 2;
1996
- const radius = Math.min(2, 0.8 + count * 0.15);
1997
  ox = Math.cos(angle) * radius;
1998
  oz = Math.sin(angle) * radius;
1999
  }
2000
 
2001
- return { x: pos.x + ox, y: 0, z: pos.z + oz + 3 };
 
 
 
2002
  }
2003
 
2004
  const dynamicLocations = {};
@@ -2286,18 +2415,35 @@ function updateAgentInfo(agentId, data) {
2286
  html += `<h4>Status</h4>`;
2287
  html += `<div class="info-row"><span class="label">Location</span><span class="value">${data.location_name || data.location || '?'}</span></div>`;
2288
  html += `<div class="info-row"><span class="label">State</span><span class="value">${data.state || '?'}</span></div>`;
2289
- html += `<div class="info-row"><span class="label">Action</span><span class="value">${data.current_action || data.action || '?'}</span></div>`;
2290
  if (data.mood !== undefined) {
2291
  html += `<div class="info-row"><span class="label">Mood</span><span class="value">${typeof data.mood === 'number' ? (data.mood * 100).toFixed(0) + '%' : data.mood}</span></div>`;
2292
  }
2293
  if (data.age !== undefined) {
2294
  html += `<div class="info-row"><span class="label">Age</span><span class="value">${data.age}</span></div>`;
2295
  }
 
 
 
2296
  if (data.occupation) {
2297
- html += `<div class="info-row"><span class="label">Job</span><span class="value">${data.occupation}</span></div>`;
 
 
 
2298
  }
2299
  html += `</div>`;
2300
 
 
 
 
 
 
 
 
 
 
 
 
2301
  // Needs
2302
  const needs = data.needs || {};
2303
  if (Object.keys(needs).length > 0) {
@@ -2325,28 +2471,17 @@ function updateAgentInfo(agentId, data) {
2325
  html += `</div>`;
2326
  }
2327
 
2328
- // Memories
2329
  const memories = data.recent_memories || data.memories || [];
2330
  if (memories.length > 0) {
2331
- html += `<div class="info-section"><h4>Recent Memories</h4>`;
2332
- for (const mem of memories.slice(0, 5)) {
2333
  const text = typeof mem === 'string' ? mem : mem.description || mem.text || JSON.stringify(mem);
2334
  html += `<div class="memory-item">${text}</div>`;
2335
  }
2336
  html += `</div>`;
2337
  }
2338
 
2339
- // Daily plan
2340
- const plan = data.daily_plan || data.plan || [];
2341
- if (plan.length > 0) {
2342
- html += `<div class="info-section"><h4>Daily Plan</h4>`;
2343
- for (const item of plan) {
2344
- const text = typeof item === 'string' ? item : `${item.time || ''} ${item.activity || item.action || ''}`;
2345
- html += `<div style="font-size:12px;color:#bbb;padding:2px 0">${text}</div>`;
2346
- }
2347
- html += `</div>`;
2348
- }
2349
-
2350
  document.getElementById('info-content').innerHTML = html;
2351
  }
2352
 
@@ -2516,6 +2651,12 @@ function animate() {
2516
  }
2517
  }
2518
 
 
 
 
 
 
 
2519
  // Sleeping, sitting, walking, or idle animation
2520
  const agentState = mesh.userData.data?.state || '';
2521
  const agentLoc = mesh.userData.data?.location || '';
@@ -2699,91 +2840,77 @@ setTimeout(() => {
2699
  }, 4000);
2700
 
2701
  function spawnDemoAgents() {
2702
- const demoNames = [
2703
- { id: 'elena', name: 'Elena', location: 'house_elena' },
2704
- { id: 'marcus', name: 'Marcus', location: 'cafe' },
2705
- { id: 'helen', name: 'Helen', location: 'park' },
2706
- { id: 'diana', name: 'Diana', location: 'office' },
2707
- { id: 'kai', name: 'Kai', location: 'house_kai' },
2708
- { id: 'priya', name: 'Priya', location: 'grocery' },
2709
- { id: 'james', name: 'James', location: 'gym' },
2710
- { id: 'rosa', name: 'Rosa', location: 'restaurant' },
2711
- { id: 'yuki', name: 'Yuki', location: 'library' },
2712
- { id: 'frank', name: 'Frank', location: 'factory' },
2713
- { id: 'lila', name: 'Lila', location: 'bakery' },
2714
- { id: 'zoe', name: 'Zoe', location: 'town_square' },
2715
- { id: 'omar', name: 'Omar', location: 'bar' },
2716
- { id: 'nina', name: 'Nina', location: 'school' },
2717
- { id: 'theo', name: 'Theo', location: 'hospital' },
2718
- { id: 'ada', name: 'Ada', location: 'house_ada' },
2719
- { id: 'ben', name: 'Ben', location: 'bookshop' },
2720
- { id: 'carlos', name: 'Carlos', location: 'house_carlos' },
2721
- { id: 'mia', name: 'Mia', location: 'florist' },
2722
- { id: 'dara', name: 'Dara', location: 'house_dara' },
2723
- { id: 'leo', name: 'Leo', location: 'pizzeria' },
2724
- { id: 'sven', name: 'Sven', location: 'house_sven' },
2725
- { id: 'hana', name: 'Hana', location: 'sushi_bar' },
2726
- { id: 'ivan', name: 'Ivan', location: 'house_ivan' },
2727
- { id: 'vera', name: 'Vera', location: 'museum' },
2728
- { id: 'nadia', name: 'Nadia', location: 'house_nadia' },
2729
- { id: 'rami', name: 'Rami', location: 'market' },
2730
- { id: 'petra', name: 'Petra', location: 'house_petra' },
2731
- { id: 'tom', name: 'Tom', location: 'police' },
2732
- { id: 'ling', name: 'Ling', location: 'house_ling' },
2733
- { id: 'jun', name: 'Jun', location: 'mall' },
2734
- { id: 'alice', name: 'Alice', location: 'coffeehouse' },
2735
- { id: 'marco', name: 'Marco', location: 'townhall' },
2736
- { id: 'devon', name: 'Devon', location: 'cinema' },
2737
- { id: 'george', name: 'George', location: 'fire_station' },
2738
- { id: 'sam', name: 'Sam', location: 'barbershop' },
2739
- { id: 'maya', name: 'Maya', location: 'pharmacy' },
2740
- { id: 'felix', name: 'Felix', location: 'electronics' },
2741
- { id: 'sophie', name: 'Sophie', location: 'pet_shop' },
2742
- { id: 'alex', name: 'Alex', location: 'laundry' },
2743
- { id: 'anya', name: 'Anya', location: 'park_east' },
2744
- { id: 'boris', name: 'Boris', location: 'apt_midtown' },
2745
- { id: 'clara', name: 'Clara', location: 'apt_heights' },
2746
- { id: 'dimitri', name: 'Dimitri', location: 'apt_plaza' },
2747
- { id: 'elsa', name: 'Elsa', location: 'apt_harbor' },
2748
- { id: 'farid', name: 'Farid', location: 'office_tech' },
2749
- { id: 'greta', name: 'Greta', location: 'office_media' },
2750
- { id: 'hamid', name: 'Hamid', location: 'tower_2' },
2751
- { id: 'irene', name: 'Irene', location: 'factory_2' },
2752
- { id: 'jake', name: 'Jake', location: 'park_south' },
2753
- { id: 'kira', name: 'Kira', location: 'playground' },
2754
- { id: 'lukas', name: 'Lukas', location: 'diner' },
2755
- { id: 'marta', name: 'Marta', location: 'sports_field' },
2756
- { id: 'nico', name: 'Nico', location: 'church' },
2757
- { id: 'olga', name: 'Olga', location: 'apartment_block_1' },
2758
- { id: 'paolo', name: 'Paolo', location: 'apartment_block_2' },
2759
- { id: 'quinn', name: 'Quinn', location: 'apartment_block_3' },
2760
- { id: 'rita', name: 'Rita', location: 'apt_northeast' },
2761
- { id: 'stefan', name: 'Stefan', location: 'apt_northwest' },
2762
- { id: 'tanya', name: 'Tanya', location: 'apt_southeast' },
2763
- { id: 'ulrich', name: 'Ulrich', location: 'apt_southwest' },
2764
- { id: 'viola', name: 'Viola', location: 'cafe' },
2765
- { id: 'walter', name: 'Walter', location: 'grocery' },
2766
- { id: 'xena', name: 'Xena', location: 'bakery' },
2767
- { id: 'yusuf', name: 'Yusuf', location: 'restaurant' },
2768
- { id: 'zara', name: 'Zara', location: 'bar' },
2769
- { id: 'arnaud', name: 'Arnaud', location: 'office' },
2770
- { id: 'bianca', name: 'Bianca', location: 'park' },
2771
- { id: 'cyril', name: 'Cyril', location: 'gym' },
2772
- { id: 'dina', name: 'Dina', location: 'library' },
2773
- { id: 'emilio', name: 'Emilio', location: 'factory' },
2774
- { id: 'fiona', name: 'Fiona', location: 'hospital' },
2775
- { id: 'gustav', name: 'Gustav', location: 'school' },
2776
- { id: 'hazel', name: 'Hazel', location: 'town_square' },
2777
- { id: 'igor', name: 'Igor', location: 'cinema' },
2778
- ];
2779
  const demoAgents = {};
2780
- for (const d of demoNames) {
2781
- demoAgents[d.id] = { name: d.name, location: d.location, state: 'idle', needs: {} };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2782
  }
 
2783
  let demoDay = 1;
2784
- let demoMinute = 630; // 10:30
2785
  const DEMO_WEATHER_CYCLE = ['sunny','sunny','sunny','cloudy','cloudy','rainy','stormy','rainy','cloudy','sunny','sunny','snowy','cloudy','sunny','foggy','sunny'];
2786
  let demoWIdx = 0;
 
 
2787
 
2788
  function demoTimeStr() {
2789
  const hh = Math.floor(demoMinute / 60);
@@ -2792,21 +2919,144 @@ function spawnDemoAgents() {
2792
  return `Day ${demoDay}, ${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')} (${phase})`;
2793
  }
2794
 
2795
- const agentHome = {};
2796
- for (const d of demoNames) {
2797
- agentHome[d.id] = d.location;
2798
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2799
 
2800
- const residentialLocs = Object.entries(LOCATION_POSITIONS)
2801
- .filter(([, v]) => v.type === 'house' || v.type === 'apartment')
2802
- .map(([k]) => k);
2803
- const publicLocs = Object.keys(LOCATION_POSITIONS).filter(k => !k.startsWith('street'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2804
 
2805
  handleStateUpdate({
2806
  type: 'tick', time: demoTimeStr(),
2807
  state: { agents: demoAgents, locations: {}, weather: DEMO_WEATHER_CYCLE[0] }
2808
  });
2809
- document.getElementById('sim-agents').textContent = `${demoNames.length} agents (demo)`;
2810
 
2811
  setInterval(() => {
2812
  if (wsConnected) return;
@@ -2823,10 +3073,21 @@ function spawnDemoAgents() {
2823
  const isNightTime = hh >= 22 || hh < 6;
2824
  const isLateEvening = hh >= 20 && hh < 22;
2825
 
2826
- const agents = Object.keys(demoAgents);
 
 
 
 
 
 
 
 
 
 
2827
 
2828
  if (isNightTime) {
2829
  for (const who of agents) {
 
2830
  const home = agentHome[who] || residentialLocs[hash(who) % residentialLocs.length];
2831
  demoAgents[who].location = home;
2832
  demoAgents[who].state = 'sleeping';
@@ -2835,38 +3096,47 @@ function spawnDemoAgents() {
2835
  const goHomeCount = Math.floor(agents.length * 0.6);
2836
  for (let i = 0; i < goHomeCount; i++) {
2837
  const who = agents[Math.floor(Math.random() * agents.length)];
 
2838
  const home = agentHome[who] || residentialLocs[hash(who) % residentialLocs.length];
2839
  demoAgents[who].location = home;
2840
  demoAgents[who].state = 'resting';
2841
  }
2842
- const moveCount = 2 + Math.floor(Math.random() * 3);
2843
- for (let i = 0; i < moveCount; i++) {
2844
- const who = agents[Math.floor(Math.random() * agents.length)];
2845
- if (demoAgents[who].state !== 'resting') {
2846
- demoAgents[who].location = publicLocs[Math.floor(Math.random() * publicLocs.length)];
2847
- demoAgents[who].state = 'idle';
2848
- }
2849
- }
2850
  } else if (badW) {
2851
  const stayInCount = Math.floor(agents.length * 0.7);
2852
  for (let i = 0; i < stayInCount; i++) {
2853
  const who = agents[Math.floor(Math.random() * agents.length)];
 
2854
  const home = agentHome[who] || residentialLocs[hash(who) % residentialLocs.length];
2855
  demoAgents[who].location = home;
2856
  demoAgents[who].state = 'sheltering';
2857
  }
2858
- const moveCount = 1 + Math.floor(Math.random() * 2);
2859
- for (let i = 0; i < moveCount; i++) {
2860
- const who = agents[Math.floor(Math.random() * agents.length)];
2861
- demoAgents[who].location = publicLocs[Math.floor(Math.random() * publicLocs.length)];
2862
- demoAgents[who].state = 'idle';
2863
- }
2864
  } else {
2865
- const moveCount = 3 + Math.floor(Math.random() * 5);
2866
- for (let i = 0; i < moveCount; i++) {
2867
- const who = agents[Math.floor(Math.random() * agents.length)];
2868
- demoAgents[who].location = publicLocs[Math.floor(Math.random() * publicLocs.length)];
2869
- demoAgents[who].state = 'idle';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2870
  }
2871
  }
2872
 
@@ -2874,6 +3144,7 @@ function spawnDemoAgents() {
2874
  type: 'tick', time: demoTimeStr(),
2875
  state: { agents: demoAgents, locations: {}, weather: w }
2876
  });
 
2877
  }, 2500);
2878
  }
2879
 
 
233
  morning: { top:'#4a90c8', mid:'#88b8d8', bot:'#c8e0f0' },
234
  afternoon: { top:'#2878b8', mid:'#60a0d0', bot:'#88c8e8' },
235
  evening: { top:'#1a1040', mid:'#884060', bot:'#e07840' },
236
+ night: { top:'#0e1530', mid:'#162040', bot:'#1e2850' },
237
  };
238
 
239
  const LOCATION_POSITIONS = {
 
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
+ // ── Cemetery ────────────────────────────────────────────────
341
+ cemetery: { x: 0.93, y: 0.92, type: 'cemetery', label: 'Eternal Rest' },
342
+ kindergarten: { x: 0.30, y: 0.12, type: 'school', label: 'Rainbow Kids' },
343
+ university: { x: 0.70, y: 0.88, type: 'office', label: 'Soci University' },
344
+
345
  // ── Streets ───────────────────────────────────────────────
346
  street_main: { x: 0.50, y: 0.28, type: 'square', label: 'Main Street' },
347
  street_west: { x: 0.15, y: 0.50, type: 'square', label: 'West Avenue' },
 
1303
  return group;
1304
  }
1305
 
1306
+ function createCemetery(id, locData) {
1307
+ const group = new THREE.Group();
1308
+ const plot = new THREE.Mesh(
1309
+ new THREE.BoxGeometry(8, 0.08, 7),
1310
+ mat(0x3a5a3a, { roughness: 0.95 })
1311
+ );
1312
+ plot.position.y = 0.04;
1313
+ plot.receiveShadow = true;
1314
+ group.add(plot);
1315
+
1316
+ const fence = mat(0x333333);
1317
+ for (let side of [-1, 1]) {
1318
+ const rail = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.8, 7), fence);
1319
+ rail.position.set(side * 4, 0.4, 0);
1320
+ group.add(rail);
1321
+ }
1322
+ for (let side of [-1, 1]) {
1323
+ const rail = new THREE.Mesh(new THREE.BoxGeometry(8, 0.8, 0.06), fence);
1324
+ rail.position.set(0, 0.4, side * 3.5);
1325
+ group.add(rail);
1326
+ }
1327
+
1328
+ const stoneMat = mat(0xaaaaaa);
1329
+ const cemeteryGraves = [];
1330
+ for (let row = 0; row < 2; row++) {
1331
+ for (let col = -2; col <= 2; col++) {
1332
+ const stone = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.7, 0.12), stoneMat);
1333
+ stone.position.set(col * 1.3, 0.43, -1.5 + row * 3);
1334
+ group.add(stone);
1335
+ cemeteryGraves.push(stone);
1336
+ }
1337
+ }
1338
+
1339
+ const label = createLabel(locData.label, group, 3);
1340
+ const badge = createOccupantBadge(group, 2);
1341
+ group.userData = { id, type: 'cemetery', label, badge, locData, graves: cemeteryGraves };
1342
+ return group;
1343
+ }
1344
+
1345
  // Building factory dispatch
1346
  function createBuilding(id, locData) {
1347
  const type = locData.type;
 
1366
  case 'mall': bldg = createMall(id, locData); break;
1367
  case 'townhall': bldg = createTownHall(id, locData); break;
1368
  case 'market': bldg = createMarket(id, locData); break;
1369
+ case 'cemetery': bldg = createCemetery(id, locData); break;
1370
  default: bldg = createShop(id, locData); break;
1371
  }
1372
  addFurniture(id, bldg, locData);
 
1378
  }
1379
 
1380
  const FOOD_LOCS = new Set(['restaurant','bar','cafe','diner','bakery','sushi_bar','pizzeria','coffeehouse','pub','ice_cream']);
1381
+ const DESK_LOCS = new Set(['office','office_tech','office_media','school','library','townhall','post_office','bank','court','daycare','kindergarten','university']);
1382
  const SEAT_LOCS = new Set(['cinema','church','museum']);
1383
  const SITTING_LOCS = new Set([...FOOD_LOCS, ...DESK_LOCS, ...SEAT_LOCS]);
1384
 
 
1390
  if (FOOD_LOCS.has(id)) {
1391
  for (let i = -1; i <= 1; i++) {
1392
  const table = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.4, 0.7), tableMat);
1393
+ table.position.set(i * 1.2, 0.2, 0);
1394
  group.add(table);
1395
  for (let s of [-0.5, 0.5]) {
1396
  const chair = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.25, 0.3), chairMat);
1397
+ chair.position.set(i * 1.2 + s, 0.125, 0.5);
1398
  group.add(chair);
1399
  const back = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.05), chairMat);
1400
+ back.position.set(i * 1.2 + s, 0.35, 0.65);
1401
  group.add(back);
1402
  }
1403
  }
1404
  } else if (DESK_LOCS.has(id)) {
1405
  for (let i = -1; i <= 1; i += 2) {
1406
  const desk = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.45, 0.6), deskMat);
1407
+ desk.position.set(i * 1.3, 0.225, 0);
1408
  group.add(desk);
1409
  const ch = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.25, 0.3), chairMat);
1410
+ ch.position.set(i * 1.3, 0.125, 0.5);
1411
  group.add(ch);
1412
  }
1413
  } else if (SEAT_LOCS.has(id)) {
1414
  for (let row = 0; row < 2; row++) {
1415
  for (let col = -1; col <= 1; col++) {
1416
  const seat = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.25, 0.35), chairMat);
1417
+ seat.position.set(col * 0.7, 0.125, -0.5 + row * 0.8);
1418
  group.add(seat);
1419
  const sb = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.35, 0.05), chairMat);
1420
+ sb.position.set(col * 0.7, 0.35, -0.5 + row * 0.8 + 0.18);
1421
  group.add(sb);
1422
  }
1423
  }
 
1431
  house: [4,4], apartment: [6,5], shop: [5,4], office: [6,6], tower: [5,5],
1432
  hospital: [8,6], church: [5,7], school: [7,6], factory: [8,6], cinema: [6,6],
1433
  park: [8,8], sports: [10,8], square: [8,8], police: [6,5], fire: [6,6],
1434
+ museum: [8,7], mall: [9,7], townhall: [8,7], market: [9,7], cemetery: [9,8],
1435
  };
1436
  const obstacles = [];
1437
 
 
1461
  }
1462
  }
1463
 
1464
+ // ============================================================
1465
+ // STREET LAMPS
1466
+ // ============================================================
1467
+ const streetLamps = [];
1468
+ function createStreetLamp(x, z) {
1469
+ const group = new THREE.Group();
1470
+ const pole = new THREE.Mesh(
1471
+ new THREE.CylinderGeometry(0.06, 0.08, 3.5, 6),
1472
+ mat(0x444444, { metalness: 0.4 })
1473
+ );
1474
+ pole.position.y = 1.75;
1475
+ pole.castShadow = true;
1476
+ group.add(pole);
1477
+
1478
+ const arm = new THREE.Mesh(
1479
+ new THREE.BoxGeometry(0.8, 0.05, 0.05),
1480
+ mat(0x444444, { metalness: 0.4 })
1481
+ );
1482
+ arm.position.set(0.35, 3.4, 0);
1483
+ group.add(arm);
1484
+
1485
+ const bulb = new THREE.Mesh(
1486
+ new THREE.SphereGeometry(0.15, 8, 6),
1487
+ new THREE.MeshStandardMaterial({ color: 0xffeebb, emissive: 0xffd860, emissiveIntensity: 0, roughness: 0.3 })
1488
+ );
1489
+ bulb.position.set(0.7, 3.3, 0);
1490
+ group.add(bulb);
1491
+
1492
+ const light = new THREE.PointLight(0xffd860, 0, 12, 1.5);
1493
+ light.position.set(0.7, 3.2, 0);
1494
+ group.add(light);
1495
+
1496
+ group.position.set(x, 0, z);
1497
+ scene.add(group);
1498
+ streetLamps.push({ group, light, bulb });
1499
+ }
1500
+
1501
+ // Place lamps along main roads
1502
+ for (let i = -6; i <= 6; i++) {
1503
+ createStreetLamp(i * 10, -5);
1504
+ createStreetLamp(i * 10, -9);
1505
+ createStreetLamp(2, i * 10);
1506
+ createStreetLamp(-2, i * 10);
1507
+ }
1508
+ // Along secondary roads
1509
+ for (let i = -5; i <= 5; i++) {
1510
+ createStreetLamp(i * 12, -16);
1511
+ createStreetLamp(i * 12, 15);
1512
+ createStreetLamp(i * 12, 28);
1513
+ createStreetLamp(-26, i * 12);
1514
+ createStreetLamp(28, i * 12);
1515
+ }
1516
+
1517
  // ============================================================
1518
  // SKY ENVIRONMENT — mountains, sun, moon, clouds, stars
1519
  // ============================================================
 
1622
  scene.add(sunSprite);
1623
 
1624
  // Moon
1625
+ const moonGeo = new THREE.SphereGeometry(4, 32, 32);
1626
+ const moonMat = new THREE.MeshBasicMaterial({ color: 0xeeeeff, transparent: true, opacity: 0.95 });
1627
+ const moonMesh = new THREE.Mesh(moonGeo, moonMat);
1628
+ moonMesh.renderOrder = -1;
1629
+ moonMesh.visible = false;
1630
+ scene.add(moonMesh);
1631
+ const moonGlow = new THREE.Sprite(
1632
+ new THREE.SpriteMaterial({ map: celestialTex(0xaabbee), transparent: true, depthTest: false })
1633
  );
1634
+ moonGlow.scale.set(22, 22, 1);
1635
+ moonGlow.renderOrder = -2;
1636
+ moonGlow.visible = false;
1637
+ scene.add(moonGlow);
1638
 
1639
  // Stars
1640
+ const starCount = 500;
1641
  const starPos = new Float32Array(starCount * 3);
1642
+ const starSizes = new Float32Array(starCount);
1643
  for (let i = 0; i < starCount; i++) {
1644
  const th = Math.random() * Math.PI * 2;
1645
+ const ph = Math.random() * Math.PI * 0.5;
1646
+ const r = 90;
1647
  starPos[i*3] = Math.cos(th) * Math.sin(ph) * r;
1648
+ starPos[i*3+1] = Math.cos(ph) * r + 15;
1649
  starPos[i*3+2] = Math.sin(th) * Math.sin(ph) * r;
1650
+ starSizes[i] = 0.6 + Math.random() * 1.2;
1651
  }
1652
  const starGeo = new THREE.BufferGeometry();
1653
  starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
1654
+ starGeo.setAttribute('size', new THREE.BufferAttribute(starSizes, 1));
1655
+ const starPoints = new THREE.Points(starGeo, new THREE.PointsMaterial({
1656
+ color: 0xffffff, size: 1.2, transparent: true, opacity: 0.9,
1657
+ sizeAttenuation: false
1658
+ }));
1659
  starPoints.visible = false;
1660
  scene.add(starPoints);
1661
 
 
1765
  hemiLight.intensity *= (0.6 + wm * 0.4);
1766
  }
1767
 
1768
+ const fairVis = !isSunny && !(precipitating);
1769
+ const fairOp = isCloudy ? 0.95 : 0.5;
1770
  clouds.forEach(c => {
1771
  c.visible = fairVis || isCloudy;
1772
  if (!isNight) c.children.forEach(p => { p.material.opacity = fairOp; });
 
1814
  const mh = ((hour - 19 + 24) % 24);
1815
  if (hour >= 18 || hour <= 6) {
1816
  const ma = (mh / 11) * Math.PI;
1817
+ const mx = -Math.cos(ma) * 65;
1818
+ const my = Math.sin(ma) * 50 + 8;
1819
+ const mz = -55;
1820
+ moonMesh.position.set(mx, my, mz);
1821
+ moonMesh.visible = true;
1822
+ moonGlow.position.set(mx, my, mz - 1);
1823
+ moonGlow.visible = true;
1824
  } else {
1825
+ moonMesh.visible = false;
1826
+ moonGlow.visible = false;
1827
  }
1828
+ starPoints.visible = hour >= 18 || hour <= 5;
1829
+ starPoints.material.opacity = (hour >= 20 || hour <= 4) ? 0.95 : 0.6;
1830
  }
1831
  updateCelestials(10);
1832
 
 
1853
 
1854
  // Lighting
1855
  if (isNight) {
1856
+ hemiLight.intensity = 0.45; hemiLight.color.set(0x4466aa);
1857
+ ambientLight.intensity = 0.35;
1858
+ sunLight.intensity = 0.25; sunLight.color.set(0x6688bb);
1859
+ ground.material.color.set(0x1e401e);
1860
+ gridHelper.material.opacity = 0.08;
1861
+ scene.fog = new THREE.FogExp2(0x101830, 0.003);
1862
  } else if (phase === 'evening') {
1863
  hemiLight.intensity = 0.35; hemiLight.color.set(0xcc8855);
1864
  ambientLight.intensity = 0.2;
 
1882
  scene.fog = new THREE.FogExp2(PALETTE.fog, 0.004);
1883
  }
1884
 
1885
+ // Street lamps: on at night/evening, off during day
1886
+ const lampsOn = isNight || phase === 'evening';
1887
+ for (const lamp of streetLamps) {
1888
+ lamp.light.intensity = lampsOn ? 1.8 : 0;
1889
+ lamp.bulb.material.emissiveIntensity = lampsOn ? 1.5 : 0;
1890
+ }
1891
+
1892
  // Window glow + building transparency at night
1893
  scene.traverse(o => {
1894
  if (o.userData?.isWindow && o.material) {
 
2103
  return group;
2104
  }
2105
 
2106
+ const OUTDOOR_LOCS = new Set(['park','park_east','park_south','playground','town_square','sports_field',
2107
+ 'street_main','street_west','market','cemetery']);
2108
+
2109
  function getAgentScenePosition(agentId, locationId, agents) {
2110
  const loc = LOCATION_POSITIONS[locationId] || dynamicLocations[locationId];
2111
  if (!loc) return { x: 0, y: 0, z: 0 };
2112
 
2113
  const pos = toWorld(loc.x, loc.y);
 
2114
  const agentsHere = Object.entries(agents).filter(([, a]) => a.location === locationId);
2115
  const myIdx = agentsHere.findIndex(([id]) => id === agentId);
2116
  const count = agentsHere.length;
2117
+ const isOutdoor = OUTDOOR_LOCS.has(locationId) || loc.type === 'park' || loc.type === 'square' || loc.type === 'sports';
2118
 
2119
  let ox = 0, oz = 0;
2120
  if (count > 1 && myIdx >= 0) {
2121
  const angle = (myIdx / count) * Math.PI * 2;
2122
+ const radius = isOutdoor ? Math.min(3, 1.0 + count * 0.2) : Math.min(1.5, 0.4 + count * 0.12);
2123
  ox = Math.cos(angle) * radius;
2124
  oz = Math.sin(angle) * radius;
2125
  }
2126
 
2127
+ if (isOutdoor) {
2128
+ return { x: pos.x + ox, y: 0, z: pos.z + oz };
2129
+ }
2130
+ return { x: pos.x + ox, y: 0, z: pos.z + oz };
2131
  }
2132
 
2133
  const dynamicLocations = {};
 
2415
  html += `<h4>Status</h4>`;
2416
  html += `<div class="info-row"><span class="label">Location</span><span class="value">${data.location_name || data.location || '?'}</span></div>`;
2417
  html += `<div class="info-row"><span class="label">State</span><span class="value">${data.state || '?'}</span></div>`;
2418
+ html += `<div class="info-row"><span class="label">Action</span><span class="value">${data.current_action || data.action || data.state || '?'}</span></div>`;
2419
  if (data.mood !== undefined) {
2420
  html += `<div class="info-row"><span class="label">Mood</span><span class="value">${typeof data.mood === 'number' ? (data.mood * 100).toFixed(0) + '%' : data.mood}</span></div>`;
2421
  }
2422
  if (data.age !== undefined) {
2423
  html += `<div class="info-row"><span class="label">Age</span><span class="value">${data.age}</span></div>`;
2424
  }
2425
+ if (data.gender) {
2426
+ html += `<div class="info-row"><span class="label">Gender</span><span class="value">${data.gender}</span></div>`;
2427
+ }
2428
  if (data.occupation) {
2429
+ html += `<div class="info-row"><span class="label">Occupation</span><span class="value">${data.occupation}</span></div>`;
2430
+ }
2431
+ if (data.lifePhase) {
2432
+ html += `<div class="info-row"><span class="label">Life Phase</span><span class="value">${data.lifePhase}</span></div>`;
2433
  }
2434
  html += `</div>`;
2435
 
2436
+ // Life Plan
2437
+ const lifePlan = data.plan || [];
2438
+ if (lifePlan.length > 0) {
2439
+ html += `<div class="info-section"><h4>Life Plan</h4>`;
2440
+ for (const item of lifePlan) {
2441
+ const text = typeof item === 'string' ? item : `${item.time || ''} ${item.activity || item.action || ''}`;
2442
+ html += `<div style="font-size:12px;color:#4ecca3;padding:2px 0">&#9679; ${text}</div>`;
2443
+ }
2444
+ html += `</div>`;
2445
+ }
2446
+
2447
  // Needs
2448
  const needs = data.needs || {};
2449
  if (Object.keys(needs).length > 0) {
 
2471
  html += `</div>`;
2472
  }
2473
 
2474
+ // Long-term Memory
2475
  const memories = data.recent_memories || data.memories || [];
2476
  if (memories.length > 0) {
2477
+ html += `<div class="info-section"><h4>Long-term Memory</h4>`;
2478
+ for (const mem of memories.slice(-10).reverse()) {
2479
  const text = typeof mem === 'string' ? mem : mem.description || mem.text || JSON.stringify(mem);
2480
  html += `<div class="memory-item">${text}</div>`;
2481
  }
2482
  html += `</div>`;
2483
  }
2484
 
 
 
 
 
 
 
 
 
 
 
 
2485
  document.getElementById('info-content').innerHTML = html;
2486
  }
2487
 
 
2651
  }
2652
  }
2653
 
2654
+ // Agent visibility: hide inside buildings unless interior view is active
2655
+ const agentLoc2 = mesh.userData.data?.location || '';
2656
+ const locInfo2 = LOCATION_POSITIONS[agentLoc2] || dynamicLocations[agentLoc2];
2657
+ const isOutdoorLoc = OUTDOOR_LOCS.has(agentLoc2) || locInfo2?.type === 'park' || locInfo2?.type === 'square' || locInfo2?.type === 'sports';
2658
+ mesh.visible = isOutdoorLoc || agentLoc2 === interiorBuildingId;
2659
+
2660
  // Sleeping, sitting, walking, or idle animation
2661
  const agentState = mesh.userData.data?.state || '';
2662
  const agentLoc = mesh.userData.data?.location || '';
 
2840
  }, 4000);
2841
 
2842
  function spawnDemoAgents() {
2843
+ const NAMES_F = ['Elena','Helen','Diana','Priya','Rosa','Yuki','Lila','Zoe','Nina','Ada','Mia','Dara','Hana','Vera','Nadia','Petra','Ling','Alice','Maya','Sophie','Anya','Clara','Elsa','Greta','Irene','Kira','Marta','Olga','Rita','Tanya','Viola','Xena','Zara','Bianca','Dina','Fiona','Hazel','Jenna','Layla','Sasha','Eva','Chloe','Amber','Ruby','Ivy','Luna','Nora','Aria'];
2844
+ const NAMES_M = ['Marcus','Kai','James','Frank','Omar','Theo','Ben','Carlos','Leo','Sven','Ivan','Rami','Tom','Jun','Marco','Devon','George','Sam','Felix','Alex','Boris','Dimitri','Farid','Hamid','Jake','Lukas','Nico','Paolo','Quinn','Stefan','Ulrich','Walter','Yusuf','Arnaud','Cyril','Emilio','Gustav','Igor','Erik','Hans','Oleg','Rolf','Dante','Hugo','Max','Owen','Noah','Liam'];
2845
+ const OCCUPATIONS = ['Teacher','Engineer','Doctor','Artist','Chef','Writer','Nurse','Lawyer','Merchant','Farmer','Builder','Driver','Scientist','Musician','Guard','Clerk','Designer','Mechanic','Baker','Tailor'];
2846
+
2847
+ const residentialLocs = Object.entries(LOCATION_POSITIONS)
2848
+ .filter(([, v]) => v.type === 'house' || v.type === 'apartment').map(([k]) => k);
2849
+ const publicLocs = Object.keys(LOCATION_POSITIONS).filter(k => !k.startsWith('street') && k !== 'cemetery');
2850
+ const workLocs = Object.keys(LOCATION_POSITIONS).filter(k => {
2851
+ const t = LOCATION_POSITIONS[k]?.type;
2852
+ return t === 'office' || t === 'shop' || t === 'factory' || t === 'tower' || t === 'hospital' || t === 'school';
2853
+ });
2854
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2855
  const demoAgents = {};
2856
+ const agentHome = {};
2857
+ const agentLife = {};
2858
+ const agentMemories = {};
2859
+ let nextId = 1;
2860
+
2861
+ function makeAgent(name, gender, age, homeLoc) {
2862
+ const id = name.toLowerCase();
2863
+ const occ = age >= 18 ? OCCUPATIONS[hash(id) % OCCUPATIONS.length] : (age >= 6 ? 'Student' : 'Child');
2864
+ const lifePhase = age < 3 ? 'baby' : age < 6 ? 'kindergarten' : age < 18 ? 'school' : age < 23 ? 'university' : age < 65 ? 'working' : 'retired';
2865
+ agentHome[id] = homeLoc;
2866
+ agentLife[id] = { age, gender, partner: null, children: [], lifePhase, occupation: occ, pregnant: false, pregnancyTimer: 0, alive: true };
2867
+ agentMemories[id] = [`Born in Soci City`];
2868
+ demoAgents[id] = {
2869
+ name, location: homeLoc, state: 'idle', gender,
2870
+ age, occupation: occ, lifePhase,
2871
+ needs: { hunger: 0.6 + Math.random() * 0.3, energy: 0.5 + Math.random() * 0.4, social: 0.4 + Math.random() * 0.5, fun: 0.4 + Math.random() * 0.4 },
2872
+ recent_memories: agentMemories[id],
2873
+ relationships: [],
2874
+ };
2875
+ return id;
2876
+ }
2877
+
2878
+ // Create 100 agents with diverse ages
2879
+ const allIds = [];
2880
+ for (let i = 0; i < 50; i++) {
2881
+ const fn = NAMES_F[i % NAMES_F.length] + (i >= NAMES_F.length ? String(Math.floor(i/NAMES_F.length)+1) : '');
2882
+ const age = 3 + Math.floor(Math.random() * 80);
2883
+ const home = residentialLocs[i % residentialLocs.length];
2884
+ allIds.push(makeAgent(fn, 'female', age, home));
2885
+ }
2886
+ for (let i = 0; i < 50; i++) {
2887
+ const mn = NAMES_M[i % NAMES_M.length] + (i >= NAMES_M.length ? String(Math.floor(i/NAMES_M.length)+1) : '');
2888
+ const age = 3 + Math.floor(Math.random() * 80);
2889
+ const home = residentialLocs[(i+14) % residentialLocs.length];
2890
+ allIds.push(makeAgent(mn, 'male', age, home));
2891
+ }
2892
+
2893
+ // Pre-marry some adults
2894
+ const singleF = allIds.filter(id => agentLife[id].gender === 'female' && agentLife[id].age >= 20 && !agentLife[id].partner);
2895
+ const singleM = allIds.filter(id => agentLife[id].gender === 'male' && agentLife[id].age >= 20 && !agentLife[id].partner);
2896
+ const marriageCount = Math.min(singleF.length, singleM.length, 15);
2897
+ for (let i = 0; i < marriageCount; i++) {
2898
+ const f = singleF[i], m = singleM[i];
2899
+ agentLife[f].partner = m;
2900
+ agentLife[m].partner = f;
2901
+ agentHome[m] = agentHome[f];
2902
+ demoAgents[f].relationships.push({ name: demoAgents[m].name, type: 'spouse', closeness: 0.85 });
2903
+ demoAgents[m].relationships.push({ name: demoAgents[f].name, type: 'spouse', closeness: 0.85 });
2904
+ agentMemories[f].push(`Married ${demoAgents[m].name}`);
2905
+ agentMemories[m].push(`Married ${demoAgents[f].name}`);
2906
  }
2907
+
2908
  let demoDay = 1;
2909
+ let demoMinute = 630;
2910
  const DEMO_WEATHER_CYCLE = ['sunny','sunny','sunny','cloudy','cloudy','rainy','stormy','rainy','cloudy','sunny','sunny','snowy','cloudy','sunny','foggy','sunny'];
2911
  let demoWIdx = 0;
2912
+ let tickCount = 0;
2913
+ const deadAgents = [];
2914
 
2915
  function demoTimeStr() {
2916
  const hh = Math.floor(demoMinute / 60);
 
2919
  return `Day ${demoDay}, ${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')} (${phase})`;
2920
  }
2921
 
2922
+ function getLifePlan(id) {
2923
+ const l = agentLife[id];
2924
+ if (!l) return [];
2925
+ const plan = [];
2926
+ plan.push(l.age < 3 ? 'Baby at home' : l.age < 6 ? 'Kindergarten (3-5)' : l.age < 18 ? 'School (6-18)' : l.age < 23 ? 'University (18-23)' : l.age < 65 ? `Working as ${l.occupation}` : 'Retired');
2927
+ if (l.age >= 18 && !l.partner) plan.push('Looking for a partner');
2928
+ if (l.partner) plan.push(`Married to ${demoAgents[l.partner]?.name || l.partner}`);
2929
+ if (l.pregnant) plan.push('Expecting a baby!');
2930
+ if (l.children.length > 0) plan.push(`${l.children.length} child(ren)`);
2931
+ if (l.age >= 65) plan.push('Enjoying retirement');
2932
+ return plan;
2933
+ }
2934
+
2935
+ function lifeCycleTick(hh) {
2936
+ const aliveIds = allIds.filter(id => agentLife[id]?.alive);
2937
+ tickCount++;
2938
+
2939
+ // Age all agents every 72 ticks (~1 sim-day ≈ 72 ticks of 20min → 1 year per ~3 real-min cycles)
2940
+ if (tickCount % 6 === 0) {
2941
+ for (const id of aliveIds) {
2942
+ agentLife[id].age++;
2943
+ demoAgents[id].age = agentLife[id].age;
2944
+ const a = agentLife[id].age;
2945
+ if (a === 3) { agentLife[id].lifePhase = 'kindergarten'; agentLife[id].occupation = 'Child'; }
2946
+ if (a === 6) { agentLife[id].lifePhase = 'school'; agentLife[id].occupation = 'Student'; agentMemories[id].push('Started school'); }
2947
+ if (a === 18) { agentLife[id].lifePhase = 'university'; agentLife[id].occupation = 'Student'; agentMemories[id].push('Graduated high school'); }
2948
+ if (a === 23) { agentLife[id].lifePhase = 'working'; agentLife[id].occupation = OCCUPATIONS[hash(id) % OCCUPATIONS.length]; agentMemories[id].push(`Started career as ${agentLife[id].occupation}`); }
2949
+ if (a === 65) { agentLife[id].lifePhase = 'retired'; agentMemories[id].push('Retired'); }
2950
+ demoAgents[id].occupation = agentLife[id].occupation;
2951
+ demoAgents[id].lifePhase = agentLife[id].lifePhase;
2952
+ }
2953
+ }
2954
 
2955
+ // Marriage: every 4 ticks, try to match single adults
2956
+ if (tickCount % 4 === 0) {
2957
+ const sf = aliveIds.filter(id => agentLife[id].gender === 'female' && agentLife[id].age >= 18 && !agentLife[id].partner);
2958
+ const sm = aliveIds.filter(id => agentLife[id].gender === 'male' && agentLife[id].age >= 18 && !agentLife[id].partner);
2959
+ if (sf.length > 0 && sm.length > 0 && Math.random() < 0.3) {
2960
+ const f = sf[Math.floor(Math.random() * sf.length)];
2961
+ const m = sm[Math.floor(Math.random() * sm.length)];
2962
+ agentLife[f].partner = m;
2963
+ agentLife[m].partner = f;
2964
+ agentHome[m] = agentHome[f];
2965
+ demoAgents[f].relationships.push({ name: demoAgents[m].name, type: 'spouse', closeness: 0.8 + Math.random() * 0.2 });
2966
+ demoAgents[m].relationships.push({ name: demoAgents[f].name, type: 'spouse', closeness: 0.8 + Math.random() * 0.2 });
2967
+ agentMemories[f].push(`Married ${demoAgents[m].name} (Day ${demoDay})`);
2968
+ agentMemories[m].push(`Married ${demoAgents[f].name} (Day ${demoDay})`);
2969
+ }
2970
+ }
2971
+
2972
+ // Divorce: rare
2973
+ if (tickCount % 8 === 0 && Math.random() < 0.05) {
2974
+ const married = aliveIds.filter(id => agentLife[id].partner);
2975
+ if (married.length > 0) {
2976
+ const who = married[Math.floor(Math.random() * married.length)];
2977
+ const ex = agentLife[who].partner;
2978
+ if (ex && agentLife[ex]) {
2979
+ agentLife[who].partner = null;
2980
+ agentLife[ex].partner = null;
2981
+ agentMemories[who].push(`Divorced ${demoAgents[ex]?.name} (Day ${demoDay})`);
2982
+ agentMemories[ex].push(`Divorced ${demoAgents[who]?.name} (Day ${demoDay})`);
2983
+ const newHome = residentialLocs[Math.floor(Math.random() * residentialLocs.length)];
2984
+ agentHome[ex] = newHome;
2985
+ }
2986
+ }
2987
+ }
2988
+
2989
+ // Pregnancy & birth
2990
+ for (const id of aliveIds) {
2991
+ const l = agentLife[id];
2992
+ if (l.gender === 'female' && l.partner && l.age >= 18 && l.age <= 42 && !l.pregnant && l.children.length < 4) {
2993
+ if (Math.random() < 0.008) {
2994
+ l.pregnant = true;
2995
+ l.pregnancyTimer = 9;
2996
+ agentMemories[id].push(`Became pregnant (Day ${demoDay})`);
2997
+ }
2998
+ }
2999
+ if (l.pregnant) {
3000
+ l.pregnancyTimer--;
3001
+ if (l.pregnancyTimer <= 0) {
3002
+ l.pregnant = false;
3003
+ const babyGender = Math.random() < 0.5 ? 'female' : 'male';
3004
+ const namePool = babyGender === 'female' ? NAMES_F : NAMES_M;
3005
+ const babyName = namePool[Math.floor(Math.random() * namePool.length)] + String(nextId++);
3006
+ const babyId = makeAgent(babyName, babyGender, 0, agentHome[id]);
3007
+ allIds.push(babyId);
3008
+ l.children.push(babyId);
3009
+ if (l.partner && agentLife[l.partner]) agentLife[l.partner].children.push(babyId);
3010
+ agentMemories[id].push(`Gave birth to ${babyName} (Day ${demoDay})`);
3011
+ if (l.partner) agentMemories[l.partner].push(`Baby ${babyName} was born (Day ${demoDay})`);
3012
+ }
3013
+ }
3014
+ }
3015
+
3016
+ // Illness & death: agents 70+ have increasing chance
3017
+ if (tickCount % 5 === 0) {
3018
+ for (const id of aliveIds) {
3019
+ const a = agentLife[id].age;
3020
+ if (a >= 70) {
3021
+ const deathChance = (a - 65) * 0.004;
3022
+ if (Math.random() < deathChance) {
3023
+ if (demoAgents[id].state !== 'hospitalized') {
3024
+ demoAgents[id].location = 'hospital';
3025
+ demoAgents[id].state = 'hospitalized';
3026
+ agentMemories[id].push(`Hospitalized with serious illness (Day ${demoDay})`);
3027
+ } else {
3028
+ agentLife[id].alive = false;
3029
+ demoAgents[id].location = 'cemetery';
3030
+ demoAgents[id].state = 'deceased';
3031
+ deadAgents.push({ id, name: demoAgents[id].name, age: a, day: demoDay });
3032
+ agentMemories[id].push(`Passed away at age ${a} (Day ${demoDay})`);
3033
+ if (agentLife[id].partner) {
3034
+ const p = agentLife[id].partner;
3035
+ agentLife[p].partner = null;
3036
+ agentMemories[p].push(`${demoAgents[id].name} passed away (Day ${demoDay})`);
3037
+ }
3038
+ // Remove from active after a few ticks
3039
+ setTimeout(() => { delete demoAgents[id]; }, 12000);
3040
+ }
3041
+ }
3042
+ }
3043
+ }
3044
+ }
3045
+
3046
+ // Update memories in agent data
3047
+ for (const id of allIds) {
3048
+ if (demoAgents[id]) {
3049
+ demoAgents[id].recent_memories = (agentMemories[id] || []).slice(-8);
3050
+ demoAgents[id].plan = getLifePlan(id);
3051
+ }
3052
+ }
3053
+ }
3054
 
3055
  handleStateUpdate({
3056
  type: 'tick', time: demoTimeStr(),
3057
  state: { agents: demoAgents, locations: {}, weather: DEMO_WEATHER_CYCLE[0] }
3058
  });
3059
+ document.getElementById('sim-agents').textContent = `${Object.keys(demoAgents).length} agents (demo)`;
3060
 
3061
  setInterval(() => {
3062
  if (wsConnected) return;
 
3073
  const isNightTime = hh >= 22 || hh < 6;
3074
  const isLateEvening = hh >= 20 && hh < 22;
3075
 
3076
+ const agents = Object.keys(demoAgents).filter(id => agentLife[id]?.alive);
3077
+
3078
+ // Life cycle events
3079
+ lifeCycleTick(hh);
3080
+
3081
+ // Location-based on age/lifePhase
3082
+ for (const who of agents) {
3083
+ const l = agentLife[who];
3084
+ if (!l || !l.alive) continue;
3085
+ if (l.age < 3) { demoAgents[who].location = agentHome[who]; continue; }
3086
+ }
3087
 
3088
  if (isNightTime) {
3089
  for (const who of agents) {
3090
+ if (!agentLife[who]?.alive || demoAgents[who].state === 'deceased') continue;
3091
  const home = agentHome[who] || residentialLocs[hash(who) % residentialLocs.length];
3092
  demoAgents[who].location = home;
3093
  demoAgents[who].state = 'sleeping';
 
3096
  const goHomeCount = Math.floor(agents.length * 0.6);
3097
  for (let i = 0; i < goHomeCount; i++) {
3098
  const who = agents[Math.floor(Math.random() * agents.length)];
3099
+ if (!agentLife[who]?.alive) continue;
3100
  const home = agentHome[who] || residentialLocs[hash(who) % residentialLocs.length];
3101
  demoAgents[who].location = home;
3102
  demoAgents[who].state = 'resting';
3103
  }
 
 
 
 
 
 
 
 
3104
  } else if (badW) {
3105
  const stayInCount = Math.floor(agents.length * 0.7);
3106
  for (let i = 0; i < stayInCount; i++) {
3107
  const who = agents[Math.floor(Math.random() * agents.length)];
3108
+ if (!agentLife[who]?.alive) continue;
3109
  const home = agentHome[who] || residentialLocs[hash(who) % residentialLocs.length];
3110
  demoAgents[who].location = home;
3111
  demoAgents[who].state = 'sheltering';
3112
  }
 
 
 
 
 
 
3113
  } else {
3114
+ // Daytime: agents go to age-appropriate locations
3115
+ for (const who of agents) {
3116
+ const l = agentLife[who];
3117
+ if (!l || !l.alive) continue;
3118
+ if (Math.random() > 0.15) continue;
3119
+ if (l.age >= 3 && l.age < 6) {
3120
+ demoAgents[who].location = 'kindergarten';
3121
+ demoAgents[who].state = 'playing';
3122
+ } else if (l.age >= 6 && l.age < 18) {
3123
+ demoAgents[who].location = hh >= 8 && hh < 15 ? 'school' : publicLocs[Math.floor(Math.random() * publicLocs.length)];
3124
+ demoAgents[who].state = hh >= 8 && hh < 15 ? 'studying' : 'idle';
3125
+ } else if (l.age >= 18 && l.age < 23) {
3126
+ demoAgents[who].location = hh >= 9 && hh < 16 ? 'university' : publicLocs[Math.floor(Math.random() * publicLocs.length)];
3127
+ demoAgents[who].state = hh >= 9 && hh < 16 ? 'studying' : 'idle';
3128
+ } else if (l.age >= 23 && l.age < 65) {
3129
+ if (hh >= 9 && hh < 17) {
3130
+ demoAgents[who].location = workLocs[hash(who) % workLocs.length];
3131
+ demoAgents[who].state = 'working';
3132
+ } else {
3133
+ demoAgents[who].location = publicLocs[Math.floor(Math.random() * publicLocs.length)];
3134
+ demoAgents[who].state = 'idle';
3135
+ }
3136
+ } else {
3137
+ demoAgents[who].location = publicLocs[Math.floor(Math.random() * publicLocs.length)];
3138
+ demoAgents[who].state = 'idle';
3139
+ }
3140
  }
3141
  }
3142
 
 
3144
  type: 'tick', time: demoTimeStr(),
3145
  state: { agents: demoAgents, locations: {}, weather: w }
3146
  });
3147
+ document.getElementById('sim-agents').textContent = `${agents.length} agents (demo)` + (deadAgents.length > 0 ? ` | ${deadAgents.length} deceased` : '');
3148
  }, 2500);
3149
  }
3150
 
web/index.html CHANGED
@@ -41,15 +41,24 @@
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; }
48
 
49
- /* RIGHT DATA PANEL (1/8 of screen) */
 
 
 
 
 
 
 
50
  #sidebar {
51
- flex: 1; background: #16213e; border-left: 2px solid #0f3460;
52
- display: flex; flex-direction: column; overflow: hidden; min-width: 0;
 
 
53
  }
54
  .sidebar-tabs {
55
  display: flex; border-bottom: 2px solid #0f3460; flex-shrink: 0;
@@ -298,6 +307,7 @@
298
  <div id="toast-container"></div>
299
  <div id="llm-popup"><div class="llm-pop-title">SWITCH LLM</div></div>
300
  </div>
 
301
  <div id="sidebar">
302
  <div class="sidebar-tabs">
303
  <div class="sidebar-tab active" data-tab="agents" onclick="switchTab('agents')">Agents</div>
@@ -452,6 +462,34 @@
452
  </div>
453
 
454
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  // ============================================================
456
  // CONFIG
457
  // ============================================================
 
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: 1; 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; }
48
 
49
+ /* RESIZE HANDLE */
50
+ #sidebar-resizer {
51
+ width: 5px; cursor: col-resize; background: #0f3460;
52
+ flex-shrink: 0; transition: background 0.2s;
53
+ }
54
+ #sidebar-resizer:hover, #sidebar-resizer.active { background: #4ecca3; }
55
+
56
+ /* RIGHT DATA PANEL (resizable) */
57
  #sidebar {
58
+ width: 320px; min-width: 200px; max-width: 600px;
59
+ background: #16213e; border-left: 2px solid #0f3460;
60
+ display: flex; flex-direction: column; overflow: hidden;
61
+ flex-shrink: 0;
62
  }
63
  .sidebar-tabs {
64
  display: flex; border-bottom: 2px solid #0f3460; flex-shrink: 0;
 
307
  <div id="toast-container"></div>
308
  <div id="llm-popup"><div class="llm-pop-title">SWITCH LLM</div></div>
309
  </div>
310
+ <div id="sidebar-resizer"></div>
311
  <div id="sidebar">
312
  <div class="sidebar-tabs">
313
  <div class="sidebar-tab active" data-tab="agents" onclick="switchTab('agents')">Agents</div>
 
462
  </div>
463
 
464
  <script>
465
+ // ============================================================
466
+ // SIDEBAR RESIZER
467
+ // ============================================================
468
+ (function() {
469
+ const resizer = document.getElementById('sidebar-resizer');
470
+ const sidebar = document.getElementById('sidebar');
471
+ let isResizing = false;
472
+ resizer.addEventListener('mousedown', (e) => {
473
+ isResizing = true;
474
+ resizer.classList.add('active');
475
+ document.body.style.cursor = 'col-resize';
476
+ document.body.style.userSelect = 'none';
477
+ e.preventDefault();
478
+ });
479
+ document.addEventListener('mousemove', (e) => {
480
+ if (!isResizing) return;
481
+ const newWidth = window.innerWidth - e.clientX;
482
+ sidebar.style.width = Math.max(200, Math.min(600, newWidth)) + 'px';
483
+ });
484
+ document.addEventListener('mouseup', () => {
485
+ if (!isResizing) return;
486
+ isResizing = false;
487
+ resizer.classList.remove('active');
488
+ document.body.style.cursor = '';
489
+ document.body.style.userSelect = '';
490
+ });
491
+ })();
492
+
493
  // ============================================================
494
  // CONFIG
495
  // ============================================================