mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-02 01:33:39 +00:00
Merge branch 'main' into bomb-confirmation
This commit is contained in:
@@ -157,7 +157,7 @@ export class AccountModal extends LitElement {
|
||||
return html`
|
||||
<button
|
||||
@click="${this.handleLogout}"
|
||||
class="px-6 py-3 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
|
||||
class="px-6 py-3 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
|
||||
>
|
||||
Log Out
|
||||
</button>
|
||||
@@ -172,7 +172,7 @@ export class AccountModal extends LitElement {
|
||||
<div class="mb-6">
|
||||
<button
|
||||
@click="${this.handleDiscordLogin}"
|
||||
class="w-full px-6 py-3 text-sm font-medium text-white bg-[#5865F2] border border-transparent rounded-md hover:bg-[#4752C4] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#5865F2] transition-colors duration-200 flex items-center justify-center space-x-2"
|
||||
class="w-full px-6 py-3 text-sm font-medium text-white bg-[#5865F2] border border-transparent rounded-md hover:bg-[#4752C4] focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-[#5865F2] transition-colors duration-200 flex items-center justify-center gap-2"
|
||||
>
|
||||
<img
|
||||
src="/images/DiscordLogo.svg"
|
||||
@@ -209,23 +209,23 @@ export class AccountModal extends LitElement {
|
||||
name="email"
|
||||
.value="${this.email}"
|
||||
@input="${this.handleEmailInput}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-xs focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black"
|
||||
placeholder="Enter your email address"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
@click="${this.close}"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="${this.handleSubmit}"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
@@ -233,7 +233,7 @@ export class AccountModal extends LitElement {
|
||||
</div>
|
||||
<button
|
||||
@click="${this.handleLogout}"
|
||||
class="px-3 py-1 text-xs font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
|
||||
class="px-3 py-1 text-xs font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
|
||||
>
|
||||
${translateText("account_modal.clear_session")}
|
||||
</button>
|
||||
@@ -376,10 +376,10 @@ export class AccountButton extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="fixed top-4 right-4 z-[9998]">
|
||||
<div class="fixed top-4 right-4 z-9998">
|
||||
<button
|
||||
@click="${this.open}"
|
||||
class="w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-3xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
|
||||
class="w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-2xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-hidden focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
|
||||
title="${buttonTitle}"
|
||||
>
|
||||
${this.renderIcon()}
|
||||
|
||||
@@ -35,7 +35,7 @@ export class DarkModeButton extends LitElement {
|
||||
return html`
|
||||
<button
|
||||
title="Toggle Dark Mode"
|
||||
class="absolute top-0 left-0 md:top-[10px] md:left-[10px] border-none bg-none cursor-pointer text-2xl"
|
||||
class="absolute top-0 left-0 md:top-2.5 md:left-2.5 border-none bg-none cursor-pointer text-2xl"
|
||||
@click=${() => this.toggleDarkMode()}
|
||||
>
|
||||
${this.darkMode ? "☀️" : "🌙"}
|
||||
|
||||
@@ -73,16 +73,14 @@ export class FlagInput extends LitElement {
|
||||
<div class="flex relative">
|
||||
<button
|
||||
id="flag-input_"
|
||||
class="border rounded-lg flex cursor-pointer border-black/30
|
||||
class="w-full border rounded-lg flex cursor-pointer border-black/30
|
||||
dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]
|
||||
"
|
||||
justify-center aspect-square"
|
||||
title=${translateText("flag_input.button_title")}
|
||||
>
|
||||
<span
|
||||
id="flag-preview"
|
||||
style="display:inline-block;
|
||||
vertical-align:middle; background:#333; border-radius:6px;
|
||||
overflow:hidden;"
|
||||
class="block w-full aspect-3/2 bg-[#333] overflow-hidden rounded-md"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -20,10 +20,10 @@ export class FlagInputModal extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<o-modal alwaysMaximized title=${translateText("flag_input.title")}>
|
||||
<div class="flex justify-center w-full p-[1rem]">
|
||||
<div class="flex justify-center w-full p-4">
|
||||
<input
|
||||
class="h-[2rem] border-none border border-gray-300
|
||||
rounded-xl shadow-sm text-2xl text-center focus:outline-none
|
||||
class="h-8 border-none border border-gray-300
|
||||
rounded-xl shadow-xs text-2xl text-center focus:outline-hidden
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black
|
||||
dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
|
||||
type="text"
|
||||
@@ -34,7 +34,7 @@ export class FlagInputModal extends LitElement {
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-wrap justify-evenly gap-[1rem] overflow-y-auto overflow-x-hidden h-[90%]"
|
||||
class="flex flex-wrap justify-evenly gap-4 overflow-y-auto overflow-x-hidden h-[90%]"
|
||||
>
|
||||
${this.isModalOpen
|
||||
? Countries.filter(
|
||||
@@ -47,10 +47,10 @@ export class FlagInputModal extends LitElement {
|
||||
this.setFlag(country.code);
|
||||
this.close();
|
||||
}}
|
||||
class="text-center cursor-pointer border-none bg-none opacity-70
|
||||
w-[calc(100%/2-15px)] sm:w-[calc(100%/4-15px)]
|
||||
md:w-[calc(100%/6-15px)] lg:w-[calc(100%/8-15px)]
|
||||
xl:w-[calc(100%/10-15px)] min-w-[80px]"
|
||||
class="text-center cursor-pointer border-none bg-none opacity-70
|
||||
w-[calc(100%/2-15px)] sm:w-[calc(100%/4-15px)]
|
||||
md:w-[calc(100%/6-15px)] lg:w-[calc(100%/8-15px)]
|
||||
xl:w-[calc(100%/10-15px)] min-w-20"
|
||||
>
|
||||
<img
|
||||
class="country-flag w-full h-auto"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { GameEndInfo } from "../core/Schemas";
|
||||
import { GameMapType } from "../core/game/Game";
|
||||
import { GameMapType, hasUnusualThumbnailSize } from "../core/game/Game";
|
||||
import { fetchGameById } from "./Api";
|
||||
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
@@ -49,10 +49,8 @@ export class GameInfoModal extends LitElement {
|
||||
title="${translateText("game_info_modal.title")}"
|
||||
translationKey="main.game_info"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center pl-[100px] pr-[100px] text-center mb-4"
|
||||
>
|
||||
<div class="w-[300px] sm:w-[500px]">
|
||||
<div class="flex flex-col items-center px-25 text-center mb-4">
|
||||
<div class="w-75 sm:w-125">
|
||||
${this.isLoadingGame
|
||||
? this.renderLoadingAnimation()
|
||||
: this.renderRanking()}
|
||||
@@ -107,15 +105,17 @@ export class GameInfoModal extends LitElement {
|
||||
if (!info) {
|
||||
return html``;
|
||||
}
|
||||
const isUnusualThumbnailSize = hasUnusualThumbnailSize(info.config.gameMap);
|
||||
return html`
|
||||
<div
|
||||
class="h-[150px] flex relative justify-between rounded-xl bg-blue-600 items-center"
|
||||
class="h-37.5 flex relative justify-between rounded-xl bg-blue-600 items-center"
|
||||
>
|
||||
${this.mapImage
|
||||
? html`<img
|
||||
src="${this.mapImage}"
|
||||
class="absolute place-self-start col-span-full row-span-full h-full rounded-xl"
|
||||
style="mask-image: linear-gradient(to left, transparent, #fff)"
|
||||
class="absolute place-self-start col-span-full row-span-full h-full rounded-xl mask-[linear-gradient(to_left,transparent,#fff)] ${isUnusualThumbnailSize
|
||||
? "object-cover object-center"
|
||||
: ""}"
|
||||
/>`
|
||||
: html`<div
|
||||
class="place-self-start col-span-full row-span-full h-full rounded-xl bg-gray-300"
|
||||
|
||||
@@ -55,8 +55,7 @@ export class GoogleAdElement extends LitElement {
|
||||
return html`
|
||||
<div class="google-ad-container">
|
||||
<ins
|
||||
class="adsbygoogle"
|
||||
style="display:block"
|
||||
class="adsbygoogle block"
|
||||
data-ad-client="${this.adClient}"
|
||||
data-ad-slot="${this.adSlot}"
|
||||
data-ad-format="${this.adFormat}"
|
||||
|
||||
@@ -417,8 +417,7 @@ export class HelpModal extends LitElement {
|
||||
<li class="mb-4">
|
||||
<img
|
||||
src="/images/InfoIcon.svg"
|
||||
class="inline-block icon"
|
||||
style="fill: white; background: transparent;"
|
||||
class="inline-block icon fill-white bg-transparent"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span>${translateText("help_modal.radial_info")}</span>
|
||||
@@ -614,7 +613,7 @@ export class HelpModal extends LitElement {
|
||||
class="flex flex-col items-center w-full md:w-1/3 mb-2 md:mb-0"
|
||||
>
|
||||
<div
|
||||
class="text-gray-300 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
|
||||
class="text-gray-300 flex flex-col justify-start min-h-12 w-full px-2 mb-1"
|
||||
>
|
||||
${translateText("help_modal.icon_crown")}
|
||||
</div>
|
||||
@@ -631,7 +630,7 @@ export class HelpModal extends LitElement {
|
||||
class="flex flex-col items-center w-full md:w-1/3 mb-2 md:mb-0"
|
||||
>
|
||||
<div
|
||||
class="text-gray-300 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
|
||||
class="text-gray-300 flex flex-col justify-start min-h-12 w-full px-2 mb-1"
|
||||
>
|
||||
${translateText("help_modal.icon_traitor")}
|
||||
</div>
|
||||
@@ -648,7 +647,7 @@ export class HelpModal extends LitElement {
|
||||
class="flex flex-col items-center w-full md:w-1/3 mb-2 md:mb-0"
|
||||
>
|
||||
<div
|
||||
class="text-gray-300 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
|
||||
class="text-gray-300 flex flex-col justify-start min-h-12 w-full px-2 mb-1"
|
||||
>
|
||||
${translateText("help_modal.icon_ally")}
|
||||
</div>
|
||||
@@ -667,7 +666,7 @@ export class HelpModal extends LitElement {
|
||||
class="flex flex-col items-center w-full md:w-1/3 mb-2 md:mb-0"
|
||||
>
|
||||
<div
|
||||
class="text-gray-300 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
|
||||
class="text-gray-300 flex flex-col justify-start min-h-12 w-full px-2 mb-1"
|
||||
>
|
||||
${translateText("help_modal.icon_embargo")}
|
||||
</div>
|
||||
@@ -684,7 +683,7 @@ export class HelpModal extends LitElement {
|
||||
class="flex flex-col items-center w-full md:w-1/3 mb-2 md:mb-0"
|
||||
>
|
||||
<div
|
||||
class="text-gray-300 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
|
||||
class="text-gray-300 flex flex-col justify-start min-h-12 w-full px-2 mb-1"
|
||||
>
|
||||
${translateText("help_modal.icon_request")}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
UnitType,
|
||||
mapCategories,
|
||||
} from "../core/game/Game";
|
||||
import { getCompactMapNationCount } from "../core/game/NationCreation";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import {
|
||||
ClientInfo,
|
||||
@@ -97,12 +98,11 @@ export class HostLobbyModal extends LitElement {
|
||||
${
|
||||
this.lobbyIdVisible
|
||||
? html`<svg
|
||||
class="visibility-icon"
|
||||
class="visibility-icon mr-2 cursor-pointer"
|
||||
@click=${() => {
|
||||
this.lobbyIdVisible = !this.lobbyIdVisible;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
style="margin-right: 8px; cursor: pointer;"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
@@ -116,12 +116,11 @@ export class HostLobbyModal extends LitElement {
|
||||
></path>
|
||||
</svg>`
|
||||
: html`<svg
|
||||
class="visibility-icon"
|
||||
class="visibility-icon mr-2 cursor-pointer"
|
||||
@click=${() => {
|
||||
this.lobbyIdVisible = !this.lobbyIdVisible;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
style="margin-right: 8px; cursor: pointer;"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
@@ -146,12 +145,12 @@ export class HostLobbyModal extends LitElement {
|
||||
</svg>`
|
||||
}
|
||||
<!-- Lobby ID (conditionally shown) -->
|
||||
<span class="lobby-id" @click=${this.copyToClipboard} style="cursor: pointer;">
|
||||
<span class="lobby-id cursor-pointer" @click=${this.copyToClipboard}>
|
||||
${this.lobbyIdVisible ? this.lobbyId : "••••••••"}
|
||||
</span>
|
||||
|
||||
<!-- Copy icon/success indicator -->
|
||||
<div @click=${this.copyToClipboard} style="margin-left: 8px; cursor: pointer;">
|
||||
<div @click=${this.copyToClipboard} class="cursor-pointer ml-2">
|
||||
${
|
||||
this.copySuccess
|
||||
? html`<span class="copy-success-icon">✓</span>`
|
||||
@@ -225,7 +224,7 @@ export class HostLobbyModal extends LitElement {
|
||||
<img
|
||||
src=${randomMap}
|
||||
alt="Random Map"
|
||||
style="width:100%; aspect-ratio: 4/2; object-fit:cover; border-radius:8px;"
|
||||
class="w-full aspect-2/1 object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="option-card-title">
|
||||
@@ -516,8 +515,8 @@ export class HostLobbyModal extends LitElement {
|
||||
id="end-timer-value"
|
||||
min="0"
|
||||
max="120"
|
||||
.value=${String(this.maxTimerValue ?? 0)}
|
||||
style="width: 60px; color: black; text-align: right; border-radius: 8px;"
|
||||
.value=${String(this.maxTimerValue ?? "")}
|
||||
class="w-15 text-black text-right rounded-lg"
|
||||
@input=${this.handleMaxTimerValueChanges}
|
||||
@keydown=${this.handleMaxTimerValueKeyDown}
|
||||
/>`
|
||||
@@ -526,7 +525,6 @@ export class HostLobbyModal extends LitElement {
|
||||
${translateText("host_modal.max_timer")}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
for="spawn-immunity"
|
||||
class="option-card ${this.spawnImmunity ? "selected" : ""}"
|
||||
@@ -566,17 +564,17 @@ export class HostLobbyModal extends LitElement {
|
||||
<span>${translateText("host_modal.player_immunity_duration")}</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<hr style="width: 100%; border-top: 1px solid #444; margin: 16px 0;" />
|
||||
|
||||
<hr class="w-full border-t border-t-[#444] my-4" />
|
||||
|
||||
<!-- Individual disables for structures/weapons -->
|
||||
<div
|
||||
style="margin: 8px 0 12px 0; font-weight: bold; color: #ccc; text-align: center;"
|
||||
class="mt-2 mb-3 font-bold text-[#ccc] text-center"
|
||||
>
|
||||
${translateText("host_modal.enables_title")}
|
||||
</div>
|
||||
<div
|
||||
style="display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;"
|
||||
class="flex flex-wrap justify-center gap-3"
|
||||
>
|
||||
${renderUnitTypeOptions({
|
||||
disabledUnits: this.disabledUnits,
|
||||
@@ -597,7 +595,7 @@ export class HostLobbyModal extends LitElement {
|
||||
? translateText("host_modal.player")
|
||||
: translateText("host_modal.players")
|
||||
}
|
||||
<span style="margin: 0 8px;">•</span>
|
||||
<span class="mx-2">•</span>
|
||||
${this.getEffectiveNationCount()}
|
||||
${
|
||||
this.getEffectiveNationCount() === 1
|
||||
@@ -947,6 +945,7 @@ export class HostLobbyModal extends LitElement {
|
||||
/**
|
||||
* Returns the effective nation count for display purposes.
|
||||
* In HumansVsNations mode, this equals the number of human players.
|
||||
* For compact maps, only 25% of nations are used.
|
||||
* Otherwise, it uses the manifest nation count (or 0 if nations are disabled).
|
||||
*/
|
||||
private getEffectiveNationCount(): number {
|
||||
@@ -956,7 +955,7 @@ export class HostLobbyModal extends LitElement {
|
||||
if (this.gameMode === GameMode.Team && this.teamCount === HumansVsNations) {
|
||||
return this.clients.length;
|
||||
}
|
||||
return this.nationCount;
|
||||
return getCompactMapNationCount(this.nationCount, this.compactMap);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export class LangSelector extends LitElement {
|
||||
@state() private languageList: any[] = [];
|
||||
@state() private showModal: boolean = false;
|
||||
@state() private debugMode: boolean = false;
|
||||
@state() isVisible = true;
|
||||
|
||||
private debugKeyPressed: boolean = false;
|
||||
private languageMetadata: LanguageMetadata[] = metadata;
|
||||
@@ -195,6 +196,7 @@ export class LangSelector extends LitElement {
|
||||
"o-modal",
|
||||
"o-button",
|
||||
"territory-patterns-modal",
|
||||
"fluent-slider",
|
||||
];
|
||||
|
||||
document.title = this.translateText("main.title") ?? document.title;
|
||||
@@ -247,7 +249,16 @@ export class LangSelector extends LitElement {
|
||||
await this.loadLanguageList();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.showModal = false;
|
||||
this.isVisible = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
}
|
||||
const currentLang =
|
||||
this.languageList.find((l) => l.code === this.currentLang) ??
|
||||
(this.currentLang === "debug"
|
||||
|
||||
@@ -64,10 +64,10 @@ export class LanguageModal extends LitElement {
|
||||
|
||||
return html`
|
||||
<aside
|
||||
class="fixed p-4 z-[9999] inset-0 bg-black/50 overflow-y-auto flex items-center justify-center"
|
||||
class="fixed p-4 z-9999 inset-0 bg-black/50 overflow-y-auto flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800/80 dark:bg-gray-900/90 backdrop-blur-md rounded-lg min-w-[340px] max-w-[480px] w-full"
|
||||
class="bg-gray-800/80 dark:bg-gray-900/90 backdrop-blur-md rounded-lg min-w-85 max-w-120 w-full"
|
||||
>
|
||||
<header
|
||||
class="relative rounded-t-md text-lg bg-black/60 dark:bg-black/80 text-center text-white px-6 py-4 pr-10"
|
||||
|
||||
@@ -560,6 +560,7 @@ class Client {
|
||||
"stats-button",
|
||||
"token-login",
|
||||
"matchmaking-modal",
|
||||
"lang-selector",
|
||||
].forEach((tag) => {
|
||||
const modal = document.querySelector(tag) as HTMLElement & {
|
||||
close?: () => void;
|
||||
|
||||
@@ -170,10 +170,10 @@ export class MatchmakingButton extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="z-[9999]">
|
||||
<div class="z-9999">
|
||||
<button
|
||||
@click="${this.open}"
|
||||
class="w-full h-16 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-3xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
|
||||
class="w-full h-16 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-2xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-hidden focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
|
||||
title="${translateText("matchmaking_modal.title")}"
|
||||
>
|
||||
Matchmaking
|
||||
|
||||
@@ -158,11 +158,11 @@ export class NewsButton extends LitElement {
|
||||
return html`
|
||||
<div class="flex relative">
|
||||
<button
|
||||
class="border p-[4px] rounded-lg flex cursor-pointer border-black/30 dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
|
||||
class="border p-1 rounded-lg flex cursor-pointer border-black/30 dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
|
||||
@click=${this.openNewsModel}
|
||||
>
|
||||
<img
|
||||
class="size-[48px] dark:invert"
|
||||
class="size-12 dark:invert"
|
||||
src="${megaphone}"
|
||||
alt=${translateText("news.title")}
|
||||
/>
|
||||
|
||||
+49
-14
@@ -1,11 +1,13 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { renderDuration, translateText } from "../client/Utils";
|
||||
import {
|
||||
Duos,
|
||||
GameMapType,
|
||||
GameMode,
|
||||
hasUnusualThumbnailSize,
|
||||
HumansVsNations,
|
||||
PublicGameModifiers,
|
||||
Quads,
|
||||
Trios,
|
||||
} from "../core/game/Game";
|
||||
@@ -113,7 +115,14 @@ export class PublicLobby extends LitElement {
|
||||
: `${modeLabel} ${teamDetailLabel}`;
|
||||
}
|
||||
|
||||
const modifierLabel = this.getModifierLabels(
|
||||
lobby.gameConfig.publicGameModifiers,
|
||||
);
|
||||
|
||||
const mapImageSrc = this.mapImages.get(lobby.gameID);
|
||||
const isUnusualThumbnailSize = hasUnusualThumbnailSize(
|
||||
lobby.gameConfig.gameMap,
|
||||
);
|
||||
|
||||
return html`
|
||||
<button
|
||||
@@ -121,8 +130,8 @@ export class PublicLobby extends LitElement {
|
||||
?disabled=${this.isButtonDebounced}
|
||||
class="isolate grid h-40 grid-cols-[100%] grid-rows-[100%] place-content-stretch w-full overflow-hidden ${this
|
||||
.isLobbyHighlighted
|
||||
? "bg-gradient-to-r from-green-600 to-green-500"
|
||||
: "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${this
|
||||
? "bg-linear-to-r via-none from-green-600 to-green-500"
|
||||
: "bg-linear-to-r via-none from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${this
|
||||
.isButtonDebounced
|
||||
? "opacity-70 cursor-not-allowed"
|
||||
: ""}"
|
||||
@@ -131,8 +140,9 @@ export class PublicLobby extends LitElement {
|
||||
? html`<img
|
||||
src="${mapImageSrc}"
|
||||
alt="${lobby.gameConfig.gameMap}"
|
||||
class="place-self-start col-span-full row-span-full h-full -z-10"
|
||||
style="mask-image: linear-gradient(to left, transparent, #fff)"
|
||||
class="place-self-start col-span-full row-span-full h-full -z-10 mask-[linear-gradient(to_left,transparent,#fff)] ${isUnusualThumbnailSize
|
||||
? "object-cover object-center"
|
||||
: ""}"
|
||||
/>`
|
||||
: html`<div
|
||||
class="place-self-start col-span-full row-span-full h-full -z-10 bg-gray-300"
|
||||
@@ -151,16 +161,25 @@ export class PublicLobby extends LitElement {
|
||||
.join("")}`
|
||||
: translateText("public_lobby.join")}
|
||||
</div>
|
||||
<div class="text-md font-medium text-white-400">
|
||||
${fullModeLabel
|
||||
? html`<span
|
||||
class="text-sm ${this.isLobbyHighlighted
|
||||
? "text-green-600"
|
||||
: "text-blue-600"} bg-white rounded-sm px-1 ml-1"
|
||||
>${fullModeLabel}</span
|
||||
>`
|
||||
: ""}
|
||||
<div
|
||||
class="text-md font-medium text-white-400 flex flex-wrap justify-end items-center gap-1"
|
||||
>
|
||||
<span
|
||||
class="text-sm whitespace-nowrap ${this.isLobbyHighlighted
|
||||
? "text-green-600"
|
||||
: "text-blue-600"} bg-white rounded-xs px-1"
|
||||
>${fullModeLabel}</span
|
||||
>
|
||||
${modifierLabel.map(
|
||||
(label) =>
|
||||
html`<span
|
||||
class="text-sm whitespace-nowrap ${this.isLobbyHighlighted
|
||||
? "text-green-600"
|
||||
: "text-blue-600"} bg-white rounded-xs px-1"
|
||||
>${label}</span
|
||||
>`,
|
||||
)}
|
||||
<span class="whitespace-nowrap"
|
||||
>${translateText(
|
||||
`map.${lobby.gameConfig.gameMap.toLowerCase().replace(/[\s.]+/g, "")}`,
|
||||
)}</span
|
||||
@@ -291,6 +310,22 @@ export class PublicLobby extends LitElement {
|
||||
return { label: null, isFullLabel: false };
|
||||
}
|
||||
|
||||
private getModifierLabels(
|
||||
publicGameModifiers: PublicGameModifiers | undefined,
|
||||
): string[] {
|
||||
if (!publicGameModifiers) {
|
||||
return [];
|
||||
}
|
||||
const labels: string[] = [];
|
||||
if (publicGameModifiers.isRandomSpawn) {
|
||||
labels.push(translateText("public_game_modifier.random_spawn"));
|
||||
}
|
||||
if (publicGameModifiers.isCompact) {
|
||||
labels.push(translateText("public_game_modifier.compact_map"));
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
private lobbyClicked(lobby: GameInfo) {
|
||||
if (this.isButtonDebounced) return;
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
<img
|
||||
src=${randomMap}
|
||||
alt="Random Map"
|
||||
style="width:100%; aspect-ratio: 4/2; object-fit:cover; border-radius:8px;"
|
||||
class="w-full aspect-2/1 object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="option-card-title">
|
||||
@@ -457,7 +457,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
min="0"
|
||||
max="120"
|
||||
.value=${String(this.maxTimerValue ?? "")}
|
||||
style="width: 60px; color: black; text-align: right; border-radius: 8px;"
|
||||
class="w-15 text-black text-right rounded-lg"
|
||||
@input=${this.handleMaxTimerValueChanges}
|
||||
@keydown=${this.handleMaxTimerValueKeyDown}
|
||||
/>`}
|
||||
@@ -467,17 +467,11 @@ export class SinglePlayerModal extends LitElement {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<hr
|
||||
style="width: 100%; border-top: 1px solid #444; margin: 16px 0;"
|
||||
/>
|
||||
<div
|
||||
style="margin: 8px 0 12px 0; font-weight: bold; color: #ccc; text-align: center;"
|
||||
>
|
||||
<hr class="w-full border-t border-t-[#444] my-4" />
|
||||
<div class="mt-2 mb-3 font-bold text-[#ccc] text-center">
|
||||
${translateText("single_modal.enables_title")}
|
||||
</div>
|
||||
<div
|
||||
style="display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;"
|
||||
>
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
${renderUnitTypeOptions({
|
||||
disabledUnits: this.disabledUnits,
|
||||
toggleUnit: this.toggleUnit.bind(this),
|
||||
|
||||
@@ -91,7 +91,7 @@ export class StatsModal extends LitElement {
|
||||
<div class="flex flex-col items-center justify-center p-6 text-white">
|
||||
<p class="mb-4 text-center">${this.error}</p>
|
||||
<button
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm font-medium"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-sm text-sm font-medium"
|
||||
@click=${() => this.loadLeaderboard()}
|
||||
>
|
||||
Retry
|
||||
@@ -222,10 +222,10 @@ export class StatsButton extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="fixed top-20 right-4 z-[9998]">
|
||||
<div class="fixed top-20 right-4 z-9998">
|
||||
<button
|
||||
@click="${this.open}"
|
||||
class="w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-3xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
|
||||
class="w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-2xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-hidden focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
|
||||
title="${translateText("stats_modal.title")}"
|
||||
>
|
||||
<img src="/icons/stats.svg" alt="Stats" class="w-6 h-6" />
|
||||
|
||||
@@ -140,10 +140,7 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
? this.renderMySkinsButton()
|
||||
: this.renderNotLoggedInWarning()}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap gap-4 p-2"
|
||||
style="justify-content: center; align-items: flex-start;"
|
||||
>
|
||||
<div class="flex flex-wrap gap-4 p-2 justify-center items-start">
|
||||
${this.affiliateCode === null
|
||||
? html`
|
||||
<pattern-button
|
||||
@@ -193,8 +190,8 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
${hexCodes.map(
|
||||
(hexCode) => html`
|
||||
<div
|
||||
class="w-12 h-12 rounded-lg border-2 border-white/30 cursor-pointer transition-all duration-200 hover:scale-110 hover:shadow-lg"
|
||||
style="background-color: ${hexCode};"
|
||||
class="w-12 h-12 rounded-lg border-2 border-white/30 bg-(--bg) cursor-pointer transition-all duration-200 hover:scale-110 hover:shadow-lg"
|
||||
style="--bg: ${hexCode};"
|
||||
title="${hexCode}"
|
||||
@click=${() => this.selectColor(hexCode)}
|
||||
></div>
|
||||
@@ -267,8 +264,8 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
class="rounded"
|
||||
style="width: ${width}px; height: ${height}px; background-color: ${hexCode};"
|
||||
class="rounded-sm size-(--size) bg-(--bg)"
|
||||
style="--size: ${width}px; --bg: ${hexCode};"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -40,12 +40,12 @@ export class UsernameInput extends LitElement {
|
||||
@change=${this.handleChange}
|
||||
placeholder="${translateText("username.enter_username")}"
|
||||
maxlength="${MAX_USERNAME_LENGTH}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-xl shadow-sm text-2xl text-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-xl shadow-xs text-2xl text-center focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
${this.validationError
|
||||
? html`<div
|
||||
id="username-validation-error"
|
||||
class="absolute z-10 w-full mt-2 px-3 py-1 text-lg border rounded bg-white text-red-600 border-red-600 dark:bg-gray-700 dark:text-red-300 dark:border-red-300"
|
||||
class="absolute z-10 w-full mt-2 px-3 py-1 text-lg border rounded-sm bg-white text-red-600 border-red-600 dark:bg-gray-700 dark:text-red-300 dark:border-red-300"
|
||||
>
|
||||
${this.validationError}
|
||||
</div>`
|
||||
|
||||
@@ -83,7 +83,7 @@ export class LobbyTeamView extends LitElement {
|
||||
this.clients,
|
||||
(c) => c.clientID ?? c.username,
|
||||
(client) =>
|
||||
html`<div class="px-2 py-1 rounded bg-gray-700/70 mb-1 text-xs">
|
||||
html`<div class="px-2 py-1 rounded-sm bg-gray-700/70 mb-1 text-xs">
|
||||
${client.username}
|
||||
</div>`,
|
||||
)}
|
||||
@@ -162,9 +162,9 @@ export class LobbyTeamView extends LitElement {
|
||||
class="px-2 py-1 font-bold flex items-center justify-between text-white rounded-t-xl text-[13px] gap-2 bg-gray-700/70"
|
||||
>
|
||||
${this.showTeamColors
|
||||
? html`<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full border-2 border-white/90 shadow-inner"
|
||||
style="background:${this.teamHeaderColor(preview.team)};"
|
||||
? html` <span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full border-2 border-white/90 shadow-inner bg-(--bg)"
|
||||
style="--bg:${this.teamHeaderColor(preview.team)};"
|
||||
></span>`
|
||||
: null}
|
||||
<span class="truncate">${preview.team}</span>
|
||||
@@ -180,7 +180,7 @@ export class LobbyTeamView extends LitElement {
|
||||
(p) => p.clientID ?? p.username,
|
||||
(p) =>
|
||||
html` <div
|
||||
class="bg-gray-700/70 px-2 py-1 rounded text-xs flex items-center justify-between"
|
||||
class="bg-gray-700/70 px-2 py-1 rounded-sm text-xs flex items-center justify-between"
|
||||
>
|
||||
<span class="truncate">${p.username}</span>
|
||||
${p.clientID === this.lobbyCreatorClientID
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { Difficulty, GameMapType } from "../../core/game/Game";
|
||||
import {
|
||||
Difficulty,
|
||||
GameMapType,
|
||||
hasUnusualThumbnailSize,
|
||||
} from "../../core/game/Game";
|
||||
import { terrainMapFileLoader } from "../TerrainMapFileLoader";
|
||||
import { translateText } from "../Utils";
|
||||
|
||||
@@ -48,6 +52,7 @@ export const MapDescription: Record<keyof typeof GameMapType, string> = {
|
||||
StraitOfHormuz: "Strait of Hormuz",
|
||||
Surrounded: "Surrounded",
|
||||
Didier: "Didier",
|
||||
AmazonRiver: "Amazon River",
|
||||
};
|
||||
|
||||
@customElement("map-display")
|
||||
@@ -153,6 +158,14 @@ export class MapDisplay extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const mapType = GameMapType[this.mapKey as keyof typeof GameMapType];
|
||||
const isUnusualThumbnailSize = mapType
|
||||
? hasUnusualThumbnailSize(mapType)
|
||||
: false;
|
||||
const objectFitStyle = isUnusualThumbnailSize
|
||||
? "object-fit: cover; object-position: center;"
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<div class="option-card ${this.selected ? "selected" : ""}">
|
||||
${this.isLoading
|
||||
@@ -164,6 +177,7 @@ export class MapDisplay extends LitElement {
|
||||
src="${this.mapWebpPath}"
|
||||
alt="${this.mapKey}"
|
||||
class="option-image"
|
||||
style="${objectFitStyle}"
|
||||
/>`
|
||||
: html`<div class="option-image">Error</div>`}
|
||||
${this.showMedals
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("modal-overlay")
|
||||
export class ModalOverlay extends LitElement {
|
||||
|
||||
@@ -70,7 +70,7 @@ export class PatternButton extends LitElement {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="flex flex-col items-center gap-1 p-1 bg-white/10 rounded-lg max-w-[200px]"
|
||||
class="flex flex-col items-center gap-1 p-1 bg-white/10 rounded-lg max-w-50"
|
||||
>
|
||||
<button
|
||||
class="bg-white/90 border-2 border-black/10 rounded-lg cursor-pointer transition-all duration-200 w-full
|
||||
@@ -98,8 +98,7 @@ export class PatternButton extends LitElement {
|
||||
`
|
||||
: null}
|
||||
<div
|
||||
class="w-[120px] h-[120px] flex items-center justify-center bg-white rounded p-1 mx-auto"
|
||||
style="overflow: hidden;"
|
||||
class="size-30 flex items-center justify-center bg-white rounded-sm p-1 mx-auto overflow-hidden"
|
||||
>
|
||||
${renderPatternPreview(
|
||||
this.pattern !== null
|
||||
@@ -143,43 +142,27 @@ export function renderPatternPreview(
|
||||
return html`<img
|
||||
src="${generatePreviewDataUrl(pattern, width, height)}"
|
||||
alt="Pattern preview"
|
||||
class="w-full h-full object-contain"
|
||||
style="image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges;"
|
||||
<!-- pixelated should also handle crisp-edges -->
|
||||
class="w-full h-full object-contain [image-rendering:pixelated]"
|
||||
/>`;
|
||||
}
|
||||
|
||||
function renderBlankPreview(width: number, height: number): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
class="flex items-center justify-center bg-white rounded-sm box-border overflow-hidden relative border border-[#ccc] w-(--width) h-(--height)"
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: ${height}px;
|
||||
width: ${width}px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
--height: ${height}px;
|
||||
--width: ${width}px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 0; width: calc(100% - 1px); height: calc(100% - 2px); box-sizing: border-box;"
|
||||
class="grid grid-cols-2 grid-rows-2 gap-0 w-[calc(100%-1px)] h-[calc(100%-2px)] box-border"
|
||||
>
|
||||
<div
|
||||
style="background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.1); box-sizing: border-box;"
|
||||
></div>
|
||||
<div
|
||||
style="background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.1); box-sizing: border-box;"
|
||||
></div>
|
||||
<div
|
||||
style="background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.1); box-sizing: border-box;"
|
||||
></div>
|
||||
<div
|
||||
style="background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.1); box-sizing: border-box;"
|
||||
></div>
|
||||
<div class="bg-white border border-black/10 box-border"></div>
|
||||
<div class="bg-white border border-black/10 box-border"></div>
|
||||
<div class="bg-white border border-black/10 box-border"></div>
|
||||
<div class="bg-white border border-black/10 box-border"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -22,16 +22,18 @@ export class PlayerRow extends LitElement {
|
||||
const visibleBorder = player.winner || this.currentPlayer;
|
||||
return html`
|
||||
<li
|
||||
class="bg-gradient-to-r ${player.winner
|
||||
? "from-sky-400 to-blue-700"
|
||||
: "bg-slate-700"} border-[2px]
|
||||
class="${player.winner
|
||||
? "bg-linear-to-r via-none from-sky-400 to-blue-700"
|
||||
: "bg-slate-700"} border-2
|
||||
${player.winner
|
||||
? "border-yellow-500"
|
||||
: "border-yellow-50"} ${visibleBorder ? "" : "border-opacity-0"}
|
||||
relative pt-1 pb-1 pr-2 pl-2 sm:pl-5 sm:pr-5 mb-[5px] rounded-lg flex justify-between items-center hover:bg-slate-500 transition duration-150 ease-in-out"
|
||||
: visibleBorder
|
||||
? "border-yellow-50"
|
||||
: "border-yellow-50/0"}
|
||||
relative pt-1 pb-1 pr-2 pl-2 sm:pl-5 sm:pr-5 mb-1.25 rounded-lg flex justify-between items-center hover:bg-slate-500 transition duration-150 ease-in-out"
|
||||
>
|
||||
<div
|
||||
class="font-bold text-right w-[30px] text-lg text-white absolute left-[-40px]"
|
||||
class="font-bold text-right w-7.5 text-lg text-white absolute -left-10"
|
||||
>
|
||||
${this.rank}
|
||||
</div>
|
||||
@@ -50,7 +52,7 @@ export class PlayerRow extends LitElement {
|
||||
return html`
|
||||
<img
|
||||
src="/images/CrownIcon.svg"
|
||||
class="absolute top-[-3px] left-[16px] w-[15px] h-[15px] sm:top-[-7px] sm:left-[30px] sm:w-[20px] sm:h-[20px]"
|
||||
class="absolute -top-0.75 left-4 size-3.75 sm:-top-1.75 sm:left-7.5 sm:size-5"
|
||||
/>
|
||||
`;
|
||||
}
|
||||
@@ -84,7 +86,7 @@ export class PlayerRow extends LitElement {
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="font-bold rounded-[50%] w-[30px] h-[30px] leading-[1.6rem] border text-center bg-white text-black"
|
||||
class="font-bold rounded-[50%] size-7.5 leading-[1.6rem] border border-gray-200 text-center bg-white text-black"
|
||||
>
|
||||
${Number(this.score).toFixed(0)}
|
||||
</div>
|
||||
@@ -96,10 +98,13 @@ export class PlayerRow extends LitElement {
|
||||
const bestScore = Math.max(this.bestScore, 1);
|
||||
const width = Math.min(Math.max((this.score / bestScore) * 100, 0), 100);
|
||||
return html`
|
||||
<div class="w-full pr-[10px] m-auto">
|
||||
<div class="h-[7px] bg-neutral-800" style="width: 100%;">
|
||||
<div class="w-full pr-2.5 m-auto">
|
||||
<div class="h-1.75 bg-neutral-800 w-full">
|
||||
<!-- bar background -->
|
||||
<div class="h-[7px] bg-white" style="width: ${width}%;"></div>
|
||||
<div
|
||||
class="h-1.75 bg-white w-(--width)"
|
||||
style="--width: ${width}%;"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -109,7 +114,7 @@ export class PlayerRow extends LitElement {
|
||||
<div
|
||||
class="${highlight
|
||||
? "font-bold text-[18px]"
|
||||
: ""} min-w-[30px] sm:min-w-[60px] inline-block text-center"
|
||||
: ""} min-w-7.5 sm:min-w-15 inline-block text-center"
|
||||
>
|
||||
${value}
|
||||
</div>
|
||||
@@ -152,32 +157,27 @@ export class PlayerRow extends LitElement {
|
||||
return html`
|
||||
<div class="flex gap-3 items-center">
|
||||
${this.renderPlayerIcon()}
|
||||
<div
|
||||
class="text-left w-[125px] max-w-[125px] sm:w-[250px] sm:max-w-[250px]"
|
||||
>
|
||||
<div class="text-left w-31.25 sm:w-62.5">
|
||||
${this.renderPlayerName()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div
|
||||
class="font-bold rounded-md w-[60px] max-w-[60px] h-[30px] text-sm sm:w-[100px] sm:h-[30px] leading-[1.9rem] text-center"
|
||||
class="font-bold rounded-md w-15 shrink-0 h-7.5 text-sm sm:w-25 sm:h-7.5 leading-[1.9rem] text-center"
|
||||
>
|
||||
${renderNumber(this.score)}
|
||||
</div>
|
||||
<img
|
||||
src="/images/GoldCoinIcon.svg"
|
||||
class="w-[14px] h-[14px] sm:w-[20px] sm:h-[20px] m-auto"
|
||||
/>
|
||||
<img src="/images/GoldCoinIcon.svg" class="size-3.5 sm:size-5 m-auto" />
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPlayerName() {
|
||||
return html`
|
||||
<div class="flex gap-1 items-center max-w-[200px] min-w-[200px]">
|
||||
<div class="flex gap-1 items-center w-50 shrink-0">
|
||||
${this.player.tag ? this.renderTag(this.player.tag) : ""}
|
||||
<div
|
||||
class="text-xs sm:text-sm font-bold text-ellipsis max-w-[150px] min-w-[150px] overflow-hidden whitespace-nowrap"
|
||||
class="text-xs sm:text-sm font-bold text-ellipsis w-37.5 shrink-0 overflow-hidden whitespace-nowrap"
|
||||
>
|
||||
${this.player.username}
|
||||
</div>
|
||||
@@ -188,7 +188,7 @@ export class PlayerRow extends LitElement {
|
||||
private renderTag(tag: string) {
|
||||
return html`
|
||||
<div
|
||||
class="bg-white text-black rounded-lg sm:rounded-xl border text-xs leading-[12px] sm:leading-[18px] text-blue-900 h-[15px] pr-[4px] pl-[4px] sm:h-[20px] sm:pr-[8px] sm:pl-[8px] font-bold"
|
||||
class="bg-white text-black rounded-lg sm:rounded-xl border border-gray-200 text-xs leading-3 sm:leading-4.5 text-blue-900 h-3.75 px-1 sm:h-5 sm:px-2 font-bold"
|
||||
>
|
||||
${tag}
|
||||
</div>
|
||||
@@ -198,24 +198,24 @@ export class PlayerRow extends LitElement {
|
||||
private renderIcon() {
|
||||
if (this.player.killedAt) {
|
||||
return html` <div
|
||||
class="w-[30px] h-[30px] leading-[5px] text-lg sm:min-w-[40px] sm:w-[40px] sm:h-[40px] pt-[12px] sm:leading-[15px] sm:rounded-[50%] sm:border text-center sm:bg-slate-500 sm:text-2xl"
|
||||
class="size-7.5 leading-1.25 shrink-0 text-lg sm:size-10 pt-3 sm:leading-3.75 sm:rounded-[50%] sm:border sm:border-gray-200 text-center sm:bg-slate-500 sm:text-2xl"
|
||||
>
|
||||
💀
|
||||
</div>`;
|
||||
} else if (this.player.flag) {
|
||||
return html`<img
|
||||
src="/flags/${this.player.flag}.svg"
|
||||
class="min-w-[30px] h-[30px] sm:min-w-[40px] sm:h-[40px]"
|
||||
class="min-w-7.5 h-7.5 sm:min-w-10 sm:h-10 shrink-0"
|
||||
/>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="w-[30px] h-[30px] min-w-[30px] leading-[5px] rounded-[50%] sm:min-w-[40px] sm:w-[40px] sm:h-[40px] sm:pt-[10px] sm:leading-[14px] border text-center bg-slate-500"
|
||||
class="size-7.5 leading-1.25 shrink-0 rounded-[50%] sm:size-10 sm:pt-2.5 sm:leading-3.5 border border-gray-200 text-center bg-slate-500"
|
||||
>
|
||||
<img
|
||||
src="/images/ProfileIcon.svg"
|
||||
class="w-[20px] h-[20px] mt-[2px] sm:w-[25px] sm:h-[25px] sm:mt-[-5px] m-auto"
|
||||
class="size-5 mt-0.5 sm:size-6.25 sm:-mt-1.25 m-auto"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -55,9 +55,8 @@ export class RankingControls extends LitElement {
|
||||
return html`
|
||||
<button
|
||||
class="rounded-lg bg-blue-600 text-white text-lg p-3 hover:bg-blue-400 ${active
|
||||
? "active"
|
||||
? "active outline-2 outline-white font-bold"
|
||||
: ""}"
|
||||
style="${active ? "outline: solid 2px white; font-weight: bold;" : ""}"
|
||||
@click=${() => this.onSort(type)}
|
||||
>
|
||||
${translateText(label)}
|
||||
@@ -107,8 +106,9 @@ export class RankingControls extends LitElement {
|
||||
return html`
|
||||
<button
|
||||
@click=${() => this.onSort(type)}
|
||||
class="rounded-md bg-blue-50 text-black text-sm p-2 hover:bg-blue-200"
|
||||
style="${active ? "outline: solid 2px white; font-weight: bold;" : ""}"
|
||||
class="rounded-md bg-blue-50 text-black text-sm p-2 hover:bg-blue-200 ${active
|
||||
? "outline-2 outline-white font-bold"
|
||||
: ""}"
|
||||
>
|
||||
${translateText(label)}
|
||||
</button>
|
||||
|
||||
@@ -14,7 +14,7 @@ export class RankingHeader extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<li
|
||||
class="text-lg bg-gray-800 font-bold relative pt-2 pb-2 pr-5 pl-5 mb-[5px] rounded-md flex justify-between items-center"
|
||||
class="text-lg bg-gray-800 font-bold relative pt-2 pb-2 pr-5 pl-5 mb-1.25 rounded-md flex justify-between items-center"
|
||||
>
|
||||
${this.renderHeaderContent()}
|
||||
</li>
|
||||
@@ -35,7 +35,7 @@ export class RankingHeader extends LitElement {
|
||||
case RankType.Hydros:
|
||||
case RankType.MIRV:
|
||||
return html`
|
||||
<div class="flex justify-between sm:pl-[70px] sm:pr-[70px] w-full">
|
||||
<div class="flex justify-between sm:px-17.5 w-full">
|
||||
${this.renderBombHeaderButton(
|
||||
translateText("game_info_modal.atoms"),
|
||||
RankType.Atoms,
|
||||
@@ -78,8 +78,8 @@ export class RankingHeader extends LitElement {
|
||||
return html`
|
||||
<button
|
||||
@click=${() => this.onSort(type)}
|
||||
style="${this.rankType === type
|
||||
? "border-bottom: solid 2px white;"
|
||||
class="${this.rankType === type
|
||||
? "border-b-2 border-b-white"
|
||||
: nothing}"
|
||||
>
|
||||
${label}
|
||||
|
||||
@@ -21,12 +21,11 @@ export class SettingKeybind extends LitElement {
|
||||
return html`
|
||||
<div class="setting-item column${this.easter ? " easter-egg" : ""}">
|
||||
<div class="setting-label-group">
|
||||
<label class="setting-label block mb-1">${this.label}</label>
|
||||
<label class="setting-label block mb-1">${this.label} </label>
|
||||
|
||||
<div class="setting-keybind-box flex flex-wrap items-start gap-2">
|
||||
<div
|
||||
class="setting-keybind-description flex-1 min-w-[240px] max-w-full whitespace-normal break-words text-sm text-gray-300"
|
||||
style="word-break: break-word;"
|
||||
class="setting-keybind-description flex-1 min-w-60 max-w-full whitespace-normal wrap-break-words text-sm text-gray-300 [word-break:break-word]"
|
||||
>
|
||||
${this.description}
|
||||
</div>
|
||||
@@ -44,13 +43,13 @@ export class SettingKeybind extends LitElement {
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="text-xs text-gray-400 hover:text-white border border-gray-500 px-2 py-0.5 rounded transition whitespace-normal break-words max-w-full"
|
||||
class="text-xs text-gray-400 hover:text-white border border-gray-500 px-2 py-0.5 rounded-sm transition whitespace-normal wrap-break-words max-w-full"
|
||||
@click=${this.resetToDefault}
|
||||
>
|
||||
${translateText("user_setting.reset")}
|
||||
</button>
|
||||
<button
|
||||
class="text-xs text-gray-400 hover:text-white border border-gray-500 px-2 py-0.5 rounded transition whitespace-normal break-words max-w-full"
|
||||
class="text-xs text-gray-400 hover:text-white border border-gray-500 px-2 py-0.5 rounded-sm transition whitespace-normal wrap-break-words max-w-full"
|
||||
@click=${this.unbindKey}
|
||||
>
|
||||
${translateText("user_setting.unbind")}
|
||||
|
||||
@@ -120,21 +120,19 @@ export class GameList extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="details"
|
||||
style="max-height:${this.expandedGameId === game.gameId
|
||||
? "200px"
|
||||
: "0"}; ${this.expandedGameId === game.gameId
|
||||
? ""
|
||||
: "padding-top:0; padding-bottom:0;"}"
|
||||
class="details max-h-(--max-height) ${this.expandedGameId ===
|
||||
game.gameId
|
||||
? "max-h-50"
|
||||
: "py-0"}"
|
||||
>
|
||||
<div>
|
||||
<span class="title" style="font-size:0.75rem;"
|
||||
<span class="title text-xs"
|
||||
>${translateText("game_list.started")}:</span
|
||||
>
|
||||
${new Date(game.start).toLocaleString()}
|
||||
</div>
|
||||
<div>
|
||||
<span class="title" style="font-size:0.75rem;"
|
||||
<span class="title text-xs"
|
||||
>${translateText("game_list.mode")}:</span
|
||||
>
|
||||
${game.mode === GameMode.FFA
|
||||
@@ -142,19 +140,19 @@ export class GameList extends LitElement {
|
||||
: translateText("game_list.mode_team")}
|
||||
</div>
|
||||
<div>
|
||||
<span class="title" style="font-size:0.75rem;"
|
||||
<span class="title text-xs"
|
||||
>${translateText("game_list.map")}:</span
|
||||
>
|
||||
${game.map}
|
||||
</div>
|
||||
<div>
|
||||
<span class="title" style="font-size:0.75rem;"
|
||||
<span class="title text-xs"
|
||||
>${translateText("game_list.difficulty")}:</span
|
||||
>
|
||||
${game.difficulty}
|
||||
</div>
|
||||
<div>
|
||||
<span class="title" style="font-size:0.75rem;"
|
||||
<span class="title text-xs"
|
||||
>${translateText("game_list.type")}:</span
|
||||
>
|
||||
${game.type}
|
||||
|
||||
@@ -117,16 +117,16 @@ export class PlayerStatsTable extends LitElement {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left" style="width:40%">
|
||||
<th class="text-left w-2/5">
|
||||
${translateText("player_stats_table.weapon")}
|
||||
</th>
|
||||
<th class="text-center" style="width:20%">
|
||||
<th class="text-center w-1/5">
|
||||
${translateText("player_stats_table.launched")}
|
||||
</th>
|
||||
<th class="text-center" style="width:20%">
|
||||
<th class="text-center w-1/5">
|
||||
${translateText("player_stats_table.landed")}
|
||||
</th>
|
||||
<th class="text-center" style="width:20%">
|
||||
<th class="text-center w-1/5">
|
||||
${translateText("player_stats_table.hits")}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -170,7 +170,7 @@ export class PlayerStatsTable extends LitElement {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table style="margin-top: 0.75rem;">
|
||||
<table class="mt-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${translateText("player_stats_table.gold")}</th>
|
||||
|
||||
@@ -122,8 +122,8 @@ export class PlayerStatsTreeView extends LitElement {
|
||||
${types.map(
|
||||
(t) => html`
|
||||
<button
|
||||
class="text-xs px-2 py-0.5 rounded border ${this.selectedType ===
|
||||
t
|
||||
class="text-xs px-2 py-0.5 rounded-sm border ${this
|
||||
.selectedType === t
|
||||
? "border-white/60 text-white"
|
||||
: "border-white/20 text-gray-300"}"
|
||||
@click=${() => this.setGameType(t)}
|
||||
@@ -143,7 +143,7 @@ export class PlayerStatsTreeView extends LitElement {
|
||||
${modes.map(
|
||||
(m) => html`
|
||||
<button
|
||||
class="text-xs px-2 py-0.5 rounded border ${this
|
||||
class="text-xs px-2 py-0.5 rounded-sm border ${this
|
||||
.selectedMode === m
|
||||
? "border-white/60 text-white"
|
||||
: "border-white/20 text-gray-300"}"
|
||||
@@ -162,7 +162,7 @@ export class PlayerStatsTreeView extends LitElement {
|
||||
${diffs.map(
|
||||
(d) =>
|
||||
html` <button
|
||||
class="text-xs px-2 py-0.5 rounded border ${this
|
||||
class="text-xs px-2 py-0.5 rounded-sm border ${this
|
||||
.selectedDifficulty === d
|
||||
? "border-white/60 text-white"
|
||||
: "border-white/20 text-gray-300"}"
|
||||
|
||||
@@ -25,9 +25,9 @@ const TEXT_SIZE =
|
||||
const getButtonStyles = () => {
|
||||
const btnBase =
|
||||
"group w-full min-w-[50px] select-none flex flex-col items-center justify-center " +
|
||||
"gap-1 rounded-lg py-1.5 border border-white/10 bg-white/[0.04] shadow-sm " +
|
||||
"gap-1 rounded-lg py-1.5 border border-white/10 bg-white/4 shadow-xs " +
|
||||
"transition-all duration-150 " +
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20 " +
|
||||
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-white/20 " +
|
||||
"active:translate-y-[1px]";
|
||||
|
||||
return {
|
||||
|
||||
@@ -460,7 +460,7 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
alt="gold"
|
||||
width="12"
|
||||
height="12"
|
||||
style="vertical-align: middle;"
|
||||
class="align-middle"
|
||||
/>
|
||||
</span>
|
||||
${item.countable
|
||||
|
||||
@@ -125,13 +125,12 @@ export class ChatDisplay extends LitElement implements Layer {
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
class="${this._hidden
|
||||
? "w-fit px-[10px] py-[5px]"
|
||||
: ""} rounded-md bg-black bg-opacity-60 relative max-h-[30vh] flex flex-col-reverse overflow-y-auto w-full lg:bottom-2.5 lg:right-2.5 z-50 lg:max-w-[30vw] lg:w-full lg:w-auto"
|
||||
style="pointer-events: auto"
|
||||
class="pointer-events-auto ${this._hidden
|
||||
? "w-fit px-2.5 py-1.25"
|
||||
: ""} rounded-md bg-black/60 relative max-h-[30vh] flex flex-col-reverse overflow-y-auto w-full lg:bottom-2.5 lg:right-2.5 z-50 lg:max-w-[30vw] lg:w-full lg:w-auto"
|
||||
>
|
||||
<div>
|
||||
<div class="w-full bg-black/80 sticky top-0 px-[10px]">
|
||||
<div class="w-full bg-black/80 sticky top-0 px-2.5">
|
||||
<button
|
||||
class="text-white cursor-pointer pointer-events-auto ${this
|
||||
._hidden
|
||||
@@ -153,22 +152,21 @@ export class ChatDisplay extends LitElement implements Layer {
|
||||
<span
|
||||
class="${this.newEvents
|
||||
? ""
|
||||
: "hidden"} inline-block px-2 bg-red-500 rounded-sm"
|
||||
: "hidden"} inline-block px-2 bg-red-500 rounded-xs"
|
||||
>${this.newEvents}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<table
|
||||
class="w-full border-collapse text-white shadow-lg lg:text-xl text-xs ${this
|
||||
class="w-full border-collapse text-white shadow-lg lg:text-xl text-xs pointer-events-none ${this
|
||||
._hidden
|
||||
? "hidden"
|
||||
: ""}"
|
||||
style="pointer-events: auto;"
|
||||
>
|
||||
<tbody>
|
||||
${this.chatEvents.map(
|
||||
(chat) => html`
|
||||
<tr class="border-b border-opacity-0">
|
||||
<tr class="border-b border-gray-200/0">
|
||||
<td class="lg:p-3 p-1 text-left">
|
||||
${this.getChatContent(chat)}
|
||||
</td>
|
||||
|
||||
@@ -160,13 +160,12 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
class="${this._isVisible
|
||||
? "w-full sm:max-w-[320px] text-sm sm:text-base bg-gray-800/70 p-2 pr-3 sm:p-4 shadow-lg sm:rounded-lg backdrop-blur"
|
||||
class="pointer-events-auto ${this._isVisible
|
||||
? "w-full sm:max-w-[320px] text-sm sm:text-base bg-gray-800/70 p-2 pr-3 sm:p-4 shadow-lg sm:rounded-lg backdrop-blur-sm"
|
||||
: "hidden"}"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
style="pointer-events: auto;"
|
||||
>
|
||||
<div class="block bg-black/30 text-white mb-4 p-2 rounded">
|
||||
<div class="block bg-black/30 text-white mb-4 p-2 rounded-sm">
|
||||
<div class="flex justify-between mb-1">
|
||||
<span class="font-bold"
|
||||
>${translateText("control_panel.troops")}:</span
|
||||
@@ -192,30 +191,30 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
|
||||
<div class="relative mb-0 sm:mb-4">
|
||||
<label class="block text-white mb-1">
|
||||
${translateText("control_panel.attack_ratio")}:
|
||||
${translateText("control_panel.attack_ratio")} :
|
||||
<span
|
||||
class="inline-flex items-center gap-1"
|
||||
class="inline-flex items-center gap-1 [unicode-bidi:isolate]"
|
||||
dir="ltr"
|
||||
style="unicode-bidi: isolate;"
|
||||
translate="no"
|
||||
>
|
||||
<span>${(this.attackRatio * 100).toFixed(0)}%</span>
|
||||
<span>
|
||||
(${renderTroops(
|
||||
(this.game?.myPlayer()?.troops() ?? 0) * this.attackRatio,
|
||||
)})
|
||||
)}
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="relative h-8">
|
||||
<!-- Background track -->
|
||||
<div
|
||||
class="absolute left-0 right-0 top-3 h-2 bg-white/20 rounded"
|
||||
class="absolute left-0 right-0 top-3 h-2 bg-white/20 rounded-sm"
|
||||
></div>
|
||||
<!-- Fill track -->
|
||||
<div
|
||||
class="absolute left-0 top-3 h-2 bg-red-500/60 rounded transition-all duration-300"
|
||||
style="width: ${this.attackRatio * 100}%"
|
||||
class="absolute left-0 top-3 h-2 bg-red-500/60 rounded-sm transition-all duration-300 w-(--width)"
|
||||
style="--width: ${this.attackRatio * 100}%"
|
||||
></div>
|
||||
<!-- Range input - exactly overlaying the visual elements -->
|
||||
<input
|
||||
|
||||
@@ -73,22 +73,22 @@ export class EmojiTable extends LitElement {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed inset-0 bg-black/15 backdrop-brightness-110 flex items-start sm:items-center justify-center z-[10002] pt-4 sm:pt-0"
|
||||
class="fixed inset-0 bg-black/15 backdrop-brightness-110 flex items-start sm:items-center justify-center z-10002 pt-4 sm:pt-0"
|
||||
@click=${this.handleBackdropClick}
|
||||
>
|
||||
<div class="relative">
|
||||
<!-- Close button -->
|
||||
<button
|
||||
class="absolute -top-3 -right-3 w-7 h-7 flex items-center justify-center
|
||||
bg-zinc-700 hover:bg-red-500 text-white rounded-full shadow transition-colors z-[10004]"
|
||||
bg-zinc-700 hover:bg-red-500 text-white rounded-full shadow-sm transition-colors z-10004"
|
||||
@click=${this.hideTable}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="bg-zinc-900/95 p-2 sm:p-3 rounded-[10px] z-[10003] shadow-2xl shadow-black/50 ring-1 ring-white/5
|
||||
w-[calc(100vw-32px)] sm:w-[400px] max-h-[calc(100vh-60px)] overflow-y-auto"
|
||||
class="bg-zinc-900/95 p-2 sm:p-3 rounded-[10px] z-10003 shadow-2xl shadow-black/50 ring-1 ring-white/5
|
||||
w-[calc(100vw-32px)] sm:w-100 max-h-[calc(100vh-60px)] overflow-y-auto"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
@wheel=${(e: WheelEvent) => e.stopPropagation()}
|
||||
@click=${(e: MouseEvent) => e.stopPropagation()}
|
||||
|
||||
@@ -153,9 +153,9 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
content: html`<img
|
||||
src="${src}"
|
||||
class="${toggleButtonSizeMap["default"]}"
|
||||
style="filter: ${this.eventsFilters.get(category)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
style="${this.eventsFilters.get(category)
|
||||
? "filter: grayscale(1) opacity(0.5);"
|
||||
: ""}"
|
||||
/>`,
|
||||
onClick: () => this.toggleEventFilter(category),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
@@ -816,7 +816,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
content: translateText("events_display.retaliate"),
|
||||
onClick: () => this.handleRetaliate(attack),
|
||||
className:
|
||||
"inline-block px-3 py-1 text-white rounded text-md md:text-sm cursor-pointer transition-colors duration-300 bg-red-600 hover:bg-red-700",
|
||||
"inline-block px-3 py-1 text-white rounded-sm text-md md:text-sm cursor-pointer transition-colors duration-300 bg-red-600 hover:bg-red-700",
|
||||
translate: true,
|
||||
})
|
||||
: ""}
|
||||
@@ -854,10 +854,10 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
? this.renderButton({
|
||||
content: "❌",
|
||||
onClick: () => this.emitCancelAttackIntent(attack.id),
|
||||
className: "text-left flex-shrink-0",
|
||||
className: "text-left shrink-0",
|
||||
disabled: attack.retreating,
|
||||
})
|
||||
: html`<span class="flex-shrink-0 text-blue-400"
|
||||
: html`<span class="shrink-0 text-blue-400"
|
||||
>(${translateText(
|
||||
"events_display.retreating",
|
||||
)}...)</span
|
||||
@@ -890,10 +890,10 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
content: "❌",
|
||||
onClick: () =>
|
||||
this.emitCancelAttackIntent(landAttack.id),
|
||||
className: "text-left flex-shrink-0",
|
||||
className: "text-left shrink-0",
|
||||
disabled: landAttack.retreating,
|
||||
})
|
||||
: html`<span class="flex-shrink-0 text-blue-400"
|
||||
: html`<span class="shrink-0 text-blue-400"
|
||||
>(${translateText(
|
||||
"events_display.retreating",
|
||||
)}...)</span
|
||||
@@ -926,10 +926,10 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
? this.renderButton({
|
||||
content: "❌",
|
||||
onClick: () => this.emitBoatCancelIntent(boat.id()),
|
||||
className: "text-left flex-shrink-0",
|
||||
className: "text-left shrink-0",
|
||||
disabled: boat.retreating(),
|
||||
})
|
||||
: html`<span class="flex-shrink-0 text-blue-400"
|
||||
: html`<span class="shrink-0 text-blue-400"
|
||||
>(${translateText(
|
||||
"events_display.retreating",
|
||||
)}...)</span
|
||||
@@ -1026,14 +1026,14 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
`,
|
||||
onClick: this.toggleHidden,
|
||||
className:
|
||||
"text-white cursor-pointer pointer-events-auto w-fit p-2 lg:p-3 rounded-lg bg-gray-800/70 backdrop-blur",
|
||||
"text-white cursor-pointer pointer-events-auto w-fit p-2 lg:p-3 rounded-lg bg-gray-800/70 backdrop-blur-sm",
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<!-- Main Events Display -->
|
||||
<div
|
||||
class="relative w-full sm:bottom-4 sm:right-4 z-50 sm:w-96 backdrop-blur"
|
||||
class="relative w-full sm:bottom-4 sm:right-4 z-50 sm:w-96 backdrop-blur-sm"
|
||||
>
|
||||
<!-- Button Bar -->
|
||||
<div class="w-full p-2 lg:p-3 bg-gray-800/70 rounded-t-lg">
|
||||
@@ -1083,8 +1083,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
>
|
||||
<div>
|
||||
<table
|
||||
class="w-full max-h-none border-collapse text-white shadow-lg lg:text-base text-md md:text-xs"
|
||||
style="pointer-events: auto;"
|
||||
class="w-full max-h-none border-collapse text-white shadow-lg lg:text-base text-md md:text-xs pointer-events-auto"
|
||||
>
|
||||
<tbody>
|
||||
${filteredEvents.map(
|
||||
@@ -1123,7 +1122,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
${event.buttons.map(
|
||||
(btn) => html`
|
||||
<button
|
||||
class="inline-block px-3 py-1 text-white rounded text-md md:text-sm cursor-pointer transition-colors duration-300
|
||||
class="inline-block px-3 py-1 text-white rounded-sm text-md md:text-sm cursor-pointer transition-colors duration-300
|
||||
${btn.className.includes("btn-info")
|
||||
? "bg-blue-500 hover:bg-blue-600"
|
||||
: btn.className.includes(
|
||||
|
||||
@@ -87,7 +87,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
render() {
|
||||
return html`
|
||||
<aside
|
||||
class=${`fixed top-4 left-4 z-[1000] flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-lg transition-transform duration-300 ease-out transform ${
|
||||
class=${`fixed top-4 left-4 z-1000 flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-xs shadow-xs rounded-lg transition-transform duration-300 ease-out transform ${
|
||||
this.isVisible ? "translate-x-0" : "hidden"
|
||||
}`}
|
||||
>
|
||||
@@ -98,14 +98,17 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
${translateText("help_modal.ui_your_team")}
|
||||
<span style="color: ${this.playerColor.toRgbString()}">
|
||||
<span
|
||||
style="--color: ${this.playerColor.toRgbString()}"
|
||||
class="text-(--color)"
|
||||
>
|
||||
${this.getTranslatedPlayerTeamLabel()} ⦿
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
<div
|
||||
class=${`flex items-center gap-2 space-x-2 text-white ${
|
||||
class=${`flex items-center gap-2 text-white ${
|
||||
this.isLeaderboardShow || this.isTeamLeaderboardShow ? "mb-2" : ""
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -139,7 +139,7 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<aside
|
||||
class=${`w-fit flex flex-row items-center gap-3 py-2 px-3 bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-lg transition-transform duration-300 ease-out transform text-white ${
|
||||
class=${`w-fit flex flex-row items-center gap-3 py-2 px-3 bg-gray-800/70 backdrop-blur-xs shadow-xs rounded-lg transition-transform duration-300 ease-out transform text-white ${
|
||||
this._isVisible ? "translate-x-0" : "translate-x-full"
|
||||
}`}
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
|
||||
@@ -59,8 +59,8 @@ export class HeadsUpMessage extends LitElement implements Layer {
|
||||
return html`
|
||||
<div
|
||||
class="flex items-center relative
|
||||
w-full justify-evenly h-8 lg:h-10 md:top-[70px] left-0 lg:left-4
|
||||
bg-opacity-60 bg-gray-900 rounded-md lg:rounded-lg
|
||||
w-full justify-evenly h-8 lg:h-10 md:top-17.5 left-0 lg:left-4
|
||||
bg-gray-900/60 rounded-md lg:rounded-lg
|
||||
backdrop-blur-md text-white text-md lg:text-xl p-1 lg:p-2"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
|
||||
@@ -82,7 +82,7 @@ export class ImmunityTimer extends LitElement implements Layer {
|
||||
const widthPercent = this.progressRatio * 100;
|
||||
|
||||
return html`
|
||||
<div class="w-full h-full flex z-[999]">
|
||||
<div class="w-full h-full flex z-999">
|
||||
<div
|
||||
class="h-full transition-all duration-100 ease-in-out"
|
||||
style="width: ${widthPercent}%; background-color: rgba(255, 165, 0, 0.9);"
|
||||
|
||||
@@ -171,8 +171,9 @@ export class MultiTabModal extends LitElement implements Layer {
|
||||
class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 mb-4"
|
||||
>
|
||||
<div
|
||||
class="bg-red-600 dark:bg-red-500 h-2.5 rounded-full transition-all duration-1000 ease-linear"
|
||||
style="width: ${(this.countdown / (this.duration / 1000)) * 100}%"
|
||||
class="bg-red-600 dark:bg-red-500 h-2.5 rounded-full transition-all duration-1000 ease-linear w-(--width)"
|
||||
style="--width: ${(this.countdown / (this.duration / 1000)) *
|
||||
100}%"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -537,12 +537,6 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const style = `
|
||||
left: ${this.position.x}px;
|
||||
top: ${this.position.y}px;
|
||||
transform: none;
|
||||
`;
|
||||
|
||||
const copyLabel =
|
||||
this.copyStatus === "success"
|
||||
? translateText("performance_overlay.copied")
|
||||
@@ -557,8 +551,10 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="performance-overlay ${this.isDragging ? "dragging" : ""}"
|
||||
style="${style}"
|
||||
class="performance-overlay ${this.isDragging
|
||||
? "dragging"
|
||||
: ""} transform-none left-(--left) top-(--top)"
|
||||
style="--left: ${this.position.x}; --top: ${this.position.y};"
|
||||
@mousedown="${this.handleMouseDown}"
|
||||
>
|
||||
<button class="reset-button" @click="${this.handleReset}">
|
||||
@@ -612,10 +608,13 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
);
|
||||
return html`<div class="layer-row">
|
||||
<span class="layer-name" title=${layer.name}
|
||||
>${layer.name}</span
|
||||
>
|
||||
>${layer.name}
|
||||
</span>
|
||||
<div class="layer-bar">
|
||||
<div class="layer-bar-fill" style="width: ${width}%;"></div>
|
||||
<div
|
||||
class="layer-bar-fill w-(--width)"
|
||||
style="--width: ${width}%;"
|
||||
></div>
|
||||
</div>
|
||||
<span class="layer-metrics">
|
||||
${layer.avg.toFixed(2)} / ${layer.max.toFixed(2)}ms
|
||||
|
||||
@@ -203,7 +203,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
width="20"
|
||||
height="20"
|
||||
alt="${translateText(description)}"
|
||||
style="vertical-align: middle;"
|
||||
class="align-middle"
|
||||
/>
|
||||
<span class="w-full text-right p-1"
|
||||
>${player.totalUnitLevels(type)}</span
|
||||
@@ -285,7 +285,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
alt=${translateText("player_info_overlay.alliance_timeout")}
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
class="align-middle"
|
||||
/>
|
||||
${this.allianceExpirationText(alliance)}
|
||||
</span>`;
|
||||
@@ -318,7 +318,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
${player.cosmetics.flag
|
||||
? player.cosmetics.flag!.startsWith("!")
|
||||
? html`<div
|
||||
class="h-8 mr-1 aspect-[3/4] player-flag"
|
||||
class="h-8 mr-1 aspect-3/4 player-flag"
|
||||
${ref((el) => {
|
||||
if (el instanceof HTMLElement) {
|
||||
requestAnimationFrame(() => {
|
||||
@@ -328,7 +328,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
})}
|
||||
></div>`
|
||||
: html`<img
|
||||
class="h-8 mr-1 aspect-[3/4]"
|
||||
class="h-8 mr-1 aspect-3/4"
|
||||
src=${"/flags/" + player.cosmetics.flag! + ".svg"}
|
||||
/>`
|
||||
: html``}
|
||||
@@ -390,7 +390,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
alt=${translateText("player_info_overlay.gold")}
|
||||
width="15"
|
||||
height="15"
|
||||
style="vertical-align: middle;"
|
||||
class="align-middle"
|
||||
/>
|
||||
<span class="w-full text-center"
|
||||
>${renderNumber(player.gold())}</span
|
||||
@@ -514,11 +514,11 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="block lg:flex fixed top-[150px] right-4 w-full z-50 flex-col max-w-[180px]"
|
||||
class="block lg:flex fixed top-37.5 right-4 w-full z-50 flex-col max-w-45"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-lg shadow-lg transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
|
||||
class="bg-gray-800/70 backdrop-blur-xs shadow-xs rounded-lg shadow-lg transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
|
||||
>
|
||||
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
|
||||
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
|
||||
|
||||
@@ -391,7 +391,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
const label = secs !== null ? renderDuration(secs) : null;
|
||||
const dotCls =
|
||||
secs !== null
|
||||
? `mx-1 h-[4px] w-[4px] rounded-full bg-red-400/70 ${secs <= 10 ? "animate-pulse" : ""}`
|
||||
? `mx-1 size-1 rounded-full bg-red-400/70 ${secs <= 10 ? "animate-pulse" : ""}`
|
||||
: "";
|
||||
|
||||
return html`
|
||||
@@ -402,12 +402,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
shadow-[inset_0_0_8px_rgba(239,68,68,0.12)]"
|
||||
title=${translateText("player_panel.traitor")}
|
||||
>
|
||||
<img
|
||||
src=${traitorIcon}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="h-[18px] w-[18px]"
|
||||
/>
|
||||
<img src=${traitorIcon} alt="" aria-hidden="true" class="size-4.5" />
|
||||
<span class="tracking-tight"
|
||||
>${translateText("player_panel.traitor")}</span
|
||||
>
|
||||
@@ -498,8 +493,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
return html`
|
||||
<div class="mb-1 flex justify-between gap-2">
|
||||
<div
|
||||
class="inline-flex items-center gap-1.5 rounded-lg bg-white/[0.04] px-3 py-1.5
|
||||
text-white w-[140px] min-w-[140px] flex-shrink-0"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg bg-white/4 px-3 py-1.5 shrink-0
|
||||
text-white w-35"
|
||||
>
|
||||
<span class="mr-0.5">💰</span>
|
||||
<span translate="no" class="tabular-nums w-[5ch] font-semibold">
|
||||
@@ -511,8 +506,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="inline-flex items-center gap-1.5 rounded-lg bg-white/[0.04] px-3 py-1.5
|
||||
text-white w-[140px] min-w-[140px] flex-shrink-0"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg bg-white/4 px-3 py-1.5
|
||||
text-white w-35 shrink-0"
|
||||
>
|
||||
<span class="mr-0.5">🛡️</span>
|
||||
<span translate="no" class="tabular-nums w-[5ch] font-semibold">
|
||||
@@ -530,7 +525,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
return html`
|
||||
<ui-divider></ui-divider>
|
||||
<button
|
||||
class="flex w-full items-center justify-between rounded-xl bg-white/[0.05] px-3 py-2 text-left text-white hover:bg-white/[0.08] active:scale-[0.995] transition"
|
||||
class="flex w-full items-center justify-between rounded-xl bg-white/5 px-3 py-2 text-left text-white hover:bg-white/8 active:scale-[0.995] transition"
|
||||
@click=${(e: Event) => this.handleToggleRocketDirection(e)}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
@@ -551,7 +546,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
private renderStats(other: PlayerView, my: PlayerView) {
|
||||
return html`
|
||||
<!-- Betrayals -->
|
||||
<div class="grid grid-cols-[auto,1fr] gap-x-6 gap-y-2">
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2">
|
||||
<div
|
||||
class="flex items-center gap-2 text-[15px] font-medium text-zinc-100 leading-snug"
|
||||
>
|
||||
@@ -564,7 +559,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
</div>
|
||||
|
||||
<!-- Trading / Embargo -->
|
||||
<div class="grid grid-cols-[auto,1fr] gap-x-6 gap-y-2">
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2">
|
||||
<div
|
||||
class="flex items-center gap-2 text-[15px] font-medium text-zinc-100 leading-snug"
|
||||
>
|
||||
@@ -605,7 +600,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
</div>
|
||||
<span
|
||||
aria-labelledby="alliances-title"
|
||||
class="inline-flex items-center justify-center min-w-[20px] h-5 px-[6px] rounded-[10px]
|
||||
class="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-[10px]
|
||||
text-[12px] text-zinc-100 bg-white/10 border border-white/20"
|
||||
>
|
||||
${allies.length}
|
||||
@@ -616,7 +611,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
class="rounded-lg bg-zinc-800/70 ring-1 ring-zinc-700/60 w-full min-w-0"
|
||||
>
|
||||
<ul
|
||||
class="max-h-[120px] overflow-y-auto p-2
|
||||
class="max-h-30 overflow-y-auto p-2
|
||||
flex flex-wrap gap-1.5
|
||||
scrollbar-thin scrollbar-thumb-zinc-600 hover:scrollbar-thumb-zinc-500 scrollbar-track-zinc-800"
|
||||
role="list"
|
||||
@@ -631,9 +626,9 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
(p) =>
|
||||
html`<li
|
||||
class="max-w-full inline-flex items-center gap-1.5
|
||||
rounded-md border border-white/10 bg-white/[0.05]
|
||||
rounded-md border border-white/10 bg-white/5
|
||||
px-2.5 py-1 text-[14px] text-zinc-100
|
||||
hover:bg-white/[0.08] active:scale-[0.99] transition"
|
||||
hover:bg-white/8 active:scale-[0.99] transition"
|
||||
title=${p.name()}
|
||||
>
|
||||
<span class="truncate">${p.name()}</span>
|
||||
@@ -648,7 +643,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
private renderAllianceExpiry() {
|
||||
if (this.allianceExpiryText === null) return html``;
|
||||
return html`
|
||||
<div class="grid grid-cols-[auto,1fr] gap-x-6 gap-y-2 text-base">
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2 text-base">
|
||||
<div class="font-semibold text-zinc-300">
|
||||
${translateText("player_panel.alliance_time_remaining")}
|
||||
</div>
|
||||
@@ -859,14 +854,14 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
</style>
|
||||
|
||||
<div
|
||||
class="fixed inset-0 z-[10001] flex items-center justify-center overflow-auto
|
||||
class="fixed inset-0 z-10001 flex items-center justify-center overflow-auto
|
||||
bg-black/15 backdrop-brightness-110 pointer-events-auto"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
@wheel=${(e: MouseEvent) => e.stopPropagation()}
|
||||
@click=${() => this.hide()}
|
||||
>
|
||||
<div
|
||||
class="pointer-events-auto max-h-[90vh] min-w-[300px] max-w-[400px] px-4 py-2"
|
||||
class="pointer-events-auto max-h-[90vh] min-w-75 max-w-100 px-4 py-2"
|
||||
@click=${(e: MouseEvent) => e.stopPropagation()}
|
||||
>
|
||||
<div class="relative">
|
||||
@@ -877,18 +872,20 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
class=${`relative w-full bg-zinc-900/95 rounded-2xl text-zinc-100 shadow-2xl shadow-black/50
|
||||
${other.isTraitor() ? "traitor-ring" : "ring-1 ring-white/5"}`}
|
||||
>
|
||||
<div style="overflow: visible;">
|
||||
<div class="overflow-visible">
|
||||
<div
|
||||
style="max-height: calc(100vh - 120px - env(safe-area-inset-bottom)); overflow:auto; -webkit-overflow-scrolling: touch; resize: vertical;"
|
||||
class="overflow-auto [-webkit-overflow-scrolling:touch] resize-y max-h-[calc(100vh-120px-env(safe-area-inset-bottom))]"
|
||||
>
|
||||
<button
|
||||
@click=${this.handleClose}
|
||||
class="absolute right-3 top-3 z-20 flex h-7 w-7 items-center justify-center rounded-full bg-zinc-700 text-white shadow hover:bg-red-500 transition-colors"
|
||||
aria-label=${translateText("common.close") || "Close"}
|
||||
title=${translateText("common.close") || "Close"}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<div class="sticky top-0 z-20 flex justify-end p-2">
|
||||
<button
|
||||
@click=${this.handleClose}
|
||||
class="absolute right-3 top-3 z-20 flex h-7 w-7 items-center justify-center rounded-full bg-zinc-700 text-white shadow-sm hover:bg-red-500 transition-colors"
|
||||
aria-label=${translateText("common.close") || "Close"}
|
||||
title=${translateText("common.close") || "Close"}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="p-6 flex flex-col gap-2 font-sans antialiased text-[14.5px] leading-relaxed"
|
||||
|
||||
@@ -66,7 +66,7 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="p-2 bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-lg"
|
||||
class="p-2 bg-gray-800/70 backdrop-blur-xs shadow-xs rounded-lg"
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<label class="block mb-2 text-white" translate="no">
|
||||
@@ -93,7 +93,7 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<button
|
||||
class="py-0.5 px-1 text-sm text-white rounded border transition border-gray-500 ${backgroundColor} hover:border-gray-200"
|
||||
class="py-0.5 px-1 text-sm text-white rounded-sm border transition border-gray-500 ${backgroundColor} hover:border-gray-200"
|
||||
@click=${() => this.onReplaySpeedChange(value)}
|
||||
>
|
||||
${label}
|
||||
|
||||
@@ -255,7 +255,7 @@ export class SendResourceModal extends LitElement {
|
||||
<button
|
||||
type="button"
|
||||
@click=${() => this.closeModal()}
|
||||
class="absolute -top-3 -right-3 flex h-7 w-7 items-center justify-center rounded-full bg-zinc-700 text-white shadow hover:bg-red-500 transition-colors focus-visible:ring-2 focus-visible:ring-white/30 focus:outline-none"
|
||||
class="absolute -top-3 -right-3 flex h-7 w-7 items-center justify-center rounded-full bg-zinc-700 text-white shadow-sm hover:bg-red-500 transition-colors focus-visible:ring-2 focus-visible:ring-white/30 focus:outline-hidden"
|
||||
aria-label=${this.i18n.closeLabel()}
|
||||
title=${this.i18n.closeLabel()}
|
||||
>
|
||||
@@ -361,7 +361,7 @@ export class SendResourceModal extends LitElement {
|
||||
const clamped = Math.min(raw, hardMax);
|
||||
this.sendAmount = this.clampSend(clamped);
|
||||
}}
|
||||
class="w-full appearance-none bg-transparent range-x focus:outline-none"
|
||||
class="w-full appearance-none bg-transparent range-x focus:outline-hidden"
|
||||
aria-label=${this.i18n.ariaSlider()}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax=${hardMax}
|
||||
@@ -374,11 +374,11 @@ export class SendResourceModal extends LitElement {
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="pointer-events-none absolute -top-6 -translate-x-1/2 select-none"
|
||||
style="left:${percentNow}%"
|
||||
class="pointer-events-none absolute -top-6 -translate-x-1/2 select-none left-(--pos)"
|
||||
style="--pos: ${percentNow}%"
|
||||
>
|
||||
<div
|
||||
class="rounded bg-[#0f1116] ring-1 ring-zinc-700 text-zinc-100 px-1.5 py-0.5 text-[12px] shadow whitespace-nowrap w-max z-50"
|
||||
class="rounded-sm bg-[#0f1116] ring-1 ring-zinc-700 text-zinc-100 px-1.5 py-0.5 text-[12px] shadow-sm whitespace-nowrap w-max z-50"
|
||||
>
|
||||
${percentNow}% • ${this.format(this.sendAmount)}
|
||||
</div>
|
||||
@@ -388,16 +388,16 @@ export class SendResourceModal extends LitElement {
|
||||
${capPercent !== null
|
||||
? html`
|
||||
<div
|
||||
class="pointer-events-none absolute top-1/2 -translate-y-1/2 h-3 w-[2px] bg-amber-400/80 shadow"
|
||||
style="left:${capPercent}%;"
|
||||
class="pointer-events-none absolute top-1/2 -translate-y-1/2 h-3 w-0.5 bg-amber-400/80 shadow-sm left-(--pos)"
|
||||
style="--pos:${capPercent}%;"
|
||||
title=${this.i18n.capTooltip()}
|
||||
></div>
|
||||
<div
|
||||
class="pointer-events-none absolute top-full mt-1.5 -translate-x-1/2 select-none"
|
||||
style="left:${capPercent}%"
|
||||
class="pointer-events-none absolute top-full mt-1.5 -translate-x-1/2 select-none left-(--pos)"
|
||||
style="--pos:${capPercent}%"
|
||||
>
|
||||
<div
|
||||
class="rounded bg-[#0f1116] ring-1 ring-amber-400/40 text-amber-200 px-1 py-0.5 text-[11px] shadow whitespace-nowrap"
|
||||
class="rounded-sm bg-[#0f1116] ring-1 ring-amber-400/40 text-amber-200 px-1 py-0.5 text-[11px] shadow-sm whitespace-nowrap"
|
||||
>
|
||||
${this.i18n.cap()}
|
||||
</div>
|
||||
@@ -451,7 +451,7 @@ export class SendResourceModal extends LitElement {
|
||||
<button
|
||||
class="h-10 min-w-24 rounded-lg px-3 text-sm font-semibold
|
||||
text-zinc-100 bg-zinc-800 ring-1 ring-zinc-700
|
||||
hover:bg-zinc-700 focus:outline-none
|
||||
hover:bg-zinc-700 focus:outline-hidden
|
||||
focus-visible:ring-2 focus-visible:ring-white/20"
|
||||
@click=${() => this.closeModal()}
|
||||
>
|
||||
@@ -460,7 +460,7 @@ export class SendResourceModal extends LitElement {
|
||||
<button
|
||||
class="h-10 min-w-24 rounded-lg px-3 text-sm font-semibold text-white
|
||||
bg-indigo-600 enabled:hover:bg-indigo-500
|
||||
focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400/50
|
||||
focus:outline-hidden focus-visible:ring-2 focus-visible:ring-indigo-400/50
|
||||
disabled:cursor-not-allowed disabled:opacity-50"
|
||||
?disabled=${disabled}
|
||||
@click=${() => this.confirm()}
|
||||
@@ -541,9 +541,7 @@ export class SendResourceModal extends LitElement {
|
||||
const allowed = this.limitAmount(this.sendAmount);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="absolute inset-0 z-[1100] flex items-center justify-center p-4"
|
||||
>
|
||||
<div class="absolute inset-0 z-1100 flex items-center justify-center p-4">
|
||||
<div
|
||||
class="absolute inset-0 bg-black/60 rounded-2xl"
|
||||
@click=${() => this.closeModal()}
|
||||
@@ -553,7 +551,7 @@ export class SendResourceModal extends LitElement {
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="send-title"
|
||||
class="relative z-10 w-full max-w-[540px] focus:outline-none"
|
||||
class="relative z-10 w-full max-w-135 focus:outline-hidden"
|
||||
tabindex="0"
|
||||
@keydown=${this.handleKeydown}
|
||||
>
|
||||
|
||||
@@ -189,7 +189,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="modal-overlay fixed inset-0 bg-black/50 backdrop-blur-sm z-[2000] flex items-center justify-center p-4"
|
||||
class="modal-overlay fixed inset-0 bg-black/50 backdrop-blur-xs z-2000 flex items-center justify-center p-4"
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
@@ -204,7 +204,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
alt="settings"
|
||||
width="24"
|
||||
height="24"
|
||||
style="vertical-align: middle;"
|
||||
class="align-middle"
|
||||
/>
|
||||
<h2 class="text-xl font-semibold text-white">
|
||||
${translateText("user_setting.tab_basic")}
|
||||
@@ -218,9 +218,9 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-4 space-y-3">
|
||||
<div class="p-4 flex flex-col gap-3">
|
||||
<div
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
>
|
||||
<img src=${musicIcon} alt="musicIcon" width="20" height="20" />
|
||||
<div class="flex-1">
|
||||
@@ -242,7 +242,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
>
|
||||
<img
|
||||
src=${musicIcon}
|
||||
@@ -269,7 +269,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onTerrainButtonClick}"
|
||||
>
|
||||
<img src=${treeIcon} alt="treeIcon" width="20" height="20" />
|
||||
@@ -289,7 +289,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onToggleEmojisButtonClick}"
|
||||
>
|
||||
<img src=${emojiIcon} alt="emojiIcon" width="20" height="20" />
|
||||
@@ -309,7 +309,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onToggleDarkModeButtonClick}"
|
||||
>
|
||||
<img
|
||||
@@ -334,7 +334,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onToggleSpecialEffectsButtonClick}"
|
||||
>
|
||||
<img
|
||||
@@ -359,7 +359,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onToggleAlertFrameButtonClick}"
|
||||
>
|
||||
<img src=${sirenIcon} alt="alertFrame" width="20" height="20" />
|
||||
@@ -379,7 +379,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onToggleStructureSpritesButtonClick}"
|
||||
>
|
||||
<img
|
||||
@@ -404,7 +404,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onToggleCursorCostLabelButtonClick}"
|
||||
>
|
||||
<img
|
||||
@@ -429,7 +429,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onToggleRandomNameModeButtonClick}"
|
||||
>
|
||||
<img src=${ninjaIcon} alt="ninjaIcon" width="20" height="20" />
|
||||
@@ -449,7 +449,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onToggleLeftClickOpensMenu}"
|
||||
>
|
||||
<img src=${mouseIcon} alt="mouseIcon" width="20" height="20" />
|
||||
@@ -469,7 +469,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onTogglePerformanceOverlayButtonClick}"
|
||||
>
|
||||
<img
|
||||
@@ -495,7 +495,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
|
||||
<div class="border-t border-slate-600 pt-3 mt-4">
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-red-600/20 rounded text-red-400 transition-colors"
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-red-600/20 rounded-sm text-red-400 transition-colors"
|
||||
@click="${this.onExitButtonClick}"
|
||||
>
|
||||
<img src=${exitIcon} alt="exitIcon" width="20" height="20" />
|
||||
|
||||
@@ -93,13 +93,13 @@ export class SpawnTimer extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="w-full h-full flex z-[999]">
|
||||
<div class="w-full h-full flex z-999">
|
||||
${this.ratios.map((ratio, i) => {
|
||||
const color = this.colors[i] || "rgba(0, 0, 0, 0.5)";
|
||||
return html`
|
||||
<div
|
||||
class="h-full transition-all duration-100 ease-in-out"
|
||||
style="width: ${ratio * 100}%; background-color: ${color};"
|
||||
class="h-full transition-all duration-100 ease-in-out w-(--width) bg-(--bg)"
|
||||
style="--width: ${ratio * 100}%; --bg: ${color};"
|
||||
></div>
|
||||
`;
|
||||
})}
|
||||
|
||||
@@ -129,8 +129,8 @@ export class TeamStats extends LitElement implements Layer {
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class="grid w-full"
|
||||
style="grid-template-columns: repeat(${this.showUnits ? 5 : 4}, 1fr);"
|
||||
class="grid w-full grid-cols-[repeat(var(--cols),1fr)]"
|
||||
style="--cols:${this.showUnits ? 5 : 4};"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="contents font-bold bg-slate-700/50">
|
||||
|
||||
@@ -263,6 +263,62 @@ export class TerritoryLayer implements Layer {
|
||||
baseColor, // Always draw white static semi-transparent ring
|
||||
teamColor, // Pass the breathing ring color. White for FFA, Duos, Trios, Quads. Transparent team color for TEAM games.
|
||||
);
|
||||
|
||||
// Draw breathing rings for teammates in team games (helps colorblind players identify teammates)
|
||||
this.drawTeammateHighlights(minRad, maxRad, radius);
|
||||
}
|
||||
|
||||
private drawTeammateHighlights(
|
||||
minRad: number,
|
||||
maxRad: number,
|
||||
radius: number,
|
||||
) {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (myPlayer === null || myPlayer.team() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const teammates = this.game
|
||||
.playerViews()
|
||||
.filter((p) => p !== myPlayer && myPlayer.isOnSameTeam(p));
|
||||
|
||||
// Smaller radius for teammates (more subtle than self highlight)
|
||||
const teammateMinRad = 5;
|
||||
const teammateMaxRad = 14;
|
||||
const teammateRadius =
|
||||
teammateMinRad +
|
||||
(teammateMaxRad - teammateMinRad) *
|
||||
((radius - minRad) / (maxRad - minRad));
|
||||
|
||||
const teamColors = Object.values(ColoredTeams);
|
||||
for (const teammate of teammates) {
|
||||
const center = teammate.nameLocation();
|
||||
if (!center) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const team = teammate.team();
|
||||
let baseColor: Colord;
|
||||
let breathingColor: Colord;
|
||||
|
||||
if (team !== null && teamColors.includes(team)) {
|
||||
baseColor = this.theme.teamColor(team).alpha(0.5);
|
||||
breathingColor = this.theme.teamColor(team).alpha(0.5);
|
||||
} else {
|
||||
baseColor = this.theme.spawnHighlightTeamColor();
|
||||
breathingColor = this.theme.spawnHighlightTeamColor();
|
||||
}
|
||||
|
||||
this.drawBreathingRing(
|
||||
center.x,
|
||||
center.y,
|
||||
teammateMinRad,
|
||||
teammateMaxRad,
|
||||
teammateRadius,
|
||||
baseColor,
|
||||
breathingColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
|
||||
@@ -130,9 +130,9 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="hidden 2xl:flex lg:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] 2xl:flex-row xl:flex-col lg:flex-col 2xl:gap-5 xl:gap-2 lg:gap-2 justify-center items-center"
|
||||
class="hidden 2xl:flex lg:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-1100 2xl:flex-row xl:flex-col lg:flex-col 2xl:gap-5 xl:gap-2 lg:gap-2 justify-center items-center"
|
||||
>
|
||||
<div class="bg-gray-800/70 backdrop-blur-sm rounded-lg p-0.5">
|
||||
<div class="bg-gray-800/70 backdrop-blur-xs rounded-lg p-0.5">
|
||||
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1 w-fit">
|
||||
${this.renderUnitItem(
|
||||
cityIcon,
|
||||
@@ -178,7 +178,7 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-800/70 backdrop-blur-sm rounded-lg p-0.5 w-fit">
|
||||
<div class="bg-gray-800/70 backdrop-blur-xs rounded-lg p-0.5 w-fit">
|
||||
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1">
|
||||
${this.renderUnitItem(
|
||||
warshipIcon,
|
||||
@@ -242,7 +242,7 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
${hovered
|
||||
? html`
|
||||
<div
|
||||
class="absolute -top-[250%] left-1/2 -translate-x-1/2 text-gray-200 text-center w-max text-xs bg-gray-800/90 backdrop-blur-sm rounded p-1 z-20 shadow-lg pointer-events-none"
|
||||
class="absolute -top-[250%] left-1/2 -translate-x-1/2 text-gray-200 text-center w-max text-xs bg-gray-800/90 backdrop-blur-xs rounded-sm p-1 z-20 shadow-lg pointer-events-none"
|
||||
>
|
||||
<div class="font-bold text-sm mb-1">
|
||||
${translateText(
|
||||
@@ -264,9 +264,9 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
<div
|
||||
class="${this.canBuild(unitType)
|
||||
? ""
|
||||
: "opacity-40"} border border-slate-500 rounded pr-2 pb-1 flex items-center gap-2 cursor-pointer
|
||||
: "opacity-40"} border border-slate-500 rounded-sm pr-2 pb-1 flex items-center gap-2 cursor-pointer
|
||||
${selected ? "hover:bg-gray-400/10" : "hover:bg-gray-800"}
|
||||
rounded text-white ${selected ? "bg-slate-400/20" : ""}"
|
||||
rounded-sm text-white ${selected ? "bg-slate-400/20" : ""}"
|
||||
@click=${() => {
|
||||
if (selected) {
|
||||
this.uiState.ghostStructure = null;
|
||||
@@ -302,11 +302,7 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
${hotkey.toUpperCase()}
|
||||
</div>`}
|
||||
<div class="flex items-center gap-1 pt-1">
|
||||
<img
|
||||
src=${icon}
|
||||
alt=${structureKey}
|
||||
style="vertical-align: middle; width: 24px; height: 24px;"
|
||||
/>
|
||||
<img src=${icon} alt=${structureKey} class="align-middle size-6" />
|
||||
${number !== null ? renderNumber(number) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
return html`
|
||||
<div
|
||||
class="${this.isVisible
|
||||
? "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gray-800/70 p-6 rounded-lg z-[9999] shadow-2xl backdrop-blur-sm text-white w-[350px] max-w-[90%] md:w-[700px] md:max-w-[700px] animate-fadeIn"
|
||||
? "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gray-800/70 p-6 shrink-0 rounded-lg z-9999 shadow-2xl backdrop-blur-xs text-white w-87.5 max-w-[90%] md:w-175 animate-fadeIn"
|
||||
: "hidden"}"
|
||||
>
|
||||
<h2 class="m-0 mb-4 text-[26px] text-center text-white">
|
||||
@@ -70,13 +70,13 @@ export class WinModal extends LitElement implements Layer {
|
||||
>
|
||||
<button
|
||||
@click=${this._handleExit}
|
||||
class="flex-1 px-3 py-3 text-base cursor-pointer bg-blue-500/60 text-white border-0 rounded transition-all duration-200 hover:bg-blue-500/80 hover:-translate-y-px active:translate-y-px"
|
||||
class="flex-1 px-3 py-3 text-base cursor-pointer bg-blue-500/60 text-white border-0 rounded-sm transition-all duration-200 hover:bg-blue-500/80 hover:-translate-y-px active:translate-y-px"
|
||||
>
|
||||
${translateText("win_modal.exit")}
|
||||
</button>
|
||||
<button
|
||||
@click=${this.hide}
|
||||
class="flex-1 px-3 py-3 text-base cursor-pointer bg-blue-500/60 text-white border-0 rounded transition-all duration-200 hover:bg-blue-500/80 hover:-translate-y-px active:translate-y-px"
|
||||
class="flex-1 px-3 py-3 text-base cursor-pointer bg-blue-500/60 text-white border-0 rounded-sm transition-all duration-200 hover:bg-blue-500/80 hover:-translate-y-px active:translate-y-px"
|
||||
>
|
||||
${this.isWin
|
||||
? translateText("win_modal.keep")
|
||||
@@ -123,13 +123,14 @@ export class WinModal extends LitElement implements Layer {
|
||||
|
||||
renderYoutubeTutorial() {
|
||||
return html`
|
||||
<div class="text-center mb-6 bg-black/30 p-2.5 rounded">
|
||||
<div class="text-center mb-6 bg-black/30 p-2.5 rounded-sm">
|
||||
<h3 class="text-xl font-semibold text-white mb-3">
|
||||
${translateText("win_modal.youtube_tutorial")}
|
||||
</h3>
|
||||
<div class="relative w-full" style="padding-bottom: 56.25%;">
|
||||
<!-- 56.25% = 9:16 -->
|
||||
<div class="relative w-full pb-[56.25%]">
|
||||
<iframe
|
||||
class="absolute top-0 left-0 w-full h-full rounded"
|
||||
class="absolute top-0 left-0 w-full h-full rounded-sm"
|
||||
src="${this.isVisible
|
||||
? "https://www.youtube.com/embed/EN2oOog3pSs"
|
||||
: ""}"
|
||||
@@ -145,7 +146,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
|
||||
renderPatternButton() {
|
||||
return html`
|
||||
<div class="text-center mb-6 bg-black/30 p-2.5 rounded">
|
||||
<div class="text-center mb-6 bg-black/30 p-2.5 rounded-sm">
|
||||
<h3 class="text-xl font-semibold text-white mb-3">
|
||||
${translateText("win_modal.support_openfront")}
|
||||
</h3>
|
||||
@@ -215,7 +216,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
steamWishlist(): TemplateResult {
|
||||
return html`<p class="m-0 mb-5 text-center bg-black/30 p-2.5 rounded">
|
||||
return html`<p class="m-0 mb-5 text-center bg-black/30 p-2.5 rounded-sm">
|
||||
<a
|
||||
href="https://store.steampowered.com/app/3560670"
|
||||
target="_blank"
|
||||
@@ -229,7 +230,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
|
||||
discordDisplay(): TemplateResult {
|
||||
return html`
|
||||
<div class="text-center mb-6 bg-black/30 p-2.5 rounded">
|
||||
<div class="text-center mb-6 bg-black/30 p-2.5 rounded-sm">
|
||||
<h3 class="text-xl font-semibold text-white mb-3">
|
||||
${translateText("win_modal.join_discord")}
|
||||
</h3>
|
||||
@@ -240,7 +241,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
href="https://discord.com/invite/openfront"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-block px-6 py-3 bg-indigo-600 text-white rounded font-semibold transition-all duration-200 hover:bg-indigo-700 hover:-translate-y-px no-underline"
|
||||
class="inline-block px-6 py-3 bg-indigo-600 text-white rounded-sm font-semibold transition-all duration-200 hover:bg-indigo-700 hover:-translate-y-px no-underline"
|
||||
>
|
||||
${translateText("win_modal.join_server")}
|
||||
</a>
|
||||
|
||||
+32
-3
@@ -1,6 +1,35 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant hover (&:hover);
|
||||
|
||||
@theme {
|
||||
--default-ring-width: 3px;
|
||||
--default-ring-color: var(--color-blue-500);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
dialog {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-box-sizing: border-box;
|
||||
|
||||
@@ -28,8 +28,9 @@ export function renderUnitTypeOptions({
|
||||
return unitOptions.map(
|
||||
({ type, translationKey }) => html`
|
||||
<label
|
||||
class="option-card ${disabledUnits.includes(type) ? "" : "selected"}"
|
||||
style="width: 140px;"
|
||||
class="option-card ${disabledUnits.includes(type)
|
||||
? ""
|
||||
: "selected"} w-35"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
@@ -40,7 +41,7 @@ export function renderUnitTypeOptions({
|
||||
toggleUnit(type, checked);
|
||||
}}
|
||||
/>
|
||||
<div class="option-card-title" style="text-align: center;">
|
||||
<div class="option-card-title text-center">
|
||||
${translateText(translationKey)}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -171,6 +171,12 @@ export const GameConfigSchema = z.object({
|
||||
gameType: z.enum(GameType),
|
||||
gameMode: z.enum(GameMode),
|
||||
gameMapSize: z.enum(GameMapSize),
|
||||
publicGameModifiers: z
|
||||
.object({
|
||||
isCompact: z.boolean(),
|
||||
isRandomSpawn: z.boolean(),
|
||||
})
|
||||
.optional(),
|
||||
disableNations: z.boolean(),
|
||||
bots: z.number().int().min(0).max(400),
|
||||
infiniteGold: z.boolean(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Gold,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PublicGameModifiers,
|
||||
Team,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
@@ -34,6 +35,7 @@ export interface ServerConfig {
|
||||
map: GameMapType,
|
||||
mode: GameMode,
|
||||
numPlayerTeams: TeamCountConfig | undefined,
|
||||
isCompactMap?: boolean,
|
||||
): number;
|
||||
numWorkers(): number;
|
||||
workerIndex(gameID: GameID): number;
|
||||
@@ -57,6 +59,8 @@ export interface ServerConfig {
|
||||
stripePublishableKey(): string;
|
||||
allowedFlares(): string[] | undefined;
|
||||
enableMatchmaking(): boolean;
|
||||
getRandomPublicGameModifiers(): PublicGameModifiers;
|
||||
supportsCompactMapForTeams(map: GameMapType): boolean;
|
||||
}
|
||||
|
||||
export interface NukeMagnitude {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
PublicGameModifiers,
|
||||
Quads,
|
||||
TerrainType,
|
||||
TerraNullius,
|
||||
@@ -89,6 +90,7 @@ const numPlayersConfig = {
|
||||
[GameMapType.StraitOfHormuz]: [40, 36, 30],
|
||||
[GameMapType.Surrounded]: [42, 28, 14], // 3, 2, 1 player(s) per island
|
||||
[GameMapType.Didier]: [100, 70, 50],
|
||||
[GameMapType.AmazonRiver]: [50, 40, 30],
|
||||
} as const satisfies Record<GameMapType, [number, number, number]>;
|
||||
|
||||
export abstract class DefaultServerConfig implements ServerConfig {
|
||||
@@ -175,11 +177,16 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
map: GameMapType,
|
||||
mode: GameMode,
|
||||
numPlayerTeams: TeamCountConfig | undefined,
|
||||
isCompactMap?: boolean,
|
||||
): number {
|
||||
const [l, m, s] = numPlayersConfig[map] ?? [50, 30, 20];
|
||||
const r = Math.random();
|
||||
const base = r < 0.3 ? l : r < 0.6 ? m : s;
|
||||
let p = Math.min(mode === GameMode.Team ? Math.ceil(base * 1.5) : base, l);
|
||||
// Apply compact map 75% player reduction
|
||||
if (isCompactMap) {
|
||||
p = Math.max(3, Math.floor(p * 0.25));
|
||||
}
|
||||
if (numPlayerTeams === undefined) return p;
|
||||
switch (numPlayerTeams) {
|
||||
case Duos:
|
||||
@@ -217,6 +224,20 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
enableMatchmaking(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getRandomPublicGameModifiers(): PublicGameModifiers {
|
||||
return {
|
||||
isRandomSpawn: Math.random() < 0.1, // 10% chance
|
||||
isCompact: Math.random() < 0.05, // 5% chance
|
||||
};
|
||||
}
|
||||
|
||||
supportsCompactMapForTeams(map: GameMapType): boolean {
|
||||
// Maps with smallest player count < 50 don't support compact map in team games
|
||||
// The smallest player count is the 3rd number in numPlayersConfig
|
||||
const [, , smallest] = numPlayersConfig[map] ?? [50, 30, 20];
|
||||
return smallest >= 50;
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultConfig implements Config {
|
||||
|
||||
@@ -111,10 +111,16 @@ export enum GameMapType {
|
||||
StraitOfHormuz = "Strait of Hormuz",
|
||||
Surrounded = "Surrounded",
|
||||
Didier = "Didier",
|
||||
AmazonRiver = "Amazon River",
|
||||
}
|
||||
|
||||
export type GameMapName = keyof typeof GameMapType;
|
||||
|
||||
/** Maps that have unusual thumbnail dimensions requiring object-fit: cover */
|
||||
export function hasUnusualThumbnailSize(map: GameMapType): boolean {
|
||||
return map === GameMapType.AmazonRiver;
|
||||
}
|
||||
|
||||
export const mapCategories: Record<string, GameMapType[]> = {
|
||||
continental: [
|
||||
GameMapType.World,
|
||||
@@ -151,6 +157,7 @@ export const mapCategories: Record<string, GameMapType[]> = {
|
||||
GameMapType.Lemnos,
|
||||
GameMapType.TwoLakes,
|
||||
GameMapType.StraitOfHormuz,
|
||||
GameMapType.AmazonRiver,
|
||||
],
|
||||
fantasy: [
|
||||
GameMapType.Pangaea,
|
||||
@@ -186,6 +193,11 @@ export enum GameMapSize {
|
||||
Normal = "Normal",
|
||||
}
|
||||
|
||||
export interface PublicGameModifiers {
|
||||
isCompact: boolean;
|
||||
isRandomSpawn: boolean;
|
||||
}
|
||||
|
||||
export interface UnitInfo {
|
||||
cost: (game: Game, player: Player) => Gold;
|
||||
// Determines if its owner changes when its tile is conquered.
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PseudoRandom } from "../PseudoRandom";
|
||||
import { GameStartInfo } from "../Schemas";
|
||||
import {
|
||||
Cell,
|
||||
GameMapSize,
|
||||
GameMode,
|
||||
GameType,
|
||||
HumansVsNations,
|
||||
@@ -14,6 +15,7 @@ import { Nation as ManifestNation } from "./TerrainMapLoader";
|
||||
/**
|
||||
* Creates the nations array for a game, handling HumansVsNations mode specially.
|
||||
* In HumansVsNations mode, the number of nations matches the number of human players to ensure fair gameplay.
|
||||
* For compact maps, only 25% of the nations are used.
|
||||
*/
|
||||
export function createNationsForGame(
|
||||
gameStart: GameStartInfo,
|
||||
@@ -31,13 +33,23 @@ export function createNationsForGame(
|
||||
new PlayerInfo(n.name, PlayerType.Nation, null, random.nextID()),
|
||||
);
|
||||
|
||||
const isCompactMap = gameStart.config.gameMapSize === GameMapSize.Compact;
|
||||
|
||||
const isHumansVsNations =
|
||||
gameStart.config.gameMode === GameMode.Team &&
|
||||
gameStart.config.playerTeams === HumansVsNations;
|
||||
|
||||
// For non-HumansVsNations modes, simply use the manifest nations
|
||||
// For compact maps, use only 25% of nations (minimum 1)
|
||||
let effectiveNations = manifestNations;
|
||||
if (isCompactMap && !isHumansVsNations) {
|
||||
const targetCount = getCompactMapNationCount(manifestNations.length, true);
|
||||
const shuffled = random.shuffleArray(manifestNations);
|
||||
effectiveNations = shuffled.slice(0, targetCount);
|
||||
}
|
||||
|
||||
// For non-HumansVsNations modes, simply use the effective nations
|
||||
if (!isHumansVsNations) {
|
||||
return manifestNations.map(toNation);
|
||||
return effectiveNations.map(toNation);
|
||||
}
|
||||
|
||||
// HumansVsNations mode: balance nation count to match human count
|
||||
@@ -71,6 +83,20 @@ export function createNationsForGame(
|
||||
return nations;
|
||||
}
|
||||
|
||||
// For compact maps, only 25% of nations are used (minimum 1).
|
||||
export function getCompactMapNationCount(
|
||||
manifestNationCount: number,
|
||||
isCompactMap: boolean,
|
||||
): number {
|
||||
if (manifestNationCount === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (isCompactMap) {
|
||||
return Math.max(1, Math.floor(manifestNationCount * 0.25));
|
||||
}
|
||||
return manifestNationCount;
|
||||
}
|
||||
|
||||
const PLURAL_NOUN = Symbol("plural!");
|
||||
const NOUN = Symbol("noun!");
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ const frequency: Partial<Record<GameMapName, number>> = {
|
||||
StraitOfHormuz: 4,
|
||||
Surrounded: 4,
|
||||
Didier: 2,
|
||||
AmazonRiver: 3,
|
||||
};
|
||||
|
||||
interface MapWithMode {
|
||||
@@ -92,25 +93,43 @@ export class MapPlaylist {
|
||||
const playerTeams =
|
||||
mode === GameMode.Team ? this.getTeamCount() : undefined;
|
||||
|
||||
let { isCompact, isRandomSpawn } = config.getRandomPublicGameModifiers();
|
||||
|
||||
// Duos, Trios, and Quads should not get random spawn (as it defeats the purpose)
|
||||
if (
|
||||
playerTeams === Duos ||
|
||||
playerTeams === Trios ||
|
||||
playerTeams === Quads
|
||||
) {
|
||||
isRandomSpawn = false;
|
||||
}
|
||||
|
||||
// Maps with smallest player count < 50 don't support compact map in team games
|
||||
// The smallest player count is the 3rd number in numPlayersConfig
|
||||
if (mode === GameMode.Team && !config.supportsCompactMapForTeams(map)) {
|
||||
isCompact = false;
|
||||
}
|
||||
|
||||
// Create the default public game config (from your GameManager)
|
||||
return {
|
||||
donateGold: mode === GameMode.Team,
|
||||
donateTroops: mode === GameMode.Team,
|
||||
gameMap: map,
|
||||
maxPlayers: config.lobbyMaxPlayers(map, mode, playerTeams),
|
||||
maxPlayers: config.lobbyMaxPlayers(map, mode, playerTeams, isCompact),
|
||||
gameType: GameType.Public,
|
||||
gameMapSize: GameMapSize.Normal,
|
||||
gameMapSize: isCompact ? GameMapSize.Compact : GameMapSize.Normal,
|
||||
publicGameModifiers: { isCompact, isRandomSpawn },
|
||||
difficulty:
|
||||
playerTeams === HumansVsNations ? Difficulty.Hard : Difficulty.Easy,
|
||||
infiniteGold: false,
|
||||
infiniteTroops: false,
|
||||
maxTimerValue: undefined,
|
||||
instantBuild: false,
|
||||
randomSpawn: false,
|
||||
randomSpawn: isRandomSpawn,
|
||||
disableNations: mode === GameMode.Team && playerTeams !== HumansVsNations,
|
||||
gameMode: mode,
|
||||
playerTeams,
|
||||
bots: 400,
|
||||
bots: isCompact ? 100 : 400,
|
||||
spawnImmunityDuration: 5 * 10,
|
||||
disabledUnits: [],
|
||||
} satisfies GameConfig;
|
||||
|
||||
Reference in New Issue
Block a user