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"))