+ update user settings modal

This commit is contained in:
Mittani
2025-04-17 17:05:07 +02:00
parent 0f776f7970
commit 60c9be3a81
28 changed files with 625 additions and 474 deletions
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Присъединяване към частна игра",
"single_player": "Самостоятелна игра",
"instructions": "Инструкции",
"settings": "настройки",
"how_to_play": "Как се играе",
"wiki": "Уики"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "লবিতে যোগ দিন",
"single_player": "কম্পিউটার মোড",
"instructions": "নির্দেশনা",
"settings": "সেটিংস",
"how_to_play": "খেলার নিয়ম",
"wiki": "উইকি"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Lobby beitreten",
"single_player": "Einzelspieler",
"instructions": "Anleitung",
"settings": "Einstellungen",
"how_to_play": "Wie man Spielt",
"wiki": "Wiki"
},
+1
View File
@@ -11,6 +11,7 @@
"join_lobby": "main.join_lobby",
"single_player": "main.single_player",
"instructions": "main.instructions",
"settings": "main.settings",
"how_to_play": "main.how_to_play",
"wiki": "main.wiki"
},
+1
View File
@@ -12,6 +12,7 @@
"join_lobby": "Join Lobby",
"single_player": "Single Player",
"instructions": "Instructions",
"settings": "Settings",
"how_to_play": "How to Play",
"wiki": "Wiki"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Kunigi salonon",
"single_player": "Sola Ludanto",
"instructions": "Instrukcioj",
"settings": "agordojn",
"how_to_play": "Kiel ludi",
"wiki": "Vikio"
},
+1
View File
@@ -11,6 +11,7 @@
"join_lobby": "Unirse a una Partida Privada",
"single_player": "Un jugador",
"instructions": "Instrucciones",
"settings": "ajustes",
"how_to_play": "Cómo jugar",
"wiki": "Wiki"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Rejoindre un salon",
"single_player": "Mode solo",
"instructions": "Instructions",
"settings": "paramètres",
"how_to_play": "Comment jouer ?",
"wiki": "Wiki"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "लॉबी में शामिल हों",
"single_player": "कंप्यूटर मोड",
"instructions": "निर्देश",
"settings": "सेटिंग्स",
"how_to_play": "कैसे खेलें",
"wiki": "विकी"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Unisciti a una Lobby",
"single_player": "Giocatore Singolo",
"instructions": "Istruzioni",
"settings": "Istruzioni",
"how_to_play": "Come si gioca",
"wiki": "Wiki"
},
+1
View File
@@ -11,6 +11,7 @@
"join_lobby": "ロビーに参加",
"single_player": "シングルプレイヤー",
"instructions": "説明書",
"settings": "設定",
"how_to_play": "遊び方",
"wiki": "ウィキ"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Lobby toetreden",
"single_player": "Eén Speler",
"instructions": "Instructies",
"settings": "instellingen",
"how_to_play": "Hoe spelen?",
"wiki": "Wiki"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Dołącz do lobby",
"single_player": "Gra jednoosobowa",
"instructions": "Instrukcje",
"settings": "ustawienia",
"how_to_play": "Jak grać",
"wiki": "Wiki"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Entrar na Sala",
"single_player": "Um Jogador",
"instructions": "Instruções",
"settings": "configurações",
"how_to_play": "Como Jogar",
"wiki": "Wiki"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Присоединиться к лобби",
"single_player": "Одиночная игра",
"instructions": "Инструкции",
"settings": "настройки",
"how_to_play": "Как играть",
"wiki": "Вики"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Pridruži se čekaonici",
"single_player": "Igraj sam",
"instructions": "Instrukcije",
"settings": "postavke",
"how_to_play": "Kako igrati",
"wiki": "Wiki"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Lobiye Katıl",
"single_player": "Tek Oyunculu",
"instructions": "Rehber",
"settings": "ayarlar",
"how_to_play": "Nasıl Oynanır",
"wiki": "Wiki"
},
+1
View File
@@ -5,6 +5,7 @@
"join_lobby": "Приєднатися до лобі",
"single_player": "Гра наодинці",
"instructions": "Інструкції",
"settings": "налаштування",
"how_to_play": "Як грати",
"wiki": "Вікі"
},
-293
View File
@@ -1,293 +0,0 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import "./LanguageModal";
import bg from "../../resources/lang/bg.json";
import bn from "../../resources/lang/bn.json";
import de from "../../resources/lang/de.json";
import en from "../../resources/lang/en.json";
import eo from "../../resources/lang/eo.json";
import es from "../../resources/lang/es.json";
import fr from "../../resources/lang/fr.json";
import hi from "../../resources/lang/hi.json";
import it from "../../resources/lang/it.json";
import ja from "../../resources/lang/ja.json";
import nl from "../../resources/lang/nl.json";
import pl from "../../resources/lang/pl.json";
import pt_br from "../../resources/lang/pt_br.json";
import ru from "../../resources/lang/ru.json";
import sh from "../../resources/lang/sh.json";
import tr from "../../resources/lang/tr.json";
import uk from "../../resources/lang/uk.json";
@customElement("lang-selector")
export class LangSelector extends LitElement {
@state() public translations: any = {};
@state() private defaultTranslations: any = {};
@state() private currentLang: string = "en";
@state() private languageList: any[] = [];
@state() private showModal: boolean = false;
@state() private debugMode: boolean = false;
private dKeyPressed: boolean = false;
private languageMap: Record<string, any> = {
bg,
bn,
de,
en,
es,
eo,
fr,
it,
hi,
ja,
nl,
pl,
pt_br,
ru,
sh,
tr,
uk,
};
createRenderRoot() {
return this; // Use Light DOM if you prefer this
}
connectedCallback() {
super.connectedCallback();
this.setupDebugKey();
this.initializeLanguage();
}
private setupDebugKey() {
window.addEventListener("keydown", (e) => {
if (e.key.toLowerCase() === "t") this.dKeyPressed = true;
});
window.addEventListener("keyup", (e) => {
if (e.key.toLowerCase() === "t") this.dKeyPressed = false;
});
}
private async initializeLanguage() {
const locale = new Intl.Locale(navigator.language);
const defaultLang = locale.language;
const userLang = localStorage.getItem("lang") || defaultLang;
this.defaultTranslations = await this.loadLanguage("en");
this.translations = await this.loadLanguage(userLang);
this.currentLang = userLang;
await this.loadLanguageList();
this.applyTranslation(this.translations);
}
private async loadLanguage(lang: string): Promise<any> {
return Promise.resolve(this.languageMap[lang] || {});
}
private async loadLanguageList() {
try {
const data = this.languageMap;
let list: any[] = [];
const browserLang = new Intl.Locale(navigator.language).language;
for (const langCode of Object.keys(data)) {
const langData = data[langCode].lang;
if (!langData) continue;
list.push({
code: langData.lang_code ?? langCode,
native: langData.native ?? langCode,
en: langData.en ?? langCode,
svg: langData.svg ?? langCode,
});
}
let debugLang: any = null;
if (this.dKeyPressed) {
debugLang = {
code: "debug",
native: "Debug",
en: "Debug",
svg: "xx",
};
this.debugMode = true;
}
const currentLangEntry = list.find((l) => l.code === this.currentLang);
const browserLangEntry =
browserLang !== this.currentLang && browserLang !== "en"
? list.find((l) => l.code === browserLang)
: undefined;
const englishEntry =
this.currentLang !== "en"
? list.find((l) => l.code === "en")
: undefined;
list = list.filter(
(l) =>
l.code !== this.currentLang &&
l.code !== browserLang &&
l.code !== "en" &&
l.code !== "debug",
);
list.sort((a, b) => a.en.localeCompare(b.en));
const finalList: any[] = [];
if (currentLangEntry) finalList.push(currentLangEntry);
if (englishEntry) finalList.push(englishEntry);
if (browserLangEntry) finalList.push(browserLangEntry);
finalList.push(...list);
if (debugLang) finalList.push(debugLang);
this.languageList = finalList;
} catch (err) {
console.error("Failed to load language list:", err);
}
}
private async changeLanguage(lang: string) {
localStorage.setItem("lang", lang);
this.translations = await this.loadLanguage(lang);
this.currentLang = lang;
this.applyTranslation(this.translations);
this.showModal = false;
}
private applyTranslation(translations: any) {
const components = [
"single-player-modal",
"host-lobby-modal",
"join-private-lobby-modal",
"emoji-table",
"leader-board",
"build-menu",
"win-modal",
"game-starting-modal",
"top-bar",
"player-panel",
"help-modal",
"username-input",
"public-lobby",
"o-modal",
"o-button",
];
document.title = translations.main?.title || document.title;
document.querySelectorAll("[data-i18n]").forEach((element) => {
const key = element.getAttribute("data-i18n");
const keys = key?.split(".") || [];
let text = translations;
for (const k of keys) {
text = text?.[k];
if (!text) break;
}
if (!text && this.defaultTranslations) {
let fallback = this.defaultTranslations;
for (const k of keys) {
fallback = fallback?.[k];
if (!fallback) break;
}
text = fallback;
}
if (text) {
element.innerHTML = text;
} else {
console.warn(`Translation key not found: ${key}`);
}
});
components.forEach((tag) => {
document.querySelectorAll(tag).forEach((el) => {
if (typeof (el as any).requestUpdate === "function") {
(el as any).requestUpdate();
}
});
});
}
public translateText(
key: string,
params: Record<string, string | number> = {},
): string {
const keys = key.split(".");
let text: any = this.translations;
for (const k of keys) {
text = text?.[k];
if (!text) break;
}
if (!text && this.defaultTranslations) {
text = this.defaultTranslations;
for (const k of keys) {
text = text?.[k];
if (!text) return key;
}
}
for (const [param, value] of Object.entries(params)) {
text = text.replace(`{${param}}`, String(value));
}
return text;
}
private openModal() {
this.debugMode = this.dKeyPressed;
this.showModal = true;
this.loadLanguageList();
}
render() {
const currentLang =
this.languageList.find((l) => l.code === this.currentLang) ??
(this.currentLang === "debug"
? {
code: "debug",
native: "Debug",
en: "Debug",
svg: "xx",
}
: {
native: "English",
en: "English",
svg: "uk_us_flag",
});
return html`
<div class="container__row">
<button
id="lang-selector"
@click=${this.openModal}
class="text-center appearance-none w-full bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md border-none cursor-pointer transition-colors duration-300 flex items-center gap-2 justify-center"
>
<img
id="lang-flag"
class="w-6 h-4"
src="/flags/${currentLang.svg}.svg"
alt="flag"
/>
<span id="lang-name">${currentLang.native} (${currentLang.en})</span>
</button>
</div>
<language-modal
.visible=${this.showModal}
.languageList=${this.languageList}
.currentLang=${this.currentLang}
@language-selected=${(e: CustomEvent) =>
this.changeLanguage(e.detail.lang)}
@close-modal=${() => (this.showModal = false)}
></language-modal>
`;
}
}
+1 -16
View File
@@ -16,9 +16,6 @@ import GoogleAdElement from "./GoogleAdElement";
import { HelpModal } from "./HelpModal";
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
import "./LangSelector";
import { LangSelector } from "./LangSelector";
import { LanguageModal } from "./LanguageModal";
import "./PublicLobby";
import { PublicLobby } from "./PublicLobby";
import { SinglePlayerModal } from "./SinglePlayerModal";
@@ -28,6 +25,7 @@ import { UsernameInput } from "./UsernameInput";
import { generateCryptoRandomUUID } from "./Utils";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./components/baseComponents/Select";
import "./styles.css";
export interface JoinLobbyEvent {
@@ -55,19 +53,6 @@ class Client {
constructor() {}
initialize(): void {
const langSelector = document.querySelector(
"lang-selector",
) as LangSelector;
const LanguageModal = document.querySelector(
"lang-selector",
) as LanguageModal;
if (!langSelector) {
consolex.warn("Lang selector element not found");
}
if (!LanguageModal) {
consolex.warn("Language modal element not found");
}
this.flagInput = document.querySelector("flag-input") as FlagInput;
if (!this.flagInput) {
consolex.warn("Flag input element not found");
+293 -118
View File
@@ -1,6 +1,24 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import bg from "../../resources/lang/bg.json";
import bn from "../../resources/lang/bn.json";
import de from "../../resources/lang/de.json";
import en from "../../resources/lang/en.json";
import eo from "../../resources/lang/eo.json";
import es from "../../resources/lang/es.json";
import fr from "../../resources/lang/fr.json";
import hi from "../../resources/lang/hi.json";
import it from "../../resources/lang/it.json";
import ja from "../../resources/lang/ja.json";
import nl from "../../resources/lang/nl.json";
import pl from "../../resources/lang/pl.json";
import pt_br from "../../resources/lang/pt_br.json";
import ru from "../../resources/lang/ru.json";
import sh from "../../resources/lang/sh.json";
import tr from "../../resources/lang/tr.json";
import uk from "../../resources/lang/uk.json";
import { UserSettings } from "../core/game/UserSettings";
import "./components/baseComponents/Select";
import "./components/baseComponents/setting/SettingNumber";
import "./components/baseComponents/setting/SettingSlider";
import "./components/baseComponents/setting/SettingToggle";
@@ -10,29 +28,69 @@ export class UserSettingModal extends LitElement {
private userSettings: UserSettings = new UserSettings();
@state() private darkMode: boolean = this.userSettings.darkMode();
@state() public translations: any = {};
@state() private defaultTranslations: any = {};
@state() private keySequence: string[] = [];
@state() private showEasterEggSettings = false;
connectedCallback() {
super.connectedCallback();
window.addEventListener("keydown", this.handleKeyDown);
}
@state() private language = localStorage.getItem("lang") || "en";
@state() private languageList: any[] = [];
@query("o-modal") private modalEl!: HTMLElement & {
open: () => void;
close: () => void;
isModalOpen: boolean;
};
createRenderRoot() {
return this;
private languageMap: Record<string, any> = {
bg,
bn,
de,
en,
eo,
es,
fr,
hi,
it,
ja,
nl,
pl,
pt_br,
ru,
sh,
tr,
uk,
};
private async loadLanguageList() {
const data = this.languageMap;
const list: any[] = [];
for (const langCode of Object.keys(data)) {
const langData = data[langCode].lang;
if (!langData) continue;
list.push({
code: langData.lang_code ?? langCode,
native: langData.native ?? langCode,
en: langData.en ?? langCode,
svg: langData.svg ?? langCode,
});
}
list.sort((a, b) => a.en.localeCompare(b.en));
this.languageList = list;
}
disconnectedCallback() {
window.removeEventListener("keydown", this.handleKeyDown);
super.disconnectedCallback();
document.body.style.overflow = "auto";
private async initializeLanguage() {
const locale = new Intl.Locale(navigator.language);
const defaultLang = locale.language;
const userLang = localStorage.getItem("lang") || defaultLang;
this.defaultTranslations = await this.loadLanguage("en");
this.translations = await this.loadLanguage(userLang);
this.language = userLang;
await this.loadLanguageList();
this.applyTranslation(this.translations);
}
private handleKeyDown = (e: KeyboardEvent) => {
@@ -49,7 +107,6 @@ export class UserSettingModal extends LitElement {
};
private triggerEasterEgg() {
console.log("🪺 Setting~ unlocked by EVAN combo!");
this.showEasterEggSettings = true;
const popup = document.createElement("div");
popup.className = "easter-egg-popup";
@@ -61,23 +118,65 @@ export class UserSettingModal extends LitElement {
}, 5000);
}
toggleDarkMode(e: CustomEvent<{ checked: boolean }>) {
const enabled = e.detail?.checked;
private applyTranslation(translations: any) {
const components = [
"single-player-modal",
"host-lobby-modal",
"join-private-lobby-modal",
"emoji-table",
"leader-board",
"build-menu",
"win-modal",
"game-starting-modal",
"top-bar",
"player-panel",
"help-modal",
"username-input",
"public-lobby",
"user-setting",
"setting-slider",
"setting-number",
"setting-number",
"o-modal",
"o-button",
"o-select",
];
if (typeof enabled !== "boolean") {
console.warn("Unexpected toggle event payload", e);
return;
}
document.title = translations.main?.title || document.title;
this.userSettings.set("settings.darkMode", enabled);
document.querySelectorAll("[data-i18n]").forEach((element) => {
const key = element.getAttribute("data-i18n");
const keys = key?.split(".") || [];
let text = translations;
if (enabled) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
for (const k of keys) {
text = text?.[k];
if (!text) break;
}
console.log("🌙 Dark Mode:", enabled ? "ON" : "OFF");
if (!text && this.defaultTranslations) {
let fallback = this.defaultTranslations;
for (const k of keys) {
fallback = fallback?.[k];
if (!fallback) break;
}
text = fallback;
}
if (text) {
element.innerHTML = text;
} else {
console.warn(`Translation key not found: ${key}`);
}
});
components.forEach((tag) => {
document.querySelectorAll(tag).forEach((el) => {
if (typeof (el as any).requestUpdate === "function") {
(el as any).requestUpdate();
}
});
});
}
private toggleEmojis(e: CustomEvent<{ checked: boolean }>) {
@@ -85,8 +184,6 @@ export class UserSettingModal extends LitElement {
if (typeof enabled !== "boolean") return;
this.userSettings.set("settings.emojis", enabled);
console.log("🤡 Emojis:", enabled ? "ON" : "OFF");
}
private toggleLeftClickOpensMenu(e: CustomEvent<{ checked: boolean }>) {
@@ -94,7 +191,6 @@ export class UserSettingModal extends LitElement {
if (typeof enabled !== "boolean") return;
this.userSettings.set("settings.leftClickOpensMenu", enabled);
console.log("🖱️ Left Click Opens Menu:", enabled ? "ON" : "OFF");
this.requestUpdate();
}
@@ -119,101 +215,59 @@ export class UserSettingModal extends LitElement {
}
}
render() {
return html`
<o-modal title="User Settings">
<div class="modal-overlay">
<div class="modal-content user-setting-modal">
<div class="settings-list">
<setting-toggle
label="🌙 Dark Mode"
description="Toggle the sites appearance between light and dark themes"
id="dark-mode-toggle"
.checked=${this.userSettings.darkMode()}
@change=${(e: CustomEvent<{ checked: boolean }>) =>
this.toggleDarkMode(e)}
></setting-toggle>
private async loadLanguage(lang: string): Promise<any> {
return Promise.resolve(this.languageMap[lang] || {});
}
<setting-toggle
label="😊 Emojis"
description="Toggle whether emojis are shown in game"
id="emoji-toggle"
.checked=${this.userSettings.emojis()}
@change=${this.toggleEmojis}
></setting-toggle>
private async changeLanguage(lang: string) {
localStorage.setItem("lang", lang);
this.language = lang;
this.translations = await this.loadLanguage(lang);
this.applyTranslation(this.translations);
}
<setting-toggle
label="🖱️ Left Click to Open Menu"
description="When ON, left-click opens menu and sword button attacks. When OFF, right-click attacks directly."
id="left-click-toggle"
.checked=${this.userSettings.leftClickOpensMenu()}
@change=${this.toggleLeftClickOpensMenu}
></setting-toggle>
public toggleDarkMode(e: CustomEvent<{ checked: boolean }>) {
const enabled = e.detail?.checked;
<setting-slider
label="⚔️ Attack Ratio"
description="What percentage of your troops to send in an attack (1100%)"
min="1"
max="100"
.value=${Number(
localStorage.getItem("settings.attackRatio") ?? "0.2",
) * 100}
@change=${this.sliderAttackRatio}
></setting-slider>
if (typeof enabled !== "boolean") {
console.warn("Unexpected toggle event payload", e);
return;
}
<setting-slider
label="🪖🛠️ Troops and Workers Ratio"
description="Adjust the balance between troops (for combat) and workers (for gold production) (1100%)"
min="1"
max="100"
.value=${Number(
localStorage.getItem("settings.troopRatio") ?? "0.95",
) * 100}
@change=${this.sliderTroopRatio}
></setting-slider>
this.userSettings.set("settings.darkMode", enabled);
${this.showEasterEggSettings
? html`
<setting-slider
label="Writing Speed Multiplier"
description="Adjust how fast you pretend to code (x1x100)"
min="0"
max="100"
value="40"
easter="true"
@change=${(e: CustomEvent) => {
const value = e.detail?.value;
if (typeof value !== "undefined") {
console.log("Changed:", value);
} else {
console.warn("Slider event missing detail.value", e);
}
}}
></setting-slider>
if (enabled) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
<setting-number
label="Bug Count"
description="How many bugs you're okay with (01000, emotionally)"
value="100"
min="0"
max="1000"
easter="true"
@change=${(e: CustomEvent) => {
const value = e.detail?.value;
if (typeof value !== "undefined") {
console.log("Changed:", value);
} else {
console.warn("Slider event missing detail.value", e);
}
}}
></setting-number>
`
: null}
</div>
</div>
</div>
</o-modal>
`;
public translateText(
key: string,
params: Record<string, string | number> = {},
): string {
const keys = key.split(".");
let text: any = this.translations;
for (const k of keys) {
text = text?.[k];
if (!text) break;
}
if (!text && this.defaultTranslations) {
text = this.defaultTranslations;
for (const k of keys) {
text = text?.[k];
if (!text) return key;
}
}
for (const [param, value] of Object.entries(params)) {
text = text.replace(`{${param}}`, String(value));
}
return text;
}
public open() {
@@ -223,4 +277,125 @@ export class UserSettingModal extends LitElement {
public close() {
this.modalEl?.close();
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.loadLanguageList();
this.initializeLanguage();
window.addEventListener("keydown", this.handleKeyDown);
}
disconnectedCallback() {
window.removeEventListener("keydown", this.handleKeyDown);
super.disconnectedCallback();
document.body.style.overflow = "auto";
}
render() {
return html`
<o-modal title="User Settings">
<div class="container">
<o-select
id="lang-selector"
label="Language"
translationKey="select_lang.title"
.items=${this.languageList.map((l) => ({
label: `${l.native} (${l.en})`,
value: l.code,
image: `/flags/${l.svg}.svg`,
}))}
.selectedValue=${this.language}
.showImageWithLabel=${true}
@o-select-change=${(e: CustomEvent) =>
this.changeLanguage(e.detail)}
></o-select>
<setting-toggle
label="🌙 Dark Mode"
description="Toggle the sites appearance between light and dark themes"
id="dark-mode-toggle"
.checked=${this.userSettings.darkMode()}
@change=${(e: CustomEvent<{ checked: boolean }>) =>
this.toggleDarkMode(e)}
></setting-toggle>
<setting-toggle
label="😊 Emojis"
description="Toggle whether emojis are shown in game"
id="emoji-toggle"
.checked=${this.userSettings.emojis()}
@change=${this.toggleEmojis}
></setting-toggle>
<setting-toggle
label="🖱️ Left Click to Open Menu"
description="When ON, left-click opens menu and sword button attacks. When OFF, right-click attacks directly."
id="left-click-toggle"
.checked=${this.userSettings.leftClickOpensMenu()}
@change=${this.toggleLeftClickOpensMenu}
></setting-toggle>
<setting-slider
label="⚔️ Attack Ratio"
description="What percentage of your troops to send in an attack (1100%)"
min="1"
max="100"
.value=${Number(
localStorage.getItem("settings.attackRatio") ?? "0.2",
) * 100}
@change=${this.sliderAttackRatio}
></setting-slider>
<setting-slider
label="🪖🛠️ Troops and Workers Ratio"
description="Adjust the balance between troops (for combat) and workers (for gold production) (1100%)"
min="1"
max="100"
.value=${Number(
localStorage.getItem("settings.troopRatio") ?? "0.95",
) * 100}
@change=${this.sliderTroopRatio}
></setting-slider>
${this.showEasterEggSettings
? html`
<setting-slider
label="Writing Speed Multiplier"
description="Adjust how fast you pretend to code (x1x100)"
min="0"
max="100"
value="40"
easter="true"
@change=${(e: CustomEvent) => {
const value = e.detail?.value;
if (typeof value === "undefined") {
console.warn("Slider event missing detail.value", e);
}
}}
></setting-slider>
<setting-number
label="Bug Count"
description="How many bugs you're okay with (01000, emotionally)"
value="100"
min="0"
max="1000"
easter="true"
@change=${(e: CustomEvent) => {
const value = e.detail?.value;
if (typeof value === "undefined") {
console.warn("Slider event missing detail.value", e);
}
}}
></setting-number>
`
: null}
</div>
</o-modal>
`;
}
}
+4 -2
View File
@@ -1,4 +1,4 @@
import { LangSelector } from "./LangSelector";
import { UserSettingModal } from "./UserSettingModal";
export function renderTroops(troops: number): string {
return renderNumber(troops / 10);
@@ -78,7 +78,9 @@ export const translateText = (
key: string,
params: Record<string, string | number> = {},
): string => {
const langSelector = document.querySelector("lang-selector") as LangSelector;
const langSelector = document.querySelector(
"user-setting",
) as UserSettingModal;
if (!langSelector) {
console.warn("LangSelector not found in DOM");
return key;
@@ -39,14 +39,21 @@ export class OModal extends LitElement {
background: #000000a1;
text-align: center;
color: #fff;
padding: 1rem 2.4rem 1rem 1.4rem;
padding: 0.6rem 2.4rem 0.6rem 1.4rem;
@media (min-width: 1024px) {
padding: 1rem 2.4rem 1rem 1.4rem;
}
}
.c-modal__close {
cursor: pointer;
position: absolute;
right: 1rem;
top: 1rem;
top: 0.6rem;
@media (min-width: 1024px) {
top: 1rem;
}
}
.c-modal__content {
@@ -0,0 +1,260 @@
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { translateText } from "../../Utils";
interface selectItems {
label: string;
value: any;
image?: string;
}
@customElement("o-select")
export class OSelect extends LitElement {
/**
* Array of selectable items.
*/
@property({ type: Array }) items: selectItems[] = [];
/**
* Currently selected value.
*/
@property({ type: String }) selectedValue: string = "";
/**
* Error message to display.
*/
@property({ type: String }) errorMessage: string = "";
/**
* Enables search filtering in the dropdown.
*/
@property({ type: Boolean }) filterEnabled: boolean = false;
/**
* If true, show image next to label.
*/
@property({ type: Boolean }) showImageWithLabel: boolean = false;
/**
* Label shown above the select.
*/
@property({ type: String }) label: string = "";
@property({ type: String }) translationKey = "";
@state() private selectedItem: selectItems | null = null;
@state() private filter: string = "";
@state() private isOpen: boolean = false;
static styles = css`
.c-label {
color: var(--fontColorLight);
font-size: 14px;
}
.c-select {
position: relative;
cursor: pointer;
padding: 0 10px;
border: 1px solid transparent;
border-radius: var(--borderRadius--md);
background: #1e1e1e;
height: 50px;
&.is-error {
border-color: var(--errorColor);
+ .c-message {
color: var(--errorColor);
}
}
}
.c-select__display {
align-items: center;
color: var(--fontColorLight);
width: 100%;
display: flex;
gap: 10px;
span {
flex: 1;
}
img {
width: 24px;
}
}
.c-select__listWrapper {
border: 1px solid var(--secondaryBorderColor);
position: absolute;
left: -1px;
right: -1px;
background: var(--boxBackgroundColor);
backdrop-filter: blur(var(--blur-md));
top: 50px;
z-index: 1000;
}
.c-select__list {
list-style: none;
overflow: scroll;
max-height: 35dvh;
display: flex;
flex-direction: column;
gap: 10px;
padding: 5px;
margin: 0;
}
.c-select__item {
align-items: center;
display: flex;
min-height: 35px;
color: var(--fontColorLight);
gap: 10px;
img {
max-width: 35px;
height: auto;
}
}
.c-select__input {
outline: none;
padding: 4px 8px;
margin-bottom: 5px;
}
`;
connectedCallback() {
super.connectedCallback();
window.addEventListener("click", this._handleOutsideClick);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("click", this._handleOutsideClick);
}
willUpdate(changedProps: Map<string | number | symbol, unknown>) {
if (changedProps.has("items") || changedProps.has("selectedValue")) {
const match = this.items.find(
(item) => item.value === this.selectedValue,
);
if (match) {
this.selectedItem = match;
}
}
}
private _handleOutsideClick = (event: MouseEvent) => {
if (!this.contains(event.target as Node)) {
this.isOpen = false;
}
};
private selectItem(item: selectItems) {
this.selectedItem = item;
this.filter = "";
this.isOpen = false;
this.dispatchEvent(
new CustomEvent("o-select-change", {
detail: item.value,
bubbles: true,
composed: true,
}),
);
}
private renderSelectedDisplay() {
if (!this.selectedItem) {
return html`<span>Select</span>`;
}
const { image, label } = this.selectedItem;
if (this.showImageWithLabel) {
return html`
${image ? html`<img src="${image}" alt="${label} flag" />` : null}
<span>${label}</span>
`;
}
return image
? html`<img src="${image}" alt="${label} flag" />`
: html`<span>${label}</span>`;
}
get filteredItems() {
return this.items.filter((item) =>
item.label.toLowerCase().includes(this.filter.toLowerCase()),
);
}
render() {
return html`
<div
class="c-select ${this.errorMessage ? "is-error" : ""}"
@click=${() => (this.isOpen = !this.isOpen)}
>
${this.label
? html`<label class="c-label">
${`${this.translationKey}` === ""
? `${this.label}`
: `${translateText(this.translationKey)}`}</label
>`
: null}
<div class="c-select__display">${this.renderSelectedDisplay()}</div>
${this.isOpen
? html`
<div class="c-select__listWrapper">
${this.filterEnabled
? html`
<input
class="c-select__input"
type="text"
placeholder="Search..."
.value=${this.filter}
@input=${(e: InputEvent) => {
this.filter = (e.target as HTMLInputElement).value;
e.stopPropagation();
e.stopImmediatePropagation();
}}
@click=${(e: Event) => e.stopPropagation()}
/>
`
: null}
<ul class="c-select__list">
${this.filteredItems.map(
(item) => html`
<li
class="c-select__item"
@click=${(e: Event) => {
this.selectItem(item);
e.stopPropagation();
e.stopImmediatePropagation();
}}
>
${item.image
? html`<img
src="${item.image}"
alt="${item.label}"
/>`
: ""}
<div>${item.label}</div>
</li>
`,
)}
</ul>
</div>
`
: ""}
</div>
${this.errorMessage
? html` <div class="c-message">${this.errorMessage}</div>`
: ""}
`;
}
}
+7 -36
View File
@@ -263,26 +263,15 @@
block
secondary
></o-button>
<div class="container__row">
<lang-selector class="w-full"></lang-selector>
</div>
<o-button
id="settings-button"
title="Settings"
translationKey="main.settings"
block
secondary
></o-button>
</div>
</main>
<!-- User Setting -->
<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: 80px; height: 80px; background-color: #0075ff"
>
<img
src="../../resources/images/SettingIconWhite.svg"
alt="Settings"
style="width: 72px; height: 72px"
/>
</button>
<!-- Game components -->
<div id="customMenu" class="mt-4 sm:mt-6 lg:mt-8">
<ul></ul>
@@ -359,24 +348,6 @@
<dark-mode-button></dark-mode-button>
<user-setting></user-setting>
<multi-tab-modal></multi-tab-modal>
<div
id="language-modal"
class="fixed inset-0 bg-black bg-opacity-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"
></div>
<button
class="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"
onclick="document.getElementById('language-modal').classList.add('hidden')"
>
Close
</button>
</div>
</div>
<!-- Scripts -->
<script>
// Remove preload class after everything is loaded
+8
View File
@@ -10,6 +10,7 @@
@import url("./styles/components/modal.css");
@import url("./styles/components/setting.css");
@import url("./styles/components/controls.css");
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
@@ -228,6 +229,13 @@ label.option-card:hover {
padding: 0 16px;
}
o-select,
setting-toggle,
setting-slider,
setting-number {
width: 100%;
}
.player-tag {
display: flex;
align-items: center;
-3
View File
@@ -14,9 +14,6 @@
border: 1px solid #333;
border-radius: 10px;
padding: 12px 20px;
width: 360px !important;
max-width: 360px !important;
min-width: 360px !important;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
transition: background 0.3s ease;
gap: 12px;
+25 -4
View File
@@ -3,17 +3,38 @@
--breakPoint-tablet: 800px;
--breakPoint-Desktop: 1024px;
--blur-md: 4px;
--boxBackgroundColor: #111827cc;
--fontColor: #202020;
--borderRadius--md: 0px;
--borderRadius--lg: 2px;
--inputBackgroundColor: #fefefe;
--boxBackgroundColor: #2434574f;
--secondaryBoxBackgroundColor: #3a4d766e;
--boxShadow: inset 0px 0px 4px 2px rgba(0, 175, 255, 0.2);
--boxShadowBorder: inset 0px 0px 4px 2px rgba(0, 175, 255, 0.2);
--fontColor: #1e293b;
--fontColorLight: #fff;
--primaryColor: #2563eb;
--primaryColorHover: #1d4ed8;
--primaryColorHover: color-mix(in srgb, var(--primaryColor) 80%, transparent);
--primaryColorDisabled: linear-gradient(
to right,
rgb(74, 74, 74),
rgb(61, 61, 61)
);
--secondaryColor: #dbeafe;
--secondaryColorHover: #bfdbfe;
--secondaryColorHover: color-mix(
in srgb,
var(--secondaryColor) 80%,
transparent
);
--errorColor: #f0572d;
--errorColorTransparent: #ff005529;
--successColor: #17eb14;
--successColorBackground: #00b37a;
--borderColor: #0000004d;
--secondaryBorderColor: #333;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--space-xs: 6px;
--space-sm: 8px;
--space-md: 10px;
--space-lg: 12px;
--space-xl: 14px;
}