diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index bb348cdc5..66f5c6a57 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -30,9 +30,14 @@ export class PlayerExecution implements Execution { this.player.units().forEach((u) => { const tileOwner = this.mg!.owner(u.tile()); if (u.info().territoryBound) { - if (tileOwner.isPlayer()) { + if (tileOwner?.isPlayer()) { if (tileOwner !== this.player) { - this.mg!.player(tileOwner.id()).captureUnit(u); + if (u.type() === UnitType.DefensePost) { + this.mg!.player(tileOwner.id()).captureUnit(u); + u.decreaseLevel(this.mg!.player(tileOwner.id())); + } else { + this.mg!.player(tileOwner.id()).captureUnit(u); + } } } else { u.delete(); diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 03c828fcc..db48e0a2f 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -494,6 +494,7 @@ export interface Unit { // Upgradable Structures level(): number; increaseLevel(): void; + decreaseLevel(destroyer?: Player): void; // Warships setPatrolTile(tile: TileRef): void; diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 899e56d79..fc1eab97a 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -397,6 +397,18 @@ export class UnitImpl implements Unit { this.mg.addUpdate(this.toUpdate()); } + decreaseLevel(destroyer?: Player): void { + this._level--; + if ([UnitType.MissileSilo, UnitType.SAMLauncher].includes(this.type())) { + this._missileTimerQueue.pop(); + } + if (this._level <= 0) { + this.delete(true, destroyer); + return; + } + this.mg.addUpdate(this.toUpdate()); + } + trainType(): TrainType | undefined { return this._trainType; } diff --git a/tests/core/executions/PlayerExecution.test.ts b/tests/core/executions/PlayerExecution.test.ts new file mode 100644 index 000000000..bbb74b32b --- /dev/null +++ b/tests/core/executions/PlayerExecution.test.ts @@ -0,0 +1,95 @@ +import { PlayerExecution } from "../../../src/core/execution/PlayerExecution"; +import { + Game, + Player, + PlayerInfo, + PlayerType, + UnitType, +} from "../../../src/core/game/Game"; +import { setup } from "../../util/Setup"; +import { executeTicks } from "../../util/utils"; + +let game: Game; +let player: Player; +let otherPlayer: Player; + +describe("PlayerExecution", () => { + beforeEach(async () => { + game = await setup( + "big_plains", + { + infiniteGold: true, + instantBuild: true, + }, + [ + new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id"), + new PlayerInfo("other", PlayerType.Human, "client_id2", "other_id"), + ], + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + player = game.player("player_id"); + otherPlayer = game.player("other_id"); + + game.addExecution(new PlayerExecution(player)); + game.addExecution(new PlayerExecution(otherPlayer)); + }); + + test("DefensePost lv. 1 is destroyed when tile owner changes", () => { + const tile = game.ref(50, 50); + player.conquer(tile); + const defensePost = player.buildUnit(UnitType.DefensePost, tile, {}); + + game.executeNextTick(); + expect(game.unitCount(UnitType.DefensePost)).toBe(1); + expect(defensePost.level()).toBe(1); + + otherPlayer.conquer(tile); + executeTicks(game, 2); + + expect(game.unitCount(UnitType.DefensePost)).toBe(0); + }); + + test("DefensePost lv. 2+ is downgraded when tile owner changes", () => { + const tile = game.ref(50, 50); + player.conquer(tile); + const defensePost = player.buildUnit(UnitType.DefensePost, tile, {}); + defensePost.increaseLevel(); + + expect(defensePost.level()).toBe(2); + expect(game.unitCount(UnitType.DefensePost)).toBe(2); // unitCount sums levels + expect(player.units(UnitType.DefensePost)).toHaveLength(1); + expect(defensePost.isActive()).toBe(true); + + otherPlayer.conquer(tile); + executeTicks(game, 2); + + expect(defensePost.level()).toBe(1); + expect(game.unitCount(UnitType.DefensePost)).toBe(1); + expect(otherPlayer.units(UnitType.DefensePost)).toHaveLength(1); + expect(defensePost.owner()).toBe(otherPlayer); + expect(defensePost.isActive()).toBe(true); + }); + + test("Non-DefensePost structures are transferred (not downgraded) when tile owner changes", () => { + const tile = game.ref(50, 50); + player.conquer(tile); + const city = player.buildUnit(UnitType.City, tile, {}); + + expect(game.unitCount(UnitType.City)).toBe(1); + expect(city.level()).toBe(1); + expect(city.owner()).toBe(player); + expect(city.isActive()).toBe(true); + + otherPlayer.conquer(tile); + executeTicks(game, 2); + + expect(game.unitCount(UnitType.City)).toBe(1); + expect(city.level()).toBe(1); + expect(city.owner()).toBe(otherPlayer); + expect(city.isActive()).toBe(true); + }); +});