Nuke Wars: enforce 2-team, separate spawn→preparation phase, block nukes during prep, and show prep+elapsed timers in HUD

This commit is contained in:
Restart2008
2025-10-24 20:35:57 -07:00
parent 4d528548c7
commit d3aa71321c
9 changed files with 142 additions and 41 deletions
+5 -3
View File
@@ -269,9 +269,8 @@ export class HostLobbyModal extends LitElement {
</div>
${
this.gameMode === GameMode.FFA
? ""
: html`
this.gameMode === GameMode.Team
? html`
<!-- Team Count Selection -->
<div class="options-section">
<div class="option-title">
@@ -299,6 +298,7 @@ export class HostLobbyModal extends LitElement {
</div>
</div>
`
: ""
}
<!-- Game Options -->
@@ -692,6 +692,8 @@ export class HostLobbyModal extends LitElement {
// 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;
+6 -4
View File
@@ -206,9 +206,8 @@ export class SinglePlayerModal extends LitElement {
</div>
</div>
${this.gameMode === GameMode.FFA
? ""
: html`
${this.gameMode === GameMode.Team
? html`
<!-- Team Count Selection -->
<div class="options-section">
<div class="option-title">
@@ -233,7 +232,8 @@ export class SinglePlayerModal extends LitElement {
)}
</div>
</div>
`}
`
: ""}
<!-- Game Options -->
<div class="options-section">
@@ -477,6 +477,8 @@ export class SinglePlayerModal extends LitElement {
// 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;
+87 -26
View File
@@ -21,41 +21,102 @@ export class NukeWarsTopBanner implements Layer {
const config = this.game.config().gameConfig();
if (config.gameMode !== GameMode.NukeWars) return;
if (config.gameMap !== GameMapType.Baikal) return;
if (!this.game.inSpawnPhase()) return;
const numSpawn = this.game.config().numSpawnPhaseTurns();
const remainingTicks = Math.max(0, numSpawn - this.game.ticks());
// 1 second = 10 ticks (100ms per tick)
const remainingSeconds = Math.ceil(remainingTicks / 10);
const minutes = Math.floor(remainingSeconds / 60);
const seconds = remainingSeconds % 60;
const timeStr = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
const canvasWidth = context.canvas.width;
const padding = 12;
const fontSize = Math.max(16, Math.floor(canvasWidth * 0.02));
const fontSize = Math.max(14, Math.floor(canvasWidth * 0.02));
context.save();
context.font = `bold ${fontSize}px sans-serif`;
context.textAlign = "center";
context.textBaseline = "top";
// background rounded rectangle
const textMetrics = context.measureText(timeStr);
const textWidth = textMetrics.width;
const rectWidth = textWidth + padding * 2;
const rectHeight = fontSize + padding * 2;
const x = canvasWidth / 2 - rectWidth / 2;
const y = 8;
// During spawn phase show spawn timer centered
if (this.game.inSpawnPhase()) {
const numSpawn = this.game.config().numSpawnPhaseTurns();
const remainingTicks = Math.max(0, numSpawn - this.game.ticks());
const remainingSeconds = Math.ceil(remainingTicks / 10);
const minutes = Math.floor(remainingSeconds / 60);
const seconds = remainingSeconds % 60;
const timeStr = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
// shadow / backdrop
context.fillStyle = "rgba(0,0,0,0.55)";
roundRect(context, x, y, rectWidth, rectHeight, 8);
context.fill();
const textMetrics = context.measureText(timeStr);
const textWidth = textMetrics.width;
const rectWidth = textWidth + padding * 2;
const rectHeight = fontSize + padding * 2;
const x = canvasWidth / 2 - rectWidth / 2;
const y = 8;
// text
context.fillStyle = "#ffcc00";
context.fillText(timeStr, canvasWidth / 2, y + padding);
context.fillStyle = "rgba(0,0,0,0.55)";
roundRect(context, x, y, rectWidth, rectHeight, 8);
context.fill();
context.fillStyle = "#ffcc00";
context.fillText(timeStr, canvasWidth / 2, y + padding);
}
// During preparation phase show prep countdown centered and elapsed timer top-right
if (this.game.inPreparationPhase()) {
const spawn = this.game.config().numSpawnPhaseTurns();
const prep = this.game.config().numPreparationPhaseTurns();
const remainingTicks = Math.max(0, spawn + prep - this.game.ticks());
const remainingSeconds = Math.ceil(remainingTicks / 10);
const minutes = Math.floor(remainingSeconds / 60);
const seconds = remainingSeconds % 60;
const prepStr = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
// center prep timer
const textMetrics = context.measureText(prepStr);
const textWidth = textMetrics.width;
const rectWidth = textWidth + padding * 2;
const rectHeight = fontSize + padding * 2;
const x = canvasWidth / 2 - rectWidth / 2;
const y = 8;
context.fillStyle = "rgba(0,0,0,0.55)";
roundRect(context, x, y, rectWidth, rectHeight, 8);
context.fill();
context.fillStyle = "#ffcc00";
context.fillText(prepStr, canvasWidth / 2, y + padding);
// elapsed game time (top-right)
const elapsedSeconds = Math.floor(this.game.ticks() / 10);
const eMinutes = Math.floor(elapsedSeconds / 60);
const eSeconds = elapsedSeconds % 60;
const elapsedStr = `${String(eMinutes).padStart(2, "0")}:${String(eSeconds).padStart(2, "0")}`;
context.textAlign = "right";
context.textBaseline = "top";
const elTextMetrics = context.measureText(elapsedStr);
const elRectWidth = elTextMetrics.width + padding * 2;
const ex = canvasWidth - 8 - elRectWidth;
const ey = 8;
context.fillStyle = "rgba(0,0,0,0.4)";
roundRect(context, ex, ey, elRectWidth, rectHeight, 8);
context.fill();
context.fillStyle = "#ffffff";
context.fillText(elapsedStr, canvasWidth - 8 - padding, ey + padding);
// restore text alignment for any following draws
context.textAlign = "center";
}
// After preparation phase show elapsed game timer centered
if (!this.game.inSpawnPhase() && !this.game.inPreparationPhase()) {
const elapsedSeconds = Math.floor(this.game.ticks() / 10);
const eMinutes = Math.floor(elapsedSeconds / 60);
const eSeconds = elapsedSeconds % 60;
const elapsedStr = `${String(eMinutes).padStart(2, "0")}:${String(eSeconds).padStart(2, "0")}`;
const textMetrics = context.measureText(elapsedStr);
const textWidth = textMetrics.width;
const rectWidth = textWidth + padding * 2;
const rectHeight = fontSize + padding * 2;
const x = canvasWidth / 2 - rectWidth / 2;
const y = 8;
context.fillStyle = "rgba(0,0,0,0.45)";
roundRect(context, x, y, rectWidth, rectHeight, 8);
context.fill();
context.fillStyle = "#ffffff";
context.fillText(elapsedStr, canvasWidth / 2, y + padding);
}
context.restore();
}
+1
View File
@@ -88,6 +88,7 @@ export interface Config {
donateTroops(): boolean;
instantBuild(): boolean;
numSpawnPhaseTurns(): number;
numPreparationPhaseTurns(): number;
userSettings(): UserSettings;
playerTeams(): TeamCountConfig;
+9 -5
View File
@@ -613,13 +613,17 @@ export class DefaultConfig implements Config {
return 3;
}
numSpawnPhaseTurns(): number {
// Nuke Wars uses a 3 minute preparation phase (3 minutes = 180 seconds)
// Server tick is 100ms => 10 ticks per second -> 180 * 10 = 1800 ticks
if (this._gameConfig.gameMode === GameMode.NukeWars) {
return 180 * 10;
}
// Spawn phase (choosing spawn points) defaults
return this._gameConfig.gameType === GameType.Singleplayer ? 100 : 300;
}
numPreparationPhaseTurns(): number {
// Preparation phase duration (Nuke Wars uses a 3 minute prep phase)
if (this._gameConfig.gameMode === GameMode.NukeWars) {
return 180 * 10; // 180 seconds * 10 ticks/sec
}
return 0;
}
numBots(): number {
return this.bots();
}
+1
View File
@@ -656,6 +656,7 @@ export interface Game extends GameMap {
isOnMap(cell: Cell): boolean;
width(): number;
height(): number;
inPreparationPhase(): boolean;
map(): GameMap;
miniMap(): GameMap;
forEachTile(fn: (tile: TileRef) => void): void;
+21 -3
View File
@@ -96,8 +96,11 @@ export class GameImpl implements Game {
this._width = _map.width();
this._height = _map.height();
this.unitGrid = new UnitGrid(this._map);
if (_config.gameConfig().gameMode === GameMode.Team) {
// 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
) {
this.populateTeams();
}
this.addPlayers();
@@ -105,6 +108,10 @@ export class GameImpl implements Game {
private populateTeams() {
let numPlayerTeams = this._config.playerTeams();
// Force 2 teams for NukeWars
if (this._config.gameConfig().gameMode === GameMode.NukeWars) {
numPlayerTeams = 2;
}
if (typeof numPlayerTeams !== "number") {
const players = this._humans.length + this._nations.length;
switch (numPlayerTeams) {
@@ -323,6 +330,14 @@ export class GameImpl implements Game {
return this._ticks <= this.config().numSpawnPhaseTurns();
}
inPreparationPhase(): boolean {
const spawn = this.config().numSpawnPhaseTurns();
const prep = this.config().numPreparationPhaseTurns?.()
? this.config().numPreparationPhaseTurns()
: 0;
return this._ticks > spawn && this._ticks <= spawn + prep;
}
ticks(): number {
return this._ticks;
}
@@ -666,7 +681,10 @@ export class GameImpl implements Game {
teams(): Team[] {
if (this._config.gameConfig().gameMode !== GameMode.Team) {
return [];
// Treat NukeWars as a team-based mode (2 teams)
if (this._config.gameConfig().gameMode !== GameMode.NukeWars) {
return [];
}
}
return [this.botTeam, ...this.playerTeams];
}
+6
View File
@@ -637,6 +637,12 @@ export class GameView implements GameMap {
inSpawnPhase(): boolean {
return this.ticks() <= this._config.numSpawnPhaseTurns();
}
inPreparationPhase(): boolean {
const spawn = this._config.numSpawnPhaseTurns();
const prep = this._config.numPreparationPhaseTurns();
return this.ticks() > spawn && this.ticks() <= spawn + prep;
}
config(): Config {
return this._config;
}
+6
View File
@@ -991,6 +991,12 @@ export class PlayerImpl implements Player {
}
nukeSpawn(tile: TileRef): TileRef | false {
// During the Nuke Wars preparation phase nukes cannot be launched across
// to enemy territory. Block all nuke launches while in the preparation phase.
const gc = this.mg.config().gameConfig();
if (gc.gameMode === GameMode.NukeWars && this.mg.inPreparationPhase()) {
return false;
}
const owner = this.mg.owner(tile);
if (owner.isPlayer()) {
if (this.isOnSameTeam(owner)) {