Slider UI change (#2148)

## Description:

1. adds feature so when making single player server, bots amount can be
determined by slider and manual
1-1.
[video](https://github.com/user-attachments/assets/23186592-2330-478b-8f73-313ca073803b)

## 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:

jack_45183

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
Baeck Dong Jae
2025-10-14 03:37:17 +09:00
committed by GitHub
parent 11ae047e33
commit 972697a5ae
5 changed files with 170 additions and 41 deletions
+4
View File
@@ -132,6 +132,10 @@
"info_enemy_panel": "Enemy info panel",
"exit_confirmation": "Are you sure you want to exit the game?"
},
"slider": {
"label": "Bots:",
"disabled": "Disabled"
},
"single_modal": {
"title": "Single Player",
"allow_alliances": "Allow alliances",
+4
View File
@@ -132,6 +132,10 @@
"enables_title": "설정 활성화",
"start": "게임 시작하기"
},
"slider": {
"label": "봇:",
"disabled": "사용 안 함"
},
"map": {
"map": "지도",
"world": "세계",
+11 -20
View File
@@ -311,26 +311,17 @@ export class HostLobbyModal extends LitElement {
${translateText("host_modal.options_title")}
</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${String(this.bots)}"
/>
<div class="option-card-title">
<span>${translateText("host_modal.bots")}</span>${
this.bots === 0
? translateText("host_modal.bots_disabled")
: this.bots
}
</div>
</label>
<label for="bots-count" class="option-card">
<!-- Slider -->
<fluent-slider
.value=${this.bots}
.min=${0}
.max=${400}
.step=${1}
ariaLabel=${translateText("single_modal.bots")}
@input=${this.handleBotsChange}
></fluent-slider>
</label>
<label
for="disable-npcs"
class="option-card ${this.disableNPCs ? "selected" : ""}"
+14 -21
View File
@@ -20,6 +20,7 @@ import { generateID } from "../core/Util";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./components/Difficulties";
import "./components/FluentSlider";
import "./components/Maps";
import { fetchCosmetics } from "./Cosmetics";
import { FlagInput } from "./FlagInput";
@@ -44,7 +45,6 @@ export class SinglePlayerModal extends LitElement {
@state() private useRandomMap: boolean = false;
@state() private gameMode: GameMode = GameMode.FFA;
@state() private teamCount: TeamCountConfig = 2;
@state() private disabledUnits: UnitType[] = [];
private userSettings: UserSettings = new UserSettings();
@@ -220,24 +220,16 @@ export class SinglePlayerModal extends LitElement {
</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
min="0"
max="400"
step="1"
<!-- Slider -->
<fluent-slider
.value=${this.bots}
.min=${0}
.max=${400}
.step=${1}
ariaLabel=${translateText("single_modal.bots")}
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${String(this.bots)}"
/>
<div class="option-card-title">
<span>${translateText("single_modal.bots")}</span>${this
.bots === 0
? translateText("single_modal.bots_disabled")
: this.bots}
</div>
></fluent-slider>
</label>
<label
for="singleplayer-modal-disable-npcs"
class="option-card ${this.disableNPCs ? "selected" : ""}"
@@ -372,10 +364,11 @@ export class SinglePlayerModal extends LitElement {
}
private handleBotsChange(e: Event) {
const value = parseInt((e.target as HTMLInputElement).value);
if (isNaN(value) || value < 0 || value > 400) {
return;
}
const inputEl = e.target as HTMLInputElement;
let value = inputEl.valueAsNumber;
if (Number.isNaN(value)) return;
if (value < 0) value = 0;
if (value > 400) value = 400;
this.bots = value;
}
+137
View File
@@ -0,0 +1,137 @@
import { LitElement, css, html } from "lit";
import { property, query, state } from "lit/decorators.js";
import { translateText } from "../Utils";
export class FluentSlider extends LitElement {
static styles = css`
:host {
display: block;
width: 100%;
font-family: inherit;
}
.slider-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
text-align: center;
}
.option-card-title {
font-size: 14px; /* match other cards */
color: #aaa; /* light gray text */
text-align: center;
margin: 0 0 4px 0;
font-weight: normal;
}
input[type="range"] {
width: 100%;
max-width: 100%;
background-color: transparent;
}
input[type="number"] {
width: 60px;
background-color: #2d3748;
color: #aaa; /* match label color */
border: 1px solid #4a5568;
text-align: center;
border-radius: 4px;
font-weight: normal;
font-family: inherit;
}
span.editable {
cursor: pointer;
min-width: 60px;
display: inline-block;
text-align: center;
color: #aaa; /* match label color */
font-weight: normal;
user-select: none;
}
`;
@property({ type: Number }) value = 0;
@property({ type: Number }) min = 0;
@property({ type: Number }) max = 400;
@property({ type: Number }) step = 1;
@state() private isEditing = false;
@query("input[type='number']") private numberInput!: HTMLInputElement;
private handleSliderChange(e: Event) {
const target = e.target as HTMLInputElement;
this.value = Number(target.value);
}
private handleNumberChange(e: Event) {
const target = e.target as HTMLInputElement;
let val = Number(target.value);
if (val < this.min) val = this.min;
if (val > this.max) val = this.max;
this.value = val;
}
private handleNumberKeyDown(e: KeyboardEvent) {
if (e.key === "Enter") this.isEditing = false;
}
private enableEditing() {
this.isEditing = true;
this.updateComplete.then(() => this.numberInput?.focus());
}
render() {
return html`
<div class="slider-container">
<input
type="range"
.min=${this.min}
.max=${this.max}
.step=${this.step}
.value=${String(this.value)}
@input=${this.handleSliderChange}
/>
<div class="option-card-title">
<span>${translateText("slider.label")}</span>
${this.isEditing
? html`<input
type="number"
.min=${this.min}
.max=${this.max}
.value=${this.value}
@input=${this.handleNumberChange}
@blur=${() => (this.isEditing = false)}
@keydown=${this.handleNumberKeyDown}
/>`
: html`<span
class="editable"
role="button"
tabindex="0"
@click=${this.enableEditing}
@keydown=${(e: KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
this.enableEditing();
e.preventDefault();
}
}}
>
${this.value === 0
? translateText("slider.disabled")
: this.value}
</span>`}
</div>
</div>
`;
}
}
customElements.define("fluent-slider", FluentSlider);