Move settings to it's own modal (#1366)

## Description:

The settings has grown to the point where it deserves its own modal.
<img width="1144" alt="Screenshot 2025-07-07 at 3 55 23 PM"
src="https://github.com/user-attachments/assets/a1527d79-93c3-4bf3-ba67-dce643bc81ea"
/>

NOTE: styling is still needed.

## 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
- [ ] I have added relevant tests to the test directory
- [ ] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [ ] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
evanpelle
2025-07-07 16:22:55 -07:00
committed by GitHub
parent 8aa3775bd8
commit a387909262
5 changed files with 326 additions and 189 deletions
+1
View File
@@ -198,6 +198,7 @@ export class LangSelector extends LitElement {
"player-panel",
"replay-panel",
"help-modal",
"settings-modal",
"username-input",
"public-lobby",
"user-setting",
+11
View File
@@ -27,6 +27,7 @@ import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
import { RailroadLayer } from "./layers/RailroadLayer";
import { ReplayPanel } from "./layers/ReplayPanel";
import { SettingsModal } from "./layers/SettingsModal";
import { SpawnAd } from "./layers/SpawnAd";
import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureIconsLayer } from "./layers/StructureIconsLayer";
@@ -152,6 +153,15 @@ export function createRenderer(
gameRightSidebar.game = game;
gameRightSidebar.eventBus = eventBus;
const settingsModal = document.querySelector(
"settings-modal",
) as SettingsModal;
if (!(settingsModal instanceof SettingsModal)) {
console.error("settings modal not found");
}
settingsModal.userSettings = userSettings;
settingsModal.eventBus = eventBus;
const gameTopBar = document.querySelector("game-top-bar") as GameTopBar;
if (!(gameTopBar instanceof GameTopBar)) {
console.error("top bar not found");
@@ -252,6 +262,7 @@ export function createRenderer(
playerInfo,
winModal,
replayPanel,
settingsModal,
teamStats,
playerPanel,
headsUpMessage,
+13 -188
View File
@@ -1,25 +1,17 @@
import { html, LitElement } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import darkModeIcon from "../../../../resources/images/DarkModeIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
import explosionIcon from "../../../../resources/images/ExplosionIconWhite.svg";
import { customElement, state } from "lit/decorators.js";
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
import mouseIcon from "../../../../resources/images/MouseIconWhite.svg";
import ninjaIcon from "../../../../resources/images/NinjaIconWhite.svg";
import populationIcon from "../../../../resources/images/PopulationIconSolidWhite.svg";
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
import treeIcon from "../../../../resources/images/TreeIconWhite.svg";
import troopIcon from "../../../../resources/images/TroopIconWhite.svg";
import workerIcon from "../../../../resources/images/WorkerIconWhite.svg";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
import { renderNumber, renderTroops } from "../../Utils";
import { Layer } from "./Layer";
import { ShowSettingsModalEvent } from "./SettingsModal";
@customElement("game-top-bar")
export class GameTopBar extends LitElement implements Layer {
@@ -33,17 +25,9 @@ export class GameTopBar extends LitElement implements Layer {
private _popRateIsIncreasing = false;
private hasWinner = false;
@state()
private showSettingsMenu = false;
@state()
private alternateView: boolean = false;
@state()
private timer: number = 0;
@query(".settings-container")
private settingsContainer!: HTMLElement;
createRenderRoot() {
return this;
}
@@ -70,66 +54,8 @@ export class GameTopBar extends LitElement implements Layer {
this.requestUpdate();
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("click", this.handleOutsideClick, true);
}
disconnectedCallback() {
window.removeEventListener("click", this.handleOutsideClick, true);
super.disconnectedCallback();
}
private handleOutsideClick = (event: MouseEvent) => {
if (
this.showSettingsMenu &&
this.settingsContainer &&
!this.settingsContainer.contains(event.target as Node)
) {
this.showSettingsMenu = false;
}
};
private onExitButtonClick() {
const isAlive = this.game.myPlayer()?.isAlive();
if (isAlive) {
const isConfirmed = confirm("Are you sure you want to exit the game?");
if (!isConfirmed) return;
}
// redirect to the home page
window.location.href = "/";
}
private onTerrainButtonClick() {
this.alternateView = !this.alternateView;
this.eventBus.emit(new AlternateViewEvent(this.alternateView));
this.requestUpdate();
}
private onToggleEmojisButtonClick() {
this._userSettings.toggleEmojis();
this.requestUpdate();
}
private onToggleSpecialEffectsButtonClick() {
this._userSettings.toggleFxLayer();
this.requestUpdate();
}
private onToggleDarkModeButtonClick() {
this._userSettings.toggleDarkMode();
this.requestUpdate();
this.eventBus.emit(new RefreshGraphicsEvent());
}
private onToggleRandomNameModeButtonClick() {
this._userSettings.toggleRandomName();
}
private onToggleLeftClickOpensMenu() {
this._userSettings.toggleLeftClickOpenMenu();
}
private toggleSettingsMenu() {
this.showSettingsMenu = !this.showSettingsMenu;
private onSettingsButtonClick() {
this.eventBus.emit(new ShowSettingsModalEvent(true));
}
private updatePopulationIncrease() {
@@ -270,116 +196,15 @@ export class GameTopBar extends LitElement implements Layer {
>
${this.secondsToHms(this.timer)}
</div>
<div class="relative settings-container">
<img
class="cursor-pointer bg-slate-800/20 border border-slate-400 p-0.5"
src=${settingsIcon}
alt="settings"
width="28"
height="28"
style="vertical-align: middle;"
@click=${this.toggleSettingsMenu}
/>
${this.showSettingsMenu
? html`
<div
class="absolute right-0 mt-1.5 bg-slate-700 border border-slate-500 rounded shadow-lg z-[1100] w-max min-w-[10rem] whitespace-nowrap"
>
<button
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
@click="${this.onTerrainButtonClick}"
>
<img
src=${treeIcon}
alt="treeIcon"
width="20"
height="20"
/>
Toggle Terrain ${this.alternateView ? "On" : "Off"}
</button>
<button
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
@click="${this.onToggleEmojisButtonClick}"
>
<img
src=${emojiIcon}
alt="emojiIcon"
width="20"
height="20"
/>
${translateText("user_setting.emojis_label")}
${this._userSettings.emojis() ? "On" : "Off"}
</button>
<button
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
@click="${this.onToggleDarkModeButtonClick}"
>
<img
src=${darkModeIcon}
alt="darkModeIcon"
width="20"
height="20"
/>
${translateText("user_setting.dark_mode_label")}
${this._userSettings.darkMode() ? "On" : "Off"}
</button>
<button
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
@click="${this.onToggleSpecialEffectsButtonClick}"
>
<img
src=${explosionIcon}
alt="onExitButtonClick"
width="20"
height="20"
/>
${translateText("user_setting.special_effects_label")}
${this._userSettings.fxLayer() ? "On" : "Off"}
</button>
<button
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
@click="${this.onToggleRandomNameModeButtonClick}"
>
<img
src=${ninjaIcon}
alt="ninjaIcon"
width="20"
height="20"
/>
${translateText("user_setting.anonymous_names_label")}
${this._userSettings.anonymousNames() ? "On" : "Off"}
</button>
<button
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
@click="${this.onToggleLeftClickOpensMenu}"
>
<img
src=${mouseIcon}
alt="mouseIcon"
width="20"
height="20"
/>
Left click
${this._userSettings.leftClickOpensMenu()
? "On"
: "Off"}
</button>
<button
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
@click="${this.onExitButtonClick}"
>
<img
src=${exitIcon}
alt="exitIcon"
width="20"
height="20"
/>
Exit game
</button>
</div>
`
: null}
</div>
<img
class="cursor-pointer bg-slate-800/20 border border-slate-400 p-0.5"
src=${settingsIcon}
alt="settings"
width="28"
height="28"
style="vertical-align: middle;"
@click=${this.onSettingsButtonClick}
/>
</div>
</div>
</div>
+300
View File
@@ -0,0 +1,300 @@
import { html, LitElement } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import darkModeIcon from "../../../../resources/images/DarkModeIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
import explosionIcon from "../../../../resources/images/ExplosionIconWhite.svg";
import mouseIcon from "../../../../resources/images/MouseIconWhite.svg";
import ninjaIcon from "../../../../resources/images/NinjaIconWhite.svg";
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
import treeIcon from "../../../../resources/images/TreeIconWhite.svg";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
import { Layer } from "./Layer";
export class ShowSettingsModalEvent {
constructor(public readonly isVisible: boolean = true) {}
}
@customElement("settings-modal")
export class SettingsModal extends LitElement implements Layer {
public eventBus: EventBus;
public userSettings: UserSettings;
@state()
private isVisible: boolean = false;
@state()
private alternateView: boolean = false;
@query(".modal-overlay")
private modalOverlay!: HTMLElement;
init() {
this.eventBus.on(ShowSettingsModalEvent, (event) => {
this.isVisible = event.isVisible;
});
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("click", this.handleOutsideClick, true);
window.addEventListener("keydown", this.handleKeyDown);
}
disconnectedCallback() {
window.removeEventListener("click", this.handleOutsideClick, true);
window.removeEventListener("keydown", this.handleKeyDown);
super.disconnectedCallback();
}
private handleOutsideClick = (event: MouseEvent) => {
if (
this.isVisible &&
this.modalOverlay &&
event.target === this.modalOverlay
) {
this.closeModal();
}
};
private handleKeyDown = (event: KeyboardEvent) => {
if (this.isVisible && event.key === "Escape") {
this.closeModal();
}
};
public openModal() {
this.isVisible = true;
document.body.style.overflow = "hidden";
this.requestUpdate();
}
public closeModal() {
this.isVisible = false;
document.body.style.overflow = "";
this.requestUpdate();
}
private onTerrainButtonClick() {
this.alternateView = !this.alternateView;
this.eventBus.emit(new AlternateViewEvent(this.alternateView));
this.requestUpdate();
}
private onToggleEmojisButtonClick() {
this.userSettings.toggleEmojis();
this.requestUpdate();
}
private onToggleSpecialEffectsButtonClick() {
this.userSettings.toggleFxLayer();
this.requestUpdate();
}
private onToggleDarkModeButtonClick() {
this.userSettings.toggleDarkMode();
this.eventBus.emit(new RefreshGraphicsEvent());
this.requestUpdate();
}
private onToggleRandomNameModeButtonClick() {
this.userSettings.toggleRandomName();
this.requestUpdate();
}
private onToggleLeftClickOpensMenu() {
this.userSettings.toggleLeftClickOpenMenu();
this.requestUpdate();
}
private onExitButtonClick() {
// redirect to the home page
window.location.href = "/";
}
render() {
if (!this.isVisible) {
return null;
}
return html`
<div
class="modal-overlay fixed inset-0 bg-black/50 backdrop-blur-sm z-[2000] flex items-center justify-center p-4"
@contextmenu=${(e: Event) => e.preventDefault()}
>
<div
class="bg-slate-800 border border-slate-600 rounded-lg shadow-xl max-w-md w-full max-h-[80vh] overflow-y-auto"
>
<div
class="flex items-center justify-between p-4 border-b border-slate-600"
>
<div class="flex items-center gap-2">
<img
src=${settingsIcon}
alt="settings"
width="24"
height="24"
style="vertical-align: middle;"
/>
<h2 class="text-xl font-semibold text-white">Settings</h2>
</div>
<button
class="text-slate-400 hover:text-white text-2xl font-bold leading-none"
@click=${this.closeModal}
>
×
</button>
</div>
<div class="p-4 space-y-3">
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onTerrainButtonClick}"
>
<img src=${treeIcon} alt="treeIcon" width="20" height="20" />
<div class="flex-1">
<div class="font-medium">Toggle Terrain</div>
<div class="text-sm text-slate-400">
${this.alternateView
? "Terrain view enabled"
: "Terrain view disabled"}
</div>
</div>
<div class="text-sm text-slate-400">
${this.alternateView ? "On" : "Off"}
</div>
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleEmojisButtonClick}"
>
<img src=${emojiIcon} alt="emojiIcon" width="20" height="20" />
<div class="flex-1">
<div class="font-medium">
${translateText("user_setting.emojis_label")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.emojis()
? "Emojis are visible"
: "Emojis are hidden"}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.emojis() ? "On" : "Off"}
</div>
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleDarkModeButtonClick}"
>
<img
src=${darkModeIcon}
alt="darkModeIcon"
width="20"
height="20"
/>
<div class="flex-1">
<div class="font-medium">
${translateText("user_setting.dark_mode_label")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.darkMode()
? "Dark mode enabled"
: "Light mode enabled"}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.darkMode() ? "On" : "Off"}
</div>
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleSpecialEffectsButtonClick}"
>
<img
src=${explosionIcon}
alt="specialEffects"
width="20"
height="20"
/>
<div class="flex-1">
<div class="font-medium">
${translateText("user_setting.special_effects_label")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.fxLayer()
? "Special effects enabled"
: "Special effects disabled"}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.fxLayer() ? "On" : "Off"}
</div>
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleRandomNameModeButtonClick}"
>
<img src=${ninjaIcon} alt="ninjaIcon" width="20" height="20" />
<div class="flex-1">
<div class="font-medium">
${translateText("user_setting.anonymous_names_label")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.anonymousNames()
? "Anonymous names enabled"
: "Real names shown"}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.anonymousNames() ? "On" : "Off"}
</div>
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleLeftClickOpensMenu}"
>
<img src=${mouseIcon} alt="mouseIcon" width="20" height="20" />
<div class="flex-1">
<div class="font-medium">Left Click Menu</div>
<div class="text-sm text-slate-400">
${this.userSettings.leftClickOpensMenu()
? "Left click opens menu"
: "Right click opens menu"}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.leftClickOpensMenu() ? "On" : "Off"}
</div>
</button>
<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"
@click="${this.onExitButtonClick}"
>
<img src=${exitIcon} alt="exitIcon" width="20" height="20" />
<div class="flex-1">
<div class="font-medium">Exit Game</div>
<div class="text-sm text-slate-400">Return to main menu</div>
</div>
</button>
</div>
</div>
</div>
</div>
`;
}
}
+1 -1
View File
@@ -362,7 +362,7 @@
<game-top-bar></game-top-bar>
<unit-display></unit-display>
<game-right-sidebar></game-right-sidebar>
<settings-modal></settings-modal>
<player-panel></player-panel>
<help-modal></help-modal>
<dark-mode-button></dark-mode-button>