feat(@projects/@magic-civilization): add ambient encounter signals

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-06 23:31:35 -07:00
parent 0cb76bfb35
commit a0ab149d10
2 changed files with 39 additions and 0 deletions

View file

@ -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.

View file

@ -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::<f64>() as f32; }
if let Some(v) = dict.get("aerosol_mitigation") { tile.aerosol_mitigation = v.to::<f64>() as f32; }
if let Some(v) = dict.get("resource_id") { tile.resource_id = v.to::<GString>().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::<f64>() as f32; }
if let Some(v) = dict.get("ecosystem_tier") { tile.ecosystem_tier = v.to::<i64>() as i32; }
if let Some(v) = dict.get("fauna_index") {
let arr = v.to::<Array<GString>>();
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<Dictionary> = 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).