diff --git a/src/simulator/api-gdext/src/lib.rs b/src/simulator/api-gdext/src/lib.rs index b34e93b0..5797e662 100644 --- a/src/simulator/api-gdext/src/lib.rs +++ b/src/simulator/api-gdext/src/lib.rs @@ -1440,6 +1440,78 @@ impl GdCity { out } + /// Query which hybrid merges are available for this city given its current + /// buildings and the player's researched techs. + /// + /// `registry_json` — JSON array of `BuildingDef` objects (the full game + /// pack's building list from DataLoader). Caller passes all buildings; this + /// method filters to hybrids. + /// + /// `researched_techs` — list of tech IDs the player has already completed. + /// + /// Returns an `Array` of `Dictionary` values, one per available merge: + /// ```text + /// { + /// "building_a": String, // first prereq id + /// "building_b": String, // second prereq id + /// "into": String, // hybrid building id + /// "tech_gated": bool, // true if military_synthesis not yet researched + /// } + /// ``` + /// The returned list includes tech-gated merges so the UI can grey them + /// out rather than hide them — the caller decides presentation. + #[func] + fn available_merges( + &self, + registry_json: GString, + researched_techs: PackedStringArray, + ) -> Array { + use mc_city::{BuildingDef, MERGE_GATING_TECH}; + + let defs: Vec = match serde_json::from_str(®istry_json.to_string()) { + Ok(v) => v, + Err(e) => { + godot_error!("GdCity::available_merges parse error: {}", e); + return Array::new(); + } + }; + + // Build fast lookup sets from the city's current buildings. + let owned: std::collections::BTreeSet<&str> = + self.inner.buildings.iter().map(|s| s.as_str()).collect(); + + // Build researched-tech set. + let techs: std::collections::BTreeSet = (0..researched_techs.len()) + .filter_map(|i| researched_techs.get(i).map(|t| t.to_string())) + .collect(); + let tech_gated = !techs.contains(MERGE_GATING_TECH); + + let mut out = Array::new(); + for def in &defs { + // Only hybrid buildings have exactly 2 prereqs. + if def.merge_requires.len() != 2 { + continue; + } + let a = def.merge_requires[0].as_str(); + let b = def.merge_requires[1].as_str(); + // Both prereqs must be present in the city. + if !owned.contains(a) || !owned.contains(b) { + continue; + } + // Irreversibility guard: skip if hybrid already built. + if owned.contains(def.id.as_str()) { + continue; + } + let mut entry = Dictionary::new(); + entry.set("building_a", GString::from(a)); + entry.set("building_b", GString::from(b)); + entry.set("into", GString::from(def.id.as_str())); + entry.set("tech_gated", tech_gated); + out.push(&entry.to_variant()); + } + out + } + /// Register a building's flat yield bonuses. GDScript passes the /// five per-turn bonuses parsed from the building JSON's `effects` array. /// The city applies these in `get_yields` for each owned building.