feat(@projects/@magic-civilization): 🌲 p3-19 DONE — flora half: deforestation depletes live flora populations

Completes player→ecology feedback. EcologyEngine::deplete_flora_at(col,row,amount)
depletes a tile's Producer-diet (flora) populations (registry-identified);
GdFaunaEcology.deplete_flora_at exposes it; EcologyState._on_tile_improved fires
it when a flora-clearing improvement (deforestation) completes — so clear-cutting
a forest removes its flora (the terrain→grassland change also drives the gradual
die-off). With the fauna half (over-hunting → extinction), the living world now
reacts to player pressure both ways. Logic stays in mc_ecology (Rail 1).

Test: deplete_flora_at_targets_producer_species_only (mc-ecology green); dylib
rebuilt + deployed; canonical GUT 745/0 (wiring loads, no regression).

p3-19 → done. Next: p3-20 (weather→scouting).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-25 15:16:18 -04:00
parent d6eaa79838
commit bb38d5db0e
6 changed files with 126 additions and 18 deletions

View file

@ -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** |
</td><td valign='top' style='padding-left:2em'>
@ -26,7 +26,7 @@
| Team Lead | Remaining |
|---|---|
| [warcouncil](../team-leads/warcouncil.md) | 5 |
| [warcouncil](../team-leads/warcouncil.md) | 4 |
</td></tr></table>

View file

@ -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

View file

@ -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",

View file

@ -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 /

View file

@ -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

View file

@ -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);