Fix nation nuke crash when attacker has no remaining tiles 🛡️ (#3703)

## Description:

Fixes a game crash (`Error: array must not be empty`) thrown from
`PseudoRandom.randElement` when a nation tries to pick a nuke target
whose territory no longer exists.

## Root cause

`NationNukeBehavior.findBestNukeTarget` calls
`findIncomingAttackPlayer`, which iterates the player's
`_incomingAttacks`. `AttackImpl` instances can linger in this array past
the point where the attacker has lost all tiles — the attack is only
removed on explicit `delete()` and `removeOnDeath` cleanup isn't
guaranteed to run before other executions tick within the same turn. A
dead attacker gets returned as the nuke target, `randTerritoryTileArray`
samples their (empty) territory, and `randElement` throws on the empty
array.

## Fix

- `Player.incomingAttacks()` now filters out attacks whose attacker is
no longer alive, so consumers can't observe stale references from
mid-tick deaths.
- `randTerritoryTile` guards against `numTilesOwned() === 0` before
falling back to `randElement(p.tiles())` as a defense-in-depth safeguard
at the util level.
- `PlayerImpl.toUpdate()` now uses the `incomingAttacks()` /
`outgoingAttacks()` accessors (rather than the raw `_` arrays) so
serialized client state stays consistent with the server-side view.

`outgoingAttacks()` is intentionally left unfiltered, the engine relies
on seeing in-flight attacks during their retreat phase after a target is
conquered (attack merging/cancellation in `AttackExecution.init`).

### Bug report from discord

Please paste the following in your bug report in Discord:
Game crashed!
game id: LQDSWbh6
client id: JjwysSLN
Error: array must not be empty
Message: Error: array must not be empty
at sa.randElement
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:28:39166)
at H1
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:116429)
at Jc
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:116135)
at G1.maybeSendNuke
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:117597)
at n5.tick
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:171764)
at https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:251463
    at Array.forEach (<anonymous>)
at c.executeNextTick
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:251383)
at b.executeNextTick
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:271256)
at S_
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:366356)

## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] 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
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

FloPinguin
This commit is contained in:
FloPinguin
2026-04-18 02:32:49 +02:00
committed by GitHub
parent cfb731595d
commit ae96eb7e98
2 changed files with 4 additions and 4 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ function randTerritoryTile(
}
}
if (p.numTilesOwned() <= 100) {
if (p.numTilesOwned() > 0 && p.numTilesOwned() <= 100) {
return random.randElement(Array.from(p.tiles()));
}
+3 -3
View File
@@ -144,7 +144,7 @@ export class PlayerImpl implements Player {
traitorRemainingTicks: this.getTraitorRemainingTicks(),
targets: this.targets().map((p) => p.smallID()),
outgoingEmojis: this.outgoingEmojis(),
outgoingAttacks: this._outgoingAttacks.map((a) => {
outgoingAttacks: this.outgoingAttacks().map((a) => {
return {
attackerID: a.attacker().smallID(),
targetID: a.target().smallID(),
@@ -153,7 +153,7 @@ export class PlayerImpl implements Player {
retreating: a.retreating(),
} satisfies AttackUpdate;
}),
incomingAttacks: this._incomingAttacks.map((a) => {
incomingAttacks: this.incomingAttacks().map((a) => {
return {
attackerID: a.attacker().smallID(),
targetID: a.target().smallID(),
@@ -1381,7 +1381,7 @@ export class PlayerImpl implements Player {
return this._outgoingAttacks;
}
incomingAttacks(): Attack[] {
return this._incomingAttacks;
return this._incomingAttacks.filter((a) => a.attacker().isAlive());
}
public isImmune(): boolean {