Street lamps, moon/stars fix, 100 agents with lifecycle, resizable panel
Browse files3D 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>
- web/3d.html +433 -162
- web/index.html +42 -4
|
@@ -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:'#
|
| 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.
|
| 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.
|
| 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.
|
| 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,
|
| 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,
|
| 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,
|
| 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,
|
| 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
|
| 1528 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1529 |
);
|
| 1530 |
-
|
| 1531 |
-
|
| 1532 |
-
|
| 1533 |
-
scene.add(
|
| 1534 |
|
| 1535 |
// Stars
|
| 1536 |
-
const starCount =
|
| 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.
|
| 1541 |
-
const r =
|
| 1542 |
starPos[i*3] = Math.cos(th) * Math.sin(ph) * r;
|
| 1543 |
-
starPos[i*3+1] = Math.cos(ph) * r +
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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 |
-
|
| 1708 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1709 |
} else {
|
| 1710 |
-
|
|
|
|
| 1711 |
}
|
| 1712 |
-
starPoints.visible = hour >=
|
| 1713 |
-
starPoints.material.opacity = (hour >=
|
| 1714 |
}
|
| 1715 |
updateCelestials(10);
|
| 1716 |
|
|
@@ -1737,12 +1853,12 @@ function updateTimeOfDay(phase, hour) {
|
|
| 1737 |
|
| 1738 |
// Lighting
|
| 1739 |
if (isNight) {
|
| 1740 |
-
hemiLight.intensity = 0.
|
| 1741 |
-
ambientLight.intensity = 0.
|
| 1742 |
-
sunLight.intensity = 0.
|
| 1743 |
-
ground.material.color.set(
|
| 1744 |
-
gridHelper.material.opacity = 0.
|
| 1745 |
-
scene.fog = new THREE.FogExp2(
|
| 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.
|
| 1997 |
ox = Math.cos(angle) * radius;
|
| 1998 |
oz = Math.sin(angle) * radius;
|
| 1999 |
}
|
| 2000 |
|
| 2001 |
-
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 2329 |
const memories = data.recent_memories || data.memories || [];
|
| 2330 |
if (memories.length > 0) {
|
| 2331 |
-
html += `<div class="info-section"><h4>
|
| 2332 |
-
for (const mem of memories.slice(
|
| 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
|
| 2703 |
-
|
| 2704 |
-
|
| 2705 |
-
|
| 2706 |
-
|
| 2707 |
-
|
| 2708 |
-
|
| 2709 |
-
|
| 2710 |
-
|
| 2711 |
-
|
| 2712 |
-
|
| 2713 |
-
|
| 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 |
-
|
| 2781 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2782 |
}
|
|
|
|
| 2783 |
let demoDay = 1;
|
| 2784 |
-
let demoMinute = 630;
|
| 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 |
-
|
| 2796 |
-
|
| 2797 |
-
|
| 2798 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2799 |
|
| 2800 |
-
|
| 2801 |
-
|
| 2802 |
-
|
| 2803 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = `${
|
| 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 |
-
|
| 2866 |
-
for (
|
| 2867 |
-
const
|
| 2868 |
-
|
| 2869 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">● ${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 |
|
|
@@ -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:
|
| 45 |
#viewport-3d iframe { width: 100%; height: 100%; border: none; display: block; }
|
| 46 |
#canvas-container { display: none; }
|
| 47 |
#cityCanvas { display: none; }
|
| 48 |
|
| 49 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
#sidebar {
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
| 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 |
// ============================================================
|