diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts
index 8b8080dcb..3bff65707 100644
--- a/src/client/graphics/GameRenderer.ts
+++ b/src/client/graphics/GameRenderer.ts
@@ -23,6 +23,7 @@ import { Leaderboard } from "./layers/Leaderboard";
import { MainRadialMenu } from "./layers/MainRadialMenu";
import { MultiTabModal } from "./layers/MultiTabModal";
import { NameLayer } from "./layers/NameLayer";
+import { NukeWarsPrepTimer } from "./layers/NukeWarsPrepTimer";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
import { RailroadLayer } from "./layers/RailroadLayer";
@@ -222,6 +223,14 @@ export function createRenderer(
spawnTimer.game = game;
spawnTimer.transformHandler = transformHandler;
+ const nukewarsPrepTimer = document.querySelector(
+ "nukewars-prep-timer",
+ ) as NukeWarsPrepTimer;
+ if (!(nukewarsPrepTimer instanceof NukeWarsPrepTimer)) {
+ console.error("NukeWarsPrepTimer not found");
+ }
+ nukewarsPrepTimer.game = game;
+
// When updating these layers please be mindful of the order.
// Try to group layers by the return value of shouldTransform.
// Not grouping the layers may cause excessive calls to context.save() and context.restore().
@@ -248,6 +257,7 @@ export function createRenderer(
playerPanel,
),
spawnTimer,
+ nukewarsPrepTimer,
leaderboard,
gameLeftSidebar,
unitDisplay,
diff --git a/src/client/graphics/layers/GameRightSidebar.ts b/src/client/graphics/layers/GameRightSidebar.ts
index 18e9e0955..c4215215c 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 { GameMode, GameType } from "../../../core/game/Game";
+import { GameType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { PauseGameEvent } from "../../Transport";
@@ -52,42 +52,23 @@ 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;
}
-
- 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) {
+ const maxTimerValue = this.game.config().gameConfig().maxTimerValue;
+ if (maxTimerValue !== undefined) {
+ if (this.game.inSpawnPhase()) {
this.timer = maxTimerValue * 60;
- } else {
- this.timer = 0;
+ } else if (!this.hasWinner && this.game.ticks() % 10 === 0) {
+ this.timer = Math.max(0, this.timer - 1);
}
- } 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++;
- }
+ if (this.game.inSpawnPhase()) {
+ this.timer = 0;
+ } else if (!this.hasWinner && this.game.ticks() % 10 === 0) {
+ this.timer++;
}
}
}
diff --git a/src/client/graphics/layers/NukeWarsPrepTimer.ts b/src/client/graphics/layers/NukeWarsPrepTimer.ts
new file mode 100644
index 000000000..87256d498
--- /dev/null
+++ b/src/client/graphics/layers/NukeWarsPrepTimer.ts
@@ -0,0 +1,70 @@
+import { html, LitElement } from "lit";
+import { customElement, state } from "lit/decorators.js";
+import { GameMode } from "../../../core/game/Game";
+import { GameView } from "../../../core/game/GameView";
+import { Layer } from "./Layer";
+
+@customElement("nukewars-prep-timer")
+export class NukeWarsPrepTimer extends LitElement implements Layer {
+ public game: GameView;
+
+ @state()
+ private timer: number = 0;
+
+ private isVisible = false;
+
+ createRenderRoot() {
+ this.style.position = "fixed";
+ this.style.top = "10px"; // Adjust position as needed
+ this.style.left = "50%";
+ this.style.transform = "translateX(-50%)";
+ this.style.zIndex = "1001"; // Above other elements
+ this.style.pointerEvents = "none";
+ return this;
+ }
+
+ init() {
+ this.isVisible = false; // Only visible during Nuke Wars prep phase
+ }
+
+ tick() {
+ 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 (isNukeWars && ticks > spawnTurns && ticks <= spawnTurns + prepTurns) {
+ this.isVisible = true;
+ const elapsedInPrep = ticks - spawnTurns;
+ this.timer = Math.max(0, (prepTurns - elapsedInPrep) / 10);
+ } else {
+ this.isVisible = false;
+ }
+ }
+
+ private secondsToHms = (d: number): string => {
+ const h = Math.floor(d / 3600);
+ const m = Math.floor((d % 3600) / 60);
+ const s = Math.floor((d % 3600) % 60);
+ let time = d === 0 ? "-" : `${s}s`;
+ if (m > 0) time = `${m}m` + time;
+ if (h > 0) time = `${h}h` + time;
+ return time;
+ };
+
+ render() {
+ if (!this.isVisible) {
+ return html``;
+ }
+
+ return html`
+
+ ${this.secondsToHms(this.timer)}
+
+ `;
+ }
+}
diff --git a/src/client/graphics/layers/SpawnTimer.ts b/src/client/graphics/layers/SpawnTimer.ts
index 112951c4f..393cf96d4 100644
--- a/src/client/graphics/layers/SpawnTimer.ts
+++ b/src/client/graphics/layers/SpawnTimer.ts
@@ -1,5 +1,5 @@
import { LitElement, html } from "lit";
-import { customElement, state } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { GameMode, Team } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
@@ -15,19 +15,6 @@ export class SpawnTimer extends LitElement implements Layer {
private isVisible = false;
- @state()
- private timerText: string = "";
-
- private secondsToHms = (d: number): string => {
- const h = Math.floor(d / 3600);
- const m = Math.floor((d % 3600) / 60);
- const s = Math.floor((d % 3600) % 60);
- let time = d === 0 ? "-" : `${s}s`;
- if (m > 0) time = `${m}m` + time;
- if (h > 0) time = `${h}h` + time;
- return time;
- };
-
createRenderRoot() {
this.style.position = "fixed";
this.style.top = "0";
@@ -44,28 +31,16 @@ export class SpawnTimer extends LitElement implements Layer {
}
tick() {
- 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) {
+ if (this.game.inSpawnPhase()) {
// During spawn phase, only one segment filling full width
- this.ratios = [ticks / spawnTurns];
+ this.ratios = [
+ this.game.ticks() / this.game.config().numSpawnPhaseTurns(),
+ ];
this.colors = ["rgba(0, 128, 255, 0.7)"];
this.requestUpdate();
return;
- } else if (isNukeWars && ticks <= spawnTurns + prepTurns) {
- // Nuke Wars Prep phase
- const elapsedInPrep = ticks - spawnTurns;
- const remainingSeconds = Math.max(0, (prepTurns - elapsedInPrep) / 10);
- this.timerText = this.secondsToHms(remainingSeconds);
- this.requestUpdate();
- return;
}
- // Existing logic for team territory ratios
this.ratios = [];
this.colors = [];
@@ -106,23 +81,6 @@ export class SpawnTimer extends LitElement implements Layer {
return html``;
}
- 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 (isNukeWars && ticks > spawnTurns && ticks <= spawnTurns + prepTurns) {
- // Display countdown timer for Nuke Wars Prep phase
- return html`
-
- ${this.timerText}
-
- `;
- }
-
if (this.ratios.length === 0 || this.colors.length === 0) {
return html``;
}
diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts
index 3706bc1b9..945ac3524 100644
--- a/src/client/graphics/layers/TerritoryLayer.ts
+++ b/src/client/graphics/layers/TerritoryLayer.ts
@@ -2,13 +2,7 @@ import { PriorityQueue } from "@datastructures-js/priority-queue";
import { Colord } from "colord";
import { Theme } from "../../../core/configuration/Config";
import { EventBus } from "../../../core/EventBus";
-import {
- Cell,
- GameMapType,
- GameMode,
- PlayerType,
- UnitType,
-} from "../../../core/game/Game";
+import { Cell, PlayerType, UnitType } from "../../../core/game/Game";
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, PlayerView } from "../../../core/game/GameView";
@@ -157,36 +151,6 @@ export class TerritoryLayer implements Layer {
}
}
- private drawTeamSpawnBox(
- context: CanvasRenderingContext2D,
- x: number,
- y: number,
- text: string,
- color: string,
- ) {
- context.font = "bold 16px Arial";
- context.textAlign = "center";
- context.textBaseline = "middle";
- context.fillStyle = color;
- context.strokeStyle = "black";
- context.lineWidth = 1;
-
- const textWidth = context.measureText(text).width;
- const padding = 10;
- const boxWidth = textWidth + 2 * padding;
- const boxHeight = 20 + 2 * padding; // Assuming font size 20
-
- context.fillRect(x - boxWidth / 2, y - boxHeight / 2, boxWidth, boxHeight);
- context.strokeRect(
- x - boxWidth / 2,
- y - boxHeight / 2,
- boxWidth,
- boxHeight,
- );
- context.fillStyle = "white";
- context.fillText(text, x, y);
- }
-
private spawnHighlight() {
if (this.game.ticks() % 5 === 0) {
return;
@@ -199,77 +163,43 @@ export class TerritoryLayer implements Layer {
this.game.height(),
);
- const isNukeWars =
- this.game.config().gameConfig().gameMode === GameMode.NukeWars;
- const isBaikal =
- this.game.config().gameConfig().gameMap === GameMapType.Baikal;
+ this.drawFocusedPlayerHighlight();
- if (isNukeWars && isBaikal && this.game.inSpawnPhase()) {
- // The map is centered, so coordinates are from -width/2 to width/2
- // The midpoint is at x=0
- // Left box at -width/4, right box at width/4
- // Y coordinate is 0 (center of the map)
+ const humans = this.game
+ .playerViews()
+ .filter((p) => p.type() === PlayerType.Human);
- // Red Team Spawn (Left Side)
- this.drawTeamSpawnBox(
- this.highlightContext,
- -this.game.width() / 4,
- 0,
- "Red Team Spawn",
- "rgba(255, 0, 0, 0.5)",
- );
+ const focusedPlayer = this.game.focusedPlayer();
+ for (const human of humans) {
+ if (human === focusedPlayer) {
+ continue;
+ }
+ const center = human.nameLocation();
+ if (!center) {
+ continue;
+ }
+ const centerTile = this.game.ref(center.x, center.y);
+ if (!centerTile) {
+ continue;
+ }
+ let color = this.theme.spawnHighlightColor();
+ const myPlayer = this.game.myPlayer();
+ if (myPlayer !== null && myPlayer !== human && myPlayer.team() === null) {
+ // In FFA games (when team === null), use default yellow spawn highlight color
+ color = this.theme.spawnHighlightColor();
+ } else if (myPlayer !== null && myPlayer !== human) {
+ // In Team games, the spawn highlight color becomes that player's team color
+ // Optionally, this could be broken down to teammate or enemy and simplified to green and red, respectively
+ const team = human.team();
+ if (team !== null) color = this.theme.teamColor(team);
+ }
- // Blue Team Spawn (Right Side)
- this.drawTeamSpawnBox(
- this.highlightContext,
- this.game.width() / 4,
- 0,
- "Blue Team Spawn",
- "rgba(0, 0, 255, 0.5)",
- );
- } else {
- this.drawFocusedPlayerHighlight();
-
- const humans = this.game
- .playerViews()
- .filter((p) => p.type() === PlayerType.Human);
-
- const focusedPlayer = this.game.focusedPlayer();
- for (const human of humans) {
- if (human === focusedPlayer) {
- continue;
- }
- const center = human.nameLocation();
- if (!center) {
- continue;
- }
- const centerTile = this.game.ref(center.x, center.y);
- if (!centerTile) {
- continue;
- }
- let color = this.theme.spawnHighlightColor();
- const myPlayer = this.game.myPlayer();
- if (
- myPlayer !== null &&
- myPlayer !== human &&
- myPlayer.team() === null
- ) {
- // In FFA games (when team === null), use default yellow spawn highlight color
- color = this.theme.spawnHighlightColor();
- } else if (myPlayer !== null && myPlayer !== human) {
- // In Team games, the spawn highlight color becomes that player's team color
- // Optionally, this could be broken down to teammate or enemy and simplified to green and red, respectively
- const team = human.team();
- if (team !== null) color = this.theme.teamColor(team);
- }
-
- for (const tile of this.game.bfs(
- centerTile,
- euclDistFN(centerTile, 9, true),
- )) {
- if (!this.game.hasOwner(tile)) {
- this.paintHighlightTile(tile, color, 255);
- }
+ for (const tile of this.game.bfs(
+ centerTile,
+ euclDistFN(centerTile, 9, true),
+ )) {
+ if (!this.game.hasOwner(tile)) {
+ this.paintHighlightTile(tile, color, 255);
}
}
}
diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts
index aa0895852..e81fdaf98 100644
--- a/src/core/game/PlayerImpl.ts
+++ b/src/core/game/PlayerImpl.ts
@@ -903,10 +903,7 @@ export class PlayerImpl implements Player {
}
return {
type: u,
- canBuild:
- this.mg.inSpawnPhase() || tile === null
- ? false
- : this.canBuild(u, tile, validTiles),
+ canBuild: tile === null ? false : this.canBuild(u, tile, validTiles),
canUpgrade: canUpgrade,
cost: this.mg.config().unitInfo(u).cost(this),
} as BuildableUnit;