diff --git a/src/simulator/api-gdext/src/lib.rs b/src/simulator/api-gdext/src/lib.rs index 86d096f5..2ec6d5d5 100644 --- a/src/simulator/api-gdext/src/lib.rs +++ b/src/simulator/api-gdext/src/lib.rs @@ -8423,6 +8423,91 @@ impl GdWorldSim { n as i64 } + /// Load the three event-threshold configs from their canonical JSON pack + /// text (Rail 2). Each argument is the full file text; an empty string + /// keeps the current (Default) thresholds for that channel. Geological and + /// biological have authored JSONs (`geological_thresholds.json`, + /// `biological_events.json`); anomalous has none yet, so pass `""` to keep + /// Default. Returns `false` if any non-empty arg failed to parse (the + /// corresponding channel is left unchanged). + /// + /// The live loop loads the production-rate JSONs (rare events). Proof scenes + /// / tests pass boosted-threshold JSON so events fire reliably in N turns. + #[func] + fn load_thresholds_from_json( + &mut self, + geo_json: GString, + bio_json: GString, + anomalous_json: GString, + ) -> bool { + let mut ok = true; + let mut load = |text: GString, apply: &mut dyn FnMut(&serde_json::Value)| { + let s = text.to_string(); + if s.is_empty() { + return; + } + match serde_json::from_str::(&s) { + Ok(v) => apply(&v), + Err(e) => { + godot_error!("GdWorldSim::load_thresholds_from_json parse error: {e}"); + ok = false; + } + } + }; + load(geo_json, &mut |v| { + self.geo_thresholds = mc_mapgen::events::GeologicalThresholds::from_spec(v); + }); + load(bio_json, &mut |v| { + self.bio_thresholds = mc_ecology::biological::BiologicalThresholds::from_spec(v); + }); + load(anomalous_json, &mut |v| { + self.anomalous_thresholds = mc_climate::anomalous::AnomalousThresholds::from_spec(v); + }); + ok + } + + /// Run one continuous-worldsim event pass against the LIVE grid for the + /// playable turn loop (`turn_manager.gd`), which holds a `GdGridState` + /// (the shared climate/ecology grid) rather than a `GdGameState`. + /// + /// Read-modify-WRITE: the live `GridState` is moved into a temporary + /// `GameState` shell, `dispatch_world_events` mutates it (eco-damage into + /// the owned `eco_map`, plus grid-side effects — `bloom_streak` + /// accumulation, geological elevation/movement deltas), then the mutated + /// grid is moved back into the `GdGridState`. This is critical: a + /// clone-and-discard would silently drop the grid mutations (bloom would + /// never accumulate, geological terrain effects would vanish) while + /// `eco_map` still grew — a false-positive. + /// + /// The temporary `fog_map` is discarded (the live loop has no Rust-side + /// fog state to write back to — fog is GDScript-rendered). FogBank events + /// therefore have no in-game effect yet; wiring fog write-back is a + /// follow-up. Returns the number of events dispatched. + #[func] + fn dispatch_on_grid(&mut self, mut grid: Gd, turn: i64, seed: i64) -> i64 { + let mut bound = grid.bind_mut(); + // Move the live grid out via mem::replace with an empty 0×0 grid, run + // dispatch against a GameState shell, then move the mutated grid back. + let taken = std::mem::replace(&mut bound.inner, mc_core::grid::GridState::new(0, 0)); + let mut shell = mc_state::game_state::GameState::default(); + shell.grid = Some(taken); + shell.turn = turn.max(0) as u32; + let n = mc_worldsim::dispatch_world_events( + &mut shell, + seed as u64, + &self.geo_thresholds, + &self.bio_thresholds, + &self.anomalous_thresholds, + &mut self.eco_map, + &mut self.chronicle, + ); + // Write the mutated grid back into the live GdGridState. + if let Some(g) = shell.grid.take() { + bound.inner = g; + } + n as i64 + } + /// Total number of world-event entries accumulated in the chronicle since /// construction (or last restore). Cheap progress probe for tests / HUD. #[func]