mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:10:42 +00:00
Redesign Player info overlay (#2000)
## Description: Redesign the player info panel to match the bottom panel. Changes: - Added alliance timeout - Various css restyling Old: <img width="180" height="276" alt="image" src="https://github.com/user-attachments/assets/4ae8994b-868c-4eb8-b42a-85f0f0ec2f96" /> New: <img width="179" height="239" alt="image" src="https://github.com/user-attachments/assets/c29c34e5-5bfd-468e-9947-e0ac319fbccf" /> ## 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: IngloriousTom --------- Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
@@ -494,7 +494,8 @@
|
||||
"nation": "Nation",
|
||||
"player": "Player",
|
||||
"team": "Team",
|
||||
"d_troops": "Defending troops",
|
||||
"alliance_timeout": "Alliance ends in",
|
||||
"troops": "Troops",
|
||||
"a_troops": "Attacking troops",
|
||||
"gold": "Gold",
|
||||
"ports": "Ports",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { renderDuration, translateText } from "../client/Utils";
|
||||
import { GameMapType, GameMode } from "../core/game/Game";
|
||||
import { GameID, GameInfo } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
@@ -104,9 +104,7 @@ export class PublicLobby extends LitElement {
|
||||
const timeRemaining = Math.max(0, Math.floor((start - Date.now()) / 1000));
|
||||
|
||||
// Format time to show minutes and seconds
|
||||
const minutes = Math.floor(timeRemaining / 60);
|
||||
const seconds = timeRemaining % 60;
|
||||
const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
||||
const timeDisplay = renderDuration(timeRemaining);
|
||||
|
||||
const teamCount =
|
||||
lobby.gameConfig.gameMode === GameMode.Team
|
||||
|
||||
@@ -2,6 +2,16 @@ import IntlMessageFormat from "intl-messageformat";
|
||||
import { MessageType } from "../core/game/Game";
|
||||
import { LangSelector } from "./LangSelector";
|
||||
|
||||
export function renderDuration(totalSeconds: number): string {
|
||||
if (totalSeconds <= 0) return "0s";
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
let time = "";
|
||||
if (minutes > 0) time += `${minutes}min `;
|
||||
time += `${seconds}s`;
|
||||
return time.trim();
|
||||
}
|
||||
|
||||
export function renderTroops(troops: number): string {
|
||||
return renderNumber(troops / 10);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
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 allianceIcon from "../../../../resources/images/AllianceIcon.svg";
|
||||
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 goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
|
||||
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
|
||||
import samLauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg";
|
||||
import { renderPlayerFlag } from "../../../core/CustomFlag";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import {
|
||||
@@ -12,9 +19,15 @@ import {
|
||||
UnitType,
|
||||
} from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { AllianceView } from "../../../core/game/GameUpdates";
|
||||
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
|
||||
import { ContextMenuEvent, MouseMoveEvent } from "../../InputHandler";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import {
|
||||
renderDuration,
|
||||
renderNumber,
|
||||
renderTroops,
|
||||
translateText,
|
||||
} from "../../Utils";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
import { CloseRadialMenuEvent } from "./RadialMenu";
|
||||
@@ -175,37 +188,83 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
private displayUnitCount(
|
||||
player: PlayerView,
|
||||
type: UnitType,
|
||||
icon: string,
|
||||
description: string,
|
||||
) {
|
||||
return !this.game.config().isUnitDisabled(type)
|
||||
? html`<div class="text-sm opacity-80" translate="no">
|
||||
${translateText(description)}: ${player.totalUnitLevels(type)}
|
||||
? html`<div
|
||||
class="flex p-1 w-[calc(50%-0.13rem)] border rounded-md border-gray-500
|
||||
items-center gap-2 text-sm opacity-80"
|
||||
translate="no"
|
||||
>
|
||||
<img
|
||||
src=${icon}
|
||||
width="20"
|
||||
height="20"
|
||||
alt="${translateText(description)}"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
<span class="w-full text-right p-1"
|
||||
>${player.totalUnitLevels(type)}</span
|
||||
>
|
||||
</div>`
|
||||
: "";
|
||||
}
|
||||
|
||||
private allianceExpirationText(alliance: AllianceView) {
|
||||
const { expiresAt } = alliance;
|
||||
const remainingTicks = expiresAt - this.game.ticks();
|
||||
let remainingSeconds = 0;
|
||||
if (remainingTicks > 0) {
|
||||
remainingSeconds = Math.max(0, Math.floor(remainingTicks / 10)); // 10 ticks per second
|
||||
}
|
||||
return renderDuration(remainingSeconds);
|
||||
}
|
||||
|
||||
private renderPlayerInfo(player: PlayerView) {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
const isFriendly = myPlayer?.isFriendly(player);
|
||||
const isAllied = myPlayer?.isAlliedWith(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) {
|
||||
if (
|
||||
player.type() === PlayerType.FakeHuman &&
|
||||
myPlayer !== null &&
|
||||
!isAllied
|
||||
) {
|
||||
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>
|
||||
<span class="ml-auto mr-0 ${relationClass}">${relationName}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
if (isAllied) {
|
||||
const alliance = myPlayer
|
||||
?.alliances()
|
||||
.find((alliance) => alliance.other === player.id());
|
||||
if (alliance !== undefined) {
|
||||
relationHtml = html` <span
|
||||
class="flex gap-2 ml-auto mr-0 text-sm font-bold"
|
||||
>
|
||||
<img
|
||||
src=${allianceIcon}
|
||||
alt=${translateText("player_info_overlay.alliance_timeout")}
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${this.allianceExpirationText(alliance)}
|
||||
</span>`;
|
||||
}
|
||||
}
|
||||
let playerType = "";
|
||||
switch (player.type()) {
|
||||
case PlayerType.Bot:
|
||||
@@ -222,7 +281,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
return html`
|
||||
<div class="p-2">
|
||||
<button
|
||||
class="text-bold text-sm lg:text-lg font-bold mb-1 inline-flex break-all ${isFriendly
|
||||
class="items-center text-bold text-sm lg:text-lg font-bold mb-1 inline-flex break-all ${isFriendly
|
||||
? "text-green-500"
|
||||
: "text-white"}"
|
||||
@click=${() => {
|
||||
@@ -259,56 +318,83 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
${player.team()}
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="text-sm opacity-80">
|
||||
${translateText("player_info_overlay.type")}: ${playerType}
|
||||
</div>
|
||||
<div class="flex text-sm">${playerType} ${relationHtml}</div>
|
||||
${player.troops() >= 1
|
||||
? html`<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.d_troops")}:
|
||||
${renderTroops(player.troops())}
|
||||
? html`<div
|
||||
class="flex gap-2 text-sm opacity-80"
|
||||
translate="no"
|
||||
>
|
||||
${translateText("player_info_overlay.troops")}
|
||||
<span class="ml-auto mr-0 font-bold">
|
||||
${renderTroops(player.troops())}
|
||||
</span>
|
||||
</div>`
|
||||
: ""}
|
||||
${attackingTroops >= 1
|
||||
? html`<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.a_troops")}:
|
||||
${renderTroops(attackingTroops)}
|
||||
? html`<div
|
||||
class="flex gap-2 text-sm opacity-80"
|
||||
translate="no"
|
||||
>
|
||||
${translateText("player_info_overlay.a_troops")}
|
||||
<span class="ml-auto mr-0 text-red-400 font-bold">
|
||||
${renderTroops(attackingTroops)}
|
||||
</span>
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.gold")}:
|
||||
${renderNumber(player.gold())}
|
||||
<div
|
||||
class="flex p-1 mb-1 mt-1 w-full border rounded-md border-yellow-400
|
||||
font-bold text-yellow-400 text-sm opacity-80"
|
||||
translate="no"
|
||||
>
|
||||
<img
|
||||
src=${goldCoinIcon}
|
||||
alt=${translateText("player_info_overlay.gold")}
|
||||
width="15"
|
||||
height="15"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
<span class="w-full text-center"
|
||||
>${renderNumber(player.gold())}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-wrap max-w-3xl gap-1">
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.City,
|
||||
cityIcon,
|
||||
"player_info_overlay.cities",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Port,
|
||||
portIcon,
|
||||
"player_info_overlay.ports",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Factory,
|
||||
factoryIcon,
|
||||
"player_info_overlay.factories",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.MissileSilo,
|
||||
missileSiloIcon,
|
||||
"player_info_overlay.missile_launchers",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.SAMLauncher,
|
||||
samLauncherIcon,
|
||||
"player_info_overlay.sams",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Warship,
|
||||
warshipIcon,
|
||||
"player_info_overlay.warships",
|
||||
)}
|
||||
</div>
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Port,
|
||||
"player_info_overlay.ports",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.City,
|
||||
"player_info_overlay.cities",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Factory,
|
||||
"player_info_overlay.factories",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.MissileSilo,
|
||||
"player_info_overlay.missile_launchers",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.SAMLauncher,
|
||||
"player_info_overlay.sams",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Warship,
|
||||
"player_info_overlay.warships",
|
||||
)}
|
||||
${relationHtml}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
SendEmojiIntentEvent,
|
||||
SendTargetPlayerIntentEvent,
|
||||
} from "../../Transport";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { renderDuration, renderNumber, renderTroops } from "../../Utils";
|
||||
import { UIState } from "../UIState";
|
||||
import { ChatModal } from "./ChatModal";
|
||||
import { EmojiTable } from "./EmojiTable";
|
||||
@@ -188,17 +188,15 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
const myPlayer = this.g.myPlayer();
|
||||
if (myPlayer !== null && myPlayer.isAlive()) {
|
||||
this.actions = await myPlayer.actions(this.tile);
|
||||
|
||||
if (this.actions?.interaction?.allianceExpiresAt !== undefined) {
|
||||
const expiresAt = this.actions.interaction.allianceExpiresAt;
|
||||
const remainingTicks = expiresAt - this.g.ticks();
|
||||
|
||||
if (remainingTicks > 0) {
|
||||
const remainingSeconds = Math.max(
|
||||
0,
|
||||
Math.floor(remainingTicks / 10),
|
||||
); // 10 ticks per second
|
||||
this.allianceExpiryText = this.formatDuration(remainingSeconds);
|
||||
this.allianceExpiryText = renderDuration(remainingSeconds);
|
||||
}
|
||||
} else {
|
||||
this.allianceExpiryText = null;
|
||||
@@ -208,16 +206,6 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private formatDuration(totalSeconds: number): string {
|
||||
if (totalSeconds <= 0) return "0s";
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
let time = "";
|
||||
if (minutes > 0) time += `${minutes}m `;
|
||||
time += `${seconds}s`;
|
||||
return time.trim();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
|
||||
Reference in New Issue
Block a user