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;