diff --git a/src/game/engine/src/autoloads/event_bus.gd b/src/game/engine/src/autoloads/event_bus.gd index c598d454..b7c33205 100644 --- a/src/game/engine/src/autoloads/event_bus.gd +++ b/src/game/engine/src/autoloads/event_bus.gd @@ -226,6 +226,10 @@ signal marine_creature_spawned(tile_pos: Vector2i, creature_type: String) signal marine_creature_cleared(tile_pos: Vector2i, creature_type: String) # -- Ecology signals -- +## Ambient encounter rolled during unit movement through a tile with +## non-zero fauna_density. Fired by the GDExtension bridge after +## AmbientEncounterFired appears in TurnResult.events_emitted. (p2-58b) +signal ambient_encounter_fired(unit_id: int, tile_pos: Vector2i, species_id: String) ## A lair's habitat degraded below threshold for too long — converted to ruin. signal lair_abandoned(pos: Vector2i) ## A lair's habitat is thriving — creatures quality-up faster. diff --git a/src/simulator/api-gdext/src/lib.rs b/src/simulator/api-gdext/src/lib.rs index f9f25f58..9c53793c 100644 --- a/src/simulator/api-gdext/src/lib.rs +++ b/src/simulator/api-gdext/src/lib.rs @@ -915,6 +915,15 @@ fn dict_to_tile(dict: &Dictionary, tile: &mut mc_core::grid::TileState) { if let Some(v) = dict.get("habitat_suitability") { tile.habitat_suitability = v.to::() as f32; } if let Some(v) = dict.get("aerosol_mitigation") { tile.aerosol_mitigation = v.to::() as f32; } if let Some(v) = dict.get("resource_id") { tile.resource_id = v.to::().to_string(); } + // p2-58b: ambient encounter fields — settable from GDScript for test scenarios. + if let Some(v) = dict.get("fauna_density") { tile.fauna_density = v.to::() as f32; } + if let Some(v) = dict.get("ecosystem_tier") { tile.ecosystem_tier = v.to::() as i32; } + if let Some(v) = dict.get("fauna_index") { + let arr = v.to::>(); + tile.fauna_index = arr.iter_shared() + .map(|s| mc_core::ids::SpeciesId::new(s.to_string())) + .collect(); + } } /// Create a TileState from a Dictionary (for spec eval functions that need a full tile). @@ -3861,6 +3870,32 @@ fn turn_result_to_dict(result: &mc_turn::TurnResult, post_turn: u32) -> Dictiona d.set("t7_t10_deaths", t7_t10_deaths); d.set("fauna_combat_log", log); + // p2-58b: ambient encounter surface. Count AmbientEncounterFired events + // from events_emitted and expose as `ambient_encounter_count` plus a + // lightweight `ambient_encounters` array for GDScript dispatch. + let mut ambient_enc: Array = Array::new(); + for ev in &result.events_emitted { + if let mc_turn::TurnEvent::AmbientEncounterFired { + turn: ev_turn, + clan, + hex, + species, + group_size, + } = ev + { + let mut e = Dictionary::new(); + e.set("turn", *ev_turn as i64); + e.set("clan", clan.0 as i64); + e.set("col", hex.q as i64); + e.set("row", hex.r as i64); + e.set("species_id", species.as_str()); + e.set("group_size", *group_size as i64); + ambient_enc.push(&e); + } + } + d.set("ambient_encounter_count", ambient_enc.len() as i64); + d.set("ambient_encounters", ambient_enc); + // p2-55 Wave 2: civilian-capture event surface. Each array is empty when // no events fired this turn (the GDScript chronicle wiring should be a // no-op in that case).