diff --git a/src/game/engine/src/modules/victory/victory_manager.gd b/src/game/engine/src/modules/victory/victory_manager.gd index 223effb7..921546fd 100644 --- a/src/game/engine/src/modules/victory/victory_manager.gd +++ b/src/game/engine/src/modules/victory/victory_manager.gd @@ -11,6 +11,7 @@ extends RefCounted const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd") const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd") +const CityScript: GDScript = preload("res://engine/src/entities/city.gd") ## Minimum turn before any victory check fires. Without this, the very first ## skirmish on turn 1 or 2 — when one side founds before the other or one @@ -37,13 +38,49 @@ func check_all(_game_map: RefCounted) -> void: return -## Domination: the last player with at least one city wins. +## Domination: a player owns every opponent's original capital, OR is the +## last player with at least one city (elimination fallback). ## -## A player without cities is still considered alive while they hold any -## founder unit, since they can still settle. This prevents the check from -## firing on the very first turn that one side founds before the other has -## had a chance — a "haven't finished setting up" state, not a real victory. +## The capital-based rule is classic Civ5 domination: founding-owners lose +## the game-ending stake the moment their starting city is conquered, even +## if they scramble to settle replacement cities elsewhere. Without this, +## a defender who loses their capital can re-found forever and no domination +## victory can ever fire within realistic turn limits. +## +## Fallback to elimination (last-player-with-cities) covers edge cases like +## a 1v1 where neither side ever flipped a capital but one side was wiped. +## The grace-period founder check still applies to the elimination rung. func _check_domination() -> int: + # Gather every original-capital location and the index of the player + # that originally founded it. Populated from live cities on the map — + # if a founder was razed along with their capital (no city at the old + # position), that player's capital requirement collapses automatically. + var capital_owner_by_player: Dictionary = {} # player_index → current city.owner + for player: Variant in GameState.players: + if not player is PlayerScript: + continue + for city: Variant in player.cities: + if city is CityScript and (city as CityScript).original_capital_owner >= 0: + var orig: int = (city as CityScript).original_capital_owner + capital_owner_by_player[orig] = (city as CityScript).owner + + if capital_owner_by_player.size() >= 2: + # Evaluate each candidate: do they own every other tracked capital? + for candidate: Variant in GameState.players: + if not candidate is PlayerScript: + continue + var idx: int = candidate.index + var owns_all_other_capitals: bool = true + for orig_player: Variant in capital_owner_by_player.keys(): + if int(orig_player) == idx: + continue + if int(capital_owner_by_player[orig_player]) != idx: + owns_all_other_capitals = false + break + if owns_all_other_capitals and candidate.cities.size() > 0: + return idx + + # Elimination fallback — the original rule. var alive_players: Array[int] = [] for player: Variant in GameState.players: if not player is PlayerScript: