Skip to content

Conversation

@DevelopingTom
Copy link
Contributor

@DevelopingTom DevelopingTom commented Feb 10, 2026

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:
image
image

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:
image

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Walkthrough

Detonation logic moved to a two-pass process: first pass accumulates affected-tile counts per player and marks fallout/relinquishes land tiles; second pass applies scaled casualties to each player's troops, outgoing attacks, and transport ships using the aggregated counts and a diminishing death factor.

Changes

Cohort / File(s) Summary
Nuke execution
src/core/execution/NukeExecution.ts
Reworks detonation to a two-pass algorithm: first pass collects tilesPerPlayers and marks fallout/relinquishes land tiles; second pass computes and applies deaths per impacted player with a diminishing-per-tile factor. Alliance-break and fallout handling remain.
Attack troops clamping
src/core/game/AttackImpl.ts
setTroops now clamps negative inputs with Math.max(0, troops) to ensure troop counts are non-negative.
Unit troops clamping
src/core/game/UnitImpl.ts
setTroops now clamps negative inputs with Math.max(0, troops) to ensure troop counts are non-negative.

Sequence Diagram(s)

sequenceDiagram
    participant N as NukeExecution
    participant T as TileGrid
    participant P as PlayerRegistry
    participant U as Units/Attacks/Transports
    N->>T: first pass — iterate affected tiles
    T-->>N: tile owner & type
    N->>P: accumulate tilesPerPlayers[owner]++
    N->>T: mark fallout / relinquish land tiles
    N->>P: second pass — iterate impacted players
    P-->>N: tilesBeforeNuke / player info
    loop per impacted tile (diminishing)
        N->>U: compute and apply troop reductions
        U-->>N: update outgoing attacks & transports
    end
    N->>P: update alliances if broken
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

⚛️ Two passes sweep the blasted ground,
Count then cut where losses found.
Tiles marked first, then totals weighed,
Troops and fleets gently frayed.
Quiet math where chaos reigned.

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (36 files):

⚔️ resources/lang/bg.json (content)
⚔️ resources/lang/de.json (content)
⚔️ resources/lang/el.json (content)
⚔️ resources/lang/en.json (content)
⚔️ resources/lang/fa.json (content)
⚔️ resources/lang/fr.json (content)
⚔️ resources/lang/id.json (content)
⚔️ resources/lang/ja.json (content)
⚔️ resources/lang/ko.json (content)
⚔️ resources/lang/nl.json (content)
⚔️ resources/lang/pl.json (content)
⚔️ resources/lang/ru.json (content)
⚔️ resources/lang/tr.json (content)
⚔️ resources/lang/uk.json (content)
⚔️ resources/lang/zh-CN.json (content)
⚔️ src/client/HostLobbyModal.ts (content)
⚔️ src/client/SinglePlayerModal.ts (content)
⚔️ src/client/components/map/MapDisplay.ts (content)
⚔️ src/client/components/map/MapPicker.ts (content)
⚔️ src/client/graphics/layers/EventsDisplay.ts (content)
⚔️ src/client/graphics/layers/HeadsUpMessage.ts (content)
⚔️ src/client/graphics/layers/ImmunityTimer.ts (content)
⚔️ src/core/configuration/Config.ts (content)
⚔️ src/core/configuration/DefaultConfig.ts (content)
⚔️ src/core/execution/BotExecution.ts (content)
⚔️ src/core/execution/NationExecution.ts (content)
⚔️ src/core/execution/NukeExecution.ts (content)
⚔️ src/core/execution/PlayerExecution.ts (content)
⚔️ src/core/game/AttackImpl.ts (content)
⚔️ src/core/game/Game.ts (content)
⚔️ src/core/game/GameImpl.ts (content)
⚔️ src/core/game/GameView.ts (content)
⚔️ src/core/game/PlayerImpl.ts (content)
⚔️ src/core/game/UnitImpl.ts (content)
⚔️ src/server/MapPlaylist.ts (content)
⚔️ tests/Attack.test.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective of the PR: optimizing the nuke explosion detonation function.
Description check ✅ Passed The description is directly related to the changeset, explaining the performance issue, profiling results, and the proposed optimization strategy.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/core/execution/NukeExecution.ts (1)

258-260: Note: maxTroops is based on the nuke destination owner, applied to all players in the blast zone.

This is existing behavior (not new in this PR), but worth keeping in mind: if the nuke lands on Player A's territory but the blast also hits Player B, the death factor for Player B uses Player A's maxTroops. With the new per-player aggregation this becomes more visible. Just flagging for awareness since the PR already acknowledges behavioral differences.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/execution/NukeExecution.ts (1)

258-261: ⚠️ Potential issue | 🟠 Major

For MIRV warheads, use each affected player's own maxTroops in damage calculations.

When a MIRV nuke hits a border, all affected players' damage is calculated using maxTroops from the target player. This breaks the MIRV damage formula for non-target players—their excess troops are normalized against the wrong capacity ceiling. Each player should use their own maxTroops value in the nukeDeathFactor call.

(Note: Standard nukes ignore maxTroops, so this only affects MIRV warheads.)

🤖 Fix all issues with AI agents
In `@src/core/execution/NukeExecution.ts`:
- Around line 268-269: tilesPerPlayers is updated incorrectly when a player is
first seen: in NukeExecution (around the tilesPerPlayers update using owner and
numTiles) the undefined case sets the count to 0 so the first tile is lost;
change the logic so that when numTiles is undefined you set the map entry to 1
(i.e. numTiles === undefined ? 1 : numTiles + 1) so the initial tile is counted
correctly.
- Around line 277-311: The loop in NukeExecution (tilesPerPlayers processing
inside detonate) computes nukeDeathFactor after owner.relinquish has already
reduced player.numTilesOwned(), causing incorrect scaling; fix by capturing each
player's pre-relinquish tile count (e.g., record originalTiles =
player.numTilesOwned() before any owner.relinquish calls) and pass that
originalTiles into nukeDeathFactor when calling player.removeTroops and when
adjusting outgoingAttacks and TransportShip units; also remove the unnecessary
optional chaining (change this.mg?.config() to this.mg.config()) in the two
places inside that loop so style is consistent.
🧹 Nitpick comments (1)
src/core/execution/NukeExecution.ts (1)

288-311: Duplicate logic for outgoing attacks and transport ships — consider extracting a helper.

The death-application block for outgoingAttacks() (lines 288-299) and TransportShip units (lines 300-311) is nearly identical. A small helper would reduce duplication and make future formula tweaks less error-prone.

Example extraction
// Inside detonate(), before the per-player loop:
const applyDeaths = (troops: number, setTroops: (t: number) => void) => {
  const deaths =
    this.mg.config().nukeDeathFactor(this.nukeType, troops, player.numTilesOwned(), maxTroops);
  setTroops(troops - deaths * numTiles);
};

// Then in the loop body:
for (const attack of player.outgoingAttacks()) {
  applyDeaths(attack.troops(), (t) => attack.setTroops(t));
}
for (const ship of player.units(UnitType.TransportShip)) {
  applyDeaths(ship.troops(), (t) => ship.setTroops(t));
}

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Feb 10, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/core/execution/NukeExecution.ts`:
- Around line 288-311: In NukeExecution, clamp troop subtraction so attacks and
transport ships never go negative: replace direct
attack.setTroops(attack.troops() - deaths) with a guarded assignment that
computes newTroops = Math.max(0, attack.troops() - deaths) (or call
player.removeTroops-like logic) and then set via setTroops/newTroops; also
eliminate the duplicated logic for player.outgoingAttacks() and
player.units(UnitType.TransportShip) by extracting a small helper (e.g.,
processAttackDeaths(attack)) that computes deaths via
this.mg.config().nukeDeathFactor(this.nukeType, attack.troops(),
player.numTilesOwned(), maxTroops) * numTiles and applies the clamped troop
update.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/execution/NukeExecution.ts (1)

258-260: ⚠️ Potential issue | 🟠 Major

Compute maxTroops per affected player, not once from the destination owner.

For MIRV warheads, the nukeDeathFactor() formula scales damage based on maxTroops. Currently, maxTroops is computed once from this.target() (destination tile owner) and applied to all affected players in the second loop. This doesn't match the per-player pattern used everywhere else in the codebase and causes incorrect MIRV damage scaling—especially problematic if the destination becomes TerraNullius where maxTroops equals 1.

Regular nukes are unaffected since their formula ignores the maxTroops parameter. Move the maxTroops calculation inside the second loop using each affected player's own max troops.

🤖 Fix all issues with AI agents
In `@src/core/execution/NukeExecution.ts`:
- Around line 282-284: The loop in NukeExecution.ts shadows the outer numTiles
(destructured from the map entry) by declaring a new const numTiles inside the
for loop; rename the inner variable to a distinct name like remainingTiles (or
tilesLeft) and update all uses inside the loop body (the references currently
expecting tilesBeforeNuke - i) to use remainingTiles so the outer numTiles
remains unshadowed and intent is clear (affects the for loop where
tilesBeforeNuke and numTiles are used).
🧹 Nitpick comments (1)
src/core/execution/NukeExecution.ts (1)

293-310: Inner loop re-fetches player.outgoingAttacks() on every tile iteration.

player.outgoingAttacks() is called numTiles times per player. The list does not change during the loop (no attacks are added or removed), so cache it once — just like transportShips is cached on line 280.

Proposed fix
       const transportShips = player.units(UnitType.TransportShip);
+      const outgoingAttacks = player.outgoingAttacks();
       // nukeDeathFactor could compute the complete fallout in a single call instead
       for (let i = 0; i < numTiles; i++) {
         ...
-        player.outgoingAttacks().forEach((attack) => {
+        outgoingAttacks.forEach((attack) => {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Performance Performance optimization

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

1 participant