From 339ace0bd677e96b7080c7b7c3758b95b00acf2f Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Mon, 23 Feb 2026 23:12:24 +0100
Subject: [PATCH] v30 nuke wars preparation: Disable boats & Team spawn zones
(#3263)
## Description:
Preparation for nuke wars, for v30.
Next PR will be adding the nuke wars modifier for public games, but
Wonders https://github.com/openfrontio/OpenFrontIO/pull/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.
### 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:
- [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
---
.../assets/maps/baikalnukewars/info.json | 8 +++-
.../assets/maps/fourislands/info.json | 14 +++++-
resources/lang/en.json | 1 +
resources/maps/baikalnukewars/manifest.json | 18 +++++++-
resources/maps/fourislands/manifest.json | 44 ++++++++++++++++++-
src/client/components/GameConfigSettings.ts | 1 +
src/core/GameRunner.ts | 1 +
src/core/configuration/DefaultConfig.ts | 3 ++
src/core/execution/NationExecution.ts | 21 +++++++++
src/core/execution/SpawnExecution.ts | 29 ++++++++++--
src/core/game/Game.ts | 10 +++++
src/core/game/GameImpl.ts | 34 +++++++++++++-
src/core/game/TerrainMapLoader.ts | 27 ++++++++++--
13 files changed, 197 insertions(+), 14 deletions(-)
diff --git a/map-generator/assets/maps/baikalnukewars/info.json b/map-generator/assets/maps/baikalnukewars/info.json
index 732498906..c8a3e024d 100644
--- a/map-generator/assets/maps/baikalnukewars/info.json
+++ b/map-generator/assets/maps/baikalnukewars/info.json
@@ -1,4 +1,10 @@
{
"name": "Baikal (Nuke Wars)",
- "nations": []
+ "nations": [],
+ "teamGameSpawnAreas": {
+ "2": [
+ { "x": 0, "y": 0, "width": 1330, "height": 1564 },
+ { "x": 1430, "y": 0, "width": 1070, "height": 1564 }
+ ]
+ }
}
diff --git a/map-generator/assets/maps/fourislands/info.json b/map-generator/assets/maps/fourislands/info.json
index 899498b7f..d313ec59d 100644
--- a/map-generator/assets/maps/fourislands/info.json
+++ b/map-generator/assets/maps/fourislands/info.json
@@ -21,5 +21,17 @@
"flag": "",
"name": "Myrkwind"
}
- ]
+ ],
+ "teamGameSpawnAreas": {
+ "2": [
+ { "x": 0, "y": 0, "width": 750, "height": 1500 },
+ { "x": 750, "y": 0, "width": 750, "height": 1500 }
+ ],
+ "4": [
+ { "x": 0, "y": 0, "width": 750, "height": 750 },
+ { "x": 750, "y": 0, "width": 750, "height": 750 },
+ { "x": 0, "y": 750, "width": 750, "height": 750 },
+ { "x": 750, "y": 750, "width": 750, "height": 750 }
+ ]
+ }
}
diff --git a/resources/lang/en.json b/resources/lang/en.json
index 99fb82071..378a86c51 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -472,6 +472,7 @@
"title": "Select Language"
},
"unit_type": {
+ "boat": "Boat",
"city": "City",
"defense_post": "Defense Post",
"port": "Port",
diff --git a/resources/maps/baikalnukewars/manifest.json b/resources/maps/baikalnukewars/manifest.json
index 0686a63ed..8e775204a 100644
--- a/resources/maps/baikalnukewars/manifest.json
+++ b/resources/maps/baikalnukewars/manifest.json
@@ -15,5 +15,21 @@
"width": 1250
},
"name": "Baikal (Nuke Wars)",
- "nations": []
+ "nations": [],
+ "teamGameSpawnAreas": {
+ "2": [
+ {
+ "height": 1564,
+ "width": 1330,
+ "x": 0,
+ "y": 0
+ },
+ {
+ "height": 1564,
+ "width": 1070,
+ "x": 1430,
+ "y": 0
+ }
+ ]
+ }
}
diff --git a/resources/maps/fourislands/manifest.json b/resources/maps/fourislands/manifest.json
index 1f58fec86..14840a152 100644
--- a/resources/maps/fourislands/manifest.json
+++ b/resources/maps/fourislands/manifest.json
@@ -36,5 +36,47 @@
"flag": "",
"name": "Myrkwind"
}
- ]
+ ],
+ "teamGameSpawnAreas": {
+ "2": [
+ {
+ "height": 1500,
+ "width": 750,
+ "x": 0,
+ "y": 0
+ },
+ {
+ "height": 1500,
+ "width": 750,
+ "x": 750,
+ "y": 0
+ }
+ ],
+ "4": [
+ {
+ "height": 750,
+ "width": 750,
+ "x": 0,
+ "y": 0
+ },
+ {
+ "height": 750,
+ "width": 750,
+ "x": 750,
+ "y": 0
+ },
+ {
+ "height": 750,
+ "width": 750,
+ "x": 0,
+ "y": 750
+ },
+ {
+ "height": 750,
+ "width": 750,
+ "x": 750,
+ "y": 750
+ }
+ ]
+ }
}
diff --git a/src/client/components/GameConfigSettings.ts b/src/client/components/GameConfigSettings.ts
index b981a89fe..8300dd2b1 100644
--- a/src/client/components/GameConfigSettings.ts
+++ b/src/client/components/GameConfigSettings.ts
@@ -93,6 +93,7 @@ const unitOptions: { type: UnitType; translationKey: string }[] = [
{ type: UnitType.DefensePost, translationKey: "unit_type.defense_post" },
{ type: UnitType.Port, translationKey: "unit_type.port" },
{ type: UnitType.Warship, translationKey: "unit_type.warship" },
+ { type: UnitType.TransportShip, translationKey: "unit_type.boat" },
{ type: UnitType.MissileSilo, translationKey: "unit_type.missile_silo" },
{ type: UnitType.SAMLauncher, translationKey: "unit_type.sam_launcher" },
{ type: UnitType.AtomBomb, translationKey: "unit_type.atom_bomb" },
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index e8c46803d..7453b03af 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -66,6 +66,7 @@ export async function createGameRunner(
gameMap.gameMap,
gameMap.miniGameMap,
config,
+ gameMap.teamGameSpawnAreas,
);
const gr = new GameRunner(
diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts
index 4274ab52d..f47d5e793 100644
--- a/src/core/configuration/DefaultConfig.ts
+++ b/src/core/configuration/DefaultConfig.ts
@@ -530,6 +530,9 @@ export class DefaultConfig implements Config {
return 80;
}
boatMaxNumber(): number {
+ if (this.isUnitDisabled(UnitType.TransportShip)) {
+ return 0;
+ }
return 3;
}
numSpawnPhaseTurns(): number {
diff --git a/src/core/execution/NationExecution.ts b/src/core/execution/NationExecution.ts
index 858b00974..ac3ce4928 100644
--- a/src/core/execution/NationExecution.ts
+++ b/src/core/execution/NationExecution.ts
@@ -110,6 +110,27 @@ export class NationExecution implements Execution {
return;
}
+ // If team spawn areas are configured and the nation's spawn cell
+ // is outside its team's area, spawn randomly within the area instead.
+ const team = this.player.team();
+ if (team !== null) {
+ const area = this.mg.teamSpawnArea(team);
+ if (area !== undefined) {
+ const cell = this.nation.spawnCell;
+ const inArea =
+ cell.x >= area.x &&
+ cell.x < area.x + area.width &&
+ cell.y >= area.y &&
+ cell.y < area.y + area.height;
+ if (!inArea) {
+ this.mg.addExecution(
+ new SpawnExecution(this.gameID, this.nation.playerInfo),
+ );
+ return;
+ }
+ }
+ }
+
// Select a tile near the position defined in the map manifest
const rl = this.randomSpawnLand();
diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts
index 1a3bc1a39..0e3e8ef46 100644
--- a/src/core/execution/SpawnExecution.ts
+++ b/src/core/execution/SpawnExecution.ts
@@ -1,4 +1,11 @@
-import { Execution, Game, Player, PlayerInfo, PlayerType } from "../game/Game";
+import {
+ Execution,
+ Game,
+ Player,
+ PlayerInfo,
+ PlayerType,
+ SpawnArea,
+} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { PseudoRandom } from "../PseudoRandom";
import { GameID } from "../Schemas";
@@ -90,12 +97,13 @@ export class SpawnExecution implements Execution {
return { center, tiles };
}
+ const spawnArea = this.getTeamSpawnArea();
let tries = 0;
while (tries < SpawnExecution.MAX_SPAWN_TRIES) {
tries++;
- const center = this.randTile();
+ const center = this.randTile(spawnArea);
if (
!this.mg.isLand(center) ||
@@ -137,10 +145,23 @@ export class SpawnExecution implements Execution {
return;
}
- private randTile(): TileRef {
+ 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);
}
+
+ private getTeamSpawnArea(): SpawnArea | undefined {
+ const player = this.mg.player(this.playerInfo.id);
+ const team = player.team();
+ if (team === null) {
+ return undefined;
+ }
+ return this.mg.teamSpawnArea(team);
+ }
}
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index 11722e142..fdd27a63d 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -52,6 +52,15 @@ export const isDifficulty = (value: unknown): value is Difficulty =>
export type Team = string;
+export interface SpawnArea {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+}
+
+export type TeamGameSpawnAreas = Record;
+
export const Duos = "Duos" as const;
export const Trios = "Trios" as const;
export const Quads = "Quads" as const;
@@ -753,6 +762,7 @@ export interface Game extends GameMap {
owner(ref: TileRef): Player | TerraNullius;
teams(): Team[];
+ teamSpawnArea(team: Team): SpawnArea | undefined;
// Alliances
alliances(): MutableAlliance[];
diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts
index 6ff6c5081..202ed12d4 100644
--- a/src/core/game/GameImpl.ts
+++ b/src/core/game/GameImpl.ts
@@ -31,7 +31,9 @@ import {
PlayerInfo,
PlayerType,
Quads,
+ SpawnArea,
Team,
+ TeamGameSpawnAreas,
TerrainType,
TerraNullius,
Trios,
@@ -56,9 +58,18 @@ export function createGame(
gameMap: GameMap,
miniGameMap: GameMap,
config: Config,
+ teamGameSpawnAreas?: TeamGameSpawnAreas,
): Game {
const stats = new StatsImpl();
- return new GameImpl(humans, nations, gameMap, miniGameMap, config, stats);
+ return new GameImpl(
+ humans,
+ nations,
+ gameMap,
+ miniGameMap,
+ config,
+ stats,
+ teamGameSpawnAreas,
+ );
}
export type CellString = string;
@@ -86,7 +97,7 @@ export class GameImpl implements Game {
private tileUpdatePairs: number[] = [];
private unitGrid: UnitGrid;
- private playerTeams: Team[];
+ private playerTeams: Team[] = [];
private botTeam: Team = ColoredTeams.Bot;
private _railNetwork: RailNetwork = createRailNetwork(this);
@@ -97,6 +108,7 @@ export class GameImpl implements Game {
private _winner: Player | Team | null = null;
private _miniWaterGraph: AbstractGraph | null = null;
private _miniWaterHPA: AStarWaterHierarchical | null = null;
+ private _teamGameSpawnAreas: TeamGameSpawnAreas | undefined;
constructor(
private _humans: PlayerInfo[],
@@ -105,9 +117,11 @@ export class GameImpl implements Game {
private miniGameMap: GameMap,
private _config: Config,
private _stats: Stats,
+ teamGameSpawnAreas?: TeamGameSpawnAreas,
) {
const constructorStart = performance.now();
+ this._teamGameSpawnAreas = teamGameSpawnAreas;
this._terraNullius = new TerraNulliusImpl();
this._width = _map.width();
this._height = _map.height();
@@ -791,6 +805,22 @@ export class GameImpl implements Game {
return [this.botTeam, ...this.playerTeams];
}
+ teamSpawnArea(team: Team): SpawnArea | undefined {
+ if (!this._teamGameSpawnAreas) {
+ return undefined;
+ }
+ const numTeams = this.playerTeams.length;
+ const areas = this._teamGameSpawnAreas[String(numTeams)];
+ if (!areas) {
+ return undefined;
+ }
+ const teamIndex = this.playerTeams.indexOf(team);
+ if (teamIndex < 0 || teamIndex >= areas.length) {
+ return undefined;
+ }
+ return areas[teamIndex];
+ }
+
displayMessage(
message: string,
type: MessageType,
diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts
index 56e998d32..8899b94f8 100644
--- a/src/core/game/TerrainMapLoader.ts
+++ b/src/core/game/TerrainMapLoader.ts
@@ -1,4 +1,4 @@
-import { GameMapSize, GameMapType } from "./Game";
+import { GameMapSize, GameMapType, TeamGameSpawnAreas } from "./Game";
import { GameMap, GameMapImpl } from "./GameMap";
import { GameMapLoader } from "./GameMapLoader";
@@ -6,9 +6,10 @@ export type TerrainMapData = {
nations: Nation[];
gameMap: GameMap;
miniGameMap: GameMap;
+ teamGameSpawnAreas?: TeamGameSpawnAreas;
};
-const loadedMaps = new Map();
+const loadedMaps = new Map();
export interface MapMetadata {
width: number;
@@ -22,6 +23,7 @@ export interface MapManifest {
map4x: MapMetadata;
map16x: MapMetadata;
nations: Nation[];
+ teamGameSpawnAreas?: TeamGameSpawnAreas;
}
export interface Nation {
@@ -35,7 +37,8 @@ export async function loadTerrainMap(
mapSize: GameMapSize,
terrainMapFileLoader: GameMapLoader,
): Promise {
- const cached = loadedMaps.get(map);
+ const cacheKey = `${map}:${mapSize}`;
+ const cached = loadedMaps.get(cacheKey);
if (cached !== undefined) return cached;
const mapFiles = terrainMapFileLoader.getMapData(map);
const manifest = await mapFiles.manifest();
@@ -62,12 +65,28 @@ export async function loadTerrainMap(
});
}
+ // Scale spawn areas for compact maps
+ let teamGameSpawnAreas = manifest.teamGameSpawnAreas;
+ if (mapSize === GameMapSize.Compact && teamGameSpawnAreas) {
+ const scaled: TeamGameSpawnAreas = {};
+ for (const [key, areas] of Object.entries(teamGameSpawnAreas)) {
+ scaled[key] = areas.map((a) => ({
+ x: Math.floor(a.x / 2),
+ y: Math.floor(a.y / 2),
+ width: Math.max(1, Math.floor(a.width / 2)),
+ height: Math.max(1, Math.floor(a.height / 2)),
+ }));
+ }
+ teamGameSpawnAreas = scaled;
+ }
+
const result = {
nations: manifest.nations,
gameMap: gameMap,
miniGameMap: miniMap,
+ teamGameSpawnAreas,
};
- loadedMaps.set(map, result);
+ loadedMaps.set(cacheKey, result);
return result;
}