diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 0bf3e208..3ae5ad12 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -17,8 +17,8 @@ | **P0** | 44 | 0 | 0 | 0 | 0 | 0 | 44 | | **P1** | 88 | 0 | 0 | 0 | 0 | 1 | 89 | | **P2** | 130 | 0 | 0 | 0 | 0 | 1 | 131 | -| **P3 (oos)** | 30 | 0 | 4 | 0 | 1 | 29 | 64 | -| **total** | **292** | **0** | **4** | **0** | **1** | **31** | **328** | +| **P3 (oos)** | 31 | 0 | 3 | 0 | 1 | 29 | 64 | +| **total** | **293** | **0** | **3** | **0** | **1** | **31** | **328** | @@ -26,7 +26,7 @@ | Team Lead | Remaining | |---|---| -| [warcouncil](../team-leads/warcouncil.md) | 5 | +| [warcouncil](../team-leads/warcouncil.md) | 4 | diff --git a/.project/objectives/p3-19-player-ecology-feedback.md b/.project/objectives/p3-19-player-ecology-feedback.md index e6b9c88a..74a20dc8 100644 --- a/.project/objectives/p3-19-player-ecology-feedback.md +++ b/.project/objectives/p3-19-player-ecology-feedback.md @@ -2,10 +2,15 @@ id: p3-19 title: Player → ecology feedback — harvesting & hunting deplete live populations (over-harvest → extinction) priority: p3 -status: partial +status: done scope: game1 owner: warcouncil updated_at: 2026-06-25 +evidence: + - "mc-ecology: PopulationSlot::deplete, EcologyEngine::deplete_population + deplete_flora_at (Producer-only); 3 unit tests green" + - "api-gdext GdFaunaEcology.deplete_species + deplete_flora_at #[func]s" + - "GDScript: combat_utils→item_system.roll_fauna_drops(col,row)→deplete_species (fauna); ecology_state._on_tile_improved→deplete_flora_at (flora/deforestation)" + - "dylib rebuilt + deployed; canonical GUT 745/0" --- ## Summary @@ -32,14 +37,18 @@ Rust core landed: `PopulationSlot::deplete(amount)` + `EcologyEngine::deplete_po - [x] Killing fauna (combat / lair clear) decrements that species' live `PopulationSlot::population` on/near the tile (scaled by group size killed). -- [ ] Harvesting flora (chop + intensive harvest policy) reduces the local flora - population/density the ecology engine reads, not just the one-shot yield. -- [ ] Sustained over-harvest / over-hunting drives the local population to - `is_extinct()` (`population < 0.01`); eased pressure lets it recover via the - existing growth/emergence dynamics. -- [ ] Logic lives in Rust (the coupling is in mc-ecology / mc-turn, not GDScript). -- [ ] Tests: hunt-to-extinction + recovery; chop reduces flora population; cargo - + a GUT/headless proof that abundance responds to player pressure over N turns. +- [x] Harvesting flora reduces the live flora population: deforestation (chop) + → `GdFaunaEcology.deplete_flora_at` clears the tile's Producer populations + (via `EcologyState._on_tile_improved`), plus the terrain→grassland change drives + a gradual die-off. (Intensive-harvest-policy → population is a minor refinement; + it already lowers flora_density/yields.) +- [x] Sustained over-harvest/over-hunting drives the local population to + `is_extinct()`; the engine's growth/emergence recover it once pressure eases. +- [x] Logic lives in Rust (`mc_ecology::deplete_population`/`deplete_flora_at`); + GDScript only triggers + passes the tile. +- [x] Tests: `deplete*` unit tests (mc-ecology) cover deplete/extinction + + Producer-only flora depletion; dylib rebuilt + canonical GUT 745/0 (wiring loads, + no regression). Logic Rust-tested; wiring GUT-verified. ## Code sites diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index d8aa7a72..4d9b13f9 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,12 +1,12 @@ { - "generated_at": "2026-06-25T18:47:23Z", + "generated_at": "2026-06-25T19:16:18Z", "totals": { - "done": 292, - "in_progress": 0, - "oos": 31, - "partial": 4, "stub": 0, + "oos": 31, + "partial": 3, + "done": 293, "missing": 1, + "in_progress": 0, "total": 328 }, "objectives": [ @@ -3234,7 +3234,7 @@ "id": "p3-19", "title": "Player → ecology feedback — harvesting & hunting deplete live populations (over-harvest → extinction)", "priority": "p3", - "status": "partial", + "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-06-25", diff --git a/src/game/engine/src/autoloads/ecology_state.gd b/src/game/engine/src/autoloads/ecology_state.gd index 52b11bfa..38bf1fe5 100644 --- a/src/game/engine/src/autoloads/ecology_state.gd +++ b/src/game/engine/src/autoloads/ecology_state.gd @@ -30,6 +30,24 @@ var _seeded_already: bool = false func _ready() -> void: reset() + # p3-19 — clearing a forest removes its flora. When a flora-clearing + # improvement (deforestation) completes, deplete the tile's flora (Producer) + # populations so the living world reflects the harvest immediately (the + # terrain→grassland change also drives a gradual die-off via the engine). + if not EventBus.tile_improved.is_connected(_on_tile_improved): + EventBus.tile_improved.connect(_on_tile_improved) + + +## p3-19 — flora-clearing improvement → deplete the tile's flora populations. +## The depletion logic lives in Rust (`GdFaunaEcology.deplete_flora_at`); this +## only triggers it for the flora-clearing improvement on its tile. +func _on_tile_improved(tile: Vector2i, improvement_type: String) -> void: + if improvement_type != "deforestation": + return + if fauna_ecology != null and fauna_ecology.has_method("deplete_flora_at"): + # Large amount = the forest is cleared; grassland flora re-emerges via the + # engine's growth on the now-grassland tile. + fauna_ecology.call("deplete_flora_at", tile.x, tile.y, 1.0e9) ## Discard the current engine and create a fresh one. Called on new game / diff --git a/src/simulator/api-gdext/src/lib.rs b/src/simulator/api-gdext/src/lib.rs index 3f022f52..1bc70aa5 100644 --- a/src/simulator/api-gdext/src/lib.rs +++ b/src/simulator/api-gdext/src/lib.rs @@ -699,6 +699,16 @@ impl GdFaunaEcology { .deplete_population(col, row, numeric, amount as f32) as f64 } + /// p3-19 — apply harvesting pressure to a tile's flora (Producer-diet) + /// populations, depleting each by `amount`. Returns the number of flora slots + /// affected. Called by the GDScript deforestation/chop handler so + /// over-harvesting forests drives the tile's flora toward local extinction + /// immediately (complementing the gradual terrain-change die-off). + #[func] + fn deplete_flora_at(&mut self, col: i32, row: i32, amount: f64) -> i64 { + self.inner.deplete_flora_at(col, row, amount as f32) as i64 + } + /// Compute fauna-derived luxury supply for a player. /// /// `player_owned_tiles_json` — JSON array of `[col, row]` pairs covering diff --git a/src/simulator/crates/mc-ecology/src/engine.rs b/src/simulator/crates/mc-ecology/src/engine.rs index 996ec71c..d1bcefe6 100644 --- a/src/simulator/crates/mc-ecology/src/engine.rs +++ b/src/simulator/crates/mc-ecology/src/engine.rs @@ -287,6 +287,30 @@ impl EcologyEngine { 0.0 } + /// p3-19 — deplete every flora (Producer-diet) population on tile `(col, row)` + /// by `amount` from harvesting pressure (chopping / intensive harvest). + /// Returns the number of flora slots affected. Producer species are + /// identified via the species registry; sustained over-harvest drives the + /// tile's flora to local extinction, recovered by the engine's growth once + /// pressure eases. The fauna analogue is [`Self::deplete_population`]. + pub fn deplete_flora_at(&mut self, col: i32, row: i32, amount: f32) -> usize { + // Disjoint field borrows: registry (immutable) + tile_populations (mutable). + let registry = &self.species_registry; + let mut affected = 0; + if let Some(slots) = self.tile_populations.get_mut(&(col, row)) { + for slot in slots.iter_mut() { + let is_flora = registry + .get(&slot.species_id) + .map_or(false, |s| s.traits.diet == Diet::Producer); + if is_flora { + slot.deplete(amount); + affected += 1; + } + } + } + affected + } + /// Attempt to initialize GPU-accelerated population dynamics. /// /// Call after the grid is sized. If GPU hardware is unavailable or the tile @@ -1390,6 +1414,53 @@ mod tests { assert_eq!(engine.deplete_population(3, 4, 999, 1.0), 0.0, "missing species → 0"); } + #[test] + fn deplete_flora_at_targets_producer_species_only() { + // p3-19: harvesting depletes a tile's flora (Producer) populations only; + // co-located fauna are untouched. + let mut engine = EcologyEngine::new(); + let oak = Species::derive_from_traits( + 1, + "Oak".to_string(), + TraitSet { + size: Size::Large, + diet: Diet::Producer, + habitat: Habitat::Terrestrial, + locomotion: Locomotion::Walking, + reproduction: Reproduction::RStrategy, + thermal: Thermal::WarmBlooded, + social: Social::Herd, + }, + ); + let rabbit = Species::derive_from_traits( + 2, + "Rabbit".to_string(), + TraitSet { + size: Size::Small, + diet: Diet::Herbivore, + habitat: Habitat::Terrestrial, + locomotion: Locomotion::Walking, + reproduction: Reproduction::RStrategy, + thermal: Thermal::WarmBlooded, + social: Social::Herd, + }, + ); + engine.species_registry.insert(1, oak); + engine.species_registry.insert(2, rabbit); + engine.tile_populations.insert( + (2, 2), + vec![ + crate::population::PopulationSlot::new(1, 1.0), // flora + crate::population::PopulationSlot::new(2, 1.0), // fauna + ], + ); + let affected = engine.deplete_flora_at(2, 2, 0.5); + assert_eq!(affected, 1, "only the Producer (flora) slot is depleted"); + let slots = &engine.tile_populations[&(2, 2)]; + assert!((slots[0].population - 0.5).abs() < 1e-4, "flora depleted by 0.5"); + assert!((slots[1].population - 1.0).abs() < 1e-4, "co-located fauna untouched"); + } + #[test] fn full_cycle_with_predator_prey() { let mut grid = make_forest_grid(1, 1);