feat(@projects/@magic-civilization): ☀️🧊 p3-26 B8 — solar + glacial event categories (9/12)
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) <noreply@anthropic.com>
This commit is contained in:
parent
245a0af95a
commit
8022722a33
1 changed files with 135 additions and 1 deletions
|
|
@ -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<FiredEvent> {
|
||||
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<FiredEvent> {
|
||||
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::*;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue