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` + + + + `; + } + + public open() { + this.modalEl?.open(); + } + + public close() { + this.modalEl?.close(); + } +} diff --git a/src/client/components/baseComponents/setting/SettingNumber.ts b/src/client/components/baseComponents/setting/SettingNumber.ts new file mode 100644 index 000000000..8b7f80770 --- /dev/null +++ b/src/client/components/baseComponents/setting/SettingNumber.ts @@ -0,0 +1,52 @@ +import { LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement("setting-number") +export class SettingNumber extends LitElement { + @property() label = "Setting"; + @property() description = ""; + @property({ type: Number }) value = 0; + @property({ type: Number }) min = 0; + @property({ type: Number }) max = 100; + @property({ type: Boolean }) easter = false; + + createRenderRoot() { + return this; + } + + private handleInput(e: Event) { + const input = e.target as HTMLInputElement; + const newValue = Number(input.value); + this.value = newValue; + + this.dispatchEvent( + new CustomEvent("change", { + detail: { value: newValue }, + bubbles: true, + composed: true, + }), + ); + } + + render() { + return html` +
+
+ +
${this.description}
+
+ +
+ `; + } +} diff --git a/src/client/components/baseComponents/setting/SettingSlider.ts b/src/client/components/baseComponents/setting/SettingSlider.ts new file mode 100644 index 000000000..989756b57 --- /dev/null +++ b/src/client/components/baseComponents/setting/SettingSlider.ts @@ -0,0 +1,76 @@ +import { LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement("setting-slider") +export class SettingSlider extends LitElement { + @property() label = "Setting"; + @property() description = ""; + @property({ type: Number }) value = 0; + @property({ type: Number }) min = 0; + @property({ type: Number }) max = 100; + @property({ type: Boolean }) easter = false; + + createRenderRoot() { + return this; + } + + private handleInput(e: Event) { + const input = e.target as HTMLInputElement; + this.value = Number(input.value); + this.updateSliderStyle(input); + + this.dispatchEvent( + new CustomEvent("change", { + detail: { value: this.value }, + bubbles: true, + composed: true, + }), + ); + } + + private handleSliderChange(e: Event) { + const detail = (e as CustomEvent)?.detail; + if (!detail || typeof detail.value === "undefined") { + console.warn("Invalid slider change event", e); + return; + } + + const value = detail.value; + console.log("Slider changed to", value); + } + + private updateSliderStyle(slider: HTMLInputElement) { + const percent = ((this.value - this.min) / (this.max - this.min)) * 100; + slider.style.background = `linear-gradient(to right, #2196f3 ${percent}%, #444 ${percent}%)`; + } + + firstUpdated() { + const slider = this.renderRoot.querySelector( + "input[type=range]", + ) as HTMLInputElement; + if (slider) this.updateSliderStyle(slider); + } + + render() { + return html` +
+
+ +
${this.description}
+
+ +
${this.value}%
+
+ `; + } +} diff --git a/src/client/components/baseComponents/setting/SettingToggle.ts b/src/client/components/baseComponents/setting/SettingToggle.ts new file mode 100644 index 000000000..18f1dfe71 --- /dev/null +++ b/src/client/components/baseComponents/setting/SettingToggle.ts @@ -0,0 +1,47 @@ +import { LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement("setting-toggle") +export class SettingToggle extends LitElement { + @property() label = "Setting"; + @property() description = ""; + @property() id = ""; + @property({ type: Boolean, reflect: true }) checked = false; + @property({ type: Boolean }) easter = false; + + createRenderRoot() { + return this; + } + + private handleChange(e: Event) { + const input = e.target as HTMLInputElement; + this.checked = input.checked; + this.dispatchEvent( + new CustomEvent("change", { + detail: { checked: this.checked }, + bubbles: true, + composed: true, + }), + ); + } + + render() { + return html` +
+
+ + +
+
${this.description}
+
+ `; + } +} diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index eac6b913e..fac9fb6b6 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -56,8 +56,16 @@ export class ControlPanel extends LitElement implements Layer { private _popRateIsIncreasing: boolean = true; + private init_: boolean = false; + init() { - this.attackRatio = 0.2; + this.attackRatio = Number( + localStorage.getItem("settings.attackRatio") ?? "0.2", + ); + this.targetTroopRatio = Number( + localStorage.getItem("settings.troopRatio") ?? "0.95", + ); + this.init_ = true; this.uiState.attackRatio = this.attackRatio; this.currentTroopRatio = this.targetTroopRatio; this.eventBus.on(AttackRatioEvent, (event) => { @@ -87,6 +95,13 @@ export class ControlPanel extends LitElement implements Layer { } tick() { + if (this.init_) { + this.eventBus.emit( + new SendSetTargetTroopRatioEvent(this.targetTroopRatio), + ); + this.init_ = false; + } + if (!this._isVisible && !this.game.inSpawnPhase()) { this.setVisibile(true); } diff --git a/src/client/index.html b/src/client/index.html index c1a475095..ab8e008ac 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -279,6 +279,20 @@ + + +
@@ -353,6 +367,7 @@ +