mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 14:09:46 +00:00
Merge branch 'main' into bomb-confirmation
This commit is contained in:
@@ -13,8 +13,6 @@ RUN --mount=type=cache,target=/root/.npm \
|
||||
# Copy only what's needed for build
|
||||
COPY tsconfig.json ./
|
||||
COPY vite.config.ts ./
|
||||
COPY tailwind.config.js ./
|
||||
COPY postcss.config.js ./
|
||||
COPY eslint.config.js ./
|
||||
COPY index.html ./
|
||||
COPY resources ./resources
|
||||
|
||||
@@ -26,8 +26,6 @@ export default [
|
||||
allowDefaultProject: [
|
||||
"__mocks__/fileMock.js",
|
||||
"eslint.config.js",
|
||||
"postcss.config.js",
|
||||
"tailwind.config.js",
|
||||
"scripts/sync-assets.mjs",
|
||||
],
|
||||
},
|
||||
|
||||
+34
-27
@@ -120,9 +120,22 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Immediate execution to prevent FOUC -->
|
||||
<!-- Immediate execution to prevent FOUC + apply persisted dark mode -->
|
||||
<script>
|
||||
document.documentElement.className = "preload";
|
||||
(function () {
|
||||
const root = document.documentElement;
|
||||
root.classList.add("preload");
|
||||
try {
|
||||
const storedDarkMode = localStorage.getItem("settings.darkMode");
|
||||
if (storedDarkMode === "true") {
|
||||
root.classList.add("dark");
|
||||
} else if (storedDarkMode === "false") {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore storage access errors.
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- CrazyGames SDK -->
|
||||
@@ -183,7 +196,7 @@
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="h-full select-none font-sans min-h-screen bg-opacity-0 bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-col"
|
||||
class="h-full select-none font-sans min-h-screen bg-black/0 bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-col"
|
||||
>
|
||||
<header class="l-header">
|
||||
<div class="l-header__content">
|
||||
@@ -251,7 +264,7 @@
|
||||
<div class="bg-image"></div>
|
||||
<div
|
||||
id="turnstile-container"
|
||||
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[99999]"
|
||||
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-99999"
|
||||
></div>
|
||||
<gutter-ads></gutter-ads>
|
||||
|
||||
@@ -265,7 +278,7 @@
|
||||
<territory-patterns-modal class="w-[20%] md:w-[15%]">
|
||||
<button
|
||||
id="territory-patterns-input-preview-button"
|
||||
class="w-full 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)] justify-center"
|
||||
class="w-full 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)] justify-center"
|
||||
title="Pick a pattern!"
|
||||
></button>
|
||||
</territory-patterns-modal>
|
||||
@@ -323,14 +336,10 @@
|
||||
<button
|
||||
id="settings-button"
|
||||
title="Settings"
|
||||
class="fixed bottom-4 right-4 z-50 rounded-full p-2 shadow-lg transition-colors duration-300 flex items-center justify-center"
|
||||
style="width: 60px; height: 60px; background-color: #0075ff"
|
||||
class="fixed bottom-4 right-4 z-50 rounded-full p-2 shadow-lg transition-colors duration-300 flex items-center justify-center size-15 bg-(--primaryColor)"
|
||||
style="--primaryColor: #0075ff"
|
||||
>
|
||||
<img
|
||||
src="/images/SettingIconWhite.svg"
|
||||
alt="Settings"
|
||||
style="width: 52px; height: 52px"
|
||||
/>
|
||||
<img src="/images/SettingIconWhite.svg" alt="Settings" class="size-18" />
|
||||
</button>
|
||||
|
||||
<!-- Game components -->
|
||||
@@ -339,22 +348,20 @@
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<div id="radialMenu" class="radial-menu"></div>
|
||||
<div class="flex gap-2 fixed right-[10px] top-[10px] z-50 flex-col">
|
||||
<div class="flex gap-2 fixed right-2.5 top-2.5 z-50 flex-col">
|
||||
<player-info-overlay></player-info-overlay>
|
||||
</div>
|
||||
<div
|
||||
class="fixed bottom-[30px] sm:bottom-auto sm:top-[20px] z-50 mx-auto max-w-max inset-x-0 items-center"
|
||||
class="fixed bottom-7.5 sm:bottom-auto sm:top-5 z-50 mx-auto max-w-max inset-x-0 items-center"
|
||||
>
|
||||
<heads-up-message></heads-up-message>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="left-0 bottom-0 sm:left-4 sm:bottom-4 w-full flex-col-reverse sm:flex-row z-50 md:w-[320px]"
|
||||
style="position: fixed; pointer-events: none"
|
||||
class="left-0 bottom-0 sm:left-4 sm:bottom-4 w-full flex-col-reverse sm:flex-row z-50 md:w-[320px] fixed pointer-events-none"
|
||||
>
|
||||
<div
|
||||
class="w-full md:w-2/3 md:fixed sm:right-0 md:bottom-0 md:flex flex-col items-end"
|
||||
style="pointer-events: none"
|
||||
class="w-full md:w-2/3 md:fixed sm:right-0 md:bottom-0 md:flex flex-col items-end pointer-events-none"
|
||||
>
|
||||
<chat-display></chat-display>
|
||||
<events-display></events-display>
|
||||
@@ -366,13 +373,13 @@
|
||||
|
||||
<!-- Footer section -->
|
||||
<footer
|
||||
class="flex justify-center px-3 py-3 md:px-6 md:py-3 bg-[var(--boxBackgroundColor)] backdrop-blur-sm"
|
||||
class="flex justify-center px-3 py-3 md:px-6 md:py-3 bg-(--boxBackgroundColor) backdrop-blur-xs"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col md:flex-row flex-nowrap justify-between items-center gap-4 md:gap-0 w-full max-w-[860px] flex-1"
|
||||
class="flex flex-col md:flex-row flex-nowrap justify-between items-center gap-4 md:gap-0 w-full max-w-215 flex-1"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col sm:flex-row gap-4 sm:gap-5 text-white/70 justify-center md:justify-start flex-shrink-0"
|
||||
class="flex flex-col sm:flex-row gap-4 sm:gap-5 text-white/70 justify-center md:justify-start shrink-0"
|
||||
>
|
||||
<a
|
||||
href="https://openfront.wiki/Main_Page"
|
||||
@@ -397,7 +404,7 @@
|
||||
<span data-i18n="main.join_discord"> Discord </span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex justify-center text-white/70 flex-shrink-0">
|
||||
<div class="flex justify-center text-white/70 shrink-0">
|
||||
<a
|
||||
href="https://github.com/openfrontio/OpenFrontIO"
|
||||
class="text-white/70 hover:text-white transition-colors duration-300 ease-in-out inline-flex items-center gap-2 whitespace-nowrap"
|
||||
@@ -414,7 +421,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col sm:flex-row gap-4 sm:gap-4 text-white/70 justify-center md:justify-end flex-shrink-0"
|
||||
class="flex flex-col sm:flex-row gap-4 sm:gap-4 text-white/70 justify-center md:justify-end shrink-0"
|
||||
>
|
||||
<a
|
||||
href="/privacy-policy.html"
|
||||
@@ -446,7 +453,7 @@
|
||||
<game-starting-modal></game-starting-modal>
|
||||
<game-top-bar></game-top-bar>
|
||||
<unit-display></unit-display>
|
||||
<div class="flex flex-col items-end fixed top-4 right-4 z-[1000] gap-2">
|
||||
<div class="flex flex-col items-end fixed top-4 right-4 z-1000 gap-2">
|
||||
<game-right-sidebar></game-right-sidebar>
|
||||
<replay-panel></replay-panel>
|
||||
</div>
|
||||
@@ -468,16 +475,16 @@
|
||||
<performance-overlay></performance-overlay>
|
||||
<div
|
||||
id="language-modal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex justify-center items-center"
|
||||
class="fixed inset-0 bg-black/50 z-50 hidden flex justify-center items-center"
|
||||
>
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 w-96 max-w-full">
|
||||
<h2 class="text-xl font-semibold mb-4">Select Language</h2>
|
||||
<div
|
||||
id="language-list"
|
||||
class="space-y-2 max-h-80 overflow-y-auto"
|
||||
class="flex flex-col gap-2 max-h-80 overflow-y-auto"
|
||||
></div>
|
||||
<button
|
||||
class="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"
|
||||
class="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-sm"
|
||||
onclick="
|
||||
document.getElementById('language-modal').classList.add('hidden')
|
||||
"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 799 KiB |
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"name": "Amazon River",
|
||||
"nations": [
|
||||
{
|
||||
"coordinates": [524, 60],
|
||||
"name": "Boa Esperança",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [298, 242],
|
||||
"name": "Santa Rosa",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [1056, 8],
|
||||
"name": "Paraíso",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [1161, 8],
|
||||
"name": "Bom Futuro",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [1662, 254],
|
||||
"name": "São Paulo de Olivença",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [1807, 8],
|
||||
"name": "São João",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [1977, 7],
|
||||
"name": "Castro Alves",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [2487, 70],
|
||||
"name": "Flora",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [2458, 217],
|
||||
"name": "Esperito Santo",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [2590, 149],
|
||||
"name": "Recreio",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [2937, 199],
|
||||
"name": "Caturiá",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [3101, 222],
|
||||
"name": "Correnteza",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [3354, 75],
|
||||
"name": "Botafogo",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [3405, 237],
|
||||
"name": "São João",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [3744, 228],
|
||||
"name": "Cajual",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [3861, 244],
|
||||
"name": "União",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [4409, 211],
|
||||
"name": "Amaturá",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [4361, 55],
|
||||
"name": "Monte Cristo",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [4649, 54],
|
||||
"name": "Floresta",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [5399, 128],
|
||||
"name": "Vargem Grande",
|
||||
"flag": "br"
|
||||
},
|
||||
{
|
||||
"coordinates": [5373, 7],
|
||||
"name": "Baia",
|
||||
"flag": "br"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -63,6 +63,7 @@ var maps = []struct {
|
||||
{Name: "lemnos"},
|
||||
{Name: "twolakes"},
|
||||
{Name: "didier"},
|
||||
{Name: "amazonriver"},
|
||||
{Name: "big_plains", IsTest: true},
|
||||
{Name: "half_land_half_ocean", IsTest: true},
|
||||
{Name: "ocean_and_land", IsTest: true},
|
||||
|
||||
Generated
+619
-793
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -32,6 +32,7 @@
|
||||
"@datastructures-js/priority-queue": "^6.3.3",
|
||||
"@eslint/compat": "^1.2.7",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/chai": "^4.3.17",
|
||||
"@types/d3": "^7.4.3",
|
||||
@@ -60,6 +61,7 @@
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-formatter-gha": "^1.5.2",
|
||||
"glob": "^13.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.4.0",
|
||||
@@ -69,14 +71,13 @@
|
||||
"mrmime": "^2.0.0",
|
||||
"pixi-filters": "^6.1.4",
|
||||
"pixi.js": "^8.11.0",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-sh": "^0.17.4",
|
||||
"protobufjs": "^7.5.3",
|
||||
"sinon": "^21.0.0",
|
||||
"sinon-chai": "^4.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.26.0",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -254,7 +254,8 @@
|
||||
"twolakes": "Two Lakes",
|
||||
"straitofhormuz": "Strait of Hormuz",
|
||||
"surrounded": "Surrounded",
|
||||
"didier": "Didier"
|
||||
"didier": "Didier",
|
||||
"amazonriver": "Amazon River"
|
||||
},
|
||||
"map_categories": {
|
||||
"continental": "Continental",
|
||||
@@ -362,6 +363,10 @@
|
||||
"ffa": "Free for All",
|
||||
"teams": "Teams"
|
||||
},
|
||||
"public_game_modifier": {
|
||||
"random_spawn": "Random Spawn",
|
||||
"compact_map": "Compact Map"
|
||||
},
|
||||
"select_lang": {
|
||||
"title": "Select Language"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"map": {
|
||||
"height": 280,
|
||||
"num_land_tiles": 1172808,
|
||||
"width": 5536
|
||||
},
|
||||
"map16x": {
|
||||
"height": 70,
|
||||
"num_land_tiles": 71047,
|
||||
"width": 1384
|
||||
},
|
||||
"map4x": {
|
||||
"height": 140,
|
||||
"num_land_tiles": 290101,
|
||||
"width": 2768
|
||||
},
|
||||
"name": "Amazon River",
|
||||
"nations": [
|
||||
{
|
||||
"coordinates": [524, 60],
|
||||
"flag": "br",
|
||||
"name": "Boa Esperança"
|
||||
},
|
||||
{
|
||||
"coordinates": [298, 242],
|
||||
"flag": "br",
|
||||
"name": "Santa Rosa"
|
||||
},
|
||||
{
|
||||
"coordinates": [1056, 8],
|
||||
"flag": "br",
|
||||
"name": "Paraíso"
|
||||
},
|
||||
{
|
||||
"coordinates": [1161, 8],
|
||||
"flag": "br",
|
||||
"name": "Bom Futuro"
|
||||
},
|
||||
{
|
||||
"coordinates": [1662, 254],
|
||||
"flag": "br",
|
||||
"name": "São Paulo de Olivença"
|
||||
},
|
||||
{
|
||||
"coordinates": [1807, 8],
|
||||
"flag": "br",
|
||||
"name": "São João"
|
||||
},
|
||||
{
|
||||
"coordinates": [1977, 7],
|
||||
"flag": "br",
|
||||
"name": "Castro Alves"
|
||||
},
|
||||
{
|
||||
"coordinates": [2487, 70],
|
||||
"flag": "br",
|
||||
"name": "Flora"
|
||||
},
|
||||
{
|
||||
"coordinates": [2458, 217],
|
||||
"flag": "br",
|
||||
"name": "Esperito Santo"
|
||||
},
|
||||
{
|
||||
"coordinates": [2590, 149],
|
||||
"flag": "br",
|
||||
"name": "Recreio"
|
||||
},
|
||||
{
|
||||
"coordinates": [2937, 199],
|
||||
"flag": "br",
|
||||
"name": "Caturiá"
|
||||
},
|
||||
{
|
||||
"coordinates": [3101, 222],
|
||||
"flag": "br",
|
||||
"name": "Correnteza"
|
||||
},
|
||||
{
|
||||
"coordinates": [3354, 75],
|
||||
"flag": "br",
|
||||
"name": "Botafogo"
|
||||
},
|
||||
{
|
||||
"coordinates": [3405, 237],
|
||||
"flag": "br",
|
||||
"name": "São João"
|
||||
},
|
||||
{
|
||||
"coordinates": [3744, 228],
|
||||
"flag": "br",
|
||||
"name": "Cajual"
|
||||
},
|
||||
{
|
||||
"coordinates": [3861, 244],
|
||||
"flag": "br",
|
||||
"name": "União"
|
||||
},
|
||||
{
|
||||
"coordinates": [4409, 211],
|
||||
"flag": "br",
|
||||
"name": "Amaturá"
|
||||
},
|
||||
{
|
||||
"coordinates": [4361, 55],
|
||||
"flag": "br",
|
||||
"name": "Monte Cristo"
|
||||
},
|
||||
{
|
||||
"coordinates": [4649, 54],
|
||||
"flag": "br",
|
||||
"name": "Floresta"
|
||||
},
|
||||
{
|
||||
"coordinates": [5399, 128],
|
||||
"flag": "br",
|
||||
"name": "Vargem Grande"
|
||||
},
|
||||
{
|
||||
"coordinates": [5373, 7],
|
||||
"flag": "br",
|
||||
"name": "Baia"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
@@ -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;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// tailwind.config.js
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{html,ts,js}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
darkMode: "class",
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { JWK } from "jose";
|
||||
import { GameEnv, ServerConfig } from "../../src/core/configuration/Config";
|
||||
import { GameMapType } from "../../src/core/game/Game";
|
||||
import { GameMapType, PublicGameModifiers } from "../../src/core/game/Game";
|
||||
import { GameID } from "../../src/core/Schemas";
|
||||
|
||||
export class TestServerConfig implements ServerConfig {
|
||||
@@ -82,4 +82,10 @@ export class TestServerConfig implements ServerConfig {
|
||||
gitCommit(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getRandomPublicGameModifiers(): PublicGameModifiers {
|
||||
return { isCompact: false, isRandomSpawn: false };
|
||||
}
|
||||
supportsCompactMapForTeams(map: GameMapType): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
// Modules
|
||||
"module": "ESNext",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"resources/*": ["resources/*"]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
@@ -67,6 +68,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
|
||||
define: {
|
||||
|
||||
Reference in New Issue
Block a user