From fa93e594252aaee8019e627b40a710186d382895 Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 26 Jun 2026 10:42:29 -0400 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=94=A5=20p3-26=20gap=202=20=E2=80=94=20wildfire=20effect?= =?UTF-8?q?=20(apply=5Fwildfire)=20ported?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first per-category event EFFECT, pure + testable. apply_wildfire burns forest tiles in a hex disk around a center (offset coords via offset_to_axial/hex_spiral/axial_to_offset): transforms matching terrain to `becomes`, drops quality (min 1) + moisture (min 0); returns tiles burned. Mirrors GDScript process_wildfire's per-tile effect. Test: a forest patch + apply_wildfire(radius 2, becomes grassland, quality_loss 2) → forest → grassland, moisture/quality dropped, non-forest tiles untouched. mc-climate events 6/6. Next: the dispatch (process_events — category_fires gate + roll_severity + deterministic center pick → apply_wildfire) + config-on-GameState + wire into the mc-turn climate phase. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/simulator/crates/mc-climate/src/events.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/simulator/crates/mc-climate/src/events.rs b/src/simulator/crates/mc-climate/src/events.rs index 80a19830..71ebfc34 100644 --- a/src/simulator/crates/mc-climate/src/events.rs +++ b/src/simulator/crates/mc-climate/src/events.rs @@ -111,6 +111,40 @@ pub fn load_event_configs( out } +/// Burn the forest tiles within `radius` (hex disk) of `center` (offset col,row): +/// transform matching terrain to `becomes`, drop quality (min 1) + moisture (min 0). +/// Returns the count burned. Pure — the dispatch chooses the center. Mirrors the per-tile +/// effect of GDScript `process_wildfire` (forest-only, radius/loss/`becomes` by tier). +pub fn apply_wildfire( + grid: &mut mc_core::grid::GridState, + center: (i32, i32), + radius: i32, + moisture_loss: f32, + quality_loss: i32, + becomes: Option<&str>, + target_terrain: &[String], +) -> i32 { + use mc_core::algorithms::hex::{axial_to_offset, hex_spiral, offset_to_axial}; + let (cq, cr) = offset_to_axial(center.0, center.1); + let mut burned = 0; + for (q, r) in hex_spiral(cq, cr, radius) { + let (col, row) = axial_to_offset(q, r); + if let Some(t) = grid.tile_mut(col, row) { + if target_terrain.iter().any(|b| b.as_str() == t.biome_label_id) { + if let Some(b) = becomes { + t.biome_label_id = b.to_string(); + } + if quality_loss > 0 { + t.quality = (t.quality - quality_loss).max(1); + } + t.moisture = (t.moisture - moisture_loss).max(0.0); + burned += 1; + } + } + } + burned +} + #[cfg(test)] mod tests { use super::*; @@ -159,6 +193,34 @@ mod tests { assert!(!category_fires(0.0, 10.0, 1.0)); } + #[test] + fn apply_wildfire_burns_forest_in_radius() { + use mc_core::grid::GridState; + let mut grid = GridState::new(12, 12); + for t in &mut grid.tiles { + t.biome_label_id = "grassland".into(); + t.moisture = 0.5; + t.quality = 3; + } + for c in 4..7 { + for r in 4..7 { + if let Some(t) = grid.tile_mut(c, r) { + t.biome_label_id = "forest".into(); + } + } + } + let target = vec!["forest".to_string()]; + let burned = apply_wildfire(&mut grid, (5, 5), 2, 0.15, 2, Some("grassland"), &target); + assert!(burned >= 1, "should burn forest tiles in radius"); + let center = grid.tile(5, 5).unwrap(); + assert_eq!(center.biome_label_id, "grassland", "forest → grassland"); + assert!(center.moisture < 0.5, "moisture dropped"); + assert_eq!(center.quality, 1, "quality 3 - 2 = 1 (floored at 1)"); + // A pre-existing grassland tile (not target terrain) is untouched. + let far = grid.tile(0, 0).unwrap(); + assert_eq!(far.moisture, 0.5, "non-forest tile untouched"); + } + #[test] fn load_event_configs_parses_real_categories() { let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))