From 5609a5d2023fd27aecb33254f5a9f2b78f3033cf Mon Sep 17 00:00:00 2001 From: Restart2008 Date: Sat, 25 Oct 2025 15:00:49 -0700 Subject: [PATCH] feat(nukewars): Enhance Nuke Wars UI for preparation phase and spawn indication This commit refines the Nuke Wars game mode UI based on user feedback. Key changes include: - Preparation Phase Timer: Modified `SpawnTimer.ts` to display a countdown for the 3-minute preparation phase, aligning with the top-right game timers design. - Spawn Area Indication: - Removed the previous white line separator from `TerrainLayer.ts`. - Implemented team-specific spawn boxes in `TerritoryLayer.ts` for Nuke Wars on the Baikal map. These boxes, labeled "Red Team Spawn" and "Blue Team Spawn", appear during the spawn phase to clearly indicate team territories. --- src/client/graphics/layers/SpawnTimer.ts | 52 ++++++- src/client/graphics/layers/TerrainLayer.ts | 16 --- src/client/graphics/layers/TerritoryLayer.ts | 140 ++++++++++++++----- 3 files changed, 152 insertions(+), 56 deletions(-) diff --git a/src/client/graphics/layers/SpawnTimer.ts b/src/client/graphics/layers/SpawnTimer.ts index 393cf96d4..112951c4f 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 } from "lit/decorators.js"; +import { customElement, state } from "lit/decorators.js"; import { GameMode, Team } from "../../../core/game/Game"; import { GameView } from "../../../core/game/GameView"; import { TransformHandler } from "../TransformHandler"; @@ -15,6 +15,19 @@ 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"; @@ -31,16 +44,28 @@ export class SpawnTimer extends LitElement implements Layer { } tick() { - if (this.game.inSpawnPhase()) { + 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) { // During spawn phase, only one segment filling full width - this.ratios = [ - this.game.ticks() / this.game.config().numSpawnPhaseTurns(), - ]; + this.ratios = [ticks / spawnTurns]; 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 = []; @@ -81,6 +106,23 @@ 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/TerrainLayer.ts b/src/client/graphics/layers/TerrainLayer.ts index 36f1d321e..1f7bc0b2d 100644 --- a/src/client/graphics/layers/TerrainLayer.ts +++ b/src/client/graphics/layers/TerrainLayer.ts @@ -1,5 +1,4 @@ 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"; @@ -74,20 +73,5 @@ 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(); - } } } diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 945ac3524..3706bc1b9 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -2,7 +2,13 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; import { Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; import { EventBus } from "../../../core/EventBus"; -import { Cell, PlayerType, UnitType } from "../../../core/game/Game"; +import { + Cell, + GameMapType, + GameMode, + 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"; @@ -151,6 +157,36 @@ 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; @@ -163,43 +199,77 @@ export class TerritoryLayer implements Layer { this.game.height(), ); - this.drawFocusedPlayerHighlight(); + const isNukeWars = + this.game.config().gameConfig().gameMode === GameMode.NukeWars; + const isBaikal = + this.game.config().gameConfig().gameMap === GameMapType.Baikal; - const humans = this.game - .playerViews() - .filter((p) => p.type() === PlayerType.Human); + 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 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); - } + // Red Team Spawn (Left Side) + this.drawTeamSpawnBox( + this.highlightContext, + -this.game.width() / 4, + 0, + "Red Team Spawn", + "rgba(255, 0, 0, 0.5)", + ); - for (const tile of this.game.bfs( - centerTile, - euclDistFN(centerTile, 9, true), - )) { - if (!this.game.hasOwner(tile)) { - this.paintHighlightTile(tile, color, 255); + // 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); + } } } }