mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 14:09:46 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export interface Config {
|
||||
donateTroops(): boolean;
|
||||
instantBuild(): boolean;
|
||||
numSpawnPhaseTurns(): number;
|
||||
numPreparationPhaseTurns(): number;
|
||||
userSettings(): UserSettings;
|
||||
playerTeams(): TeamCountConfig;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user