Files
OpenFrontIO/src/client/graphics/layers/BuildMenu.ts
T
Restart2008 db4a815390 oil oil baby
2026-02-20 15:55:44 -08:00

515 lines
13 KiB
TypeScript

import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import {
BuildableUnit,
Gold,
PlayerActions,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView } from "../../../core/game/GameView";
import {
CloseViewEvent,
MouseDownEvent,
ShowBuildMenuEvent,
ShowEmojiMenuEvent,
} from "../../InputHandler";
import {
BuildUnitIntentEvent,
SendUpgradeStructureIntentEvent,
} from "../../Transport";
import { renderNumber } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import mirvIcon from "/images/MIRVIcon.svg?url";
import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
import hydrogenBombIcon from "/images/MushroomCloudIconWhite.svg?url";
import atomBombIcon from "/images/NukeIconWhite.svg?url";
import oilRigIcon from "/images/oil-rig_2623991.svg?url";
import portIcon from "/images/PortIcon.svg?url";
import samlauncherIcon from "/images/SamLauncherIconWhite.svg?url";
import shieldIcon from "/images/ShieldIconWhite.svg?url";
export interface BuildItemDisplay {
unitType: UnitType;
icon: string;
description?: string;
key?: string;
countable?: boolean;
}
export const buildTable: BuildItemDisplay[][] = [
[
{
unitType: UnitType.AtomBomb,
icon: atomBombIcon,
description: "build_menu.desc.atom_bomb",
key: "unit_type.atom_bomb",
countable: false,
},
{
unitType: UnitType.MIRV,
icon: mirvIcon,
description: "build_menu.desc.mirv",
key: "unit_type.mirv",
countable: false,
},
{
unitType: UnitType.HydrogenBomb,
icon: hydrogenBombIcon,
description: "build_menu.desc.hydrogen_bomb",
key: "unit_type.hydrogen_bomb",
countable: false,
},
{
unitType: UnitType.Warship,
icon: warshipIcon,
description: "build_menu.desc.warship",
key: "unit_type.warship",
countable: true,
},
{
unitType: UnitType.Port,
icon: portIcon,
description: "build_menu.desc.port",
key: "unit_type.port",
countable: true,
},
{
unitType: UnitType.MissileSilo,
icon: missileSiloIcon,
description: "build_menu.desc.missile_silo",
key: "unit_type.missile_silo",
countable: true,
},
{
unitType: UnitType.SAMLauncher,
icon: samlauncherIcon,
description: "build_menu.desc.sam_launcher",
key: "unit_type.sam_launcher",
countable: true,
},
{
unitType: UnitType.DefensePost,
icon: shieldIcon,
description: "build_menu.desc.defense_post",
key: "unit_type.defense_post",
countable: true,
},
{
unitType: UnitType.City,
icon: cityIcon,
description: "build_menu.desc.city",
key: "unit_type.city",
countable: true,
},
{
unitType: UnitType.OilRig,
icon: oilRigIcon,
description: "build_menu.desc.oil_rig",
key: "unit_type.oil_rig",
countable: true,
},
],
];
export const flattenedBuildTable = buildTable.flat();
@customElement("build-menu")
export class BuildMenu extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
public uiState: UIState;
private clickedTile: TileRef;
public playerActions: PlayerActions | null = null;
private filteredBuildTable: BuildItemDisplay[][] = buildTable;
public transformHandler: TransformHandler;
init() {
this.eventBus.on(ShowBuildMenuEvent, (e) => {
if (!this.game.myPlayer()?.isAlive()) {
return;
}
if (!this._hidden) {
// Players sometimes hold control while building a unit,
// so if the menu is already open, ignore the event.
return;
}
const clickedCell = this.transformHandler.screenToWorldCoordinates(
e.x,
e.y,
);
if (clickedCell === null) {
return;
}
if (!this.game.isValidCoord(clickedCell.x, clickedCell.y)) {
return;
}
const tile = this.game.ref(clickedCell.x, clickedCell.y);
this.showMenu(tile);
});
this.eventBus.on(CloseViewEvent, () => this.hideMenu());
this.eventBus.on(ShowEmojiMenuEvent, () => this.hideMenu());
this.eventBus.on(MouseDownEvent, () => this.hideMenu());
}
tick() {
if (!this._hidden) {
this.refresh();
}
}
static styles = css`
:host {
display: block;
}
.build-menu {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
background-color: #1e1e1e;
padding: 15px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
max-width: 95vw;
max-height: 95vh;
overflow-y: auto;
}
.build-description {
font-size: 0.6rem;
}
.build-row {
display: flex;
justify-content: center;
flex-wrap: wrap;
width: 100%;
}
.build-button {
position: relative;
width: 120px;
height: 140px;
border: 2px solid #444;
background-color: #2c2c2c;
color: white;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 8px;
padding: 10px;
gap: 5px;
}
.build-button:not(:disabled):hover {
background-color: #3a3a3a;
transform: scale(1.05);
border-color: #666;
}
.build-button:not(:disabled):active {
background-color: #4a4a4a;
transform: scale(0.95);
}
.build-button:disabled {
background-color: #1a1a1a;
border-color: #333;
cursor: not-allowed;
opacity: 0.7;
}
.build-button:disabled img {
opacity: 0.5;
}
.build-button:disabled .build-cost {
color: #ff4444;
}
.build-icon {
font-size: 40px;
margin-bottom: 5px;
}
.build-name {
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
text-align: center;
}
.build-cost {
font-size: 14px;
}
.hidden {
display: none !important;
}
.build-count-chip {
position: absolute;
top: -10px;
right: -10px;
background-color: #2c2c2c;
color: white;
padding: 2px 10px;
border-radius: 10000px;
transition: all 0.3s ease;
font-size: 12px;
display: flex;
justify-content: center;
align-content: center;
border: 1px solid #444;
}
.build-button:not(:disabled):hover > .build-count-chip {
background-color: #3a3a3a;
border-color: #666;
}
.build-button:not(:disabled):active > .build-count-chip {
background-color: #4a4a4a;
}
.build-button:disabled > .build-count-chip {
background-color: #1a1a1a;
border-color: #333;
cursor: not-allowed;
}
.build-count {
font-weight: bold;
font-size: 14px;
}
@media (max-width: 768px) {
.build-menu {
padding: 10px;
max-height: 80vh;
width: 80vw;
}
.build-button {
width: 140px;
height: 120px;
margin: 4px;
padding: 6px;
gap: 5px;
}
.build-icon {
font-size: 28px;
}
.build-name {
font-size: 12px;
margin-bottom: 3px;
}
.build-cost {
font-size: 11px;
}
.build-count {
font-weight: bold;
font-size: 10px;
}
.build-count-chip {
padding: 1px 5px;
}
}
@media (max-width: 480px) {
.build-menu {
padding: 8px;
max-height: 70vh;
}
.build-button {
width: calc(50% - 6px);
height: 100px;
margin: 3px;
padding: 4px;
border-width: 1px;
}
.build-icon {
font-size: 24px;
}
.build-name {
font-size: 10px;
margin-bottom: 2px;
}
.build-cost {
font-size: 9px;
}
.build-count {
font-weight: bold;
font-size: 8px;
}
.build-count-chip {
padding: 0 3px;
}
.build-button img {
width: 24px;
height: 24px;
}
.build-cost img {
width: 10px;
height: 10px;
}
}
`;
@state()
private _hidden = true;
public canBuildOrUpgrade(item: BuildItemDisplay): boolean {
if (this.game?.myPlayer() === null || this.playerActions === null) {
return false;
}
const unit = this.playerActions.buildableUnits.filter(
(u) => u.type === item.unitType,
);
if (unit.length === 0) {
return false;
}
return unit[0].canBuild !== false || unit[0].canUpgrade !== false;
}
public cost(item: BuildItemDisplay): Gold {
for (const bu of this.playerActions?.buildableUnits ?? []) {
if (bu.type === item.unitType) {
return bu.cost;
}
}
return 0n;
}
public count(item: BuildItemDisplay): string {
const player = this.game?.myPlayer();
if (!player) {
return "?";
}
return player.totalUnitLevels(item.unitType).toString();
}
public sendBuildOrUpgrade(buildableUnit: BuildableUnit, tile: TileRef): void {
if (buildableUnit.canUpgrade !== false) {
this.eventBus.emit(
new SendUpgradeStructureIntentEvent(
buildableUnit.canUpgrade,
buildableUnit.type,
),
);
} else if (buildableUnit.canBuild) {
const rocketDirectionUp =
buildableUnit.type === UnitType.AtomBomb ||
buildableUnit.type === UnitType.HydrogenBomb
? this.uiState.rocketDirectionUp
: undefined;
this.eventBus.emit(
new BuildUnitIntentEvent(buildableUnit.type, tile, rocketDirectionUp),
);
}
this.hideMenu();
}
render() {
return html`
<div
class="build-menu ${this._hidden ? "hidden" : ""}"
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
${this.filteredBuildTable.map(
(row) => html`
<div class="build-row">
${row.map((item) => {
const buildableUnit = this.playerActions?.buildableUnits.find(
(bu) => bu.type === item.unitType,
);
if (buildableUnit === undefined) {
return html``;
}
const enabled =
buildableUnit.canBuild !== false ||
buildableUnit.canUpgrade !== false;
return html`
<button
class="build-button"
@click=${() =>
this.sendBuildOrUpgrade(buildableUnit, this.clickedTile)}
?disabled=${!enabled}
title=${!enabled
? translateText("build_menu.not_enough_money")
: ""}
>
<img
src=${item.icon}
alt="${item.unitType}"
width="40"
height="40"
/>
<span class="build-name"
>${item.key && translateText(item.key)}</span
>
<span class="build-description"
>${item.description &&
translateText(item.description)}</span
>
<span class="build-cost" translate="no">
${renderNumber(
this.game && this.game.myPlayer() ? this.cost(item) : 0,
)}
<img
src=${goldCoinIcon}
alt="gold"
width="12"
height="12"
class="align-middle"
/>
</span>
${item.countable
? html`<div class="build-count-chip">
<span class="build-count">${this.count(item)}</span>
</div>`
: ""}
</button>
`;
})}
</div>
`,
)}
</div>
`;
}
hideMenu() {
this._hidden = true;
this.requestUpdate();
}
showMenu(clickedTile: TileRef) {
this.clickedTile = clickedTile;
this._hidden = false;
this.refresh();
}
private refresh() {
this.game
.myPlayer()
?.actions(this.clickedTile)
.then((actions) => {
this.playerActions = actions;
this.requestUpdate();
});
// remove disabled buildings from the buildtable
this.filteredBuildTable = this.getBuildableUnits();
}
private getBuildableUnits(): BuildItemDisplay[][] {
return buildTable.map((row) =>
row.filter((item) => !this.game?.config()?.isUnitDisabled(item.unitType)),
);
}
get isVisible() {
return !this._hidden;
}
}