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