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:
Evan
2026-03-06 18:32:01 -08:00
committed by GitHub
parent 0eb23c0c8c
commit 815f1de67b
5 changed files with 274 additions and 279 deletions
+21 -8
View File
@@ -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"
>
+6 -6
View File
@@ -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()}
+162 -172
View File
@@ -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
}
+3 -7
View File
@@ -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(
+82 -86
View File
@@ -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>