From 1433d4adb32e8a8b71bf38fa816f13923f2a2a94 Mon Sep 17 00:00:00 2001 From: autocommit Date: Thu, 30 Apr 2026 07:35:06 -0700 Subject: [PATCH] =?UTF-8?q?feat(combat):=20=E2=9C=A8=20Introduce=20capture?= =?UTF-8?q?=5Fcity()=20and=20mark=5Fplayer=5Feliminated()=20functions=20an?= =?UTF-8?q?d=20update=20Player=20class=20to=20track=20elimination=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- src/game/engine/src/entities/combat_utils.gd | 6 +++++- src/game/engine/src/entities/player.gd | 6 ++++++ src/game/engine/src/modules/combat/combat_utils.gd | 7 ++++++- 3 files changed, 17 insertions(+), 2 deletions(-) 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)