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.
This commit is contained in:
Restart2008
2025-10-25 15:19:56 -07:00
parent 5609a5d202
commit 54548f9111
6 changed files with 132 additions and 186 deletions
+10
View File
@@ -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,
+11 -30
View File
@@ -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++;
}
}
}
@@ -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`
<div
class="w-[70px] h-8 lg:w-24 lg:h-10 border border-slate-400 p-0.5 text-xs md:text-sm lg:text-base flex items-center justify-center text-white px-1"
style="${this.timer < 60 ? "color: #ff8080;" : ""}"
>
${this.secondsToHms(this.timer)}
</div>
`;
}
}
+5 -47
View File
@@ -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`
<div
class="w-full h-full flex justify-center items-center bg-gray-800/70 text-white text-lg font-bold"
>
${this.timerText}
</div>
`;
}
if (this.ratios.length === 0 || this.colors.length === 0) {
return html``;
}
+35 -105
View File
@@ -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);
}
}
}
+1 -4
View File
@@ -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;