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.

<img width="720" height="320" alt="image"
src="https://github.com/user-attachments/assets/661bc10d-b204-4b4f-b876-ee7c9b92de8c"
/>

### 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
This commit is contained in:
FloPinguin
2026-02-23 23:12:24 +01:00
committed by GitHub
parent 4b917c4153
commit 339ace0bd6
13 changed files with 197 additions and 14 deletions
@@ -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 }
]
}
}
@@ -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 }
]
}
}
+1
View File
@@ -472,6 +472,7 @@
"title": "Select Language"
},
"unit_type": {
"boat": "Boat",
"city": "City",
"defense_post": "Defense Post",
"port": "Port",
+17 -1
View File
@@ -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
}
]
}
}
+43 -1
View File
@@ -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
}
]
}
}
@@ -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" },
+1
View File
@@ -66,6 +66,7 @@ export async function createGameRunner(
gameMap.gameMap,
gameMap.miniGameMap,
config,
gameMap.teamGameSpawnAreas,
);
const gr = new GameRunner(
+3
View File
@@ -530,6 +530,9 @@ export class DefaultConfig implements Config {
return 80;
}
boatMaxNumber(): number {
if (this.isUnitDisabled(UnitType.TransportShip)) {
return 0;
}
return 3;
}
numSpawnPhaseTurns(): number {
+21
View File
@@ -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();
+25 -4
View File
@@ -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);
}
}
+10
View File
@@ -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<string, SpawnArea[]>;
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[];
+32 -2
View File
@@ -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,
+23 -4
View File
@@ -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<GameMapType, TerrainMapData>();
const loadedMaps = new Map<string, TerrainMapData>();
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<TerrainMapData> {
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;
}