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; }