Added User Settings Modal (#379)

## Description:

This PR adds a **User Settings** modal accessible from the main UI.

Currently available settings include:
- Toggle for Dark Mode
- Writing Speed Multiplier (slider)
- Bug Count (number input)
-  Troop and Worker Ratio (slider)
-  Left Click to Open Menu
-  Emoji toggle

Settings are saved via `localStorage` and persist across sessions.
There's also a hidden Easter Egg...

https://discord.com/channels/1284581928254701718/1286741605310533653/1355900228712009908
<img width="787" alt="スクリーンショット 2025-03-31 8 40 08"
src="https://github.com/user-attachments/assets/a9943834-cf40-4fa6-b828-06a8476172da"
/>

Fixes #482 

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] 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:

<DISCORD USERNAME>
aotumuri
This commit is contained in:
Aotumuri
2025-04-15 03:47:50 +09:00
committed by GitHub
parent c4c4920b5b
commit 3eccbaa982
11 changed files with 752 additions and 1 deletions
@@ -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`
<div class="setting-item${this.easter ? " easter-egg" : ""}">
<div class="setting-label-group">
<label class="setting-label" for="setting-number-input"
>${this.label}</label
>
<div class="setting-description">${this.description}</div>
</div>
<input
type="number"
id="setting-number-input"
class="setting-input number"
.value=${String(this.value ?? 0)}
min=${this.min}
max=${this.max}
@input=${this.handleInput}
/>
</div>
`;
}
}
@@ -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`
<div class="setting-item vertical${this.easter ? " easter-egg" : ""}">
<div class="setting-label-group">
<label class="setting-label" for="setting-slider-input"
>${this.label}</label
>
<div class="setting-description">${this.description}</div>
</div>
<input
type="range"
id="setting-slider-input"
class="setting-input slider full-width"
min=${this.min}
max=${this.max}
.value=${String(this.value)}
@input=${this.handleInput}
/>
<div class="slider-value">${this.value}%</div>
</div>
`;
}
}
@@ -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`
<div class="setting-item vertical${this.easter ? " easter-egg" : ""}">
<div class="toggle-row">
<label class="setting-label" for=${this.id}>${this.label}</label>
<label class="switch">
<input
type="checkbox"
id=${this.id}
?checked=${this.checked}
@change=${this.handleChange}
/>
<span class="slider-round"></span>
</label>
</div>
<div class="setting-description">${this.description}</div>
</div>
`;
}
}