From 54548f9111a115d37a78cf04c74c0196b09e4680 Mon Sep 17 00:00:00 2001 From: Restart2008 Date: Sat, 25 Oct 2025 15:19:56 -0700 Subject: [PATCH] fix(nukewars): Address Nuke Wars UI and spawn phase issues This commit resolves issues identified in the Nuke Wars game mode, focusing on UI clarity and spawn phase functionality. Key fixes include: - Preparation Phase Timer: Introduced a dedicated `NukeWarsPrepTimer` component to display the 3-minute preparation phase countdown prominently at the top of the screen, as requested. This replaces previous attempts to integrate it into `SpawnTimer.ts` or `GameRightSidebar.ts`, which were not suitable for the desired display. - Spawn Area Indication Reverted: Reverted changes that added team-specific spawn boxes in `TerritoryLayer.ts`. The white line separator was also removed from `TerrainLayer.ts` in a previous step. - Spawn Phase Functionality: Corrected an issue in `PlayerImpl.ts` where players were unable to place their initial spawn during the spawn phase due to an incorrect build restriction. The spawn phase for Nuke Wars now functions identically to FFA and normal team games, allowing players to place their spawns. --- src/client/graphics/GameRenderer.ts | 10 ++ .../graphics/layers/GameRightSidebar.ts | 41 ++--- .../graphics/layers/NukeWarsPrepTimer.ts | 70 +++++++++ src/client/graphics/layers/SpawnTimer.ts | 52 +------ src/client/graphics/layers/TerritoryLayer.ts | 140 +++++------------- src/core/game/PlayerImpl.ts | 5 +- 6 files changed, 132 insertions(+), 186 deletions(-) create mode 100644 src/client/graphics/layers/NukeWarsPrepTimer.ts 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;