feat(ai): Add AI turn bridge class to coordinate AI components and game state transitions

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-10 19:20:20 -07:00
parent 6c1847a210
commit 5980926b84

View file

@ -13,6 +13,12 @@ extends RefCounted
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
const CombatResolverScript: GDScript = preload(
"res://engine/src/modules/combat/combat_resolver.gd"
)
const CombatUtilsScript: GDScript = preload(
"res://engine/src/modules/combat/combat_utils.gd"
)
## Run all AI decisions for one player, apply them, then return the count
@ -198,8 +204,7 @@ static func _apply_action(action: Dictionary, player: RefCounted) -> bool:
"set_production":
return _apply_set_production(action, player)
"attack":
push_warning("AiTurnBridge: 'attack' action not yet wired — skipping")
return false
return _apply_attack(action, player)
push_warning("AiTurnBridge: unknown action type '%s'" % action_type)
return false
@ -267,6 +272,60 @@ static func _apply_set_production(action: Dictionary, player: RefCounted) -> boo
return true
static func _apply_attack(action: Dictionary, player: RefCounted) -> bool:
## Resolve an AI-initiated attack via the same combat path the human UI uses.
## CombatResolver.resolve() handles HP, death, XP, signals, and capture; we
## only spend the attacker's movement and do basic input validation here.
var idx: int = int(action.get("unit_index", -1))
if idx < 0 or idx >= player.units.size():
return false
var attacker: Unit = player.units[idx] as Unit
if attacker == null or not attacker.is_alive():
return false
if attacker.movement_remaining <= 0:
return false
var target: Vector2i = Vector2i(
int(action.get("target_col", 0)),
int(action.get("target_row", 0)),
)
var primary: Dictionary = GameState.get_primary_layer()
var all_units: Array = primary.get("units", [])
var defender: RefCounted = _find_enemy_at(target, player.index, all_units)
if defender == null:
defender = CombatUtilsScript.get_city_at(target)
if defender == null or defender.owner == player.index:
return false
var game_map: RefCounted = GameState.get_game_map()
if game_map == null:
return false
var resolver: RefCounted = CombatResolverScript.new()
resolver.resolve(attacker, defender, game_map, all_units)
attacker.movement_remaining = 0
return true
static func _find_enemy_at(
pos: Vector2i, attacker_owner: int, all_units: Array
) -> RefCounted:
## Return the first enemy unit standing on `pos`, or null. Mirrors
## world_map_combat.get_enemy_at so AI and human paths agree on targeting.
for unit_ref: Variant in all_units:
if not unit_ref is UnitScript:
continue
if unit_ref.position != pos:
continue
if unit_ref.owner == attacker_owner:
continue
if not unit_ref.is_alive():
continue
return unit_ref
return null
static func _generate_city_name(player: RefCounted) -> String:
var race_data: Dictionary = DataLoader.get_race(player.race_id)
var city_names: Array = race_data.get("city_names", [])