diff --git a/src/game/engine/src/entities/combat_utils.gd b/src/game/engine/src/entities/combat_utils.gd index a6b56d18..81816042 100644 --- a/src/game/engine/src/entities/combat_utils.gd +++ b/src/game/engine/src/entities/combat_utils.gd @@ -117,6 +117,7 @@ static func capture_city( city.owner = attacker.owner city.is_capital = false + city.captured_turn = GameState.turn_number for tile_pos: Vector2i in city.owned_tiles: var layer: Dictionary = GameState.get_primary_layer() @@ -134,7 +135,10 @@ static func capture_city( EventBus.city_captured.emit(city, old_owner, attacker.owner) - if old_player != null and old_player.cities.is_empty(): + if old_player != null and old_player.cities.is_empty() and not old_player.is_eliminated: + # Latch dedupes against victory_manager._reconcile_eliminations, + # which sweeps the same condition each turn. + old_player.is_eliminated = true EventBus.player_eliminated.emit(old_owner) diff --git a/src/game/engine/src/entities/player.gd b/src/game/engine/src/entities/player.gd index beca1efb..681d96ea 100644 --- a/src/game/engine/src/entities/player.gd +++ b/src/game/engine/src/entities/player.gd @@ -30,6 +30,12 @@ var race_id: String = "" var gender_preset: String = "male" ## True for the local human player; AI otherwise. var is_human: bool = true +## True after this player has been eliminated (no cities AND no living +## founder unit). Set by `victory_manager._reconcile_eliminations` on the +## first turn the transition is observed; latches forever (eliminated +## players cannot recover under current rules). Used to ensure +## `EventBus.player_eliminated` fires exactly once per player per game. +var is_eliminated: bool = false ## Assigned UI color; populated by `GameState.add_player`. var color: Color = Color.WHITE diff --git a/src/game/engine/src/modules/combat/combat_utils.gd b/src/game/engine/src/modules/combat/combat_utils.gd index 66602f84..5ec70a53 100644 --- a/src/game/engine/src/modules/combat/combat_utils.gd +++ b/src/game/engine/src/modules/combat/combat_utils.gd @@ -136,7 +136,12 @@ static func capture_city( EventBus.city_captured.emit(city, old_owner, attacker.owner) - if old_player != null and old_player.cities.is_empty(): + if old_player != null and old_player.cities.is_empty() and not old_player.is_eliminated: + # Latch dedupes against victory_manager._reconcile_eliminations, + # which sweeps the same condition each turn. Either path may fire + # first (combat for instant-kill, reconciliation for end-of-turn + # starvation etc.); whichever wins, the other is silent. + old_player.is_eliminated = true EventBus.player_eliminated.emit(old_owner)