mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 19:25:08 +00:00
Merge branch 'v24' into evan-checkout-flow
This commit is contained in:
@@ -103,7 +103,7 @@ export function joinLobby(
|
||||
if (message.type === "error") {
|
||||
showErrorModal(
|
||||
message.error,
|
||||
"",
|
||||
message.message,
|
||||
lobbyConfig.gameID,
|
||||
lobbyConfig.clientID,
|
||||
true,
|
||||
@@ -329,7 +329,7 @@ export class ClientGameRunner {
|
||||
if (message.type === "error") {
|
||||
showErrorModal(
|
||||
message.error,
|
||||
"",
|
||||
message.message,
|
||||
this.lobby.gameID,
|
||||
this.lobby.clientID,
|
||||
true,
|
||||
@@ -385,7 +385,7 @@ export class ClientGameRunner {
|
||||
!this.gameView.hasOwner(tile) &&
|
||||
this.gameView.inSpawnPhase()
|
||||
) {
|
||||
this.eventBus.emit(new SendSpawnIntentEvent(cell));
|
||||
this.eventBus.emit(new SendSpawnIntentEvent(tile));
|
||||
return;
|
||||
}
|
||||
if (this.gameView.inSpawnPhase()) {
|
||||
@@ -582,27 +582,31 @@ export class ClientGameRunner {
|
||||
}
|
||||
|
||||
function showErrorModal(
|
||||
errMsg: string,
|
||||
stack: string,
|
||||
error: string,
|
||||
message: string | undefined,
|
||||
gameID: GameID,
|
||||
clientID: ClientID,
|
||||
closable = false,
|
||||
showDiscord = true,
|
||||
heading = "error_modal.crashed",
|
||||
) {
|
||||
const errorText = `Error: ${errMsg}\nStack: ${stack}`;
|
||||
|
||||
if (document.querySelector("#error-modal")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.createElement("div");
|
||||
|
||||
modal.id = "error-modal";
|
||||
|
||||
const discord = showDiscord ? translateText("error_modal.paste_discord") : "";
|
||||
|
||||
const content = `${discord}\n${translateText(heading)}\n game id: ${gameID}, client id: ${clientID}\n${errorText}`;
|
||||
const content = [
|
||||
showDiscord ? translateText("error_modal.paste_discord") : null,
|
||||
translateText(heading),
|
||||
`game id: ${gameID}`,
|
||||
`client id: ${clientID}`,
|
||||
`Error: ${error}`,
|
||||
message ? `Message: ${message}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
// Create elements
|
||||
const pre = document.createElement("pre");
|
||||
|
||||
+25
-10
@@ -13,20 +13,23 @@ import eo from "../../resources/lang/eo.json";
|
||||
import es from "../../resources/lang/es.json";
|
||||
import fi from "../../resources/lang/fi.json";
|
||||
import fr from "../../resources/lang/fr.json";
|
||||
import gl from "../../resources/lang/gl.json";
|
||||
import he from "../../resources/lang/he.json";
|
||||
import hi from "../../resources/lang/hi.json";
|
||||
import it from "../../resources/lang/it.json";
|
||||
import ja from "../../resources/lang/ja.json";
|
||||
import ko from "../../resources/lang/ko.json";
|
||||
import nl from "../../resources/lang/nl.json";
|
||||
import pl from "../../resources/lang/pl.json";
|
||||
import pt_br from "../../resources/lang/pt_br.json";
|
||||
import pt_BR from "../../resources/lang/pt-BR.json";
|
||||
import ru from "../../resources/lang/ru.json";
|
||||
import sh from "../../resources/lang/sh.json";
|
||||
import sv_se from "../../resources/lang/sv_se.json";
|
||||
import sl from "../../resources/lang/sl.json";
|
||||
import sv_SE from "../../resources/lang/sv-SE.json";
|
||||
import tp from "../../resources/lang/tp.json";
|
||||
import tr from "../../resources/lang/tr.json";
|
||||
import uk from "../../resources/lang/uk.json";
|
||||
import zh_cn from "../../resources/lang/zh_cn.json";
|
||||
import zh_CN from "../../resources/lang/zh-CN.json";
|
||||
|
||||
@customElement("lang-selector")
|
||||
export class LangSelector extends LitElement {
|
||||
@@ -53,7 +56,7 @@ export class LangSelector extends LitElement {
|
||||
ja,
|
||||
nl,
|
||||
pl,
|
||||
pt_br,
|
||||
"pt-BR": pt_BR,
|
||||
ru,
|
||||
sh,
|
||||
tr,
|
||||
@@ -63,8 +66,11 @@ export class LangSelector extends LitElement {
|
||||
he,
|
||||
da,
|
||||
fi,
|
||||
sv_se,
|
||||
zh_cn,
|
||||
"sv-SE": sv_SE,
|
||||
"zh-CN": zh_CN,
|
||||
ko,
|
||||
gl,
|
||||
sl,
|
||||
};
|
||||
|
||||
createRenderRoot() {
|
||||
@@ -89,15 +95,23 @@ export class LangSelector extends LitElement {
|
||||
private getClosestSupportedLang(lang: string): string {
|
||||
if (!lang) return "en";
|
||||
if (lang in this.languageMap) return lang;
|
||||
const base = lang.split("-")[0];
|
||||
if (base in this.languageMap) return base;
|
||||
|
||||
const base = lang.slice(0, 2);
|
||||
const candidates = Object.keys(this.languageMap).filter((key) =>
|
||||
key.startsWith(base),
|
||||
);
|
||||
if (candidates.length > 0) {
|
||||
candidates.sort((a, b) => b.length - a.length); // More specific first
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
return "en";
|
||||
}
|
||||
|
||||
private async initializeLanguage() {
|
||||
const browserLocale = navigator.language;
|
||||
const savedLang = localStorage.getItem("lang");
|
||||
const userLang = this.getClosestSupportedLang(savedLang || browserLocale);
|
||||
const userLang = this.getClosestSupportedLang(savedLang ?? browserLocale);
|
||||
|
||||
this.defaultTranslations = this.loadLanguage("en");
|
||||
this.translations = this.loadLanguage(userLang);
|
||||
@@ -108,7 +122,7 @@ export class LangSelector extends LitElement {
|
||||
}
|
||||
|
||||
private loadLanguage(lang: string): Record<string, string> {
|
||||
const language = this.languageMap[lang] || {};
|
||||
const language = this.languageMap[lang] ?? {};
|
||||
const flat = flattenTranslations(language);
|
||||
return flat;
|
||||
}
|
||||
@@ -204,6 +218,7 @@ export class LangSelector extends LitElement {
|
||||
"user-setting",
|
||||
"o-modal",
|
||||
"o-button",
|
||||
"territory-patterns-modal",
|
||||
];
|
||||
|
||||
document.title = this.translateText("main.title") ?? document.title;
|
||||
|
||||
@@ -162,7 +162,7 @@ export class LanguageModal extends LitElement {
|
||||
<div class="c-modal__wrapper">
|
||||
<header class="c-modal__header">
|
||||
${translateText("select_lang.title")}
|
||||
<div class="c-modal__close" @click=${this.close}>X</div>
|
||||
<div class="c-modal__close" @click=${this.close}>✕</div>
|
||||
</header>
|
||||
|
||||
<section class="c-modal__content">
|
||||
|
||||
@@ -460,6 +460,9 @@ class Client {
|
||||
"game-top-bar",
|
||||
"help-modal",
|
||||
"user-setting",
|
||||
"territory-patterns-modal",
|
||||
"language-modal",
|
||||
"news-modal",
|
||||
].forEach((tag) => {
|
||||
const modal = document.querySelector(tag) as HTMLElement & {
|
||||
close?: () => void;
|
||||
|
||||
@@ -110,7 +110,7 @@ export class PublicLobby extends LitElement {
|
||||
|
||||
const teamCount =
|
||||
lobby.gameConfig.gameMode === GameMode.Team
|
||||
? lobby.gameConfig.playerTeams || 0
|
||||
? (lobby.gameConfig.playerTeams ?? 0)
|
||||
: null;
|
||||
|
||||
const mapImageSrc = this.mapImages.get(lobby.gameID);
|
||||
|
||||
+16
-18
@@ -2,7 +2,6 @@ import { z } from "zod/v4";
|
||||
import { EventBus, GameEvent } from "../core/EventBus";
|
||||
import {
|
||||
AllPlayers,
|
||||
Cell,
|
||||
GameType,
|
||||
Gold,
|
||||
PlayerID,
|
||||
@@ -68,7 +67,7 @@ export class SendAllianceExtensionIntentEvent implements GameEvent {
|
||||
}
|
||||
|
||||
export class SendSpawnIntentEvent implements GameEvent {
|
||||
constructor(public readonly cell: Cell) {}
|
||||
constructor(public readonly tile: TileRef) {}
|
||||
}
|
||||
|
||||
export class SendAttackIntentEvent implements GameEvent {
|
||||
@@ -90,7 +89,7 @@ export class SendBoatAttackIntentEvent implements GameEvent {
|
||||
export class BuildUnitIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly unit: UnitType,
|
||||
public readonly cell: Cell,
|
||||
public readonly tile: TileRef,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -245,16 +244,14 @@ export class Transport {
|
||||
}
|
||||
|
||||
private startPing() {
|
||||
if (this.isLocal || this.pingInterval) return;
|
||||
if (this.pingInterval === null) {
|
||||
this.pingInterval = window.setInterval(() => {
|
||||
if (this.socket !== null && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.sendMsg({
|
||||
type: "ping",
|
||||
} satisfies ClientPingMessage);
|
||||
}
|
||||
}, 5 * 1000);
|
||||
}
|
||||
if (this.isLocal) return;
|
||||
this.pingInterval ??= window.setInterval(() => {
|
||||
if (this.socket !== null && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.sendMsg({
|
||||
type: "ping",
|
||||
} satisfies ClientPingMessage);
|
||||
}
|
||||
}, 5 * 1000);
|
||||
}
|
||||
|
||||
private stopPing() {
|
||||
@@ -344,7 +341,10 @@ export class Transport {
|
||||
console.log(
|
||||
`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`,
|
||||
);
|
||||
if (event.code !== 1000 && event.code !== 1002) {
|
||||
if (event.code === 1002) {
|
||||
// TODO: make this a modal
|
||||
alert(`connection refused: ${event.reason}`);
|
||||
} else if (event.code !== 1000) {
|
||||
console.log(`recieved error code ${event.code}, reconnecting`);
|
||||
this.reconnect();
|
||||
}
|
||||
@@ -437,8 +437,7 @@ export class Transport {
|
||||
pattern: this.lobbyConfig.pattern,
|
||||
name: this.lobbyConfig.playerName,
|
||||
playerType: PlayerType.Human,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y,
|
||||
tile: event.tile,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -539,8 +538,7 @@ export class Transport {
|
||||
type: "build_unit",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
unit: event.unit,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y,
|
||||
tile: event.tile,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -172,7 +172,7 @@ export function getAltKey(): string {
|
||||
|
||||
export function getGamesPlayed(): number {
|
||||
try {
|
||||
return parseInt(localStorage.getItem("gamesPlayed") || "0", 10) || 0;
|
||||
return parseInt(localStorage.getItem("gamesPlayed") ?? "0", 10) || 0;
|
||||
} catch (error) {
|
||||
console.warn("Failed to read games played from localStorage:", error);
|
||||
return 0;
|
||||
|
||||
@@ -17,9 +17,7 @@ export class NewsButton extends LitElement {
|
||||
|
||||
private checkForNewVersion() {
|
||||
try {
|
||||
const lastSeenVersion = localStorage.getItem(
|
||||
"news-button-last-seen-version",
|
||||
);
|
||||
const lastSeenVersion = localStorage.getItem("version");
|
||||
this.isActive = lastSeenVersion !== version;
|
||||
} catch (error) {
|
||||
// Fallback to NOT showing notification if localStorage fails
|
||||
@@ -28,7 +26,7 @@ export class NewsButton extends LitElement {
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
localStorage.setItem("news-button-last-seen-version", version);
|
||||
localStorage.setItem("version", version);
|
||||
this.isActive = false;
|
||||
|
||||
const newsModal = document.querySelector("news-modal") as NewsModal;
|
||||
|
||||
@@ -79,7 +79,7 @@ export class OModal extends LitElement {
|
||||
${`${this.translationKey}` === ""
|
||||
? `${this.title}`
|
||||
: `${translateText(this.translationKey)}`}
|
||||
<div class="c-modal__close" @click=${this.close}>X</div>
|
||||
<div class="c-modal__close" @click=${this.close}>✕</div>
|
||||
</header>
|
||||
<section class="c-modal__content">
|
||||
<slot></slot>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { EventsDisplay } from "./layers/EventsDisplay";
|
||||
import { FxLayer } from "./layers/FxLayer";
|
||||
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
|
||||
import { GameRightSidebar } from "./layers/GameRightSidebar";
|
||||
import { GameTopBar } from "./layers/GameTopBar";
|
||||
import { GutterAdModal } from "./layers/GutterAdModal";
|
||||
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
|
||||
import { Layer } from "./layers/Layer";
|
||||
@@ -75,8 +74,8 @@ export function createRenderer(
|
||||
buildMenu.transformHandler = transformHandler;
|
||||
|
||||
const leaderboard = document.querySelector("leader-board") as Leaderboard;
|
||||
if (!emojiTable || !(leaderboard instanceof Leaderboard)) {
|
||||
console.error("EmojiTable element not found in the DOM");
|
||||
if (!leaderboard || !(leaderboard instanceof Leaderboard)) {
|
||||
console.error("LeaderBoard element not found in the DOM");
|
||||
}
|
||||
leaderboard.eventBus = eventBus;
|
||||
leaderboard.game = game;
|
||||
@@ -90,8 +89,8 @@ export function createRenderer(
|
||||
gameLeftSidebar.game = game;
|
||||
|
||||
const teamStats = document.querySelector("team-stats") as TeamStats;
|
||||
if (!emojiTable || !(teamStats instanceof TeamStats)) {
|
||||
console.error("EmojiTable element not found in the DOM");
|
||||
if (!teamStats || !(teamStats instanceof TeamStats)) {
|
||||
console.error("TeamStats element not found in the DOM");
|
||||
}
|
||||
teamStats.eventBus = eventBus;
|
||||
teamStats.game = game;
|
||||
@@ -162,13 +161,6 @@ export function createRenderer(
|
||||
settingsModal.userSettings = userSettings;
|
||||
settingsModal.eventBus = eventBus;
|
||||
|
||||
const gameTopBar = document.querySelector("game-top-bar") as GameTopBar;
|
||||
if (!(gameTopBar instanceof GameTopBar)) {
|
||||
console.error("top bar not found");
|
||||
}
|
||||
gameTopBar.game = game;
|
||||
gameTopBar.eventBus = eventBus;
|
||||
|
||||
const unitDisplay = document.querySelector("unit-display") as UnitDisplay;
|
||||
if (!(unitDisplay instanceof UnitDisplay)) {
|
||||
console.error("unit display not found");
|
||||
@@ -255,7 +247,6 @@ export function createRenderer(
|
||||
new SpawnTimer(game, transformHandler),
|
||||
leaderboard,
|
||||
gameLeftSidebar,
|
||||
gameTopBar,
|
||||
unitDisplay,
|
||||
gameRightSidebar,
|
||||
controlPanel,
|
||||
|
||||
@@ -91,9 +91,9 @@ const getSpriteForUnit = (unit: UnitView): ImageBitmap | null => {
|
||||
const unitType = unit.type();
|
||||
if (unitType === UnitType.Train) {
|
||||
const trainType = trainTypeToSpriteType(unit);
|
||||
return spriteMap.get(trainType) || null;
|
||||
return spriteMap.get(trainType) ?? null;
|
||||
}
|
||||
return spriteMap.get(unitType) || null;
|
||||
return spriteMap.get(unitType) ?? null;
|
||||
};
|
||||
|
||||
export const isSpriteReady = (unit: UnitView): boolean => {
|
||||
|
||||
@@ -15,7 +15,6 @@ import { translateText } from "../../../client/Utils";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import {
|
||||
BuildableUnit,
|
||||
Cell,
|
||||
Gold,
|
||||
PlayerActions,
|
||||
UnitType,
|
||||
@@ -136,6 +135,11 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
if (!this.game.myPlayer()?.isAlive()) {
|
||||
return;
|
||||
}
|
||||
if (!this._hidden) {
|
||||
// Players sometimes hold control while building a unit,
|
||||
// so if the menu is already open, ignore the event.
|
||||
return;
|
||||
}
|
||||
const clickedCell = this.transformHandler.screenToWorldCoordinates(
|
||||
e.x,
|
||||
e.y,
|
||||
@@ -379,7 +383,7 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
return "?";
|
||||
}
|
||||
|
||||
return player.units(item.unitType).length.toString();
|
||||
return player.totalUnitLevels(item.unitType).toString();
|
||||
}
|
||||
|
||||
public sendBuildOrUpgrade(buildableUnit: BuildableUnit, tile: TileRef): void {
|
||||
@@ -391,12 +395,7 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
),
|
||||
);
|
||||
} else if (buildableUnit.canBuild) {
|
||||
this.eventBus.emit(
|
||||
new BuildUnitIntentEvent(
|
||||
buildableUnit.type,
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
),
|
||||
);
|
||||
this.eventBus.emit(new BuildUnitIntentEvent(buildableUnit.type, tile));
|
||||
}
|
||||
this.hideMenu();
|
||||
}
|
||||
|
||||
@@ -2,16 +2,19 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Gold } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { AttackRatioEvent } from "../../InputHandler";
|
||||
import { SendSetTargetTroopRatioEvent } from "../../Transport";
|
||||
import { renderTroops } from "../../Utils";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { UIState } from "../UIState";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("control-panel")
|
||||
export class ControlPanel extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public clientID: ClientID;
|
||||
public eventBus: EventBus;
|
||||
public uiState: UIState;
|
||||
|
||||
@@ -27,13 +30,34 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
@state()
|
||||
private _population: number;
|
||||
|
||||
@state()
|
||||
private _maxPopulation: number;
|
||||
|
||||
@state()
|
||||
private popRate: number;
|
||||
|
||||
@state()
|
||||
private _troops: number;
|
||||
|
||||
@state()
|
||||
private _workers: number;
|
||||
|
||||
@state()
|
||||
private _isVisible = false;
|
||||
|
||||
@state()
|
||||
private _manpower: number = 0;
|
||||
|
||||
@state()
|
||||
private _gold: Gold;
|
||||
|
||||
@state()
|
||||
private _goldPerSecond: Gold;
|
||||
|
||||
private _popRateIsIncreasing: boolean = true;
|
||||
|
||||
private _lastPopulationIncreaseRate: number;
|
||||
|
||||
private init_: boolean = false;
|
||||
|
||||
init() {
|
||||
@@ -90,17 +114,31 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
return;
|
||||
}
|
||||
|
||||
const popIncreaseRate = player.population() - this._population;
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
this.updatePopulationIncrease();
|
||||
}
|
||||
|
||||
this._population = player.population();
|
||||
this._maxPopulation = this.game.config().maxPopulation(player);
|
||||
this._gold = player.gold();
|
||||
this._troops = player.troops();
|
||||
this._workers = player.workers();
|
||||
this.popRate = this.game.config().populationIncreaseRate(player) * 10;
|
||||
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10n;
|
||||
|
||||
this.currentTroopRatio = player.troops() / player.population();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private updatePopulationIncrease() {
|
||||
const player = this.game?.myPlayer();
|
||||
if (player === null) return;
|
||||
const popIncreaseRate = this.game.config().populationIncreaseRate(player);
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
|
||||
onAttackRatioChange(newRatio: number) {
|
||||
this.uiState.attackRatio = newRatio;
|
||||
}
|
||||
@@ -174,21 +212,45 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
</style>
|
||||
<div
|
||||
class="${this._isVisible
|
||||
? "text-sm lg:text-m md:w-[320px] bg-gray-800/70 p-2 pr-3 lg:p-4 shadow-lg lg:rounded-lg backdrop-blur"
|
||||
? "w-full sm:max-w-[320px] text-sm sm:text-base bg-gray-800/70 p-2 pr-3 sm:p-4 shadow-lg sm:rounded-lg backdrop-blur"
|
||||
: "hidden"}"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
>
|
||||
<div class="relative mb-4 lg:mb-4">
|
||||
<label class="flex justify-between text-white mb-1" translate="no">
|
||||
<span>
|
||||
${translateText("control_panel.troops")}:
|
||||
${(this.currentTroopRatio * 100).toFixed(0)}%
|
||||
</span>
|
||||
<span>
|
||||
${translateText("control_panel.workers")}:
|
||||
${((1 - this.currentTroopRatio) * 100).toFixed(0)}%
|
||||
</span>
|
||||
</label>
|
||||
<div class="block bg-black/30 text-white mb-4 p-2 rounded">
|
||||
<div class="flex justify-between mb-1">
|
||||
<span class="font-bold"
|
||||
>${translateText("control_panel.pop")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderTroops(this._population)} /
|
||||
${renderTroops(this._maxPopulation)}
|
||||
<span
|
||||
class="${this._popRateIsIncreasing
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"}"
|
||||
translate="no"
|
||||
>(+${renderTroops(this.popRate)})</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-bold"
|
||||
>${translateText("control_panel.gold")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderNumber(this._gold)}
|
||||
(+${renderNumber(this._goldPerSecond)})</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative mb-4 sm:mb-4">
|
||||
<label class="block text-white mb-1" translate="no"
|
||||
>${translateText("control_panel.troops")}:
|
||||
<span translate="no">${renderTroops(this._troops)}</span> |
|
||||
${translateText("control_panel.workers")}:
|
||||
<span translate="no">${renderTroops(this._workers)}</span></label
|
||||
>
|
||||
<div class="relative h-8">
|
||||
<!-- Background track -->
|
||||
<div
|
||||
@@ -215,7 +277,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative mb-0 lg:mb-4">
|
||||
<div class="relative mb-0 sm:mb-4">
|
||||
<label class="block text-white mb-1" translate="no"
|
||||
>${translateText("control_panel.attack_ratio")}:
|
||||
${(this.attackRatio * 100).toFixed(0)}%
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { AllPlayers } from "../../../core/game/Game";
|
||||
@@ -11,91 +11,14 @@ import { TransformHandler } from "../TransformHandler";
|
||||
|
||||
@customElement("emoji-table")
|
||||
export class EmojiTable extends LitElement {
|
||||
@state() public isVisible = false;
|
||||
public eventBus: EventBus;
|
||||
public transformHandler: TransformHandler;
|
||||
public game: GameView;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.emoji-table {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9999;
|
||||
background-color: #1e1e1e;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.emoji-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.emoji-button {
|
||||
font-size: 60px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 1px solid #333;
|
||||
background-color: #2c2c2c;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 8px;
|
||||
}
|
||||
.emoji-button:hover {
|
||||
background-color: #3a3a3a;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.emoji-button:active {
|
||||
background-color: #4a4a4a;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.emoji-button {
|
||||
font-size: 32px;
|
||||
/* Slightly smaller font size for mobile */
|
||||
width: 60px;
|
||||
/* Smaller width for mobile */
|
||||
height: 60px;
|
||||
/* Smaller height for mobile */
|
||||
margin: 5px;
|
||||
/* Smaller margin for mobile */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.emoji-button {
|
||||
font-size: 28px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 3px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@state()
|
||||
private _hidden = true;
|
||||
|
||||
initEventBus() {
|
||||
this.eventBus.on(ShowEmojiMenuEvent, (e) => {
|
||||
this.isVisible = true;
|
||||
const cell = this.transformHandler.screenToWorldCoordinates(e.x, e.y);
|
||||
if (!this.game.isValidCoord(cell.x, cell.y)) {
|
||||
return;
|
||||
@@ -131,40 +54,66 @@ export class EmojiTable extends LitElement {
|
||||
private onEmojiClicked: (emoji: string) => void = () => {};
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="emoji-table ${this._hidden ? "hidden" : ""}">
|
||||
${emojiTable.map(
|
||||
(row) => html`
|
||||
<div class="emoji-row">
|
||||
${row.map(
|
||||
(emoji) => html`
|
||||
<button
|
||||
class="emoji-button"
|
||||
@click=${() => this.onEmojiClicked(emoji)}
|
||||
>
|
||||
${emoji}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
<div
|
||||
class="bg-slate-800 max-w-[95vw] max-h-[95vh] pt-[15px] pb-[15px] fixed flex flex-col -translate-x-1/2 -translate-y-1/2
|
||||
items-center rounded-[10px] z-[9999] top-[50%] left-[50%] justify-center"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
@wheel=${(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Close button -->
|
||||
<button
|
||||
class="absolute -top-2 -right-2 w-6 h-6 flex items-center justify-center
|
||||
bg-red-500 hover:bg-red-900 text-white rounded-full
|
||||
text-sm font-bold transition-colors"
|
||||
@click=${this.hideTable}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<div
|
||||
class="flex flex-col overflow-y-auto"
|
||||
style="scrollbar-gutter: stable both-edges;"
|
||||
>
|
||||
${emojiTable.map(
|
||||
(row) => html`
|
||||
<div class="w-full justify-center flex">
|
||||
${row.map(
|
||||
(emoji) => html`
|
||||
<button
|
||||
class="flex transition-transform duration-300 ease justify-center items-center cursor-pointer
|
||||
border border-solid border-slate-500 rounded-[12px] bg-slate-700 hover:bg-slate-600 active:bg-slate-500
|
||||
md:m-[8px] md:text-[60px] md:w-[80px] md:h-[80px] hover:scale-[1.1] active:scale-[0.95]
|
||||
sm:w-[60px] sm:h-[60px] sm:text-[32px] sm:m-[5px] text-[28px] w-[50px] h-[50px] m-[3px]"
|
||||
@click=${() => this.onEmojiClicked(emoji)}
|
||||
>
|
||||
${emoji}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
hideTable() {
|
||||
this._hidden = true;
|
||||
this.isVisible = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
showTable(oneEmojiClicked: (emoji: string) => void) {
|
||||
this.onEmojiClicked = oneEmojiClicked;
|
||||
this._hidden = false;
|
||||
this.isVisible = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return !this._hidden;
|
||||
createRenderRoot() {
|
||||
return this; // Disable shadow DOM to allow Tailwind styles
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
private toggleEventFilter(filterName: MessageCategory) {
|
||||
const currentState = this.eventsFilters.get(filterName) || false;
|
||||
const currentState = this.eventsFilters.get(filterName) ?? false;
|
||||
this.eventsFilters.set(filterName, !currentState);
|
||||
this.requestUpdate();
|
||||
}
|
||||
@@ -351,8 +351,15 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
let description: string = event.message;
|
||||
if (event.params !== undefined) {
|
||||
if (event.message.startsWith("events_display.")) {
|
||||
description = translateText(event.message, event.params);
|
||||
}
|
||||
}
|
||||
|
||||
this.addEvent({
|
||||
description: event.message,
|
||||
description: description,
|
||||
createdAt: this.game.ticks(),
|
||||
highlight: true,
|
||||
type: event.messageType,
|
||||
@@ -375,7 +382,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
if (event.target) {
|
||||
try {
|
||||
const targetPlayer = this.game.player(event.target);
|
||||
const targetName = targetPlayer?.name() ?? event.target;
|
||||
const targetName = targetPlayer?.displayName() ?? event.target;
|
||||
translatedMessage = baseMessage.replace("[P1]", targetName);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
@@ -386,9 +393,16 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
let otherPlayerDiplayName: string = "";
|
||||
if (event.recipient !== null) {
|
||||
//'recipient' parameter contains sender ID or recipient ID
|
||||
const player = this.game.player(event.recipient);
|
||||
otherPlayerDiplayName = player ? player.displayName() : "";
|
||||
}
|
||||
|
||||
this.addEvent({
|
||||
description: translateText(event.isFrom ? "chat.from" : "chat.to", {
|
||||
user: event.recipient,
|
||||
user: otherPlayerDiplayName,
|
||||
msg: translatedMessage,
|
||||
}),
|
||||
createdAt: this.game.ticks(),
|
||||
@@ -893,7 +907,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
: html`
|
||||
<!-- Main Events Display -->
|
||||
<div
|
||||
class="relative w-full lg:bottom-2.5 lg:right-2.5 z-50 lg:w-96 backdrop-blur"
|
||||
class="relative w-full sm:bottom-2.5 sm:right-2.5 z-50 sm:w-96 backdrop-blur"
|
||||
>
|
||||
<!-- Button Bar -->
|
||||
<div
|
||||
|
||||
@@ -22,6 +22,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
|
||||
private playerColor: Colord = new Colord("#FFFFFF");
|
||||
public game: GameView;
|
||||
private _shownOnInit = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -32,12 +33,15 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
if (this.isTeamGame) {
|
||||
this.isPlayerTeamLabelVisible = true;
|
||||
}
|
||||
// Make it visible by default on large screens
|
||||
if (window.innerWidth >= 1024) {
|
||||
// lg breakpoint
|
||||
this._shownOnInit = true;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this.isPlayerTeamLabelVisible) return;
|
||||
|
||||
if (!this.playerTeam && this.game.myPlayer()?.team()) {
|
||||
this.playerTeam = this.game.myPlayer()!.team();
|
||||
if (this.playerTeam) {
|
||||
@@ -49,6 +53,12 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._shownOnInit && !this.game.inSpawnPhase()) {
|
||||
this._shownOnInit = false;
|
||||
this.isLeaderboardShow = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
this.isPlayerTeamLabelVisible = false;
|
||||
this.requestUpdate();
|
||||
@@ -130,7 +140,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
<div class="block lg:flex flex-wrap gap-2">
|
||||
<leader-board .visible=${this.isLeaderboardShow}></leader-board>
|
||||
<team-stats
|
||||
class=${`flex-1 ${this.isTeamLeaderboardShow ? "sm:mt-4 lg:mt-12" : ""}`}
|
||||
class="flex-1"
|
||||
.visible=${this.isTeamLeaderboardShow && this.isTeamGame}
|
||||
></team-stats>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,9 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
|
||||
private toggleReplayPanel(): void {
|
||||
this._isReplayVisible = !this._isReplayVisible;
|
||||
this.eventBus.emit(new ShowReplayPanelEvent(this._isReplayVisible));
|
||||
this.eventBus.emit(
|
||||
new ShowReplayPanelEvent(this._isReplayVisible, this._isSinglePlayer),
|
||||
);
|
||||
}
|
||||
|
||||
private onPauseButtonClick() {
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
|
||||
import populationIcon from "../../../../resources/images/PopulationIconSolidWhite.svg";
|
||||
import troopIcon from "../../../../resources/images/TroopIconWhite.svg";
|
||||
import workerIcon from "../../../../resources/images/WorkerIconWhite.svg";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("game-top-bar")
|
||||
export class GameTopBar extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
private _troops = 0;
|
||||
private _workers = 0;
|
||||
private _lastPopulationIncreaseRate = 0;
|
||||
private _popRateIsIncreasing = false;
|
||||
private hasWinner = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.updatePopulationIncrease();
|
||||
const player = this.game?.myPlayer();
|
||||
if (!player) return;
|
||||
this._troops = player.troops();
|
||||
this._workers = player.workers();
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
if (updates) {
|
||||
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private updatePopulationIncrease() {
|
||||
const player = this.game?.myPlayer();
|
||||
if (player === null) return;
|
||||
const popIncreaseRate = this.game.config().populationIncreaseRate(player);
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
|
||||
render() {
|
||||
const myPlayer = this.game?.myPlayer();
|
||||
if (!this.game || !myPlayer || this.game.inSpawnPhase()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAlt = this.game.config().isReplay();
|
||||
if (isAlt) {
|
||||
return html`
|
||||
<div
|
||||
class="absolute top-4 left-1/2 transform -translate-x-1/2 flex justify-center items-center p-2"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
const popRate = myPlayer
|
||||
? this.game.config().populationIncreaseRate(myPlayer) * 10
|
||||
: 0;
|
||||
const maxPop = myPlayer ? this.game.config().maxPopulation(myPlayer) : 0;
|
||||
const goldPerSecond = myPlayer
|
||||
? this.game.config().goldAdditionRate(myPlayer) * 10n
|
||||
: 0n;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-4 left-1/2 transform -translate-x-1/2 flex justify-center items-center p-1 md:px-1.5 lg:px-4 z-[1100]"
|
||||
>
|
||||
<div class="flex justify-center items-center gap-1">
|
||||
${myPlayer?.isAlive() && !this.game.inSpawnPhase()
|
||||
? html`
|
||||
<div class="overflow-x-auto hide-scrollbar">
|
||||
<div
|
||||
class="grid gap-1 grid-cols-[80px_100px_80px] w-max md:gap-2 md:grid-cols-[90px_120px_90px]"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap gap-1 flex-col bg-gray-800/70 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<img
|
||||
src=${goldCoinIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
<span class="text-white"
|
||||
>+${renderNumber(goldPerSecond)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-white">
|
||||
${renderNumber(myPlayer.gold())}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap gap-1 flex-col bg-gray-800/70 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<img
|
||||
src=${populationIcon}
|
||||
alt="population"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
<span
|
||||
class="${this._popRateIsIncreasing
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"}"
|
||||
translate="no"
|
||||
>
|
||||
+${renderTroops(popRate)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-white">
|
||||
${renderTroops(myPlayer.population())} /
|
||||
${renderTroops(maxPop)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex bg-gray-800/70 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
>
|
||||
<div class="flex flex-col flex-grow gap-1 w-full ">
|
||||
<div class="flex gap-1">
|
||||
<img
|
||||
src=${troopIcon}
|
||||
alt="troops"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
<span class="text-white"
|
||||
>${renderTroops(this._troops)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<img
|
||||
src=${workerIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
<span class="text-white"
|
||||
>${renderTroops(this._workers)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`<div></div>`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
players: Entry[] = [];
|
||||
|
||||
@property({ type: Boolean }) visible = false;
|
||||
private _shownOnInit = false;
|
||||
private showTopFive = true;
|
||||
|
||||
@state()
|
||||
@@ -57,10 +56,6 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
|
||||
tick() {
|
||||
if (this.game === null) throw new Error("Not initialized");
|
||||
if (!this._shownOnInit && !this.game.inSpawnPhase()) {
|
||||
this._shownOnInit = true;
|
||||
this.updateLeaderboard();
|
||||
}
|
||||
if (!this.visible) return;
|
||||
if (this.game.ticks() % 10 === 0) {
|
||||
this.updateLeaderboard();
|
||||
@@ -174,37 +169,25 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
class="max-h-[35vh] overflow-y-auto text-white text-xs md:text-sm md:max-h-[50vh] ${this
|
||||
class="max-h-[35vh] overflow-y-auto text-white text-xs md:text-xs lg:text-sm md:max-h-[50vh] ${this
|
||||
.visible
|
||||
? ""
|
||||
: "hidden"}"
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<button
|
||||
class="mb-2 px-2 py-1 md:px-2.5 md:py-1.5 text-xs md:text-sm lg:text-base border border-white/20 hover:bg-white/10"
|
||||
@click=${() => {
|
||||
this.showTopFive = !this.showTopFive;
|
||||
this.updateLeaderboard();
|
||||
}}
|
||||
>
|
||||
${this.showTopFive
|
||||
? translateText("leaderboard.show_all")
|
||||
: translateText("leaderboard.show_top_5")}
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="grid bg-gray-800/70 w-full text-xs md:text-sm lg:text-base"
|
||||
style="grid-template-columns: 35px 100px 85px 65px 65px;"
|
||||
class="grid bg-gray-800/70 w-full text-xs md:text-xs lg:text-sm"
|
||||
style="grid-template-columns: 30px 100px 70px 55px 75px;"
|
||||
>
|
||||
<div class="contents font-bold bg-gray-700/50">
|
||||
<div class="py-1.5 md:py-2.5 text-center border-b border-slate-500">
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
#
|
||||
</div>
|
||||
<div class="py-1.5 md:py-2.5 text-center border-b border-slate-500">
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
${translateText("leaderboard.player")}
|
||||
</div>
|
||||
<div
|
||||
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
|
||||
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap"
|
||||
@click=${() => this.setSort("tiles")}
|
||||
>
|
||||
${translateText("leaderboard.owned")}
|
||||
@@ -215,7 +198,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
: ""}
|
||||
</div>
|
||||
<div
|
||||
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
|
||||
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap"
|
||||
@click=${() => this.setSort("gold")}
|
||||
>
|
||||
${translateText("leaderboard.gold")}
|
||||
@@ -226,7 +209,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
: ""}
|
||||
</div>
|
||||
<div
|
||||
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
|
||||
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap"
|
||||
@click=${() => this.setSort("troops")}
|
||||
>
|
||||
${translateText("leaderboard.troops")}
|
||||
@@ -248,29 +231,21 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
: ""} cursor-pointer"
|
||||
@click=${() => this.handleRowClickPlayer(player.player)}
|
||||
>
|
||||
<div
|
||||
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
|
||||
>
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
${player.position}
|
||||
</div>
|
||||
<div
|
||||
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 truncate"
|
||||
class="py-1 md:py-2 text-center border-b border-slate-500 truncate"
|
||||
>
|
||||
${player.name}
|
||||
</div>
|
||||
<div
|
||||
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
|
||||
>
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
${player.score}
|
||||
</div>
|
||||
<div
|
||||
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
|
||||
>
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
${player.gold}
|
||||
</div>
|
||||
<div
|
||||
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
|
||||
>
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
${player.troops}
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,6 +253,16 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="mt-1 px-1.5 py-0.5 md:px-2 md:py-0.5 text-xs md:text-xs lg:text-sm border border-white/20 hover:bg-white/10 text-white mx-auto block"
|
||||
@click=${() => {
|
||||
this.showTopFive = !this.showTopFive;
|
||||
this.updateLeaderboard();
|
||||
}}
|
||||
>
|
||||
${this.showTopFive ? "+" : "-"}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { PlayerActions, TerraNullius } from "../../../core/game/Game";
|
||||
import { PlayerActions } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
centerButtonElement,
|
||||
COLORS,
|
||||
MenuElementParams,
|
||||
rootMenuItems,
|
||||
rootMenuElement,
|
||||
} from "./RadialMenuElements";
|
||||
|
||||
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
|
||||
@@ -31,7 +31,6 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
private chatIntegration: ChatIntegration;
|
||||
|
||||
private clickedTile: TileRef | null = null;
|
||||
private selectedPlayer: PlayerView | TerraNullius | null = null;
|
||||
|
||||
constructor(
|
||||
private eventBus: EventBus,
|
||||
@@ -57,7 +56,12 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
`,
|
||||
};
|
||||
|
||||
this.radialMenu = new RadialMenu(menuConfig);
|
||||
this.radialMenu = new RadialMenu(
|
||||
this.eventBus,
|
||||
rootMenuElement,
|
||||
centerButtonElement,
|
||||
menuConfig,
|
||||
);
|
||||
|
||||
this.playerActionHandler = new PlayerActionHandler(
|
||||
this.eventBus,
|
||||
@@ -65,8 +69,6 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
);
|
||||
|
||||
this.chatIntegration = new ChatIntegration(this.game, this.eventBus);
|
||||
|
||||
this.radialMenu.setRootMenuItems(rootMenuItems, centerButtonElement);
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -83,12 +85,11 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
return;
|
||||
}
|
||||
this.clickedTile = this.game.ref(worldCoords.x, worldCoords.y);
|
||||
this.selectedPlayer = this.game.owner(this.clickedTile);
|
||||
this.game
|
||||
.myPlayer()!
|
||||
.actions(this.clickedTile)
|
||||
.then((actions) => {
|
||||
this.handlePlayerActions(
|
||||
this.updatePlayerActions(
|
||||
this.game.myPlayer()!,
|
||||
actions,
|
||||
this.clickedTile!,
|
||||
@@ -99,12 +100,12 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
});
|
||||
}
|
||||
|
||||
private async handlePlayerActions(
|
||||
private async updatePlayerActions(
|
||||
myPlayer: PlayerView,
|
||||
actions: PlayerActions,
|
||||
tile: TileRef,
|
||||
screenX: number,
|
||||
screenY: number,
|
||||
screenX: number | null = null,
|
||||
screenY: number | null = null,
|
||||
) {
|
||||
this.buildMenu.playerActions = actions;
|
||||
|
||||
@@ -130,18 +131,27 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
eventBus: this.eventBus,
|
||||
};
|
||||
|
||||
this.radialMenu.setRootMenuItems(rootMenuItems, centerButtonElement);
|
||||
this.radialMenu.setParams(params);
|
||||
this.radialMenu.showRadialMenu(screenX, screenY);
|
||||
if (screenX !== null && screenY !== null) {
|
||||
this.radialMenu.showRadialMenu(screenX, screenY);
|
||||
} else {
|
||||
this.radialMenu.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async tick() {
|
||||
if (!this.radialMenu.isMenuVisible() || this.clickedTile === null) return;
|
||||
if (this.selectedPlayer === null) return;
|
||||
const currentPlayer = this.game.owner(this.clickedTile);
|
||||
if (currentPlayer.id() !== this.selectedPlayer.id()) {
|
||||
this.closeMenu();
|
||||
return;
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this.game
|
||||
.myPlayer()!
|
||||
.actions(this.clickedTile)
|
||||
.then((actions) => {
|
||||
this.updatePlayerActions(
|
||||
this.game.myPlayer()!,
|
||||
actions,
|
||||
this.clickedTile!,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Cell, PlayerActions, PlayerID } from "../../../core/game/Game";
|
||||
import { PlayerActions, PlayerID } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { PlayerView } from "../../../core/game/GameView";
|
||||
import {
|
||||
@@ -61,8 +61,9 @@ export class PlayerActionHandler {
|
||||
): Promise<TileRef | false> {
|
||||
return await player.bestTransportShipSpawn(tile);
|
||||
}
|
||||
handleSpawn(spawnCell: Cell) {
|
||||
this.eventBus.emit(new SendSpawnIntentEvent(spawnCell));
|
||||
|
||||
handleSpawn(tile: TileRef) {
|
||||
this.eventBus.emit(new SendSpawnIntentEvent(tile));
|
||||
}
|
||||
|
||||
handleAllianceRequest(player: PlayerView, recipient: PlayerView) {
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
} from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
|
||||
import { MouseMoveEvent } from "../../InputHandler";
|
||||
import { ContextMenuEvent, MouseMoveEvent } from "../../InputHandler";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
import { CloseRadialMenuEvent } from "./RadialMenu";
|
||||
|
||||
function euclideanDistWorld(
|
||||
coord: { x: number; y: number },
|
||||
@@ -69,6 +70,10 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) =>
|
||||
this.onMouseEvent(e),
|
||||
);
|
||||
this.eventBus.on(ContextMenuEvent, (e: ContextMenuEvent) =>
|
||||
this.maybeShow(e.x, e.y),
|
||||
);
|
||||
this.eventBus.on(CloseRadialMenuEvent, () => this.hide());
|
||||
this._isActive = true;
|
||||
}
|
||||
|
||||
@@ -165,6 +170,18 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private displayUnitCount(
|
||||
player: PlayerView,
|
||||
type: UnitType,
|
||||
description: string,
|
||||
) {
|
||||
return !this.game.config().isUnitDisabled(type)
|
||||
? html`<div class="text-sm opacity-80" translate="no">
|
||||
${translateText(description)}: ${player.totalUnitLevels(type)}
|
||||
</div>`
|
||||
: "";
|
||||
}
|
||||
|
||||
private renderPlayerInfo(player: PlayerView) {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
const isFriendly = myPlayer?.isFriendly(player);
|
||||
@@ -250,66 +267,36 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
${translateText("player_info_overlay.gold")}:
|
||||
${renderNumber(player.gold())}
|
||||
</div>
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.ports")}:
|
||||
${player.units(UnitType.Port).length}
|
||||
${player
|
||||
.units(UnitType.Port)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0) > 1
|
||||
? html`(${translateText("player_info_overlay.levels")}:
|
||||
${player
|
||||
.units(UnitType.Port)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0)})`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.cities")}:
|
||||
${player.units(UnitType.City).length}
|
||||
${player
|
||||
.units(UnitType.City)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0) > 1
|
||||
? html`(${translateText("player_info_overlay.levels")}:
|
||||
${player
|
||||
.units(UnitType.City)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0)})`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.missile_launchers")}:
|
||||
${player.units(UnitType.MissileSilo).length}
|
||||
${player
|
||||
.units(UnitType.MissileSilo)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0) > 1
|
||||
? html`(${translateText("player_info_overlay.levels")}:
|
||||
${player
|
||||
.units(UnitType.MissileSilo)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0)})`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.sams")}:
|
||||
${player.units(UnitType.SAMLauncher).length}
|
||||
${player
|
||||
.units(UnitType.SAMLauncher)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0) > 1
|
||||
? html`(${translateText("player_info_overlay.levels")}:
|
||||
${player
|
||||
.units(UnitType.SAMLauncher)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0)})`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.warships")}:
|
||||
${player.units(UnitType.Warship).length}
|
||||
</div>
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Port,
|
||||
"player_info_overlay.ports",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.City,
|
||||
"player_info_overlay.cities",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Factory,
|
||||
"player_info_overlay.factories",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.MissileSilo,
|
||||
"player_info_overlay.missile_launchers",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.SAMLauncher,
|
||||
"player_info_overlay.sams",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Warship,
|
||||
"player_info_overlay.warships",
|
||||
)}
|
||||
${relationHtml}
|
||||
</div>
|
||||
`;
|
||||
@@ -352,7 +339,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="hidden lg:flex fixed top-[245px] right-0 w-full z-50 flex-col max-w-[180px]"
|
||||
class="block lg:flex fixed top-[150px] right-0 w-full z-50 flex-col max-w-[180px]"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -233,7 +233,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none overflow-auto"
|
||||
class="fixed inset-0 flex items-center justify-center z-[1001] pointer-events-none overflow-auto"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
@wheel=${(e) => e.stopPropagation()}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as d3 from "d3";
|
||||
import backIcon from "../../../../resources/images/BackIconWhite.svg";
|
||||
import { EventBus, GameEvent } from "../../../core/EventBus";
|
||||
import { Layer } from "./Layer";
|
||||
import {
|
||||
CenterButtonElement,
|
||||
@@ -7,6 +8,10 @@ import {
|
||||
MenuElementParams,
|
||||
} from "./RadialMenuElements";
|
||||
|
||||
export class CloseRadialMenuEvent implements GameEvent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
export interface TooltipItem {
|
||||
text: string;
|
||||
className: string;
|
||||
@@ -39,13 +44,11 @@ export class RadialMenu implements Layer {
|
||||
private currentLevel: number = 0; // Current menu level (0 = main menu, 1 = submenu, etc.)
|
||||
private menuStack: MenuElement[][] = []; // Stack to track menu navigation history
|
||||
private currentMenuItems: MenuElement[] = []; // Current active menu items (changes based on level)
|
||||
private rootMenuItems: MenuElement[] = []; // Store the original root menu items
|
||||
|
||||
private readonly config: RequiredRadialMenuConfig;
|
||||
private readonly backIconSize: number;
|
||||
|
||||
private centerButtonState: CenterButtonState = "default";
|
||||
private centerButtonElement: CenterButtonElement | null = null;
|
||||
|
||||
private isTransitioning: boolean = false;
|
||||
private lastHideTime: number = 0;
|
||||
@@ -72,7 +75,12 @@ export class RadialMenu implements Layer {
|
||||
|
||||
private params: MenuElementParams | null = null;
|
||||
|
||||
constructor(config: RadialMenuConfig = {}) {
|
||||
constructor(
|
||||
private eventBus: EventBus,
|
||||
private rootMenu: MenuElement,
|
||||
private centerButtonElement: CenterButtonElement,
|
||||
config: RadialMenuConfig = {},
|
||||
) {
|
||||
this.config = {
|
||||
menuSize: config.menuSize ?? 190,
|
||||
submenuScale: config.submenuScale ?? 1.5,
|
||||
@@ -112,10 +120,12 @@ export class RadialMenu implements Layer {
|
||||
.style("height", "100vh")
|
||||
.on("click", () => {
|
||||
this.hideRadialMenu();
|
||||
this.eventBus.emit(new CloseRadialMenuEvent());
|
||||
})
|
||||
.on("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
this.hideRadialMenu();
|
||||
this.eventBus.emit(new CloseRadialMenuEvent());
|
||||
});
|
||||
|
||||
// Calculate the total svg size needed for all potential nested menus
|
||||
@@ -217,7 +227,7 @@ export class RadialMenu implements Layer {
|
||||
}
|
||||
|
||||
private getInnerRadiusForLevel(level: number): number {
|
||||
return level === 0 ? 50 : 50 + 25;
|
||||
return level === 0 ? 40 : 50 + 25;
|
||||
}
|
||||
|
||||
private getOuterRadiusForLevel(level: number): number {
|
||||
@@ -237,10 +247,12 @@ export class RadialMenu implements Layer {
|
||||
.append("g")
|
||||
.attr("class", `menu-level-${level}`);
|
||||
|
||||
// Set initial animation styles
|
||||
// Set initial animation styles only for submenus (level > 0)
|
||||
if (level === 0) {
|
||||
menuGroup.style("opacity", 0.5).style("transform", "scale(0.2)");
|
||||
// Main menu appears immediately without animation
|
||||
menuGroup.style("opacity", 1).style("transform", "scale(1)");
|
||||
} else {
|
||||
// Submenus get the expansion animation
|
||||
menuGroup.style("opacity", 0).style("transform", "scale(0.5)");
|
||||
}
|
||||
|
||||
@@ -296,14 +308,14 @@ export class RadialMenu implements Layer {
|
||||
const disabled = this.params === null || d.data.disabled(this.params);
|
||||
const color = disabled
|
||||
? this.config.disabledColor
|
||||
: d.data.color || "#333333";
|
||||
: (d.data.color ?? "#333333");
|
||||
const opacity = disabled ? 0.5 : 0.7;
|
||||
|
||||
if (d.data.id === this.selectedItemId && this.currentLevel > level) {
|
||||
return color;
|
||||
}
|
||||
|
||||
return d3.color(color)?.copy({ opacity: opacity })?.toString() || color;
|
||||
return d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color;
|
||||
})
|
||||
.attr("stroke", "#ffffff")
|
||||
.attr("stroke-width", "2")
|
||||
@@ -339,7 +351,7 @@ export class RadialMenu implements Layer {
|
||||
const color =
|
||||
this.params === null || d.data.disabled(this.params)
|
||||
? this.config.disabledColor
|
||||
: d.data.color || "#333333";
|
||||
: (d.data.color ?? "#333333");
|
||||
path.attr("fill", color);
|
||||
}
|
||||
});
|
||||
@@ -403,11 +415,11 @@ export class RadialMenu implements Layer {
|
||||
path.attr("stroke-width", "2");
|
||||
const color = disabled
|
||||
? this.config.disabledColor
|
||||
: d.data.color || "#333333";
|
||||
: (d.data.color ?? "#333333");
|
||||
const opacity = disabled ? 0.5 : 0.7;
|
||||
path.attr(
|
||||
"fill",
|
||||
d3.color(color)?.copy({ opacity: opacity })?.toString() || color,
|
||||
d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -435,6 +447,8 @@ export class RadialMenu implements Layer {
|
||||
this.updateCenterButtonState("back");
|
||||
} else {
|
||||
d.data.action?.(this.params);
|
||||
// Force transition state to false to ensure menu hides
|
||||
this.isTransitioning = false;
|
||||
this.hideRadialMenu();
|
||||
}
|
||||
};
|
||||
@@ -477,6 +491,14 @@ export class RadialMenu implements Layer {
|
||||
});
|
||||
}
|
||||
|
||||
private isItemDisabled(item: MenuElement): boolean {
|
||||
return (
|
||||
this.params === null ||
|
||||
this.params.game.inSpawnPhase() ||
|
||||
item.disabled(this.params)
|
||||
);
|
||||
}
|
||||
|
||||
private renderIconsAndText(
|
||||
arcs: d3.Selection<
|
||||
SVGGElement,
|
||||
@@ -494,10 +516,7 @@ export class RadialMenu implements Layer {
|
||||
.each((d) => {
|
||||
const contentId = d.data.id;
|
||||
const content = d3.select(`g[data-id="${contentId}"]`);
|
||||
const disabled =
|
||||
this.params === null ||
|
||||
this.params.game.inSpawnPhase() ||
|
||||
d.data.disabled(this.params);
|
||||
const disabled = this.isItemDisabled(d.data);
|
||||
|
||||
if (d.data.text) {
|
||||
content
|
||||
@@ -555,24 +574,43 @@ export class RadialMenu implements Layer {
|
||||
}
|
||||
|
||||
private updateMenuGroupVisibility() {
|
||||
// Hide all menus except the current and immediate previous one
|
||||
this.updateMenuVisibility("forward");
|
||||
}
|
||||
|
||||
private updateMenuVisibility(direction: "forward" | "backward" = "backward") {
|
||||
this.menuGroups.forEach((menuGroup, level) => {
|
||||
if (level === this.currentLevel) {
|
||||
// Current level - always visible and interactive
|
||||
menuGroup.style("display", "block");
|
||||
} else if (level === this.currentLevel - 1) {
|
||||
menuGroup.style("display", "block");
|
||||
|
||||
menuGroup
|
||||
.transition()
|
||||
.duration(this.config.menuTransitionDuration * 0.8)
|
||||
.style("transform", "scale(0.5)")
|
||||
.style("transform", "scale(1)")
|
||||
.style("opacity", 1);
|
||||
|
||||
// Enable pointer events for current level
|
||||
menuGroup.selectAll("path").style("pointer-events", "auto");
|
||||
} else if (level === this.currentLevel - 1 && this.currentLevel > 0) {
|
||||
// Previous level - visible but scaled down
|
||||
menuGroup.style("display", "block");
|
||||
menuGroup
|
||||
.transition()
|
||||
.duration(this.config.menuTransitionDuration * 0.8)
|
||||
.style(
|
||||
"transform",
|
||||
`scale(${this.currentLevel === 1 ? "0.65" : "0.5"})`,
|
||||
)
|
||||
.style("opacity", 0.8);
|
||||
|
||||
menuGroup.selectAll("path").each(function () {
|
||||
const pathElement = d3.select(this);
|
||||
pathElement.style("pointer-events", "none");
|
||||
});
|
||||
} else {
|
||||
// Disable pointer events for previous level when going forward
|
||||
if (direction === "forward") {
|
||||
menuGroup.selectAll("path").each(function () {
|
||||
const pathElement = d3.select(this);
|
||||
pathElement.style("pointer-events", "none");
|
||||
});
|
||||
}
|
||||
} else if (level !== this.currentLevel + 1) {
|
||||
// Hide all other levels
|
||||
menuGroup
|
||||
.transition()
|
||||
.duration(this.config.menuTransitionDuration * 0.5)
|
||||
@@ -610,7 +648,7 @@ export class RadialMenu implements Layer {
|
||||
|
||||
this.updateMenuLevels();
|
||||
this.clearSelectedItemHoverState();
|
||||
this.updateMenuVisibility();
|
||||
this.updateMenuVisibility("backward");
|
||||
this.animateMenuTransitions();
|
||||
}
|
||||
|
||||
@@ -623,7 +661,7 @@ export class RadialMenu implements Layer {
|
||||
this.selectedItemId = null;
|
||||
}
|
||||
|
||||
this.currentMenuItems = previousItems || [];
|
||||
this.currentMenuItems = previousItems ?? [];
|
||||
|
||||
if (this.currentLevel === 0) {
|
||||
this.updateCenterButtonState("default");
|
||||
@@ -637,54 +675,10 @@ export class RadialMenu implements Layer {
|
||||
if (selectedPath) {
|
||||
selectedPath.attr("filter", null);
|
||||
selectedPath.attr("stroke-width", "2");
|
||||
|
||||
const item = this.findMenuItem(this.selectedItemId);
|
||||
if (item) {
|
||||
const disabled = this.params === null || item.disabled(this.params);
|
||||
const color = disabled
|
||||
? this.config.disabledColor
|
||||
: item.color || "#333333";
|
||||
const opacity = disabled ? 0.5 : 0.7;
|
||||
selectedPath.attr(
|
||||
"fill",
|
||||
d3.color(color)?.copy({ opacity: opacity })?.toString() || color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateMenuVisibility() {
|
||||
this.menuGroups.forEach((menuGroup, level) => {
|
||||
if (level === this.currentLevel) {
|
||||
menuGroup.style("display", "block");
|
||||
menuGroup
|
||||
.transition()
|
||||
.duration(this.config.menuTransitionDuration * 0.8)
|
||||
.style("transform", "scale(1)")
|
||||
.style("opacity", 1);
|
||||
|
||||
menuGroup.selectAll("path").style("pointer-events", "auto");
|
||||
} else if (level === this.currentLevel - 1 && this.currentLevel > 0) {
|
||||
menuGroup.style("display", "block");
|
||||
menuGroup
|
||||
.transition()
|
||||
.duration(this.config.menuTransitionDuration * 0.8)
|
||||
.style(
|
||||
"transform",
|
||||
`scale(${this.currentLevel === 1 ? "0.65" : "0.5"})`,
|
||||
)
|
||||
.style("opacity", 0.8);
|
||||
} else if (level !== this.currentLevel + 1) {
|
||||
menuGroup
|
||||
.transition()
|
||||
.duration(this.config.menuTransitionDuration * 0.5)
|
||||
.style("opacity", 0)
|
||||
.on("end", function () {
|
||||
d3.select(this).style("display", "none");
|
||||
});
|
||||
}
|
||||
});
|
||||
// Use refresh() to update all item appearances consistently
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private animateMenuTransitions() {
|
||||
@@ -765,10 +759,13 @@ export class RadialMenu implements Layer {
|
||||
}
|
||||
|
||||
public hideRadialMenu() {
|
||||
if (!this.isVisible || this.isTransitioning) {
|
||||
if (!this.isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force transition state to false to ensure menu hides
|
||||
this.isTransitioning = false;
|
||||
|
||||
this.menuElement.style("display", "none");
|
||||
this.isVisible = false;
|
||||
this.selectedItemId = null;
|
||||
@@ -786,7 +783,7 @@ export class RadialMenu implements Layer {
|
||||
|
||||
private handleCenterButtonClick() {
|
||||
if (this.centerButtonState === "default") {
|
||||
if (this.params) {
|
||||
if (this.params && this.isCenterButtonEnabled()) {
|
||||
this.centerButtonElement?.action(this.params);
|
||||
}
|
||||
return;
|
||||
@@ -810,6 +807,28 @@ export class RadialMenu implements Layer {
|
||||
public updateCenterButtonState(state: CenterButtonState) {
|
||||
this.centerButtonState = state;
|
||||
if (state === "back") {
|
||||
const backButtonSize = this.config.centerButtonSize * 0.8; // Make back button 20% smaller
|
||||
this.menuElement
|
||||
.select(".center-button-hitbox")
|
||||
.transition()
|
||||
.duration(0)
|
||||
.attr("r", backButtonSize);
|
||||
this.menuElement
|
||||
.select(".center-button-visible")
|
||||
.transition()
|
||||
.duration(0)
|
||||
.attr("r", backButtonSize);
|
||||
|
||||
const backIconImg = this.menuElement.select(".center-button-icon");
|
||||
backIconImg
|
||||
.attr("xlink:href", backIcon)
|
||||
.attr("width", this.backIconSize)
|
||||
.attr("height", this.backIconSize)
|
||||
.attr("x", -this.backIconSize / 2)
|
||||
.attr("y", -this.backIconSize / 2);
|
||||
}
|
||||
if (state === "default") {
|
||||
// Restore original button size
|
||||
this.menuElement
|
||||
.select(".center-button-hitbox")
|
||||
.transition()
|
||||
@@ -821,15 +840,6 @@ export class RadialMenu implements Layer {
|
||||
.duration(0)
|
||||
.attr("r", this.config.centerButtonSize);
|
||||
|
||||
const backIconImg = this.menuElement.select(".center-button-icon");
|
||||
backIconImg
|
||||
.attr("xlink:href", backIcon)
|
||||
.attr("width", this.backIconSize)
|
||||
.attr("height", this.backIconSize)
|
||||
.attr("x", -this.backIconSize / 2)
|
||||
.attr("y", -this.backIconSize / 2);
|
||||
}
|
||||
if (state === "default") {
|
||||
const iconImg = this.menuElement.select(".center-button-icon");
|
||||
iconImg
|
||||
.attr("xlink:href", this.originalCenterButtonIcon)
|
||||
@@ -857,6 +867,11 @@ export class RadialMenu implements Layer {
|
||||
}
|
||||
|
||||
private isCenterButtonEnabled(): boolean {
|
||||
// Back button should always be enabled when in submenu levels
|
||||
if (this.currentLevel > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.params && this.centerButtonElement) {
|
||||
return !this.centerButtonElement.disabled(this.params);
|
||||
}
|
||||
@@ -889,18 +904,6 @@ export class RadialMenu implements Layer {
|
||||
return this.currentLevel;
|
||||
}
|
||||
|
||||
public setRootMenuItems(
|
||||
items: MenuElement[],
|
||||
centerButton: CenterButtonElement,
|
||||
) {
|
||||
this.currentMenuItems = [...items];
|
||||
this.rootMenuItems = [...items];
|
||||
this.centerButtonElement = centerButton;
|
||||
if (this.isVisible) {
|
||||
this.refreshMenu();
|
||||
}
|
||||
}
|
||||
|
||||
public setParams(params: MenuElementParams) {
|
||||
this.params = params;
|
||||
}
|
||||
@@ -913,7 +916,7 @@ export class RadialMenu implements Layer {
|
||||
this.currentLevel = 0;
|
||||
this.menuStack = [];
|
||||
|
||||
this.currentMenuItems = [...this.rootMenuItems];
|
||||
this.currentMenuItems = this.rootMenu.subMenu!(this.params!);
|
||||
|
||||
this.navigationInProgress = false;
|
||||
|
||||
@@ -944,6 +947,49 @@ export class RadialMenu implements Layer {
|
||||
this.renderMenuItems(this.currentMenuItems, this.currentLevel);
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
if (!this.isVisible || !this.params) return;
|
||||
|
||||
// Refresh the disabled state of all menu items
|
||||
this.menuPaths.forEach((path, itemId) => {
|
||||
const item = this.findMenuItem(itemId);
|
||||
if (item) {
|
||||
const disabled = this.isItemDisabled(item);
|
||||
const color = disabled
|
||||
? this.config.disabledColor
|
||||
: (item.color ?? "#333333");
|
||||
const opacity = disabled ? 0.5 : 0.7;
|
||||
|
||||
// Update path appearance
|
||||
path.attr(
|
||||
"fill",
|
||||
d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color,
|
||||
);
|
||||
path.style("opacity", disabled ? 0.5 : 1);
|
||||
path.style("cursor", disabled ? "not-allowed" : "pointer");
|
||||
|
||||
// Update icon/text appearance using the same logic as renderIconsAndText
|
||||
const icon = this.menuIcons.get(itemId);
|
||||
if (icon) {
|
||||
// Update text opacity
|
||||
const textElement = icon.select("text");
|
||||
if (!textElement.empty()) {
|
||||
textElement.style("opacity", disabled ? 0.5 : 1);
|
||||
}
|
||||
|
||||
// Update image opacity
|
||||
const imageElement = icon.select("image");
|
||||
if (!imageElement.empty()) {
|
||||
imageElement.attr("opacity", disabled ? 0.5 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh center button state
|
||||
this.updateCenterButtonState(this.centerButtonState);
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
// No need to render anything on the canvas
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Config } from "../../../core/configuration/Config";
|
||||
import {
|
||||
AllPlayers,
|
||||
Cell,
|
||||
PlayerActions,
|
||||
UnitType,
|
||||
} from "../../../core/game/Game";
|
||||
import { AllPlayers, PlayerActions, UnitType } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { flattenedEmojiTable } from "../../../core/Util";
|
||||
@@ -352,9 +347,9 @@ export const buildMenuElement: MenuElement = {
|
||||
: undefined,
|
||||
icon: item.icon,
|
||||
tooltipItems: [
|
||||
{ text: translateText(item.key || ""), className: "title" },
|
||||
{ text: translateText(item.key ?? ""), className: "title" },
|
||||
{
|
||||
text: translateText(item.description || ""),
|
||||
text: translateText(item.description ?? ""),
|
||||
className: "description",
|
||||
},
|
||||
{
|
||||
@@ -401,7 +396,7 @@ export const boatMenuElement: MenuElement = {
|
||||
|
||||
params.playerActionHandler.handleBoatAttack(
|
||||
params.myPlayer,
|
||||
params.selected?.id() || null,
|
||||
params.selected?.id() ?? null,
|
||||
params.tile,
|
||||
spawn !== false ? spawn : null,
|
||||
);
|
||||
@@ -423,15 +418,11 @@ export const centerButtonElement: CenterButtonElement = {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return !params.playerActions.canAttack;
|
||||
},
|
||||
action: (params: MenuElementParams) => {
|
||||
if (params.game.inSpawnPhase()) {
|
||||
const cell = new Cell(
|
||||
params.game.x(params.tile),
|
||||
params.game.y(params.tile),
|
||||
);
|
||||
params.playerActionHandler.handleSpawn(cell);
|
||||
params.playerActionHandler.handleSpawn(params.tile);
|
||||
} else {
|
||||
params.playerActionHandler.handleAttack(
|
||||
params.myPlayer,
|
||||
@@ -442,8 +433,17 @@ export const centerButtonElement: CenterButtonElement = {
|
||||
},
|
||||
};
|
||||
|
||||
export const rootMenuItems: MenuElement[] = [
|
||||
infoMenuElement,
|
||||
boatMenuElement,
|
||||
buildMenuElement,
|
||||
];
|
||||
export const rootMenuElement: MenuElement = {
|
||||
id: "root",
|
||||
name: "root",
|
||||
disabled: () => false,
|
||||
icon: infoIcon,
|
||||
color: COLORS.info,
|
||||
subMenu: (params: MenuElementParams) => {
|
||||
let ally = allyRequestElement;
|
||||
if (params.selected?.isAlliedWith(params.myPlayer)) {
|
||||
ally = allyBreakElement;
|
||||
}
|
||||
return [infoMenuElement, boatMenuElement, ally, buildMenuElement];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,7 +11,10 @@ import { translateText } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class ShowReplayPanelEvent {
|
||||
constructor(public visible: boolean = true) {}
|
||||
constructor(
|
||||
public visible: boolean = true,
|
||||
public isSingleplayer: boolean = false,
|
||||
) {}
|
||||
}
|
||||
|
||||
@customElement("replay-panel")
|
||||
@@ -36,6 +39,7 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
if (this.eventBus) {
|
||||
this.eventBus.on(ShowReplayPanelEvent, (event: ShowReplayPanelEvent) => {
|
||||
this.visible = event.visible;
|
||||
this.isSingleplayer = event.isSingleplayer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { getGamesPlayed } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
@@ -55,8 +56,8 @@ export class SpawnAd extends LitElement implements Layer {
|
||||
this.g.ticks() > 10 &&
|
||||
this.gamesPlayed > 5
|
||||
) {
|
||||
console.log("showing bottom left ad");
|
||||
this.show();
|
||||
console.log("not showing spawn ad");
|
||||
// this.show();
|
||||
}
|
||||
if (this.isVisible && !this.g.inSpawnPhase()) {
|
||||
console.log("hiding bottom left ad");
|
||||
@@ -123,7 +124,9 @@ export class SpawnAd extends LitElement implements Layer {
|
||||
class="w-full h-full flex items-center justify-center"
|
||||
>
|
||||
${!this.adLoaded
|
||||
? html`<span class="text-white text-sm">Loading ad...</span>`
|
||||
? html`<span class="text-white text-sm"
|
||||
>${translateText("spawn_ad.loading")}</span
|
||||
>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,17 +59,11 @@ export class SpawnTimer implements Layer {
|
||||
|
||||
const barHeight = 10;
|
||||
const barWidth = this.transformHandler.width();
|
||||
let yOffset: number;
|
||||
|
||||
if (this.game.inSpawnPhase()) {
|
||||
// At spawn time, draw at top
|
||||
yOffset = 0;
|
||||
} else if (this.game.config().gameConfig().gameMode === GameMode.Team) {
|
||||
// After spawn, only in team mode, offset based on screen width
|
||||
const screenW = window.innerWidth;
|
||||
yOffset = screenW > 1024 ? 80 : 58;
|
||||
} else {
|
||||
// Not spawn and not team mode: no bar
|
||||
if (
|
||||
!this.game.inSpawnPhase() &&
|
||||
this.game.config().gameConfig().gameMode !== GameMode.Team
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,7 +74,7 @@ export class SpawnTimer implements Layer {
|
||||
const segmentWidth = barWidth * ratio;
|
||||
|
||||
context.fillStyle = this.colors[i];
|
||||
context.fillRect(x, yOffset, segmentWidth, barHeight);
|
||||
context.fillRect(x, 0, segmentWidth, barHeight);
|
||||
|
||||
x += segmentWidth;
|
||||
filledRatio += ratio;
|
||||
|
||||
@@ -52,7 +52,7 @@ export class TeamStats extends LitElement implements Layer {
|
||||
for (const player of players) {
|
||||
const team = player.team();
|
||||
if (team === null) continue;
|
||||
if (!grouped[team]) grouped[team] = [];
|
||||
grouped[team] ??= [];
|
||||
grouped[team].push(player);
|
||||
}
|
||||
|
||||
|
||||
@@ -317,10 +317,16 @@ export class UILayer implements Layer {
|
||||
if (constructionType === undefined) {
|
||||
return 1;
|
||||
}
|
||||
const constDuration =
|
||||
this.game.unitInfo(constructionType).constructionDuration;
|
||||
if (constDuration === undefined) {
|
||||
throw new Error("unit does not have constructionTime");
|
||||
}
|
||||
return (
|
||||
(this.game.ticks() - unit.createdAt()) /
|
||||
(this.game.unitInfo(constructionType).constructionDuration || 1)
|
||||
(constDuration === 0 ? 1 : constDuration)
|
||||
);
|
||||
|
||||
case UnitType.MissileSilo:
|
||||
case UnitType.SAMLauncher:
|
||||
return unit.missileReadinesss();
|
||||
|
||||
@@ -24,12 +24,21 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
private _port = 0;
|
||||
private _defensePost = 0;
|
||||
private _samLauncher = 0;
|
||||
private allDisabled = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
const config = this.game.config();
|
||||
this.allDisabled =
|
||||
config.isUnitDisabled(UnitType.City) &&
|
||||
config.isUnitDisabled(UnitType.Factory) &&
|
||||
config.isUnitDisabled(UnitType.Port) &&
|
||||
config.isUnitDisabled(UnitType.DefensePost) &&
|
||||
config.isUnitDisabled(UnitType.MissileSilo) &&
|
||||
config.isUnitDisabled(UnitType.SAMLauncher);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -89,6 +98,10 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.allDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] bg-gray-800/70 backdrop-blur-sm border border-slate-400 rounded-lg p-2 hidden lg:block"
|
||||
|
||||
+12
-2
@@ -332,10 +332,20 @@
|
||||
class="ml-2 mr-4"
|
||||
/>
|
||||
</a>
|
||||
<a href="/privacy-policy.html" class="t-link" target="_blank">
|
||||
<a
|
||||
href="/privacy-policy.html"
|
||||
data-i18n="main.privacy_policy"
|
||||
class="t-link"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
<a href="/terms-of-service.html" class="t-link" target="_blank">
|
||||
<a
|
||||
href="/terms-of-service.html"
|
||||
data-i18n="main.terms_of_service"
|
||||
class="t-link"
|
||||
target="_blank"
|
||||
>
|
||||
Terms of Service
|
||||
</a>
|
||||
<p style="text-align: center">
|
||||
|
||||
+2
-3
@@ -104,9 +104,8 @@ export type IsLoggedInResponse =
|
||||
| false;
|
||||
let __isLoggedIn: IsLoggedInResponse | undefined = undefined;
|
||||
export function isLoggedIn(): IsLoggedInResponse {
|
||||
if (__isLoggedIn === undefined) {
|
||||
__isLoggedIn = _isLoggedIn();
|
||||
}
|
||||
__isLoggedIn ??= _isLoggedIn();
|
||||
|
||||
return __isLoggedIn;
|
||||
}
|
||||
function _isLoggedIn(): IsLoggedInResponse {
|
||||
|
||||
@@ -369,6 +369,11 @@ label.option-card:hover {
|
||||
mask: url("../../resources/images/CityIconWhite.svg") no-repeat center / cover;
|
||||
}
|
||||
|
||||
#helpModal .factory-icon {
|
||||
mask: url("../../resources/images/FactoryIconWhite.svg") no-repeat center /
|
||||
cover;
|
||||
}
|
||||
|
||||
#helpModal .defense-post-icon {
|
||||
mask: url("../../resources/images/ShieldIconWhite.svg") no-repeat center /
|
||||
cover;
|
||||
@@ -626,7 +631,7 @@ label.option-card:hover {
|
||||
}
|
||||
|
||||
/* News Button Notification */
|
||||
.news-button .active button {
|
||||
news-button .active button {
|
||||
position: relative;
|
||||
border-color: #2563eb !important;
|
||||
border-width: 2px !important;
|
||||
|
||||
Reference in New Issue
Block a user