mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-04 11:06:09 +00:00
Update control panel UI (#3357)
Relates to #2260 ## Description: Inspired by https://github.com/openfrontio/OpenFrontIO/pull/3359 This PR centers the control panel and combines it with the units display. The reasoning is that the control panel contains the most critical info so it should be in the center of the screen. Combining it with the units display reduces the number of UI components on screen. Also made the attack ratio bar persistent on mobile <img width="618" height="216" alt="Screenshot 2026-03-06 at 2 06 34 PM" src="https://github.com/user-attachments/assets/34b041c1-d78b-46b5-a7ab-f2a44145a7e2" /> <img width="941" height="343" alt="Screenshot 2026-03-06 at 2 06 55 PM" src="https://github.com/user-attachments/assets/1e3b026c-8eb2-407c-be38-0e71e1ae426c" /> <img width="562" height="228" alt="Screenshot 2026-03-06 at 4 11 20 PM" src="https://github.com/user-attachments/assets/56eac49f-c8a4-4ac1-a60a-f1bcb2fad2d0" /> <img width="939" height="357" alt="Screenshot 2026-03-06 at 4 11 32 PM" src="https://github.com/user-attachments/assets/eb5591d5-3cc2-4182-944b-3a4b0b76852a" /> ## 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: evan Co-authored-by: hkio120 <111693579+hkio120@users.noreply.github.com>
This commit is contained in:
+21
-8
@@ -264,23 +264,37 @@
|
||||
<!-- Game components -->
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- Bottom HUD: <sm=column, sm..lg=2col (HUD left | events right), lg+=3col grid centered -->
|
||||
<div
|
||||
class="fixed left-0 bottom-0 min-[1200px]:left-4 min-[1200px]:bottom-4 w-full flex flex-col sm:flex-row sm:items-end z-50 pointer-events-none"
|
||||
class="fixed bottom-0 left-0 w-full z-[200] flex flex-col pointer-events-none sm:flex-row sm:items-end lg:grid lg:grid-cols-[1fr_460px_1fr] lg:items-end min-[1200px]:bottom-4 min-[1200px]:px-4"
|
||||
style="
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
"
|
||||
>
|
||||
<!-- HUD: <sm full-width, sm..lg left side 460px, lg+ col-2 -->
|
||||
<div
|
||||
class="contents sm:flex sm:flex-col sm:w-1/2 min-[1200px]:w-auto lg:max-w-[400px]"
|
||||
class="flex flex-col pointer-events-none w-full sm:w-[460px] lg:col-start-2 z-10"
|
||||
>
|
||||
<attacks-display class="order-2 sm:order-none w-full"></attacks-display>
|
||||
<control-panel class="order-4 sm:order-none w-full"></control-panel>
|
||||
<attacks-display class="w-full pointer-events-auto"></attacks-display>
|
||||
<div
|
||||
class="pointer-events-auto bg-gray-800/70 backdrop-blur-xs rounded-lg shadow-lg"
|
||||
>
|
||||
<control-panel class="w-full"></control-panel>
|
||||
<unit-display class="hidden lg:block w-full"></unit-display>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- events+chat: <sm above HUD (order-first), sm..lg right side, lg+ col-3 -->
|
||||
<div
|
||||
class="contents sm:flex sm:flex-col sm:flex-1 min-[1200px]:w-auto min-[1200px]:fixed min-[1200px]:right-0 min-[1200px]:bottom-0 sm:items-end pointer-events-none"
|
||||
class="flex flex-col pointer-events-none items-end order-first sm:order-none sm:flex-1 lg:col-start-3 lg:self-end lg:justify-end"
|
||||
>
|
||||
<chat-display
|
||||
class="order-1 sm:order-none w-full sm:w-auto"
|
||||
class="w-full sm:w-auto pointer-events-auto"
|
||||
></chat-display>
|
||||
<events-display
|
||||
class="order-3 sm:order-none w-full sm:w-auto"
|
||||
class="w-full sm:w-auto pointer-events-auto"
|
||||
></events-display>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,7 +304,6 @@
|
||||
<build-menu></build-menu>
|
||||
<win-modal></win-modal>
|
||||
<game-starting-modal></game-starting-modal>
|
||||
<unit-display></unit-display>
|
||||
<div
|
||||
class="flex flex-col items-end fixed top-0 right-0 min-[1200px]:top-4 min-[1200px]:right-4 z-1000 gap-2"
|
||||
>
|
||||
|
||||
@@ -221,7 +221,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
return this.incomingAttacks.map(
|
||||
(attack) => html`
|
||||
<div
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs min-[1200px]:rounded-lg sm:rounded-r-lg px-1.5 py-0.5 overflow-hidden"
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs rounded-lg px-1.5 py-0.5 overflow-hidden"
|
||||
>
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
@@ -269,7 +269,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
return this.outgoingAttacks.map(
|
||||
(attack) => html`
|
||||
<div
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs min-[1200px]:rounded-lg sm:rounded-r-lg px-1.5 py-0.5 overflow-hidden"
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs rounded-lg px-1.5 py-0.5 overflow-hidden"
|
||||
>
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
@@ -311,7 +311,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
return this.outgoingLandAttacks.map(
|
||||
(landAttack) => html`
|
||||
<div
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs min-[1200px]:rounded-lg sm:rounded-r-lg px-1.5 py-0.5 overflow-hidden"
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs rounded-lg px-1.5 py-0.5 overflow-hidden"
|
||||
>
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
@@ -367,7 +367,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
return this.outgoingBoats.map(
|
||||
(boat) => html`
|
||||
<div
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs min-[1200px]:rounded-lg sm:rounded-r-lg px-1.5 py-0.5 overflow-hidden"
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs rounded-lg px-1.5 py-0.5 overflow-hidden"
|
||||
>
|
||||
${this.renderButton({
|
||||
content: html`${this.renderBoatIcon(boat)}
|
||||
@@ -403,7 +403,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
return this.incomingBoats.map(
|
||||
(boat) => html`
|
||||
<div
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs min-[1200px]:rounded-lg sm:rounded-r-lg px-1.5 py-0.5 overflow-hidden"
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs rounded-lg px-1.5 py-0.5 overflow-hidden"
|
||||
>
|
||||
${this.renderButton({
|
||||
content: html`${this.renderBoatIcon(boat)}
|
||||
@@ -441,7 +441,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="w-full mb-1 mt-1 sm:mt-0 pointer-events-auto grid grid-cols-2 sm:grid-cols-1 gap-1 text-white text-sm lg:text-base"
|
||||
class="w-full mb-1 mt-1 sm:mt-0 pointer-events-auto grid grid-cols-2 gap-1 text-white text-sm lg:text-base"
|
||||
>
|
||||
${this.renderOutgoingAttacks()} ${this.renderOutgoingLandAttacks()}
|
||||
${this.renderBoats()} ${this.renderIncomingAttacks()}
|
||||
|
||||
@@ -40,9 +40,6 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
@state()
|
||||
private _attackingTroops: number = 0;
|
||||
|
||||
@state()
|
||||
private _touchDragging = false;
|
||||
|
||||
private _troopRateIsIncreasing: boolean = true;
|
||||
|
||||
private _lastTroopIncreaseRate: number;
|
||||
@@ -127,73 +124,13 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _outsideTouchHandler: ((ev: Event) => void) | null = null;
|
||||
|
||||
private handleAttackTouchStart(e: TouchEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this._touchDragging) {
|
||||
this.closeAttackBar();
|
||||
return;
|
||||
}
|
||||
|
||||
this._touchDragging = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this._outsideTouchHandler = () => {
|
||||
this.closeAttackBar();
|
||||
};
|
||||
document.addEventListener("touchstart", this._outsideTouchHandler);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private closeAttackBar() {
|
||||
this._touchDragging = false;
|
||||
if (this._outsideTouchHandler) {
|
||||
document.removeEventListener("touchstart", this._outsideTouchHandler);
|
||||
this._outsideTouchHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleBarTouch(e: TouchEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.setRatioFromTouch(e.touches[0]);
|
||||
|
||||
const onMove = (ev: TouchEvent) => {
|
||||
ev.preventDefault();
|
||||
this.setRatioFromTouch(ev.touches[0]);
|
||||
};
|
||||
|
||||
const onEnd = () => {
|
||||
document.removeEventListener("touchmove", onMove);
|
||||
document.removeEventListener("touchend", onEnd);
|
||||
};
|
||||
|
||||
document.addEventListener("touchmove", onMove, { passive: false });
|
||||
document.addEventListener("touchend", onEnd);
|
||||
}
|
||||
|
||||
private setRatioFromTouch(touch: Touch) {
|
||||
const barEl = this.querySelector(".attack-drag-bar");
|
||||
if (!barEl) return;
|
||||
|
||||
const rect = barEl.getBoundingClientRect();
|
||||
const ratio = (rect.bottom - touch.clientY) / (rect.bottom - rect.top);
|
||||
this.attackRatio =
|
||||
Math.round(Math.max(1, Math.min(100, ratio * 100))) / 100;
|
||||
this.onAttackRatioChange(this.attackRatio);
|
||||
}
|
||||
|
||||
private handleRatioSliderInput(e: Event) {
|
||||
const value = Number((e.target as HTMLInputElement).value);
|
||||
this.attackRatio = value / 100;
|
||||
this.onAttackRatioChange(this.attackRatio);
|
||||
}
|
||||
|
||||
private renderTroopBar() {
|
||||
private calculateTroopBar(): { greenPercent: number; orangePercent: number } {
|
||||
const base = Math.max(this._maxTroops, 1);
|
||||
const greenPercentRaw = (this._troops / base) * 100;
|
||||
const orangePercentRaw = (this._attackingTroops / base) * 100;
|
||||
@@ -204,9 +141,14 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
Math.min(100 - greenPercent, orangePercentRaw),
|
||||
);
|
||||
|
||||
return { greenPercent, orangePercent };
|
||||
}
|
||||
|
||||
private renderMobileTroopBar() {
|
||||
const { greenPercent, orangePercent } = this.calculateTroopBar();
|
||||
return html`
|
||||
<div
|
||||
class="w-full h-6 lg:h-8 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
|
||||
class="w-full h-6 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
|
||||
>
|
||||
<div class="h-full flex">
|
||||
${greenPercent > 0
|
||||
@@ -223,7 +165,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
: ""}
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-between px-1.5 lg:px-2 text-xs lg:text-sm font-bold leading-none pointer-events-none"
|
||||
class="absolute inset-0 flex items-center justify-between px-1.5 text-xs font-bold leading-none pointer-events-none"
|
||||
translate="no"
|
||||
>
|
||||
<span class="text-white drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]"
|
||||
@@ -243,10 +185,10 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
aria-hidden="true"
|
||||
width="12"
|
||||
height="12"
|
||||
class="lg:w-4 lg:h-4 brightness-0 invert drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]"
|
||||
class="brightness-0 invert drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]"
|
||||
/>
|
||||
<span
|
||||
class="text-[10px] lg:text-xs font-bold drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)] ${this
|
||||
class="text-[10px] font-bold drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)] ${this
|
||||
._troopRateIsIncreasing
|
||||
? "text-green-400"
|
||||
: "text-orange-400"}"
|
||||
@@ -257,127 +199,175 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
private renderDesktopTroopBar() {
|
||||
const { greenPercent, orangePercent } = this.calculateTroopBar();
|
||||
return html`
|
||||
<div
|
||||
class="relative pointer-events-auto ${this._isVisible
|
||||
? "relative z-[60] w-full lg:max-w-[400px] text-sm lg:text-base bg-gray-800/70 p-1.5 pr-2 lg:p-5 shadow-lg sm:rounded-tr-lg min-[1200px]:rounded-lg backdrop-blur-xs"
|
||||
: "hidden"}"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
class="w-full h-6 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
|
||||
>
|
||||
<div class="flex gap-2 lg:gap-3 items-center">
|
||||
<!-- Gold: 1/4 -->
|
||||
<div
|
||||
class="flex items-center justify-center p-1 lg:p-1.5 lg:gap-1 border rounded-md border-yellow-400 font-bold text-yellow-400 text-xs lg:text-sm w-1/5 lg:w-auto shrink-0"
|
||||
translate="no"
|
||||
>
|
||||
<img
|
||||
src=${goldCoinIcon}
|
||||
width="13"
|
||||
height="13"
|
||||
class="lg:w-4 lg:h-4"
|
||||
/>
|
||||
<span class="px-0.5">${renderNumber(this._gold)}</span>
|
||||
</div>
|
||||
<!-- Troop bar: 2/4 -->
|
||||
<div class="w-3/5 lg:flex-1">${this.renderTroopBar()}</div>
|
||||
<!-- Attack ratio: 1/4 -->
|
||||
<div
|
||||
class="relative w-1/5 shrink-0 flex items-center justify-center gap-1 cursor-pointer lg:hidden"
|
||||
@touchstart=${(e: TouchEvent) => this.handleAttackTouchStart(e)}
|
||||
>
|
||||
<div class="flex flex-col items-center w-10 shrink-0">
|
||||
<div
|
||||
class="flex items-center gap-0.5 text-white text-xs font-bold tabular-nums"
|
||||
translate="no"
|
||||
>
|
||||
<img
|
||||
src=${swordIcon}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width="10"
|
||||
height="10"
|
||||
class="brightness-0 invert sepia saturate-[10000%] hue-rotate-[0deg]"
|
||||
style="filter: brightness(0) saturate(100%) invert(36%) sepia(95%) saturate(5500%) hue-rotate(350deg) brightness(95%) contrast(95%);"
|
||||
/>
|
||||
${(this.attackRatio * 100).toFixed(0)}%
|
||||
</div>
|
||||
<div class="text-[10px] text-red-400 tabular-nums" translate="no">
|
||||
(${renderTroops(
|
||||
(this.game?.myPlayer()?.troops() ?? 0) * this.attackRatio,
|
||||
)})
|
||||
</div>
|
||||
</div>
|
||||
<!-- Small red vertical bar indicator -->
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
class="w-1.5 h-8 bg-white/20 rounded-full relative overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="absolute bottom-0 w-full bg-red-500 rounded-full transition-all duration-200"
|
||||
style="height: ${this.attackRatio * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full flex">
|
||||
${greenPercent > 0
|
||||
? html`<div
|
||||
class="h-full bg-green-500 transition-[width] duration-200"
|
||||
style="width: ${greenPercent}%;"
|
||||
></div>`
|
||||
: ""}
|
||||
${orangePercent > 0
|
||||
? html`<div
|
||||
class="h-full bg-orange-400 transition-[width] duration-200"
|
||||
style="width: ${orangePercent}%;"
|
||||
></div>`
|
||||
: ""}
|
||||
</div>
|
||||
${this._touchDragging
|
||||
? html`
|
||||
<div
|
||||
class="absolute bottom-full right-0 flex flex-col items-center pointer-events-auto z-[10000] bg-gray-800/70 backdrop-blur-xs rounded-tl-lg sm:rounded-lg p-2 w-12"
|
||||
style="height: 50vh;"
|
||||
@touchstart=${(e: TouchEvent) => this.handleBarTouch(e)}
|
||||
>
|
||||
<span class="text-red-400 text-sm font-bold mb-1" translate="no"
|
||||
>${(this.attackRatio * 100).toFixed(0)}%</span
|
||||
>
|
||||
<div
|
||||
class="attack-drag-bar flex-1 w-3 bg-white/20 rounded-full relative overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="absolute bottom-0 w-full bg-red-500 rounded-full"
|
||||
style="height: ${this.attackRatio * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<!-- Attack ratio bar (desktop, always visible) -->
|
||||
<div class="hidden lg:block mt-2">
|
||||
<div
|
||||
class="flex items-center justify-between text-sm font-bold mb-1"
|
||||
translate="no"
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-start px-1.5 text-xs font-bold leading-none pointer-events-none gap-0.5"
|
||||
translate="no"
|
||||
>
|
||||
<span class="text-white drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]"
|
||||
>${renderTroops(this._troops)}</span
|
||||
>
|
||||
<span class="text-white flex items-center gap-1"
|
||||
><img
|
||||
src=${swordIcon}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width="14"
|
||||
height="14"
|
||||
style="filter: brightness(0) saturate(100%) invert(36%) sepia(95%) saturate(5500%) hue-rotate(350deg) brightness(95%) contrast(95%);"
|
||||
/>Attack Ratio</span
|
||||
>
|
||||
<span class="text-white tabular-nums"
|
||||
>${(this.attackRatio * 100).toFixed(0)}%
|
||||
(${renderTroops(
|
||||
(this.game?.myPlayer()?.troops() ?? 0) * this.attackRatio,
|
||||
)})</span
|
||||
>
|
||||
</div>
|
||||
<span class="text-white/60 drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]"
|
||||
>/</span
|
||||
>
|
||||
<span class="text-white drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]"
|
||||
>${renderTroops(this._maxTroops)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDesktop() {
|
||||
return html`
|
||||
<!-- Row 1: troop rate | troop bar | gold -->
|
||||
<div class="flex gap-1.5 items-center mb-1.5">
|
||||
<!-- Troop rate -->
|
||||
<div
|
||||
class="flex items-center gap-1 shrink-0 border rounded-md font-bold text-xs p-1 w-[5.5rem] ${this
|
||||
._troopRateIsIncreasing
|
||||
? "border-green-400"
|
||||
: "border-orange-400"}"
|
||||
translate="no"
|
||||
>
|
||||
<img
|
||||
src=${soldierIcon}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width="13"
|
||||
height="13"
|
||||
class="shrink-0"
|
||||
style="filter: ${this._troopRateIsIncreasing
|
||||
? "brightness(0) saturate(100%) invert(74%) sepia(44%) saturate(500%) hue-rotate(83deg) brightness(103%)"
|
||||
: "brightness(0) saturate(100%) invert(65%) sepia(60%) saturate(600%) hue-rotate(330deg) brightness(105%)"}"
|
||||
/>
|
||||
<span
|
||||
class="text-xs font-bold tabular-nums ${this._troopRateIsIncreasing
|
||||
? "text-green-400"
|
||||
: "text-orange-400"}"
|
||||
>+${renderTroops(this.troopRate)}/s</span
|
||||
>
|
||||
</div>
|
||||
<!-- Troop bar -->
|
||||
<div class="flex-1">${this.renderDesktopTroopBar()}</div>
|
||||
<!-- Gold -->
|
||||
<div
|
||||
class="flex items-center gap-1 shrink-0 border rounded-md border-yellow-400 font-bold text-yellow-400 text-xs p-1 w-[4.5rem]"
|
||||
translate="no"
|
||||
>
|
||||
<img src=${goldCoinIcon} width="13" height="13" class="shrink-0" />
|
||||
<span class="tabular-nums">${renderNumber(this._gold)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row 2: attack ratio | slider -->
|
||||
<div class="flex items-center gap-2" translate="no">
|
||||
<div
|
||||
class="flex items-center gap-1 shrink-0 border border-gray-600 rounded-md p-1 text-xs font-bold text-white cursor-pointer w-[7rem]"
|
||||
>
|
||||
<img
|
||||
src=${swordIcon}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width="12"
|
||||
height="12"
|
||||
style="filter: brightness(0) invert(1);"
|
||||
/>
|
||||
<span
|
||||
>${(this.attackRatio * 100).toFixed(0)}%
|
||||
(${renderTroops(
|
||||
(this.game?.myPlayer()?.troops() ?? 0) * this.attackRatio,
|
||||
)})</span
|
||||
>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${String(Math.round(this.attackRatio * 100))}
|
||||
@input=${(e: Event) => this.handleRatioSliderInput(e)}
|
||||
class="flex-1 h-2 accent-blue-500 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMobile() {
|
||||
return html`
|
||||
<div class="flex gap-2 items-center">
|
||||
<!-- Gold -->
|
||||
<div
|
||||
class="flex items-center justify-center p-1 gap-0.5 border rounded-md border-yellow-400 font-bold text-yellow-400 text-xs w-1/5 shrink-0"
|
||||
translate="no"
|
||||
>
|
||||
<img src=${goldCoinIcon} width="13" height="13" />
|
||||
<span class="px-0.5">${renderNumber(this._gold)}</span>
|
||||
</div>
|
||||
<!-- Troop bar -->
|
||||
<div class="w-[40%] shrink-0 flex items-center">
|
||||
${this.renderMobileTroopBar()}
|
||||
</div>
|
||||
<!-- Sword + % label -->
|
||||
<div class="flex flex-col items-center shrink-0 gap-0.5" translate="no">
|
||||
<img
|
||||
src=${swordIcon}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width="10"
|
||||
height="10"
|
||||
style="filter: brightness(0) invert(1);"
|
||||
/>
|
||||
<span class="text-white text-xs font-bold tabular-nums"
|
||||
>${(this.attackRatio * 100).toFixed(0)}%</span
|
||||
>
|
||||
</div>
|
||||
<!-- Attack ratio slider -->
|
||||
<div class="flex-1" translate="no">
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${String(Math.round(this.attackRatio * 100))}
|
||||
@input=${(e: Event) => this.handleRatioSliderInput(e)}
|
||||
class="w-full h-2 accent-red-500 cursor-pointer"
|
||||
class="w-full h-1.5 accent-blue-500 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
class="relative pointer-events-auto ${this._isVisible
|
||||
? "relative w-full text-sm px-2 py-1.5"
|
||||
: "hidden"}"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
<div class="lg:hidden">${this.renderMobile()}</div>
|
||||
<div class="hidden lg:block">${this.renderDesktop()}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this; // Disable shadow DOM to allow Tailwind styles
|
||||
}
|
||||
|
||||
@@ -794,9 +794,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
<!-- Events Toggle (when hidden) -->
|
||||
${this._hidden
|
||||
? html`
|
||||
<div
|
||||
class="relative w-fit min-[1200px]:bottom-4 min-[1200px]:right-4 z-50"
|
||||
>
|
||||
<div class="relative w-fit z-50">
|
||||
${this.renderButton({
|
||||
content: html`
|
||||
<span class="flex items-center gap-2">
|
||||
@@ -818,12 +816,10 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
: html`
|
||||
<!-- Main Events Display -->
|
||||
<div
|
||||
class="relative w-full min-[1200px]:bottom-4 min-[1200px]:right-4 z-50 min-[1200px]:w-96 backdrop-blur-sm"
|
||||
class="relative w-full z-50 min-[1200px]:w-96 backdrop-blur-sm"
|
||||
>
|
||||
<!-- Button Bar -->
|
||||
<div
|
||||
class="w-full p-2 lg:p-3 bg-gray-800/70 min-[1200px]:rounded-t-lg sm:rounded-tl-lg"
|
||||
>
|
||||
<div class="w-full p-2 lg:p-3 bg-gray-800/70 rounded-t-lg">
|
||||
<div class="flex justify-between items-center gap-3">
|
||||
<div class="flex gap-4">
|
||||
${this.renderToggleButton(
|
||||
|
||||
@@ -126,86 +126,80 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="hidden min-[1200px]:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] 2xl:flex-row xl:flex-col min-[1200px]:flex-col 2xl:gap-5 xl:gap-2 min-[1200px]:gap-2 justify-center items-center"
|
||||
>
|
||||
<div class="bg-gray-800/70 backdrop-blur-xs rounded-lg p-0.5">
|
||||
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1 w-fit">
|
||||
${this.renderUnitItem(
|
||||
cityIcon,
|
||||
this._cities,
|
||||
UnitType.City,
|
||||
"city",
|
||||
this.keybinds["buildCity"]?.key ?? "1",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
factoryIcon,
|
||||
this._factories,
|
||||
UnitType.Factory,
|
||||
"factory",
|
||||
this.keybinds["buildFactory"]?.key ?? "2",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
portIcon,
|
||||
this._port,
|
||||
UnitType.Port,
|
||||
"port",
|
||||
this.keybinds["buildPort"]?.key ?? "3",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
defensePostIcon,
|
||||
this._defensePost,
|
||||
UnitType.DefensePost,
|
||||
"defense_post",
|
||||
this.keybinds["buildDefensePost"]?.key ?? "4",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
missileSiloIcon,
|
||||
this._missileSilo,
|
||||
UnitType.MissileSilo,
|
||||
"missile_silo",
|
||||
this.keybinds["buildMissileSilo"]?.key ?? "5",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
samLauncherIcon,
|
||||
this._samLauncher,
|
||||
UnitType.SAMLauncher,
|
||||
"sam_launcher",
|
||||
this.keybinds["buildSamLauncher"]?.key ?? "6",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-800/70 backdrop-blur-xs rounded-lg p-0.5 w-fit">
|
||||
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1">
|
||||
${this.renderUnitItem(
|
||||
warshipIcon,
|
||||
this._warships,
|
||||
UnitType.Warship,
|
||||
"warship",
|
||||
this.keybinds["buildWarship"]?.key ?? "7",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
atomBombIcon,
|
||||
null,
|
||||
UnitType.AtomBomb,
|
||||
"atom_bomb",
|
||||
this.keybinds["buildAtomBomb"]?.key ?? "8",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
hydrogenBombIcon,
|
||||
null,
|
||||
UnitType.HydrogenBomb,
|
||||
"hydrogen_bomb",
|
||||
this.keybinds["buildHydrogenBomb"]?.key ?? "9",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
mirvIcon,
|
||||
null,
|
||||
UnitType.MIRV,
|
||||
"mirv",
|
||||
this.keybinds["buildMIRV"]?.key ?? "0",
|
||||
)}
|
||||
</div>
|
||||
<div class="border-t border-white/10 p-0.5 w-full">
|
||||
<div
|
||||
class="grid grid-rows-1 auto-cols-max grid-flow-col gap-0.5 w-fit mx-auto"
|
||||
>
|
||||
${this.renderUnitItem(
|
||||
cityIcon,
|
||||
this._cities,
|
||||
UnitType.City,
|
||||
"city",
|
||||
this.keybinds["buildCity"]?.key ?? "1",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
factoryIcon,
|
||||
this._factories,
|
||||
UnitType.Factory,
|
||||
"factory",
|
||||
this.keybinds["buildFactory"]?.key ?? "2",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
portIcon,
|
||||
this._port,
|
||||
UnitType.Port,
|
||||
"port",
|
||||
this.keybinds["buildPort"]?.key ?? "3",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
defensePostIcon,
|
||||
this._defensePost,
|
||||
UnitType.DefensePost,
|
||||
"defense_post",
|
||||
this.keybinds["buildDefensePost"]?.key ?? "4",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
missileSiloIcon,
|
||||
this._missileSilo,
|
||||
UnitType.MissileSilo,
|
||||
"missile_silo",
|
||||
this.keybinds["buildMissileSilo"]?.key ?? "5",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
samLauncherIcon,
|
||||
this._samLauncher,
|
||||
UnitType.SAMLauncher,
|
||||
"sam_launcher",
|
||||
this.keybinds["buildSamLauncher"]?.key ?? "6",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
warshipIcon,
|
||||
this._warships,
|
||||
UnitType.Warship,
|
||||
"warship",
|
||||
this.keybinds["buildWarship"]?.key ?? "7",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
atomBombIcon,
|
||||
null,
|
||||
UnitType.AtomBomb,
|
||||
"atom_bomb",
|
||||
this.keybinds["buildAtomBomb"]?.key ?? "8",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
hydrogenBombIcon,
|
||||
null,
|
||||
UnitType.HydrogenBomb,
|
||||
"hydrogen_bomb",
|
||||
this.keybinds["buildHydrogenBomb"]?.key ?? "9",
|
||||
)}
|
||||
${this.renderUnitItem(
|
||||
mirvIcon,
|
||||
null,
|
||||
UnitType.MIRV,
|
||||
"mirv",
|
||||
this.keybinds["buildMIRV"]?.key ?? "0",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -243,7 +237,7 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
${hovered
|
||||
? html`
|
||||
<div
|
||||
class="absolute -top-[250%] left-1/2 -translate-x-1/2 text-gray-200 text-center w-max text-xs bg-gray-800/90 backdrop-blur-xs rounded-sm p-1 z-20 shadow-lg pointer-events-none"
|
||||
class="absolute -top-[250%] left-1/2 -translate-x-1/2 text-gray-200 text-center w-max text-xs bg-gray-800/90 backdrop-blur-xs rounded-sm p-1 z-[100] shadow-lg pointer-events-none"
|
||||
>
|
||||
<div class="font-bold text-sm mb-1">
|
||||
${translateText(
|
||||
@@ -265,7 +259,7 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
<div
|
||||
class="${this.canBuild(unitType)
|
||||
? ""
|
||||
: "opacity-40"} border border-slate-500 rounded-sm pr-2 pb-1 flex items-center gap-2 cursor-pointer
|
||||
: "opacity-40"} border border-slate-500 rounded-sm px-0.5 pb-0.5 flex items-center gap-0.5 cursor-pointer
|
||||
${selected ? "hover:bg-gray-400/10" : "hover:bg-gray-800"}
|
||||
rounded-sm text-white ${selected ? "bg-slate-400/20" : ""}"
|
||||
@click=${() => {
|
||||
@@ -299,12 +293,14 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
@mouseleave=${() =>
|
||||
this.eventBus?.emit(new ToggleStructureEvent(null))}
|
||||
>
|
||||
${html`<div class="ml-1 text-xs relative -top-1.5 text-gray-400">
|
||||
${html`<div class="ml-0.5 text-[10px] relative -top-1 text-gray-400">
|
||||
${displayHotkey}
|
||||
</div>`}
|
||||
<div class="flex items-center gap-1 pt-1">
|
||||
<img src=${icon} alt=${structureKey} class="align-middle size-6" />
|
||||
${number !== null ? renderNumber(number) : null}
|
||||
<div class="flex items-center gap-0.5 pt-0.5">
|
||||
<img src=${icon} alt=${structureKey} class="align-middle size-5" />
|
||||
${number !== null
|
||||
? html`<span class="text-xs">${renderNumber(number)}</span>`
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user