diff --git a/src/simulator/crates/mc-climate/src/events.rs b/src/simulator/crates/mc-climate/src/events.rs index eec0f7c0..ec41c685 100644 --- a/src/simulator/crates/mc-climate/src/events.rs +++ b/src/simulator/crates/mc-climate/src/events.rs @@ -205,7 +205,8 @@ pub fn process_events( let ev = match *category { "wildfire" => dispatch_wildfire(grid, cfg, tier, turn_seed, channel), "drought" => dispatch_drought(grid, cfg, tier, turn_seed, channel), - // TODO(p3-26 gap 2): volcanic/seismic/tsunami/plague/marine/… handlers. + "volcanic" => dispatch_volcanic(grid, cfg, tier, turn_seed, channel), + // TODO(p3-26 gap 2): impact/seismic/tsunami/plague/pandemic/marine/solar/glacial. _ => None, }; if let Some(ev) = ev { @@ -336,6 +337,82 @@ fn dispatch_wildfire( }) } +/// Apply a volcanic eruption: the center becomes a `volcano` (quality 1), a scorch disk +/// turns non-water tiles to `scorched_terrain` (quality 1, drier), and sulfate aerosol is +/// injected in `aerosol_radius` (the climate physics turns this into cooling next tick). +/// Returns tiles affected. Mirrors GDScript `process_volcanic` (anchor/resource spawns are +/// magic → Game-3 deferred). +pub fn apply_volcanic( + grid: &mut mc_core::grid::GridState, + center: (i32, i32), + radius: i32, + scorched_terrain: &str, + moisture_loss: f32, + aerosol_strength: f32, + aerosol_radius: i32, +) -> i32 { + use mc_core::algorithms::hex::{axial_to_offset, hex_spiral, offset_to_axial}; + use mc_core::grid::biome_registry::{has_tag, BiomeTag}; + if let Some(t) = grid.tile_mut(center.0, center.1) { + t.biome_label_id = "volcano".to_string(); + t.quality = 1; + } + let (cq, cr) = offset_to_axial(center.0, center.1); + let mut affected = 1; + for (q, r) in hex_spiral(cq, cr, radius) { + let (col, row) = axial_to_offset(q, r); + if (col, row) == center { + continue; + } + if let Some(t) = grid.tile_mut(col, row) { + if !has_tag(&t.biome_label_id, BiomeTag::IsWater) { + t.biome_label_id = scorched_terrain.to_string(); + t.moisture = (t.moisture - moisture_loss).max(0.0); + t.quality = 1; + affected += 1; + } + } + } + if aerosol_strength > 0.0 && aerosol_radius > 0 { + for (q, r) in hex_spiral(cq, cr, aerosol_radius) { + let (col, row) = axial_to_offset(q, r); + if let Some(t) = grid.tile_mut(col, row) { + t.sulfate_aerosol += aerosol_strength; + } + } + } + affected +} + +/// Resolve the volcanic tier config + pick a non-water center, then erupt. +fn dispatch_volcanic( + grid: &mut mc_core::grid::GridState, + cfg: &EventCategoryConfig, + tier: usize, + turn_seed: f64, + channel: f64, +) -> Option { + use mc_core::grid::biome_registry::{has_tag, BiomeTag}; + let tc = cfg.raw.get("tiers").and_then(|t| t.get(tier.to_string()))?; + let radius = tc.get("radius").and_then(|v| v.as_i64()).unwrap_or(1) as i32; + let scorched = tc + .get("scorched_terrain") + .and_then(|v| v.as_str()) + .unwrap_or("desert") + .to_string(); + let moisture_loss = tc.get("scorched_moisture_loss").and_then(|v| v.as_f64()).unwrap_or(0.05) as f32; + let aero_str = tc.get("aerosol_strength").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32; + let aero_rad = tc.get("aerosol_radius").and_then(|v| v.as_i64()).unwrap_or(0) as i32; + let center = pick_matching_tile(grid, channel, turn_seed, |b| !has_tag(b, BiomeTag::IsWater))?; + let affected = apply_volcanic(grid, center, radius, &scorched, moisture_loss, aero_str, aero_rad); + Some(FiredEvent { + category: "volcanic".to_string(), + tier, + center, + affected, + }) +} + #[cfg(test)] mod tests { use super::*; @@ -451,6 +528,31 @@ mod tests { assert_eq!(fired, fired2, "dispatch must be deterministic"); } + #[test] + fn apply_volcanic_erupts_scorches_and_injects_aerosol() { + 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; + t.sulfate_aerosol = 0.0; + } + // ocean tile in scorch radius stays water. + if let Some(t) = grid.tile_mut(6, 5) { + t.biome_label_id = "ocean".into(); + } + let affected = apply_volcanic(&mut grid, (5, 5), 2, "desert", 0.1, 0.2, 4); + assert!(affected >= 1); + assert_eq!(grid.tile(5, 5).unwrap().biome_label_id, "volcano", "center → volcano"); + assert_eq!(grid.tile(5, 5).unwrap().quality, 1); + // a land tile in radius scorched to desert + assert_eq!(grid.tile(4, 5).unwrap().biome_label_id, "desert", "scorched → desert"); + assert_eq!(grid.tile(6, 5).unwrap().biome_label_id, "ocean", "water not scorched"); + // aerosol injected near center + assert!(grid.tile(5, 5).unwrap().sulfate_aerosol >= 0.2, "aerosol injected"); + } + #[test] fn apply_drought_dries_land_skips_water() { use mc_core::grid::GridState;