Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 9 additions & 16 deletions src/engine/systems/GameStateSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,16 +204,13 @@ export class GameStateSystem extends System {
// Skip victory conditions in battle simulator mode
if (isBattleSimulatorMode()) return;

const players = new Set<string>();
// Use the stable set of game participants (from game setup), not current entities.
// Entity-based counting is unreliable: buildings are immediately destroyed on death,
// so eliminated players with no surviving units would disappear from entity queries,
// causing players.size to drop to 1 and preventing victory from ever being declared.
const players = new Set<string>(this.playerTeams.keys());
const playersWithBuildings = new Set<string>();

// Collect all players
const allEntities = this.world.getEntitiesWith('Selectable');
for (const entity of allEntities) {
const selectable = entity.get<Selectable>('Selectable')!;
players.add(selectable.playerId);
}

// Check which players still have complete buildings (not blueprints)
const buildings = this.world.getEntitiesWith('Building', 'Selectable', 'Health');
for (const entity of buildings) {
Expand Down Expand Up @@ -290,14 +287,10 @@ export class GameStateSystem extends System {
}

private handleSurrender(playerId: string): void {
const players = new Set<string>();
const allEntities = this.world.getEntitiesWith('Selectable');
for (const entity of allEntities) {
const selectable = entity.get<Selectable>('Selectable')!;
players.add(selectable.playerId);
}

const remainingPlayers = [...players].filter((p) => p !== playerId);
const players = new Set<string>(this.playerTeams.keys());
const remainingPlayers = [...players].filter(
(p) => p !== playerId && !this.eliminatedPlayers.has(p)
);
if (remainingPlayers.length === 1) {
this.declareVictory(remainingPlayers[0], playerId, 'surrender');
}
Expand Down
15 changes: 15 additions & 0 deletions src/engine/systems/ai/AICoordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ export class AICoordinator extends System {
/** Reactive defense tick tracking per player */
private lastReactiveDefenseTick: Map<string, number> = new Map();

/** Players eliminated from the game (lost all buildings) - stop AI operations */
private eliminatedPlayers: Set<string> = new Set();

// Subsystems
private economyManager: AIEconomyManager;
private buildOrderExecutor: AIBuildOrderExecutor;
Expand Down Expand Up @@ -400,6 +403,14 @@ export class AICoordinator extends System {
}
);

// Stop AI operations for eliminated players
this.game.eventBus.on('game:playerEliminated', (data: { playerId: string }) => {
if (this.aiPlayers.has(data.playerId)) {
this.eliminatedPlayers.add(data.playerId);
debugAI.log(`[AICoordinator] ${data.playerId} eliminated - AI operations stopped`);
}
});

// Track research completion
this.game.eventBus.on(
'research:complete',
Expand Down Expand Up @@ -1236,6 +1247,7 @@ export class AICoordinator extends System {
// This ensures defense response isn't gated by the 40-tick medium delay
if (currentTick % 10 === 0) {
for (const [, ai] of this.aiPlayers) {
if (this.eliminatedPlayers.has(ai.playerId)) continue;
if (this.tacticsManager.isUnderAttack(ai)) {
if (ai.state === 'attacking' && ai.activeAttackOperation) {
// During active attacks, trigger reactive defense for home units
Expand All @@ -1250,6 +1262,9 @@ export class AICoordinator extends System {
}

for (const [playerId, ai] of this.aiPlayers) {
// Skip eliminated players - their units become inert
if (this.eliminatedPlayers.has(playerId)) continue;

const actionDelay = this.getActionDelay(ai.difficulty);
if (currentTick - ai.lastActionTick < actionDelay) continue;

Expand Down
Loading
Loading