From 8022722a334df180ff72dfb22efaac9957800824 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 27 Jun 2026 02:03:02 -0400 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=98=80=EF=B8=8F=F0=9F=A7=8A=20p3-26=20B8=20=E2=80=94=20solar?= =?UTF-8?q?=20+=20glacial=20event=20categories=20(9/12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two climate disaster categories via the existing magic_heat_delta forcing field (which the climate physics adds to temperature each step, then decays — so the effect is persistent + transient like the GDScript duration, no new grid field/physics change): - solar: global warming — every tile's magic_heat_delta += global_heat. - glacial (cold): cools a hex disk (magic_heat_delta += negative temp_delta) + dries it (moisture_loss), skipping open water. (warm-T5 runaway + river-freeze deferred.) apply_solar/apply_glacial + dispatch + match arms + tests. mc-climate 65/0. Events: 9/12 live (wildfire/drought/volcanic/seismic/impact/tsunami/plague/solar/glacial); pandemic/ecological fauna handled by the disease applier; marine via process_step; magical→G3. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/simulator/crates/mc-climate/src/events.rs | 136 +++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/src/simulator/crates/mc-climate/src/events.rs b/src/simulator/crates/mc-climate/src/events.rs index 9e925890..8e45ea21 100644 --- a/src/simulator/crates/mc-climate/src/events.rs +++ b/src/simulator/crates/mc-climate/src/events.rs @@ -210,7 +210,9 @@ pub fn process_events( "impact" => dispatch_impact(grid, cfg, tier, turn_seed, channel), "tsunami" => dispatch_tsunami(grid, cfg, tier, turn_seed, channel), "plague" => dispatch_plague(grid, cfg, tier, turn_seed, channel), - // TODO(p3-26 gap 2): pandemic (fauna+city), marine, solar/glacial. + "solar" => dispatch_solar(grid, cfg, tier, turn_seed, channel), + "glacial" => dispatch_glacial(grid, cfg, tier, turn_seed, channel), + // TODO(p3-26 gap 2): pandemic (fauna+city, via mc-ecology disease) + marine. _ => None, }; if let Some(ev) = ev { @@ -806,6 +808,101 @@ fn dispatch_plague( }) } +/// Apply a solar event: inject `global_heat` into every tile's `magic_heat_delta` +/// (the persistent temperature forcing the climate physics adds each step, then +/// decays). Positive = global warming. Returns tiles affected. Mirrors GDScript +/// `process_solar` (the magic/mana bonus is Game-3 deferred). +pub fn apply_solar(grid: &mut mc_core::grid::GridState, global_heat: f32) -> i32 { + if global_heat == 0.0 { + return 0; + } + for t in &mut grid.tiles { + t.magic_heat_delta += global_heat; + } + grid.tiles.len() as i32 +} + +/// Resolve the solar tier config + apply the global heat forcing. +fn dispatch_solar( + grid: &mut mc_core::grid::GridState, + cfg: &EventCategoryConfig, + tier: usize, + _turn_seed: f64, + _channel: f64, +) -> Option { + let tc = cfg.raw.get("tiers").and_then(|t| t.get(tier.to_string()))?; + let global_heat = tc.get("global_heat").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32; + let affected = apply_solar(grid, global_heat); + if affected == 0 { + return None; + } + Some(FiredEvent { + category: "solar".to_string(), + tier, + center: (0, 0), + affected, + }) +} + +/// Apply a glacial cold event: cool a hex disk via `magic_heat_delta += temp_delta` +/// (temp_delta is negative for cold) and dry it by `moisture_loss`, skipping open +/// water. Returns tiles affected. Mirrors GDScript `process_glacial` cold direction +/// (river-freeze + warm-T5 runaway are deferred refinements). +pub fn apply_glacial( + grid: &mut mc_core::grid::GridState, + center: (i32, i32), + radius: i32, + temp_delta: f32, + moisture_loss: f32, +) -> i32 { + use mc_core::algorithms::hex::{axial_to_offset, hex_spiral, offset_to_axial}; + use mc_core::grid::biome_registry::{has_tag, BiomeTag}; + let (cq, cr) = offset_to_axial(center.0, center.1); + let mut affected = 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 has_tag(&t.biome_label_id, BiomeTag::IsWater) { + continue; + } + t.magic_heat_delta += temp_delta; + t.moisture = (t.moisture - moisture_loss).max(0.0); + affected += 1; + } + } + affected +} + +/// Resolve the glacial (cold) tier config + pick a non-water center, then freeze it. +fn dispatch_glacial( + 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()))?; + // Warm-direction (T5 runaway) is a deferred refinement; only cold freezes here. + if tc.get("direction").and_then(|v| v.as_str()) == Some("warm") { + return None; + } + let radius = tc.get("radius").and_then(|v| v.as_i64()).unwrap_or(3) as i32; + let temp_delta = tc.get("temp_delta").and_then(|v| v.as_f64()).unwrap_or(-0.05) as f32; + let moisture_loss = tc.get("moisture_loss").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32; + let center = pick_matching_tile(grid, channel, turn_seed, |b| !has_tag(b, BiomeTag::IsWater))?; + let affected = apply_glacial(grid, center, radius, temp_delta, moisture_loss); + if affected == 0 { + return None; + } + Some(FiredEvent { + category: "glacial".to_string(), + tier, + center, + affected, + }) +} + #[cfg(test)] mod plague_tests { use super::*; @@ -850,6 +947,43 @@ mod plague_tests { } } +#[cfg(test)] +mod solar_glacial_tests { + use super::*; + use mc_core::grid::GridState; + + #[test] + fn apply_solar_warms_every_tile() { + let mut grid = GridState::new(6, 6); + for t in &mut grid.tiles { + t.magic_heat_delta = 0.0; + } + let affected = apply_solar(&mut grid, 0.02); + assert_eq!(affected, 36, "all tiles warmed"); + assert!((grid.tile(3, 3).unwrap().magic_heat_delta - 0.02).abs() < 1e-4); + } + + #[test] + fn apply_glacial_cools_land_dries_skips_water() { + let mut grid = GridState::new(10, 10); + for t in &mut grid.tiles { + t.biome_label_id = "grassland".into(); + t.magic_heat_delta = 0.0; + t.moisture = 0.5; + } + if let Some(t) = grid.tile_mut(4, 5) { + t.biome_label_id = "ocean".into(); + } + let affected = apply_glacial(&mut grid, (5, 5), 2, -0.08, 0.05); + assert!(affected >= 1); + let c = grid.tile(5, 5).unwrap(); + assert!((c.magic_heat_delta - (-0.08)).abs() < 1e-4, "land cooled (negative forcing)"); + assert!((c.moisture - 0.45).abs() < 1e-4, "land dried"); + let water = grid.tile(4, 5).unwrap(); + assert_eq!(water.magic_heat_delta, 0.0, "open water unaffected"); + } +} + #[cfg(test)] mod tests { use super::*;