import { World, EVENT_TYPES } from "./world.js"; import { EntityManager } from "./entityManager.js"; import { Entity } from "./entities.js"; import { ParticleSystem } from "./particles.js"; import { StatsTracker } from "./stats.js"; import { UI } from "./ui.js"; import { createGenome, ENTITY_TYPES } from "./genetics.js"; import { randomRange } from "./utils.js"; const SAVE_KEY = "digital-life-simulator-state"; const TARGET_FPS = 60; const MIN_POPULATION = 15; const MAX_POPULATION = 80; const INITIAL_POPULATION = 35; const GENERATION_INTERVAL = 45; export class Ecosystem { constructor() { this.world = null; this.entityManager = null; this.particles = null; this.stats = null; this.ui = null; this.simCanvas = null; this.simCtx = null; this.particleCanvas = null; this.particleCtx = null; this.running = false; this._rafId = null; this._lastTime = 0; this._fpsAccum = 0; this._fpsFrames = 0; this._currentFps = 60; this._genTimer = 0; this._saveTimer = 0; this._eventCheckTimer = 0; this._lastEventCount = 0; } init() { this.simCanvas = document.getElementById("simulation-canvas"); this.particleCanvas = document.getElementById("particle-canvas"); if (!this.simCanvas || !this.particleCanvas) { console.error("Canvas elements not found"); return; } this.simCtx = this.simCanvas.getContext("2d"); this.particleCtx = this.particleCanvas.getContext("2d"); this._resizeCanvases(); window.addEventListener("resize", () => this._resizeCanvases()); const w = this.simCanvas.width; const h = this.simCanvas.height; const saved = this._loadState(); if (saved) { this.world = World.fromJSON(saved.world); this.stats = StatsTracker.fromJSON(saved.stats); this.entityManager = new EntityManager(w, h); if (saved.entities && saved.entities.length > 0) { for (const ed of saved.entities) { try { const { Genome } = await_import_workaround(); } catch (_) {} } } this.entityManager.spawnRandom(INITIAL_POPULATION, this.world); } else { this.world = new World(w, h); this.stats = new StatsTracker(); this.entityManager = new EntityManager(w, h); this.entityManager.spawnRandom(INITIAL_POPULATION, this.world); } this.particles = new ParticleSystem(2000); const container = document.getElementById("app"); this.ui = new UI(container); this.ui.init(); this.ui.addEvent("Ecosystem initialized", "milestone"); this.ui.announce("Digital Life Simulator Active"); const types = Object.keys(ENTITY_TYPES); for (const type of types) { const count = this.entityManager.entities.filter( (e) => e.type === type, ).length; if (count > 0) { this.ui.addEvent( `${ENTITY_TYPES[type].name}: ${count} spawned`, "birth", ); } } } start() { if (this.running) return; this.running = true; this._lastTime = performance.now(); this._loop(this._lastTime); } stop() { this.running = false; if (this._rafId) { cancelAnimationFrame(this._rafId); this._rafId = null; } } _loop(timestamp) { if (!this.running) return; this._rafId = requestAnimationFrame((t) => this._loop(t)); const rawDt = (timestamp - this._lastTime) / 1000; const dt = Math.min(rawDt, 0.05); this._lastTime = timestamp; this._fpsAccum += rawDt; this._fpsFrames++; if (this._fpsAccum >= 0.5) { this._currentFps = this._fpsFrames / this._fpsAccum; this._fpsFrames = 0; this._fpsAccum = 0; } this._update(dt); this._render(); } _update(dt) { this.world.update(dt); this._eventCheckTimer += dt; if (this._eventCheckTimer >= 1) { this._eventCheckTimer = 0; if (this.world.events.length > this._lastEventCount) { const newEvent = this.world.events[this.world.events.length - 1]; this.ui.announceWorldEvent(newEvent); this._lastEventCount = this.world.events.length; } if (this.world.events.length < this._lastEventCount) { this._lastEventCount = this.world.events.length; } } this.entityManager.update(dt, this.world, this.particles, this.stats); this.particles.update(dt); for (const event of this.world.events) { if (event.type === "storm" || event.type === "bloom") { this.particles.emitWeather(event, dt); } } this._managePopulation(dt); this._genTimer += dt; if (this._genTimer >= GENERATION_INTERVAL) { this._genTimer = 0; this.stats.onGeneration(); this.ui.addEvent( `Generation ${this.stats.generationCount} reached`, "evolution", ); this.ui.announce(`Generation ${this.stats.generationCount}`); const genEl = document.getElementById("gen-count"); if (genEl) { genEl.classList.remove("generation-flash"); void genEl.offsetWidth; genEl.classList.add("generation-flash"); } } this.stats.record( this.entityManager.entities, this.world, dt, this._currentFps, this.particles.count, ); this._saveTimer += dt; if (this._saveTimer >= 30) { this._saveTimer = 0; this._saveState(); } this.ui.update(this.stats, this.world, this.entityManager, dt); } _render() { const simCtx = this.simCtx; const partCtx = this.particleCtx; const w = this.world.width; const h = this.world.height; simCtx.fillStyle = "#06060f"; simCtx.fillRect(0, 0, w, h); partCtx.clearRect(0, 0, w, h); this.world.render(simCtx); this._renderFrame = (this._renderFrame || 0) + 1; if (this._renderFrame % 15 === 0) { this.stats.renderHeatmap(simCtx, w, h); } this.entityManager.render(simCtx); this.particles.render(partCtx); } _managePopulation(dt) { const alive = this.entityManager.entities.length; if (alive < MIN_POPULATION) { const deficit = MIN_POPULATION - alive; const toSpawn = Math.min(deficit, 5); if (alive >= 2) { this.entityManager.spawnFromPopulation( this.entityManager.entities, toSpawn, this.world, ); this.ui.addEvent( `${toSpawn} entities evolved from survivors`, "evolution", ); } else { this.entityManager.spawnRandom(toSpawn, this.world); this.ui.addEvent(`${toSpawn} new entities seeded`, "birth"); } } this._diversityTimer = (this._diversityTimer || 0) + dt; if (this._diversityTimer >= 8) { this._diversityTimer = 0; const types = Object.keys(ENTITY_TYPES); const typeCounts = {}; for (const t of types) typeCounts[t] = 0; for (const e of this.entityManager.entities) { if (e.alive) typeCounts[e.type]++; } for (const type of types) { if (typeCounts[type] < 2 && alive < MAX_POPULATION - 3) { const count = Math.min(3, MAX_POPULATION - alive); for (let i = 0; i < count; i++) { const genome = createGenome(type); const x = randomRange(40, this.world.width - 40); const y = randomRange(40, this.world.height - 40); const entity = new Entity(x, y, genome, this.stats.generationCount); this.entityManager.add(entity); } this.ui.addEvent( `${ENTITY_TYPES[type].name} migration wave (${count})`, "birth", ); } } } if (alive > MAX_POPULATION) { const excess = alive - MAX_POPULATION; const sorted = [...this.entityManager.entities].sort( (a, b) => a.fitness - b.fitness, ); for (let i = 0; i < excess && i < sorted.length; i++) { sorted[i].alive = false; sorted[i].energy = 0; } } } _resizeCanvases() { const w = window.innerWidth; const h = window.innerHeight; const dpr = Math.min(window.devicePixelRatio || 1, 2); for (const canvas of [this.simCanvas, this.particleCanvas]) { if (!canvas) continue; canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + "px"; canvas.style.height = h + "px"; const ctx = canvas.getContext("2d"); ctx.scale(dpr, dpr); } if (this.world) { this.world.width = w; this.world.height = h; } if (this.entityManager) { this.entityManager.spatialGrid = new (Object.getPrototypeOf( this.entityManager.spatialGrid, ).constructor)(w, h, 60); } } _saveState() { try { const state = { world: this.world.toJSON(), stats: this.stats.toJSON(), version: 1, }; localStorage.setItem(SAVE_KEY, JSON.stringify(state)); } catch (e) {} } _loadState() { try { const raw = localStorage.getItem(SAVE_KEY); if (!raw) return null; return JSON.parse(raw); } catch (e) { return null; } } } function await_import_workaround() { return null; } export function bootstrap() { const eco = new Ecosystem(); eco.init(); eco.start(); window.__ecosystem = eco; return eco; }