Skip to content

Comments

v30 nuke wars preparation: Disable boats & Team spawn zones#3263

Merged
evanpelle merged 10 commits intomainfrom
new-modifiers
Feb 23, 2026
Merged

v30 nuke wars preparation: Disable boats & Team spawn zones#3263
evanpelle merged 10 commits intomainfrom
new-modifiers

Conversation

@FloPinguin
Copy link
Contributor

Description:

Preparation for nuke wars, for v30.
Next PR will be adding the nuke wars modifier for public games, but Wonders #3224 needs to be merged first to avoid merge conflicts.

1. Disable boats setting

It's possible to disable UnitType.TransportShip now. Because they are not needed in nuke wars and can even be annoying.

image

2. Team spawn zones for random spawn

Maps can have teamGameSpawnAreas in their json file now.
Spawn areas are currently active if

  • a supported map is chosen (Baikal Nuke Wars or Four Islands)
  • a supported team size is chosen (2 teams on Baikal Nuke Wars or 2/4 teams on Four Islands)
  • random spawn is enabled

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:

FloPinguin

@FloPinguin FloPinguin added this to the v30 milestone Feb 21, 2026
@FloPinguin FloPinguin requested a review from a team as a code owner February 21, 2026 14:53
@FloPinguin FloPinguin changed the title v30 nuke wars preparation: Team spawn zones & disable boats v30 nuke wars preparation: Disable boats & Team spawn zones Feb 21, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

Walkthrough

The PR adds team-based spawn area definitions to maps and introduces the TransportShip unit type. Changes include new teamGameSpawnAreas fields in map manifests, SpawnArea type definitions in game core, spawn location enforcement within designated team areas, and UI/localization support for the boat unit.

Changes

Cohort / File(s) Summary
Map Configuration
map-generator/assets/maps/baikalnukewars/info.json, map-generator/assets/maps/fourislands/info.json, resources/maps/baikalnukewars/manifest.json, resources/maps/fourislands/manifest.json
Added new teamGameSpawnAreas field to map definitions, specifying rectangular spawn regions for each team. Baikal (Nuke Wars) defines 2-team spawn areas; Four Islands supports both 2-team and 4-team configurations.
Localization
resources/lang/en.json
Added translation entry unit_type.boat with value "Boat".
Game Configuration UI
src/client/components/GameConfigSettings.ts
Added TransportShip unit option to the available unit types presented in game configuration settings.
Game Core - Type Definitions
src/core/game/Game.ts
Introduced SpawnArea interface (x, y, width, height), TeamGameSpawnAreas type (team-to-areas mapping), and teamSpawnArea() method on Game interface.
Game Core - Implementation
src/core/game/GameImpl.ts
Added teamGameSpawnAreas field and teamSpawnArea() method to GameImpl class. Constructor now accepts optional teamGameSpawnAreas parameter and stores it for team-based spawn area lookup.
Game Core - Spawn Location Logic
src/core/execution/SpawnExecution.ts
Modified getSpawn() to retrieve team spawn area via getTeamSpawnArea(). Updated randTile() to accept optional SpawnArea and generate coordinates within specified bounds when area is provided.
Game Core - Nation Spawn Enforcement
src/core/execution/NationExecution.ts
Added logic during spawn phase to detect when a nation's spawn location falls outside its team's designated area, then reschedule spawn execution to a random location within the team area.
Game Core - Boat Unit Configuration
src/core/configuration/DefaultConfig.ts
Updated boatMaxNumber() to return 0 when TransportShip unit is disabled, otherwise return 3.
Game Core - Game Creation Flow
src/core/GameRunner.ts
Updated createGame() to accept and forward teamGameSpawnAreas parameter to Game constructor.
Game Core - Map Data Loading
src/core/game/TerrainMapLoader.ts
Extended TerrainMapData and MapManifest to include optional teamGameSpawnAreas. Added scaling of teamGameSpawnAreas for Compact maps (mirroring nation scaling). Updated loadTerrainMap() to include scaled spawn areas in returned data. Changed loadedMaps cache key from GameMapType to string for composite key support.

Sequence Diagram

sequenceDiagram
    actor User
    participant GameRunner
    participant TerrainMapLoader
    participant GameImpl
    participant SpawnExecution
    participant NationExecution

    User->>GameRunner: createGame(config with map)
    GameRunner->>TerrainMapLoader: loadTerrainMap(mapType)
    TerrainMapLoader-->>GameRunner: TerrainMapData {gameMap, teamGameSpawnAreas}
    GameRunner->>GameImpl: new GameImpl(..., teamGameSpawnAreas)
    GameImpl->>GameImpl: store teamGameSpawnAreas
    GameRunner-->>User: Game ready

    User->>GameImpl: startGame()
    GameImpl->>NationExecution: execute spawn phase
    NationExecution->>GameImpl: teamSpawnArea(nation.team)
    GameImpl-->>NationExecution: SpawnArea or undefined
    alt Spawn outside team area
        NationExecution->>SpawnExecution: schedule with team area constraint
    else Spawn inside or no area
        NationExecution->>SpawnExecution: proceed normally
    end
    SpawnExecution->>SpawnExecution: getTeamSpawnArea()
    SpawnExecution->>SpawnExecution: randTile(spawnArea)
    SpawnExecution-->>NationExecution: spawn tile
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested reviewers

  • evanpelle

Poem

⛵ Boats set sail to spawn with care,
Teams claim regions, fair and square,
Spawn logic bends to honor bounds,
Team waters mapped in coordinate grounds! 🗺️

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes both main changes: disabling boats and adding team spawn zones for the v30 nuke wars preparation.
Description check ✅ Passed The description is directly related to the changeset, explaining the two features implemented: boat disabling and team spawn zones, with context about nuke wars v30 preparation.

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

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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: 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/game/TerrainMapLoader.ts (1)

43-44: ⚠️ Potential issue | 🟠 Major

Compact-map spawn-area scaling is silently bypassed for cached map types

loadedMaps is keyed only by GameMapType (line 43–44). If fourislands is first loaded as Normal size, the cache stores the full-size teamGameSpawnAreas ({x:750, y:0, …}). A later Compact-size load hits the early return and returns the same unscaled areas. At runtime, randTile would generate coordinates in the range [0, 1500) on a 750×750 map, producing out-of-bounds tile refs on every team-area spawn attempt.

The same cache-key gap exists for gameMap/miniGameMap selection (pre-existing), but this PR adds new behavior that is immediately incorrect when triggered.

The minimal fix is to include mapSize in the cache key:

🔧 Proposed fix
-const loadedMaps = new Map<GameMapType, TerrainMapData>();
+const loadedMaps = new Map<string, TerrainMapData>();

 export async function loadTerrainMap(
   map: GameMapType,
   mapSize: GameMapSize,
   terrainMapFileLoader: GameMapLoader,
 ): Promise<TerrainMapData> {
-  const cached = loadedMaps.get(map);
+  const cacheKey = `${map}:${mapSize}`;
+  const cached = loadedMaps.get(cacheKey);
   if (cached !== undefined) return cached;
   // ...
-  loadedMaps.set(map, result);
+  loadedMaps.set(cacheKey, result);
   return result;
 }

Also applies to: 70-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/TerrainMapLoader.ts` around lines 43 - 44, The cache for
loadedMaps is keyed only by GameMapType, causing Compact vs Normal loads to
share unscaled teamGameSpawnAreas; update TerrainMapLoader to incorporate
mapSize into the cache key (e.g., use a composite key of GameMapType + mapSize)
so that loadedMaps.get(...) and loadedMaps.set(...) distinguish sizes, and apply
the same composite-key change to the other cache usages around the
gameMap/miniGameMap selection (lines ~70–83) so randTile and teamGameSpawnAreas
are generated/stored per (map, mapSize) combination.
🧹 Nitpick comments (2)
src/core/execution/SpawnExecution.ts (1)

81-133: getTeamSpawnArea() is re-evaluated on every loop iteration despite returning a constant value

The spawn area for a given player/team does not change during randomSpawnLand(). Calling getTeamSpawnArea() (and therefore hasPlayer(), player(), team(), teamSpawnArea()) up to MAX_SPAWN_TRIES times adds needless overhead and reduces readability. Computing it once before the loop also keeps randTile() a pure coordinate-generator.

♻️ Proposed refactor
  private randomSpawnLand(): TileRef | undefined {
    let tries = 0;
+   const spawnArea = this.getTeamSpawnArea();

    while (tries < SpawnExecution.MAX_SPAWN_TRIES) {
      tries++;

-     const tile = this.randTile();
+     const tile = this.randTile(spawnArea);
      // ...
    }
    return;
  }

- private randTile(): TileRef {
-   const area = this.getTeamSpawnArea();
-   if (area) {
+ private randTile(area?: SpawnArea): TileRef {
+   if (area) {
      const x = this.random.nextInt(area.x, area.x + area.width);
      const y = this.random.nextInt(area.y, area.y + area.height);
      return this.mg.ref(x, y);
    }
    const x = this.random.nextInt(0, this.mg.width());
    const y = this.random.nextInt(0, this.mg.height());
    return this.mg.ref(x, y);
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/execution/SpawnExecution.ts` around lines 81 - 133, In
randomSpawnLand(), cache the team spawn area once before the while loop instead
of calling getTeamSpawnArea() repeatedly via randTile(); compute const area =
this.getTeamSpawnArea() (or similar) once and pass that area into randTile (or
make randTile accept an optional area parameter) so randTile becomes a pure
coordinate generator that uses the provided area when present; update
randomSpawnLand() to call randTile(area) each iteration and leave
MAX_SPAWN_TRIES, random, and other logic unchanged.
src/core/game/TerrainMapLoader.ts (1)

4-6: Re-exporting SpawnArea/TeamGameSpawnAreas from TerrainMapLoader creates a duplicate import path

SpawnArea.ts is the canonical source. All changed files in this PR import directly from SpawnArea.ts. The export type { SpawnArea, TeamGameSpawnAreas } here adds a second path (TerrainMapLoader) for the same types with no current consumer. If left in place, future code may import from either location inconsistently. Consider removing the re-export and letting callers depend on SpawnArea.ts directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/TerrainMapLoader.ts` around lines 4 - 6, Remove the re-export
that creates a duplicate import path: delete the line "export type { SpawnArea,
TeamGameSpawnAreas }" from TerrainMapLoader.ts so SpawnArea and
TeamGameSpawnAreas remain exported only from SpawnArea.ts; keep the local import
if TerrainMapLoader still uses those types, and if any files were accidentally
importing the types from TerrainMapLoader, change them to import from
SpawnArea.ts instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/game/GameImpl.ts`:
- Around line 800-814: The field playerTeams is declared as Team[] but can be
uninitialized at runtime; update its declaration to be explicitly optional
(Team[] | undefined) or initialize it to an empty array (Team[] = []) so
TypeScript reflects the runtime possibility, then adjust any code that relies on
it (e.g., teamSpawnArea and populateTeams) to handle the new type (use non-null
checks or assign before use) so the existing runtime guard in teamSpawnArea
remains meaningful to the type system.

---

Outside diff comments:
In `@src/core/game/TerrainMapLoader.ts`:
- Around line 43-44: The cache for loadedMaps is keyed only by GameMapType,
causing Compact vs Normal loads to share unscaled teamGameSpawnAreas; update
TerrainMapLoader to incorporate mapSize into the cache key (e.g., use a
composite key of GameMapType + mapSize) so that loadedMaps.get(...) and
loadedMaps.set(...) distinguish sizes, and apply the same composite-key change
to the other cache usages around the gameMap/miniGameMap selection (lines
~70–83) so randTile and teamGameSpawnAreas are generated/stored per (map,
mapSize) combination.

---

Nitpick comments:
In `@src/core/execution/SpawnExecution.ts`:
- Around line 81-133: In randomSpawnLand(), cache the team spawn area once
before the while loop instead of calling getTeamSpawnArea() repeatedly via
randTile(); compute const area = this.getTeamSpawnArea() (or similar) once and
pass that area into randTile (or make randTile accept an optional area
parameter) so randTile becomes a pure coordinate generator that uses the
provided area when present; update randomSpawnLand() to call randTile(area) each
iteration and leave MAX_SPAWN_TRIES, random, and other logic unchanged.

In `@src/core/game/TerrainMapLoader.ts`:
- Around line 4-6: Remove the re-export that creates a duplicate import path:
delete the line "export type { SpawnArea, TeamGameSpawnAreas }" from
TerrainMapLoader.ts so SpawnArea and TeamGameSpawnAreas remain exported only
from SpawnArea.ts; keep the local import if TerrainMapLoader still uses those
types, and if any files were accidentally importing the types from
TerrainMapLoader, change them to import from SpawnArea.ts instead.

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Feb 21, 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.

🧹 Nitpick comments (1)
src/core/game/GameImpl.ts (1)

800-814: Remove dead !this.playerTeams guard.

playerTeams is now initialized to [] (line 98), so !this.playerTeams is always false — an empty array is truthy in JavaScript. The !areas check on line 806 already handles the non-team-mode case (when playerTeams is [], numTeams is 0, and _teamGameSpawnAreas["0"] is undefined).

♻️ Remove dead guard
  teamSpawnArea(team: Team): SpawnArea | undefined {
-   if (!this._teamGameSpawnAreas || !this.playerTeams) {
+   if (!this._teamGameSpawnAreas) {
      return undefined;
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/GameImpl.ts` around lines 800 - 814, In teamSpawnArea, remove
the dead guard that checks !this.playerTeams: since playerTeams is always
initialized to an array, the check is redundant; update the function
(referencing teamSpawnArea, this.playerTeams, and this._teamGameSpawnAreas) to
only early-return when this._teamGameSpawnAreas is falsy, leaving the later
logic (numTeams, areas lookup, teamIndex bounds check) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/core/game/GameImpl.ts`:
- Around line 800-814: In teamSpawnArea, remove the dead guard that checks
!this.playerTeams: since playerTeams is always initialized to an array, the
check is redundant; update the function (referencing teamSpawnArea,
this.playerTeams, and this._teamGameSpawnAreas) to only early-return when
this._teamGameSpawnAreas is falsy, leaving the later logic (numTeams, areas
lookup, teamIndex bounds check) unchanged.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 21, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 21, 2026
…ction

refactor(TerrainMapLoader): clean up imports by removing unused SpawnArea export
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.

Caution

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

⚠️ Outside diff range comments (1)
src/core/game/TerrainMapLoader.ts (1)

41-42: ⚠️ Potential issue | 🟡 Minor

Cache is keyed by map only, not by mapSize — stale data risk.

The module-level loadedMaps cache uses GameMapType as the sole key (line 13). However, the cached data includes mapSize-dependent values: nations are scaled when mapSize is Compact (lines 59-65), and teamGameSpawnAreas are scaled conditionally (lines 70-81).

If the same map is loaded with different mapSize values in one process, the second call returns the first call's cached result, including its (un)scaled nations and spawn areas. This is incorrect.

This is a pre-existing issue, but the new spawn-area scaling makes it more visible. Consider fixing the cache key to include mapSize: new Map<[GameMapType, GameMapSize], TerrainMapData>().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/TerrainMapLoader.ts` around lines 41 - 42, The cache is
currently keyed only by GameMapType (loadedMaps) so calls to loadTerrainMap (or
the loader function that reads `map` and `mapSize`) can return stale data when
the same map is requested with different GameMapSize; change loadedMaps to use a
composite key that includes mapSize (e.g., Map<[GameMapType, GameMapSize],
TerrainMapData> or a serialized string key `${map}:${mapSize}`), update the
lookup where `const cached = loadedMaps.get(map)` to use the composite key, and
update where results are stored into loadedMaps to use the same composite key so
scaled nations and teamGameSpawnAreas are cached per map+size combination
(ensure all references to loadedMaps.get/set use the new key format).
🧹 Nitpick comments (2)
src/core/game/TerrainMapLoader.ts (1)

68-81: Scaling logic is consistent with nation coordinates — good. One small note on Math.floor for width/height.

When you halve width and height with Math.floor, odd values lose a pixel (e.g., width 11 becomes 5, not 5.5). This shrinks the spawn area slightly. For large areas this is fine, but if any area dimension is 1, it becomes 0, which would make randTile call nextInt(x, x) — a degenerate range. Probably never happens in practice, but a Math.max(..., 1) guard would be cheap insurance.

Optional: guard against zero-sized dimensions
       scaled[key] = areas.map((a) => ({
         x: Math.floor(a.x / 2),
         y: Math.floor(a.y / 2),
-        width: Math.floor(a.width / 2),
-        height: Math.floor(a.height / 2),
+        width: Math.max(Math.floor(a.width / 2), 1),
+        height: Math.max(Math.floor(a.height / 2), 1),
       }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/TerrainMapLoader.ts` around lines 68 - 81, The scaling block
for teamGameSpawnAreas can produce zero width/height when halving odd small
dimensions; update the scaling in TerrainMapLoader where teamGameSpawnAreas is
computed (the loop that builds scaled) to clamp width and height to at least 1
(e.g., replace Math.floor(a.width / 2) and Math.floor(a.height / 2) with
Math.max(Math.floor(...), 1)) so spawn areas never become zero-sized; keep x/y
as Math.floor(a.x/2) but ensure width/height use the Math.max guard to avoid
degenerate ranges later when randTile or similar functions consume these areas.
src/core/execution/SpawnExecution.ts (1)

135-145: The hasPlayer guard on line 136 is always true at the call site — harmless but redundant.

When randomSpawnLand() is called from tick() (line 51), the player has already been added at lines 40–44. So hasPlayer can never be false here. This is safe defensive code — not a problem, just noting it in case you want to simplify.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/execution/SpawnExecution.ts` around lines 135 - 145, The hasPlayer()
check in getTeamSpawnArea() is redundant at the call site (randomSpawnLand()
invoked from tick() after the player is added), so remove the unnecessary guard
and simplify the method: directly call this.mg.player(this.playerInfo.id),
retrieve team via player.team(), return undefined if team is null, otherwise
return this.mg.teamSpawnArea(team); keep method name getTeamSpawnArea and
callers unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/core/game/TerrainMapLoader.ts`:
- Around line 41-42: The cache is currently keyed only by GameMapType
(loadedMaps) so calls to loadTerrainMap (or the loader function that reads `map`
and `mapSize`) can return stale data when the same map is requested with
different GameMapSize; change loadedMaps to use a composite key that includes
mapSize (e.g., Map<[GameMapType, GameMapSize], TerrainMapData> or a serialized
string key `${map}:${mapSize}`), update the lookup where `const cached =
loadedMaps.get(map)` to use the composite key, and update where results are
stored into loadedMaps to use the same composite key so scaled nations and
teamGameSpawnAreas are cached per map+size combination (ensure all references to
loadedMaps.get/set use the new key format).

---

Nitpick comments:
In `@src/core/execution/SpawnExecution.ts`:
- Around line 135-145: The hasPlayer() check in getTeamSpawnArea() is redundant
at the call site (randomSpawnLand() invoked from tick() after the player is
added), so remove the unnecessary guard and simplify the method: directly call
this.mg.player(this.playerInfo.id), retrieve team via player.team(), return
undefined if team is null, otherwise return this.mg.teamSpawnArea(team); keep
method name getTeamSpawnArea and callers unchanged.

In `@src/core/game/TerrainMapLoader.ts`:
- Around line 68-81: The scaling block for teamGameSpawnAreas can produce zero
width/height when halving odd small dimensions; update the scaling in
TerrainMapLoader where teamGameSpawnAreas is computed (the loop that builds
scaled) to clamp width and height to at least 1 (e.g., replace
Math.floor(a.width / 2) and Math.floor(a.height / 2) with
Math.max(Math.floor(...), 1)) so spawn areas never become zero-sized; keep x/y
as Math.floor(a.x/2) but ensure width/height use the Math.max guard to avoid
degenerate ranges later when randTile or similar functions consume these areas.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 21, 2026
…ieval

refactor(SpawnExecution): remove unnecessary player check in getTeamSpawnArea
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.

🧹 Nitpick comments (2)
src/core/execution/SpawnExecution.ts (1)

124-133: randTile logic is correct; note implicit assumption about nextInt upper-bound exclusivity.

nextInt(area.x, area.x + area.width) produces exactly width distinct x-values only if nextInt is exclusive on the upper bound — which is consistent with the existing nextInt(0, mg.width()) usage for the full-map path. If PseudoRandom.nextInt semantics ever change to inclusive, this would silently sample one column/row outside the area. A brief comment here would make the intent explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/execution/SpawnExecution.ts` around lines 124 - 133, Add a short
clarifying comment above the randTile method that documents the assumption that
PseudoRandom.nextInt(a, b) treats b as an exclusive upper bound (so
nextInt(area.x, area.x + area.width) yields exactly area.width distinct values),
and note that if nextInt semantics change to inclusive the sampling could go out
of the intended area; reference the randTile function and PseudoRandom.nextInt
in the comment and, if desired, suggest clamping or using (width-1)/(height-1)
to guard against out-of-range coordinates.
src/core/game/TerrainMapLoader.ts (1)

41-42: Cache key fix is correct; consider a nested Map to eliminate string-collision risk entirely.

The old single-key lookup was buggy — loading the same map in two different sizes could return stale scaled/unscaled data from cache. The composite string key ${map}:${mapSize} fixes that correctly.

Minor: when concatenating strings, the joiner must not appear in either key part. GameMapType values look like "WorldMap" / "BaikalNukeWars" so : is safe in practice, but a nested Map removes the concern completely:

♻️ Nested map alternative
-const loadedMaps = new Map<string, TerrainMapData>();
+const loadedMaps = new Map<GameMapType, Map<GameMapSize, TerrainMapData>>();

Then adjust lookup/insert:

-  const cacheKey = `${map}:${mapSize}`;
-  const cached = loadedMaps.get(cacheKey);
-  if (cached !== undefined) return cached;
+  const cached = loadedMaps.get(map)?.get(mapSize);
+  if (cached !== undefined) return cached;
-  loadedMaps.set(cacheKey, result);
+  if (!loadedMaps.has(map)) loadedMaps.set(map, new Map());
+  loadedMaps.get(map)!.set(mapSize, result);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/TerrainMapLoader.ts` around lines 41 - 42, Replace the
composite string key approach in TerrainMapLoader (the cacheKey variable and its
use with loadedMaps) with a nested Map keyed first by map (GameMapType) then by
mapSize to avoid string-collision risks; change loadedMaps to Map<map,
Map<mapSize, LoadedMapType>> (or equivalent), read via inner =
loadedMaps.get(map) then cached = inner?.get(mapSize), and on insert ensure an
inner map is created if missing (loadedMaps.set(map, new Map()) then
inner.set(mapSize, value)); update all lookup/insert sites that currently use
cacheKey to use this two-level map access pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/core/execution/SpawnExecution.ts`:
- Around line 124-133: Add a short clarifying comment above the randTile method
that documents the assumption that PseudoRandom.nextInt(a, b) treats b as an
exclusive upper bound (so nextInt(area.x, area.x + area.width) yields exactly
area.width distinct values), and note that if nextInt semantics change to
inclusive the sampling could go out of the intended area; reference the randTile
function and PseudoRandom.nextInt in the comment and, if desired, suggest
clamping or using (width-1)/(height-1) to guard against out-of-range
coordinates.

In `@src/core/game/TerrainMapLoader.ts`:
- Around line 41-42: Replace the composite string key approach in
TerrainMapLoader (the cacheKey variable and its use with loadedMaps) with a
nested Map keyed first by map (GameMapType) then by mapSize to avoid
string-collision risks; change loadedMaps to Map<map, Map<mapSize,
LoadedMapType>> (or equivalent), read via inner = loadedMaps.get(map) then
cached = inner?.get(mapSize), and on insert ensure an inner map is created if
missing (loadedMaps.set(map, new Map()) then inner.set(mapSize, value)); update
all lookup/insert sites that currently use cacheKey to use this two-level map
access pattern.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 21, 2026
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: mind keeping this in Game.ts for now. Eventually we'll need to figure out how to break it up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@github-project-automation github-project-automation bot moved this from Development to Final Review in OpenFront Release Management Feb 23, 2026
@evanpelle evanpelle merged commit 339ace0 into main Feb 23, 2026
10 of 11 checks passed
@evanpelle evanpelle deleted the new-modifiers branch February 23, 2026 22:12
@github-project-automation github-project-automation bot moved this from Final Review to Complete in OpenFront Release Management Feb 23, 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.

🧹 Nitpick comments (3)
src/core/execution/SpawnExecution.ts (2)

159-166: Consider passing the player directly instead of re-fetching it.

getTeamSpawnArea() calls this.mg.player(this.playerInfo.id) to get the player, but the caller (getSpawn) is itself called from tick() which already has the player reference. Passing it as a parameter avoids a redundant map lookup and makes the data flow easier to follow.

This is a minor style point — the lookup is safe because tick() guarantees the player exists by this point.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/execution/SpawnExecution.ts` around lines 159 - 166, The
getTeamSpawnArea method re-fetches the player via
this.mg.player(this.playerInfo.id) causing a redundant lookup; change
getTeamSpawnArea to accept a Player (or the exact player type) as a parameter
and use that to obtain team and spawn area, then update callers (notably
getSpawn and its caller tick) to pass the existing player reference instead of
relying on playerInfo.id — ensure signature updates to getTeamSpawnArea(player:
Player): SpawnArea | undefined and adjust any type imports/usages accordingly.

148-157: Clarify that mg.ref() throws loudly on invalid coordinates.

If a SpawnArea has coordinates extending beyond the map dimensions (e.g., area.x + area.width > map.width()), calling nextInt(area.x, area.x + area.width) could generate a coordinate that exceeds the map bounds. When passed to this.mg.ref(x, y), it throws an error: Invalid coordinates: ${x},${y}.

Since map manifests are author-controlled and the code already fails loudly on corruption (rather than silently), the current behavior aligns with the codebase's established pattern for trusted data sources. Adding clamping remains a reasonable robustness improvement but is not necessary for correctness:

Suggested bounds clamping for extra safety
  private randTile(area?: SpawnArea): TileRef {
    if (area) {
-     const x = this.random.nextInt(area.x, area.x + area.width);
-     const y = this.random.nextInt(area.y, area.y + area.height);
+     const x = this.random.nextInt(
+       Math.max(0, area.x),
+       Math.min(this.mg.width(), area.x + area.width),
+     );
+     const y = this.random.nextInt(
+       Math.max(0, area.y),
+       Math.min(this.mg.height(), area.y + area.height),
+     );
      return this.mg.ref(x, y);
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/execution/SpawnExecution.ts` around lines 148 - 157, randTile can
generate coordinates outside the map if a SpawnArea extends beyond map bounds
and this.mg.ref(x,y) will throw "Invalid coordinates: x,y" — make this explicit:
in randTile (method randTile and using SpawnArea, random.nextInt,
mg.width/height and this.mg.ref) add a pre-check that clamps or validates x/y
against this.mg.width()/height() and either clamp to valid range or throw a
clearer error message before calling this.mg.ref; include a short comment
explaining that manifests are trusted so we fail loudly (or that clamping is
applied for extra robustness).
src/core/game/TerrainMapLoader.ts (1)

59-66: Pre-existing: nation scaling mutates manifest.nations in place.

Unlike the new spawn-area scaling (which correctly creates a fresh object), this block mutates manifest.nations[i].coordinates directly. Since manifest comes from a loader and the result caches nations: manifest.nations, this means the cached result holds already-mutated references. A second call with the same map but a different size would re-read the (now-mutated) manifest from a different source, but if any code path reuses the manifest object, coordinates would be wrong.

This is not introduced by your PR — just flagging it since you did the right thing with spawn areas and may want to align this block later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/TerrainMapLoader.ts` around lines 59 - 66, The nation scaling
currently mutates manifest.nations in place (inside TerrainMapLoader where
mapSize === GameMapSize.Compact), which contaminates the cached manifest;
instead, create and return a new nations array with new nation objects and
copied/scaled coordinate arrays (e.g. use manifest.nations.map to produce {
...nation, coordinates: [Math.floor(...), Math.floor(...)] }) so the original
manifest remains unmodified—align this approach with the spawn-area scaling
logic to ensure cached results hold fresh copies rather than mutated references.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/core/execution/SpawnExecution.ts`:
- Around line 159-166: The getTeamSpawnArea method re-fetches the player via
this.mg.player(this.playerInfo.id) causing a redundant lookup; change
getTeamSpawnArea to accept a Player (or the exact player type) as a parameter
and use that to obtain team and spawn area, then update callers (notably
getSpawn and its caller tick) to pass the existing player reference instead of
relying on playerInfo.id — ensure signature updates to getTeamSpawnArea(player:
Player): SpawnArea | undefined and adjust any type imports/usages accordingly.
- Around line 148-157: randTile can generate coordinates outside the map if a
SpawnArea extends beyond map bounds and this.mg.ref(x,y) will throw "Invalid
coordinates: x,y" — make this explicit: in randTile (method randTile and using
SpawnArea, random.nextInt, mg.width/height and this.mg.ref) add a pre-check that
clamps or validates x/y against this.mg.width()/height() and either clamp to
valid range or throw a clearer error message before calling this.mg.ref; include
a short comment explaining that manifests are trusted so we fail loudly (or that
clamping is applied for extra robustness).

In `@src/core/game/TerrainMapLoader.ts`:
- Around line 59-66: The nation scaling currently mutates manifest.nations in
place (inside TerrainMapLoader where mapSize === GameMapSize.Compact), which
contaminates the cached manifest; instead, create and return a new nations array
with new nation objects and copied/scaled coordinate arrays (e.g. use
manifest.nations.map to produce { ...nation, coordinates: [Math.floor(...),
Math.floor(...)] }) so the original manifest remains unmodified—align this
approach with the spawn-area scaling logic to ensure cached results hold fresh
copies rather than mutated references.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69cd337 and 0d79f5a.

📒 Files selected for processing (7)
  • resources/lang/en.json
  • src/core/GameRunner.ts
  • src/core/configuration/DefaultConfig.ts
  • src/core/execution/SpawnExecution.ts
  • src/core/game/Game.ts
  • src/core/game/GameImpl.ts
  • src/core/game/TerrainMapLoader.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/GameRunner.ts
  • src/core/game/GameImpl.ts

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

Labels

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

2 participants