feat(@projects/@magic-civilization): 📋 p3-26 gap 2 — event-config loader (mc-climate::events)
Next brick of the events port: load per-category configs from the canonical JSON
(Rail-2, nothing hardcoded).
- EventCategoryConfig { base_frequency, severity_weights, raw } — typed dispatch inputs +
the full JSON kept in `raw` so each per-category handler reads its own fields (tiers,
target_terrain, becomes, aerosol_strength) without modeling all 12 shapes up front.
- load_event_configs(dir) reads public/resources/events/<category>.json (category =
filename stem; skips *.schema / cross_triggers / events). Test parses the real
wildfire.json (base_frequency 0.04, severity_weights, target_terrain ∋ "forest").
mc-climate events 5/5. Next: dispatch (process_events using category_fires + roll_severity)
+ per-category handlers (wildfire first — burn forest in radius, transform biome) + wire
into the mc-turn climate phase. Tile-picking will use a Rust-deterministic RNG (the
headless sim needs internal determinism, not byte-match with the live game's Godot RNG;
the dispatch GATE already matches GDScript bit-for-bit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ccc7e10ff
commit
4e82b322cb
1 changed files with 93 additions and 0 deletions
|
|
@ -48,6 +48,69 @@ pub fn category_fires(base_frequency: f64, channel: f64, turn_seed: f64) -> bool
|
|||
hash_noise(channel, 0.0, turn_seed) < base_frequency
|
||||
}
|
||||
|
||||
/// One natural-event category's config, loaded from `public/resources/events/<cat>.json`.
|
||||
/// `base_frequency` + `severity_weights` drive the dispatch (typed); `raw` keeps the full
|
||||
/// JSON so each per-category handler can read its specific fields (tiers, target_terrain,
|
||||
/// becomes, aerosol_strength, …) without this struct having to model all 12 shapes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventCategoryConfig {
|
||||
pub base_frequency: f64,
|
||||
pub severity_weights: Vec<i32>,
|
||||
pub raw: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Load per-category event configs from `public/resources/events/<category>.json`. The
|
||||
/// category name is the filename stem. Non-category files (`*.schema`, `cross_triggers`,
|
||||
/// `events`) are skipped, as are files lacking `base_frequency`/`severity_weights`. The
|
||||
/// per-category JSON is the canonical content store (Rail-2); nothing is hardcoded here.
|
||||
pub fn load_event_configs(
|
||||
events_dir: &std::path::Path,
|
||||
) -> std::collections::BTreeMap<String, EventCategoryConfig> {
|
||||
let mut out = std::collections::BTreeMap::new();
|
||||
let entries = match std::fs::read_dir(events_dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return out,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("json") {
|
||||
continue;
|
||||
}
|
||||
let stem = match path.file_stem().and_then(|s| s.to_str()) {
|
||||
Some(s) => s.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
if stem.ends_with(".schema") || stem == "cross_triggers" || stem == "events" {
|
||||
continue;
|
||||
}
|
||||
let text = match std::fs::read_to_string(&path) {
|
||||
Ok(t) => t,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let val: serde_json::Value = match serde_json::from_str(&text) {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let base_frequency = val.get("base_frequency").and_then(|v| v.as_f64());
|
||||
let weights = val.get("severity_weights").and_then(|v| v.as_array());
|
||||
if let (Some(bf), Some(w)) = (base_frequency, weights) {
|
||||
let severity_weights = w
|
||||
.iter()
|
||||
.filter_map(|x| x.as_i64().map(|n| n as i32))
|
||||
.collect();
|
||||
out.insert(
|
||||
stem,
|
||||
EventCategoryConfig {
|
||||
base_frequency: bf,
|
||||
severity_weights,
|
||||
raw: val,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -95,4 +158,34 @@ mod tests {
|
|||
assert!(category_fires(1.0, 10.0, 1.0));
|
||||
assert!(!category_fires(0.0, 10.0, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_event_configs_parses_real_categories() {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.ancestors()
|
||||
.nth(2)
|
||||
.expect("workspace root")
|
||||
.join("public/resources/events");
|
||||
let cfgs = load_event_configs(&dir);
|
||||
assert!(!cfgs.is_empty(), "should load event categories from {dir:?}");
|
||||
let wf = cfgs.get("wildfire").expect("wildfire config present");
|
||||
assert!(
|
||||
(wf.base_frequency - 0.04).abs() < 1e-9,
|
||||
"wildfire base_frequency = {}",
|
||||
wf.base_frequency
|
||||
);
|
||||
assert!(!wf.severity_weights.is_empty(), "severity_weights parsed");
|
||||
let tt = wf
|
||||
.raw
|
||||
.get("target_terrain")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("wildfire target_terrain in raw");
|
||||
assert!(
|
||||
tt.iter().any(|t| t.as_str() == Some("forest")),
|
||||
"wildfire targets forest"
|
||||
);
|
||||
// Non-category files are not loaded as categories.
|
||||
assert!(!cfgs.contains_key("cross_triggers"));
|
||||
assert!(!cfgs.contains_key("events"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue