mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-28 18:54:18 +00:00
4e48eba910
## Description: Top Bar Refactor – UI & UX Improvement Proposal This update overhauls the top game bar to improve clarity, responsiveness, and overall user experience across desktop and mobile. It consolidates player resources (e.g., building counts), integrates game controls (pause, settings, time), and enhances visual contrast. Key changes: Redesigned top bar with player data and game options. Team color indicator bar (team games only). Countdown bar during "Choose Starting Position" phase. Removed redundant info (e.g., troop/worker counts shown elsewhere). Inspired by strategy games like Travian Legends, this refactor offers a cleaner and more intuitive layout, especially for smaller screens. ⚠️ Note: This is a large change and likely contains visual or functional bugs I can’t currently spot due to fatigue. Thorough testing is required before approval. ## 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 - [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: Diessel  om/user-attachments/assets/3a0edbef-e621-4fc4-b6b7-c1ed  8f9a8219)  Closes #1165 --------- Co-authored-by: Scott Anderson <scottanderson@users.noreply.github.com> Co-authored-by: evanpelle <evanpelle@gmail.com>
260 lines
7.6 KiB
TypeScript
260 lines
7.6 KiB
TypeScript
import { LitElement, html } from "lit";
|
|
import { customElement, state } from "lit/decorators.js";
|
|
import { translateText } from "../../../client/Utils";
|
|
import { EventBus } from "../../../core/EventBus";
|
|
import { GameView } from "../../../core/game/GameView";
|
|
import { AttackRatioEvent } from "../../InputHandler";
|
|
import { SendSetTargetTroopRatioEvent } from "../../Transport";
|
|
import { renderTroops } from "../../Utils";
|
|
import { UIState } from "../UIState";
|
|
import { Layer } from "./Layer";
|
|
|
|
@customElement("control-panel")
|
|
export class ControlPanel extends LitElement implements Layer {
|
|
public game: GameView;
|
|
public eventBus: EventBus;
|
|
public uiState: UIState;
|
|
|
|
@state()
|
|
private attackRatio: number = 0.2;
|
|
|
|
@state()
|
|
private targetTroopRatio = 0.95;
|
|
|
|
@state()
|
|
private currentTroopRatio = 0.95;
|
|
|
|
@state()
|
|
private _population: number;
|
|
|
|
@state()
|
|
private _isVisible = false;
|
|
|
|
@state()
|
|
private _manpower: number = 0;
|
|
|
|
private _lastPopulationIncreaseRate: number;
|
|
private init_: boolean = false;
|
|
|
|
init() {
|
|
this.attackRatio = Number(
|
|
localStorage.getItem("settings.attackRatio") ?? "0.2",
|
|
);
|
|
this.targetTroopRatio = Number(
|
|
localStorage.getItem("settings.troopRatio") ?? "0.95",
|
|
);
|
|
this.init_ = true;
|
|
this.uiState.attackRatio = this.attackRatio;
|
|
this.currentTroopRatio = this.targetTroopRatio;
|
|
this.eventBus.on(AttackRatioEvent, (event) => {
|
|
let newAttackRatio =
|
|
(parseInt(
|
|
(document.getElementById("attack-ratio") as HTMLInputElement).value,
|
|
) +
|
|
event.attackRatio) /
|
|
100;
|
|
|
|
if (newAttackRatio < 0.01) {
|
|
newAttackRatio = 0.01;
|
|
}
|
|
|
|
if (newAttackRatio > 1) {
|
|
newAttackRatio = 1;
|
|
}
|
|
|
|
if (newAttackRatio === 0.11 && this.attackRatio === 0.01) {
|
|
// If we're changing the ratio from 1%, then set it to 10% instead of 11% to keep a consistency
|
|
newAttackRatio = 0.1;
|
|
}
|
|
|
|
this.attackRatio = newAttackRatio;
|
|
this.onAttackRatioChange(this.attackRatio);
|
|
});
|
|
}
|
|
|
|
tick() {
|
|
if (this.init_) {
|
|
this.eventBus.emit(
|
|
new SendSetTargetTroopRatioEvent(this.targetTroopRatio),
|
|
);
|
|
this.init_ = false;
|
|
}
|
|
|
|
if (!this._isVisible && !this.game.inSpawnPhase()) {
|
|
this.setVisibile(true);
|
|
}
|
|
|
|
const player = this.game.myPlayer();
|
|
if (player === null || !player.isAlive()) {
|
|
this.setVisibile(false);
|
|
return;
|
|
}
|
|
|
|
const popIncreaseRate = player.population() - this._population;
|
|
if (this.game.ticks() % 5 === 0) {
|
|
this._lastPopulationIncreaseRate = popIncreaseRate;
|
|
}
|
|
|
|
this._population = player.population();
|
|
|
|
this.currentTroopRatio = player.troops() / player.population();
|
|
this.requestUpdate();
|
|
}
|
|
|
|
onAttackRatioChange(newRatio: number) {
|
|
this.uiState.attackRatio = newRatio;
|
|
}
|
|
|
|
renderLayer(context: CanvasRenderingContext2D) {
|
|
// Render any necessary canvas elements
|
|
}
|
|
|
|
shouldTransform(): boolean {
|
|
return false;
|
|
}
|
|
|
|
setVisibile(visible: boolean) {
|
|
this._isVisible = visible;
|
|
this.requestUpdate();
|
|
}
|
|
|
|
targetTroops(): number {
|
|
return this._manpower * this.targetTroopRatio;
|
|
}
|
|
|
|
onTroopChange(newRatio: number) {
|
|
this.eventBus.emit(new SendSetTargetTroopRatioEvent(newRatio));
|
|
}
|
|
|
|
delta(): number {
|
|
const d = this._population - this.targetTroops();
|
|
return d;
|
|
}
|
|
|
|
render() {
|
|
return html`
|
|
<style>
|
|
input[type="range"] {
|
|
-webkit-appearance: none;
|
|
background: transparent;
|
|
outline: none;
|
|
}
|
|
input[type="range"]::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
background: white;
|
|
border-width: 2px;
|
|
border-style: solid;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
input[type="range"]::-moz-range-thumb {
|
|
width: 16px;
|
|
height: 16px;
|
|
background: white;
|
|
border-width: 2px;
|
|
border-style: solid;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
.targetTroopRatio::-webkit-slider-thumb {
|
|
border-color: rgb(59 130 246);
|
|
}
|
|
.targetTroopRatio::-moz-range-thumb {
|
|
border-color: rgb(59 130 246);
|
|
}
|
|
.attackRatio::-webkit-slider-thumb {
|
|
border-color: rgb(239 68 68);
|
|
}
|
|
.attackRatio::-moz-range-thumb {
|
|
border-color: rgb(239 68 68);
|
|
}
|
|
</style>
|
|
<div
|
|
class="${this._isVisible
|
|
? "text-sm lg:text-m md:w-[320px] bg-gray-800/70 p-2 pr-3 lg:p-4 shadow-lg lg:rounded-lg backdrop-blur"
|
|
: "hidden"}"
|
|
@contextmenu=${(e) => e.preventDefault()}
|
|
>
|
|
<div class="relative mb-4 lg:mb-4">
|
|
<label class="flex justify-between text-white mb-1" translate="no">
|
|
<span>
|
|
${translateText("control_panel.troops")}:
|
|
${(this.currentTroopRatio * 100).toFixed(0)}%
|
|
</span>
|
|
<span>
|
|
${translateText("control_panel.workers")}:
|
|
${((1 - this.currentTroopRatio) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<div class="relative h-8">
|
|
<!-- Background track -->
|
|
<div
|
|
class="absolute left-0 right-0 top-3 h-2 bg-white/20 rounded"
|
|
></div>
|
|
<!-- Fill track -->
|
|
<div
|
|
class="absolute left-0 top-3 h-2 bg-blue-500/60 rounded transition-all duration-300"
|
|
style="width: ${this.currentTroopRatio * 100}%"
|
|
></div>
|
|
<!-- Range input - exactly overlaying the visual elements -->
|
|
<input
|
|
type="range"
|
|
min="1"
|
|
max="100"
|
|
.value=${(this.targetTroopRatio * 100).toString()}
|
|
@input=${(e: Event) => {
|
|
this.targetTroopRatio =
|
|
parseInt((e.target as HTMLInputElement).value) / 100;
|
|
this.onTroopChange(this.targetTroopRatio);
|
|
}}
|
|
class="absolute left-0 right-0 top-2 m-0 h-4 cursor-pointer targetTroopRatio"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative mb-0 lg:mb-4">
|
|
<label class="block text-white mb-1" translate="no"
|
|
>${translateText("control_panel.attack_ratio")}:
|
|
${(this.attackRatio * 100).toFixed(0)}%
|
|
(${renderTroops(
|
|
(this.game?.myPlayer()?.troops() ?? 0) * this.attackRatio,
|
|
)})</label
|
|
>
|
|
<div class="relative h-8">
|
|
<!-- Background track -->
|
|
<div
|
|
class="absolute left-0 right-0 top-3 h-2 bg-white/20 rounded"
|
|
></div>
|
|
<!-- Fill track -->
|
|
<div
|
|
class="absolute left-0 top-3 h-2 bg-red-500/60 rounded transition-all duration-300"
|
|
style="width: ${this.attackRatio * 100}%"
|
|
></div>
|
|
<!-- Range input - exactly overlaying the visual elements -->
|
|
<input
|
|
id="attack-ratio"
|
|
type="range"
|
|
min="1"
|
|
max="100"
|
|
.value=${(this.attackRatio * 100).toString()}
|
|
@input=${(e: Event) => {
|
|
this.attackRatio =
|
|
parseInt((e.target as HTMLInputElement).value) / 100;
|
|
this.onAttackRatioChange(this.attackRatio);
|
|
}}
|
|
class="absolute left-0 right-0 top-2 m-0 h-4 cursor-pointer attackRatio"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
createRenderRoot() {
|
|
return this; // Disable shadow DOM to allow Tailwind styles
|
|
}
|
|
}
|