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.
This commit is contained in:
Restart2008
2025-10-25 14:36:58 -07:00
parent f4b8c42a5a
commit 27fbb71b55
5 changed files with 99 additions and 13 deletions
+31 -12
View File
@@ -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++;
}
}
}
}
@@ -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();
}
}
}
+1 -1
View File
@@ -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;
+22
View File
@@ -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)) {
+29
View File
@@ -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");
}