mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-02 15:48:08 +00:00
Merge branch 'main' into icslucas-patch-1
This commit is contained in:
@@ -13,6 +13,7 @@ import { ServerConfig } from "../core/configuration/Config";
|
||||
import { getConfig } from "../core/configuration/ConfigLoader";
|
||||
import { PlayerActions, UnitType } from "../core/game/Game";
|
||||
import { TileRef } from "../core/game/GameMap";
|
||||
import { GameMapLoader } from "../core/game/GameMapLoader";
|
||||
import {
|
||||
ErrorUpdate,
|
||||
GameUpdateType,
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
} from "./InputHandler";
|
||||
import { endGame, startGame, startTime } from "./LocalPersistantStats";
|
||||
import { getPersistentID } from "./Main";
|
||||
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||
import {
|
||||
SendAttackIntentEvent,
|
||||
SendBoatAttackIntentEvent,
|
||||
@@ -58,12 +60,11 @@ export interface LobbyConfig {
|
||||
}
|
||||
|
||||
export function joinLobby(
|
||||
eventBus: EventBus,
|
||||
lobbyConfig: LobbyConfig,
|
||||
onPrestart: () => void,
|
||||
onJoin: () => void,
|
||||
): () => void {
|
||||
const eventBus = new EventBus();
|
||||
|
||||
console.log(
|
||||
`joining lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}`,
|
||||
);
|
||||
@@ -82,7 +83,7 @@ export function joinLobby(
|
||||
const onmessage = (message: ServerMessage) => {
|
||||
if (message.type === "prestart") {
|
||||
console.log(`lobby: game prestarting: ${JSON.stringify(message)}`);
|
||||
terrainLoad = loadTerrainMap(message.gameMap);
|
||||
terrainLoad = loadTerrainMap(message.gameMap, terrainMapFileLoader);
|
||||
onPrestart();
|
||||
}
|
||||
if (message.type === "start") {
|
||||
@@ -98,6 +99,7 @@ export function joinLobby(
|
||||
transport,
|
||||
userSettings,
|
||||
terrainLoad,
|
||||
terrainMapFileLoader,
|
||||
).then((r) => r.start());
|
||||
}
|
||||
if (message.type === "error") {
|
||||
@@ -125,6 +127,7 @@ async function createClientGame(
|
||||
transport: Transport,
|
||||
userSettings: UserSettings,
|
||||
terrainLoad: Promise<TerrainMapData> | null,
|
||||
mapLoader: GameMapLoader,
|
||||
): Promise<ClientGameRunner> {
|
||||
if (lobbyConfig.gameStartInfo === undefined) {
|
||||
throw new Error("missing gameStartInfo");
|
||||
@@ -139,7 +142,10 @@ async function createClientGame(
|
||||
if (terrainLoad) {
|
||||
gameMap = await terrainLoad;
|
||||
} else {
|
||||
gameMap = await loadTerrainMap(lobbyConfig.gameStartInfo.config.gameMap);
|
||||
gameMap = await loadTerrainMap(
|
||||
lobbyConfig.gameStartInfo.config.gameMap,
|
||||
mapLoader,
|
||||
);
|
||||
}
|
||||
const worker = new WorkerClient(
|
||||
lobbyConfig.gameStartInfo,
|
||||
|
||||
@@ -68,8 +68,21 @@ export class FlagInput extends LitElement {
|
||||
super.connectedCallback();
|
||||
this.flag = this.getStoredFlag();
|
||||
this.dispatchFlagEvent();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.showModal = false;
|
||||
}
|
||||
};
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
+17
-4
@@ -15,6 +15,23 @@ export class HelpModal extends LitElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal
|
||||
@@ -166,11 +183,7 @@ export class HelpModal extends LitElement {
|
||||
<div>
|
||||
<p class="mb-4">${translateText("help_modal.ui_control_desc")}</p>
|
||||
<ul>
|
||||
<li class="mb-4">${translateText("help_modal.ui_pop")}</li>
|
||||
<li class="mb-4">${translateText("help_modal.ui_gold")}</li>
|
||||
<li class="mb-4">
|
||||
${translateText("help_modal.ui_troops_workers")}
|
||||
</li>
|
||||
<li class="mb-4">
|
||||
${translateText("help_modal.ui_attack_ratio")}
|
||||
</li>
|
||||
|
||||
@@ -14,7 +14,12 @@ import {
|
||||
mapCategories,
|
||||
} from "../core/game/Game";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import { GameConfig, GameInfo, TeamCountConfig } from "../core/Schemas";
|
||||
import {
|
||||
ClientInfo,
|
||||
GameConfig,
|
||||
GameInfo,
|
||||
TeamCountConfig,
|
||||
} from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import "./components/baseComponents/Modal";
|
||||
import "./components/Difficulties";
|
||||
@@ -40,9 +45,10 @@ export class HostLobbyModal extends LitElement {
|
||||
@state() private instantBuild: boolean = false;
|
||||
@state() private lobbyId = "";
|
||||
@state() private copySuccess = false;
|
||||
@state() private players: string[] = [];
|
||||
@state() private clients: ClientInfo[] = [];
|
||||
@state() private useRandomMap: boolean = false;
|
||||
@state() private disabledUnits: UnitType[] = [UnitType.Factory];
|
||||
@state() private lobbyCreatorClientID: string = "";
|
||||
@state() private lobbyIdVisible: boolean = true;
|
||||
|
||||
private playersInterval: NodeJS.Timeout | null = null;
|
||||
@@ -50,6 +56,23 @@ export class HostLobbyModal extends LitElement {
|
||||
private botsUpdateTimer: number | null = null;
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal title=${translateText("host_modal.title")}>
|
||||
@@ -111,7 +134,7 @@ export class HostLobbyModal extends LitElement {
|
||||
<span class="lobby-id" @click=${this.copyToClipboard} style="cursor: pointer;">
|
||||
${this.lobbyIdVisible ? this.lobbyId : "••••••••"}
|
||||
</span>
|
||||
|
||||
|
||||
<!-- Copy icon/success indicator -->
|
||||
<div @click=${this.copyToClipboard} style="margin-left: 8px; cursor: pointer;">
|
||||
${
|
||||
@@ -395,29 +418,45 @@ export class HostLobbyModal extends LitElement {
|
||||
<!-- Lobby Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">
|
||||
${this.players.length}
|
||||
${this.clients.length}
|
||||
${
|
||||
this.players.length === 1
|
||||
this.clients.length === 1
|
||||
? translateText("host_modal.player")
|
||||
: translateText("host_modal.players")
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="players-list">
|
||||
${this.players.map(
|
||||
(player) => html`<span class="player-tag">${player}</span>`,
|
||||
${this.clients.map(
|
||||
(client) => html`
|
||||
<span class="player-tag">
|
||||
${client.username}
|
||||
${client.clientID === this.lobbyCreatorClientID
|
||||
? html`<span class="host-badge"
|
||||
>(${translateText("host_modal.host_badge")})</span
|
||||
>`
|
||||
: html`
|
||||
<button
|
||||
class="remove-player-btn"
|
||||
@click=${() => this.kickPlayer(client.clientID)}
|
||||
title="Remove ${client.username}"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
`}
|
||||
</span>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="start-game-button-container">
|
||||
<button
|
||||
@click=${this.startGame}
|
||||
?disabled=${this.players.length < 2}
|
||||
?disabled=${this.clients.length < 2}
|
||||
class="start-game-button"
|
||||
>
|
||||
${
|
||||
this.players.length === 1
|
||||
this.clients.length === 1
|
||||
? translateText("host_modal.waiting")
|
||||
: translateText("host_modal.start")
|
||||
}
|
||||
@@ -434,12 +473,13 @@ export class HostLobbyModal extends LitElement {
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.lobbyCreatorClientID = generateID();
|
||||
this.lobbyIdVisible = this.userSettings.get(
|
||||
"settings.lobbyIdVisibility",
|
||||
true,
|
||||
);
|
||||
|
||||
createLobby()
|
||||
createLobby(this.lobbyCreatorClientID)
|
||||
.then((lobby) => {
|
||||
this.lobbyId = lobby.gameID;
|
||||
// join lobby
|
||||
@@ -449,7 +489,7 @@ export class HostLobbyModal extends LitElement {
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: this.lobbyId,
|
||||
clientID: generateID(),
|
||||
clientID: this.lobbyCreatorClientID,
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
@@ -633,17 +673,29 @@ export class HostLobbyModal extends LitElement {
|
||||
.then((response) => response.json())
|
||||
.then((data: GameInfo) => {
|
||||
console.log(`got game info response: ${JSON.stringify(data)}`);
|
||||
this.players = data.clients?.map((p) => p.username) ?? [];
|
||||
|
||||
this.clients = data.clients ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
private kickPlayer(clientID: string) {
|
||||
// Dispatch event to be handled by WebSocket instead of HTTP
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("kick-player", {
|
||||
detail: { target: clientID },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function createLobby(): Promise<GameInfo> {
|
||||
async function createLobby(creatorClientID: string): Promise<GameInfo> {
|
||||
const config = await getServerConfigFromClient();
|
||||
try {
|
||||
const id = generateID();
|
||||
const response = await fetch(
|
||||
`/${config.workerPath(id)}/api/create_game/${id}`,
|
||||
`/${config.workerPath(id)}/api/create_game/${id}?creatorClientID=${encodeURIComponent(creatorClientID)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -654,6 +706,8 @@ async function createLobby(): Promise<GameInfo> {
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Server error response:", errorText);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -663,6 +717,6 @@ async function createLobby(): Promise<GameInfo> {
|
||||
return data as GameInfo;
|
||||
} catch (error) {
|
||||
console.error("Error creating lobby:", error);
|
||||
throw error; // Re-throw the error so the caller can handle it
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,8 @@ export class CloseViewEvent implements GameEvent {}
|
||||
|
||||
export class RefreshGraphicsEvent implements GameEvent {}
|
||||
|
||||
export class TogglePerformanceOverlayEvent implements GameEvent {}
|
||||
|
||||
export class ToggleStructureEvent implements GameEvent {
|
||||
constructor(public readonly structureType: UnitType | null) {}
|
||||
}
|
||||
@@ -183,6 +185,14 @@ export class InputHandler {
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
|
||||
// Skip if shift is held down
|
||||
if (
|
||||
this.activeKeys.has("ShiftLeft") ||
|
||||
this.activeKeys.has("ShiftRight")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.activeKeys.has(this.keybinds.moveUp) ||
|
||||
this.activeKeys.has("ArrowUp")
|
||||
@@ -258,6 +268,8 @@ export class InputHandler {
|
||||
this.keybinds.centerCamera,
|
||||
"ControlLeft",
|
||||
"ControlRight",
|
||||
"ShiftLeft",
|
||||
"ShiftRight",
|
||||
].includes(e.code)
|
||||
) {
|
||||
this.activeKeys.add(e.code);
|
||||
@@ -300,6 +312,14 @@ export class InputHandler {
|
||||
this.eventBus.emit(new CenterCameraEvent());
|
||||
}
|
||||
|
||||
// Shift-D to toggle performance overlay
|
||||
console.log(e.code, e.shiftKey, e.ctrlKey, e.altKey, e.metaKey);
|
||||
if (e.code === "KeyD" && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
console.log("TogglePerformanceOverlayEvent");
|
||||
this.eventBus.emit(new TogglePerformanceOverlayEvent());
|
||||
}
|
||||
|
||||
this.activeKeys.delete(e.code);
|
||||
});
|
||||
}
|
||||
@@ -371,7 +391,8 @@ export class InputHandler {
|
||||
|
||||
private onShiftScroll(event: WheelEvent) {
|
||||
if (event.shiftKey) {
|
||||
const ratio = event.deltaY > 0 ? -10 : 10;
|
||||
const scrollValue = event.deltaY === 0 ? event.deltaX : event.deltaY;
|
||||
const ratio = scrollValue > 0 ? -10 : 10;
|
||||
this.eventBus.emit(new AttackRatioEvent(ratio));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,23 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
|
||||
private playersInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal title=${translateText("private_lobby.title")}>
|
||||
|
||||
@@ -292,7 +292,7 @@ export class LangSelector extends LitElement {
|
||||
<button
|
||||
id="lang-selector"
|
||||
@click=${this.openModal}
|
||||
class="text-center appearance-none w-full bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md border-none cursor-pointer transition-colors duration-300 flex items-center gap-2 justify-center"
|
||||
class="text-center appearance-none w-full bg-blue-100 dark:bg-gray-700 hover:bg-blue-200 dark:hover:bg-gray-600 text-blue-900 dark:text-gray-100 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md border-none cursor-pointer transition-colors duration-300 flex items-center gap-2 justify-center"
|
||||
>
|
||||
<img
|
||||
id="lang-flag"
|
||||
|
||||
+53
-122
@@ -1,4 +1,4 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
|
||||
@@ -8,117 +8,9 @@ export class LanguageModal extends LitElement {
|
||||
@property({ type: Array }) languageList: any[] = [];
|
||||
@property({ type: String }) currentLang = "en";
|
||||
|
||||
static styles = css`
|
||||
.c-modal {
|
||||
position: fixed;
|
||||
padding: 1rem;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.c-modal__wrapper {
|
||||
background: #23232382;
|
||||
border-radius: 8px;
|
||||
min-width: 340px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c-modal__header {
|
||||
position: relative;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
font-size: 18px;
|
||||
background: #000000a1;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
padding: 1rem 2.4rem 1rem 1.4rem;
|
||||
}
|
||||
|
||||
.c-modal__close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.c-modal__content {
|
||||
position: relative;
|
||||
color: #fff;
|
||||
padding: 1.4rem;
|
||||
max-height: 60dvh;
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.lang-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: background-color 0.3s;
|
||||
border: 1px solid #aaa;
|
||||
background-color: #505050;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.lang-button:hover {
|
||||
background-color: #969696;
|
||||
}
|
||||
|
||||
.lang-button.active {
|
||||
background-color: #aaaaaa;
|
||||
border-color: #bbb;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.flag-icon {
|
||||
width: 24px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@keyframes rainbow {
|
||||
0% {
|
||||
background-color: #990033;
|
||||
}
|
||||
20% {
|
||||
background-color: #996600;
|
||||
}
|
||||
40% {
|
||||
background-color: #336600;
|
||||
}
|
||||
60% {
|
||||
background-color: #008080;
|
||||
}
|
||||
80% {
|
||||
background-color: #1c3f99;
|
||||
}
|
||||
100% {
|
||||
background-color: #5e0099;
|
||||
}
|
||||
}
|
||||
|
||||
.lang-button.debug {
|
||||
animation: rainbow 10s infinite;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
border: 2px dashed aqua;
|
||||
box-shadow: 0 0 4px aqua;
|
||||
}
|
||||
`;
|
||||
createRenderRoot() {
|
||||
return this; // Use Light DOM for TailwindCSS classes
|
||||
}
|
||||
|
||||
private close = () => {
|
||||
this.dispatchEvent(
|
||||
@@ -139,11 +31,24 @@ export class LanguageModal extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
private selectLanguage = (lang: string) => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("language-selected", {
|
||||
@@ -158,27 +63,53 @@ export class LanguageModal extends LitElement {
|
||||
if (!this.visible) return null;
|
||||
|
||||
return html`
|
||||
<aside class="c-modal">
|
||||
<div class="c-modal__wrapper">
|
||||
<header class="c-modal__header">
|
||||
<aside
|
||||
class="fixed p-4 z-[1000] inset-0 bg-black/50 overflow-y-auto flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800/80 dark:bg-gray-900/90 backdrop-blur-md rounded-lg min-w-[340px] max-w-[480px] w-full"
|
||||
>
|
||||
<header
|
||||
class="relative rounded-t-md text-lg bg-black/60 dark:bg-black/80 text-center text-white px-6 py-4 pr-10"
|
||||
>
|
||||
${translateText("select_lang.title")}
|
||||
<div class="c-modal__close" @click=${this.close}>✕</div>
|
||||
<div
|
||||
class="cursor-pointer absolute right-4 top-4 font-bold hover:text-gray-300"
|
||||
@click=${this.close}
|
||||
>
|
||||
✕
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="c-modal__content">
|
||||
<section
|
||||
class="relative text-white dark:text-gray-100 p-6 max-h-[60dvh] overflow-y-auto"
|
||||
>
|
||||
${this.languageList.map((lang) => {
|
||||
const isActive = this.currentLang === lang.code;
|
||||
const isDebug = lang.code === "debug";
|
||||
|
||||
let buttonClasses =
|
||||
"w-full flex items-center gap-2 p-2 mb-2 rounded-md transition-colors duration-300 border";
|
||||
|
||||
if (isDebug) {
|
||||
buttonClasses +=
|
||||
" animate-pulse font-bold text-white border-2 border-dashed border-cyan-400 shadow-lg shadow-cyan-400/25 bg-gradient-to-r from-red-600 via-yellow-600 via-green-600 via-blue-600 to-purple-600";
|
||||
} else if (isActive) {
|
||||
buttonClasses +=
|
||||
" bg-gray-400 dark:bg-gray-500 border-gray-300 dark:border-gray-400 text-black dark:text-white";
|
||||
} else {
|
||||
buttonClasses +=
|
||||
" bg-gray-600 dark:bg-gray-700 border-gray-500 dark:border-gray-600 text-white dark:text-gray-100 hover:bg-gray-500 dark:hover:bg-gray-600";
|
||||
}
|
||||
|
||||
return html`
|
||||
<button
|
||||
class="lang-button ${isActive ? "active" : ""} ${lang.code ===
|
||||
"debug"
|
||||
? "debug"
|
||||
: ""}"
|
||||
class="${buttonClasses}"
|
||||
@click=${() => this.selectLanguage(lang.code)}
|
||||
>
|
||||
<img
|
||||
src="/flags/${lang.svg}.svg"
|
||||
class="flag-icon"
|
||||
class="w-6 h-4 object-contain"
|
||||
alt="${lang.code}"
|
||||
/>
|
||||
<span>${lang.native} (${lang.en})</span>
|
||||
|
||||
@@ -97,11 +97,6 @@ export class LocalServer {
|
||||
return;
|
||||
}
|
||||
if (this.paused) {
|
||||
if (clientMsg.intent.type === "troop_ratio") {
|
||||
// Store troop change events because otherwise they are
|
||||
// not registered when game is paused.
|
||||
this.intents.push(clientMsg.intent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.intents.push(clientMsg.intent);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import favicon from "../../resources/images/Favicon.svg";
|
||||
import version from "../../resources/version.txt";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { EventBus } from "../core/EventBus";
|
||||
import { GameRecord, GameStartInfo, ID } from "../core/Schemas";
|
||||
import { ServerConfig } from "../core/configuration/Config";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
@@ -24,6 +25,7 @@ import "./PublicLobby";
|
||||
import { PublicLobby } from "./PublicLobby";
|
||||
import { SinglePlayerModal } from "./SinglePlayerModal";
|
||||
import { TerritoryPatternsModal } from "./TerritoryPatternsModal";
|
||||
import { SendKickPlayerIntentEvent } from "./Transport";
|
||||
import { UserSettingModal } from "./UserSettingModal";
|
||||
import "./UsernameInput";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
@@ -77,6 +79,7 @@ export interface JoinLobbyEvent {
|
||||
|
||||
class Client {
|
||||
private gameStop: (() => void) | null = null;
|
||||
private eventBus: EventBus = new EventBus();
|
||||
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
private flagInput: FlagInput | null = null;
|
||||
@@ -163,11 +166,13 @@ class Client {
|
||||
setFavicon();
|
||||
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
|
||||
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
|
||||
document.addEventListener("kick-player", this.handleKickPlayer.bind(this));
|
||||
|
||||
const spModal = document.querySelector(
|
||||
"single-player-modal",
|
||||
) as SinglePlayerModal;
|
||||
spModal instanceof SinglePlayerModal;
|
||||
|
||||
const singlePlayer = document.getElementById("single-player");
|
||||
if (singlePlayer === null) throw new Error("Missing single-player");
|
||||
singlePlayer.addEventListener("click", () => {
|
||||
@@ -429,6 +434,7 @@ class Client {
|
||||
const config = await getServerConfigFromClient();
|
||||
|
||||
this.gameStop = joinLobby(
|
||||
this.eventBus,
|
||||
{
|
||||
gameID: lobby.gameID,
|
||||
serverConfig: config,
|
||||
@@ -514,6 +520,15 @@ class Client {
|
||||
this.gameStop = null;
|
||||
this.publicLobby.leaveLobby();
|
||||
}
|
||||
|
||||
private handleKickPlayer(event: CustomEvent) {
|
||||
const { target } = event.detail;
|
||||
|
||||
// Forward to eventBus if available
|
||||
if (this.eventBus) {
|
||||
this.eventBus.emit(new SendKickPlayerIntentEvent(target));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the client when the DOM is loaded
|
||||
|
||||
@@ -13,6 +13,23 @@ export class NewsModal extends LitElement {
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
@property({ type: String }) markdown = "Loading...";
|
||||
|
||||
private initialized: boolean = false;
|
||||
|
||||
@@ -2,10 +2,10 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { GameMapType, GameMode } from "../core/game/Game";
|
||||
import { terrainMapFileLoader } from "../core/game/TerrainMapFileLoader";
|
||||
import { GameID, GameInfo } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||
|
||||
@customElement("public-lobby")
|
||||
export class PublicLobby extends LitElement {
|
||||
|
||||
@@ -47,6 +47,23 @@ export class SinglePlayerModal extends LitElement {
|
||||
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal title=${translateText("single_modal.title")}>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import version from "../../resources/version.txt";
|
||||
import { FetchGameMapLoader } from "../core/game/FetchGameMapLoader";
|
||||
|
||||
export const terrainMapFileLoader = new FetchGameMapLoader(`/maps`, version);
|
||||
@@ -37,14 +37,16 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
private isActive = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.selectedPattern = this.userSettings.getSelectedPattern();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
this.selectedPattern = this.userSettings.getSelectedPattern();
|
||||
this.updateComplete.then(() => {
|
||||
const containers = this.renderRoot.querySelectorAll(".preview-container");
|
||||
if (this.resizeObserver) {
|
||||
@@ -54,12 +56,12 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
}
|
||||
this.updatePreview();
|
||||
});
|
||||
this.open();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
this.resizeObserver.disconnect();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
async onUserMe(userMeResponse: UserMeResponse | null) {
|
||||
@@ -69,6 +71,11 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
const nextSequence = [...this.keySequence, key].slice(-5);
|
||||
this.keySequence = nextSequence;
|
||||
@@ -220,6 +227,7 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isActive) return html``;
|
||||
return html`
|
||||
${this.renderTooltip()}
|
||||
<o-modal
|
||||
@@ -233,10 +241,15 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
|
||||
public open() {
|
||||
this.modalEl?.open();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.modalEl?.close();
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
this.resizeObserver?.disconnect();
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
private selectPattern(pattern: string | undefined) {
|
||||
@@ -336,6 +349,7 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
const patternCache = new Map<string, string>();
|
||||
const DEFAULT_PATTERN_B64 = "AAAAAA"; // Empty 2x2 pattern
|
||||
const COLOR_SET = [0, 0, 0, 255]; // Black
|
||||
const COLOR_UNSET = [255, 255, 255, 255]; // White
|
||||
@@ -344,11 +358,14 @@ export function generatePreviewDataUrl(
|
||||
width?: number,
|
||||
height?: number,
|
||||
): string {
|
||||
pattern ??= DEFAULT_PATTERN_B64;
|
||||
|
||||
if (patternCache.has(pattern)) {
|
||||
return patternCache.get(pattern)!;
|
||||
}
|
||||
|
||||
// Calculate canvas size
|
||||
const decoder = new PatternDecoder(
|
||||
pattern ?? DEFAULT_PATTERN_B64,
|
||||
base64url.decode,
|
||||
);
|
||||
const decoder = new PatternDecoder(pattern, base64url.decode);
|
||||
const scaledWidth = decoder.scaledWidth();
|
||||
const scaledHeight = decoder.scaledHeight();
|
||||
|
||||
@@ -384,5 +401,7 @@ export function generatePreviewDataUrl(
|
||||
|
||||
// Create a data URL
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return canvas.toDataURL("image/png");
|
||||
const dataUrl = canvas.toDataURL("image/png");
|
||||
patternCache.set(pattern, dataUrl);
|
||||
return dataUrl;
|
||||
}
|
||||
|
||||
+15
-15
@@ -141,10 +141,6 @@ export class CancelBoatIntentEvent implements GameEvent {
|
||||
constructor(public readonly unitID: number) {}
|
||||
}
|
||||
|
||||
export class SendSetTargetTroopRatioEvent implements GameEvent {
|
||||
constructor(public readonly ratio: number) {}
|
||||
}
|
||||
|
||||
export class SendWinnerEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly winner: Winner,
|
||||
@@ -165,6 +161,10 @@ export class MoveWarshipIntentEvent implements GameEvent {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendKickPlayerIntentEvent implements GameEvent {
|
||||
constructor(public readonly target: string) {}
|
||||
}
|
||||
|
||||
export class Transport {
|
||||
private socket: WebSocket | null = null;
|
||||
|
||||
@@ -223,9 +223,6 @@ export class Transport {
|
||||
this.eventBus.on(SendEmbargoIntentEvent, (e) =>
|
||||
this.onSendEmbargoIntent(e),
|
||||
);
|
||||
this.eventBus.on(SendSetTargetTroopRatioEvent, (e) =>
|
||||
this.onSendSetTargetTroopRatioEvent(e),
|
||||
);
|
||||
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onBuildUnitIntent(e));
|
||||
|
||||
this.eventBus.on(PauseGameEvent, (e) => this.onPauseGameEvent(e));
|
||||
@@ -241,6 +238,9 @@ export class Transport {
|
||||
this.eventBus.on(MoveWarshipIntentEvent, (e) => {
|
||||
this.onMoveWarshipEvent(e);
|
||||
});
|
||||
this.eventBus.on(SendKickPlayerIntentEvent, (e) =>
|
||||
this.onSendKickPlayerIntent(e),
|
||||
);
|
||||
}
|
||||
|
||||
private startPing() {
|
||||
@@ -525,14 +525,6 @@ export class Transport {
|
||||
});
|
||||
}
|
||||
|
||||
private onSendSetTargetTroopRatioEvent(event: SendSetTargetTroopRatioEvent) {
|
||||
this.sendIntent({
|
||||
type: "troop_ratio",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
ratio: event.ratio,
|
||||
});
|
||||
}
|
||||
|
||||
private onBuildUnitIntent(event: BuildUnitIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "build_unit",
|
||||
@@ -611,6 +603,14 @@ export class Transport {
|
||||
});
|
||||
}
|
||||
|
||||
private onSendKickPlayerIntent(event: SendKickPlayerIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "kick_player",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
target: event.target,
|
||||
});
|
||||
}
|
||||
|
||||
private sendIntent(intent: Intent) {
|
||||
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
|
||||
const msg = {
|
||||
|
||||
@@ -51,6 +51,11 @@ export class UserSettingModal extends LitElement {
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!this.modalEl?.isModalOpen || this.showEasterEggSettings) return;
|
||||
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
const nextSequence = [...this.keySequence, key].slice(-4);
|
||||
this.keySequence = nextSequence;
|
||||
@@ -176,6 +181,13 @@ export class UserSettingModal extends LitElement {
|
||||
console.log("🏳️ Territory Patterns:", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
private togglePerformanceOverlay(e: CustomEvent<{ checked: boolean }>) {
|
||||
const enabled = e.detail?.checked;
|
||||
if (typeof enabled !== "boolean") return;
|
||||
|
||||
this.userSettings.set("settings.performanceOverlay", enabled);
|
||||
}
|
||||
|
||||
private handleKeybindChange(
|
||||
e: CustomEvent<{ action: string; value: string }>,
|
||||
) {
|
||||
@@ -315,6 +327,15 @@ export class UserSettingModal extends LitElement {
|
||||
@change=${this.toggleTerritoryPatterns}
|
||||
></setting-toggle>
|
||||
|
||||
<!-- 📱 Performance Overlay -->
|
||||
<setting-toggle
|
||||
label="${translateText("user_setting.performance_overlay_label")}"
|
||||
description="${translateText("user_setting.performance_overlay_desc")}"
|
||||
id="performance-overlay-toggle"
|
||||
.checked=${this.userSettings.performanceOverlay()}
|
||||
@change=${this.togglePerformanceOverlay}
|
||||
></setting-toggle>
|
||||
|
||||
<!-- ⚔️ Attack Ratio -->
|
||||
<setting-slider
|
||||
label="${translateText("user_setting.attack_ratio_label")}"
|
||||
@@ -326,17 +347,6 @@ export class UserSettingModal extends LitElement {
|
||||
@change=${this.sliderAttackRatio}
|
||||
></setting-slider>
|
||||
|
||||
<!-- 🪖🛠️ Troop Ratio -->
|
||||
<setting-slider
|
||||
label="${translateText("user_setting.troop_ratio_label")}"
|
||||
description="${translateText("user_setting.troop_ratio_desc")}"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${Number(localStorage.getItem("settings.troopRatio") ?? "0.95") *
|
||||
100}
|
||||
@change=${this.sliderTroopRatio}
|
||||
></setting-slider>
|
||||
|
||||
${this.showEasterEggSettings
|
||||
? html`
|
||||
<setting-slider
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { GameMapType } from "../../core/game/Game";
|
||||
import { terrainMapFileLoader } from "../../core/game/TerrainMapFileLoader";
|
||||
import { terrainMapFileLoader } from "../TerrainMapFileLoader";
|
||||
import { translateText } from "../Utils";
|
||||
|
||||
// Add map descriptions
|
||||
|
||||
@@ -48,7 +48,7 @@ export class NewsButton extends LitElement {
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
<img
|
||||
class="size-[48px]"
|
||||
class="size-[48px] dark:invert"
|
||||
src="${megaphone}"
|
||||
alt=${translateText("news.title")}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ChatModal } from "./layers/ChatModal";
|
||||
import { ControlPanel } from "./layers/ControlPanel";
|
||||
import { EmojiTable } from "./layers/EmojiTable";
|
||||
import { EventsDisplay } from "./layers/EventsDisplay";
|
||||
import { FPSDisplay } from "./layers/FPSDisplay";
|
||||
import { FxLayer } from "./layers/FxLayer";
|
||||
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
|
||||
import { GameRightSidebar } from "./layers/GameRightSidebar";
|
||||
@@ -60,10 +61,9 @@ export function createRenderer(
|
||||
if (!emojiTable || !(emojiTable instanceof EmojiTable)) {
|
||||
console.error("EmojiTable element not found in the DOM");
|
||||
}
|
||||
emojiTable.eventBus = eventBus;
|
||||
emojiTable.transformHandler = transformHandler;
|
||||
emojiTable.game = game;
|
||||
emojiTable.initEventBus();
|
||||
emojiTable.initEventBus(eventBus);
|
||||
|
||||
const buildMenu = document.querySelector("build-menu") as BuildMenu;
|
||||
if (!buildMenu || !(buildMenu instanceof BuildMenu)) {
|
||||
@@ -173,7 +173,7 @@ export function createRenderer(
|
||||
console.error("player panel not found");
|
||||
}
|
||||
playerPanel.g = game;
|
||||
playerPanel.eventBus = eventBus;
|
||||
playerPanel.initEventBus(eventBus);
|
||||
playerPanel.emojiTable = emojiTable;
|
||||
playerPanel.uiState = uiState;
|
||||
|
||||
@@ -182,7 +182,7 @@ export function createRenderer(
|
||||
console.error("chat modal not found");
|
||||
}
|
||||
chatModal.g = game;
|
||||
chatModal.eventBus = eventBus;
|
||||
chatModal.initEventBus(eventBus);
|
||||
|
||||
const multiTabModal = document.querySelector(
|
||||
"multi-tab-modal",
|
||||
@@ -202,6 +202,13 @@ export function createRenderer(
|
||||
|
||||
const structureLayer = new StructureLayer(game, eventBus, transformHandler);
|
||||
|
||||
const fpsDisplay = document.querySelector("fps-display") as FPSDisplay;
|
||||
if (!(fpsDisplay instanceof FPSDisplay)) {
|
||||
console.error("fps display not found");
|
||||
}
|
||||
fpsDisplay.eventBus = eventBus;
|
||||
fpsDisplay.userSettings = userSettings;
|
||||
|
||||
const spawnAd = document.querySelector("spawn-ad") as SpawnAd;
|
||||
if (!(spawnAd instanceof SpawnAd)) {
|
||||
console.error("spawn ad not found");
|
||||
@@ -261,6 +268,7 @@ export function createRenderer(
|
||||
spawnAd,
|
||||
gutterAdModal,
|
||||
alertFrame,
|
||||
fpsDisplay,
|
||||
];
|
||||
|
||||
return new GameRenderer(
|
||||
@@ -270,6 +278,7 @@ export function createRenderer(
|
||||
transformHandler,
|
||||
uiState,
|
||||
layers,
|
||||
fpsDisplay,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -283,6 +292,7 @@ export class GameRenderer {
|
||||
public transformHandler: TransformHandler,
|
||||
public uiState: UIState,
|
||||
private layers: Layer[],
|
||||
private fpsDisplay: FPSDisplay,
|
||||
) {
|
||||
const context = canvas.getContext("2d");
|
||||
if (context === null) throw new Error("2d context not supported");
|
||||
@@ -290,14 +300,7 @@ export class GameRenderer {
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.eventBus.on(RedrawGraphicsEvent, (e) => {
|
||||
this.layers.forEach((l) => {
|
||||
if (l.redraw) {
|
||||
l.redraw();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.eventBus.on(RedrawGraphicsEvent, () => this.redraw());
|
||||
this.layers.forEach((l) => l.init?.());
|
||||
|
||||
document.body.appendChild(this.canvas);
|
||||
@@ -307,7 +310,14 @@ export class GameRenderer {
|
||||
//show whole map on startup
|
||||
this.transformHandler.centerAll(0.9);
|
||||
|
||||
requestAnimationFrame(() => this.renderGame());
|
||||
let rafId = requestAnimationFrame(() => this.renderGame());
|
||||
this.canvas.addEventListener("contextlost", () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
});
|
||||
this.canvas.addEventListener("contextrestored", () => {
|
||||
this.redraw();
|
||||
rafId = requestAnimationFrame(() => this.renderGame());
|
||||
});
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
@@ -317,6 +327,14 @@ export class GameRenderer {
|
||||
//this.redraw()
|
||||
}
|
||||
|
||||
redraw() {
|
||||
this.layers.forEach((l) => {
|
||||
if (l.redraw) {
|
||||
l.redraw();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderGame() {
|
||||
const start = performance.now();
|
||||
// Set background
|
||||
@@ -356,8 +374,10 @@ export class GameRenderer {
|
||||
this.transformHandler.resetChanged();
|
||||
|
||||
requestAnimationFrame(() => this.renderGame());
|
||||
|
||||
const duration = performance.now() - start;
|
||||
|
||||
this.fpsDisplay.updateFPS(duration);
|
||||
|
||||
if (duration > 50) {
|
||||
console.warn(
|
||||
`tick ${this.game.ticks()} took ${duration}ms to render frame`,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
|
||||
import quickChatData from "../../../../resources/QuickChat.json";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { CloseViewEvent } from "../../InputHandler";
|
||||
import { SendQuickChatEvent } from "../../Transport";
|
||||
import { translateText } from "../../Utils";
|
||||
|
||||
@@ -172,6 +173,15 @@ export class ChatModal extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
initEventBus(eventBus: EventBus) {
|
||||
this.eventBus = eventBus;
|
||||
eventBus.on(CloseViewEvent, (e) => {
|
||||
if (!this.hidden) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private selectCategory(categoryId: string) {
|
||||
this.selectedCategory = categoryId;
|
||||
this.selectedPhraseText = null;
|
||||
|
||||
@@ -6,7 +6,6 @@ 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 { renderNumber, renderTroops } from "../../Utils";
|
||||
import { UIState } from "../UIState";
|
||||
import { Layer } from "./Layer";
|
||||
@@ -22,54 +21,29 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
private attackRatio: number = 0.2;
|
||||
|
||||
@state()
|
||||
private targetTroopRatio = 0.95;
|
||||
private _maxTroops: number;
|
||||
|
||||
@state()
|
||||
private currentTroopRatio = 0.95;
|
||||
|
||||
@state()
|
||||
private _population: number;
|
||||
|
||||
@state()
|
||||
private _maxPopulation: number;
|
||||
|
||||
@state()
|
||||
private popRate: number;
|
||||
private troopRate: 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 _troopRateIsIncreasing: boolean = true;
|
||||
|
||||
private _popRateIsIncreasing: boolean = true;
|
||||
|
||||
private _lastPopulationIncreaseRate: number;
|
||||
|
||||
private init_: boolean = false;
|
||||
private _lastTroopIncreaseRate: number;
|
||||
|
||||
init() {
|
||||
this.attackRatio = Number(
|
||||
localStorage.getItem("settings.attackRatio") ?? "0.2",
|
||||
);
|
||||
this.targetTroopRatio = Number(
|
||||
localStorage.getItem("settings.troopRatio") ?? "0.95",
|
||||
);
|
||||
this.init_ = true;
|
||||
this.uiState.attackRatio = this.attackRatio;
|
||||
this.currentTroopRatio = this.targetTroopRatio;
|
||||
this.eventBus.on(AttackRatioEvent, (event) => {
|
||||
let newAttackRatio =
|
||||
(parseInt(
|
||||
@@ -97,13 +71,6 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.init_) {
|
||||
this.eventBus.emit(
|
||||
new SendSetTargetTroopRatioEvent(this.targetTroopRatio),
|
||||
);
|
||||
this.init_ = false;
|
||||
}
|
||||
|
||||
if (!this._isVisible && !this.game.inSpawnPhase()) {
|
||||
this.setVisibile(true);
|
||||
}
|
||||
@@ -115,28 +82,24 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this.updatePopulationIncrease();
|
||||
this.updateTroopIncrease();
|
||||
}
|
||||
|
||||
this._population = player.population();
|
||||
this._maxPopulation = this.game.config().maxPopulation(player);
|
||||
this._troops = player.troops();
|
||||
this._maxTroops = this.game.config().maxTroops(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.troopRate = this.game.config().troopIncreaseRate(player) * 10;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private updatePopulationIncrease() {
|
||||
private updateTroopIncrease() {
|
||||
const player = this.game?.myPlayer();
|
||||
if (player === null) return;
|
||||
const popIncreaseRate = this.game.config().populationIncreaseRate(player);
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
const troopIncreaseRate = this.game.config().troopIncreaseRate(player);
|
||||
this._troopRateIsIncreasing =
|
||||
troopIncreaseRate >= this._lastTroopIncreaseRate;
|
||||
this._lastTroopIncreaseRate = troopIncreaseRate;
|
||||
}
|
||||
|
||||
onAttackRatioChange(newRatio: number) {
|
||||
@@ -156,19 +119,6 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
targetTroops(): number {
|
||||
return this._manpower * this.targetTroopRatio;
|
||||
}
|
||||
|
||||
onTroopChange(newRatio: number) {
|
||||
this.eventBus.emit(new SendSetTargetTroopRatioEvent(newRatio));
|
||||
}
|
||||
|
||||
delta(): number {
|
||||
const d = this._population - this.targetTroops();
|
||||
return d;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<style>
|
||||
@@ -219,17 +169,16 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
<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
|
||||
>${translateText("control_panel.troops")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderTroops(this._population)} /
|
||||
${renderTroops(this._maxPopulation)}
|
||||
>${renderTroops(this._troops)} / ${renderTroops(this._maxTroops)}
|
||||
<span
|
||||
class="${this._popRateIsIncreasing
|
||||
class="${this._troopRateIsIncreasing
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"}"
|
||||
translate="no"
|
||||
>(+${renderTroops(this.popRate)})</span
|
||||
>(+${renderTroops(this.troopRate)})</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
@@ -237,43 +186,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
<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
|
||||
class="absolute left-0 right-0 top-3 h-2 bg-white/20 rounded"
|
||||
></div>
|
||||
<!-- Fill track -->
|
||||
<div
|
||||
class="absolute left-0 top-3 h-2 bg-blue-500/60 rounded transition-all duration-300"
|
||||
style="width: ${this.currentTroopRatio * 100}%"
|
||||
></div>
|
||||
<!-- Range input - exactly overlaying the visual elements -->
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${(this.targetTroopRatio * 100).toString()}
|
||||
@input=${(e: Event) => {
|
||||
this.targetTroopRatio =
|
||||
parseInt((e.target as HTMLInputElement).value) / 100;
|
||||
this.onTroopChange(this.targetTroopRatio);
|
||||
}}
|
||||
class="absolute left-0 right-0 top-2 m-0 h-4 cursor-pointer targetTroopRatio"
|
||||
/>
|
||||
<span translate="no">${renderNumber(this._gold)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,19 +5,18 @@ import { AllPlayers } from "../../../core/game/Game";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { TerraNulliusImpl } from "../../../core/game/TerraNulliusImpl";
|
||||
import { emojiTable, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { ShowEmojiMenuEvent } from "../../InputHandler";
|
||||
import { CloseViewEvent, ShowEmojiMenuEvent } from "../../InputHandler";
|
||||
import { SendEmojiIntentEvent } from "../../Transport";
|
||||
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;
|
||||
|
||||
initEventBus() {
|
||||
this.eventBus.on(ShowEmojiMenuEvent, (e) => {
|
||||
initEventBus(eventBus: EventBus) {
|
||||
eventBus.on(ShowEmojiMenuEvent, (e) => {
|
||||
this.isVisible = true;
|
||||
const cell = this.transformHandler.screenToWorldCoordinates(e.x, e.y);
|
||||
if (!this.game.isValidCoord(cell.x, cell.y)) {
|
||||
@@ -40,7 +39,7 @@ export class EmojiTable extends LitElement {
|
||||
targetPlayer === this.game.myPlayer()
|
||||
? AllPlayers
|
||||
: (targetPlayer as PlayerView);
|
||||
this.eventBus.emit(
|
||||
eventBus.emit(
|
||||
new SendEmojiIntentEvent(
|
||||
recipient,
|
||||
flattenedEmojiTable.indexOf(emoji),
|
||||
@@ -49,6 +48,11 @@ export class EmojiTable extends LitElement {
|
||||
this.hideTable();
|
||||
});
|
||||
});
|
||||
eventBus.on(CloseViewEvent, (e) => {
|
||||
if (!this.hidden) {
|
||||
this.hideTable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onEmojiClicked: (emoji: string) => void = () => {};
|
||||
|
||||
@@ -475,7 +475,6 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
const recipient = this.game.playerBySmallID(
|
||||
update.request.recipientID,
|
||||
) as PlayerView;
|
||||
|
||||
this.addEvent({
|
||||
description: translateText("events_display.alliance_request_status", {
|
||||
name: recipient.name(),
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { TogglePerformanceOverlayEvent } from "../../InputHandler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("fps-display")
|
||||
export class FPSDisplay extends LitElement implements Layer {
|
||||
@property({ type: Object })
|
||||
public eventBus!: EventBus;
|
||||
|
||||
@property({ type: Object })
|
||||
public userSettings!: UserSettings;
|
||||
|
||||
@state()
|
||||
private currentFPS: number = 0;
|
||||
|
||||
@state()
|
||||
private averageFPS: number = 0;
|
||||
|
||||
@state()
|
||||
private frameTime: number = 0;
|
||||
|
||||
@state()
|
||||
private isVisible: boolean = false;
|
||||
|
||||
@state()
|
||||
private isDragging: boolean = false;
|
||||
|
||||
@state()
|
||||
private position: { x: number; y: number } = { x: 50, y: 20 }; // Percentage values
|
||||
|
||||
private frameCount: number = 0;
|
||||
private lastTime: number = 0;
|
||||
private frameTimes: number[] = [];
|
||||
private fpsHistory: number[] = [];
|
||||
private lastSecondTime: number = 0;
|
||||
private framesThisSecond: number = 0;
|
||||
private dragStart: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
static styles = css`
|
||||
.fps-display {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
z-index: 9999;
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.fps-display.dragging {
|
||||
cursor: grabbing;
|
||||
transition: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.fps-line {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.fps-good {
|
||||
color: #4ade80; /* green-400 */
|
||||
}
|
||||
|
||||
.fps-warning {
|
||||
color: #fbbf24; /* amber-400 */
|
||||
}
|
||||
|
||||
.fps-bad {
|
||||
color: #f87171; /* red-400 */
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(TogglePerformanceOverlayEvent, () => {
|
||||
this.userSettings.togglePerformanceOverlay();
|
||||
});
|
||||
}
|
||||
|
||||
setVisible(visible: boolean) {
|
||||
this.isVisible = visible;
|
||||
}
|
||||
|
||||
private handleClose() {
|
||||
this.userSettings.togglePerformanceOverlay();
|
||||
}
|
||||
|
||||
private handleMouseDown = (e: MouseEvent) => {
|
||||
// Don't start dragging if clicking on close button
|
||||
if ((e.target as HTMLElement).classList.contains("close-button")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDragging = true;
|
||||
this.dragStart = {
|
||||
x: e.clientX - this.position.x,
|
||||
y: e.clientY - this.position.y,
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", this.handleMouseMove);
|
||||
document.addEventListener("mouseup", this.handleMouseUp);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
private handleMouseMove = (e: MouseEvent) => {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const newX = e.clientX - this.dragStart.x;
|
||||
const newY = e.clientY - this.dragStart.y;
|
||||
|
||||
// Convert to percentage of viewport
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
this.position = {
|
||||
x: Math.max(0, Math.min(viewportWidth - 100, newX)), // Keep within viewport bounds
|
||||
y: Math.max(0, Math.min(viewportHeight - 100, newY)),
|
||||
};
|
||||
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private handleMouseUp = () => {
|
||||
this.isDragging = false;
|
||||
document.removeEventListener("mousemove", this.handleMouseMove);
|
||||
document.removeEventListener("mouseup", this.handleMouseUp);
|
||||
};
|
||||
|
||||
updateFPS(frameDuration: number) {
|
||||
this.isVisible = this.userSettings.performanceOverlay();
|
||||
|
||||
if (!this.isVisible) return;
|
||||
|
||||
const now = performance.now();
|
||||
|
||||
// Initialize timing on first call
|
||||
if (this.lastTime === 0) {
|
||||
this.lastTime = now;
|
||||
this.lastSecondTime = now;
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaTime = now - this.lastTime;
|
||||
|
||||
// Track frame times for current FPS calculation (last 60 frames)
|
||||
this.frameTimes.push(deltaTime);
|
||||
if (this.frameTimes.length > 60) {
|
||||
this.frameTimes.shift();
|
||||
}
|
||||
|
||||
// Calculate current FPS based on average frame time
|
||||
if (this.frameTimes.length > 0) {
|
||||
const avgFrameTime =
|
||||
this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;
|
||||
this.currentFPS = Math.round(1000 / avgFrameTime);
|
||||
this.frameTime = Math.round(avgFrameTime);
|
||||
}
|
||||
|
||||
// Track FPS for 60-second average
|
||||
this.framesThisSecond++;
|
||||
|
||||
// Update every second
|
||||
if (now - this.lastSecondTime >= 1000) {
|
||||
this.fpsHistory.push(this.framesThisSecond);
|
||||
if (this.fpsHistory.length > 60) {
|
||||
this.fpsHistory.shift();
|
||||
}
|
||||
|
||||
// Calculate 60-second average
|
||||
if (this.fpsHistory.length > 0) {
|
||||
this.averageFPS = Math.round(
|
||||
this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length,
|
||||
);
|
||||
}
|
||||
|
||||
this.framesThisSecond = 0;
|
||||
this.lastSecondTime = now;
|
||||
}
|
||||
|
||||
this.lastTime = now;
|
||||
this.frameCount++;
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
private getFPSColor(fps: number): string {
|
||||
if (fps >= 55) return "fps-good";
|
||||
if (fps >= 30) return "fps-warning";
|
||||
return "fps-bad";
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const style = `
|
||||
left: ${this.position.x}px;
|
||||
top: ${this.position.y}px;
|
||||
transform: none;
|
||||
`;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fps-display ${this.isDragging ? "dragging" : ""}"
|
||||
style="${style}"
|
||||
@mousedown="${this.handleMouseDown}"
|
||||
>
|
||||
<button class="close-button" @click="${this.handleClose}">×</button>
|
||||
<div class="fps-line">
|
||||
FPS:
|
||||
<span class="${this.getFPSColor(this.currentFPS)}"
|
||||
>${this.currentFPS}</span
|
||||
>
|
||||
</div>
|
||||
<div class="fps-line">
|
||||
Avg (60s):
|
||||
<span class="${this.getFPSColor(this.averageFPS)}"
|
||||
>${this.averageFPS}</span
|
||||
>
|
||||
</div>
|
||||
<div class="fps-line">
|
||||
Frame:
|
||||
<span class="${this.getFPSColor(1000 / this.frameTime)}"
|
||||
>${this.frameTime}ms</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -269,9 +269,6 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
|
||||
function formatPercentage(value: number): string {
|
||||
const perc = value * 100;
|
||||
if (perc > 99.5) return "100%";
|
||||
if (perc < 0.01) return "0%";
|
||||
if (perc < 0.1) return perc.toPrecision(1) + "%";
|
||||
if (Number.isNaN(perc)) return "0%";
|
||||
return perc.toPrecision(2) + "%";
|
||||
return perc.toFixed(1) + "%";
|
||||
}
|
||||
|
||||
@@ -137,6 +137,11 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onTogglePerformanceOverlayButtonClick() {
|
||||
this.userSettings.togglePerformanceOverlay();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log("init called from OptionsMenu");
|
||||
this.showPauseButton =
|
||||
@@ -251,6 +256,12 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
? "Opens menu"
|
||||
: "Attack"),
|
||||
})}
|
||||
${button({
|
||||
onClick: this.onTogglePerformanceOverlayButtonClick,
|
||||
title: "Performance Overlay",
|
||||
children:
|
||||
"🚀: " + (this.userSettings.performanceOverlay() ? "On" : "Off"),
|
||||
})}
|
||||
<!-- ${button({
|
||||
onClick: this.onToggleFocusLockedButtonClick,
|
||||
title: "Lock Focus",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AllPlayers, PlayerActions } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { flattenedEmojiTable } from "../../../core/Util";
|
||||
import { MouseUpEvent } from "../../InputHandler";
|
||||
import { CloseViewEvent, MouseUpEvent } from "../../InputHandler";
|
||||
import {
|
||||
SendAllianceRequestIntentEvent,
|
||||
SendBreakAllianceIntentEvent,
|
||||
@@ -164,8 +164,20 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
|
||||
private ctModal: ChatModal;
|
||||
|
||||
initEventBus(eventBus: EventBus) {
|
||||
this.eventBus = eventBus;
|
||||
eventBus.on(CloseViewEvent, (e) => {
|
||||
if (!this.hidden) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(MouseUpEvent, () => this.hide());
|
||||
this.eventBus.on(CloseViewEvent, (e) => {
|
||||
this.hide();
|
||||
});
|
||||
|
||||
this.ctModal = document.querySelector("chat-modal") as ChatModal;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as d3 from "d3";
|
||||
import backIcon from "../../../../resources/images/BackIconWhite.svg";
|
||||
import { EventBus, GameEvent } from "../../../core/EventBus";
|
||||
import { CloseViewEvent } from "../../InputHandler";
|
||||
import { Layer } from "./Layer";
|
||||
import {
|
||||
CenterButtonElement,
|
||||
@@ -102,6 +103,9 @@ export class RadialMenu implements Layer {
|
||||
init() {
|
||||
this.createMenuElement();
|
||||
this.createTooltipElement();
|
||||
this.eventBus.on(CloseViewEvent, (e) => {
|
||||
this.hideRadialMenu();
|
||||
});
|
||||
}
|
||||
|
||||
private createMenuElement() {
|
||||
|
||||
@@ -114,6 +114,11 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onTogglePerformanceOverlayButtonClick() {
|
||||
this.userSettings.togglePerformanceOverlay();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onExitButtonClick() {
|
||||
// redirect to the home page
|
||||
window.location.href = "/";
|
||||
@@ -298,6 +303,35 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
@click="${this.onTogglePerformanceOverlayButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${settingsIcon}
|
||||
alt="performanceIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
${translateText("user_setting.performance_overlay_label")}
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${this.userSettings.performanceOverlay()
|
||||
? translateText("user_setting.performance_overlay_enabled")
|
||||
: translateText(
|
||||
"user_setting.performance_overlay_disabled",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${this.userSettings.performanceOverlay()
|
||||
? translateText("user_setting.on")
|
||||
: translateText("user_setting.off")}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="border-t border-slate-600 pt-3 mt-4">
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-red-600/20 rounded text-red-400 transition-colors"
|
||||
|
||||
@@ -135,7 +135,14 @@ export class StructureLayer implements Layer {
|
||||
|
||||
this.canvas.width = this.game.width() * 2;
|
||||
this.canvas.height = this.game.height() * 2;
|
||||
this.game.units().forEach((u) => this.handleUnitRendering(u));
|
||||
|
||||
Promise.all(
|
||||
Array.from(this.unitIcons.values()).map((img) =>
|
||||
img.decode?.().catch(() => {}),
|
||||
),
|
||||
).finally(() => {
|
||||
this.game.units().forEach((u) => this.handleUnitRendering(u));
|
||||
});
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
|
||||
@@ -167,8 +167,6 @@ export class TeamStats extends LitElement implements Layer {
|
||||
|
||||
function formatPercentage(value: number): string {
|
||||
const perc = value * 100;
|
||||
if (perc > 99.5) return "100%";
|
||||
if (perc < 0.01) return "0%";
|
||||
if (perc < 0.1) return perc.toPrecision(1) + "%";
|
||||
if (Number.isNaN(perc)) return "0%";
|
||||
return perc.toPrecision(2) + "%";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import logo from "../../../../resources/images/ofm/logo_MASTER_2025.png";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
@@ -22,9 +21,6 @@ export class WinModal extends LitElement implements Layer {
|
||||
@state()
|
||||
showButtons = false;
|
||||
|
||||
@state()
|
||||
private showSteamContent = Math.random() > 0.5;
|
||||
|
||||
private _title: string;
|
||||
|
||||
// Override to prevent shadow DOM creation
|
||||
@@ -142,9 +138,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
return html`
|
||||
<div class="win-modal ${this.isVisible ? "visible" : ""}">
|
||||
<h2>${this._title || ""}</h2>
|
||||
${this.showSteamContent
|
||||
? this.steamWishlist()
|
||||
: this.openfrontMasters()}
|
||||
${this.innerHtml()}
|
||||
<div
|
||||
class="button-container ${this.showButtons ? "visible" : "hidden"}"
|
||||
>
|
||||
@@ -159,7 +153,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
`;
|
||||
}
|
||||
|
||||
steamWishlist() {
|
||||
innerHtml() {
|
||||
return html`<p>
|
||||
<a
|
||||
href="https://store.steampowered.com/app/3560670"
|
||||
@@ -180,33 +174,6 @@ export class WinModal extends LitElement implements Layer {
|
||||
</p>`;
|
||||
}
|
||||
|
||||
openfrontMasters() {
|
||||
return html`<p>
|
||||
<img
|
||||
src="${logo}"
|
||||
alt="OpenFront Masters"
|
||||
style="max-width: 100%; height: auto; margin-bottom: 16px;"
|
||||
/>
|
||||
<a
|
||||
href="https://discord.gg/gStsGh5vWR"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="
|
||||
color: #4a9eff;
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
font-size: 24px;
|
||||
"
|
||||
onmouseover="this.style.color='#6db3ff'"
|
||||
onmouseout="this.style.color='#4a9eff'"
|
||||
>
|
||||
Watch the best compete in the
|
||||
<span style="font-weight: bold;">OpenFront Masters</span>
|
||||
</a>
|
||||
</p>`;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.eventBus.emit(new GutterAdModalEvent(true));
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
.dark .bg-image {
|
||||
filter: blur(4px) brightness(0.7);
|
||||
filter: blur(4px) brightness(0.4) saturate(0.3) contrast(1.2);
|
||||
}
|
||||
|
||||
/* display:none if child has class parent-hidden since we can't use shadow DOM in Lit due to Tailwind */
|
||||
@@ -386,6 +386,7 @@
|
||||
<news-modal></news-modal>
|
||||
<game-left-sidebar></game-left-sidebar>
|
||||
<spawn-ad></spawn-ad>
|
||||
<fps-display></fps-display>
|
||||
<div
|
||||
id="language-modal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex justify-center items-center"
|
||||
@@ -412,6 +413,10 @@
|
||||
document.documentElement.classList.remove("preload");
|
||||
});
|
||||
});
|
||||
window.addEventListener("beforeunload", function (e) {
|
||||
e.preventDefault();
|
||||
e.returnValue = "Are you sure you want to leave?";
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Playwire ads -->
|
||||
|
||||
+31
-18
@@ -251,15 +251,15 @@ label.option-card:hover {
|
||||
}
|
||||
|
||||
.player-tag {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 4px 16px;
|
||||
gap: 8px;
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
padding: 8px 12px;
|
||||
margin: 4px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
#bots-count,
|
||||
@@ -625,18 +625,6 @@ label.option-card:hover {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.player-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 4px 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* News Button Notification */
|
||||
news-button .active button {
|
||||
position: relative;
|
||||
@@ -670,3 +658,28 @@ news-button .active button::after {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
|
||||
.host-badge {
|
||||
font-size: 11px;
|
||||
color: #4caf50;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.remove-player-btn {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.remove-player-btn:hover {
|
||||
background: #ff6666;
|
||||
}
|
||||
|
||||
@@ -55,3 +55,30 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .c-button {
|
||||
background: var(--primaryColorDark);
|
||||
color: var(--fontColorLight);
|
||||
}
|
||||
|
||||
.dark .c-button:hover,
|
||||
.dark .c-button:active,
|
||||
.dark .c-button:focus {
|
||||
background: var(--primaryColorHoverDark);
|
||||
}
|
||||
|
||||
.dark .c-button:disabled {
|
||||
background: var(--primaryColorDisabledDark);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.dark .c-button--secondary {
|
||||
background: var(--secondaryColorDark);
|
||||
color: var(--fontColorDark);
|
||||
}
|
||||
|
||||
.dark .c-button--secondary:hover,
|
||||
.dark .c-button--secondary:active,
|
||||
.dark .c-button--secondary:focus {
|
||||
background: var(--secondaryColorHoverDark);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,11 @@
|
||||
--secondaryColor: #dbeafe;
|
||||
--secondaryColorHover: #bfdbfe;
|
||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
--primaryColorDark: #3b82f6;
|
||||
--primaryColorHoverDark: #2563eb;
|
||||
--primaryColorDisabledDark: #4b5563;
|
||||
--secondaryColorDark: #374151;
|
||||
--secondaryColorHoverDark: #4b5563;
|
||||
--fontColorDark: #f3f4f6;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user