mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-30 13:52:12 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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``;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user