mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-18 09:14:09 +00:00
Homepage update & add 3 public lobbies (#3191)
## Description: Update UI check https://homepageupdate.openfront.dev/ Improved mobile UI (now fills whole screen for all modals) e.g.: <img width="432" height="852" alt="image" src="https://github.com/user-attachments/assets/56de40af-4137-4c57-96b7-3910c9a665b8" /> Converted PublicLobby to be "GameModeSelector" to get a nicer 4x4 grid div, where <GameModeSelector> now handles all the username validation now (removed redundant code from modals such as matchmaking) also fixed a bug where someone could have "[XX] X" as thier username (when the minimum should be 3 chars for their name) Now visually displays the 3 lobbies ffa/team/special (which is a continuation from the work done in: #3196 ) <img width="818" height="563" alt="image" src="https://github.com/user-attachments/assets/a15cd31b-6061-4fb8-83ee-ffde6225cfa7" /> updated the background: <img width="1919" height="807" alt="image" src="https://github.com/user-attachments/assets/358a7434-51b8-4540-baf2-d1be05053c44" /> slightly updated the glassy-look to be less glassy: <img width="825" height="729" alt="image" src="https://github.com/user-attachments/assets/1801871b-bbf8-43db-ac53-489337ae80a5" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n
This commit is contained in:
+34
-9
@@ -122,12 +122,29 @@
|
|||||||
<body
|
<body
|
||||||
class="h-full select-none font-sans min-h-screen bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-row overflow-hidden"
|
class="h-full select-none font-sans min-h-screen bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-row overflow-hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div id="hex-grid" class="fixed inset-0 -z-50 pointer-events-none">
|
||||||
class="fixed inset-0 w-full h-full -z-50 bg-cover bg-center bg-fixed pointer-events-none brightness-[0.5]"
|
<div
|
||||||
style="
|
id="background-layer"
|
||||||
background-image: url("/images/EuropeBackgroundBlurred.webp");
|
class="absolute inset-0 bg-cover bg-center opacity-50 [filter:brightness(1.0)] dark:[filter:sepia(0.2)_saturate(1.2)_hue-rotate(180deg)_brightness(0.9)]"
|
||||||
"
|
style="
|
||||||
></div>
|
background-image: url("/resources/images/background.png");
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-center bg-no-repeat bg-contain hidden lg:block"
|
||||||
|
style="
|
||||||
|
background-image: url("/resources/images/OpenFront.png");
|
||||||
|
opacity: 0.25;
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-center bg-no-repeat bg-contain lg:hidden"
|
||||||
|
style="
|
||||||
|
background-image: url("/resources/images/OF.png");
|
||||||
|
opacity: 0.25;
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- LEFT SIDEBAR MENU -->
|
<!-- LEFT SIDEBAR MENU -->
|
||||||
|
|
||||||
@@ -140,10 +157,9 @@
|
|||||||
|
|
||||||
<mobile-nav-bar
|
<mobile-nav-bar
|
||||||
id="sidebar-menu"
|
id="sidebar-menu"
|
||||||
class="peer in-[.in-game]:hidden z-40001 fixed left-0 top-0 h-full flex flex-col justify-start overflow-visible bg-black/60 backdrop-blur-md transition-transform duration-500 ease-out transform -translate-x-full w-[80%] [&.open]:translate-x-0 lg:hidden"
|
class="peer [.in-game_&]:hidden z-[40001] fixed left-0 top-0 h-full flex flex-col justify-start overflow-visible bg-black/70 backdrop-blur-xl border-r border-white/10 transition-transform duration-500 ease-out transform -translate-x-full w-[70%] [&.open]:translate-x-0 lg:hidden"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
data-i18n-aria-label="main.menu"
|
data-i18n-aria-label="main.menu"
|
||||||
aria-hidden="true"
|
|
||||||
></mobile-nav-bar>
|
></mobile-nav-bar>
|
||||||
|
|
||||||
<!-- MAIN CONTENT AREA -->
|
<!-- MAIN CONTENT AREA -->
|
||||||
@@ -163,7 +179,11 @@
|
|||||||
<!-- Main container with responsive padding -->
|
<!-- Main container with responsive padding -->
|
||||||
<main-layout class="contents">
|
<main-layout class="contents">
|
||||||
<play-page class="contents"></play-page>
|
<play-page class="contents"></play-page>
|
||||||
|
<matchmaking-modal
|
||||||
|
id="page-matchmaking"
|
||||||
|
inline
|
||||||
|
class="hidden w-full h-full page-content"
|
||||||
|
></matchmaking-modal>
|
||||||
<news-modal
|
<news-modal
|
||||||
id="page-news"
|
id="page-news"
|
||||||
inline
|
inline
|
||||||
@@ -225,6 +245,11 @@
|
|||||||
inline
|
inline
|
||||||
class="hidden w-full h-full page-content"
|
class="hidden w-full h-full page-content"
|
||||||
></flag-input-modal>
|
></flag-input-modal>
|
||||||
|
<ranked-modal
|
||||||
|
id="page-ranked"
|
||||||
|
inline
|
||||||
|
class="hidden w-full h-full page-content"
|
||||||
|
></ranked-modal>
|
||||||
</main-layout>
|
</main-layout>
|
||||||
|
|
||||||
<!-- Desktop Footer -->
|
<!-- Desktop Footer -->
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
+12
-7
@@ -356,7 +356,6 @@
|
|||||||
},
|
},
|
||||||
"public_lobby": {
|
"public_lobby": {
|
||||||
"title": "Waiting for Game Start...",
|
"title": "Waiting for Game Start...",
|
||||||
"join": "Join next Game",
|
|
||||||
"teams_Duos": "{team_count} teams of 2 (Duos)",
|
"teams_Duos": "{team_count} teams of 2 (Duos)",
|
||||||
"teams_Trios": "{team_count} teams of 3 (Trios)",
|
"teams_Trios": "{team_count} teams of 3 (Trios)",
|
||||||
"teams_Quads": "{team_count} teams of 4 (Quads)",
|
"teams_Quads": "{team_count} teams of 4 (Quads)",
|
||||||
@@ -377,7 +376,8 @@
|
|||||||
"connecting": "Connecting to matchmaking server...",
|
"connecting": "Connecting to matchmaking server...",
|
||||||
"searching": "Searching for game...",
|
"searching": "Searching for game...",
|
||||||
"waiting_for_game": "Waiting for game to start...",
|
"waiting_for_game": "Waiting for game to start...",
|
||||||
"elo": "Your ELO: {elo}"
|
"elo": "Your ELO: {elo}",
|
||||||
|
"no_elo": "No ELO yet"
|
||||||
},
|
},
|
||||||
"username": {
|
"username": {
|
||||||
"enter_username": "Enter your username",
|
"enter_username": "Enter your username",
|
||||||
@@ -391,7 +391,6 @@
|
|||||||
},
|
},
|
||||||
"host_modal": {
|
"host_modal": {
|
||||||
"title": "Create Private Lobby",
|
"title": "Create Private Lobby",
|
||||||
"label": "Private",
|
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
"team_count": "Number of Teams",
|
"team_count": "Number of Teams",
|
||||||
"team_type": "Team Type",
|
"team_type": "Team Type",
|
||||||
@@ -454,6 +453,16 @@
|
|||||||
"ffa": "Free for All",
|
"ffa": "Free for All",
|
||||||
"teams": "Teams"
|
"teams": "Teams"
|
||||||
},
|
},
|
||||||
|
"mode_selector": {
|
||||||
|
"special_title": "Special Mix",
|
||||||
|
"teams_title": "Teams",
|
||||||
|
"teams_count": "{teamCount} teams",
|
||||||
|
"teams_of": "{teamCount} teams of {playersPerTeam}",
|
||||||
|
"ranked_title": "Ranked",
|
||||||
|
"ranked_1v1_title": "1v1",
|
||||||
|
"ranked_2v2_title": "2v2",
|
||||||
|
"coming_soon": "Coming Soon"
|
||||||
|
},
|
||||||
"public_game_modifier": {
|
"public_game_modifier": {
|
||||||
"random_spawn": "Random Spawn",
|
"random_spawn": "Random Spawn",
|
||||||
"compact_map": "Compact Map",
|
"compact_map": "Compact Map",
|
||||||
@@ -931,8 +940,6 @@
|
|||||||
"recent_games": "Recent Games",
|
"recent_games": "Recent Games",
|
||||||
"game_id": "Game ID",
|
"game_id": "Game ID",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
"mode_ffa": "Free-for-All",
|
|
||||||
"mode_team": "Team",
|
|
||||||
"replay": "Replay",
|
"replay": "Replay",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"ranking": "Ranking",
|
"ranking": "Ranking",
|
||||||
@@ -949,8 +956,6 @@
|
|||||||
"stats_losses": "Losses",
|
"stats_losses": "Losses",
|
||||||
"stats_wlr": "Win:Loss Ratio",
|
"stats_wlr": "Win:Loss Ratio",
|
||||||
"stats_games_played": "Games Played",
|
"stats_games_played": "Games Played",
|
||||||
"mode_ffa": "Free-for-All",
|
|
||||||
"mode_team": "Team",
|
|
||||||
"no_stats": "No stats recorded for this selection."
|
"no_stats": "No stats recorded for this selection."
|
||||||
},
|
},
|
||||||
"matchmaking_button": {
|
"matchmaking_button": {
|
||||||
|
|||||||
@@ -61,18 +61,9 @@ export class AccountModal extends BaseModal {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content = this.isLoadingUser
|
const content = this.isLoadingUser
|
||||||
? html`
|
? this.renderLoadingSpinner(
|
||||||
<div
|
translateText("account_modal.fetching_account"),
|
||||||
class="flex flex-col items-center justify-center p-12 text-white bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 h-full min-h-[400px]"
|
)
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-12 h-12 border-4 border-blue-500/30 border-t-blue-500 rounded-full animate-spin mb-4"
|
|
||||||
></div>
|
|
||||||
<p class="text-white/60 font-medium tracking-wide animate-pulse">
|
|
||||||
${translateText("account_modal.fetching_account")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: this.renderInner();
|
: this.renderInner();
|
||||||
|
|
||||||
if (this.inline) {
|
if (this.inline) {
|
||||||
@@ -99,9 +90,7 @@ export class AccountModal extends BaseModal {
|
|||||||
const displayId = publicId || translateText("account_modal.not_found");
|
const displayId = publicId || translateText("account_modal.not_found");
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
|
|
||||||
>
|
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title,
|
title,
|
||||||
onBack: () => this.close(),
|
onBack: () => this.close(),
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export class FlagInput extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<button
|
<button
|
||||||
id="flag-input"
|
id="flag-input"
|
||||||
class="flag-btn p-0! m-0 border-0 w-full h-full flex cursor-pointer justify-center items-center focus:outline-none focus:ring-0 transition-all duration-200 hover:scale-105 bg-slate-900/80 hover:bg-slate-800/80 active:bg-slate-800/90 rounded-lg overflow-hidden"
|
class="flag-btn p-0 m-0 border-0 w-full h-full flex cursor-pointer justify-center items-center focus:outline-none focus:ring-0 transition-all duration-200 hover:scale-105 bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] hover:brightness-[1.08] active:brightness-[0.95] rounded-lg overflow-hidden"
|
||||||
title=${buttonTitle}
|
title=${buttonTitle}
|
||||||
@click=${this.onInputClick}
|
@click=${this.onInputClick}
|
||||||
>
|
>
|
||||||
@@ -94,7 +94,7 @@ export class FlagInput extends LitElement {
|
|||||||
></span>
|
></span>
|
||||||
${showSelect
|
${showSelect
|
||||||
? html`<span
|
? html`<span
|
||||||
class="text-[10px] font-black text-white/40 uppercase leading-none break-words w-full text-center px-1"
|
class="text-[10px] font-black text-white uppercase leading-none break-words w-full text-center px-1"
|
||||||
>
|
>
|
||||||
${translateText("flag_input.title")}
|
${translateText("flag_input.title")}
|
||||||
</span>`
|
</span>`
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ export class FlagInputModal extends BaseModal {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="relative flex flex-col border-b border-white/10 pb-4 shrink-0"
|
class="relative flex flex-col border-b border-white/10 pb-4 shrink-0"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export class GameInfoModal extends LitElement {
|
|||||||
try {
|
try {
|
||||||
const mapType = gameMap as GameMapType;
|
const mapType = gameMap as GameMapType;
|
||||||
const data = terrainMapFileLoader.getMapData(mapType);
|
const data = terrainMapFileLoader.getMapData(mapType);
|
||||||
this.mapImage = await data.webpPath();
|
this.mapImage = data.webpPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load map image:", error);
|
console.error("Failed to load map image:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,363 @@
|
|||||||
|
import { html, LitElement, nothing, type TemplateResult } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators.js";
|
||||||
|
import {
|
||||||
|
Duos,
|
||||||
|
GameMapType,
|
||||||
|
GameMode,
|
||||||
|
HumansVsNations,
|
||||||
|
PublicGameModifiers,
|
||||||
|
Quads,
|
||||||
|
Trios,
|
||||||
|
} from "../core/game/Game";
|
||||||
|
import { PublicGameInfo, PublicGames } from "../core/Schemas";
|
||||||
|
import { HostLobbyModal } from "./HostLobbyModal";
|
||||||
|
import { JoinLobbyModal } from "./JoinLobbyModal";
|
||||||
|
import { PublicLobbySocket } from "./LobbySocket";
|
||||||
|
import { JoinLobbyEvent } from "./Main";
|
||||||
|
import { SinglePlayerModal } from "./SinglePlayerModal";
|
||||||
|
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||||
|
import { getMapName, renderDuration, translateText } from "./Utils";
|
||||||
|
|
||||||
|
const CARD_BG = "bg-[color-mix(in_oklab,var(--frenchBlue)_70%,black)]";
|
||||||
|
|
||||||
|
@customElement("game-mode-selector")
|
||||||
|
export class GameModeSelector extends LitElement {
|
||||||
|
@state() private lobbies: PublicGames | null = null;
|
||||||
|
private serverTimeOffset: number = 0;
|
||||||
|
|
||||||
|
private lobbySocket = new PublicLobbySocket((lobbies) =>
|
||||||
|
this.handleLobbiesUpdate(lobbies),
|
||||||
|
);
|
||||||
|
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates username input and shows error message if invalid.
|
||||||
|
* Returns true if valid, false otherwise.
|
||||||
|
*/
|
||||||
|
private validateUsername(): boolean {
|
||||||
|
const usernameInput = document.querySelector("username-input") as any;
|
||||||
|
if (usernameInput?.isValid?.() === false) {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("show-message", {
|
||||||
|
detail: {
|
||||||
|
message: usernameInput.validationError,
|
||||||
|
color: "red",
|
||||||
|
duration: 3000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.lobbySocket.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.stop();
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
this.lobbySocket.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleLobbiesUpdate(lobbies: PublicGames) {
|
||||||
|
this.lobbies = lobbies;
|
||||||
|
this.serverTimeOffset = lobbies.serverTime - Date.now();
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("public-lobbies-update", {
|
||||||
|
detail: { payload: lobbies },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const ffa = this.lobbies?.games?.["ffa"]?.[0];
|
||||||
|
const teams = this.lobbies?.games?.["team"]?.[0];
|
||||||
|
const special = this.lobbies?.games?.["special"]?.[0];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 lg:grid-cols-2 gap-4 w-[70%] lg:w-full mx-auto"
|
||||||
|
>
|
||||||
|
${ffa ? this.renderLobbyCard(ffa, this.getLobbyTitle(ffa)) : nothing}
|
||||||
|
${teams
|
||||||
|
? this.renderLobbyCard(teams, this.getLobbyTitle(teams))
|
||||||
|
: nothing}
|
||||||
|
${special ? this.renderSpecialLobbyCard(special) : nothing}
|
||||||
|
${this.renderQuickActionsSection()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSpecialLobbyCard(lobby: PublicGameInfo) {
|
||||||
|
const subtitle = this.getLobbyTitle(lobby);
|
||||||
|
const mainTitle = translateText("mode_selector.special_title");
|
||||||
|
const titleContent = subtitle
|
||||||
|
? html`
|
||||||
|
<span class="block">${mainTitle}</span>
|
||||||
|
<span class="block text-[10px] leading-tight text-white/70">
|
||||||
|
${subtitle}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: mainTitle;
|
||||||
|
return this.renderLobbyCard(lobby, titleContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderQuickActionsSection() {
|
||||||
|
return html`
|
||||||
|
<div class="grid grid-cols-2 gap-2 h-40 lg:h-56">
|
||||||
|
${this.renderSmallActionCard(
|
||||||
|
translateText("main.solo"),
|
||||||
|
this.openSinglePlayerModal,
|
||||||
|
)}
|
||||||
|
${this.renderSmallActionCard(
|
||||||
|
translateText("mode_selector.ranked_title"),
|
||||||
|
this.openRankedMenu,
|
||||||
|
)}
|
||||||
|
${this.renderSmallActionCard(
|
||||||
|
translateText("main.create"),
|
||||||
|
this.openHostLobby,
|
||||||
|
)}
|
||||||
|
${this.renderSmallActionCard(
|
||||||
|
translateText("main.join"),
|
||||||
|
this.openJoinLobby,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private openRankedMenu = () => {
|
||||||
|
if (!this.validateUsername()) return;
|
||||||
|
window.showPage?.("page-ranked");
|
||||||
|
};
|
||||||
|
|
||||||
|
private openSinglePlayerModal = () => {
|
||||||
|
if (!this.validateUsername()) return;
|
||||||
|
(
|
||||||
|
document.querySelector("single-player-modal") as SinglePlayerModal
|
||||||
|
)?.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
private openHostLobby = () => {
|
||||||
|
if (!this.validateUsername()) return;
|
||||||
|
(document.querySelector("host-lobby-modal") as HostLobbyModal)?.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
private openJoinLobby = () => {
|
||||||
|
if (!this.validateUsername()) return;
|
||||||
|
(document.querySelector("join-lobby-modal") as JoinLobbyModal)?.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderSmallActionCard(title: string, onClick: () => void) {
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
@click=${onClick}
|
||||||
|
class="flex items-center justify-center w-full h-full rounded-xl ${CARD_BG} border-0 transition-transform hover:scale-[1.02] active:scale-[0.98] text-sm lg:text-base font-bold text-white uppercase tracking-wider text-center"
|
||||||
|
>
|
||||||
|
${title}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLobbyCard(
|
||||||
|
lobby: PublicGameInfo,
|
||||||
|
titleContent: string | TemplateResult,
|
||||||
|
) {
|
||||||
|
const mapType = lobby.gameConfig!.gameMap as GameMapType;
|
||||||
|
const mapImageSrc = terrainMapFileLoader.getMapData(mapType).webpPath;
|
||||||
|
const timeRemaining = Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor((lobby.startsAt - this.serverTimeOffset - Date.now()) / 1000),
|
||||||
|
);
|
||||||
|
const timeDisplay = renderDuration(timeRemaining);
|
||||||
|
const mapName = getMapName(lobby.gameConfig?.gameMap);
|
||||||
|
|
||||||
|
const modifierLabels = this.getModifierLabels(
|
||||||
|
lobby.gameConfig?.publicGameModifiers,
|
||||||
|
);
|
||||||
|
// Sort by length for visual consistency (shorter labels first)
|
||||||
|
if (modifierLabels.length > 1) {
|
||||||
|
modifierLabels.sort((a, b) => a.length - b.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
@click=${() => this.validateAndJoin(lobby)}
|
||||||
|
class="group flex flex-col w-full h-40 lg:h-56 text-white uppercase rounded-2xl overflow-hidden transition-transform duration-200 hover:scale-[1.02] active:scale-[0.98] ${CARD_BG}"
|
||||||
|
>
|
||||||
|
<div class="relative flex-1 overflow-hidden ${CARD_BG}">
|
||||||
|
${mapImageSrc
|
||||||
|
? html`<img
|
||||||
|
src="${mapImageSrc}"
|
||||||
|
alt="${mapName ?? lobby.gameConfig?.gameMap ?? "map"}"
|
||||||
|
draggable="false"
|
||||||
|
class="absolute inset-0 w-full h-full object-contain object-center scale-[1.05] pointer-events-none"
|
||||||
|
/>`
|
||||||
|
: null}
|
||||||
|
<div
|
||||||
|
class="absolute inset-x-2 bottom-2 flex items-end justify-between gap-2"
|
||||||
|
>
|
||||||
|
${modifierLabels.length > 0
|
||||||
|
? html`<div class="flex flex-col items-start gap-1">
|
||||||
|
${modifierLabels.map(
|
||||||
|
(label) =>
|
||||||
|
html`<span
|
||||||
|
class="px-2 py-0.5 rounded text-[10px] font-medium uppercase tracking-wide bg-teal-600 text-white shadow-[0_0_6px_rgba(13,148,136,0.35)]"
|
||||||
|
>${label}</span
|
||||||
|
>`,
|
||||||
|
)}
|
||||||
|
</div>`
|
||||||
|
: html`<div></div>`}
|
||||||
|
<div class="shrink-0">
|
||||||
|
${timeRemaining > 0
|
||||||
|
? html`<span
|
||||||
|
class="text-[10px] font-bold uppercase tracking-widest bg-blue-600 px-2 py-0.5 rounded"
|
||||||
|
>${timeDisplay}</span
|
||||||
|
>`
|
||||||
|
: html`<span
|
||||||
|
class="text-[10px] font-bold uppercase tracking-widest bg-green-600 px-2 py-0.5 rounded"
|
||||||
|
>${translateText("public_lobby.starting_game")}</span
|
||||||
|
>`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between px-3 py-2">
|
||||||
|
<div class="flex flex-col gap-0.5 min-w-0">
|
||||||
|
<h3
|
||||||
|
class="text-sm lg:text-base font-bold uppercase tracking-wider text-left leading-tight"
|
||||||
|
>
|
||||||
|
${titleContent}
|
||||||
|
</h3>
|
||||||
|
${mapName
|
||||||
|
? html`<p
|
||||||
|
class="text-[10px] text-white/70 uppercase tracking-wider text-left"
|
||||||
|
>
|
||||||
|
${mapName}
|
||||||
|
</p>`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="text-xs font-bold uppercase tracking-widest shrink-0 ml-2"
|
||||||
|
>
|
||||||
|
${lobby.numClients}/${lobby.gameConfig?.maxPlayers}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateAndJoin(lobby: PublicGameInfo) {
|
||||||
|
if (!this.validateUsername()) return;
|
||||||
|
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("join-lobby", {
|
||||||
|
detail: {
|
||||||
|
gameID: lobby.gameID,
|
||||||
|
source: "public",
|
||||||
|
publicLobbyInfo: lobby,
|
||||||
|
} as JoinLobbyEvent,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getModifierLabels(mods: PublicGameModifiers | undefined): string[] {
|
||||||
|
if (!mods) return [];
|
||||||
|
return [
|
||||||
|
mods.isRandomSpawn && translateText("public_game_modifier.random_spawn"),
|
||||||
|
mods.isCompact && translateText("public_game_modifier.compact_map"),
|
||||||
|
mods.isCrowded && translateText("public_game_modifier.crowded"),
|
||||||
|
mods.startingGold && translateText("public_game_modifier.starting_gold"),
|
||||||
|
].filter((x): x is string => !!x);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLobbyTitle(lobby: PublicGameInfo): string {
|
||||||
|
const config = lobby.gameConfig!;
|
||||||
|
if (config.gameMode === GameMode.FFA) {
|
||||||
|
return translateText("game_mode.ffa");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config?.gameMode === GameMode.Team) {
|
||||||
|
const totalPlayers = config.maxPlayers ?? lobby.numClients ?? undefined;
|
||||||
|
const formatTeamsOf = (
|
||||||
|
teamCount: number | undefined,
|
||||||
|
playersPerTeam: number | undefined,
|
||||||
|
label?: string,
|
||||||
|
) => {
|
||||||
|
if (!teamCount)
|
||||||
|
return label ?? translateText("mode_selector.teams_title");
|
||||||
|
const baseTitle = playersPerTeam
|
||||||
|
? translateText("mode_selector.teams_of", {
|
||||||
|
teamCount: String(teamCount),
|
||||||
|
playersPerTeam: String(playersPerTeam),
|
||||||
|
})
|
||||||
|
: translateText("mode_selector.teams_count", {
|
||||||
|
teamCount: String(teamCount),
|
||||||
|
});
|
||||||
|
return `${baseTitle}${label ? ` (${label})` : ""}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (config.playerTeams) {
|
||||||
|
case Duos: {
|
||||||
|
const teamCount = totalPlayers
|
||||||
|
? Math.floor(totalPlayers / 2)
|
||||||
|
: undefined;
|
||||||
|
return teamCount
|
||||||
|
? translateText("public_lobby.teams_Duos", {
|
||||||
|
team_count: String(teamCount),
|
||||||
|
})
|
||||||
|
: formatTeamsOf(undefined, 2);
|
||||||
|
}
|
||||||
|
case Trios: {
|
||||||
|
const teamCount = totalPlayers
|
||||||
|
? Math.floor(totalPlayers / 3)
|
||||||
|
: undefined;
|
||||||
|
return teamCount
|
||||||
|
? translateText("public_lobby.teams_Trios", {
|
||||||
|
team_count: String(teamCount),
|
||||||
|
})
|
||||||
|
: formatTeamsOf(undefined, 3);
|
||||||
|
}
|
||||||
|
case Quads: {
|
||||||
|
const teamCount = totalPlayers
|
||||||
|
? Math.floor(totalPlayers / 4)
|
||||||
|
: undefined;
|
||||||
|
return teamCount
|
||||||
|
? translateText("public_lobby.teams_Quads", {
|
||||||
|
team_count: String(teamCount),
|
||||||
|
})
|
||||||
|
: formatTeamsOf(undefined, 4);
|
||||||
|
}
|
||||||
|
case HumansVsNations: {
|
||||||
|
const humanSlots = config.maxPlayers ?? lobby.numClients;
|
||||||
|
return humanSlots
|
||||||
|
? translateText("public_lobby.teams_hvn_detailed", {
|
||||||
|
num: String(humanSlots),
|
||||||
|
})
|
||||||
|
: translateText("public_lobby.teams_hvn");
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if (typeof config.playerTeams === "number") {
|
||||||
|
const teamCount = config.playerTeams;
|
||||||
|
const playersPerTeam =
|
||||||
|
totalPlayers && teamCount > 0
|
||||||
|
? Math.floor(totalPlayers / teamCount)
|
||||||
|
: undefined;
|
||||||
|
return formatTeamsOf(teamCount, playersPerTeam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,14 +99,10 @@ export class HelpModal extends BaseModal {
|
|||||||
const keybinds = this.keybinds;
|
const keybinds = this.keybinds;
|
||||||
|
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col ${this.inline
|
|
||||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
|
||||||
: ""}"
|
|
||||||
>
|
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title: translateText("main.help"),
|
title: translateText("main.help"),
|
||||||
onBack: this.close,
|
onBack: () => this.close(),
|
||||||
ariaLabel: translateText("common.back"),
|
ariaLabel: translateText("common.back"),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
@state() private nationCount: number = 0;
|
@state() private nationCount: number = 0;
|
||||||
|
|
||||||
@property({ attribute: false }) eventBus: EventBus | null = null;
|
@property({ attribute: false }) eventBus: EventBus | null = null;
|
||||||
|
|
||||||
private playersInterval: NodeJS.Timeout | null = null;
|
|
||||||
// Add a new timer for debouncing bot changes
|
// Add a new timer for debouncing bot changes
|
||||||
private botsUpdateTimer: number | null = null;
|
private botsUpdateTimer: number | null = null;
|
||||||
private mapLoader = terrainMapFileLoader;
|
private mapLoader = terrainMapFileLoader;
|
||||||
@@ -88,6 +86,9 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
|
|
||||||
private readonly handleLobbyInfo = (event: LobbyInfoEvent) => {
|
private readonly handleLobbyInfo = (event: LobbyInfoEvent) => {
|
||||||
const lobby = event.lobby;
|
const lobby = event.lobby;
|
||||||
|
if (!this.lobbyId || lobby.gameID !== this.lobbyId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.lobbyCreatorClientID = lobby.lobbyCreatorClientID ?? "";
|
this.lobbyCreatorClientID = lobby.lobbyCreatorClientID ?? "";
|
||||||
if (lobby.clients) {
|
if (lobby.clients) {
|
||||||
this.clients = lobby.clients;
|
this.clients = lobby.clients;
|
||||||
@@ -209,9 +210,7 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden select-none"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title: translateText("host_modal.title"),
|
title: translateText("host_modal.title"),
|
||||||
@@ -391,7 +390,6 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
this.close();
|
this.close();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
|
|
||||||
this.loadNationCount();
|
this.loadNationCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,10 +416,6 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
crazyGamesSDK.hideInviteButton();
|
crazyGamesSDK.hideInviteButton();
|
||||||
|
|
||||||
// Clean up timers and resources
|
// Clean up timers and resources
|
||||||
if (this.playersInterval) {
|
|
||||||
clearInterval(this.playersInterval);
|
|
||||||
this.playersInterval = null;
|
|
||||||
}
|
|
||||||
if (this.botsUpdateTimer !== null) {
|
if (this.botsUpdateTimer !== null) {
|
||||||
clearTimeout(this.botsUpdateTimer);
|
clearTimeout(this.botsUpdateTimer);
|
||||||
this.botsUpdateTimer = null;
|
this.botsUpdateTimer = null;
|
||||||
@@ -811,20 +805,6 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pollPlayers() {
|
|
||||||
const config = await getServerConfigFromClient();
|
|
||||||
fetch(`/${config.workerPath(this.lobbyId)}/api/game/${this.lobbyId}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data: GameInfo) => {
|
|
||||||
this.clients = data.clients ?? [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private kickPlayer(clientID: string) {
|
private kickPlayer(clientID: string) {
|
||||||
// Dispatch event to be handled by WebSocket instead of HTTP
|
// Dispatch event to be handled by WebSocket instead of HTTP
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { customElement, property, query, state } from "lit/decorators.js";
|
|||||||
import {
|
import {
|
||||||
getActiveModifiers,
|
getActiveModifiers,
|
||||||
getGameModeLabel,
|
getGameModeLabel,
|
||||||
normaliseMapKey,
|
getMapName,
|
||||||
renderDuration,
|
renderDuration,
|
||||||
renderNumber,
|
renderNumber,
|
||||||
translateText,
|
translateText,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
GameInfo,
|
GameInfo,
|
||||||
GameRecordSchema,
|
GameRecordSchema,
|
||||||
LobbyInfoEvent,
|
LobbyInfoEvent,
|
||||||
|
PublicGameInfo,
|
||||||
} from "../core/Schemas";
|
} from "../core/Schemas";
|
||||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||||
import {
|
import {
|
||||||
@@ -96,9 +97,7 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
? (this.lobbyCreatorClientID ?? "")
|
? (this.lobbyCreatorClientID ?? "")
|
||||||
: "";
|
: "";
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden select-none"
|
|
||||||
>
|
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title: translateText("public_lobby.title"),
|
title: translateText("public_lobby.title"),
|
||||||
onBack: () => this.closeAndLeave(),
|
onBack: () => this.closeAndLeave(),
|
||||||
@@ -149,7 +148,7 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
${this.isPrivateLobby()
|
${this.isPrivateLobby()
|
||||||
? html`
|
? html`
|
||||||
<div
|
<div
|
||||||
class="p-6 pt-4 border-t border-white/10 bg-black/20 shrink-0"
|
class="p-6 lg:p-6 border-t border-white/10 bg-black/20 shrink-0"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="w-full py-4 text-sm font-bold text-white uppercase tracking-widest bg-blue-600 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed rounded-xl transition-all shadow-lg shadow-blue-900/20 hover:shadow-blue-900/40 hover:-translate-y-0.5 active:translate-y-0 disabled:transform-none"
|
class="w-full py-4 text-sm font-bold text-white uppercase tracking-widest bg-blue-600 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed rounded-xl transition-all shadow-lg shadow-blue-900/20 hover:shadow-blue-900/40 hover:-translate-y-0.5 active:translate-y-0 disabled:transform-none"
|
||||||
@@ -161,7 +160,7 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<div
|
<div
|
||||||
class="p-6 pt-4 border-t border-white/10 bg-black/20 shrink-0"
|
class="p-6 lg:p-6 border-t border-white/10 bg-black/20 shrink-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-full px-4 py-3 rounded-xl border border-white/10 bg-white/5 flex items-center justify-between gap-3"
|
class="w-full px-4 py-3 rounded-xl border border-white/10 bg-white/5 flex items-center justify-between gap-3"
|
||||||
@@ -216,9 +215,7 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
|
|
||||||
private renderJoinForm() {
|
private renderJoinForm() {
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden select-none"
|
|
||||||
>
|
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title: translateText("private_lobby.title"),
|
title: translateText("private_lobby.title"),
|
||||||
onBack: () => this.closeAndLeave(),
|
onBack: () => this.closeAndLeave(),
|
||||||
@@ -280,15 +277,12 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public open(lobbyId: string = "", isPublic: boolean = false) {
|
public open(lobbyId: string = "", lobbyInfo?: GameInfo | PublicGameInfo) {
|
||||||
super.open();
|
super.open();
|
||||||
if (lobbyId) {
|
if (lobbyId) {
|
||||||
this.startTrackingLobby(lobbyId);
|
this.startTrackingLobby(lobbyId, lobbyInfo);
|
||||||
// If opened with lobbyInfo (public lobby case), auto-join the lobby
|
// If opened with lobbyId but no lobbyInfo (URL join case), auto-join the lobby
|
||||||
if (isPublic) {
|
if (!lobbyInfo) {
|
||||||
this.joinPublicLobby(lobbyId);
|
|
||||||
} else {
|
|
||||||
// If opened with lobbyId but no lobbyInfo (URL join case), check if active and join
|
|
||||||
this.handleUrlJoin(lobbyId);
|
this.handleUrlJoin(lobbyId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,21 +320,10 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private joinPublicLobby(lobbyId: string) {
|
private startTrackingLobby(
|
||||||
// Dispatch join-lobby event to actually connect to the lobby
|
lobbyId: string,
|
||||||
this.dispatchEvent(
|
lobbyInfo?: GameInfo | PublicGameInfo,
|
||||||
new CustomEvent("join-lobby", {
|
) {
|
||||||
detail: {
|
|
||||||
gameID: lobbyId,
|
|
||||||
source: "public",
|
|
||||||
} as JoinLobbyEvent,
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private startTrackingLobby(lobbyId: string, lobbyInfo?: GameInfo) {
|
|
||||||
this.currentLobbyId = lobbyId;
|
this.currentLobbyId = lobbyId;
|
||||||
// clientID will be assigned by server via lobby_info message
|
// clientID will be assigned by server via lobby_info message
|
||||||
this.currentClientID = "";
|
this.currentClientID = "";
|
||||||
@@ -355,7 +338,7 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
if (lobbyInfo) {
|
if (lobbyInfo) {
|
||||||
this.updateFromLobby(lobbyInfo);
|
this.updateFromLobby(lobbyInfo);
|
||||||
// Only stop showing spinner when we have player info
|
// Only stop showing spinner when we have player info
|
||||||
if (lobbyInfo.clients) {
|
if ("clients" in lobbyInfo && lobbyInfo.clients) {
|
||||||
this.isConnecting = false;
|
this.isConnecting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,7 +419,7 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
if (!this.gameConfig) return html``;
|
if (!this.gameConfig) return html``;
|
||||||
|
|
||||||
const c = this.gameConfig;
|
const c = this.gameConfig;
|
||||||
const mapName = translateText("map." + normaliseMapKey(c.gameMap));
|
const mapName = getMapName(c.gameMap);
|
||||||
const modeName = getGameModeLabel(c);
|
const modeName = getGameModeLabel(c);
|
||||||
const modifiers = getActiveModifiers(c.publicGameModifiers);
|
const modifiers = getActiveModifiers(c.publicGameModifiers);
|
||||||
|
|
||||||
@@ -530,8 +513,8 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
|
|
||||||
// --- Lobby event handling ---
|
// --- Lobby event handling ---
|
||||||
|
|
||||||
private updateFromLobby(lobby: GameInfo) {
|
private updateFromLobby(lobby: GameInfo | PublicGameInfo) {
|
||||||
this.players = lobby.clients ?? [];
|
this.players = "clients" in lobby ? (lobby.clients ?? []) : [];
|
||||||
this.lobbyStartAt = lobby.startsAt ?? null;
|
this.lobbyStartAt = lobby.startsAt ?? null;
|
||||||
this.syncCountdownTimer();
|
this.syncCountdownTimer();
|
||||||
if (lobby.gameConfig) {
|
if (lobby.gameConfig) {
|
||||||
@@ -542,7 +525,10 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lobbyCreatorClientID = lobby.lobbyCreatorClientID ?? null;
|
this.lobbyCreatorClientID =
|
||||||
|
"lobbyCreatorClientID" in lobby
|
||||||
|
? (lobby.lobbyCreatorClientID ?? null)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private startLobbyUpdates() {
|
private startLobbyUpdates() {
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export class LangSelector extends LitElement {
|
|||||||
"help-modal",
|
"help-modal",
|
||||||
"settings-modal",
|
"settings-modal",
|
||||||
"username-input",
|
"username-input",
|
||||||
"public-lobby",
|
"game-mode-selector",
|
||||||
"user-setting",
|
"user-setting",
|
||||||
"o-modal",
|
"o-modal",
|
||||||
"o-button",
|
"o-button",
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ export class LanguageModal extends BaseModal {
|
|||||||
render() {
|
render() {
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div
|
||||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden select-none"
|
class="${this.modalContainerClass}"
|
||||||
>
|
>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title: translateText("select_lang.title"),
|
title: translateText("select_lang.title"),
|
||||||
onBack: this.close,
|
onBack: () => this.close(),
|
||||||
ariaLabel: translateText("common.back"),
|
ariaLabel: translateText("common.back"),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ export class LanguageModal extends BaseModal {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/flags/${lang.svg}.svg"
|
src="/flags/${lang.svg}.svg"
|
||||||
class="w-8 h-6 object-contain shadow-sm rounded-sm shrink-0"
|
class="w-8 h-6 object-contain rounded-sm shrink-0"
|
||||||
alt="${lang.code}"
|
alt="${lang.code}"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col items-start min-w-0">
|
<div class="flex flex-col items-start min-w-0">
|
||||||
|
|||||||
@@ -82,11 +82,7 @@ export class LeaderboardModal extends BaseModal {
|
|||||||
>`;
|
>`;
|
||||||
|
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col overflow-hidden ${this.inline
|
|
||||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
|
||||||
: ""}"
|
|
||||||
>
|
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
titleContent: html`
|
titleContent: html`
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -99,7 +95,7 @@ export class LeaderboardModal extends BaseModal {
|
|||||||
${this.activeTab === "players" ? refreshTime : ""}
|
${this.activeTab === "players" ? refreshTime : ""}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
onBack: this.close,
|
onBack: () => this.close(),
|
||||||
ariaLabel: translateText("common.close"),
|
ariaLabel: translateText("common.close"),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
+33
-93
@@ -1,7 +1,13 @@
|
|||||||
import version from "resources/version.txt?raw";
|
import version from "resources/version.txt?raw";
|
||||||
import { UserMeResponse } from "../core/ApiSchemas";
|
import { UserMeResponse } from "../core/ApiSchemas";
|
||||||
import { EventBus } from "../core/EventBus";
|
import { EventBus } from "../core/EventBus";
|
||||||
import { GAME_ID_REGEX, GameRecord, GameStartInfo } from "../core/Schemas";
|
import {
|
||||||
|
GAME_ID_REGEX,
|
||||||
|
GameInfo,
|
||||||
|
GameRecord,
|
||||||
|
GameStartInfo,
|
||||||
|
PublicGameInfo,
|
||||||
|
} from "../core/Schemas";
|
||||||
import { GameEnv } from "../core/configuration/Config";
|
import { GameEnv } from "../core/configuration/Config";
|
||||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||||
import { GameType } from "../core/game/Game";
|
import { GameType } from "../core/game/Game";
|
||||||
@@ -17,6 +23,8 @@ import { FlagInput } from "./FlagInput";
|
|||||||
import "./FlagInputModal";
|
import "./FlagInputModal";
|
||||||
import { FlagInputModal } from "./FlagInputModal";
|
import { FlagInputModal } from "./FlagInputModal";
|
||||||
import { GameInfoModal } from "./GameInfoModal";
|
import { GameInfoModal } from "./GameInfoModal";
|
||||||
|
import "./GameModeSelector";
|
||||||
|
import { GameModeSelector } from "./GameModeSelector";
|
||||||
import { GameStartingModal } from "./GameStartingModal";
|
import { GameStartingModal } from "./GameStartingModal";
|
||||||
import "./GoogleAdElement";
|
import "./GoogleAdElement";
|
||||||
import { GutterAds } from "./GutterAds";
|
import { GutterAds } from "./GutterAds";
|
||||||
@@ -32,9 +40,7 @@ import { MatchmakingModal } from "./Matchmaking";
|
|||||||
import { initNavigation } from "./Navigation";
|
import { initNavigation } from "./Navigation";
|
||||||
import "./NewsModal";
|
import "./NewsModal";
|
||||||
import "./PatternInput";
|
import "./PatternInput";
|
||||||
import "./PublicLobby";
|
import "./SinglePlayerModal";
|
||||||
import { PublicLobby, ShowPublicLobbyModalEvent } from "./PublicLobby";
|
|
||||||
import { SinglePlayerModal } from "./SinglePlayerModal";
|
|
||||||
import { TerritoryPatternsModal } from "./TerritoryPatternsModal";
|
import { TerritoryPatternsModal } from "./TerritoryPatternsModal";
|
||||||
import { TokenLoginModal } from "./TokenLoginModal";
|
import { TokenLoginModal } from "./TokenLoginModal";
|
||||||
import {
|
import {
|
||||||
@@ -55,6 +61,7 @@ import "./components/Footer";
|
|||||||
import "./components/MainLayout";
|
import "./components/MainLayout";
|
||||||
import "./components/MobileNavBar";
|
import "./components/MobileNavBar";
|
||||||
import "./components/PlayPage";
|
import "./components/PlayPage";
|
||||||
|
import "./components/RankedModal";
|
||||||
import "./components/baseComponents/Button";
|
import "./components/baseComponents/Button";
|
||||||
import "./components/baseComponents/Modal";
|
import "./components/baseComponents/Modal";
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
@@ -203,15 +210,16 @@ declare global {
|
|||||||
BOLT_AD_CLICKED: string;
|
BOLT_AD_CLICKED: string;
|
||||||
SHOW_HIDDEN_CONTAINER: string;
|
SHOW_HIDDEN_CONTAINER: string;
|
||||||
};
|
};
|
||||||
|
currentPageId?: string;
|
||||||
showPage?: (pageId: string) => void;
|
showPage?: (pageId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extend the global interfaces to include your custom events
|
// Extend the global interfaces to include your custom events
|
||||||
interface DocumentEventMap {
|
interface DocumentEventMap {
|
||||||
"join-lobby": CustomEvent<JoinLobbyEvent>;
|
"join-lobby": CustomEvent<JoinLobbyEvent>;
|
||||||
"show-public-lobby-modal": CustomEvent<ShowPublicLobbyModalEvent>;
|
|
||||||
"kick-player": CustomEvent;
|
"kick-player": CustomEvent;
|
||||||
"join-changed": CustomEvent;
|
"join-changed": CustomEvent;
|
||||||
|
"open-matchmaking": CustomEvent<undefined>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +231,7 @@ export interface JoinLobbyEvent {
|
|||||||
// GameRecord exists when replaying an archived game.
|
// GameRecord exists when replaying an archived game.
|
||||||
gameRecord?: GameRecord;
|
gameRecord?: GameRecord;
|
||||||
source?: "public" | "private" | "host" | "matchmaking" | "singleplayer";
|
source?: "public" | "private" | "host" | "matchmaking" | "singleplayer";
|
||||||
|
publicLobbyInfo?: GameInfo | PublicGameInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Client {
|
class Client {
|
||||||
@@ -236,21 +245,18 @@ class Client {
|
|||||||
|
|
||||||
private hostModal: HostPrivateLobbyModal;
|
private hostModal: HostPrivateLobbyModal;
|
||||||
private joinModal: JoinLobbyModal;
|
private joinModal: JoinLobbyModal;
|
||||||
private publicLobby: PublicLobby;
|
private gameModeSelector: GameModeSelector;
|
||||||
private userSettings: UserSettings = new UserSettings();
|
private userSettings: UserSettings = new UserSettings();
|
||||||
private patternsModal: TerritoryPatternsModal;
|
private patternsModal: TerritoryPatternsModal;
|
||||||
private tokenLoginModal: TokenLoginModal;
|
private tokenLoginModal: TokenLoginModal;
|
||||||
private matchmakingModal: MatchmakingModal;
|
private matchmakingModal: MatchmakingModal;
|
||||||
|
|
||||||
private gutterAds: GutterAds;
|
private gutterAds: GutterAds;
|
||||||
|
|
||||||
private turnstileTokenPromise: Promise<{
|
private turnstileTokenPromise: Promise<{
|
||||||
token: string;
|
token: string;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
}> | null = null;
|
}> | null = null;
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
crazyGamesSDK.maybeInit();
|
crazyGamesSDK.maybeInit();
|
||||||
// Prefetch turnstile token so it is available when
|
// Prefetch turnstile token so it is available when
|
||||||
@@ -293,7 +299,9 @@ class Client {
|
|||||||
console.warn("Username input element not found");
|
console.warn("Username input element not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.publicLobby = document.querySelector("public-lobby") as PublicLobby;
|
this.gameModeSelector = document.querySelector(
|
||||||
|
"game-mode-selector",
|
||||||
|
) as GameModeSelector;
|
||||||
|
|
||||||
window.addEventListener("beforeunload", async () => {
|
window.addEventListener("beforeunload", async () => {
|
||||||
console.log("Browser is closing");
|
console.log("Browser is closing");
|
||||||
@@ -309,41 +317,16 @@ class Client {
|
|||||||
this.gutterAds = gutterAds;
|
this.gutterAds = gutterAds;
|
||||||
|
|
||||||
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
|
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
|
||||||
document.addEventListener(
|
|
||||||
"show-public-lobby-modal",
|
|
||||||
this.handleShowPublicLobbyModal.bind(this),
|
|
||||||
);
|
|
||||||
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
|
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
|
||||||
document.addEventListener("kick-player", this.handleKickPlayer.bind(this));
|
document.addEventListener("kick-player", this.handleKickPlayer.bind(this));
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"update-game-config",
|
"update-game-config",
|
||||||
this.handleUpdateGameConfig.bind(this),
|
this.handleUpdateGameConfig.bind(this),
|
||||||
);
|
);
|
||||||
|
document.addEventListener(
|
||||||
const spModal = document.querySelector(
|
"open-matchmaking",
|
||||||
"single-player-modal",
|
this.handleOpenMatchmaking.bind(this),
|
||||||
) as SinglePlayerModal;
|
);
|
||||||
if (!spModal || !(spModal instanceof SinglePlayerModal)) {
|
|
||||||
console.warn("Singleplayer modal element not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const singlePlayer = document.getElementById("single-player");
|
|
||||||
if (singlePlayer === null) throw new Error("Missing single-player");
|
|
||||||
singlePlayer.addEventListener("click", () => {
|
|
||||||
if (this.usernameInput?.isValid()) {
|
|
||||||
window.showPage?.("page-single-player");
|
|
||||||
} else {
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent("show-message", {
|
|
||||||
detail: {
|
|
||||||
message: this.usernameInput?.validationError,
|
|
||||||
color: "red",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const hlpModal = document.querySelector("help-modal") as HelpModal;
|
const hlpModal = document.querySelector("help-modal") as HelpModal;
|
||||||
if (!hlpModal || !(hlpModal instanceof HelpModal)) {
|
if (!hlpModal || !(hlpModal instanceof HelpModal)) {
|
||||||
@@ -512,23 +495,6 @@ class Client {
|
|||||||
} else {
|
} else {
|
||||||
this.hostModal.eventBus = this.eventBus;
|
this.hostModal.eventBus = this.eventBus;
|
||||||
}
|
}
|
||||||
const hostLobbyButton = document.getElementById("host-lobby-button");
|
|
||||||
if (hostLobbyButton === null) throw new Error("Missing host-lobby-button");
|
|
||||||
hostLobbyButton.addEventListener("click", () => {
|
|
||||||
if (this.usernameInput?.isValid()) {
|
|
||||||
window.showPage?.("page-host-lobby");
|
|
||||||
} else {
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent("show-message", {
|
|
||||||
detail: {
|
|
||||||
message: this.usernameInput?.validationError,
|
|
||||||
color: "red",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.joinModal = document.querySelector(
|
this.joinModal = document.querySelector(
|
||||||
"join-lobby-modal",
|
"join-lobby-modal",
|
||||||
@@ -538,26 +504,6 @@ class Client {
|
|||||||
} else {
|
} else {
|
||||||
this.joinModal.eventBus = this.eventBus;
|
this.joinModal.eventBus = this.eventBus;
|
||||||
}
|
}
|
||||||
const joinPrivateLobbyButton = document.getElementById(
|
|
||||||
"join-private-lobby-button",
|
|
||||||
);
|
|
||||||
if (joinPrivateLobbyButton === null)
|
|
||||||
throw new Error("Missing join-private-lobby-button");
|
|
||||||
joinPrivateLobbyButton.addEventListener("click", () => {
|
|
||||||
if (this.usernameInput?.isValid()) {
|
|
||||||
window.showPage?.("page-join-lobby");
|
|
||||||
} else {
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent("show-message", {
|
|
||||||
detail: {
|
|
||||||
message: this.usernameInput?.validationError,
|
|
||||||
color: "red",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.userSettings.darkMode()) {
|
if (this.userSettings.darkMode()) {
|
||||||
document.documentElement.classList.add("dark");
|
document.documentElement.classList.add("dark");
|
||||||
@@ -773,8 +719,7 @@ class Client {
|
|||||||
(searchParams.toString() ? "?" + searchParams.toString() : "") +
|
(searchParams.toString() ? "?" + searchParams.toString() : "") +
|
||||||
window.location.hash;
|
window.location.hash;
|
||||||
history.replaceState(null, "", newUrl);
|
history.replaceState(null, "", newUrl);
|
||||||
// Wait for matchmaking button to be defined, then trigger its click handler
|
// Wait for matchmaking button to be defined, then trigger its click handler.
|
||||||
// This goes through username validation instead of bypassing it
|
|
||||||
customElements.whenDefined("matchmaking-button").then(() => {
|
customElements.whenDefined("matchmaking-button").then(() => {
|
||||||
const matchmakingButton = document.querySelector(
|
const matchmakingButton = document.querySelector(
|
||||||
"matchmaking-button button",
|
"matchmaking-button button",
|
||||||
@@ -798,12 +743,14 @@ class Client {
|
|||||||
this.gameStop(true);
|
this.gameStop(true);
|
||||||
document.body.classList.remove("in-game");
|
document.body.classList.remove("in-game");
|
||||||
}
|
}
|
||||||
|
if (lobby.source === "public") {
|
||||||
|
this.joinModal?.open(lobby.gameID, lobby.publicLobbyInfo);
|
||||||
|
}
|
||||||
const config = await getServerConfigFromClient();
|
const config = await getServerConfigFromClient();
|
||||||
// Only update URL immediately for private lobbies, not public ones
|
// Only update URL immediately for private lobbies, not public ones
|
||||||
if (lobby.source !== "public") {
|
if (lobby.source !== "public") {
|
||||||
this.updateJoinUrlForShare(lobby.gameID, config);
|
this.updateJoinUrlForShare(lobby.gameID, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.gameStop = joinLobby(
|
this.gameStop = joinLobby(
|
||||||
this.eventBus,
|
this.eventBus,
|
||||||
{
|
{
|
||||||
@@ -856,7 +803,7 @@ class Client {
|
|||||||
modal.isModalOpen = false;
|
modal.isModalOpen = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.publicLobby.stop();
|
this.gameModeSelector.stop();
|
||||||
document.querySelectorAll(".ad").forEach((ad) => {
|
document.querySelectorAll(".ad").forEach((ad) => {
|
||||||
(ad as HTMLElement).style.display = "none";
|
(ad as HTMLElement).style.display = "none";
|
||||||
});
|
});
|
||||||
@@ -872,8 +819,8 @@ class Client {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.joinModal.close();
|
this.joinModal?.closeWithoutLeaving();
|
||||||
this.publicLobby.stop();
|
this.gameModeSelector.stop();
|
||||||
incrementGamesPlayed();
|
incrementGamesPlayed();
|
||||||
|
|
||||||
document.querySelectorAll(".ad").forEach((ad) => {
|
document.querySelectorAll(".ad").forEach((ad) => {
|
||||||
@@ -915,17 +862,6 @@ class Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleShowPublicLobbyModal(
|
|
||||||
event: CustomEvent<ShowPublicLobbyModalEvent>,
|
|
||||||
) {
|
|
||||||
const { lobby } = event.detail;
|
|
||||||
console.log(`Opening JoinLobbyModal for public lobby ${lobby.gameID}`);
|
|
||||||
|
|
||||||
// Open the join lobby modal page and pass the lobby info
|
|
||||||
window.showPage?.("page-join-lobby");
|
|
||||||
this.joinModal?.open(lobby.gameID, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleLeaveLobby(/* event: CustomEvent */) {
|
private async handleLeaveLobby(/* event: CustomEvent */) {
|
||||||
if (this.gameStop === null) {
|
if (this.gameStop === null) {
|
||||||
return;
|
return;
|
||||||
@@ -946,6 +882,10 @@ class Client {
|
|||||||
crazyGamesSDK.gameplayStop();
|
crazyGamesSDK.gameplayStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleOpenMatchmaking(_event: CustomEvent<undefined>) {
|
||||||
|
this.matchmakingModal?.open();
|
||||||
|
}
|
||||||
|
|
||||||
private handleKickPlayer(event: CustomEvent) {
|
private handleKickPlayer(event: CustomEvent) {
|
||||||
const { target } = event.detail;
|
const { target } = event.detail;
|
||||||
|
|
||||||
|
|||||||
+20
-67
@@ -1,5 +1,5 @@
|
|||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, query, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { UserMeResponse } from "../core/ApiSchemas";
|
import { UserMeResponse } from "../core/ApiSchemas";
|
||||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||||
import { getUserMe, hasLinkedAccount } from "./Api";
|
import { getUserMe, hasLinkedAccount } from "./Api";
|
||||||
@@ -18,7 +18,7 @@ export class MatchmakingModal extends BaseModal {
|
|||||||
@state() private connected = false;
|
@state() private connected = false;
|
||||||
@state() private socket: WebSocket | null = null;
|
@state() private socket: WebSocket | null = null;
|
||||||
@state() private gameID: string | null = null;
|
@state() private gameID: string | null = null;
|
||||||
private elo: number | "unknown" = "unknown";
|
private elo: number | string = "...";
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -37,14 +37,10 @@ export class MatchmakingModal extends BaseModal {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col ${this.inline
|
|
||||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
|
||||||
: ""}"
|
|
||||||
>
|
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title: translateText("matchmaking_modal.title"),
|
title: translateText("matchmaking_modal.title"),
|
||||||
onBack: this.close,
|
onBack: () => this.close(),
|
||||||
ariaLabel: translateText("common.back"),
|
ariaLabel: translateText("common.back"),
|
||||||
})}
|
})}
|
||||||
<div class="flex-1 flex flex-col items-center justify-center gap-6 p-6">
|
<div class="flex-1 flex flex-col items-center justify-center gap-6 p-6">
|
||||||
@@ -71,39 +67,21 @@ export class MatchmakingModal extends BaseModal {
|
|||||||
|
|
||||||
private renderInner() {
|
private renderInner() {
|
||||||
if (!this.connected) {
|
if (!this.connected) {
|
||||||
return html`
|
return this.renderLoadingSpinner(
|
||||||
<div class="flex flex-col items-center gap-4">
|
translateText("matchmaking_modal.connecting"),
|
||||||
<div
|
"blue",
|
||||||
class="w-12 h-12 border-4 border-blue-500/30 border-t-blue-500 rounded-full animate-spin"
|
);
|
||||||
></div>
|
|
||||||
<p class="text-center text-white/80">
|
|
||||||
${translateText("matchmaking_modal.connecting")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
if (this.gameID === null) {
|
if (this.gameID === null) {
|
||||||
return html`
|
return this.renderLoadingSpinner(
|
||||||
<div class="flex flex-col items-center gap-4">
|
translateText("matchmaking_modal.searching"),
|
||||||
<div
|
"green",
|
||||||
class="w-12 h-12 border-4 border-green-500/30 border-t-green-500 rounded-full animate-spin"
|
);
|
||||||
></div>
|
|
||||||
<p class="text-center text-white/80">
|
|
||||||
${translateText("matchmaking_modal.searching")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return this.renderLoadingSpinner(
|
||||||
<div class="flex flex-col items-center gap-4">
|
translateText("matchmaking_modal.waiting_for_game"),
|
||||||
<div
|
"yellow",
|
||||||
class="w-12 h-12 border-4 border-yellow-500/30 border-t-yellow-500 rounded-full animate-spin"
|
);
|
||||||
></div>
|
|
||||||
<p class="text-center text-white/80">
|
|
||||||
${translateText("matchmaking_modal.waiting_for_game")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +155,9 @@ export class MatchmakingModal extends BaseModal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.elo = userMe.player.leaderboard?.oneVone?.elo ?? "unknown";
|
this.elo =
|
||||||
|
userMe.player.leaderboard?.oneVone?.elo ??
|
||||||
|
translateText("matchmaking_modal.no_elo");
|
||||||
|
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.gameID = null;
|
this.gameID = null;
|
||||||
@@ -241,7 +221,6 @@ export class MatchmakingModal extends BaseModal {
|
|||||||
|
|
||||||
@customElement("matchmaking-button")
|
@customElement("matchmaking-button")
|
||||||
export class MatchmakingButton extends LitElement {
|
export class MatchmakingButton extends LitElement {
|
||||||
@query("matchmaking-modal") private matchmakingModal?: MatchmakingModal;
|
|
||||||
@state() private isLoggedIn = false;
|
@state() private isLoggedIn = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -281,7 +260,6 @@ export class MatchmakingButton extends LitElement {
|
|||||||
${translateText("matchmaking_button.description")}
|
${translateText("matchmaking_button.description")}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<matchmaking-modal></matchmaking-modal>
|
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<button
|
<button
|
||||||
@@ -296,35 +274,10 @@ export class MatchmakingButton extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleLoggedInClick() {
|
private handleLoggedInClick() {
|
||||||
const usernameInput = document.querySelector("username-input") as any;
|
document.dispatchEvent(new CustomEvent("open-matchmaking"));
|
||||||
const publicLobby = document.querySelector("public-lobby") as any;
|
|
||||||
|
|
||||||
if (usernameInput?.isValid()) {
|
|
||||||
this.open();
|
|
||||||
publicLobby?.leaveLobby();
|
|
||||||
} else {
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent("show-message", {
|
|
||||||
detail: {
|
|
||||||
message: usernameInput?.validationError,
|
|
||||||
color: "red",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleLoggedOutClick() {
|
private handleLoggedOutClick() {
|
||||||
window.showPage?.("page-account");
|
window.showPage?.("page-account");
|
||||||
}
|
}
|
||||||
|
|
||||||
public open() {
|
|
||||||
this.matchmakingModal?.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
public close() {
|
|
||||||
this.matchmakingModal?.close();
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
export function initNavigation() {
|
export function initNavigation() {
|
||||||
|
const closeMobileSidebar = () => {
|
||||||
|
const sidebar = document.getElementById("sidebar-menu");
|
||||||
|
const backdrop = document.getElementById("mobile-menu-backdrop");
|
||||||
|
if (sidebar?.classList.contains("open")) {
|
||||||
|
sidebar.classList.remove("open");
|
||||||
|
backdrop?.classList.remove("open");
|
||||||
|
document.documentElement.classList.remove("overflow-hidden");
|
||||||
|
sidebar.setAttribute("aria-hidden", "true");
|
||||||
|
backdrop?.setAttribute("aria-hidden", "true");
|
||||||
|
const hb = document.getElementById("hamburger-btn");
|
||||||
|
if (hb) hb.setAttribute("aria-expanded", "false");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const showPage = (pageId: string) => {
|
const showPage = (pageId: string) => {
|
||||||
(window as any).currentPageId = pageId;
|
window.currentPageId = pageId;
|
||||||
|
|
||||||
|
// Close mobile sidebar if a nav item was clicked
|
||||||
|
closeMobileSidebar();
|
||||||
|
|
||||||
// Hide only the currently visible modal
|
// Hide only the currently visible modal
|
||||||
const visibleModal = document.querySelector(".page-content:not(.hidden)");
|
const visibleModal = document.querySelector(".page-content:not(.hidden)");
|
||||||
@@ -106,9 +123,6 @@ export function initNavigation() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set default page to play if no menu item is active
|
// Ensure Play is the default visible/active page on load.
|
||||||
const anyActive = document.querySelector(".nav-menu-item.active");
|
showPage("page-play");
|
||||||
if (!anyActive) {
|
|
||||||
showPage("page-play");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,14 +18,10 @@ export class NewsModal extends BaseModal {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col ${this.inline
|
|
||||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
|
||||||
: ""}"
|
|
||||||
>
|
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title: translateText("news.title"),
|
title: translateText("news.title"),
|
||||||
onBack: this.close,
|
onBack: () => this.close(),
|
||||||
ariaLabel: translateText("common.back"),
|
ariaLabel: translateText("common.back"),
|
||||||
})}
|
})}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export class PatternInput extends LitElement {
|
|||||||
@property({ type: Boolean, attribute: "show-select-label" })
|
@property({ type: Boolean, attribute: "show-select-label" })
|
||||||
public showSelectLabel: boolean = false;
|
public showSelectLabel: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "adaptive-size" })
|
||||||
|
public adaptiveSize: boolean = false;
|
||||||
|
|
||||||
private _abortController: AbortController | null = null;
|
private _abortController: AbortController | null = null;
|
||||||
|
|
||||||
private _onPatternSelected = async () => {
|
private _onPatternSelected = async () => {
|
||||||
@@ -60,13 +63,39 @@ export class PatternInput extends LitElement {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getIsDefaultPattern(): boolean {
|
||||||
|
return this.pattern === null && this.selectedColor === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldShowSelectLabel(): boolean {
|
||||||
|
return this.showSelectLabel && this.getIsDefaultPattern();
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyAdaptiveSize(): void {
|
||||||
|
if (!this.adaptiveSize) {
|
||||||
|
this.style.removeProperty("width");
|
||||||
|
this.style.removeProperty("height");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSelect = this.showSelectLabel && this.getIsDefaultPattern();
|
||||||
|
this.style.setProperty("height", "3rem");
|
||||||
|
this.style.setProperty(
|
||||||
|
"width",
|
||||||
|
showSelect ? "clamp(6.5rem, 28vw, 9.5rem)" : "3rem",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(): void {
|
||||||
|
this.applyAdaptiveSize();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (crazyGamesSDK.isOnCrazyGames()) {
|
if (crazyGamesSDK.isOnCrazyGames()) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDefault = this.pattern === null && this.selectedColor === null;
|
const showSelect = this.shouldShowSelectLabel();
|
||||||
const showSelect = this.showSelectLabel && isDefault;
|
|
||||||
const buttonTitle = translateText("territory_patterns.title");
|
const buttonTitle = translateText("territory_patterns.title");
|
||||||
|
|
||||||
// Show loading state
|
// Show loading state
|
||||||
@@ -74,7 +103,7 @@ export class PatternInput extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<button
|
<button
|
||||||
id="pattern-input"
|
id="pattern-input"
|
||||||
class="pattern-btn m-0 border-0 !p-0 w-full h-full flex cursor-pointer justify-center items-center focus:outline-none focus:ring-0 bg-slate-900/80 rounded-lg overflow-hidden"
|
class="pattern-btn m-0 p-0 w-full h-full flex cursor-pointer justify-center items-center focus:outline-none focus:ring-0 bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] rounded-lg overflow-hidden"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -94,7 +123,7 @@ export class PatternInput extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<button
|
<button
|
||||||
id="pattern-input"
|
id="pattern-input"
|
||||||
class="pattern-btn m-0 border-0 !p-0 w-full h-full flex cursor-pointer justify-center items-center focus:outline-none focus:ring-0 transition-all duration-200 hover:scale-105 bg-slate-900/80 hover:bg-slate-800/80 active:bg-slate-800/90 rounded-lg overflow-hidden"
|
class="pattern-btn m-0 p-0 w-full h-full flex cursor-pointer justify-center items-center focus:outline-none focus:ring-0 transition-all duration-200 hover:scale-105 bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] hover:brightness-[1.08] active:brightness-[0.95] rounded-lg overflow-hidden"
|
||||||
title=${buttonTitle}
|
title=${buttonTitle}
|
||||||
@click=${this.onInputClick}
|
@click=${this.onInputClick}
|
||||||
>
|
>
|
||||||
@@ -107,7 +136,7 @@ export class PatternInput extends LitElement {
|
|||||||
</span>
|
</span>
|
||||||
${showSelect
|
${showSelect
|
||||||
? html`<span
|
? html`<span
|
||||||
class="text-[10px] font-black text-white/40 uppercase leading-none break-words w-full text-center px-1"
|
class="text-[10px] font-black text-white uppercase leading-none break-words w-full text-center px-1"
|
||||||
>
|
>
|
||||||
${translateText("territory_patterns.select_skin")}
|
${translateText("territory_patterns.select_skin")}
|
||||||
</span>`
|
</span>`
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
import { html, LitElement } from "lit";
|
|
||||||
import { customElement, state } from "lit/decorators.js";
|
|
||||||
import { GameMapType } from "../core/game/Game";
|
|
||||||
import { GameID, PublicGameInfo, PublicGames } from "../core/Schemas";
|
|
||||||
import { PublicLobbySocket } from "./LobbySocket";
|
|
||||||
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
|
||||||
import {
|
|
||||||
getGameModeLabel,
|
|
||||||
getModifierLabels,
|
|
||||||
normaliseMapKey,
|
|
||||||
renderDuration,
|
|
||||||
translateText,
|
|
||||||
} from "./Utils";
|
|
||||||
|
|
||||||
export interface ShowPublicLobbyModalEvent {
|
|
||||||
lobby: PublicGameInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("public-lobby")
|
|
||||||
export class PublicLobby extends LitElement {
|
|
||||||
@state() private publicGames: PublicGames | null = null;
|
|
||||||
@state() public isLobbyHighlighted: boolean = false;
|
|
||||||
@state() private mapImages: Map<GameID, string> = new Map();
|
|
||||||
|
|
||||||
private lobbyIDToStart = new Map<GameID, number>();
|
|
||||||
private serverTimeOffset = 0;
|
|
||||||
private lobbySocket = new PublicLobbySocket((data) =>
|
|
||||||
this.handleLobbiesUpdate(data),
|
|
||||||
);
|
|
||||||
|
|
||||||
createRenderRoot() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.lobbySocket.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this.lobbySocket.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleLobbiesUpdate(publicGames: PublicGames) {
|
|
||||||
this.publicGames = publicGames;
|
|
||||||
|
|
||||||
// Calculate offset between server time and client time
|
|
||||||
if (this.publicGames) {
|
|
||||||
this.serverTimeOffset = this.publicGames.serverTime - Date.now();
|
|
||||||
}
|
|
||||||
// TODO: thihs is just a temporary scaffolding until PR #3191 is merged.
|
|
||||||
this.publicGames.games["ffa"]?.forEach((l) => {
|
|
||||||
if (!this.lobbyIDToStart.has(l.gameID)) {
|
|
||||||
// Convert server's startsAt to client time by subtracting offset
|
|
||||||
const startsAt = l.startsAt ?? Date.now();
|
|
||||||
this.lobbyIDToStart.set(l.gameID, startsAt - this.serverTimeOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (l.gameConfig && !this.mapImages.has(l.gameID)) {
|
|
||||||
this.loadMapImage(l.gameID, l.gameConfig.gameMap);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadMapImage(gameID: GameID, gameMap: string) {
|
|
||||||
try {
|
|
||||||
const mapType = gameMap as GameMapType;
|
|
||||||
const data = terrainMapFileLoader.getMapData(mapType);
|
|
||||||
this.mapImages.set(gameID, await data.webpPath());
|
|
||||||
this.requestUpdate();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load map image:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.publicGames) return html``;
|
|
||||||
|
|
||||||
const lobby = this.publicGames.games["ffa"]?.[0];
|
|
||||||
if (!lobby?.gameConfig) return html``;
|
|
||||||
|
|
||||||
const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0;
|
|
||||||
const timeRemaining = Math.max(0, Math.floor((start - Date.now()) / 1000));
|
|
||||||
const isStarting = timeRemaining <= 2;
|
|
||||||
const timeDisplay = renderDuration(timeRemaining);
|
|
||||||
|
|
||||||
const modeLabel = getGameModeLabel(lobby.gameConfig);
|
|
||||||
const modifierLabels = getModifierLabels(
|
|
||||||
lobby.gameConfig.publicGameModifiers,
|
|
||||||
);
|
|
||||||
const mapImageSrc = this.mapImages.get(lobby.gameID);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<button
|
|
||||||
@click=${() => this.lobbyClicked(lobby)}
|
|
||||||
class="group relative isolate flex flex-col w-full h-80 lg:h-96 overflow-hidden rounded-2xl transition-all duration-200 bg-[#3d7bab] hover:scale-[1.01] active:scale-[0.98] focus:outline-none focus-visible:ring-2 focus-visible:ring-white/50"
|
|
||||||
>
|
|
||||||
<div class="font-sans w-full h-full flex flex-col">
|
|
||||||
<!-- Main card gradient - stops before text -->
|
|
||||||
<div class="absolute inset-0 pointer-events-none z-10"></div>
|
|
||||||
|
|
||||||
<!-- Map Image Area with gradient overlay -->
|
|
||||||
<div class="flex-1 w-full relative overflow-hidden">
|
|
||||||
${mapImageSrc
|
|
||||||
? html`<img
|
|
||||||
src="${mapImageSrc}"
|
|
||||||
alt="${lobby.gameConfig.gameMap}"
|
|
||||||
class="absolute inset-0 w-full h-full object-cover object-center z-10"
|
|
||||||
/>`
|
|
||||||
: ""}
|
|
||||||
<!-- Vignette overlay for dark edges -->
|
|
||||||
<div class="pointer-events-none absolute inset-0 z-20"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mode Badge in top left -->
|
|
||||||
${modeLabel
|
|
||||||
? html`<span
|
|
||||||
class="absolute top-4 left-4 px-4 py-1 rounded font-bold text-sm lg:text-base uppercase tracking-widest z-30 bg-slate-800 text-white ring-1 ring-white/10 shadow-sm"
|
|
||||||
>
|
|
||||||
${modeLabel}
|
|
||||||
</span>`
|
|
||||||
: ""}
|
|
||||||
|
|
||||||
<!-- Timer in top right -->
|
|
||||||
${timeRemaining > 0
|
|
||||||
? html`
|
|
||||||
<span
|
|
||||||
class="absolute top-4 right-4 px-4 py-1 rounded font-bold text-sm lg:text-base tracking-widest z-30 bg-blue-600 text-white"
|
|
||||||
>
|
|
||||||
${timeDisplay}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: html`<span
|
|
||||||
class="absolute top-4 right-4 px-4 py-1 rounded font-bold text-sm lg:text-base uppercase tracking-widest z-30 bg-green-600 text-white"
|
|
||||||
>
|
|
||||||
${translateText("public_lobby.started")}
|
|
||||||
</span>`}
|
|
||||||
|
|
||||||
<!-- Content Banner -->
|
|
||||||
<div class="absolute bottom-0 left-0 right-0 z-20">
|
|
||||||
<!-- Modifier badges placed just above the gradient overlay -->
|
|
||||||
${modifierLabels.length > 0
|
|
||||||
? html`<div
|
|
||||||
class="absolute -top-8 left-4 z-30 flex gap-2 flex-wrap"
|
|
||||||
>
|
|
||||||
${modifierLabels.map(
|
|
||||||
(label) => html`
|
|
||||||
<span
|
|
||||||
class="px-2 py-0.5 rounded text-xs font-medium uppercase tracking-wide bg-purple-600 text-white"
|
|
||||||
>
|
|
||||||
${label}
|
|
||||||
</span>
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
</div>`
|
|
||||||
: html``}
|
|
||||||
|
|
||||||
<!-- Gradient overlay for text area - adds extra darkening -->
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 bg-gradient-to-b from-black/60 to-black/90 pointer-events-none"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div class="relative p-6 flex flex-col gap-2 text-left">
|
|
||||||
<!-- Header row: Status/Join on left, Player Count on right -->
|
|
||||||
<div class="flex items-center justify-between w-full">
|
|
||||||
<div class="text-base uppercase tracking-widest text-white">
|
|
||||||
${isStarting
|
|
||||||
? html`<span class="text-green-400 animate-pulse"
|
|
||||||
>${translateText("public_lobby.starting_game")}</span
|
|
||||||
>`
|
|
||||||
: html`${translateText("public_lobby.join")}`}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 text-white z-30">
|
|
||||||
<span class="text-base font-bold uppercase tracking-widest"
|
|
||||||
>${lobby.numClients}/${lobby.gameConfig.maxPlayers}</span
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-5 h-5 text-white"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Map Name - Full Width -->
|
|
||||||
<div
|
|
||||||
class="text-2xl lg:text-3xl font-bold text-white leading-none uppercase tracking-widest w-full"
|
|
||||||
>
|
|
||||||
${translateText(
|
|
||||||
`map.${normaliseMapKey(lobby.gameConfig.gameMap)}`,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- modifiers moved above gradient overlay -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public stop() {
|
|
||||||
this.lobbySocket.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private lobbyClicked(lobby: PublicGameInfo) {
|
|
||||||
// Validate username before opening the modal
|
|
||||||
const usernameInput = document.querySelector("username-input") as any;
|
|
||||||
if (
|
|
||||||
usernameInput &&
|
|
||||||
typeof usernameInput.isValid === "function" &&
|
|
||||||
!usernameInput.isValid()
|
|
||||||
) {
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent("show-message", {
|
|
||||||
detail: {
|
|
||||||
message: usernameInput.validationError,
|
|
||||||
color: "red",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent("show-public-lobby-modal", {
|
|
||||||
detail: { lobby } as ShowPublicLobbyModalEvent,
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -217,13 +217,11 @@ export class SinglePlayerModal extends BaseModal {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title: translateText("main.solo") || "Solo",
|
title: translateText("main.solo") || "Solo",
|
||||||
onBack: this.close,
|
onBack: () => this.close(),
|
||||||
ariaLabel: translateText("common.back"),
|
ariaLabel: translateText("common.back"),
|
||||||
rightContent: hasLinkedAccount(this.userMeResponse)
|
rightContent: hasLinkedAccount(this.userMeResponse)
|
||||||
? html`<button
|
? html`<button
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export class TerritoryPatternsModal extends BaseModal {
|
|||||||
return html`
|
return html`
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title: translateText("territory_patterns.title"),
|
title: translateText("territory_patterns.title"),
|
||||||
onBack: this.close,
|
onBack: () => this.close(),
|
||||||
ariaLabel: translateText("common.back"),
|
ariaLabel: translateText("common.back"),
|
||||||
rightContent: !hasLinkedAccount(this.userMeResponse)
|
rightContent: !hasLinkedAccount(this.userMeResponse)
|
||||||
? html`<div class="flex items-center">
|
? html`<div class="flex items-center">
|
||||||
@@ -259,11 +259,7 @@ export class TerritoryPatternsModal extends BaseModal {
|
|||||||
if (!this.isActive && !this.inline) return html``;
|
if (!this.isActive && !this.inline) return html``;
|
||||||
|
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col ${this.inline
|
|
||||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
|
||||||
: ""}"
|
|
||||||
>
|
|
||||||
${this.renderTabNavigation()}
|
${this.renderTabNavigation()}
|
||||||
<div class="overflow-y-auto pr-2 custom-scrollbar mr-1">
|
<div class="overflow-y-auto pr-2 custom-scrollbar mr-1">
|
||||||
${this.activeTab === "patterns"
|
${this.activeTab === "patterns"
|
||||||
|
|||||||
@@ -26,11 +26,7 @@ export class TokenLoginModal extends BaseModal {
|
|||||||
render() {
|
render() {
|
||||||
const title = translateText("token_login_modal.title");
|
const title = translateText("token_login_modal.title");
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col ${this.inline
|
|
||||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
|
|
||||||
: ""}"
|
|
||||||
>
|
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title,
|
title,
|
||||||
onBack: () => this.close(),
|
onBack: () => this.close(),
|
||||||
|
|||||||
@@ -30,11 +30,7 @@ export class TroubleshootingModal extends BaseModal {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full select-text flex flex-col ${this.inline
|
|
||||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
|
||||||
: ""}"
|
|
||||||
>
|
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
titleContent: html` <div
|
titleContent: html` <div
|
||||||
class="w-full flex flex-col sm:flex-row justify-between gap-2"
|
class="w-full flex flex-col sm:flex-row justify-between gap-2"
|
||||||
@@ -56,7 +52,7 @@ export class TroubleshootingModal extends BaseModal {
|
|||||||
${translateText("common.copy")}
|
${translateText("common.copy")}
|
||||||
</button>
|
</button>
|
||||||
</div>`,
|
</div>`,
|
||||||
onBack: this.close,
|
onBack: () => this.close(),
|
||||||
ariaLabel: translateText("common.back"),
|
ariaLabel: translateText("common.back"),
|
||||||
})}
|
})}
|
||||||
${this.loading
|
${this.loading
|
||||||
|
|||||||
@@ -394,20 +394,18 @@ export class UserSettingModal extends BaseModal {
|
|||||||
: this.renderKeybindSettings();
|
: this.renderKeybindSettings();
|
||||||
|
|
||||||
const content = html`
|
const content = html`
|
||||||
<div
|
<div class="${this.modalContainerClass}">
|
||||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="relative flex flex-col border-b border-white/10 pb-4 shrink-0"
|
class="relative flex flex-col border-b border-white/10 lg:pb-4 shrink-0"
|
||||||
>
|
>
|
||||||
${modalHeader({
|
${modalHeader({
|
||||||
title: translateText("user_setting.title"),
|
title: translateText("user_setting.title"),
|
||||||
onBack: this.close,
|
onBack: () => this.close(),
|
||||||
ariaLabel: translateText("common.back"),
|
ariaLabel: translateText("common.back"),
|
||||||
showDivider: true,
|
showDivider: true,
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div class="hidden md:flex items-center gap-2 justify-center mt-4">
|
<div class="hidden lg:flex items-center gap-2 justify-center mt-4">
|
||||||
<button
|
<button
|
||||||
class="px-6 py-2 text-xs font-bold transition-all duration-200 rounded-lg uppercase tracking-widest ${this
|
class="px-6 py-2 text-xs font-bold transition-all duration-200 rounded-lg uppercase tracking-widest ${this
|
||||||
.activeTab === "basic"
|
.activeTab === "basic"
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export class UsernameInput extends LitElement {
|
|||||||
@input=${this.handleClanTagChange}
|
@input=${this.handleClanTagChange}
|
||||||
placeholder="${translateText("username.tag")}"
|
placeholder="${translateText("username.tag")}"
|
||||||
maxlength="5"
|
maxlength="5"
|
||||||
class="w-[6rem] bg-transparent border-b border-white/20 text-white placeholder-white/30 text-xl font-bold text-center focus:outline-none focus:border-white/50 transition-colors uppercase shrink-0"
|
class="w-[6rem] text-xl font-bold text-center uppercase shrink-0 bg-transparent text-white placeholder-white/70 focus:placeholder-transparent border-0 border-b border-white/40 focus:outline-none focus:border-white/60"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -86,7 +86,7 @@ export class UsernameInput extends LitElement {
|
|||||||
@input=${this.handleUsernameChange}
|
@input=${this.handleUsernameChange}
|
||||||
placeholder="${translateText("username.enter_username")}"
|
placeholder="${translateText("username.enter_username")}"
|
||||||
maxlength="${MAX_USERNAME_LENGTH}"
|
maxlength="${MAX_USERNAME_LENGTH}"
|
||||||
class="flex-1 min-w-0 bg-transparent border-0 text-white placeholder-white/30 text-2xl font-bold text-left focus:outline-none focus:ring-0 transition-colors overflow-x-auto whitespace-nowrap text-ellipsis pr-2"
|
class="flex-1 min-w-0 border-0 text-2xl font-bold text-left text-white placeholder-white/70 focus:outline-none focus:ring-0 overflow-x-auto whitespace-nowrap text-ellipsis pr-2 bg-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
${this.validationError
|
${this.validationError
|
||||||
@@ -147,8 +147,9 @@ export class UsernameInput extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private validateAndStore() {
|
private validateAndStore() {
|
||||||
// Validate base username meets minimum length (clan tag doesn't count)
|
// Prevent empty username even if clan tag is present
|
||||||
if (this.baseUsername.trim().length < MIN_USERNAME_LENGTH) {
|
const trimmedBase = this.baseUsername.trim();
|
||||||
|
if (!trimmedBase || trimmedBase.length < MIN_USERNAME_LENGTH) {
|
||||||
this._isValid = false;
|
this._isValid = false;
|
||||||
this.validationError = translateText("username.too_short", {
|
this.validationError = translateText("username.too_short", {
|
||||||
min: MIN_USERNAME_LENGTH,
|
min: MIN_USERNAME_LENGTH,
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ export function normaliseMapKey(mapName: string): string {
|
|||||||
return mapName.toLowerCase().replace(/[\s.]+/g, "");
|
return mapName.toLowerCase().replace(/[\s.]+/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMapName(mapName: string | undefined): string | null {
|
||||||
|
if (!mapName) return null;
|
||||||
|
return translateText(`map.${normaliseMapKey(mapName)}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a display label for the game mode (e.g. "FFA", "4 Teams", "Duos").
|
* Returns a display label for the game mode (e.g. "FFA", "4 Teams", "Duos").
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LitElement } from "lit";
|
import { html, LitElement, TemplateResult } from "lit";
|
||||||
import { property, query, state } from "lit/decorators.js";
|
import { property, query, state } from "lit/decorators.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,11 +10,21 @@ import { property, query, state } from "lit/decorators.js";
|
|||||||
* - Automatic listener lifecycle management
|
* - Automatic listener lifecycle management
|
||||||
* - Common inline/modal element handling
|
* - Common inline/modal element handling
|
||||||
* - Shared open/close logic with hooks for custom behavior
|
* - Shared open/close logic with hooks for custom behavior
|
||||||
|
* - Standardized loading spinner UI
|
||||||
|
* - Consistent modal container styling
|
||||||
*/
|
*/
|
||||||
export abstract class BaseModal extends LitElement {
|
export abstract class BaseModal extends LitElement {
|
||||||
@state() protected isModalOpen = false;
|
@state() protected isModalOpen = false;
|
||||||
@property({ type: Boolean }) inline = false;
|
@property({ type: Boolean }) inline = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard modal container class string.
|
||||||
|
* Provides consistent dark glassmorphic styling across all modals.
|
||||||
|
* No rounding on mobile for full-screen appearance.
|
||||||
|
*/
|
||||||
|
protected readonly modalContainerClass =
|
||||||
|
"h-full flex flex-col overflow-hidden bg-black/70 backdrop-blur-xl lg:rounded-2xl lg:border border-white/10";
|
||||||
|
|
||||||
@query("o-modal") protected modalEl?: HTMLElement & {
|
@query("o-modal") protected modalEl?: HTMLElement & {
|
||||||
open: () => void;
|
open: () => void;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
@@ -121,4 +131,43 @@ export abstract class BaseModal extends LitElement {
|
|||||||
this.modalEl?.close();
|
this.modalEl?.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a standardized loading spinner with optional custom message.
|
||||||
|
* Use this for consistent loading states across all modals.
|
||||||
|
*
|
||||||
|
* @param message - Optional loading message text. Defaults to no message.
|
||||||
|
* @param spinnerColor - Optional spinner color. Defaults to 'blue'.
|
||||||
|
* @returns TemplateResult of the loading UI
|
||||||
|
*/
|
||||||
|
protected renderLoadingSpinner(
|
||||||
|
message?: string,
|
||||||
|
spinnerColor: "blue" | "green" | "yellow" | "white" = "blue",
|
||||||
|
): TemplateResult {
|
||||||
|
const colorClasses = {
|
||||||
|
blue: "border-blue-500/30 border-t-blue-500",
|
||||||
|
green: "border-green-500/30 border-t-green-500",
|
||||||
|
yellow: "border-yellow-500/30 border-t-yellow-500",
|
||||||
|
white: "border-white/20 border-t-white",
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center p-12 text-white h-full min-h-[400px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 border-4 ${colorClasses[
|
||||||
|
spinnerColor
|
||||||
|
]} rounded-full animate-spin mb-4"
|
||||||
|
></div>
|
||||||
|
${message
|
||||||
|
? html`<p
|
||||||
|
class="text-white/60 font-medium tracking-wide animate-pulse"
|
||||||
|
>
|
||||||
|
${message}
|
||||||
|
</p>`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class DesktopNavBar extends LitElement {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
window.addEventListener("showPage", this._onShowPage);
|
window.addEventListener("showPage", this._onShowPage);
|
||||||
|
|
||||||
const current = (window as any).currentPageId;
|
const current = window.currentPageId;
|
||||||
if (current) {
|
if (current) {
|
||||||
// Wait for render
|
// Wait for render
|
||||||
this.updateComplete.then(() => {
|
this.updateComplete.then(() => {
|
||||||
@@ -79,9 +79,12 @@ export class DesktopNavBar extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
window.currentPageId ??= "page-play";
|
||||||
|
const currentPage = window.currentPageId;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<nav
|
<nav
|
||||||
class="hidden lg:flex w-full bg-slate-950/70 backdrop-blur-md border-b border-white/10 items-center justify-center gap-8 py-4 shrink-0 transition-opacity z-50 relative"
|
class="hidden lg:flex w-full bg-slate-900 items-center justify-center gap-8 py-4 shrink-0 z-50 relative"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center justify-center">
|
<div class="flex flex-col items-center justify-center">
|
||||||
<div class="h-8 text-[#2563eb]">
|
<div class="h-8 text-[#2563eb]">
|
||||||
@@ -89,7 +92,7 @@ export class DesktopNavBar extends LitElement {
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 1364 259"
|
viewBox="0 0 1364 259"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="h-full w-auto drop-shadow-[0_0_10px_rgba(37,99,235,0.4)]"
|
class="h-full w-auto"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
|
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
|
||||||
@@ -131,20 +134,26 @@ export class DesktopNavBar extends LitElement {
|
|||||||
class="l-header__highlightText text-center"
|
class="l-header__highlightText text-center"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Desktop Navigation Menu Items -->
|
|
||||||
<button
|
<button
|
||||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
class="nav-menu-item ${currentPage === "page-play"
|
||||||
|
? "active"
|
||||||
|
: ""} text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||||
data-page="page-play"
|
data-page="page-play"
|
||||||
data-i18n="main.play"
|
data-i18n="main.play"
|
||||||
></button>
|
></button>
|
||||||
|
<!-- Desktop Navigation Menu Items -->
|
||||||
<button
|
<button
|
||||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
class="nav-menu-item ${currentPage === "page-news"
|
||||||
|
? "active"
|
||||||
|
: ""} text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||||
data-page="page-news"
|
data-page="page-news"
|
||||||
data-i18n="main.news"
|
data-i18n="main.news"
|
||||||
></button>
|
></button>
|
||||||
<div class="relative no-crazygames">
|
<div class="relative no-crazygames">
|
||||||
<button
|
<button
|
||||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
class="nav-menu-item ${currentPage === "page-item-store"
|
||||||
|
? "active"
|
||||||
|
: ""} text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||||
data-page="page-item-store"
|
data-page="page-item-store"
|
||||||
data-i18n="main.store"
|
data-i18n="main.store"
|
||||||
@click=${this.onStoreClick}
|
@click=${this.onStoreClick}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class Footer extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<footer
|
<footer
|
||||||
class="[.in-game_&]:hidden bg-black/60 backdrop-blur-md flex flex-col items-center justify-center gap-2 pt-[2px] pb-2 text-white/50 w-full border-t border-white/10 shrink-0 mt-auto"
|
class="[.in-game_&]:hidden bg-slate-950/70 backdrop-blur-md flex flex-col items-center justify-center gap-2 pt-[2px] pb-2 text-white/50 w-full border-t border-white/10 shrink-0 mt-auto"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-center gap-6 pt-2">
|
<div class="flex items-center justify-center gap-6 pt-2">
|
||||||
<a
|
<a
|
||||||
@@ -43,7 +43,7 @@ export class Footer extends LitElement {
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://discord.gg/jRpxXvG42t"
|
href="https://discord.gg/openfront"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class LobbyTeamView extends LitElement {
|
|||||||
|
|
||||||
willUpdate(changedProperties: Map<string, any>) {
|
willUpdate(changedProperties: Map<string, any>) {
|
||||||
// Recompute team preview when relevant properties change
|
// Recompute team preview when relevant properties change
|
||||||
// clients is 'changed' every 1s from pollPlayers, chose to not compare for actual change
|
// clients is updated from WebSocket lobby_info events
|
||||||
if (
|
if (
|
||||||
changedProperties.has("gameMode") ||
|
changedProperties.has("gameMode") ||
|
||||||
changedProperties.has("clients") ||
|
changedProperties.has("clients") ||
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ export class MainLayout extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<main
|
<main
|
||||||
class="relative [.in-game_&]:hidden flex flex-col flex-1 overflow-hidden w-full px-[clamp(1.5rem,3vw,3rem)] pt-[clamp(0.75rem,1.5vw,1.5rem)] pb-[clamp(0.75rem,1.5vw,1.5rem)]"
|
class="relative [.in-game_&]:hidden flex flex-col flex-1 overflow-hidden w-full px-0 lg:px-[clamp(1.5rem,3vw,3rem)] pt-0 lg:pt-[clamp(0.75rem,1.5vw,1.5rem)] pb-0 lg:pb-[clamp(0.75rem,1.5vw,1.5rem)]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-full max-w-[20cm] mx-auto flex flex-col flex-1 gap-[clamp(1.5rem,3vw,3rem)] overflow-y-auto overflow-x-hidden [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden lg:[scrollbar-width:auto] lg:[-ms-overflow-style:auto] lg:[&::-webkit-scrollbar]:block"
|
class="w-full lg:max-w-[20cm] mx-auto flex flex-col flex-1 gap-0 lg:gap-[clamp(1.5rem,3vw,3rem)] overflow-y-auto overflow-x-hidden"
|
||||||
>
|
>
|
||||||
${this._initialChildren}
|
${this._initialChildren}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class MobileNavBar extends LitElement {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
window.addEventListener("showPage", this._onShowPage);
|
window.addEventListener("showPage", this._onShowPage);
|
||||||
|
|
||||||
const current = (window as any).currentPageId;
|
const current = window.currentPageId;
|
||||||
if (current) {
|
if (current) {
|
||||||
this.updateComplete.then(() => {
|
this.updateComplete.then(() => {
|
||||||
this._updateActiveState(current);
|
this._updateActiveState(current);
|
||||||
@@ -40,6 +40,9 @@ export class MobileNavBar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
window.currentPageId ??= "page-play";
|
||||||
|
const currentPage = window.currentPageId;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<!-- Border Segments (Custom right border with gap for button) -->
|
<!-- Border Segments (Custom right border with gap for button) -->
|
||||||
<div
|
<div
|
||||||
@@ -52,7 +55,7 @@ export class MobileNavBar extends LitElement {
|
|||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex-1 w-full flex flex-col justify-start overflow-y-auto md:pt-[clamp(1rem,3vh,4rem)] md:pb-[clamp(0.5rem,2vh,2rem)] md:px-[clamp(1rem,1.5vw,2rem)] p-5 gap-[clamp(1rem,3vh,3rem)]"
|
class="flex-1 w-full flex flex-col justify-start overflow-y-auto lg:pt-[clamp(1rem,3vh,4rem)] lg:pb-[clamp(0.5rem,2vh,2rem)] lg:px-[clamp(1rem,1.5vw,2rem)] p-5 gap-[clamp(1rem,3vh,3rem)]"
|
||||||
>
|
>
|
||||||
<!-- Logo + Menu -->
|
<!-- Logo + Menu -->
|
||||||
<div
|
<div
|
||||||
@@ -110,7 +113,10 @@ export class MobileNavBar extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<!-- Mobile Navigation Menu Items -->
|
<!-- Mobile Navigation Menu Items -->
|
||||||
<button
|
<button
|
||||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] ${currentPage ===
|
||||||
|
"page-play"
|
||||||
|
? "active"
|
||||||
|
: ""}"
|
||||||
data-page="page-play"
|
data-page="page-play"
|
||||||
data-i18n="main.play"
|
data-i18n="main.play"
|
||||||
></button>
|
></button>
|
||||||
|
|||||||
+106
-111
@@ -11,136 +11,131 @@ export class PlayPage extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
id="page-play"
|
id="page-play"
|
||||||
class="flex flex-col gap-2 w-full max-w-6xl mx-auto px-0 sm:px-4 transition-all duration-300 my-auto min-h-0"
|
class="flex flex-col gap-2 w-full lg:max-w-6xl mx-auto px-0 lg:px-4 lg:my-auto min-h-0"
|
||||||
>
|
>
|
||||||
<token-login class="absolute"></token-login>
|
<token-login class="absolute"></token-login>
|
||||||
|
|
||||||
<!-- Header / Identity Section -->
|
<!-- Mobile: Fixed top bar -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-2 lg:gap-6 w-full">
|
<div
|
||||||
|
class="lg:hidden fixed left-0 right-0 top-0 z-40 pt-[env(safe-area-inset-top)] bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] border-b border-white/10"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="lg:col-span-9 flex flex-row flex-nowrap gap-x-2 h-[60px] items-center bg-slate-900/80 backdrop-blur-md p-3 rounded-xl relative z-20 text-sm sm:text-base shrink-0"
|
class="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center h-14 px-2 gap-2"
|
||||||
>
|
>
|
||||||
<!-- Flag -->
|
<button
|
||||||
<div
|
id="hamburger-btn"
|
||||||
class="h-[40px] sm:h-[50px] shrink-0 aspect-[4/3] flex items-center justify-center lg:hidden"
|
class="col-start-1 justify-self-start h-10 shrink-0 aspect-[4/3] flex text-white/90 rounded-md items-center justify-center transition-colors"
|
||||||
|
data-i18n-aria-label="main.menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="sidebar-menu"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
data-i18n-title="main.menu"
|
||||||
>
|
>
|
||||||
<!-- Hamburger (Mobile) -->
|
<svg
|
||||||
<button
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
id="hamburger-btn"
|
fill="none"
|
||||||
class="lg:hidden flex w-full h-full bg-slate-800/40 text-white/90 hover:bg-slate-700/40 p-0 rounded-md items-center justify-center cursor-pointer transition-all duration-200"
|
viewBox="0 0 24 24"
|
||||||
data-i18n-aria-label="main.menu"
|
stroke-width="1.5"
|
||||||
aria-expanded="false"
|
stroke="currentColor"
|
||||||
aria-controls="sidebar-menu"
|
class="size-8"
|
||||||
aria-haspopup="dialog"
|
|
||||||
data-i18n-title="main.menu"
|
|
||||||
>
|
>
|
||||||
<svg
|
<path
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
stroke-linecap="round"
|
||||||
fill="none"
|
stroke-linejoin="round"
|
||||||
viewBox="0 0 24 24"
|
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||||
stroke-width="1.5"
|
/>
|
||||||
stroke="currentColor"
|
</svg>
|
||||||
class="w-8 h-8"
|
</button>
|
||||||
>
|
|
||||||
<path
|
<div
|
||||||
stroke-linecap="round"
|
class="col-start-2 flex items-center justify-center text-[#2563eb] min-w-0"
|
||||||
stroke-linejoin="round"
|
>
|
||||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
<svg
|
||||||
/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</svg>
|
viewBox="0 0 1364 259"
|
||||||
</button>
|
fill="currentColor"
|
||||||
|
class="h-6 w-auto drop-shadow-[0_0_10px_rgba(37,99,235,0.3)] shrink-0"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M189.1,154.78v17.07h-16.9v16.75h-51.07v-16.42h-16.9v-17.07h-16.97v-84.88h16.63v-17.07h16.9v-16.84h51.07v16.5h17.07v17.07h16.7v84.89h-16.54ZM137.87,53.1v17.15h-16.6v84.86h16.97v16.61h16.89v-16.97h16.6v-84.86h-16.97v-16.79h-16.89Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M273.91,104.06v-16.73h50.92v16.45h16.85v68.05h-16.44v17.06h-50.96v16.88h16.4v16.96h-67.28v-16.61h16.33v-101.86h-16.38v-16.98h33.4v16.63c6.12,0,11.72,0,17.31,0,0,22.56,0,45.13,0,67.75h33.59v-67.61h-33.73Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M631.12,188.64v-16.36h16.53V53.2h-16.25v-16.86h118.33v33.29h-16.65v-16.36h-50.72v50.44h33.36v-16.35h16.99v50.25h-16.6v-16.33h-33.73v50.65h16.37v16.72h-67.63Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M596.78,103.8v84.94h-33.54v-84.39h-34.03v84.25h-33.85v-101.29h84.5v16.49h16.93Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M1107.12,188.71v-84.34h-34.03v84.37h-33.7v-101.41h84.42v16.41h16.86v84.96h-33.54Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M988.1,171.78v16.87h-67.88v-16.38h-16.87v-68.06h16.38v-16.87h68.06v16.38h16.87v68.06h-16.55ZM970.78,104.35h-33.39v67.38h33.39v-67.38Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M460.77,155.38v16.49h-16.58v16.83h-68.05v-16.5h-16.83v-68.05h16.49v-16.83h68.05v16.49h16.83v34.06h-67.31v33.82h33.47v-16.31h33.92ZM393.39,104.18v16.56h33.3v-16.56h-33.3Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M1209.13,172h-16.9v-67.9h-16.96v-16.9h16.68v-17.08h16.9v-16.82h16.9v33.58h50.98v16.91h-50.4v67.96h16.48v-16.43h50.95v16.54h-16.55v16.76h-68.08v-16.6Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M834.91,120.94v16.96h-16.65v33.88h16.41v16.96h-67.29v-16.63h16.34v-67.87h-16.4v-16.97h50.42v33.81h17.3l-.14-.14Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M835.05,121.08v-33.75h33.76v16.43h16.85v33.96h-33.43v-16.79c-6.13,0-11.73,0-17.32,0,0,0,.14.14.14.14Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Username -->
|
<div
|
||||||
<div class="flex-1 min-w-0 h-[40px] sm:h-[50px] flex items-center">
|
aria-hidden="true"
|
||||||
|
class="col-start-3 justify-self-end h-10 shrink-0 aspect-[4/3]"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="w-full pb-4 lg:pb-0 flex flex-col gap-0 lg:grid lg:grid-cols-12 lg:gap-2"
|
||||||
|
>
|
||||||
|
<!-- Mobile: spacer for fixed top bar -->
|
||||||
|
<div class="lg:hidden h-[calc(env(safe-area-inset-top)+56px)]"></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="px-2 py-2 bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] border-y border-white/10 overflow-visible lg:col-span-9 lg:flex lg:items-center lg:gap-x-2 lg:h-[60px] lg:p-3 lg:relative lg:z-20 lg:border-y-0 lg:rounded-xl"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 min-w-0 w-full">
|
||||||
<username-input
|
<username-input
|
||||||
class="relative w-full h-full block text-ellipsis overflow-hidden whitespace-nowrap"
|
class="flex-1 min-w-0 h-10 lg:h-[50px]"
|
||||||
></username-input>
|
></username-input>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pattern button (Mobile - inside bar, Desktop - hidden here) -->
|
|
||||||
<pattern-input
|
|
||||||
id="pattern-input-mobile"
|
|
||||||
show-select-label
|
|
||||||
class="aspect-square h-[50px] sm:h-[50px] lg:hidden shrink-0"
|
|
||||||
></pattern-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pattern & Flag buttons (Desktop only - separate column) -->
|
|
||||||
<div class="hidden lg:flex lg:col-span-3">
|
|
||||||
<div class="w-full h-[60px] flex gap-2">
|
|
||||||
<pattern-input
|
<pattern-input
|
||||||
id="pattern-input-desktop"
|
id="pattern-input-mobile"
|
||||||
show-select-label
|
show-select-label
|
||||||
class="flex-1 h-full"
|
adaptive-size
|
||||||
|
class="shrink-0 lg:hidden"
|
||||||
></pattern-input>
|
></pattern-input>
|
||||||
<flag-input
|
|
||||||
id="flag-input-desktop"
|
|
||||||
show-select-label
|
|
||||||
class="flex-1 h-full"
|
|
||||||
></flag-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Primary Game Actions Area -->
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 w-full">
|
|
||||||
<!-- Left Column: Featured Lobbies / Quick Play -->
|
|
||||||
<div class="lg:col-span-9 flex flex-col gap-6 min-w-0">
|
|
||||||
<!-- Public Lobby Card -->
|
|
||||||
<public-lobby
|
|
||||||
class="block w-full transition-all duration-[50ms]"
|
|
||||||
></public-lobby>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: Custom Games & Modes -->
|
|
||||||
<div class="lg:col-span-3">
|
|
||||||
<div
|
|
||||||
class="group relative isolate flex flex-col w-full h-40 lg:h-96 overflow-hidden rounded-2xl transition-all duration-300"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="h-full flex flex-col bg-slate-900/40 backdrop-blur-sm rounded-2xl overflow-hidden"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="py-2 bg-blue-900/20 text-center text-sm font-bold text-gray-300 uppercase tracking-widest"
|
|
||||||
data-i18n="host_modal.label"
|
|
||||||
></div>
|
|
||||||
<div class="flex-1 p-2 flex flex-row lg:flex-col gap-2">
|
|
||||||
<o-button
|
|
||||||
id="single-player"
|
|
||||||
data-i18n-title="main.solo"
|
|
||||||
translationKey="main.solo"
|
|
||||||
fill
|
|
||||||
class="flex-1 transition-transform"
|
|
||||||
></o-button>
|
|
||||||
|
|
||||||
<o-button
|
|
||||||
id="host-lobby-button"
|
|
||||||
data-i18n-title="main.create"
|
|
||||||
translationKey="main.create"
|
|
||||||
fill
|
|
||||||
secondary
|
|
||||||
class="flex-1 opacity-90 hover:opacity-100"
|
|
||||||
></o-button>
|
|
||||||
|
|
||||||
<o-button
|
|
||||||
id="join-private-lobby-button"
|
|
||||||
data-i18n-title="main.join"
|
|
||||||
translationKey="main.join"
|
|
||||||
fill
|
|
||||||
secondary
|
|
||||||
class="flex-1 opacity-90 hover:opacity-100"
|
|
||||||
></o-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Matchmaking Buttons (Full Width across entire grid) -->
|
<div class="hidden lg:flex lg:col-span-3 h-[60px] gap-2">
|
||||||
<div class="lg:col-span-12 flex flex-col gap-6">
|
<pattern-input
|
||||||
<matchmaking-button></matchmaking-button>
|
id="pattern-input-desktop"
|
||||||
|
show-select-label
|
||||||
|
class="flex-1 h-full"
|
||||||
|
></pattern-input>
|
||||||
|
<flag-input
|
||||||
|
id="flag-input-desktop"
|
||||||
|
show-select-label
|
||||||
|
class="flex-1 h-full"
|
||||||
|
></flag-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<game-mode-selector></game-mode-selector>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { html } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators.js";
|
||||||
|
import { UserMeResponse } from "../../core/ApiSchemas";
|
||||||
|
import { getUserMe, hasLinkedAccount } from "../Api";
|
||||||
|
import { userAuth } from "../Auth";
|
||||||
|
import { translateText } from "../Utils";
|
||||||
|
import { BaseModal } from "./BaseModal";
|
||||||
|
import { modalHeader } from "./ui/ModalHeader";
|
||||||
|
|
||||||
|
@customElement("ranked-modal")
|
||||||
|
export class RankedModal extends BaseModal {
|
||||||
|
@state() private elo: number | string = "...";
|
||||||
|
@state() private userMeResponse: UserMeResponse | false = false;
|
||||||
|
@state() private errorMessage: string | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.id = "page-ranked";
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
document.addEventListener(
|
||||||
|
"userMeResponse",
|
||||||
|
this.handleUserMeResponse as EventListener,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
document.removeEventListener(
|
||||||
|
"userMeResponse",
|
||||||
|
this.handleUserMeResponse as EventListener,
|
||||||
|
);
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleUserMeResponse = (
|
||||||
|
event: CustomEvent<UserMeResponse | false>,
|
||||||
|
) => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
this.userMeResponse = event.detail;
|
||||||
|
this.updateElo();
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateElo() {
|
||||||
|
if (this.errorMessage) {
|
||||||
|
this.elo = translateText("map_component.error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLinkedAccount(this.userMeResponse)) {
|
||||||
|
this.elo =
|
||||||
|
this.userMeResponse &&
|
||||||
|
this.userMeResponse.player.leaderboard?.oneVone?.elo
|
||||||
|
? this.userMeResponse.player.leaderboard.oneVone.elo
|
||||||
|
: translateText("matchmaking_modal.no_elo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async onOpen(): Promise<void> {
|
||||||
|
this.elo = "...";
|
||||||
|
this.errorMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userMe = await getUserMe();
|
||||||
|
this.userMeResponse = userMe;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch user profile for ranked modal", error);
|
||||||
|
this.userMeResponse = false;
|
||||||
|
this.errorMessage = translateText("map_component.error");
|
||||||
|
this.elo = translateText("map_component.error");
|
||||||
|
} finally {
|
||||||
|
this.updateElo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const content = html`
|
||||||
|
<div class="${this.modalContainerClass}">
|
||||||
|
${modalHeader({
|
||||||
|
title: translateText("mode_selector.ranked_title"),
|
||||||
|
onBack: () => this.close(),
|
||||||
|
ariaLabel: translateText("common.back"),
|
||||||
|
})}
|
||||||
|
<div class="flex-1 min-h-0 overflow-y-auto custom-scrollbar p-6">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
${this.renderCard(
|
||||||
|
translateText("mode_selector.ranked_1v1_title"),
|
||||||
|
this.errorMessage ??
|
||||||
|
(hasLinkedAccount(this.userMeResponse)
|
||||||
|
? translateText("matchmaking_modal.elo", { elo: this.elo })
|
||||||
|
: translateText("mode_selector.ranked_title")),
|
||||||
|
() => this.handleRanked(),
|
||||||
|
)}
|
||||||
|
${this.renderDisabledCard(
|
||||||
|
translateText("mode_selector.ranked_2v2_title"),
|
||||||
|
translateText("mode_selector.coming_soon"),
|
||||||
|
)}
|
||||||
|
${this.renderDisabledCard(
|
||||||
|
translateText("mode_selector.coming_soon"),
|
||||||
|
"",
|
||||||
|
)}
|
||||||
|
${this.renderDisabledCard(
|
||||||
|
translateText("mode_selector.coming_soon"),
|
||||||
|
"",
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (this.inline) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<o-modal ?hideHeader=${true} ?hideCloseButton=${true}>
|
||||||
|
${content}
|
||||||
|
</o-modal>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCard(title: string, subtitle: string, onClick: () => void) {
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
@click=${onClick}
|
||||||
|
class="flex flex-col w-full h-28 sm:h-32 rounded-2xl bg-[color-mix(in_oklab,var(--frenchBlue)_70%,black)] border-0 transition-transform hover:scale-[1.02] active:scale-[0.98] p-6 items-center justify-center gap-3"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center gap-1 text-center">
|
||||||
|
<h3
|
||||||
|
class="text-lg sm:text-xl font-bold text-white uppercase tracking-widest leading-tight"
|
||||||
|
>
|
||||||
|
${title}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
class="text-xs text-white/60 uppercase tracking-wider whitespace-pre-line leading-tight"
|
||||||
|
>
|
||||||
|
${subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDisabledCard(title: string, subtitle: string) {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="group relative isolate flex flex-col w-full h-28 sm:h-32 overflow-hidden rounded-2xl bg-slate-900/40 backdrop-blur-md border-0 shadow-none p-6 items-center justify-center gap-3 opacity-50 cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center gap-1 text-center">
|
||||||
|
<h3
|
||||||
|
class="text-lg sm:text-xl font-bold text-white/60 uppercase tracking-widest leading-tight"
|
||||||
|
>
|
||||||
|
${title}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
class="text-xs text-white/40 uppercase tracking-wider whitespace-pre-line leading-tight"
|
||||||
|
>
|
||||||
|
${subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRanked() {
|
||||||
|
if ((await userAuth()) === false) {
|
||||||
|
this.close();
|
||||||
|
window.showPage?.("page-account");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.dispatchEvent(new CustomEvent("open-matchmaking"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ export class OModal extends LitElement {
|
|||||||
|
|
||||||
const wrapperClass = this.inline
|
const wrapperClass = this.inline
|
||||||
? "relative flex flex-col w-full h-full m-0 max-w-full max-h-none shadow-none"
|
? "relative flex flex-col w-full h-full m-0 max-w-full max-h-none shadow-none"
|
||||||
: `relative flex flex-col w-[90%] min-w-[400px] max-w-[900px] m-8 rounded-lg shadow-[0_20px_60px_rgba(0,0,0,0.8)] max-h-[calc(100vh-4rem)] ${
|
: `relative flex flex-col w-full h-full lg:w-[90%] lg:h-auto lg:min-w-[400px] lg:max-w-[900px] lg:m-8 lg:rounded-lg shadow-[0_20px_60px_rgba(0,0,0,0.8)] lg:max-h-[calc(100vh-4rem)] ${
|
||||||
this.alwaysMaximized ? "h-auto" : ""
|
this.alwaysMaximized ? "h-auto" : ""
|
||||||
}`;
|
}`;
|
||||||
const wrapperStyle =
|
const wrapperStyle =
|
||||||
@@ -101,7 +101,7 @@ export class OModal extends LitElement {
|
|||||||
</div>`
|
</div>`
|
||||||
: html``}
|
: html``}
|
||||||
<section
|
<section
|
||||||
class="relative flex-1 min-h-0 p-[1.4rem] text-white bg-[#23232382] backdrop-blur-md rounded-lg overflow-y-auto"
|
class="relative flex-1 min-h-0 p-0 lg:p-[1.4rem] text-white bg-[#23232382] backdrop-blur-md lg:rounded-lg overflow-y-auto"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ export class SettingSlider extends LitElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="flex flex-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 ${rainbowClass}"
|
class="flex flex-col sm:flex-row sm:items-center sm:justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-3 sm:gap-4 ${rainbowClass}"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col flex-1 min-w-0 mr-4">
|
<div class="flex flex-col flex-1 min-w-0 sm:mr-4">
|
||||||
<label class="text-white font-bold text-base block mb-1"
|
<label class="text-white font-bold text-base block mb-1"
|
||||||
>${this.label}</label
|
>${this.label}</label
|
||||||
>
|
>
|
||||||
@@ -59,21 +59,28 @@ export class SettingSlider extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-end gap-2 shrink-0 w-[200px]">
|
<div
|
||||||
<span class="text-white font-bold text-sm">${this.value}%</span>
|
class="flex flex-col items-start sm:items-end gap-2 shrink-0 w-full sm:w-[200px]"
|
||||||
<input
|
>
|
||||||
type="range"
|
<div class="flex items-center gap-2 w-full">
|
||||||
class="w-full appearance-none h-2 bg-transparent rounded outline-none
|
<input
|
||||||
|
type="range"
|
||||||
|
class="flex-1 w-auto appearance-none h-2 bg-transparent rounded outline-none
|
||||||
[&::-webkit-slider-runnable-track]:h-2 [&::-webkit-slider-runnable-track]:rounded [&::-webkit-slider-runnable-track]:bg-[image:linear-gradient(to_right,#3b82f6_0%,#3b82f6_var(--fill),rgba(255,255,255,0.1)_var(--fill),rgba(255,255,255,0.1)_100%)]
|
[&::-webkit-slider-runnable-track]:h-2 [&::-webkit-slider-runnable-track]:rounded [&::-webkit-slider-runnable-track]:bg-[image:linear-gradient(to_right,#3b82f6_0%,#3b82f6_var(--fill),rgba(255,255,255,0.1)_var(--fill),rgba(255,255,255,0.1)_100%)]
|
||||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-[18px] [&::-webkit-slider-thumb]:w-[18px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:-mt-[6px] [&::-webkit-slider-thumb]:shadow-[0_0_0_4px_rgba(59,130,246,0.2)] [&::-webkit-slider-thumb]:transition-all active:[&::-webkit-slider-thumb]:scale-110 active:[&::-webkit-slider-thumb]:shadow-[0_0_0_6px_rgba(59,130,246,0.3)]
|
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-[18px] [&::-webkit-slider-thumb]:w-[18px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:-mt-[6px] [&::-webkit-slider-thumb]:shadow-[0_0_0_4px_rgba(59,130,246,0.2)] [&::-webkit-slider-thumb]:transition-all active:[&::-webkit-slider-thumb]:scale-110 active:[&::-webkit-slider-thumb]:shadow-[0_0_0_6px_rgba(59,130,246,0.3)]
|
||||||
[&::-moz-range-track]:h-2 [&::-moz-range-track]:rounded [&::-moz-range-track]:bg-white/10
|
[&::-moz-range-track]:h-2 [&::-moz-range-track]:rounded [&::-moz-range-track]:bg-white/10
|
||||||
[&::-moz-range-progress]:h-2 [&::-moz-range-progress]:rounded [&::-moz-range-progress]:bg-blue-500
|
[&::-moz-range-progress]:h-2 [&::-moz-range-progress]:rounded [&::-moz-range-progress]:bg-blue-500
|
||||||
[&::-moz-range-thumb]:h-[18px] [&::-moz-range-thumb]:w-[18px] [&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-blue-500 [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:shadow-[0_0_0_4px_rgba(59,130,246,0.2)] [&::-moz-range-thumb]:transition-all active:[&::-moz-range-thumb]:scale-110 active:[&::-moz-range-thumb]:shadow-[0_0_0_6px_rgba(59,130,246,0.3)]"
|
[&::-moz-range-thumb]:h-[18px] [&::-moz-range-thumb]:w-[18px] [&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-blue-500 [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:shadow-[0_0_0_4px_rgba(59,130,246,0.2)] [&::-moz-range-thumb]:transition-all active:[&::-moz-range-thumb]:scale-110 active:[&::-moz-range-thumb]:shadow-[0_0_0_6px_rgba(59,130,246,0.3)]"
|
||||||
min=${this.min}
|
min=${this.min}
|
||||||
max=${this.max}
|
max=${this.max}
|
||||||
.value=${String(this.value)}
|
.value=${String(this.value)}
|
||||||
@input=${this.handleInput}
|
@input=${this.handleInput}
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
class="text-white font-bold text-sm shrink-0 text-right min-w-[3ch]"
|
||||||
|
>${this.value}%</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ export class GameList extends LitElement {
|
|||||||
>
|
>
|
||||||
${translateText("game_list.mode")}:
|
${translateText("game_list.mode")}:
|
||||||
${game.mode === GameMode.FFA
|
${game.mode === GameMode.FFA
|
||||||
? translateText("game_list.mode_ffa")
|
? translateText("game_mode.ffa")
|
||||||
: html`${translateText("game_list.mode_team")}`}
|
: html`${translateText("game_mode.teams")}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ export class PlayerStatsTreeView extends LitElement {
|
|||||||
|
|
||||||
private labelForMode(m: GameMode) {
|
private labelForMode(m: GameMode) {
|
||||||
return m === GameMode.FFA
|
return m === GameMode.FFA
|
||||||
? translateText("player_stats_tree.mode_ffa")
|
? translateText("game_mode.ffa")
|
||||||
: translateText("player_stats_tree.mode_team");
|
: translateText("game_mode.teams");
|
||||||
}
|
}
|
||||||
|
|
||||||
createRenderRoot() {
|
createRenderRoot() {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export class MapDisplay extends LitElement {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const mapValue = GameMapType[this.mapKey as keyof typeof GameMapType];
|
const mapValue = GameMapType[this.mapKey as keyof typeof GameMapType];
|
||||||
const data = terrainMapFileLoader.getMapData(mapValue);
|
const data = terrainMapFileLoader.getMapData(mapValue);
|
||||||
this.mapWebpPath = await data.webpPath();
|
this.mapWebpPath = data.webpPath;
|
||||||
const manifest = await data.manifest();
|
const manifest = await data.manifest();
|
||||||
this.mapName = manifest.name;
|
this.mapName = manifest.name;
|
||||||
this.hasNations =
|
this.hasNations =
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ export interface ModalHeaderProps {
|
|||||||
|
|
||||||
const DEFAULT_WRAPPER_CLASS = "flex flex-wrap items-center gap-2 shrink-0";
|
const DEFAULT_WRAPPER_CLASS = "flex flex-wrap items-center gap-2 shrink-0";
|
||||||
const DEFAULT_DIVIDER_CLASS = "border-b border-white/10";
|
const DEFAULT_DIVIDER_CLASS = "border-b border-white/10";
|
||||||
const DEFAULT_PADDING_CLASS = "p-6";
|
const DEFAULT_PADDING_CLASS = "p-4 lg:p-6";
|
||||||
const DEFAULT_LEFT_CLASS = "flex items-center gap-4 flex-1";
|
const DEFAULT_LEFT_CLASS = "flex items-center gap-4 flex-1";
|
||||||
const DEFAULT_BUTTON_CLASS =
|
const DEFAULT_BUTTON_CLASS =
|
||||||
"group flex items-center justify-center w-10 h-10 rounded-full shrink-0 " +
|
"group flex items-center justify-center w-10 h-10 rounded-full shrink-0 " +
|
||||||
"bg-white/5 hover:bg-white/10 transition-all border border-white/10";
|
"bg-white/5 hover:bg-white/10 transition-all border border-white/10";
|
||||||
const DEFAULT_TITLE_CLASS =
|
const DEFAULT_TITLE_CLASS =
|
||||||
"text-white text-xl sm:text-2xl md:text-3xl font-bold uppercase " +
|
"text-white text-xl lg:text-2xl font-bold uppercase " +
|
||||||
"tracking-widest break-words hyphens-auto";
|
"tracking-widest break-words hyphens-auto";
|
||||||
|
|
||||||
const withClasses = (...classes: Array<string | undefined>) =>
|
const withClasses = (...classes: Array<string | undefined>) =>
|
||||||
|
|||||||
+25
-2
@@ -57,8 +57,24 @@ body {
|
|||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
scrollbar-width: auto;
|
}
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
|
||||||
|
/* Hide scrollbar on mobile, show on larger screens */
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
* {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
* {
|
||||||
|
scrollbar-width: auto;
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add custom scrollbar styles */
|
/* Add custom scrollbar styles */
|
||||||
@@ -66,6 +82,13 @@ body {
|
|||||||
width: 12px;
|
width: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -6,22 +6,28 @@
|
|||||||
--boxBackgroundColor: #111827cc;
|
--boxBackgroundColor: #111827cc;
|
||||||
--fontColor: #202020;
|
--fontColor: #202020;
|
||||||
--fontColorLight: #fff;
|
--fontColorLight: #fff;
|
||||||
--primaryColor: #2563eb;
|
|
||||||
--primaryColorHover: #1d4ed8;
|
/* Palette: Deep French Blue / Muted Cyan / Black / Forest Teal */
|
||||||
|
--frenchBlue: #1f3a70; /* Deeper French Blue */
|
||||||
|
--cyanBlue: #0f6ca3; /* Muted Cyan secondary */
|
||||||
|
--tealAccent: #1f6c5a; /* Darker Teal accent */
|
||||||
|
|
||||||
|
--primaryColor: var(--frenchBlue);
|
||||||
|
--primaryColorHover: var(--tealAccent);
|
||||||
--primaryColorDisabled: linear-gradient(
|
--primaryColorDisabled: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
rgb(74, 74, 74),
|
rgb(74, 74, 74),
|
||||||
rgb(61, 61, 61)
|
rgb(61, 61, 61)
|
||||||
);
|
);
|
||||||
--secondaryColor: #dbeafe;
|
--secondaryColor: var(--cyanBlue);
|
||||||
--secondaryColorHover: #bfdbfe;
|
--secondaryColorHover: var(--cyanBlue);
|
||||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
--primaryColorDark: #3b82f6;
|
--primaryColorDark: var(--frenchBlue);
|
||||||
--primaryColorHoverDark: #2563eb;
|
--primaryColorHoverDark: var(--tealAccent);
|
||||||
--primaryColorDisabledDark: #4b5563;
|
--primaryColorDisabledDark: #4b5563;
|
||||||
--secondaryColorDark: #374151;
|
--secondaryColorDark: var(--tealAccent);
|
||||||
--secondaryColorHoverDark: #4b5563;
|
--secondaryColorHoverDark: var(--frenchBlue);
|
||||||
--fontColorDark: #f3f4f6;
|
--fontColorDark: #f3f4f6;
|
||||||
|
|
||||||
/* Achievements */
|
/* Achievements */
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ export class BinaryLoaderGameMapLoader implements GameMapLoader {
|
|||||||
return res.json() as Promise<MapManifest>;
|
return res.json() as Promise<MapManifest>;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
webpPath: this.createLazyLoader(() =>
|
webpPath: `${mapBasePath}/thumbnail.webp`,
|
||||||
Promise.resolve(`${mapBasePath}/thumbnail.webp`),
|
|
||||||
),
|
|
||||||
} satisfies MapData;
|
} satisfies MapData;
|
||||||
|
|
||||||
this.maps.set(map, mapData);
|
this.maps.set(map, mapData);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class FetchGameMapLoader implements GameMapLoader {
|
|||||||
map4xBin: () => this.loadBinaryFromUrl(this.url(fileName, "map4x.bin")),
|
map4xBin: () => this.loadBinaryFromUrl(this.url(fileName, "map4x.bin")),
|
||||||
map16xBin: () => this.loadBinaryFromUrl(this.url(fileName, "map16x.bin")),
|
map16xBin: () => this.loadBinaryFromUrl(this.url(fileName, "map16x.bin")),
|
||||||
manifest: () => this.loadJsonFromUrl(this.url(fileName, "manifest.json")),
|
manifest: () => this.loadJsonFromUrl(this.url(fileName, "manifest.json")),
|
||||||
webpPath: async () => this.url(fileName, "thumbnail.webp"),
|
webpPath: this.url(fileName, "thumbnail.webp"),
|
||||||
} satisfies MapData;
|
} satisfies MapData;
|
||||||
|
|
||||||
this.maps.set(map, mapData);
|
this.maps.set(map, mapData);
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ export interface MapData {
|
|||||||
map4xBin: () => Promise<Uint8Array>;
|
map4xBin: () => Promise<Uint8Array>;
|
||||||
map16xBin: () => Promise<Uint8Array>;
|
map16xBin: () => Promise<Uint8Array>;
|
||||||
manifest: () => Promise<MapManifest>;
|
manifest: () => Promise<MapManifest>;
|
||||||
webpPath: () => Promise<string>;
|
webpPath: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user