feat(@projects/@magic-civilization): 🦌 p3-19 (fauna) — over-hunting depletes live populations → local extinction

Wires the player→ecology coupling for fauna (the USP: the living world reacts to
the player). GdFaunaEcology.deplete_species #[func] resolves the string species id
→ numeric via the species library and calls EcologyEngine::deplete_population.
combat_utils._roll_wild_creature_loot now passes the slain creature's tile into
item_system.roll_fauna_drops, which calls EcologyState.fauna_ecology.deplete_species
on every fauna kill — so sustained hunting drives a species to local extinction
(is_extinct), and the engine's growth/emergence recover it once pressure eases.
Logic stays in mc_ecology (Rail 1); GDScript only triggers + passes the tile.

Verified: mc-ecology cargo green; dylib rebuilt + deployed; canonical GUT 745/0
(new roll_fauna_drops signature + caller load cleanly, deplete_species callable).

p3-19 stays partial — flora-harvest half (chop/intensive → flora population) next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-25 14:47:23 -04:00
parent eb2cf18c2d
commit d6eaa79838
5 changed files with 43 additions and 6 deletions

View file

@ -26,11 +26,11 @@ and abundance doesn't respond to player pressure — which undercuts the
## Progress (2026-06-25)
Rust core landed: `PopulationSlot::deplete(amount)` + `EcologyEngine::deplete_population(col,row,species_id,amount)` (floors at 0, drives local extinction; missing tile/species = safe 0.0 no-op). Tests green (mc-ecology). **Remaining (the coupling/wiring):** a `GdEcologyEngine` `#[func]` exposing deplete + the GDScript kill (item_system.gd / lair clear) and intensive-harvest paths calling it for the victim/flora species at the tile + dylib rebuild + GUT proof that abundance responds to player pressure.
Rust core landed: `PopulationSlot::deplete(amount)` + `EcologyEngine::deplete_population(col,row,species_id,amount)` (floors at 0, drives local extinction; missing tile/species = safe 0.0 no-op). **Fauna half DONE + verified:** `GdFaunaEcology.deplete_species` #[func] (string→u32 via species library); `combat_utils._roll_wild_creature_loot``item_system.roll_fauna_drops(col,row)``EcologyState.fauna_ecology.deplete_species` on every fauna kill (over-hunting → local extinction). Dylib rebuilt + deployed; GUT 745/0 (loads + no regression). **Remaining (flora half):** wire chop/intensive-harvest (`mc-city::harvest` / harvest_policy) → deplete the tile's flora (Producer) population.
## Acceptance
- [ ] Killing fauna (combat / lair clear) decrements that species' live
- [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.

View file

@ -1,12 +1,12 @@
{
"generated_at": "2026-06-25T18:06:31Z",
"generated_at": "2026-06-25T18:47:23Z",
"totals": {
"done": 292,
"in_progress": 0,
"missing": 1,
"oos": 31,
"partial": 4,
"stub": 0,
"done": 292,
"missing": 1,
"total": 328
},
"objectives": [

View file

@ -182,7 +182,12 @@ static func _roll_wild_creature_loot(victim: RefCounted, killer: RefCounted) ->
var turn_seed: int = GameState.game_rng.seed ^ GameState.turn_number
var killer_id: int = hash(killer.id)
var victim_id: int = hash(victim.id)
ItemSystemScript.roll_fauna_drops(creature_type, killer_player, turn_seed, killer_id, victim_id)
# p3-19: pass the slain creature's tile so its kill depletes the live ecology
# population there (over-hunting → local extinction).
var vpos: Vector2i = victim.position
ItemSystemScript.roll_fauna_drops(
creature_type, killer_player, turn_seed, killer_id, victim_id, vpos.x, vpos.y
)
## Destroy the High Archon of a player (capital capture penalty).

View file

@ -145,9 +145,24 @@ static func roll_fauna_drops(
turn_seed: int,
killer_id: int,
victim_id: int,
col: int = -1,
row: int = -1,
deplete_amount: float = 1.0,
) -> void:
if killer_player == null:
return
# p3-19 player→ecology feedback: killing fauna reduces its LIVE population on
# the tile (logic in mc_ecology via GdFaunaEcology.deplete_species), so
# sustained hunting drives local extinction; the engine's growth/emergence
# recover abundance once the pressure eases. Fires for any valid fauna kill
# with a known tile, independent of whether loot drops.
if col >= 0 and row >= 0:
var tree: SceneTree = Engine.get_main_loop() as SceneTree
var eco_state: Node = tree.root.get_node_or_null("/root/EcologyState") if tree else null
if eco_state != null:
var eco: RefCounted = eco_state.get("fauna_ecology") as RefCounted
if eco != null and eco.has_method("deplete_species"):
eco.deplete_species(col, row, victim_species_id, deplete_amount)
var fauna: Dictionary = _lookup_fauna(victim_species_id)
if fauna.is_empty():
return

View file

@ -682,6 +682,23 @@ impl IRefCounted for GdFaunaEcology {
#[godot_api]
impl GdFaunaEcology {
/// p3-19 — apply player kill/harvest pressure to a tile's live population of
/// `species_id` (string id, e.g. `"grey_wolf"`), reducing it by `amount`.
/// Resolves the string id → numeric via the species library and returns the
/// post-depletion population (`0.0` when the species/tile is absent). The
/// GDScript fauna-kill + intensive-harvest handlers call this so over-harvest
/// drives local extinction; the engine's growth/emergence recover it once
/// pressure eases. Source of truth stays in `mc_ecology` (Rail 1).
#[func]
fn deplete_species(&mut self, col: i32, row: i32, species_id: GString, amount: f64) -> f64 {
let sid = species_id.to_string();
let Some(numeric) = self.inner.species_library.get(&sid).map(|s| s.id) else {
return 0.0;
};
self.inner
.deplete_population(col, row, numeric, amount as f32) as f64
}
/// Compute fauna-derived luxury supply for a player.
///
/// `player_owned_tiles_json` — JSON array of `[col, row]` pairs covering