From 170f506200f7091694a302b317096a546ca7eaa7 Mon Sep 17 00:00:00 2001 From: AmanorsElliot Date: Mon, 22 Jun 2026 19:24:12 +0000 Subject: [PATCH] Fix transport ship's troop count to update when a hydro hits the player. (#4381) **Add approved & assigned issue number here:** Resolves #4308 ## Description: When nuclear damage reduces a player's troop count, it also affects any transports ships in the water. This works well and is useful to avoid exploiting tranports to avoid hydro damages. `UnitImpl.setTroops()` changes the transports troop count without queuing a unit update. the core value changes, but the client never receives a fresh UnitUpdate unless something else touches the ship. - UnitImpl.ts now emits a UnitUpdate when a unit troop count actually changes. - NukeExecution.ts batches transport ship nuke losses, then applies one final troop update per ship. - Attack.test.ts now asserts the nuke tick includes a transport UnitUpdate with the reduced troop count. ## Please complete the following: - [N/A] I have added screenshots for all UI updates - [N/A] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: elliotlepley --- src/core/execution/NukeExecution.ts | 11 +++++++++-- src/core/game/UnitImpl.ts | 7 ++++++- tests/Attack.test.ts | 19 +++++++++++++++++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 284724e75..d4eaefcdb 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -375,6 +375,10 @@ export class NukeExecution implements Execution { for (const [player, numImpactedTiles] of tilesPerPlayers) { const tilesBeforeNuke = player.numTilesOwned() + numImpactedTiles; const transportShips = player.units(UnitType.TransportShip); + const transportShipTroops = new Map(); + for (const unit of transportShips) { + transportShipTroops.set(unit, unit.troops()); + } const outgoingAttacks = player.outgoingAttacks(); const maxTroops = config.maxTroops(player); // nukeDeathFactor could compute the complete fallout in a single call instead @@ -400,16 +404,19 @@ export class NukeExecution implements Execution { attack.setTroops(attackTroops - deaths); } for (const unit of transportShips) { - const unitTroops = unit.troops(); + const unitTroops = transportShipTroops.get(unit) ?? unit.troops(); const deaths = config.nukeDeathFactor( this.nukeType, unitTroops, numTilesLeft, maxTroops, ); - unit.setTroops(unitTroops - deaths); + transportShipTroops.set(unit, Math.max(0, unitTroops - deaths)); } } + for (const [unit, troops] of transportShipTroops) { + unit.setTroops(troops); + } } const outer2 = magnitude.outer * magnitude.outer; diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 46b38c7a3..5071d51f0 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -172,7 +172,12 @@ export class UnitImpl implements Unit { } setTroops(troops: number): void { - this._troops = Math.max(0, troops); + const nextTroops = Math.max(0, troops); + if (this._troops === nextTroops) { + return; + } + this._troops = nextTroops; + this.mg.addUpdate(this.toUpdate()); } troops(): number { return this._troops; diff --git a/tests/Attack.test.ts b/tests/Attack.test.ts index da7ec9a2a..781f04704 100644 --- a/tests/Attack.test.ts +++ b/tests/Attack.test.ts @@ -9,6 +9,7 @@ import { UnitType, } from "../src/core/game/Game"; import { TileRef } from "../src/core/game/GameMap"; +import { GameUpdateType, UnitUpdate } from "../src/core/game/GameUpdates"; import { GameID } from "../src/core/Schemas"; import { setup } from "./util/Setup"; import { TestConfig } from "./util/TestConfig"; @@ -119,10 +120,24 @@ describe("Attack", () => { const ship = defender.units(UnitType.TransportShip)[0]; expect(ship.troops()).toBe(100); - game.executeNextTick(); + const updates = game.executeNextTick(); + const updatedShip = defender.units(UnitType.TransportShip)[0]; + const shipUpdates = (updates[GameUpdateType.Unit] as UnitUpdate[]).filter( + (u) => u.id === ship.id(), + ); expect(nuke.isActive()).toBe(false); - expect(defender.units(UnitType.TransportShip)[0].troops()).toBeLessThan(90); + expect(updatedShip.troops()).toBeLessThan(90); + expect(shipUpdates).toContainEqual( + expect.objectContaining({ + id: ship.id(), + unitType: UnitType.TransportShip, + troops: updatedShip.troops(), + transportShipState: expect.objectContaining({ + troops: updatedShip.troops(), + }), + }), + ); }); test("Boat penalty on retreat Transport Ship arrival", async () => {