Move player info panel to top of the screen & simplify (#3087)

related to #2260

## Description:

* Moves the player info panel from the right to the top of the screen
* Disable the header ad for now because it would cover up the player
info, we'll find a better place for it in the future
* Remove the collapsable button/functionality. It's hard to even click
the button because the panel disappears when you move away from a
player, and I think the info is too valuable to ever need to be
collapsed.
* Removed the "land" and "irradiated land" since it didn't add much
value
* Remove all alt text & translation, you can't hover over the player
overlay so it's irrelevant.
* put troop info inside the troop bar to reduce amount of text


<img width="479" height="88" alt="Screenshot 2026-02-01 at 8 57 33 PM"
src="https://github.com/user-attachments/assets/3b72eb16-2efa-4c00-a4d0-5e085548fa78"
/>

<img width="438" height="136" alt="Screenshot 2026-02-01 at 8 58 06 PM"
src="https://github.com/user-attachments/assets/285bb2c9-6deb-4ee8-bcc8-743cccd6b77e"
/>

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

evan
This commit is contained in:
Evan
2026-02-02 09:09:52 -08:00
committed by GitHub
parent 6b80337aa9
commit 7d3ec0fcb8
6 changed files with 125 additions and 223 deletions
+2 -1
View File
@@ -21,7 +21,8 @@ export class InGameHeaderAd extends LitElement implements Layer {
}
init() {
this.showHeaderAd();
// TODO: move ad and re-enable.
// this.showHeaderAd();
}
private showHeaderAd(): void {
+111 -196
View File
@@ -32,6 +32,7 @@ import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
import portIcon from "/images/PortIcon.svg?url";
import samLauncherIcon from "/images/SamLauncherIconWhite.svg?url";
import soldierIcon from "/images/SoldierIcon.svg?url";
function euclideanDistWorld(
coord: { x: number; y: number },
@@ -73,12 +74,6 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
@state()
private unit: UnitView | null = null;
@state()
private isWilderness: boolean = false;
@state()
private isIrradiatedWilderness: boolean = false;
@state()
private _isInfoVisible: boolean = false;
@@ -86,8 +81,6 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private lastMouseUpdate = 0;
private showDetails = true;
init() {
this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) =>
this.onMouseEvent(e),
@@ -112,8 +105,6 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
this.setVisible(false);
this.unit = null;
this.player = null;
this.isWilderness = false;
this.isIrradiatedWilderness = false;
}
public maybeShow(x: number, y: number) {
@@ -134,13 +125,6 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
this.playerProfile = p;
});
this.setVisible(true);
} else if (owner && !owner.isPlayer() && this.game.isLand(tile)) {
if (this.game.hasFallout(tile)) {
this.isIrradiatedWilderness = true;
} else {
this.isWilderness = true;
}
this.setVisible(true);
} else if (!this.game.isLand(tile)) {
const units = this.game
.units(UnitType.Warship, UnitType.TradeShip, UnitType.TransportShip)
@@ -201,28 +185,17 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
}
}
private displayUnitCount(
player: PlayerView,
type: UnitType,
icon: string,
description: string,
) {
private displayUnitCount(player: PlayerView, type: UnitType, icon: string) {
return !this.game.config().isUnitDisabled(type)
? html`<div
class="flex p-1 w-[calc(50%-0.13rem)] border rounded-md border-gray-500
items-center gap-2 text-sm"
class="flex items-center justify-center gap-0.5 lg:gap-1 p-0.5 lg:p-1 border rounded-md border-gray-500 text-[10px] lg:text-xs w-9 lg:w-12 h-6 lg:h-7"
translate="no"
>
<img
src=${icon}
width="20"
height="20"
alt="${translateText(description)}"
class="align-middle"
class="w-3 h-3 lg:w-4 lg:h-4 object-contain shrink-0"
/>
<span class="w-full text-right p-1"
>${player.totalUnitLevels(type)}</span
>
<span>${player.totalUnitLevels(type)}</span>
</div>`
: "";
}
@@ -268,7 +241,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
const myPlayer = this.game.myPlayer();
const isFriendly = myPlayer?.isFriendly(player);
const isAllied = myPlayer?.isAlliedWith(player);
let relationHtml: TemplateResult | null = null;
let allianceHtml: TemplateResult | null = null;
const maxTroops = this.game.config().maxTroops(player);
const attackingTroops = player
.outgoingAttacks()
@@ -276,34 +249,17 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
.reduce((a, b) => a + b, 0);
const totalTroops = player.troops();
if (player.type() === PlayerType.Nation && myPlayer !== null && !isAllied) {
const relation =
this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;
const relationClass = this.getRelationClass(relation);
const relationName = this.getRelationName(relation);
relationHtml = html`
<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"
allianceHtml = html` <div
class="flex flex-col items-center ml-auto mr-0 text-sm font-bold leading-tight"
>
<img
src=${allianceIcon}
alt=${translateText("player_info_overlay.alliance_timeout")}
width="20"
height="20"
class="align-middle"
/>
<img src=${allianceIcon} width="20" height="20" />
${this.allianceExpirationText(alliance)}
</span>`;
</div>`;
}
}
let playerType = "";
@@ -320,128 +276,83 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
}
return html`
<div class="p-2">
<button
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=${() => {
this.showDetails = !this.showDetails;
this.requestUpdate?.();
}}
>
${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``}
<span>${player.name()}</span>
${this.renderPlayerNameIcons(player)}
</button>
<!-- Collapsible section -->
${this.showDetails
? html`
${player.team() !== null
? html`<div class="text-sm">
${translateText("player_info_overlay.team")}:
${player.team()}
</div>`
: ""}
<div class="flex text-sm">${playerType} ${relationHtml}</div>
${player.troops() >= 1
? html`<div class="flex gap-2 text-sm" translate="no">
${translateText("player_info_overlay.troops")}
<span class="ml-auto mr-0 font-bold">
${renderTroops(player.troops())}
</span>
</div>`
: ""}
${maxTroops >= 1
? html`<div class="flex gap-2 text-sm" translate="no">
${translateText("player_info_overlay.maxtroops")}
<span class="ml-auto mr-0 font-bold">
${renderTroops(maxTroops)}
</span>
</div>`
: ""}
${attackingTroops >= 1
? html`<div class="flex gap-2 text-sm" translate="no">
${translateText("player_info_overlay.a_troops")}
<span class="ml-auto mr-0 text-red-400 font-bold">
${renderTroops(attackingTroops)}
</span>
</div>`
: ""}
${this.renderTroopBar(totalTroops, attackingTroops, maxTroops)}
<div
class="flex p-1 mb-1 mt-1 w-full border rounded-md border-yellow-400
font-bold text-yellow-400 text-sm"
translate="no"
>
<img
src=${goldCoinIcon}
alt=${translateText("player_info_overlay.gold")}
width="15"
height="15"
class="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.Factory,
factoryIcon,
"player_info_overlay.factories",
)}
${this.displayUnitCount(
player,
UnitType.Port,
portIcon,
"player_info_overlay.ports",
)}
${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>
`
: ""}
<div class="flex items-start gap-2 lg:gap-3 p-1.5 lg:p-2">
<!-- Left: Gold & Troop bar -->
<div class="flex flex-col gap-1 shrink-0 w-28">
<div
class="flex items-center justify-center p-1 border rounded-md border-yellow-400 font-bold text-yellow-400 text-xs w-28"
translate="no"
>
<img src=${goldCoinIcon} width="13" height="13" />
<span class="px-0.5">${renderNumber(player.gold())}</span>
</div>
<div class="w-28" translate="no">
${this.renderTroopBar(totalTroops, attackingTroops, maxTroops)}
</div>
</div>
<!-- Right: Player identity + Units below -->
<div class="flex flex-col justify-between self-stretch">
<div
class="flex items-center gap-2 font-bold text-sm lg:text-lg ${isFriendly
? "text-green-500"
: "text-white"}"
>
${player.cosmetics.flag
? player.cosmetics.flag!.startsWith("!")
? html`<div
class="h-6 aspect-3/4 player-flag"
${ref((el) => {
if (el instanceof HTMLElement) {
requestAnimationFrame(() => {
renderPlayerFlag(player.cosmetics.flag!, el);
});
}
})}
></div>`
: html`<img
class="h-6 aspect-3/4"
src=${"/flags/" + player.cosmetics.flag! + ".svg"}
/>`
: html``}
<span>${player.name()}</span>
${player.team() !== null && player.type() !== PlayerType.Bot
? html`<div class="flex flex-col leading-tight">
<span class="text-gray-400 text-xs font-normal"
>${playerType}</span
>
<span class="text-xs font-normal text-gray-400"
>[<span
style="color: ${this.game
.config()
.theme()
.teamColor(player.team()!)
.toHex()}"
>${player.team()}</span
>]</span
>
</div>`
: html`<span class="text-gray-400 text-xs font-normal"
>${playerType}</span
>`}
${this.renderPlayerNameIcons(player)} ${allianceHtml ?? ""}
</div>
<div class="flex gap-0.5 lg:gap-1 items-center mt-1">
${this.displayUnitCount(player, UnitType.City, cityIcon)}
${this.displayUnitCount(player, UnitType.Factory, factoryIcon)}
${this.displayUnitCount(player, UnitType.Port, portIcon)}
${this.displayUnitCount(
player,
UnitType.MissileSilo,
missileSiloIcon,
)}
${this.displayUnitCount(
player,
UnitType.SAMLauncher,
samLauncherIcon,
)}
${this.displayUnitCount(player, UnitType.Warship, warshipIcon)}
</div>
</div>
</div>
`;
}
@@ -463,7 +374,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
return html`
<div
class="w-full mt-2 mb-2 h-5 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden"
class="w-full mt-1 lg:mt-2 h-5 lg:h-6 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
>
<div class="h-full flex">
${greenPercent > 0
@@ -479,6 +390,25 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
></div>`
: ""}
</div>
<div
class="absolute inset-0 flex items-center justify-between px-1.5 text-xs font-bold leading-none pointer-events-none"
translate="no"
>
<span class="text-white drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]"
>${renderTroops(totalTroops)}</span
>
<span class="text-white drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]"
>${renderTroops(maxTroops)}</span
>
</div>
<img
src=${soldierIcon}
alt=""
aria-hidden="true"
width="14"
height="14"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 brightness-0 invert drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)] pointer-events-none"
/>
</div>
`;
}
@@ -497,18 +427,12 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
<div class="mt-1">
<div class="text-sm opacity-80">${unit.type()}</div>
${unit.hasHealth()
? html`
<div class="text-sm">
${translateText("player_info_overlay.health")}:
${unit.health()}
</div>
`
? html` <div class="text-sm">Health: ${unit.health()}</div> `
: ""}
${unit.type() === UnitType.TransportShip
? html`
<div class="text-sm">
${translateText("player_info_overlay.troops")}:
${renderTroops(unit.troops())}
Troops: ${renderTroops(unit.troops())}
</div>
`
: ""}
@@ -528,21 +452,12 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
return html`
<div
class="block lg:flex fixed top-37.5 right-4 w-full z-50 flex-col max-w-45"
class="fixed top-0 lg:top-[10px] left-0 right-0 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 z-[1001]"
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
<div
class="bg-gray-800/70 backdrop-blur-xs shadow-xs rounded-lg shadow-lg transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
class="bg-gray-800/70 backdrop-blur-xs shadow-xs lg:rounded-lg shadow-lg transition-all duration-300 text-white text-lg lg:text-base w-full sm:w-auto sm:min-w-[400px] overflow-hidden ${containerClasses}"
>
${this.isWilderness || this.isIrradiatedWilderness
? html`<div class="p-2 font-bold">
${translateText(
this.isIrradiatedWilderness
? "player_info_overlay.irradiated_wilderness_title"
: "player_info_overlay.wilderness_title",
)}
</div>`
: ""}
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
</div>
+3 -2
View File
@@ -13,6 +13,7 @@ import { Layer } from "./Layer";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
import factoryIcon from "/images/FactoryIconWhite.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";
@@ -256,11 +257,11 @@ export class UnitDisplay extends LitElement implements Layer {
<div class="p-2">
${translateText("build_menu.desc." + structureKey)}
</div>
<div>
<div class="flex items-center justify-center gap-1">
<img src=${goldCoinIcon} width="13" height="13" />
<span class="text-yellow-300"
>${renderNumber(this.cost(unitType))}</span
>
${translateText("player_info_overlay.gold")}
</div>
</div>
`