diff --git a/resources/images/SettingIconWhite.svg b/resources/images/SettingIconWhite.svg
new file mode 100644
index 000000000..17afdad3d
--- /dev/null
+++ b/resources/images/SettingIconWhite.svg
@@ -0,0 +1,25 @@
+
+
\ No newline at end of file
diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts
index fad943459..017f979a5 100644
--- a/src/client/ClientGameRunner.ts
+++ b/src/client/ClientGameRunner.ts
@@ -215,6 +215,7 @@ export class ClientGameRunner {
public start() {
consolex.log("starting client game");
+
this.isActive = true;
this.lastMessageTime = Date.now();
setTimeout(() => {
diff --git a/src/client/Main.ts b/src/client/Main.ts
index 6b4b623c9..567a6673b 100644
--- a/src/client/Main.ts
+++ b/src/client/Main.ts
@@ -22,6 +22,7 @@ import { LanguageModal } from "./LanguageModal";
import "./PublicLobby";
import { PublicLobby } from "./PublicLobby";
import { SinglePlayerModal } from "./SinglePlayerModal";
+import { UserSettingModal } from "./UserSettingModal";
import "./UsernameInput";
import { UsernameInput } from "./UsernameInput";
import { generateCryptoRandomUUID } from "./Utils";
@@ -118,6 +119,14 @@ class Client {
hlpModal.open();
});
+ const settingsModal = document.querySelector(
+ "user-setting",
+ ) as UserSettingModal;
+ settingsModal instanceof UserSettingModal;
+ document.getElementById("settings-button").addEventListener("click", () => {
+ settingsModal.open();
+ });
+
const hostModal = document.querySelector(
"host-lobby-modal",
) as HostPrivateLobbyModal;
@@ -200,6 +209,33 @@ class Client {
gameRecord: lobby.gameRecord,
},
() => {
+ console.log("Closing modals");
+ document.getElementById("settings-button").classList.add("hidden");
+ [
+ "single-player-modal",
+ "host-lobby-modal",
+ "join-private-lobby-modal",
+ "game-starting-modal",
+ "top-bar",
+ "help-modal",
+ "user-setting",
+ ].forEach((tag) => {
+ const modal = document.querySelector(tag) as HTMLElement & {
+ close?: () => void;
+ isModalOpen?: boolean;
+ };
+ if (modal?.close) {
+ modal.close();
+ } else if ("isModalOpen" in modal) {
+ modal.isModalOpen = false;
+ }
+ });
+ this.publicLobby.stop();
+ document.querySelectorAll(".ad").forEach((ad) => {
+ (ad as HTMLElement).style.display = "none";
+ });
+
+ // show when the game loads
const startingModal = document.querySelector(
"game-starting-modal",
) as GameStartingModal;
diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts
new file mode 100644
index 000000000..8c065a1f7
--- /dev/null
+++ b/src/client/UserSettingModal.ts
@@ -0,0 +1,226 @@
+import { LitElement, html } from "lit";
+import { customElement, query, state } from "lit/decorators.js";
+import { UserSettings } from "../core/game/UserSettings";
+import "./components/baseComponents/setting/SettingNumber";
+import "./components/baseComponents/setting/SettingSlider";
+import "./components/baseComponents/setting/SettingToggle";
+
+@customElement("user-setting")
+export class UserSettingModal extends LitElement {
+ private userSettings: UserSettings = new UserSettings();
+
+ @state() private darkMode: boolean = this.userSettings.darkMode();
+
+ @state() private keySequence: string[] = [];
+ @state() private showEasterEggSettings = false;
+
+ connectedCallback() {
+ super.connectedCallback();
+ window.addEventListener("keydown", this.handleKeyDown);
+ }
+
+ @query("o-modal") private modalEl!: HTMLElement & {
+ open: () => void;
+ close: () => void;
+ isModalOpen: boolean;
+ };
+
+ createRenderRoot() {
+ return this;
+ }
+
+ disconnectedCallback() {
+ window.removeEventListener("keydown", this.handleKeyDown);
+ super.disconnectedCallback();
+ document.body.style.overflow = "auto";
+ }
+
+ private handleKeyDown = (e: KeyboardEvent) => {
+ if (!this.modalEl?.isModalOpen || this.showEasterEggSettings) return;
+
+ const key = e.key.toLowerCase();
+ const nextSequence = [...this.keySequence, key].slice(-4);
+ this.keySequence = nextSequence;
+
+ if (nextSequence.join("") === "evan") {
+ this.triggerEasterEgg();
+ this.keySequence = [];
+ }
+ };
+
+ private triggerEasterEgg() {
+ console.log("πͺΊ Setting~ unlocked by EVAN combo!");
+ this.showEasterEggSettings = true;
+ const popup = document.createElement("div");
+ popup.className = "easter-egg-popup";
+ popup.textContent = "π You found a secret setting!";
+ document.body.appendChild(popup);
+
+ setTimeout(() => {
+ popup.remove();
+ }, 5000);
+ }
+
+ toggleDarkMode(e: CustomEvent<{ checked: boolean }>) {
+ const enabled = e.detail?.checked;
+
+ if (typeof enabled !== "boolean") {
+ console.warn("Unexpected toggle event payload", e);
+ return;
+ }
+
+ this.userSettings.set("settings.darkMode", enabled);
+
+ if (enabled) {
+ document.documentElement.classList.add("dark");
+ } else {
+ document.documentElement.classList.remove("dark");
+ }
+
+ console.log("π Dark Mode:", enabled ? "ON" : "OFF");
+ }
+
+ private toggleEmojis(e: CustomEvent<{ checked: boolean }>) {
+ const enabled = e.detail?.checked;
+ if (typeof enabled !== "boolean") return;
+
+ this.userSettings.set("settings.emojis", enabled);
+
+ console.log("π€‘ Emojis:", enabled ? "ON" : "OFF");
+ }
+
+ private toggleLeftClickOpensMenu(e: CustomEvent<{ checked: boolean }>) {
+ const enabled = e.detail?.checked;
+ if (typeof enabled !== "boolean") return;
+
+ this.userSettings.set("settings.leftClickOpensMenu", enabled);
+ console.log("π±οΈ Left Click Opens Menu:", enabled ? "ON" : "OFF");
+
+ this.requestUpdate();
+ }
+
+ private sliderAttackRatio(e: CustomEvent<{ value: number }>) {
+ const value = e.detail?.value;
+ if (typeof value === "number") {
+ const ratio = value / 100;
+ localStorage.setItem("settings.attackRatio", ratio.toString());
+ } else {
+ console.warn("Slider event missing detail.value", e);
+ }
+ }
+
+ private sliderTroopRatio(e: CustomEvent<{ value: number }>) {
+ const value = e.detail?.value;
+ if (typeof value === "number") {
+ const ratio = value / 100;
+ localStorage.setItem("settings.troopRatio", ratio.toString());
+ } else {
+ console.warn("Slider event missing detail.value", e);
+ }
+ }
+
+ render() {
+ return html`
+