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); + } }