Optimize nuke explosion (#3176)

## Description:

When a nuke detonates, the explosion is looping over each tiles. Among
other things, it filters the owner units to find the `TransportShips`
units.

This operation is cpu-intensive and repeated over each tiles can make
the tick noticably slower.

On this screenshot the detonation function took 100ms:
<img width="1617" height="488" alt="image"
src="https://github.com/user-attachments/assets/07009b18-4342-4caf-9e82-9ae5147b63f8"
/>
<img width="1645" height="375" alt="image"
src="https://github.com/user-attachments/assets/fe9ead87-550a-4166-96ab-092d0c08be82"
/>

I suspect it led to a few frame freeze when I MIRVed too.

Suggestion: loop over the impacted players rather than the tiles.

With this suggestion, the same nuke takes between 4ms to 20ms:
<img width="989" height="365" alt="image"
src="https://github.com/user-attachments/assets/25c0faf0-cc34-41b7-8091-b14bde6db595"
/>


However this changes the death formula used, as they were repeated over
each tiles with ever-smaller values, and with this change the operation
is done all-at-once. This will result in a different outcome.

In my opinion the performance gain is seductive enough to maybe tweak
the formula to make it work with this revised strategy.
What do you think?

## Please complete the following:

- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory
- [ ] 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:

IngloriousTom
This commit is contained in:
DevelopingTom
2026-02-19 00:00:56 +01:00
committed by GitHub
parent 8b66c8bd53
commit 031a17d880
3 changed files with 45 additions and 41 deletions
+43 -39
View File
@@ -255,55 +255,59 @@ export class NukeExecution implements Execution {
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
const toDestroy = this.tilesToDestroy();
const maxTroops = this.target().isPlayer()
? this.mg.config().maxTroops(this.target() as Player)
: 1;
// Retrieve all impacted players and the number of tiles
const tilesPerPlayers = new Map<Player, number>();
for (const tile of toDestroy) {
const owner = this.mg.owner(tile);
if (owner.isPlayer()) {
owner.relinquish(tile);
owner.removeTroops(
this.mg
.config()
.nukeDeathFactor(
this.nukeType,
owner.troops(),
owner.numTilesOwned(),
maxTroops,
),
);
owner.outgoingAttacks().forEach((attack) => {
const deaths =
this.mg
?.config()
.nukeDeathFactor(
this.nukeType,
attack.troops(),
owner.numTilesOwned(),
maxTroops,
) ?? 0;
attack.setTroops(attack.troops() - deaths);
});
owner.units(UnitType.TransportShip).forEach((attack) => {
const deaths =
this.mg
?.config()
.nukeDeathFactor(
this.nukeType,
attack.troops(),
owner.numTilesOwned(),
maxTroops,
) ?? 0;
attack.setTroops(attack.troops() - deaths);
});
const numTiles = tilesPerPlayers.get(owner);
tilesPerPlayers.set(owner, numTiles === undefined ? 1 : numTiles + 1);
}
if (this.mg.isLand(tile)) {
this.mg.setFallout(tile, true);
}
}
// Then compute the explosion effect on each player
for (const [player, numImpactedTiles] of tilesPerPlayers) {
const config = this.mg.config();
const tilesBeforeNuke = player.numTilesOwned() + numImpactedTiles;
const transportShips = player.units(UnitType.TransportShip);
const maxTroops = config.maxTroops(player);
// nukeDeathFactor could compute the complete fallout in a single call instead
for (let i = 0; i < numImpactedTiles; i++) {
// Diminishing effect as each affected tile has been nuked
const numTilesLeft = tilesBeforeNuke - i;
player.removeTroops(
config.nukeDeathFactor(
this.nukeType,
player.troops(),
numTilesLeft,
maxTroops,
),
);
player.outgoingAttacks().forEach((attack) => {
const deaths = config.nukeDeathFactor(
this.nukeType,
attack.troops(),
numTilesLeft,
maxTroops,
);
attack.setTroops(attack.troops() - deaths);
});
transportShips.forEach((unit) => {
const deaths = config.nukeDeathFactor(
this.nukeType,
unit.troops(),
numTilesLeft,
maxTroops,
);
unit.setTroops(unit.troops() - deaths);
});
}
}
const outer2 = magnitude.outer * magnitude.outer;
for (const unit of this.mg.units()) {
if (
+1 -1
View File
@@ -33,7 +33,7 @@ export class AttackImpl implements Attack {
return this._troops;
}
setTroops(troops: number) {
this._troops = troops;
this._troops = Math.max(0, troops);
}
isActive() {
+1 -1
View File
@@ -164,7 +164,7 @@ export class UnitImpl implements Unit {
}
setTroops(troops: number): void {
this._troops = troops;
this._troops = Math.max(0, troops);
}
troops(): number {
return this._troops;