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
This commit is contained in:
AmanorsElliot
2026-06-22 19:24:12 +00:00
committed by evanpelle
parent 5a097317cb
commit 170f506200
3 changed files with 32 additions and 5 deletions
+9 -2
View File
@@ -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<Unit, number>();
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;
+6 -1
View File
@@ -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;
+17 -2
View File
@@ -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 () => {