## Description:

Make the unit display bar a proper unit build bar
Add shortcuts for all structures and units
Add ranges for ranged structures and units
Changed the shortcuts to use the key instead of the code for
internationalization purposes


![buildbar](https://github.com/user-attachments/assets/6407dc9c-14b4-40cc-8faa-cdd9e88c9fd2)
<img width="285" height="517" alt="image"
src="https://github.com/user-attachments/assets/91bb01e6-e48c-4255-ace1-306af9cdc25b"
/>

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

Mr.Box

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
Co-authored-by: icslucas <carolinacarazolli@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Vivacious Box
2025-10-02 21:38:28 +02:00
committed by GitHub
parent 6061c97d78
commit 311d43ab4f
17 changed files with 1283 additions and 515 deletions
+229 -68
View File
@@ -1,30 +1,38 @@
import { html, LitElement } from "lit";
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import portIcon from "../../../../resources/images/AnchorIcon.png";
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
import hydrogenBombIcon from "../../../../resources/images/MushroomCloudIconWhite.svg";
import atomBombIcon from "../../../../resources/images/NukeIconWhite.svg";
import portIcon from "../../../../resources/images/PortIcon.svg";
import samLauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg";
import defensePostIcon from "../../../../resources/images/ShieldIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
import { UnitType } from "../../../core/game/Game";
import { Gold, PlayerActions, UnitType } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { ToggleStructureEvent } from "../../InputHandler";
import { renderNumber } from "../../Utils";
import { renderNumber, translateText } from "../../Utils";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
@customElement("unit-display")
export class UnitDisplay extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
private _selectedStructure: UnitType | null = null;
public uiState: UIState;
private playerActions: PlayerActions | null = null;
private keybinds: Record<string, { value: string; key: string }> = {};
private _cities = 0;
private _warships = 0;
private _factories = 0;
private _missileSilo = 0;
private _port = 0;
private _defensePost = 0;
private _samLauncher = 0;
private allDisabled = false;
private _hoveredUnit: UnitType | null = null;
createRenderRoot() {
return this;
@@ -32,18 +40,63 @@ export class UnitDisplay extends LitElement implements Layer {
init() {
const config = this.game.config();
const savedKeybinds = localStorage.getItem("settings.keybinds");
if (savedKeybinds) {
try {
this.keybinds = JSON.parse(savedKeybinds);
} catch (e) {
console.warn("Invalid keybinds JSON:", e);
}
}
this.allDisabled =
config.isUnitDisabled(UnitType.City) &&
config.isUnitDisabled(UnitType.Factory) &&
config.isUnitDisabled(UnitType.Port) &&
config.isUnitDisabled(UnitType.DefensePost) &&
config.isUnitDisabled(UnitType.MissileSilo) &&
config.isUnitDisabled(UnitType.SAMLauncher);
config.isUnitDisabled(UnitType.SAMLauncher) &&
config.isUnitDisabled(UnitType.Warship) &&
config.isUnitDisabled(UnitType.AtomBomb) &&
config.isUnitDisabled(UnitType.HydrogenBomb);
this.requestUpdate();
}
private cost(item: UnitType): Gold {
for (const bu of this.playerActions?.buildableUnits ?? []) {
if (bu.type === item) {
return bu.cost;
}
}
return 0n;
}
private canBuild(item: UnitType): boolean {
if (this.game?.config().isUnitDisabled(item)) return false;
const player = this.game?.myPlayer();
switch (item) {
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
return (
this.cost(item) <= (player?.gold() ?? 0n) &&
(player?.units(UnitType.MissileSilo).length ?? 0) > 0
);
case UnitType.Warship:
return (
this.cost(item) <= (player?.gold() ?? 0n) &&
(player?.units(UnitType.Port).length ?? 0) > 0
);
default:
return this.cost(item) <= (player?.gold() ?? 0n);
}
}
tick() {
const player = this.game?.myPlayer();
player?.actions().then((actions) => {
this.playerActions = actions;
});
if (!player) return;
this._cities = player.totalUnitLevels(UnitType.City);
this._missileSilo = player.totalUnitLevels(UnitType.MissileSilo);
@@ -51,42 +104,10 @@ export class UnitDisplay extends LitElement implements Layer {
this._defensePost = player.totalUnitLevels(UnitType.DefensePost);
this._samLauncher = player.totalUnitLevels(UnitType.SAMLauncher);
this._factories = player.totalUnitLevels(UnitType.Factory);
this._warships = player.totalUnitLevels(UnitType.Warship);
this.requestUpdate();
}
private renderUnitItem(
icon: string,
number: number,
unitType: UnitType,
altText: string,
) {
if (this.game.config().isUnitDisabled(unitType)) {
return html``;
}
return html`
<div
class="px-2 flex items-center gap-2 cursor-pointer hover:bg-slate-700/50 rounded text-white"
style="background: ${this._selectedStructure === unitType
? "#ffffff2e"
: "none"}"
@mouseenter="${() =>
this.eventBus.emit(new ToggleStructureEvent(unitType))}"
@mouseleave="${() =>
this.eventBus.emit(new ToggleStructureEvent(null))}"
>
<img
src=${icon}
alt=${altText}
width="20"
height="20"
style="vertical-align: middle;"
/>
${renderNumber(number)}
</div>
`;
}
render() {
const myPlayer = this.game?.myPlayer();
if (
@@ -97,42 +118,182 @@ export class UnitDisplay extends LitElement implements Layer {
) {
return null;
}
if (this.allDisabled) {
return null;
}
return html`
<div
class="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] bg-gray-800/70 backdrop-blur-sm border border-slate-400 rounded-lg p-2 hidden lg:block"
class="hidden xl:flex md:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] xl:flex-row lg:flex-col md:flex-col xl:gap-5 lg:gap-2 md:gap-2 justify-center items-center"
>
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1">
${this.renderUnitItem(cityIcon, this._cities, UnitType.City, "city")}
${this.renderUnitItem(
factoryIcon,
this._factories,
UnitType.Factory,
"factory",
)}
${this.renderUnitItem(portIcon, this._port, UnitType.Port, "port")}
${this.renderUnitItem(
defensePostIcon,
this._defensePost,
UnitType.DefensePost,
"defense post",
)}
${this.renderUnitItem(
missileSiloIcon,
this._missileSilo,
UnitType.MissileSilo,
"missile silo",
)}
${this.renderUnitItem(
samLauncherIcon,
this._samLauncher,
UnitType.SAMLauncher,
"SAM launcher",
)}
<div class="bg-gray-800/70 backdrop-blur-sm 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-sm rounded-lg p-0.5 w-fit">
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1">
${this.renderUnitItem(
atomBombIcon,
null,
UnitType.AtomBomb,
"atom_bomb",
this.keybinds["buildAtomBomb"]?.key ?? "7",
)}
${this.renderUnitItem(
hydrogenBombIcon,
null,
UnitType.HydrogenBomb,
"hydrogen_bomb",
this.keybinds["buildHydrogenBomb"]?.key ?? "8",
)}
${this.renderUnitItem(
warshipIcon,
this._warships,
UnitType.Warship,
"warship",
this.keybinds["buildWarship"]?.key ?? "9",
)}
</div>
</div>
</div>
`;
}
private renderUnitItem(
icon: string,
number: number | null,
unitType: UnitType,
structureKey: string,
hotkey: string,
) {
if (this.game.config().isUnitDisabled(unitType)) {
return html``;
}
const selected = this.uiState.ghostStructure === unitType;
const hovered = this._hoveredUnit === unitType;
return html`
<div
class="flex flex-col items-center relative"
@mouseenter=${() => {
this._hoveredUnit = unitType;
this.requestUpdate();
}}
@mouseleave=${() => {
this._hoveredUnit = null;
this.requestUpdate();
}}
>
${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-sm rounded p-1 z-20 shadow-lg pointer-events-none"
>
<div class="font-bold text-sm mb-1">
${translateText(
"unit_type." + structureKey,
)}${` [${hotkey.toUpperCase()}]`}
</div>
<div class="p-2">
${translateText("build_menu.desc." + structureKey)}
</div>
<div>
<span class="text-yellow-300"
>${renderNumber(this.cost(unitType))}</span
>
${translateText("player_info_overlay.gold")}
</div>
</div>
`
: null}
<div
class="${this.canBuild(unitType)
? ""
: "opacity-40"} border border-slate-500 rounded pr-2 pb-1 flex items-center gap-2 cursor-pointer
${selected ? "hover:bg-gray-400/10" : "hover:bg-gray-800"}
rounded text-white ${selected ? "bg-slate-400/20" : ""}"
@click=${() => {
if (selected) {
this.uiState.ghostStructure = null;
} else if (this.canBuild(unitType)) {
this.uiState.ghostStructure = unitType;
}
this.requestUpdate();
}}
@mouseenter=${() => {
switch (unitType) {
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
this.eventBus?.emit(
new ToggleStructureEvent([
UnitType.MissileSilo,
UnitType.SAMLauncher,
]),
);
break;
case UnitType.Warship:
this.eventBus?.emit(new ToggleStructureEvent([UnitType.Port]));
break;
default:
this.eventBus?.emit(new ToggleStructureEvent([unitType]));
}
}}
@mouseleave=${() =>
this.eventBus?.emit(new ToggleStructureEvent(null))}
>
${html`<div class="ml-1 text-xs relative -top-1.5 text-gray-400">
${hotkey.toUpperCase()}
</div>`}
<div class="flex items-center gap-1 pt-1">
<img
src=${icon}
alt=${structureKey}
style="vertical-align: middle; width: 24px; height: 24px;"
/>
${number !== null ? renderNumber(number) : null}
</div>
</div>
</div>
`;