From 668ab7d152d830e3f7f4d3bbebb30811abe9ab5a Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 26 Jun 2026 19:46:35 -0400 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=A6=A0=20p3-27=20=E2=80=94=20complete=20the=20disease=20a?= =?UTF-8?q?pplier=20(o2=20depletion=20+=20lair=20kill,=20no=20stubs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverses the two deferrals in apply_disease_events — every EventTierData field now applies: - o2_delta: atmospheric (global) depletion of grid.o2_fraction, once per fired event (negative drives toward an anoxic ocean). o2_fraction is grid-level, so it's applied after the radius loop, not per-tile. - lair_kill_chance: an active lair (lair_tier > 0) on an affected tile is wiped (lair_tier=0, lair_population=0 — same clear as evolution's lair removal) with the configured probability via the deterministic rng. Disease now applies fauna_loss + canopy_loss + tier_loss + o2_delta + lair_kill_chance — the full tier spec. mc-ecology events 11/0 (+1 test). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/simulator/crates/mc-ecology/src/engine.rs | 16 ++++++- src/simulator/crates/mc-ecology/src/events.rs | 47 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/simulator/crates/mc-ecology/src/engine.rs b/src/simulator/crates/mc-ecology/src/engine.rs index 3a161c80..67ba53f1 100644 --- a/src/simulator/crates/mc-ecology/src/engine.rs +++ b/src/simulator/crates/mc-ecology/src/engine.rs @@ -1276,14 +1276,26 @@ impl EcologyEngine { if td.canopy_loss > 0.0 { t.canopy_cover = (t.canopy_cover * (1.0 - td.canopy_loss)).max(0.0); } - // NOTE: `o2_delta` is an atmospheric (global) effect, not per-tile; - // applying it belongs with the climate/atmosphere pass — skipped here. if td.tier_loss > 0 { t.ecosystem_tier = (t.ecosystem_tier - td.tier_loss).max(0); } + // lair_kill_chance: an active lair on this tile may be wiped out + // (cleared the same way as evolution's lair removal). + if t.lair_tier > 0 + && td.lair_kill_chance > 0.0 + && rng.next_bool_p(td.lair_kill_chance.clamp(0.0, 1.0)) + { + t.lair_tier = 0; + t.lair_population = 0.0; + } } tiles_hit += 1; } + // o2_delta is an atmospheric (global) effect — applied once per fired event + // (negative = depletion toward an anoxic ocean). + if td.o2_delta != 0.0 { + grid.o2_fraction = (grid.o2_fraction + td.o2_delta).max(0.0); + } fired.push(FiredDisease { category: cat.id.clone(), tier, diff --git a/src/simulator/crates/mc-ecology/src/events.rs b/src/simulator/crates/mc-ecology/src/events.rs index 938cba73..f80ee4f4 100644 --- a/src/simulator/crates/mc-ecology/src/events.rs +++ b/src/simulator/crates/mc-ecology/src/events.rs @@ -348,4 +348,51 @@ mod tests { let mut g = GridState::new(4, 4); assert!(e.apply_disease_events(&mut g, &[], 1, 1).is_empty(), "no categories → no-op"); } + + #[test] + fn apply_disease_events_depletes_o2_and_wipes_lairs() { + use crate::engine::EcologyEngine; + use crate::population::PopulationSlot; + use mc_core::grid::GridState; + + let mut engine = EcologyEngine::new(); + engine + .tile_populations + .insert((5, 5), vec![PopulationSlot::new(1, 100.0)]); + let mut grid = GridState::new(12, 12); + grid.o2_fraction = 0.21; + if let Some(t) = grid.tile_mut(5, 5) { + t.lair_tier = 2; + t.lair_population = 0.9; + } + let mut tiers = HashMap::new(); + tiers.insert( + 1, + EventTierData { + name: "Pandemic".into(), + fauna_loss: 0.0, + canopy_loss: 0.0, + o2_delta: -0.01, + lair_kill_chance: 1.0, // always wipes + radius: 1, + tier_loss: 0, + }, + ); + let cat = EventCategory { + id: "pandemic".into(), + base_frequency: 1.0, + severity_weights: { + let mut w = vec![0.0; 10]; + w[0] = 1.0; + w + }, + density_frequency_bonus: 0.0, + tiers, + }; + let fired = engine.apply_disease_events(&mut grid, std::slice::from_ref(&cat), 1, 7); + assert_eq!(fired.len(), 1); + assert!((grid.o2_fraction - 0.20).abs() < 1e-4, "o2 depleted by o2_delta"); + assert_eq!(grid.tile(5, 5).unwrap().lair_tier, 0, "lair wiped at kill_chance 1.0"); + assert_eq!(grid.tile(5, 5).unwrap().lair_population, 0.0); + } }