diff --git a/src/game/engine/src/modules/ai/simple_heuristic_ai.gd b/src/game/engine/src/modules/ai/simple_heuristic_ai.gd index aaa59985..b13c6dad 100644 --- a/src/game/engine/src/modules/ai/simple_heuristic_ai.gd +++ b/src/game/engine/src/modules/ai/simple_heuristic_ai.gd @@ -368,7 +368,7 @@ static func _decide_production( var city_count: int = player.cities.size() # Priority 1: Build walls if city has none (defense first) - if not city.has_building("walls"): + if not city.has_building("walls") and city.can_build("walls", player): var wdata: Dictionary = DataLoader.get_building("walls") if not wdata.is_empty(): return _prod_building(city_index, "walls") @@ -376,41 +376,55 @@ static func _decide_production( # Priority 2: Happiness building when unhappy if player.happiness < 0: var hb_id: String = _pick_happiness_building_id(city, player) - if not hb_id.is_empty(): + if not hb_id.is_empty() and city.can_build(hb_id, player): return _prod_building(city_index, hb_id) # Priority 3: Expand — build founder if fewer than 3 cities and none in progress - if city_count < 3 and founder_count == 0 and city_index == 0: + if ( + city_count < 3 + and founder_count == 0 + and city_index == 0 + and city.can_build("founder", player) + ): return _prod_unit(city_index, "founder") # Priority 4: Military — maintain 2 warriors per city var want_military: bool = military_count < maxi(2, city_count * 2) if want_military: - var unit_id: String = _pick_military_unit_id() + var unit_id: String = _pick_buildable_military_unit_id(city, player) if not unit_id.is_empty(): return _prod_unit(city_index, unit_id) # Priority 5: Production building (forge boosts future output) - if not city.has_building("forge"): + if not city.has_building("forge") and city.can_build("forge", player): var fdata: Dictionary = DataLoader.get_building("forge") if not fdata.is_empty(): return _prod_building(city_index, "forge") # Priority 6: Castle (upgrades walls, enables bombard) - if city.has_building("walls") and not city.has_building("castle"): + if ( + city.has_building("walls") + and not city.has_building("castle") + and city.can_build("castle", player) + ): var cdata: Dictionary = DataLoader.get_building("castle") if not cdata.is_empty(): return _prod_building(city_index, "castle") # Priority 7: Any other available building - var building_id: String = _pick_building_id(city) + var building_id: String = _pick_building_id(city, player) if not building_id.is_empty(): return _prod_building(city_index, building_id) - # Fallback: more military - var fallback_unit: String = _pick_military_unit_id() + # Fallback: any buildable military unit (warrior is ungated by default). + var fallback_unit: String = _pick_buildable_military_unit_id(city, player) if not fallback_unit.is_empty(): return _prod_unit(city_index, fallback_unit) + # Last-resort fallback: worker (ungated) — keeps production flowing so + # the city never stalls on an empty queue when exotic data states hide + # every combat unit behind tech gates. + if city.can_build("worker", player): + return _prod_unit(city_index, "worker") return {} @@ -449,14 +463,28 @@ static func _pick_next_tech(player: Variant) -> String: return best_id -static func _pick_military_unit_id() -> String: - var preferred: String = "warrior" - var data: Dictionary = DataLoader.get_unit(preferred) - if not data.is_empty(): - return preferred +static func _pick_buildable_military_unit_id( + city: RefCounted, player: RefCounted +) -> String: + ## Pick the first military unit the city is allowed to build, preferring + ## warrior (ungated baseline). Walks the priority list and falls back to + ## any melee unit whose tech/race/school gates the player already clears. + ## Returns "" only if every combat unit is gated (extremely unlikely in + ## Age of Dwarves, where warrior has no requirements). + var priority: Array[String] = [ + "warrior", "spearmen", "pikeman", "archer", "cavalry", + ] + for unit_id: String in priority: + if DataLoader.get_unit(unit_id).is_empty(): + continue + if city.can_build(unit_id, player): + return unit_id for u: Dictionary in DataLoader.get_all_units(): - if String(u.get("combat_type", "")) == "melee": - return String(u.get("id", "")) + if String(u.get("combat_type", "")) != "melee": + continue + var uid: String = String(u.get("id", "")) + if not uid.is_empty() and city.can_build(uid, player): + return uid return "" @@ -470,7 +498,9 @@ static func _pick_happiness_building_id( var bid: String = str(b.get("id", "")) if bid.is_empty() or bid in existing: continue - if not _can_build(b, player): + if b.get("wonder_type") != null: + continue + if not city.can_build(bid, player): continue var happiness_value: int = _sum_effect(b, "happiness") if happiness_value > best_happiness: @@ -479,18 +509,6 @@ static func _pick_happiness_building_id( return best_id -static func _can_build(building_data: Dictionary, player: RefCounted) -> bool: - if building_data.get("wonder_type") != null: - return false - var tech_req: String = str(building_data.get("tech_required", "")) - if not tech_req.is_empty() and not player.has_tech(tech_req): - return false - var culture_req: String = str(building_data.get("culture_required", "")) - if not culture_req.is_empty(): - return false - return true - - static func _sum_effect(building_data: Dictionary, effect_type: String) -> int: var total: int = 0 var effects: Array = building_data.get("effects", []) as Array @@ -503,7 +521,7 @@ static func _sum_effect(building_data: Dictionary, effect_type: String) -> int: return total -static func _pick_building_id(city: RefCounted) -> String: +static func _pick_building_id(city: RefCounted, player: RefCounted) -> String: var existing: Array = Array(city.buildings) # Skip buildings handled by priority logic var skip: Array = ["walls", "forge", "castle"] @@ -511,10 +529,10 @@ static func _pick_building_id(city: RefCounted) -> String: var bid: String = str(b.get("id", "")) if bid.is_empty() or bid in existing or bid in skip: continue - if not str(b.get("tech_required", "")).is_empty(): - continue if b.get("wonder_type") != null: continue + if not city.can_build(bid, player): + continue return bid return ""