Files
OpenFrontIO/src/client/graphics/layers/PlayerInfoOverlay.ts
T
evanpelle 2cbe34ed79 update ui (#1368)
## Description:

Center pop & gold information, and split up & remove the top bar.

Before:

<img width="1913" alt="Screenshot 2025-07-07 at 7 54 55 PM"
src="https://github.com/user-attachments/assets/8ab59102-1f0e-4023-905f-e0b1b2fa73a2"
/>


After:

<img width="1916" alt="Screenshot 2025-07-07 at 7 53 02 PM"
src="https://github.com/user-attachments/assets/06c885e3-a179-4447-9d1d-0da3849e726d"
/>


reference:

![image](https://github.com/user-attachments/assets/8c985a1a-e232-45ce-9af7-c1981df7fb15)

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

evan
2025-07-07 20:06:14 -07:00

372 lines
11 KiB
TypeScript

import { LitElement, TemplateResult, html } from "lit";
import { ref } from "lit-html/directives/ref.js";
import { customElement, property, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import {
PlayerProfile,
PlayerType,
Relation,
Unit,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { MouseMoveEvent } from "../../InputHandler";
import { renderNumber, renderTroops } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
function euclideanDistWorld(
coord: { x: number; y: number },
tileRef: TileRef,
game: GameView,
): number {
const x = game.x(tileRef);
const y = game.y(tileRef);
const dx = coord.x - x;
const dy = coord.y - y;
return Math.sqrt(dx * dx + dy * dy);
}
function distSortUnitWorld(coord: { x: number; y: number }, game: GameView) {
return (a: Unit | UnitView, b: Unit | UnitView) => {
const distA = euclideanDistWorld(coord, a.tile(), game);
const distB = euclideanDistWorld(coord, b.tile(), game);
return distA - distB;
};
}
@customElement("player-info-overlay")
export class PlayerInfoOverlay extends LitElement implements Layer {
@property({ type: Object })
public game!: GameView;
@property({ type: Object })
public eventBus!: EventBus;
@property({ type: Object })
public transform!: TransformHandler;
@state()
private player: PlayerView | null = null;
@state()
private playerProfile: PlayerProfile | null = null;
@state()
private unit: UnitView | null = null;
@state()
private _isInfoVisible: boolean = false;
private _isActive = false;
private lastMouseUpdate = 0;
init() {
this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) =>
this.onMouseEvent(e),
);
this._isActive = true;
}
private onMouseEvent(event: MouseMoveEvent) {
const now = Date.now();
if (now - this.lastMouseUpdate < 100) {
return;
}
this.lastMouseUpdate = now;
this.maybeShow(event.x, event.y);
}
public hide() {
this.setVisible(false);
this.unit = null;
this.player = null;
}
public maybeShow(x: number, y: number) {
this.hide();
const worldCoord = this.transform.screenToWorldCoordinates(x, y);
if (!this.game.isValidCoord(worldCoord.x, worldCoord.y)) {
return;
}
const tile = this.game.ref(worldCoord.x, worldCoord.y);
if (!tile) return;
const owner = this.game.owner(tile);
if (owner && owner.isPlayer()) {
this.player = owner as PlayerView;
this.player.profile().then((p) => {
this.playerProfile = p;
});
this.setVisible(true);
} else if (!this.game.isLand(tile)) {
const units = this.game
.units(UnitType.Warship, UnitType.TradeShip, UnitType.TransportShip)
.filter((u) => euclideanDistWorld(worldCoord, u.tile(), this.game) < 50)
.sort(distSortUnitWorld(worldCoord, this.game));
if (units.length > 0) {
this.unit = units[0];
this.setVisible(true);
}
}
}
tick() {
this.requestUpdate();
}
renderLayer(context: CanvasRenderingContext2D) {
// Implementation for Layer interface
}
shouldTransform(): boolean {
return false;
}
setVisible(visible: boolean) {
this._isInfoVisible = visible;
this.requestUpdate();
}
private getRelationClass(relation: Relation): string {
switch (relation) {
case Relation.Hostile:
return "text-red-500";
case Relation.Distrustful:
return "text-red-300";
case Relation.Neutral:
return "text-white";
case Relation.Friendly:
return "text-green-500";
default:
return "text-white";
}
}
private getRelationName(relation: Relation): string {
switch (relation) {
case Relation.Hostile:
return translateText("relation.hostile");
case Relation.Distrustful:
return translateText("relation.distrustful");
case Relation.Neutral:
return translateText("relation.neutral");
case Relation.Friendly:
return translateText("relation.friendly");
default:
return translateText("relation.default");
}
}
private renderPlayerInfo(player: PlayerView) {
const myPlayer = this.game.myPlayer();
const isFriendly = myPlayer?.isFriendly(player);
let relationHtml: TemplateResult | null = null;
const attackingTroops = player
.outgoingAttacks()
.map((a) => a.troops)
.reduce((a, b) => a + b, 0);
if (player.type() === PlayerType.FakeHuman && myPlayer !== null) {
const relation =
this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;
const relationClass = this.getRelationClass(relation);
const relationName = this.getRelationName(relation);
relationHtml = html`
<div class="text-sm opacity-80">
${translateText("player_info_overlay.attitude")}:
<span class="${relationClass}">${relationName}</span>
</div>
`;
}
let playerType = "";
switch (player.type()) {
case PlayerType.Bot:
playerType = translateText("player_info_overlay.bot");
break;
case PlayerType.FakeHuman:
playerType = translateText("player_info_overlay.nation");
break;
case PlayerType.Human:
playerType = translateText("player_info_overlay.player");
break;
}
return html`
<div class="p-2">
<div
class="text-bold text-sm lg:text-lg font-bold mb-1 inline-flex break-all ${isFriendly
? "text-green-500"
: "text-white"}"
>
${player.cosmetics.flag
? player.cosmetics.flag!.startsWith("!")
? html`<div
class="h-8 mr-1 aspect-[3/4] player-flag"
${ref((el) => {
if (el instanceof HTMLElement) {
requestAnimationFrame(() => {
renderPlayerFlag(player.cosmetics.flag!, el);
});
}
})}
></div>`
: html`<img
class="h-8 mr-1 aspect-[3/4]"
src=${"/flags/" + player.cosmetics.flag! + ".svg"}
/>`
: html``}
${player.name()}
</div>
${player.team() !== null
? html`<div class="text-sm opacity-80">
${translateText("player_info_overlay.team")}: ${player.team()}
</div>`
: ""}
<div class="text-sm opacity-80">
${translateText("player_info_overlay.type")}: ${playerType}
</div>
${player.troops() >= 1
? html`<div class="text-sm opacity-80" translate="no">
${translateText("player_info_overlay.d_troops")}:
${renderTroops(player.troops())}
</div>`
: ""}
${attackingTroops >= 1
? html`<div class="text-sm opacity-80" translate="no">
${translateText("player_info_overlay.a_troops")}:
${renderTroops(attackingTroops)}
</div>`
: ""}
<div class="text-sm opacity-80" translate="no">
${translateText("player_info_overlay.gold")}:
${renderNumber(player.gold())}
</div>
<div class="text-sm opacity-80" translate="no">
${translateText("player_info_overlay.ports")}:
${player.units(UnitType.Port).length}
${player
.units(UnitType.Port)
.map((unit) => unit.level())
.reduce((a, b) => a + b, 0) > 1
? html`(${translateText("player_info_overlay.levels")}:
${player
.units(UnitType.Port)
.map((unit) => unit.level())
.reduce((a, b) => a + b, 0)})`
: ""}
</div>
<div class="text-sm opacity-80" translate="no">
${translateText("player_info_overlay.cities")}:
${player.units(UnitType.City).length}
${player
.units(UnitType.City)
.map((unit) => unit.level())
.reduce((a, b) => a + b, 0) > 1
? html`(${translateText("player_info_overlay.levels")}:
${player
.units(UnitType.City)
.map((unit) => unit.level())
.reduce((a, b) => a + b, 0)})`
: ""}
</div>
<div class="text-sm opacity-80" translate="no">
${translateText("player_info_overlay.missile_launchers")}:
${player.units(UnitType.MissileSilo).length}
${player
.units(UnitType.MissileSilo)
.map((unit) => unit.level())
.reduce((a, b) => a + b, 0) > 1
? html`(${translateText("player_info_overlay.levels")}:
${player
.units(UnitType.MissileSilo)
.map((unit) => unit.level())
.reduce((a, b) => a + b, 0)})`
: ""}
</div>
<div class="text-sm opacity-80" translate="no">
${translateText("player_info_overlay.sams")}:
${player.units(UnitType.SAMLauncher).length}
${player
.units(UnitType.SAMLauncher)
.map((unit) => unit.level())
.reduce((a, b) => a + b, 0) > 1
? html`(${translateText("player_info_overlay.levels")}:
${player
.units(UnitType.SAMLauncher)
.map((unit) => unit.level())
.reduce((a, b) => a + b, 0)})`
: ""}
</div>
<div class="text-sm opacity-80" translate="no">
${translateText("player_info_overlay.warships")}:
${player.units(UnitType.Warship).length}
</div>
${relationHtml}
</div>
`;
}
private renderUnitInfo(unit: UnitView) {
const isAlly =
(unit.owner() === this.game.myPlayer() ||
this.game.myPlayer()?.isFriendly(unit.owner())) ??
false;
return html`
<div class="p-2">
<div class="font-bold mb-1 ${isAlly ? "text-green-500" : "text-white"}">
${unit.owner().name()}
</div>
<div class="mt-1">
<div class="text-sm opacity-80">${unit.type()}</div>
${unit.hasHealth()
? html`
<div class="text-sm opacity-80">
${translateText("player_info_overlay.health")}:
${unit.health()}
</div>
`
: ""}
</div>
</div>
`;
}
render() {
if (!this._isActive) {
return html``;
}
const containerClasses = this._isInfoVisible
? "opacity-100 visible"
: "opacity-0 invisible pointer-events-none";
return html`
<div
class="hidden lg:flex fixed top-[245px] right-0 w-full z-50 flex-col max-w-[180px]"
@contextmenu=${(e) => e.preventDefault()}
>
<div
class="bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-lg shadow-lg transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
>
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
</div>
</div>
`;
}
createRenderRoot() {
return this; // Disable shadow DOM to allow Tailwind styles
}
}