From 27ade4cd439178020eca0b642f21a71f7b7ea658 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sun, 7 Jun 2026 19:58:36 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20per-tile=20fauna=20density=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../engine/src/autoloads/ecology_state.gd | 11 ++++++++++ src/game/engine/src/autoloads/event_bus.gd | 5 +++++ src/game/engine/src/autoloads/turn_manager.gd | 4 ++++ src/simulator/api-gdext/src/lib.rs | 20 +++++++++++++++++++ 4 files changed, 40 insertions(+) diff --git a/src/game/engine/src/autoloads/ecology_state.gd b/src/game/engine/src/autoloads/ecology_state.gd index d6812233..8bcf3a8d 100644 --- a/src/game/engine/src/autoloads/ecology_state.gd +++ b/src/game/engine/src/autoloads/ecology_state.gd @@ -46,6 +46,17 @@ func tick(grid: RefCounted, seed: int) -> void: fauna_ecology.call("tick_populations", grid, seed) +## Per-tile total fauna population for the live-map fauna overlay +## (`fauna_overlay_renderer.gd`). One bulk bridge read per refresh: a Dictionary +## keyed by `Vector2i(col, row)` → total population (float). Empty when no +## engine or no populations yet (emergence has not fired). The overlay reads +## this on each `EventBus.worldsim_updated` and normalizes for its heatmap. +func tile_densities() -> Dictionary: + if fauna_ecology == null: + return {} + return fauna_ecology.call("populated_tile_densities") + + func _ensure_species_registered() -> void: ## Idempotent: a non-empty registry short-circuits via the engine's ## `population_slot_count` proxy on subsequent ticks (slots only appear diff --git a/src/game/engine/src/autoloads/event_bus.gd b/src/game/engine/src/autoloads/event_bus.gd index 84988e75..f7e9ed63 100644 --- a/src/game/engine/src/autoloads/event_bus.gd +++ b/src/game/engine/src/autoloads/event_bus.gd @@ -272,6 +272,11 @@ signal creature_died(pos: Vector2i, species_name: String, quality: int) signal creature_born(pos: Vector2i, species_name: String) ## Tile crossed Q4 threshold — natural wonder emerged from ecology. signal landmark_formed(pos: Vector2i, name: String, quality: int) +## Emitted once per full game turn after the continuous worldsim has advanced +## (climate physics + fauna `EcologyState.tick` + `WorldsimState.dispatch`). +## The fauna overlay subscribes to refresh its per-tile density heatmap so the +## evolving living world is visible on the playable map (p2-80 render hook). +signal worldsim_updated(turn: int) # -- Natural event signals -- signal natural_event_spawned(event_type: String, position: Vector2i, intensity: float) diff --git a/src/game/engine/src/autoloads/turn_manager.gd b/src/game/engine/src/autoloads/turn_manager.gd index f19abc81..57f5f71c 100644 --- a/src/game/engine/src/autoloads/turn_manager.gd +++ b/src/game/engine/src/autoloads/turn_manager.gd @@ -301,6 +301,10 @@ func next_player() -> void: WorldsimState.dispatch( fauna_grid, GameState.turn_number, GameState.map_seed ) + # p2-80 render hook: signal the fauna overlay to refresh its + # per-tile density heatmap so the evolving living world is + # visible on the playable map, not just simulated. + EventBus.worldsim_updated.emit(GameState.turn_number) GameState.current_player_index = 0 GameState.turn_number += 1 # Check victory conditions after all players have moved diff --git a/src/simulator/api-gdext/src/lib.rs b/src/simulator/api-gdext/src/lib.rs index 673b7c2c..9eb94879 100644 --- a/src/simulator/api-gdext/src/lib.rs +++ b/src/simulator/api-gdext/src/lib.rs @@ -851,6 +851,26 @@ impl GdFaunaEcology { fn populated_tile_count(&self) -> i64 { self.inner.tile_populations.len() as i64 } + + /// Per-tile total fauna population, as a `Dictionary` keyed by + /// `Vector2i(col, row)` → total population (`f64`, summed across all species + /// slots on the tile). One bulk read so the live-map fauna overlay + /// (`fauna_overlay_renderer.gd`) refreshes per turn with a single bridge + /// call instead of `populated_tile_count`×`populations_on_tile`. Tiles with + /// zero population are omitted (the map is sparse, mirroring the engine's + /// own `tile_populations`). Deterministic order is irrelevant here — the + /// renderer keys by position, not iteration order. + #[func] + fn populated_tile_densities(&self) -> Dictionary { + let mut out = Dictionary::new(); + for (&(col, row), slots) in &self.inner.tile_populations { + let total: f32 = slots.iter().map(|s| s.population).sum(); + if total > 0.0 { + out.set(Vector2i::new(col, row), f64::from(total)); + } + } + out + } } // ── GdAtmosphericChemistry ──────────────────────────────────────────────