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:
DevelopingTom
2025-09-08 04:47:30 +02:00
committed by GitHub
parent 0c9149e5b7
commit 63a14738cd
5 changed files with 155 additions and 72 deletions
+2 -1
View File
@@ -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",
+2 -4
View File
@@ -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
+10
View File
@@ -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);
}
+139 -53
View File
@@ -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>
+2 -14
View File
@@ -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``;