mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:40:46 +00:00
Added keybinding configuration. (#548)
## Description: Added support for customizable keybind settings. fixes #549 fixed these bug https://discord.com/channels/1284581928254701718/1363254158341308668 https://discord.com/channels/1284581928254701718/1362159209642459166 ## 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: aotumuri
This commit is contained in:
@@ -189,5 +189,45 @@
|
||||
},
|
||||
"select_lang": {
|
||||
"title": "Select Language"
|
||||
},
|
||||
"user_setting": {
|
||||
"title": "User Settings",
|
||||
"tab_basic": "Basic Settings",
|
||||
"tab_keybinds": "Keybinds",
|
||||
"dark_mode_label": "🌙 Dark Mode",
|
||||
"dark_mode_desc": "Toggle the site’s appearance between light and dark themes",
|
||||
"emojis_label": "😊 Emojis",
|
||||
"emojis_desc": "Toggle whether emojis are shown in game",
|
||||
"left_click_label": "🖱️ Left Click to Open Menu",
|
||||
"left_click_desc": "When ON, left-click opens menu and sword button attacks. When OFF, left-click attacks directly.",
|
||||
"attack_ratio_label": "⚔️ Attack Ratio",
|
||||
"attack_ratio_desc": "What percentage of your troops to send in an attack (1–100%)",
|
||||
"troop_ratio_label": "🪖🛠️ Troops and Workers Ratio",
|
||||
"troop_ratio_desc": "Adjust the balance between troops (for combat) and workers (for gold production) (1–100%)",
|
||||
"easter_writing_speed_label": "Writing Speed Multiplier",
|
||||
"easter_writing_speed_desc": "Adjust how fast you pretend to code (x1–x100)",
|
||||
"easter_bug_count_label": "Bug Count",
|
||||
"easter_bug_count_desc": "How many bugs you're okay with (0–1000, emotionally)",
|
||||
"view_options": "View Options",
|
||||
"toggle_view": "Toggle View",
|
||||
"toggle_view_desc": "Alternate view (terrain/countries)",
|
||||
"zoom_controls": "Zoom Controls",
|
||||
"zoom_out": "Zoom Out",
|
||||
"zoom_out_desc": "Zoom out the map",
|
||||
"zoom_in": "Zoom In",
|
||||
"zoom_in_desc": "Zoom in the map",
|
||||
"camera_movement": "Camera Movement",
|
||||
"center_camera": "Center Camera",
|
||||
"center_camera_desc": "Center camera on player",
|
||||
"move_up": "Move Camera Up",
|
||||
"move_up_desc": "Move the camera upward",
|
||||
"move_left": "Move Camera Left",
|
||||
"move_left_desc": "Move the camera to the left",
|
||||
"move_down": "Move Camera Down",
|
||||
"move_down_desc": "Move the camera downward",
|
||||
"move_right": "Move Camera Right",
|
||||
"move_right_desc": "Move the camera to the right",
|
||||
"reset": "Reset",
|
||||
"unbind": "Unbind"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,5 +171,45 @@
|
||||
"game_mode": {
|
||||
"ffa": "Todos contra Todos",
|
||||
"teams": "Equipos"
|
||||
},
|
||||
"user_setting": {
|
||||
"title": "Uzantparametroj",
|
||||
"tab_basic": "Bazaj parametroj",
|
||||
"tab_keybinds": "Fulmoklavoj",
|
||||
"dark_mode_label": "🌙 Malhela Modo",
|
||||
"dark_mode_desc": "Baskuli la retpaĝa aspekto inter hela kaj malhela temo",
|
||||
"emojis_label": "😊 Emoĝioj",
|
||||
"emojis_desc": "Montri/Maski la emoĝiojn en la ludo",
|
||||
"left_click_label": "🖱️Maldekstra alklako por malfermi menuon",
|
||||
"left_click_desc": "Kiam aktiviga, maldekstra alklako malfermas menuon kaj glava atakbutono. Kiam malaktiviga, maldekstra alklako atakas direkten.",
|
||||
"attack_ratio_label": "⚔️ Atakkvociento",
|
||||
"attack_ratio_desc": "Kian procenton de viaj trupoj sendi en atako (1–100%)",
|
||||
"troop_ratio_label": "🪖🛠️ Trupoj kaj Laboristoj kvociento",
|
||||
"troop_ratio_desc": "Alĝustigu la ekvilibron inter soldatoj (por batalo) kaj laboristoj (por orproduktado) (1–100%)",
|
||||
"easter_writing_speed_label": "Rapidskriba multiganto",
|
||||
"easter_writing_speed_desc": "Alĝustigu kiom rapide vi ŝajnigas kodi (x1–x100)",
|
||||
"easter_bug_count_label": "Nombro da cimoj",
|
||||
"easter_bug_count_desc": "Kiom da cimoj vi bonfartas (0–1000, emocie)",
|
||||
"view_options": "Vidi opciojn",
|
||||
"toggle_view": "Baskuli vido",
|
||||
"toggle_view_desc": "Alterna vido (tereno/landoj)",
|
||||
"zoom_controls": "Zomaj kontroloj",
|
||||
"zoom_out": "Malzomi",
|
||||
"zoom_out_desc": "Malzomi la mapon",
|
||||
"zoom_in": "Zomi",
|
||||
"zoom_in_desc": "Zomi en la mapon",
|
||||
"camera_movement": "Fotila movado",
|
||||
"center_camera": "Centriĝi la fotilon",
|
||||
"center_camera_desc": "Centriĝi la fotilon sur la ludanto",
|
||||
"move_up": "Movi Fotilon Supren",
|
||||
"move_up_desc": "Movi la fotilon supren",
|
||||
"move_left": "Movi Fotilon Maldekstren",
|
||||
"move_left_desc": "Movi la fotilon maldekstren",
|
||||
"move_down": "Movi Fotilon Malsupren",
|
||||
"move_down_desc": "Movi la fotilon malsupren",
|
||||
"move_right": "Movi Fotilon Dekstren",
|
||||
"move_right_desc": "Movi la fotilon dekstren",
|
||||
"reset": "Rekomencigi",
|
||||
"unbind": "Senligi"
|
||||
}
|
||||
}
|
||||
|
||||
+56
-63
@@ -113,6 +113,17 @@ export class InputHandler {
|
||||
) {}
|
||||
|
||||
initialize() {
|
||||
const keybinds = {
|
||||
toggleView: "Space",
|
||||
centerCamera: "KeyC",
|
||||
moveUp: "KeyW",
|
||||
moveDown: "KeyS",
|
||||
moveLeft: "KeyA",
|
||||
moveRight: "KeyD",
|
||||
zoomOut: "KeyQ",
|
||||
zoomIn: "KeyE",
|
||||
...JSON.parse(localStorage.getItem("settings.keybinds") ?? "{}"),
|
||||
};
|
||||
this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e));
|
||||
window.addEventListener("pointerup", (e) => this.onPointerUp(e));
|
||||
this.canvas.addEventListener(
|
||||
@@ -122,59 +133,65 @@ export class InputHandler {
|
||||
this.onShiftScroll(e);
|
||||
e.preventDefault();
|
||||
},
|
||||
{
|
||||
passive: false,
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
window.addEventListener("pointermove", this.onPointerMove.bind(this));
|
||||
this.canvas.addEventListener("contextmenu", (e: MouseEvent) => {
|
||||
this.onContextMenu(e);
|
||||
});
|
||||
this.canvas.addEventListener("contextmenu", (e) => this.onContextMenu(e));
|
||||
window.addEventListener("mousemove", (e) => {
|
||||
if (e.movementX == 0 && e.movementY == 0) {
|
||||
return;
|
||||
if (e.movementX || e.movementY) {
|
||||
this.eventBus.emit(new MouseMoveEvent(e.clientX, e.clientY));
|
||||
}
|
||||
this.eventBus.emit(new MouseMoveEvent(e.clientX, e.clientY));
|
||||
});
|
||||
this.pointers.clear();
|
||||
|
||||
// Initialize the combined movement interval
|
||||
this.moveInterval = setInterval(() => {
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
|
||||
// Handle both WASD and arrow keys
|
||||
if (this.activeKeys.has("KeyW") || this.activeKeys.has("ArrowUp"))
|
||||
if (
|
||||
this.activeKeys.has(keybinds.moveUp) ||
|
||||
this.activeKeys.has("ArrowUp")
|
||||
)
|
||||
deltaY += this.PAN_SPEED;
|
||||
if (this.activeKeys.has("KeyS") || this.activeKeys.has("ArrowDown"))
|
||||
if (
|
||||
this.activeKeys.has(keybinds.moveDown) ||
|
||||
this.activeKeys.has("ArrowDown")
|
||||
)
|
||||
deltaY -= this.PAN_SPEED;
|
||||
if (this.activeKeys.has("KeyA") || this.activeKeys.has("ArrowLeft"))
|
||||
if (
|
||||
this.activeKeys.has(keybinds.moveLeft) ||
|
||||
this.activeKeys.has("ArrowLeft")
|
||||
)
|
||||
deltaX += this.PAN_SPEED;
|
||||
if (this.activeKeys.has("KeyD") || this.activeKeys.has("ArrowRight"))
|
||||
if (
|
||||
this.activeKeys.has(keybinds.moveRight) ||
|
||||
this.activeKeys.has("ArrowRight")
|
||||
)
|
||||
deltaX -= this.PAN_SPEED;
|
||||
|
||||
if (deltaX !== 0 || deltaY !== 0) {
|
||||
if (deltaX || deltaY) {
|
||||
this.eventBus.emit(new DragEvent(deltaX, deltaY));
|
||||
}
|
||||
|
||||
// Handle zooming
|
||||
const screenCenterX = window.innerWidth / 2;
|
||||
const screenCenterY = window.innerHeight / 2;
|
||||
const cx = window.innerWidth / 2;
|
||||
const cy = window.innerHeight / 2;
|
||||
|
||||
if (this.activeKeys.has("Minus") || this.activeKeys.has("KeyQ")) {
|
||||
this.eventBus.emit(
|
||||
new ZoomEvent(screenCenterX, screenCenterY, this.ZOOM_SPEED),
|
||||
);
|
||||
if (
|
||||
this.activeKeys.has(keybinds.zoomOut) ||
|
||||
this.activeKeys.has("Minus")
|
||||
) {
|
||||
this.eventBus.emit(new ZoomEvent(cx, cy, this.ZOOM_SPEED));
|
||||
}
|
||||
if (this.activeKeys.has("Equal") || this.activeKeys.has("KeyE")) {
|
||||
this.eventBus.emit(
|
||||
new ZoomEvent(screenCenterX, screenCenterY, -this.ZOOM_SPEED),
|
||||
);
|
||||
if (
|
||||
this.activeKeys.has(keybinds.zoomIn) ||
|
||||
this.activeKeys.has("Equal")
|
||||
) {
|
||||
this.eventBus.emit(new ZoomEvent(cx, cy, -this.ZOOM_SPEED));
|
||||
}
|
||||
}, 1);
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.code === "Space") {
|
||||
if (e.code === keybinds.toggleView) {
|
||||
e.preventDefault();
|
||||
if (!this.alternateView) {
|
||||
this.alternateView = true;
|
||||
@@ -187,24 +204,23 @@ export class InputHandler {
|
||||
this.eventBus.emit(new CloseViewEvent());
|
||||
}
|
||||
|
||||
// Add all movement keys to activeKeys
|
||||
if (
|
||||
[
|
||||
"KeyW",
|
||||
"KeyA",
|
||||
"KeyS",
|
||||
"KeyD",
|
||||
keybinds.moveUp,
|
||||
keybinds.moveDown,
|
||||
keybinds.moveLeft,
|
||||
keybinds.moveRight,
|
||||
keybinds.zoomOut,
|
||||
keybinds.zoomIn,
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"ArrowRight",
|
||||
"Minus",
|
||||
"Equal",
|
||||
"KeyE",
|
||||
"KeyQ",
|
||||
"Digit1",
|
||||
"Digit2",
|
||||
"KeyC",
|
||||
keybinds.centerCamera,
|
||||
"ControlLeft",
|
||||
"ControlRight",
|
||||
].includes(e.code)
|
||||
@@ -212,13 +228,13 @@ export class InputHandler {
|
||||
this.activeKeys.add(e.code);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("keyup", (e) => {
|
||||
if (e.code === "Space") {
|
||||
if (e.code === keybinds.toggleView) {
|
||||
e.preventDefault();
|
||||
this.alternateView = false;
|
||||
this.eventBus.emit(new AlternateViewEvent(false));
|
||||
}
|
||||
|
||||
if (e.key.toLowerCase() === "r" && e.altKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit(new RefreshGraphicsEvent());
|
||||
@@ -234,35 +250,12 @@ export class InputHandler {
|
||||
this.eventBus.emit(new AttackRatioEvent(10));
|
||||
}
|
||||
|
||||
if (e.code === "KeyC") {
|
||||
if (e.code === keybinds.centerCamera) {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit(new CenterCameraEvent());
|
||||
}
|
||||
|
||||
// Remove all movement keys from activeKeys
|
||||
if (
|
||||
[
|
||||
"KeyW",
|
||||
"KeyA",
|
||||
"KeyS",
|
||||
"KeyD",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"ArrowRight",
|
||||
"Minus",
|
||||
"Equal",
|
||||
"KeyE",
|
||||
"KeyQ",
|
||||
"Digit1",
|
||||
"Digit2",
|
||||
"KeyC",
|
||||
"ControlLeft",
|
||||
"ControlRight",
|
||||
].includes(e.code)
|
||||
) {
|
||||
this.activeKeys.delete(e.code);
|
||||
}
|
||||
this.activeKeys.delete(e.code);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -173,6 +173,7 @@ export class LangSelector extends LitElement {
|
||||
"help-modal",
|
||||
"username-input",
|
||||
"public-lobby",
|
||||
"user-setting",
|
||||
"o-modal",
|
||||
"o-button",
|
||||
];
|
||||
|
||||
+253
-86
@@ -1,6 +1,9 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import "./components/baseComponents/setting/SettingKeybind";
|
||||
import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind";
|
||||
import "./components/baseComponents/setting/SettingNumber";
|
||||
import "./components/baseComponents/setting/SettingSlider";
|
||||
import "./components/baseComponents/setting/SettingToggle";
|
||||
@@ -9,7 +12,8 @@ import "./components/baseComponents/setting/SettingToggle";
|
||||
export class UserSettingModal extends LitElement {
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
@state() private darkMode: boolean = this.userSettings.darkMode();
|
||||
@state() private settingsMode: "basic" | "keybinds" = "basic";
|
||||
@state() private keybinds: Record<string, string> = {};
|
||||
|
||||
@state() private keySequence: string[] = [];
|
||||
@state() private showEasterEggSettings = false;
|
||||
@@ -17,6 +21,15 @@ export class UserSettingModal extends LitElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
|
||||
const savedKeybinds = localStorage.getItem("settings.keybinds");
|
||||
if (savedKeybinds) {
|
||||
try {
|
||||
this.keybinds = JSON.parse(savedKeybinds);
|
||||
} catch (e) {
|
||||
console.warn("Invalid keybinds JSON:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@query("o-modal") private modalEl!: HTMLElement & {
|
||||
@@ -119,96 +132,63 @@ export class UserSettingModal extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeybindChange(
|
||||
e: CustomEvent<{ action: string; value: string }>,
|
||||
) {
|
||||
const { action, value } = e.detail;
|
||||
const prevValue = this.keybinds[action] ?? "";
|
||||
|
||||
const values = Object.entries(this.keybinds)
|
||||
.filter(([k]) => k !== action)
|
||||
.map(([, v]) => v);
|
||||
if (values.includes(value) && value !== "Null") {
|
||||
const popup = document.createElement("div");
|
||||
popup.className = "setting-popup";
|
||||
popup.textContent = `The key "${value}" is already assigned to another action.`;
|
||||
document.body.appendChild(popup);
|
||||
const element = this.renderRoot.querySelector(
|
||||
`setting-keybind[action="${action}"]`,
|
||||
) as SettingKeybind;
|
||||
if (element) {
|
||||
element.value = prevValue;
|
||||
element.requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.keybinds = { ...this.keybinds, [action]: value };
|
||||
localStorage.setItem("settings.keybinds", JSON.stringify(this.keybinds));
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal title="User Settings">
|
||||
<o-modal title="${translateText("user_setting.title")}">
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-content user-setting-modal">
|
||||
<div class="flex mb-4 w-full justify-center">
|
||||
<button
|
||||
class="w-1/2 text-center px-3 py-1 rounded-l
|
||||
${this.settingsMode === "basic"
|
||||
? "bg-white/10 text-white"
|
||||
: "bg-transparent text-gray-400"}"
|
||||
@click=${() => (this.settingsMode = "basic")}
|
||||
>
|
||||
${translateText("user_setting.tab_basic")}
|
||||
</button>
|
||||
<button
|
||||
class="w-1/2 text-center px-3 py-1 rounded-r
|
||||
${this.settingsMode === "keybinds"
|
||||
? "bg-white/10 text-white"
|
||||
: "bg-transparent text-gray-400"}"
|
||||
@click=${() => (this.settingsMode = "keybinds")}
|
||||
>
|
||||
${translateText("user_setting.tab_keybinds")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-list">
|
||||
<setting-toggle
|
||||
label="🌙 Dark Mode"
|
||||
description="Toggle the site’s appearance between light and dark themes"
|
||||
id="dark-mode-toggle"
|
||||
.checked=${this.userSettings.darkMode()}
|
||||
@change=${(e: CustomEvent<{ checked: boolean }>) =>
|
||||
this.toggleDarkMode(e)}
|
||||
></setting-toggle>
|
||||
|
||||
<setting-toggle
|
||||
label="😊 Emojis"
|
||||
description="Toggle whether emojis are shown in game"
|
||||
id="emoji-toggle"
|
||||
.checked=${this.userSettings.emojis()}
|
||||
@change=${this.toggleEmojis}
|
||||
></setting-toggle>
|
||||
|
||||
<setting-toggle
|
||||
label="🖱️ Left Click to Open Menu"
|
||||
description="When ON, left-click opens menu and sword button attacks. When OFF, right-click attacks directly."
|
||||
id="left-click-toggle"
|
||||
.checked=${this.userSettings.leftClickOpensMenu()}
|
||||
@change=${this.toggleLeftClickOpensMenu}
|
||||
></setting-toggle>
|
||||
|
||||
<setting-slider
|
||||
label="⚔️ Attack Ratio"
|
||||
description="What percentage of your troops to send in an attack (1–100%)"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${Number(
|
||||
localStorage.getItem("settings.attackRatio") ?? "0.2",
|
||||
) * 100}
|
||||
@change=${this.sliderAttackRatio}
|
||||
></setting-slider>
|
||||
|
||||
<setting-slider
|
||||
label="🪖🛠️ Troops and Workers Ratio"
|
||||
description="Adjust the balance between troops (for combat) and workers (for gold production) (1–100%)"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${Number(
|
||||
localStorage.getItem("settings.troopRatio") ?? "0.95",
|
||||
) * 100}
|
||||
@change=${this.sliderTroopRatio}
|
||||
></setting-slider>
|
||||
|
||||
${this.showEasterEggSettings
|
||||
? html`
|
||||
<setting-slider
|
||||
label="Writing Speed Multiplier"
|
||||
description="Adjust how fast you pretend to code (x1–x100)"
|
||||
min="0"
|
||||
max="100"
|
||||
value="40"
|
||||
easter="true"
|
||||
@change=${(e: CustomEvent) => {
|
||||
const value = e.detail?.value;
|
||||
if (typeof value !== "undefined") {
|
||||
console.log("Changed:", value);
|
||||
} else {
|
||||
console.warn("Slider event missing detail.value", e);
|
||||
}
|
||||
}}
|
||||
></setting-slider>
|
||||
|
||||
<setting-number
|
||||
label="Bug Count"
|
||||
description="How many bugs you're okay with (0–1000, emotionally)"
|
||||
value="100"
|
||||
min="0"
|
||||
max="1000"
|
||||
easter="true"
|
||||
@change=${(e: CustomEvent) => {
|
||||
const value = e.detail?.value;
|
||||
if (typeof value !== "undefined") {
|
||||
console.log("Changed:", value);
|
||||
} else {
|
||||
console.warn("Slider event missing detail.value", e);
|
||||
}
|
||||
}}
|
||||
></setting-number>
|
||||
`
|
||||
: null}
|
||||
${this.settingsMode === "basic"
|
||||
? this.renderBasicSettings()
|
||||
: this.renderKeybindSettings()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -216,7 +196,194 @@ export class UserSettingModal extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBasicSettings() {
|
||||
return html`
|
||||
<!-- 🌙 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>
|
||||
|
||||
<!-- 🖱️ 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>
|
||||
|
||||
<!-- ⚔️ 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>
|
||||
|
||||
<!-- 🪖🛠️ Troop Ratio -->
|
||||
<setting-slider
|
||||
label="${translateText("user_setting.troop_ratio_label")}"
|
||||
description="${translateText("user_setting.troop_ratio_desc")}"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${Number(localStorage.getItem("settings.troopRatio") ?? "0.95") *
|
||||
100}
|
||||
@change=${this.sliderTroopRatio}
|
||||
></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 (typeof 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 (typeof value !== "undefined") {
|
||||
console.log("Changed:", value);
|
||||
} else {
|
||||
console.warn("Slider event missing detail.value", e);
|
||||
}
|
||||
}}
|
||||
></setting-number>
|
||||
`
|
||||
: null}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderKeybindSettings() {
|
||||
return html`
|
||||
<div class="text-center text-white text-base font-semibold mt-5 mb-2">
|
||||
${translateText("user_setting.view_options")}
|
||||
</div>
|
||||
|
||||
<setting-keybind
|
||||
action="toggleView"
|
||||
label=${translateText("user_setting.toggle_view")}
|
||||
description=${translateText("user_setting.toggle_view_desc")}
|
||||
defaultKey="Space"
|
||||
.value=${this.keybinds["toggleView"] ?? ""}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<div class="text-center text-white text-base font-semibold mt-5 mb-2">
|
||||
${translateText("user_setting.zoom_controls")}
|
||||
</div>
|
||||
|
||||
<setting-keybind
|
||||
action="zoomOut"
|
||||
label=${translateText("user_setting.zoom_out")}
|
||||
description=${translateText("user_setting.zoom_out_desc")}
|
||||
defaultKey="KeyQ"
|
||||
.value=${this.keybinds["zoomOut"] ?? ""}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="zoomIn"
|
||||
label=${translateText("user_setting.zoom_in")}
|
||||
description=${translateText("user_setting.zoom_in_desc")}
|
||||
defaultKey="KeyE"
|
||||
.value=${this.keybinds["zoomIn"] ?? ""}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<div class="text-center text-white text-base font-semibold mt-5 mb-2">
|
||||
${translateText("user_setting.camera_movement")}
|
||||
</div>
|
||||
|
||||
<setting-keybind
|
||||
action="centerCamera"
|
||||
label=${translateText("user_setting.center_camera")}
|
||||
description=${translateText("user_setting.center_camera_desc")}
|
||||
defaultKey="KeyC"
|
||||
.value=${this.keybinds["centerCamera"] ?? ""}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveUp"
|
||||
label=${translateText("user_setting.move_up")}
|
||||
description=${translateText("user_setting.move_up_desc")}
|
||||
defaultKey="KeyW"
|
||||
.value=${this.keybinds["moveUp"] ?? ""}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveLeft"
|
||||
label=${translateText("user_setting.move_left")}
|
||||
description=${translateText("user_setting.move_left_desc")}
|
||||
defaultKey="KeyA"
|
||||
.value=${this.keybinds["moveLeft"] ?? ""}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveDown"
|
||||
label=${translateText("user_setting.move_down")}
|
||||
description=${translateText("user_setting.move_down_desc")}
|
||||
defaultKey="KeyS"
|
||||
.value=${this.keybinds["moveDown"] ?? ""}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveRight"
|
||||
label=${translateText("user_setting.move_right")}
|
||||
description=${translateText("user_setting.move_right_desc")}
|
||||
defaultKey="KeyD"
|
||||
.value=${this.keybinds["moveRight"] ?? ""}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
`;
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.requestUpdate();
|
||||
this.modalEl?.open();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { translateText } from "../../../../client/Utils";
|
||||
|
||||
@customElement("setting-keybind")
|
||||
export class SettingKeybind extends LitElement {
|
||||
@property() label = "Setting";
|
||||
@property() description = "";
|
||||
@property({ type: String, reflect: true }) action = "";
|
||||
@property({ type: String }) defaultKey = "";
|
||||
@property({ type: String }) value = "";
|
||||
@property({ type: Boolean }) easter = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private listening = false;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="setting-item column${this.easter ? " easter-egg" : ""}">
|
||||
<div class="setting-label-group">
|
||||
<label class="setting-label block mb-1">${this.label}</label>
|
||||
|
||||
<div class="setting-keybind-box">
|
||||
<div class="setting-keybind-description">${this.description}</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="setting-key"
|
||||
tabindex="0"
|
||||
@keydown=${this.handleKeydown}
|
||||
@click=${this.startListening}
|
||||
>
|
||||
${this.displayKey(this.value || this.defaultKey)}
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="text-xs text-gray-400 hover:text-white border border-gray-500 px-2 py-0.5 rounded transition"
|
||||
@click=${this.resetToDefault}
|
||||
>
|
||||
${translateText("user_setting.reset")}
|
||||
</button>
|
||||
<button
|
||||
class="text-xs text-gray-400 hover:text-white border border-gray-500 px-2 py-0.5 rounded transition"
|
||||
@click=${this.unbindKey}
|
||||
>
|
||||
${translateText("user_setting.unbind")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private displayKey(key: string): string {
|
||||
if (key === " ") return "Space";
|
||||
if (key.startsWith("Key") && key.length === 4) {
|
||||
return key.slice(3);
|
||||
}
|
||||
return key.length
|
||||
? key.charAt(0).toUpperCase() + key.slice(1)
|
||||
: "Press a key";
|
||||
}
|
||||
|
||||
private startListening() {
|
||||
this.listening = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private handleKeydown(e: KeyboardEvent) {
|
||||
if (!this.listening) return;
|
||||
e.preventDefault();
|
||||
|
||||
const code = e.code;
|
||||
|
||||
this.value = code;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { action: this.action, value: code },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
this.listening = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private resetToDefault() {
|
||||
this.value = this.defaultKey;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { action: this.action, value: this.defaultKey },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private unbindKey() {
|
||||
this.value = "";
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { action: this.action, value: "Null" },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
@@ -78,3 +78,14 @@
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.setting-input.keybind:hover .key,
|
||||
.setting-input.keybind:focus .key {
|
||||
background-color: #333;
|
||||
box-shadow: 0 2px 0 #222;
|
||||
}
|
||||
|
||||
.setting-input.keybind.listening .key {
|
||||
background-color: #1d4ed8; /* blue-700 */
|
||||
box-shadow: 0 2px 0 #0f172a; /* darker blue */
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-item.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@keyframes rainbow-background {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
@@ -64,6 +68,20 @@
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.setting-popup {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
padding: 16px 24px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
border-radius: 12px;
|
||||
animation: fadePop_2 10s ease-out forwards;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
@keyframes fadePop {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -82,6 +100,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadePop_2 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.6);
|
||||
}
|
||||
5% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
}
|
||||
95% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
@@ -158,17 +195,14 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-input.slider::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 2px solid #2196f3;
|
||||
cursor: pointer;
|
||||
.setting-input.slider::-moz-range-track {
|
||||
background-color: #444;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.setting-input.slider::-moz-range-track {
|
||||
background: linear-gradient(to right, #2196f3 50%, #444 50%);
|
||||
.setting-input.slider::-moz-range-progress {
|
||||
background-color: #2196f3;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
@@ -255,3 +289,38 @@
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.setting-keybind-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.setting-keybind-description {
|
||||
flex: 1;
|
||||
font-size: 0.75rem;
|
||||
color: #e5e5e5;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.setting-key {
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
box-shadow: 0 2px 0 #444;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.setting-key:focus {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user