mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
5e6c90d9bb
## Description: Overhauls the Main Menu UI, visit https://menu.openfront.dev to see everything. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n
459 lines
15 KiB
TypeScript
459 lines
15 KiB
TypeScript
import { html } from "lit";
|
|
import { customElement, state } from "lit/decorators.js";
|
|
import { translateText } from "../client/Utils";
|
|
import { UserSettings } from "../core/game/UserSettings";
|
|
import "./components/baseComponents/setting/SettingNumber";
|
|
import "./components/baseComponents/setting/SettingSlider";
|
|
import "./components/baseComponents/setting/SettingToggle";
|
|
import { BaseModal } from "./components/BaseModal";
|
|
import "./FlagInputModal";
|
|
|
|
interface FlagInputModalElement extends HTMLElement {
|
|
open(): void;
|
|
returnTo?: string;
|
|
}
|
|
|
|
@customElement("user-setting")
|
|
export class UserSettingModal extends BaseModal {
|
|
private userSettings: UserSettings = new UserSettings();
|
|
|
|
@state() private keySequence: string[] = [];
|
|
@state() private showEasterEggSettings = false;
|
|
|
|
disconnectedCallback() {
|
|
window.removeEventListener("keydown", this.handleEasterEggKey);
|
|
super.disconnectedCallback();
|
|
}
|
|
|
|
private handleEasterEggKey = (e: KeyboardEvent) => {
|
|
if (!this.isModalOpen || this.showEasterEggSettings) return;
|
|
|
|
// Validate that the event target is inside this component
|
|
const target = e.target as Node;
|
|
if (!this.contains(target)) {
|
|
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 =
|
|
"fixed top-10 left-1/2 p-4 px-6 bg-black/80 text-white text-xl rounded-xl animate-fadePop z-[9999]";
|
|
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");
|
|
}
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent("dark-mode-changed", {
|
|
detail: { darkMode: enabled },
|
|
bubbles: true,
|
|
composed: true,
|
|
}),
|
|
);
|
|
|
|
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 toggleAlertFrame(e: CustomEvent<{ checked: boolean }>) {
|
|
const enabled = e.detail?.checked;
|
|
if (typeof enabled !== "boolean") return;
|
|
|
|
this.userSettings.set("settings.alertFrame", enabled);
|
|
|
|
console.log("🚨 Alert frame:", enabled ? "ON" : "OFF");
|
|
}
|
|
|
|
private toggleFxLayer(e: CustomEvent<{ checked: boolean }>) {
|
|
const enabled = e.detail?.checked;
|
|
if (typeof enabled !== "boolean") return;
|
|
|
|
this.userSettings.set("settings.specialEffects", enabled);
|
|
|
|
console.log("💥 Special effects:", enabled ? "ON" : "OFF");
|
|
}
|
|
|
|
private toggleStructureSprites(e: CustomEvent<{ checked: boolean }>) {
|
|
const enabled = e.detail?.checked;
|
|
if (typeof enabled !== "boolean") return;
|
|
|
|
this.userSettings.set("settings.structureSprites", enabled);
|
|
|
|
console.log("🏠 Structure sprites:", enabled ? "ON" : "OFF");
|
|
}
|
|
|
|
private toggleCursorCostLabel(e: CustomEvent<{ checked: boolean }>) {
|
|
const enabled = e.detail?.checked;
|
|
if (typeof enabled !== "boolean") return;
|
|
|
|
this.userSettings.set("settings.cursorCostLabel", enabled);
|
|
|
|
console.log("💰 Cursor build cost:", enabled ? "ON" : "OFF");
|
|
}
|
|
|
|
private toggleAnonymousNames(e: CustomEvent<{ checked: boolean }>) {
|
|
const enabled = e.detail?.checked;
|
|
if (typeof enabled !== "boolean") return;
|
|
|
|
this.userSettings.set("settings.anonymousNames", enabled);
|
|
|
|
console.log("🙈 Anonymous Names:", enabled ? "ON" : "OFF");
|
|
}
|
|
|
|
private toggleLobbyIdVisibility(e: CustomEvent<{ checked: boolean }>) {
|
|
const hideIds = e.detail?.checked;
|
|
if (typeof hideIds !== "boolean") return;
|
|
|
|
this.userSettings.set("settings.lobbyIdVisibility", !hideIds); // Invert because checked=hide
|
|
console.log("👁️ Hidden Lobby IDs:", hideIds ? "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 toggleTerritoryPatterns(e: CustomEvent<{ checked: boolean }>) {
|
|
const enabled = e.detail?.checked;
|
|
if (typeof enabled !== "boolean") return;
|
|
|
|
this.userSettings.set("settings.territoryPatterns", enabled);
|
|
|
|
console.log("🏳️ Territory Patterns:", enabled ? "ON" : "OFF");
|
|
}
|
|
|
|
private togglePerformanceOverlay(e: CustomEvent<{ checked: boolean }>) {
|
|
const enabled = e.detail?.checked;
|
|
if (typeof enabled !== "boolean") return;
|
|
|
|
this.userSettings.set("settings.performanceOverlay", enabled);
|
|
}
|
|
|
|
private openFlagSelector = () => {
|
|
const flagInputModal =
|
|
document.querySelector<FlagInputModalElement>("#flag-input-modal");
|
|
if (flagInputModal?.open) {
|
|
this.close();
|
|
flagInputModal.returnTo = "#" + (this.id || "page-options");
|
|
flagInputModal.open();
|
|
}
|
|
};
|
|
|
|
render() {
|
|
const content = html`
|
|
<div
|
|
class="h-full flex flex-col ${this.inline
|
|
? "bg-black/40 backdrop-blur-md rounded-2xl border border-white/10"
|
|
: ""}"
|
|
>
|
|
<div
|
|
class="flex items-center mb-6 pb-2 border-b border-white/10 gap-2 shrink-0 p-6"
|
|
>
|
|
<div class="flex items-center gap-4 flex-1 flex-wrap">
|
|
<button
|
|
@click=${this.close}
|
|
class="group flex items-center justify-center w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 transition-all border border-white/10 shrink-0"
|
|
aria-label="${translateText("common.back")}"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="w-5 h-5 text-gray-400 group-hover:text-white transition-colors"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<span
|
|
class="text-white text-xl sm:text-2xl md:text-3xl font-bold uppercase tracking-widest break-all hyphens-auto min-w-0"
|
|
>
|
|
${translateText("user_setting.title")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent px-6 pb-6 mr-1"
|
|
>
|
|
<div class="flex flex-col gap-2">${this.renderBasicSettings()}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
if (this.inline) {
|
|
return content;
|
|
}
|
|
|
|
return html`
|
|
<o-modal
|
|
title="${translateText("user_setting.title")}"
|
|
?inline=${this.inline}
|
|
hideCloseButton
|
|
hideHeader
|
|
>
|
|
${content}
|
|
</o-modal>
|
|
`;
|
|
}
|
|
|
|
protected onClose(): void {
|
|
window.removeEventListener("keydown", this.handleEasterEggKey);
|
|
}
|
|
|
|
private renderBasicSettings() {
|
|
return html`
|
|
<!-- 🚩 Flag Selector -->
|
|
<div
|
|
class="flex flex-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 cursor-pointer"
|
|
role="button"
|
|
tabindex="0"
|
|
@click=${this.openFlagSelector}
|
|
@keydown=${(e: KeyboardEvent) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
this.openFlagSelector();
|
|
}
|
|
}}
|
|
>
|
|
<div class="flex flex-col flex-1 min-w-0 mr-4">
|
|
<div class="text-white font-bold text-base block mb-1">
|
|
${translateText("flag_input.title")}
|
|
</div>
|
|
<div class="text-white/50 text-sm leading-snug">
|
|
${translateText("flag_input.button_title")}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="relative inline-block w-12 h-8 shrink-0 rounded overflow-hidden border border-white/20"
|
|
>
|
|
<flag-input class="w-full h-full pointer-events-none"></flag-input>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 🌙 Dark Mode -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.dark_mode_label")}"
|
|
description="${translateText("user_setting.dark_mode_desc")}"
|
|
id="dark-mode-toggle"
|
|
.checked=${this.userSettings.darkMode()}
|
|
@change=${(e: CustomEvent<{ checked: boolean }>) =>
|
|
this.toggleDarkMode(e)}
|
|
></setting-toggle>
|
|
|
|
<!-- 😊 Emojis -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.emojis_label")}"
|
|
description="${translateText("user_setting.emojis_desc")}"
|
|
id="emoji-toggle"
|
|
.checked=${this.userSettings.emojis()}
|
|
@change=${this.toggleEmojis}
|
|
></setting-toggle>
|
|
|
|
<!-- 🚨 Alert frame -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.alert_frame_label")}"
|
|
description="${translateText("user_setting.alert_frame_desc")}"
|
|
id="alert-frame-toggle"
|
|
.checked=${this.userSettings.alertFrame()}
|
|
@change=${this.toggleAlertFrame}
|
|
></setting-toggle>
|
|
|
|
<!-- 💥 Special effects -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.special_effects_label")}"
|
|
description="${translateText("user_setting.special_effects_desc")}"
|
|
id="special-effect-toggle"
|
|
.checked=${this.userSettings.fxLayer()}
|
|
@change=${this.toggleFxLayer}
|
|
></setting-toggle>
|
|
|
|
<!-- 🏠 Structure Sprites -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.structure_sprites_label")}"
|
|
description="${translateText("user_setting.structure_sprites_desc")}"
|
|
id="structure_sprites-toggle"
|
|
.checked=${this.userSettings.structureSprites()}
|
|
@change=${this.toggleStructureSprites}
|
|
></setting-toggle>
|
|
|
|
<!-- 💰 Cursor Price Pill -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.cursor_cost_label_label")}"
|
|
description="${translateText("user_setting.cursor_cost_label_desc")}"
|
|
id="cursor_cost_label-toggle"
|
|
.checked=${this.userSettings.cursorCostLabel()}
|
|
@change=${this.toggleCursorCostLabel}
|
|
></setting-toggle>
|
|
|
|
<!-- 🖱️ Left Click Menu -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.left_click_label")}"
|
|
description="${translateText("user_setting.left_click_desc")}"
|
|
id="left-click-toggle"
|
|
.checked=${this.userSettings.leftClickOpensMenu()}
|
|
@change=${this.toggleLeftClickOpensMenu}
|
|
></setting-toggle>
|
|
|
|
<!-- 🙈 Anonymous Names -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.anonymous_names_label")}"
|
|
description="${translateText("user_setting.anonymous_names_desc")}"
|
|
id="anonymous-names-toggle"
|
|
.checked=${this.userSettings.anonymousNames()}
|
|
@change=${this.toggleAnonymousNames}
|
|
></setting-toggle>
|
|
|
|
<!-- 👁️ Hidden Lobby IDs -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.lobby_id_visibility_label")}"
|
|
description="${translateText("user_setting.lobby_id_visibility_desc")}"
|
|
id="lobby-id-visibility-toggle"
|
|
.checked=${!this.userSettings.get("settings.lobbyIdVisibility", true)}
|
|
@change=${this.toggleLobbyIdVisibility}
|
|
></setting-toggle>
|
|
|
|
<!-- 🏳️ Territory Patterns -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.territory_patterns_label")}"
|
|
description="${translateText("user_setting.territory_patterns_desc")}"
|
|
id="territory-patterns-toggle"
|
|
.checked=${this.userSettings.territoryPatterns()}
|
|
@change=${this.toggleTerritoryPatterns}
|
|
></setting-toggle>
|
|
|
|
<!-- 📱 Performance Overlay -->
|
|
<setting-toggle
|
|
label="${translateText("user_setting.performance_overlay_label")}"
|
|
description="${translateText("user_setting.performance_overlay_desc")}"
|
|
id="performance-overlay-toggle"
|
|
.checked=${this.userSettings.performanceOverlay()}
|
|
@change=${this.togglePerformanceOverlay}
|
|
></setting-toggle>
|
|
|
|
<!-- ⚔️ Attack Ratio -->
|
|
<setting-slider
|
|
label="${translateText("user_setting.attack_ratio_label")}"
|
|
description="${translateText("user_setting.attack_ratio_desc")}"
|
|
min="1"
|
|
max="100"
|
|
.value=${Number(localStorage.getItem("settings.attackRatio") ?? "0.2") *
|
|
100}
|
|
@change=${this.sliderAttackRatio}
|
|
></setting-slider>
|
|
|
|
${this.showEasterEggSettings
|
|
? html`
|
|
<setting-slider
|
|
label="${translateText(
|
|
"user_setting.easter_writing_speed_label",
|
|
)}"
|
|
description="${translateText(
|
|
"user_setting.easter_writing_speed_desc",
|
|
)}"
|
|
min="0"
|
|
max="100"
|
|
value="40"
|
|
easter="true"
|
|
@change=${(e: CustomEvent) => {
|
|
const value = e.detail?.value;
|
|
if (value !== undefined) {
|
|
console.log("Changed:", value);
|
|
} else {
|
|
console.warn("Slider event missing detail.value", e);
|
|
}
|
|
}}
|
|
></setting-slider>
|
|
|
|
<setting-number
|
|
label="${translateText("user_setting.easter_bug_count_label")}"
|
|
description="${translateText(
|
|
"user_setting.easter_bug_count_desc",
|
|
)}"
|
|
value="100"
|
|
min="0"
|
|
max="1000"
|
|
easter="true"
|
|
@change=${(e: CustomEvent) => {
|
|
const value = e.detail?.value;
|
|
if (value !== undefined) {
|
|
console.log("Changed:", value);
|
|
} else {
|
|
console.warn("Slider event missing detail.value", e);
|
|
}
|
|
}}
|
|
></setting-number>
|
|
`
|
|
: null}
|
|
`;
|
|
}
|
|
|
|
protected onOpen(): void {
|
|
window.addEventListener("keydown", this.handleEasterEggKey);
|
|
}
|
|
|
|
public open() {
|
|
super.open();
|
|
}
|
|
}
|