feat(ai): Introduce emergency garrison logic for undefended cities in SimpleHeuristicAI to enhance tactical defense under threat

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-16 12:21:23 -07:00
parent d1a681f963
commit f16d94a8f1

View file

@ -96,6 +96,21 @@ static func process_player(player: RefCounted) -> Array:
var bombard: Dictionary = _decide_city_bombard(ci, city, player)
if not bombard.is_empty():
actions.append(bombard)
# Emergency garrison: undefended city with enemy within 10 hexes
# preempts the queue with a warrior (mirrors auto_play siege replace).
if mil_now == 0 and _enemy_within(city.position, 10, player.index):
var rush_id: String = _pick_buildable_military_unit_id(city, player)
var head_id: String = (
str(city.production_queue[0].get("id", ""))
if not city.production_queue.is_empty() else ""
)
if not rush_id.is_empty() and head_id != rush_id:
var udata: Dictionary = DataLoader.get_unit(rush_id)
city.production_queue = [{"type": "unit", "id": rush_id,
"cost": int(udata.get("cost", 0))}]
city.production_progress = 0
if not rush_id.is_empty():
continue
if not city.production_queue.is_empty():
continue
var prod: Dictionary = _decide_production(ci, player)
@ -224,6 +239,18 @@ static func _count_own_military_at(
return total
static func _enemy_within(
pos: Vector2i, radius: int, own_idx: int
) -> bool:
var primary: Dictionary = GameState.get_primary_layer()
for u: Variant in primary.get("units", []):
if u == null or not u.is_alive() or int(u.get("owner")) == own_idx:
continue
if HexUtilsScript.hex_distance(pos, u.position) <= radius:
return true
return false
static func _enemy_military_threat(player: RefCounted) -> Dictionary:
## count=in-range(<=8), total_count=all enemy combat. threatens_city @<=5.
var count: int = 0