Files
OpenFrontIO/src/core/execution/SpawnExecution.ts
T
FloPinguin 339ace0bd6 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
2026-02-23 16:12:24 -06:00

168 lines
4.0 KiB
TypeScript

import {
Execution,
Game,
Player,
PlayerInfo,
PlayerType,
SpawnArea,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { PseudoRandom } from "../PseudoRandom";
import { GameID } from "../Schemas";
import { simpleHash } from "../Util";
import { BotExecution } from "./BotExecution";
import { PlayerExecution } from "./PlayerExecution";
import { getSpawnTiles } from "./Util";
type Spawn = { center: TileRef; tiles: TileRef[] };
export class SpawnExecution implements Execution {
private random: PseudoRandom;
active: boolean = true;
private mg: Game;
private static readonly MAX_SPAWN_TRIES = 1_000;
constructor(
gameID: GameID,
private playerInfo: PlayerInfo,
public tile?: TileRef,
) {
this.random = new PseudoRandom(
simpleHash(playerInfo.id) + simpleHash(gameID),
);
}
init(mg: Game, ticks: number) {
this.mg = mg;
}
tick(ticks: number) {
this.active = false;
if (!this.mg.inSpawnPhase()) {
this.active = false;
return;
}
let player: Player | null = null;
if (this.mg.hasPlayer(this.playerInfo.id)) {
player = this.mg.player(this.playerInfo.id);
} else {
player = this.mg.addPlayer(this.playerInfo);
}
// Security: If random spawn is enabled, prevent players from re-rolling their spawn location
if (this.mg.config().isRandomSpawn() && player.hasSpawned()) {
return;
}
player.tiles().forEach((t) => player.relinquish(t));
const spawn = this.getSpawn(this.tile);
if (!spawn) {
console.warn(`SpawnExecution: cannot spawn ${this.playerInfo.name}`);
return;
}
spawn.tiles.forEach((t) => {
player.conquer(t);
});
if (!player.hasSpawned()) {
this.mg.addExecution(new PlayerExecution(player));
if (player.type() === PlayerType.Bot) {
this.mg.addExecution(new BotExecution(player));
}
}
player.setSpawnTile(spawn.center);
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return true;
}
private getSpawn(center?: TileRef): Spawn | undefined {
if (center !== undefined) {
const tiles = getSpawnTiles(this.mg, center, false);
if (!tiles.length) {
return;
}
return { center, tiles };
}
const spawnArea = this.getTeamSpawnArea();
let tries = 0;
while (tries < SpawnExecution.MAX_SPAWN_TRIES) {
tries++;
const center = this.randTile(spawnArea);
if (
!this.mg.isLand(center) ||
this.mg.hasOwner(center) ||
this.mg.isBorder(center)
) {
continue;
}
const isOtherPlayerSpawnedNearby = this.mg
.allPlayers()
.filter((player) => player.id() !== this.playerInfo.id)
.some((player) => {
const spawnTile = player.spawnTile();
if (spawnTile === undefined) {
return false;
}
return (
this.mg.manhattanDist(spawnTile, center) <
this.mg.config().minDistanceBetweenPlayers()
);
});
if (isOtherPlayerSpawnedNearby) {
continue;
}
const tiles = getSpawnTiles(this.mg, center, true);
if (!tiles) {
// if some of the spawn tile is outside of the land, we want to find another spawn tile
continue;
}
return { center, tiles };
}
return;
}
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);
}
}