feat(@projects/@magic-civilization): 🌋 p3-26 gap 2 — volcanic eruption category
Third (and flagship) natural-event category, ported from GDScript process_volcanic: apply_volcanic — center → volcano (quality 1), scorch disk turns non-water tiles to scorched_terrain (desert, drier, quality 1), sulfate_aerosol injected in aerosol_radius (climate physics converts to cooling next tick). dispatch_volcanic resolves the tier config + picks a non-water center. Added to process_events dispatch. (Anchor/resource spawns are magic → Game-3 deferred.) Test: apply_volcanic_erupts_scorches_and_injects_aerosol (volcano center, desert scorch, water spared, aerosol injected). mc-climate events 9/9. 3/12 categories live (wildfire, drought, volcanic). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
750824fbbb
commit
8fd906241c
1 changed files with 103 additions and 1 deletions
|
|
@ -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<FiredEvent> {
|
||||
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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue