From 27fbb71b55dad1877e51bd808a71da4fabb40d4d Mon Sep 17 00:00:00 2001 From: Restart2008 Date: Sat, 25 Oct 2025 14:36:58 -0700 Subject: [PATCH] feat(nukewars): Implement Nuke Wars game mode This commit introduces the Nuke Wars game mode, a team-based nuclear warfare scenario on the Baikal map. Key features and rule implementations: - Movement Restrictions: Implemented midpoint crossing restrictions for units in UnitImpl.ts. Only nuclear missiles, warships, and tradeships can cross the center line. - Preparation Phase: - Added a 3-minute preparation phase at the beginning of the game. - Restricted building to a teams own territory during this phase in PlayerImpl.ts. - Disabled nuke launches during the preparation phase in PlayerImpl.ts. - UI Enhancements: - Added a countdown timer for the preparation phase in GameRightSidebar.ts. - Added a visual line indicator on the Baikal map to separate team territories in TerrainLayer.ts. - Team-based Logic: Ensured Nuke Wars is consistently treated as a team-based game mode, fixing an issue in GameImpl.addPlayers. The implementation aligns with the detailed Nuke Wars game mode specification, including rules on allowed units, win conditions, and map/team setup. --- .../graphics/layers/GameRightSidebar.ts | 43 +++++++++++++------ src/client/graphics/layers/TerrainLayer.ts | 16 +++++++ src/core/game/GameImpl.ts | 2 +- src/core/game/PlayerImpl.ts | 22 ++++++++++ src/core/game/UnitImpl.ts | 29 +++++++++++++ 5 files changed, 99 insertions(+), 13 deletions(-) diff --git a/src/client/graphics/layers/GameRightSidebar.ts b/src/client/graphics/layers/GameRightSidebar.ts index c4215215c..18e9e0955 100644 --- a/src/client/graphics/layers/GameRightSidebar.ts +++ b/src/client/graphics/layers/GameRightSidebar.ts @@ -7,7 +7,7 @@ import replayRegularIcon from "../../../../resources/images/ReplayRegularIconWhi import replaySolidIcon from "../../../../resources/images/ReplaySolidIconWhite.svg"; import settingsIcon from "../../../../resources/images/SettingIconWhite.svg"; import { EventBus } from "../../../core/EventBus"; -import { GameType } from "../../../core/game/Game"; +import { GameMode, GameType } from "../../../core/game/Game"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView } from "../../../core/game/GameView"; import { PauseGameEvent } from "../../Transport"; @@ -52,23 +52,42 @@ export class GameRightSidebar extends LitElement implements Layer { } tick() { - // Timer logic const updates = this.game.updatesSinceLastTick(); if (updates) { this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0; } - const maxTimerValue = this.game.config().gameConfig().maxTimerValue; - if (maxTimerValue !== undefined) { - if (this.game.inSpawnPhase()) { + + if (this.hasWinner) { + return; + } + + const isNukeWars = + this.game.config().gameConfig().gameMode === GameMode.NukeWars; + const spawnTurns = this.game.config().numSpawnPhaseTurns(); + const prepTurns = this.game.config().numPreparationPhaseTurns(); + const ticks = this.game.ticks(); + + if (ticks <= spawnTurns) { + // Spawn phase + const maxTimerValue = this.game.config().gameConfig().maxTimerValue; + if (maxTimerValue !== undefined) { this.timer = maxTimerValue * 60; - } else if (!this.hasWinner && this.game.ticks() % 10 === 0) { - this.timer = Math.max(0, this.timer - 1); - } - } else { - if (this.game.inSpawnPhase()) { + } else { this.timer = 0; - } else if (!this.hasWinner && this.game.ticks() % 10 === 0) { - this.timer++; + } + } else if (isNukeWars && ticks <= spawnTurns + prepTurns) { + // Nuke Wars Prep phase + const elapsedInPrep = ticks - spawnTurns; + this.timer = Math.max(0, (prepTurns - elapsedInPrep) / 10); + } else { + // Main game phase + if (this.game.ticks() % 10 === 0) { + const maxTimerValue = this.game.config().gameConfig().maxTimerValue; + if (maxTimerValue !== undefined) { + this.timer = Math.max(0, this.timer - 1); + } else { + this.timer++; + } } } } diff --git a/src/client/graphics/layers/TerrainLayer.ts b/src/client/graphics/layers/TerrainLayer.ts index 1f7bc0b2d..36f1d321e 100644 --- a/src/client/graphics/layers/TerrainLayer.ts +++ b/src/client/graphics/layers/TerrainLayer.ts @@ -1,4 +1,5 @@ import { Theme } from "../../../core/configuration/Config"; +import { GameMapType, GameMode } from "../../../core/game/Game"; import { GameView } from "../../../core/game/GameView"; import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; @@ -73,5 +74,20 @@ export class TerrainLayer implements Layer { this.game.width(), this.game.height(), ); + + if ( + this.game.config().gameConfig().gameMode === GameMode.NukeWars && + this.game.config().gameConfig().gameMap === GameMapType.Baikal + ) { + const height = this.game.height(); + const midpoint = 0; // The map is centered, so midpoint is at x=0 + + context.beginPath(); + context.moveTo(midpoint, -height / 2); + context.lineTo(midpoint, height / 2); + context.strokeStyle = "white"; + context.lineWidth = 2 / this.transformHandler.scale; // Make the line width independent of zoom + context.stroke(); + } } } diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 18f6f7cac..d4226f7ae 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -149,7 +149,7 @@ export class GameImpl implements Game { } private addPlayers() { - if (this.config().gameConfig().gameMode !== GameMode.Team) { + if (!this.isTeamBasedGame()) { this._humans.forEach((p) => this.addPlayer(p)); this._nations.forEach((n) => this.addPlayer(n.playerInfo)); return; diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 8118a1dec..aa0895852 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -21,6 +21,8 @@ import { ColoredTeams, Embargo, EmojiMessage, + GameMapType, + GameMode, Gold, MessageType, MutableAlliance, @@ -920,6 +922,23 @@ export class PlayerImpl implements Player { return false; } + if ( + this.mg.config().gameConfig().gameMode === GameMode.NukeWars && + this.mg.config().gameConfig().gameMap === GameMapType.Baikal && + this.mg.inPreparationPhase() + ) { + const midpoint = this.mg.width() / 2; + const targetX = this.mg.x(targetTile); + const isTeam1 = this.smallID() % 2 === 1; // Team 1 is on the left + + if (isTeam1 && targetX >= midpoint) { + return false; + } + if (!isTeam1 && targetX < midpoint) { + return false; + } + } + const cost = this.mg.unitInfo(unitType).cost(this); if (!this.isAlive() || this.gold() < cost) { return false; @@ -961,6 +980,9 @@ export class PlayerImpl implements Player { } nukeSpawn(tile: TileRef): TileRef | false { + if (this.mg.inPreparationPhase()) { + return false; + } const owner = this.mg.owner(tile); if (owner.isPlayer()) { if (this.isOnSameTeam(owner)) { diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 917d0af67..919b1be4c 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -1,6 +1,8 @@ import { simpleHash, toInt, withinInt } from "../Util"; import { AllUnitParams, + GameMapType, + GameMode, MessageType, Player, Tick, @@ -151,6 +153,33 @@ export class UnitImpl implements Unit { } move(tile: TileRef): void { + if ( + this.mg.config().gameConfig().gameMode === GameMode.NukeWars && + this.mg.config().gameConfig().gameMap === GameMapType.Baikal + ) { + const midpoint = this.mg.width() / 2; + const currentX = this.mg.x(this._tile); + const nextX = this.mg.x(tile); + const crossesMidpoint = + (currentX < midpoint && nextX >= midpoint) || + (currentX >= midpoint && nextX < midpoint); + + if (crossesMidpoint) { + const allowedTypes = [ + UnitType.Warship, + UnitType.TradeShip, + UnitType.AtomBomb, + UnitType.HydrogenBomb, + UnitType.MIRV, + UnitType.MIRVWarhead, + UnitType.Shell, + ]; + if (!allowedTypes.includes(this._type)) { + return; // Block movement + } + } + } + if (tile === null) { throw new Error("tile cannot be null"); }