Files
OpenFrontIO/src/client/graphics/layers/PlayerInfoOverlay.ts
T
2025-02-01 12:05:11 -08:00

205 lines
6.8 KiB
TypeScript

import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Layer } from './Layer';
import { Game, GameType, Player, PlayerProfile, PlayerType, Relation, Unit, UnitType } from '../../../core/game/Game';
import { ClientID } from '../../../core/Schemas';
import { EventBus } from '../../../core/EventBus';
import { TransformHandler } from '../TransformHandler';
import { MouseMoveEvent } from '../../InputHandler';
import { GameView, PlayerView, UnitView } from '../../../core/game/GameView';
import { TileRef } from '../../../core/game/GameMap';
import { renderNumber, renderTroops } from '../../Utils';
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: String })
public clientID!: ClientID;
@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.setVisible(false);
this.unit = null;
this.player = null;
const worldCoord = this.transform.screenToWorldCoordinates(event.x, event.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.Destroyer, UnitType.Battleship, UnitType.TradeShip)
.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 myPlayer(): PlayerView | null {
if (!this.game) {
return null;
}
return this.game.playerByClientID(this.clientID);
}
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 renderPlayerInfo(player: PlayerView) {
const myPlayer = this.myPlayer();
const isAlly = myPlayer?.isAlliedWith(player);
let relationHtml = null;
if (player.type() == PlayerType.FakeHuman && myPlayer != null) {
const relation = this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;
const relationClass = this.getRelationClass(relation);
const relationName = Relation[relation];
relationHtml = html`
<div class="text-sm opacity-80">
Attitude: <span class="${relationClass}">${relationName}</span>
</div>
`;
}
return html`
<div class="p-2">
<div class="font-bold mb-1 ${isAlly ? 'text-green-500' : 'text-white'}">${player.name()}</div>
<div class="text-sm opacity-80">Troops: ${renderTroops(player.troops())}</div>
<div class="text-sm opacity-80">Gold: ${renderNumber(player.gold())}</div>
${relationHtml}
</div>
`;
}
private renderUnitInfo(unit: UnitView) {
const isAlly = (unit.owner() == this.myPlayer() || this.myPlayer()?.isAlliedWith(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">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="fixed top-24 right-3 z-50 flex flex-col md:top-20 md:right-2">
<div class="bg-opacity-70 bg-gray-900 rounded-lg shadow-lg backdrop-blur-sm transition-all duration-300 min-w-[120px] md:min-w-[100px] 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
}
}