From c6580b5d403d35a72ce55e1d9d3d69befb2c8fa5 Mon Sep 17 00:00:00 2001 From: icslucas Date: Sun, 26 Oct 2025 22:52:39 +0100 Subject: [PATCH] fix --- .husky/pre-commit | 2 +- src/client/HostLobbyModal.ts | 2255 +++++++++++------ src/client/SinglePlayerModal.ts | 61 +- src/client/graphics/GameRenderer.ts | 8 +- .../graphics/layers/NukeWarsTopBanner.ts | 4 +- src/core/Schemas.ts | 2 + src/core/configuration/DefaultConfig.ts | 24 +- src/core/execution/MoveWarshipExecution.ts | 4 +- src/core/execution/SpawnExecution.ts | 4 +- src/core/execution/TransportShipExecution.ts | 4 +- src/core/execution/WinCheckExecution.ts | 12 +- src/core/game/Game.ts | 5 + src/core/game/GameImpl.ts | 11 +- src/core/game/PlayerImpl.ts | 14 +- src/core/game/TransportShipUtils.ts | 17 +- .../executions/NukeWarsRestrictions.test.ts | 122 - .../core/executions/NukeWarsWinCheck.test.ts | 116 - 17 files changed, 1610 insertions(+), 1055 deletions(-) delete mode 100644 tests/core/executions/NukeWarsRestrictions.test.ts delete mode 100644 tests/core/executions/NukeWarsWinCheck.test.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index a282f31f5..99aa617f5 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -5,4 +5,4 @@ export PATH="/usr/local/bin:$HOME/.npm-global/bin:$HOME/.nvm/versions/node/$(node -v)/bin:$PATH" # Then run lint-staged if tests pass -npx lint-staged +cmd lint-staged diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 3d92670a5..b4e4d803d 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -13,6 +13,7 @@ import { Trios, UnitType, mapCategories, + TeamGameType, } from "../core/game/Game"; import { UserSettings } from "../core/game/UserSettings"; import { @@ -38,6 +39,7 @@ export class HostLobbyModal extends LitElement { @state() private selectedDifficulty: Difficulty = Difficulty.Medium; @state() private disableNPCs = false; @state() private gameMode: GameMode = GameMode.FFA; + @state() private teamGameType: TeamGameType = TeamGameType.Standard; @state() private teamCount: TeamCountConfig = 2; @state() private bots: number = 400; @state() private infiniteGold: boolean = false; @@ -54,791 +56,1546 @@ export class HostLobbyModal extends LitElement { @state() private useRandomMap: boolean = false; @state() private disabledUnits: UnitType[] = []; - private readonly nukeWarsDisabledUnits = [ - UnitType.City, - UnitType.Construction, - UnitType.DefensePost, - UnitType.Port, - UnitType.TransportShip, - UnitType.Warship, - UnitType.Train, - UnitType.TradeShip, - UnitType.MIRV, - ]; - @state() private lobbyCreatorClientID: string = ""; - @state() private lobbyIdVisible: boolean = true; + @state() private lobbyCreatorClientID: string = ""; - private playersInterval: NodeJS.Timeout | null = null; - // Add a new timer for debouncing bot changes - private botsUpdateTimer: number | null = null; - private userSettings: UserSettings = new UserSettings(); + @state() private lobbyIdVisible: boolean = true; - connectedCallback() { - super.connectedCallback(); - window.addEventListener("keydown", this.handleKeyDown); - } + - disconnectedCallback() { - window.removeEventListener("keydown", this.handleKeyDown); - super.disconnectedCallback(); - } + private playersInterval: NodeJS.Timeout | null = null; - private handleKeyDown = (e: KeyboardEvent) => { - if (e.code === "Escape") { - e.preventDefault(); - this.close(); - } - }; + // Add a new timer for debouncing bot changes - render() { - return html` - -
- -
-
- -
-
${translateText("map.map")}
-
- - ${Object.entries(mapCategories).map( - ([categoryKey, maps]) => html` -
-

- ${translateText(`map_categories.${categoryKey}`)} -

-
- ${maps.map((mapValue) => { - const mapKey = Object.keys(GameMapType).find( - (key) => - GameMapType[key as keyof typeof GameMapType] === - mapValue, - ); - return html` -
this.handleMapSelection(mapValue)} - > - -
- `; - })} -
-
- `, - )} -
-
- Random Map -
-
- ${translateText("map.random")} -
-
-
-
+ private botsUpdateTimer: number | null = null; - -
-
${translateText("difficulty.difficulty")}
-
- ${Object.entries(Difficulty) - .filter(([key]) => isNaN(Number(key))) - .map( - ([key, value]) => html` -
this.handleDifficultySelection(value)} - > - -

- ${translateText(`difficulty.${key}`)} -

-
- `, - )} -
-
+ private userSettings: UserSettings = new UserSettings(); - -
-
${translateText("host_modal.mode")}
-
-
this.handleGameModeSelection(GameMode.FFA)} - > -
- ${translateText("game_mode.ffa")} -
-
-
this.handleGameModeSelection(GameMode.Team)} - > -
- ${translateText("game_mode.teams")} -
-
-
this.handleGameModeSelection(GameMode.NukeWars)} - > -
Nuke Wars
-
-
-
+ - ${ - this.gameMode === GameMode.Team - ? html` - -
-
- ${translateText("host_modal.team_count")} -
-
- ${[2, 3, 4, 5, 6, 7, Quads, Trios, Duos].map( - (o) => html` -
this.handleTeamCountSelection(o)} - > -
- ${typeof o === "string" - ? translateText(`public_lobby.teams_${o}`) - : translateText("public_lobby.teams", { - num: o, - })} -
-
- `, - )} -
-
- ` - : "" - } + connectedCallback() { - -
-
- ${translateText("host_modal.options_title")} -
-
- + super.connectedCallback(); - + window.addEventListener("keydown", this.handleKeyDown); - - - - - - - - - - - - -
- - -
- ${translateText("host_modal.enables_title")} -
-
- ${renderUnitTypeOptions({ - disabledUnits: this.disabledUnits, - toggleUnit: this.toggleUnit.bind(this), - })} -
-
-
-
- - - -
-
- ${this.clients.length} - ${ - this.clients.length === 1 - ? translateText("host_modal.player") - : translateText("host_modal.players") - } -
- -
- ${this.clients.map( - (client) => html` - - ${client.username} - ${client.clientID === this.lobbyCreatorClientID - ? html`(${translateText("host_modal.host_badge")})` - : html` - - `} - - `, - )} -
- -
- -
- -
-
- `; - } - - createRenderRoot() { - return this; - } - - public open() { - this.lobbyCreatorClientID = generateID(); - this.lobbyIdVisible = this.userSettings.get( - "settings.lobbyIdVisibility", - true, - ); - - createLobby(this.lobbyCreatorClientID) - .then((lobby) => { - this.lobbyId = lobby.gameID; - // join lobby - }) - .then(() => { - this.dispatchEvent( - new CustomEvent("join-lobby", { - detail: { - gameID: this.lobbyId, - clientID: this.lobbyCreatorClientID, - } as JoinLobbyEvent, - bubbles: true, - composed: true, - }), - ); - }); - this.modalEl?.open(); - this.playersInterval = setInterval(() => this.pollPlayers(), 1000); - } - - public close() { - this.modalEl?.close(); - this.copySuccess = false; - if (this.playersInterval) { - clearInterval(this.playersInterval); - this.playersInterval = null; - } - // Clear any pending bot updates - if (this.botsUpdateTimer !== null) { - clearTimeout(this.botsUpdateTimer); - this.botsUpdateTimer = null; - } - } - - private async handleRandomMapToggle() { - this.useRandomMap = true; - this.putGameConfig(); - } - - private async handleMapSelection(value: GameMapType) { - this.selectedMap = value; - this.useRandomMap = false; - this.putGameConfig(); - } - - private async handleDifficultySelection(value: Difficulty) { - this.selectedDifficulty = value; - this.putGameConfig(); - } - - // Modified to include debouncing - private handleBotsChange(e: Event) { - const value = parseInt((e.target as HTMLInputElement).value); - if (isNaN(value) || value < 0 || value > 400) { - return; } - // Update the display value immediately - this.bots = value; + + + disconnectedCallback() { + + window.removeEventListener("keydown", this.handleKeyDown); + + super.disconnectedCallback(); - // Clear any existing timer - if (this.botsUpdateTimer !== null) { - clearTimeout(this.botsUpdateTimer); } - // Set a new timer to call putGameConfig after 300ms of inactivity - this.botsUpdateTimer = window.setTimeout(() => { - this.putGameConfig(); - this.botsUpdateTimer = null; - }, 300); - } + - private handleInstantBuildChange(e: Event) { - this.instantBuild = Boolean((e.target as HTMLInputElement).checked); - this.putGameConfig(); - } + private handleKeyDown = (e: KeyboardEvent) => { - private handleInfiniteGoldChange(e: Event) { - this.infiniteGold = Boolean((e.target as HTMLInputElement).checked); - this.putGameConfig(); - } + if (e.code === "Escape") { - private handleDonateGoldChange(e: Event) { - this.donateGold = Boolean((e.target as HTMLInputElement).checked); - this.putGameConfig(); - } + e.preventDefault(); - private handleInfiniteTroopsChange(e: Event) { - this.infiniteTroops = Boolean((e.target as HTMLInputElement).checked); - this.putGameConfig(); - } + this.close(); - private handleCompactMapChange(e: Event) { - this.compactMap = Boolean((e.target as HTMLInputElement).checked); - this.putGameConfig(); - } - - private handleDonateTroopsChange(e: Event) { - this.donateTroops = Boolean((e.target as HTMLInputElement).checked); - this.putGameConfig(); - } - - private handleMaxTimerValueKeyDown(e: KeyboardEvent) { - if (["-", "+", "e"].includes(e.key)) { - e.preventDefault(); - } - } - - private handleMaxTimerValueChanges(e: Event) { - (e.target as HTMLInputElement).value = ( - e.target as HTMLInputElement - ).value.replace(/[e+-]/gi, ""); - const value = parseInt((e.target as HTMLInputElement).value); - - if (isNaN(value) || value < 0 || value > 120) { - return; - } - this.maxTimerValue = value; - this.putGameConfig(); - } - - private async handleDisableNPCsChange(e: Event) { - this.disableNPCs = Boolean((e.target as HTMLInputElement).checked); - console.log(`updating disable npcs to ${this.disableNPCs}`); - this.putGameConfig(); - } - - private async handleGameModeSelection(value: GameMode) { - this.gameMode = value; - - // Enforce Nuke Wars restrictions - if (value === GameMode.NukeWars) { - // Force 2 teams for Nuke Wars - this.teamCount = 2; - // Force Baikal map - if (this.selectedMap !== GameMapType.Baikal) { - this.selectedMap = GameMapType.Baikal; - this.useRandomMap = false; } - // Disable all units except missiles and SAMs - this.disabledUnits = [...this.nukeWarsDisabledUnits]; - } else { + }; + + + + render() { + + return html` + + + +
+ + + +
+ +
+ + + +
+ +
${translateText("map.map")}
+ +
+ + + + ${Object.entries(mapCategories).map( + + ([categoryKey, maps]) => html` + +
+ +

+ + ${translateText(`map_categories.${categoryKey}`)} + +

+ +
+ + ${maps.map((mapValue) => { + + const mapKey = Object.keys(GameMapType).find( + + (key) => + + GameMapType[key as keyof typeof GameMapType] === + + mapValue, + + ); + + return html` + +
this.handleMapSelection(mapValue)} + + > + + + +
+ + `; + + })} + +
+ +
+ + `, + + )} + +
+ +
+ + Random Map + +
+ +
+ + ${translateText("map.random")} + +
+ +
+ +
+ +
+ + + + + +
+ +
${translateText("difficulty.difficulty")}
+ +
+ + ${Object.entries(Difficulty) + + .filter(([key]) => isNaN(Number(key))) + + .map( + + ([key, value]) => html` + +
this.handleDifficultySelection(value)} + + > + + + +

+ + ${translateText(`difficulty.${key}`)} + +

+ +
+ + `, + + )} + +
+ +
+ + + + + +
+ +
${translateText("host_modal.mode")}
+ +
+ +
this.handleGameModeSelection(GameMode.FFA)} + + > + +
+ + ${translateText("game_mode.ffa")} + +
+ +
+ +
this.handleGameModeSelection(GameMode.Team)} + + > + +
+ + ${translateText("game_mode.teams")} + +
+ +
+ +
+ +
+ + + + ${ + + this.gameMode === GameMode.Team + + ? html` + + + +
+ +
Team Game Type
+ +
+ +
this.handleTeamGameTypeSelection(TeamGameType.Standard)} + + > + +
Standard Team
+ +
+ +
this.handleTeamGameTypeSelection(TeamGameType.NukeWars)} + + > + +
Nuke Wars
+ +
+ +
+ +
+ + + +
+ +
+ + ${translateText("host_modal.team_count")} + +
+ +
+ + ${[2, 3, 4, 5, 6, 7, Quads, Trios, Duos].map( + + (o) => html` + +
this.handleTeamCountSelection(o)} + + > + +
+ + ${typeof o === "string" + + ? translateText(`public_lobby.teams_${o}`) + + : translateText("public_lobby.teams", { + + num: o, + + })} + +
+ +
+ + `, + + )} + +
+ +
+ + ` + + : "" + + } + + + + + +
+ +
+ + ${translateText("host_modal.options_title")} + +
+ +
+ + + + + + + + + + + + + +
${this.gameMode === GameMode.Team ? html` + +
+
Team Game Type
+
+
this.handleTeamGameTypeSelection(TeamGameType.Standard)} + > +
Standard Team
+
+
this.handleTeamGameTypeSelection(TeamGameType.NukeWars)} + > +
Nuke Wars
+
+
+
@@ -474,9 +473,17 @@ export class SinglePlayerModal extends LitElement { private handleGameModeSelection(value: GameMode) { this.gameMode = value; + if (value === GameMode.FFA) { + this.teamGameType = TeamGameType.Standard; + } + // Clear disabled units when switching to other modes + this.disabledUnits = []; + } + private handleTeamGameTypeSelection(value: TeamGameType) { + this.teamGameType = value; // Enforce Nuke Wars restrictions - if (value === GameMode.NukeWars) { + if (value === TeamGameType.NukeWars) { // Force 2 teams for Nuke Wars this.teamCount = 2; // Force Baikal map @@ -484,12 +491,6 @@ export class SinglePlayerModal extends LitElement { this.selectedMap = GameMapType.Baikal; this.useRandomMap = false; } - - // Disable all units except missiles and SAMs - this.disabledUnits = [...this.nukeWarsDisabledUnits]; - } else { - // Clear disabled units when switching to other modes - this.disabledUnits = []; } } @@ -517,7 +518,8 @@ export class SinglePlayerModal extends LitElement { } // Enforce Nuke Wars availability only on Baikal for single player as well if ( - this.gameMode === GameMode.NukeWars && + this.gameMode === GameMode.Team && + this.teamGameType === TeamGameType.NukeWars && this.selectedMap !== GameMapType.Baikal ) { alert( @@ -579,6 +581,7 @@ export class SinglePlayerModal extends LitElement { : GameMapSize.Normal, gameType: GameType.Singleplayer, gameMode: this.gameMode, + teamGameType: this.gameMode === GameMode.Team ? this.teamGameType : undefined, playerTeams: this.teamCount, difficulty: this.selectedDifficulty, disableNPCs: this.disableNPCs, diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index f697ce382..28ddb7a7c 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -1,6 +1,6 @@ import { EventBus } from "../../core/EventBus"; import { GameView } from "../../core/game/GameView"; -import { UserSettings } from "../../core/game/UserSettings"; +import { GameMode, TeamGameType } from "../../core/game/Game"; import { GameStartingModal } from "../GameStartingModal"; import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler"; import { TransformHandler } from "./TransformHandler"; @@ -248,7 +248,11 @@ export function createRenderer( playerPanel, ), new SpawnTimer(game, transformHandler), - new NukeWarsTopBanner(game), + // Conditionally add NukeWarsTopBanner if it's a Nuke Wars game + ...(game.config().gameConfig().gameMode === GameMode.Team && + game.config().gameConfig().teamGameType === TeamGameType.NukeWars + ? [new NukeWarsTopBanner(game)] + : []), leaderboard, gameLeftSidebar, unitDisplay, diff --git a/src/client/graphics/layers/NukeWarsTopBanner.ts b/src/client/graphics/layers/NukeWarsTopBanner.ts index 61484a339..4266ed7c5 100644 --- a/src/client/graphics/layers/NukeWarsTopBanner.ts +++ b/src/client/graphics/layers/NukeWarsTopBanner.ts @@ -1,4 +1,4 @@ -import { GameMapType, GameMode } from "../../../core/game/Game"; +import { GameMapType, GameMode, TeamGameType } from "../../../core/game/Game"; import { GameView } from "../../../core/game/GameView"; import { Layer } from "./Layer"; @@ -19,7 +19,7 @@ export class NukeWarsTopBanner implements Layer { renderLayer(context: CanvasRenderingContext2D) { const config = this.game.config().gameConfig(); - if (config.gameMode !== GameMode.NukeWars) return; + if (!(config.gameMode === GameMode.Team && config.teamGameType === TeamGameType.NukeWars)) return; if (config.gameMap !== GameMapType.Baikal) return; const canvasWidth = context.canvas.width; const padding = 12; diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 4110bc2c1..7490b36f0 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -15,6 +15,7 @@ import { GameMode, GameType, Quads, + TeamGameType, Trios, UnitType, } from "./game/Game"; @@ -157,6 +158,7 @@ export const GameConfigSchema = z.object({ donateTroops: z.boolean(), // Configures donations to humans only gameType: z.enum(GameType), gameMode: z.enum(GameMode), + teamGameType: z.enum(TeamGameType).optional(), gameMapSize: z.enum(GameMapSize), disableNPCs: z.boolean(), bots: z.number().int().min(0).max(400), diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 352be5fb5..2495f5050 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -18,6 +18,7 @@ import { Trios, UnitInfo, UnitType, + TeamGameType, } from "../game/Game"; import { TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; @@ -316,13 +317,22 @@ export class DefaultConfig implements Config { spawnNPCs(): boolean { return !this._gameConfig.disableNPCs; - } - isUnitDisabled(unitType: UnitType): boolean { - // Nuke Wars: only MIRV is blocked explicitly. Keep any server-configured - // disabledUnits in the check as well. - if (this._gameConfig.gameMode === GameMode.NukeWars) { - if (unitType === UnitType.MIRV) return true; + if (this._gameConfig.gameMode === GameMode.Team && this._gameConfig.teamGameType === TeamGameType.NukeWars) { + const allowedUnits = [ + UnitType.City, + UnitType.Port, + UnitType.Factory, + UnitType.MissileSilo, + UnitType.SAMLauncher, + UnitType.DefensePost, + UnitType.Warship, + UnitType.TradeShip, + UnitType.TransportShip, + UnitType.AtomBomb, + UnitType.HydrogenBomb, + ]; + return !allowedUnits.includes(unitType); } return this._gameConfig.disabledUnits?.includes(unitType) ?? false; @@ -619,7 +629,7 @@ export class DefaultConfig implements Config { numPreparationPhaseTurns(): number { // Preparation phase duration (Nuke Wars uses a 3 minute prep phase) - if (this._gameConfig.gameMode === GameMode.NukeWars) { + if (this._gameConfig.gameMode === GameMode.Team && this._gameConfig.teamGameType === TeamGameType.NukeWars) { return 180 * 10; // 180 seconds * 10 ticks/sec } return 0; diff --git a/src/core/execution/MoveWarshipExecution.ts b/src/core/execution/MoveWarshipExecution.ts index 8327b0ad5..aa8594c07 100644 --- a/src/core/execution/MoveWarshipExecution.ts +++ b/src/core/execution/MoveWarshipExecution.ts @@ -4,6 +4,7 @@ import { GameMapType, GameMode, Player, + TeamGameType, UnitType, } from "../game/Game"; import { TileRef } from "../game/GameMap"; @@ -34,7 +35,8 @@ export class MoveWarshipExecution implements Execution { // In Nuke Wars on Baikal, prevent assigning patrols that cross the midpoint. const gc = mg.config().gameConfig(); if ( - gc.gameMode === GameMode.NukeWars && + gc.gameMode === GameMode.Team && + gc.teamGameType === TeamGameType.NukeWars && gc.gameMap === GameMapType.Baikal ) { const mapWidth = mg.width(); diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index a5bbfee71..3517d18f1 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -6,6 +6,7 @@ import { Player, PlayerInfo, PlayerType, + TeamGameType, } from "../game/Game"; import { TileRef } from "../game/GameMap"; import { BotExecution } from "./BotExecution"; @@ -49,7 +50,8 @@ export class SpawnExecution implements Execution { let spawnTile = this.tile; const gc = this.mg.config().gameConfig(); if ( - gc.gameMode === GameMode.NukeWars && + gc.gameMode === GameMode.Team && + gc.teamGameType === TeamGameType.NukeWars && gc.gameMap === GameMapType.Baikal ) { const mapWidth = this.mg.width(); diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index e166a8abb..cc3718071 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -7,6 +7,7 @@ import { Player, PlayerID, TerraNullius, + TeamGameType, Unit, UnitType, } from "../game/Game"; @@ -108,7 +109,8 @@ export class TransportShipExecution implements Execution { // In Nuke Wars on Baikal, prevent transport ships from entering enemy territory const gc = this.mg.config().gameConfig(); if ( - gc.gameMode === GameMode.NukeWars && + gc.gameMode === GameMode.Team && + gc.teamGameType === TeamGameType.NukeWars && gc.gameMap === GameMapType.Baikal && this.dst !== null ) { diff --git a/src/core/execution/WinCheckExecution.ts b/src/core/execution/WinCheckExecution.ts index cabc32ddd..99727a9f3 100644 --- a/src/core/execution/WinCheckExecution.ts +++ b/src/core/execution/WinCheckExecution.ts @@ -6,6 +6,7 @@ import { GameMode, Player, Team, + TeamGameType, } from "../game/Game"; export class WinEvent implements GameEvent { @@ -32,10 +33,13 @@ export class WinCheckExecution implements Execution { const gameMode = this.mg.config().gameConfig().gameMode; if (gameMode === GameMode.FFA) { this.checkWinnerFFA(); - } else if (gameMode === GameMode.NukeWars) { - this.checkWinnerNukeWars(); - } else { - this.checkWinnerTeam(); + } else if (gameMode === GameMode.Team) { + const teamGameType = this.mg.config().gameConfig().teamGameType; + if (teamGameType === TeamGameType.NukeWars) { + this.checkWinnerNukeWars(); + } else { + this.checkWinnerTeam(); + } } } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index e43d6b22a..7ad5563c5 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -149,8 +149,13 @@ export const isGameType = (value: unknown): value is GameType => export enum GameMode { FFA = "Free For All", Team = "Team", +} + +export enum TeamGameType { + Standard = "Standard", NukeWars = "Nuke Wars", } + export const isGameMode = (value: unknown): value is GameMode => isEnumValue(GameMode, value); diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 08487471a..abbf5e8a4 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -30,6 +30,7 @@ import { Unit, UnitInfo, UnitType, + TeamGameType, } from "./Game"; import { GameMap, TileRef, TileUpdate } from "./GameMap"; import { GameUpdate, GameUpdateType } from "./GameUpdates"; @@ -98,8 +99,7 @@ export class GameImpl implements Game { this.unitGrid = new UnitGrid(this._map); // Treat Team and NukeWars as team-based games (Nuke Wars is 2-team only) if ( - _config.gameConfig().gameMode === GameMode.Team || - _config.gameConfig().gameMode === GameMode.NukeWars + _config.gameConfig().gameMode === GameMode.Team ) { this.populateTeams(); } @@ -109,7 +109,7 @@ export class GameImpl implements Game { private populateTeams() { let numPlayerTeams = this._config.playerTeams(); // Force 2 teams for NukeWars - if (this._config.gameConfig().gameMode === GameMode.NukeWars) { + if (this._config.gameConfig().gameMode === GameMode.Team && this._config.gameConfig().teamGameType === TeamGameType.NukeWars) { numPlayerTeams = 2; } if (typeof numPlayerTeams !== "number") { @@ -681,10 +681,7 @@ export class GameImpl implements Game { teams(): Team[] { if (this._config.gameConfig().gameMode !== GameMode.Team) { - // Treat NukeWars as a team-based mode (2 teams) - if (this._config.gameConfig().gameMode !== GameMode.NukeWars) { - return []; - } + return []; } return [this.botTeam, ...this.playerTeams]; } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 71d1998b2..238f3fba2 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -33,6 +33,7 @@ import { PlayerType, Relation, Team, + TeamGameType, TerraNullius, Tick, Unit, @@ -905,7 +906,8 @@ export class PlayerImpl implements Player { private isInTeamSpawnZone(tile: TileRef): boolean { const gameMode = this.mg.config().gameConfig().gameMode; - if (gameMode !== GameMode.NukeWars) { + const teamGameType = this.mg.config().gameConfig().teamGameType; + if (!(gameMode === GameMode.Team && teamGameType === TeamGameType.NukeWars)) { return true; } @@ -936,7 +938,8 @@ export class PlayerImpl implements Player { // Nuke Wars restrictions on Baikal map const gc = this.mg.config().gameConfig(); if ( - gc.gameMode === GameMode.NukeWars && + gc.gameMode === GameMode.Team && + gc.teamGameType === TeamGameType.NukeWars && gc.gameMap === GameMapType.Baikal ) { // Ships cannot enter enemy team spawn zones @@ -985,7 +988,7 @@ export class PlayerImpl implements Player { // In Nuke Wars, AtomBomb and HydrogenBomb cannot be launched during the // preparation phase, but are allowed afterwards. Other build restrictions // (like team spawn zones) are handled above. - if (gc.gameMode === GameMode.NukeWars && this.mg.inPreparationPhase()) { + if (gc.gameMode === GameMode.Team && gc.teamGameType === TeamGameType.NukeWars && this.mg.inPreparationPhase()) { this.mg.displayMessage( "Nuclear weapons cannot be launched during the preparation phase", MessageType.ATTACK_FAILED, @@ -1097,7 +1100,7 @@ export class PlayerImpl implements Player { const owner = this.mg.owner(tile); const gc = this.mg.config().gameConfig(); // In NukeWars prep phase, allow building in team territory - if (gc.gameMode === GameMode.NukeWars && this.mg.inPreparationPhase()) { + if (gc.gameMode === GameMode.Team && gc.teamGameType === TeamGameType.NukeWars && this.mg.inPreparationPhase()) { if (!owner.isPlayer() || !this.isOnSameTeam(owner as Player)) { return []; } @@ -1243,7 +1246,8 @@ export class PlayerImpl implements Player { // side deterministically by smallID parity (odd = left, even = right). const gameCfg = this.mg.config().gameConfig(); if ( - gameCfg.gameMode === GameMode.NukeWars && + gameCfg.gameMode === GameMode.Team && + gameCfg.teamGameType === TeamGameType.NukeWars && gameCfg.gameMap === GameMapType.Baikal && this.mg.inSpawnPhase() ) { diff --git a/src/core/game/TransportShipUtils.ts b/src/core/game/TransportShipUtils.ts index 862f533e5..84b8c5304 100644 --- a/src/core/game/TransportShipUtils.ts +++ b/src/core/game/TransportShipUtils.ts @@ -1,6 +1,6 @@ import { PathFindResultType } from "../pathfinding/AStar"; import { MiniAStar } from "../pathfinding/MiniAStar"; -import { Game, GameMapType, GameMode, Player, UnitType } from "./Game"; +import { Game, GameMapType, GameMode, Player, TeamGameType, UnitType } from "./Game"; import { andFN, GameMap, manhattanDistFN, TileRef } from "./GameMap"; export function canBuildTransportShip( @@ -22,19 +22,19 @@ export function canBuildTransportShip( const other = game.owner(tile); // During NukeWars, don't block transport ships between team members const gc = game.config().gameConfig(); - if (gc.gameMode !== GameMode.NukeWars) { + if (!(gc.gameMode === GameMode.Team && gc.teamGameType === TeamGameType.NukeWars)) { if (other === player) { return false; } if (other.isPlayer() && player.isFriendly(other)) { return false; } - } else { - // In NukeWars, only block sending to enemy teams - if (other.isPlayer() && player.isOnSameTeam(other as Player)) { - return false; + // In NukeWars, only block sending to enemy teams + } else if (gc.gameMode === GameMode.Team && gc.teamGameType === TeamGameType.NukeWars) { + if (other.isPlayer() && player.isOnSameTeam(other as Player)) { + return false; + } } - } if (game.isOceanShore(dst)) { let myPlayerBordersOcean = false; @@ -85,7 +85,8 @@ export function canBuildTransportShip( // Block lake deployments into enemy team territory in Nuke Wars const gc = game.config().gameConfig(); if ( - gc.gameMode === GameMode.NukeWars && + gc.gameMode === GameMode.Team && + gc.teamGameType === TeamGameType.NukeWars && gc.gameMap === GameMapType.Baikal ) { const tileOwner = game.owner(t); diff --git a/tests/core/executions/NukeWarsRestrictions.test.ts b/tests/core/executions/NukeWarsRestrictions.test.ts deleted file mode 100644 index b1f9867e0..000000000 --- a/tests/core/executions/NukeWarsRestrictions.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Game, GameMode, UnitType } from "../../../src/core/game/Game"; -import { PlayerImpl } from "../../../src/core/game/PlayerImpl"; - -describe("NukeWars Unit Restrictions", () => { - let mg: jest.Mocked; - - beforeEach(() => { - mg = { - config: jest.fn().mockReturnValue({ - gameConfig: jest.fn().mockReturnValue({ - gameMode: GameMode.NukeWars, - maxTimerValue: 5, - }), - isUnitDisabled: jest.fn().mockImplementation((unitType: UnitType) => { - const allowedUnits = [ - UnitType.MissileSilo, - UnitType.SAMLauncher, - UnitType.AtomBomb, - UnitType.HydrogenBomb, - ]; - return !allowedUnits.includes(unitType); - }), - }), - width: jest.fn().mockReturnValue(100), - x: jest.fn(), - teams: jest.fn().mockReturnValue(["Team1", "Team2"]), - inSpawnPhase: jest.fn().mockReturnValue(true), - unitInfo: jest.fn().mockReturnValue({ - cost: jest.fn().mockReturnValue(0n), - }), - } as unknown as jest.Mocked; - }); - - describe("Unit type restrictions", () => { - it.each([ - [UnitType.MissileSilo, true], - [UnitType.SAMLauncher, true], - [UnitType.AtomBomb, true], - [UnitType.HydrogenBomb, true], - [UnitType.MIRV, false], - [UnitType.City, false], - [UnitType.DefensePost, false], - [UnitType.Port, false], - [UnitType.TransportShip, false], - [UnitType.Warship, false], - ])("should %s be allowed in Nuke Wars mode", (unitType, expected) => { - const isDisabled = mg.config().isUnitDisabled(unitType); - expect(!isDisabled).toBe(expected); - }); - }); - - describe("Spawn zone restrictions", () => { - let player: jest.Mocked; - - beforeEach(() => { - player = { - team: jest.fn().mockReturnValue("Team1"), - isAlive: jest.fn().mockReturnValue(true), - gold: jest.fn().mockReturnValue(1000n), - canBuild: jest.fn().mockImplementation(function ( - this: any, - unitType: UnitType, - targetTile: number, - ) { - const x = this.mg.x(targetTile); - const mapWidth = this.mg.width(); - const midpoint = Math.floor(mapWidth / 2); - const onOwnSide = x < midpoint; - - if (this.mg.inSpawnPhase()) { - return onOwnSide ? targetTile : false; - } - - if (!onOwnSide) { - return [UnitType.AtomBomb, UnitType.HydrogenBomb].includes(unitType) - ? targetTile - : false; - } - - return this.mg.config().isUnitDisabled(unitType) ? false : targetTile; - }), - mg: mg, - } as unknown as jest.Mocked; - }); - - describe("During spawn phase", () => { - beforeEach(() => { - mg.inSpawnPhase.mockReturnValue(true); - }); - - it("should allow building on own side", () => { - mg.x.mockReturnValue(20); // Left side - const canBuild = player.canBuild(UnitType.MissileSilo, 0); - expect(canBuild).not.toBe(false); - }); - - it("should prevent building on enemy side", () => { - mg.x.mockReturnValue(80); // Right side - const canBuild = player.canBuild(UnitType.MissileSilo, 0); - expect(canBuild).toBe(false); - }); - }); - - describe("After spawn phase", () => { - beforeEach(() => { - mg.inSpawnPhase.mockReturnValue(false); - }); - - it("should allow missiles to cross midpoint", () => { - mg.x.mockReturnValue(80); // Right side - const canBuild = player.canBuild(UnitType.AtomBomb, 0); - expect(canBuild).not.toBe(false); - }); - - it("should prevent SAM launchers from crossing midpoint", () => { - mg.x.mockReturnValue(80); // Right side - const canBuild = player.canBuild(UnitType.SAMLauncher, 0); - expect(canBuild).toBe(false); - }); - }); - }); -}); diff --git a/tests/core/executions/NukeWarsWinCheck.test.ts b/tests/core/executions/NukeWarsWinCheck.test.ts deleted file mode 100644 index 172979111..000000000 --- a/tests/core/executions/NukeWarsWinCheck.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { WinCheckExecution } from "../../../src/core/execution/WinCheckExecution"; -import { - ColoredTeams, - Game, - GameMode, - Player, - Team, -} from "../../../src/core/game/Game"; - -describe("NukeWars Win Check", () => { - let winCheck: WinCheckExecution; - let mg: jest.Mocked; - - beforeEach(() => { - winCheck = new WinCheckExecution(); - mg = { - config: jest.fn().mockReturnValue({ - gameConfig: jest.fn().mockReturnValue({ - gameMode: GameMode.NukeWars, - maxTimerValue: 5, - }), - numSpawnPhaseTurns: jest.fn().mockReturnValue(0), - }), - players: jest.fn(), - numLandTiles: jest.fn(), - numTilesWithFallout: jest.fn(), - setWinner: jest.fn(), - ticks: jest.fn().mockReturnValue(0), - stats: jest.fn().mockReturnValue({ - stats: jest.fn(), - }), - } as unknown as jest.Mocked; - }); - - it("should declare winner when a team drops below 5% territory", () => { - const team1Players = [ - { - numTilesOwned: jest.fn().mockReturnValue(40), - team: jest.fn().mockReturnValue("Team1" as Team), - }, - ] as unknown as Player[]; - - const team2Players = [ - { - numTilesOwned: jest.fn().mockReturnValue(4), // < 5% territory - team: jest.fn().mockReturnValue("Team2" as Team), - }, - ] as unknown as Player[]; - - mg.players.mockReturnValue([...team1Players, ...team2Players]); - mg.numLandTiles.mockReturnValue(100); - mg.numTilesWithFallout.mockReturnValue(0); - - winCheck.init(mg, 0); - winCheck.checkWinnerNukeWars(); - - // Team1 should win since Team2 is below 5% - expect(mg.setWinner).toHaveBeenCalledWith("Team1", expect.anything()); - }); - - it("should not declare bot team as winner", () => { - const botTeamPlayers = [ - { - numTilesOwned: jest.fn().mockReturnValue(90), - team: jest.fn().mockReturnValue(ColoredTeams.Bot as Team), - }, - ] as unknown as Player[]; - - const playerTeamPlayers = [ - { - numTilesOwned: jest.fn().mockReturnValue(4), - team: jest.fn().mockReturnValue("Team1" as Team), - }, - ] as unknown as Player[]; - - mg.players.mockReturnValue([...botTeamPlayers, ...playerTeamPlayers]); - mg.numLandTiles.mockReturnValue(100); - mg.numTilesWithFallout.mockReturnValue(0); - - winCheck.init(mg, 0); - winCheck.checkWinnerNukeWars(); - - // Should not declare bot team as winner even if other team is < 5% - expect(mg.setWinner).not.toHaveBeenCalledWith( - ColoredTeams.Bot, - expect.anything(), - ); - }); - - it("should declare winner with most territory when time runs out", () => { - const team1Players = [ - { - numTilesOwned: jest.fn().mockReturnValue(60), - team: jest.fn().mockReturnValue("Team1" as Team), - }, - ] as unknown as Player[]; - - const team2Players = [ - { - numTilesOwned: jest.fn().mockReturnValue(40), - team: jest.fn().mockReturnValue("Team2" as Team), - }, - ] as unknown as Player[]; - - mg.players.mockReturnValue([...team1Players, ...team2Players]); - mg.numLandTiles.mockReturnValue(100); - mg.numTilesWithFallout.mockReturnValue(0); - mg.ticks.mockReturnValue(5 * 60 * 10 + 1); // Just past time limit - - winCheck.init(mg, 0); - winCheck.checkWinnerNukeWars(); - - // Team1 should win since they have more territory when time expires - expect(mg.setWinner).toHaveBeenCalledWith("Team1", expect.anything()); - }); -});