From 90b73451a822cea4d522ed2a5da48296c7843f0e Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Wed, 19 Nov 2025 00:03:52 +0100
Subject: [PATCH] =?UTF-8?q?Added=20NameLayer-Icons=20to=20PlayerInfoOverla?=
=?UTF-8?q?y=20=E2=9C=A8=20(#2446)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Resolves #1686
## Description:
Above or behind the player names on the map, there are icons (images
and/or emojis).
This PR also adds these icons to the PlayerInfoOverlay (on the right
side of the player name).
To share the logic, a new file PlayerIcons.ts has been created.
The alliance icon is NOT shown because it's already on the left side of
the alliance timer.
### Why is this change needed?
Sometimes you can't quickly find the nametag of a player on the map.
Especially if a player's territory is scattered around the map, maybe
even on several small islands.
But you still want to know if the player is AFK, has a traitor debuff,
etc.
So it's very useful to get this information by just hovering over a
player instead of needing to search for the floating nametag.
## 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:
FloPinguin
---------
Co-authored-by: Evan
---
src/client/graphics/PlayerIcons.ts | 154 ++++++++
src/client/graphics/layers/NameLayer.ts | 372 ++++--------------
.../graphics/layers/PlayerInfoOverlay.ts | 31 +-
3 files changed, 267 insertions(+), 290 deletions(-)
create mode 100644 src/client/graphics/PlayerIcons.ts
diff --git a/src/client/graphics/PlayerIcons.ts b/src/client/graphics/PlayerIcons.ts
new file mode 100644
index 000000000..6f1602578
--- /dev/null
+++ b/src/client/graphics/PlayerIcons.ts
@@ -0,0 +1,154 @@
+import allianceIcon from "../../../resources/images/AllianceIcon.svg";
+import allianceRequestBlackIcon from "../../../resources/images/AllianceRequestBlackIcon.svg";
+import allianceRequestWhiteIcon from "../../../resources/images/AllianceRequestWhiteIcon.svg";
+import crownIcon from "../../../resources/images/CrownIcon.svg";
+import disconnectedIcon from "../../../resources/images/DisconnectedIcon.svg";
+import embargoBlackIcon from "../../../resources/images/EmbargoBlackIcon.svg";
+import embargoWhiteIcon from "../../../resources/images/EmbargoWhiteIcon.svg";
+import nukeRedIcon from "../../../resources/images/NukeIconRed.svg";
+import nukeWhiteIcon from "../../../resources/images/NukeIconWhite.svg";
+import targetIcon from "../../../resources/images/TargetIcon.svg";
+import traitorIcon from "../../../resources/images/TraitorIcon.svg";
+import { AllPlayers, nukeTypes } from "../../core/game/Game";
+import { GameView, PlayerView } from "../../core/game/GameView";
+
+export type PlayerIconId =
+ | "crown"
+ | "traitor"
+ | "disconnected"
+ | "alliance"
+ | "alliance-request"
+ | "target"
+ | "emoji"
+ | "embargo"
+ | "nuke";
+
+export type PlayerIconKind = "image" | "emoji";
+
+export interface PlayerIconDescriptor {
+ id: PlayerIconId;
+ kind: PlayerIconKind;
+ /** Image URL for image icons */
+ src?: string;
+ /** Text content for emoji icons */
+ text?: string;
+ /** Whether the icon should be visually centered over the name */
+ center?: boolean;
+}
+
+export interface PlayerIconParams {
+ game: GameView;
+ player: PlayerView;
+ /** Whether the alliance icon (handshake) should be included */
+ includeAllianceIcon: boolean;
+ /** Player currently in first place, used for the crown icon */
+ firstPlace: PlayerView | null;
+}
+
+export function getFirstPlacePlayer(game: GameView): PlayerView | null {
+ const sorted = game
+ .playerViews()
+ .sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
+
+ return sorted.length > 0 ? sorted[0] : null;
+}
+
+export function getPlayerIcons(
+ params: PlayerIconParams,
+): PlayerIconDescriptor[] {
+ const { game, player, includeAllianceIcon, firstPlace } = params;
+
+ const myPlayer = game.myPlayer();
+ const userSettings = game.config().userSettings();
+ const isDarkMode = userSettings?.darkMode() ?? false;
+ const emojisEnabled = userSettings?.emojis() ?? false;
+
+ const icons: PlayerIconDescriptor[] = [];
+
+ // Crown icon for first place
+ if (player === firstPlace) {
+ icons.push({ id: "crown", kind: "image", src: crownIcon });
+ }
+
+ // Traitor icon
+ if (player.isTraitor()) {
+ icons.push({ id: "traitor", kind: "image", src: traitorIcon });
+ }
+
+ // Disconnected icon
+ if (player.isDisconnected()) {
+ icons.push({ id: "disconnected", kind: "image", src: disconnectedIcon });
+ }
+
+ // Alliance icon
+ if (
+ includeAllianceIcon &&
+ myPlayer !== null &&
+ myPlayer.isAlliedWith(player)
+ ) {
+ icons.push({ id: "alliance", kind: "image", src: allianceIcon });
+ }
+
+ // Alliance request icon (theme dependent)
+ if (myPlayer !== null && player.isRequestingAllianceWith(myPlayer)) {
+ const allianceRequestIcon = isDarkMode
+ ? allianceRequestWhiteIcon
+ : allianceRequestBlackIcon;
+ icons.push({
+ id: "alliance-request",
+ kind: "image",
+ src: allianceRequestIcon,
+ });
+ }
+
+ // Target icon (centered on the map, but regular in overlays)
+ if (myPlayer !== null && new Set(myPlayer.transitiveTargets()).has(player)) {
+ icons.push({ id: "target", kind: "image", src: targetIcon, center: true });
+ }
+
+ // Emoji handling
+ if (emojisEnabled) {
+ const emojis = player
+ .outgoingEmojis()
+ .filter(
+ (emoji) =>
+ emoji.recipientID === AllPlayers ||
+ emoji.recipientID === myPlayer?.smallID(),
+ );
+
+ if (emojis.length > 0) {
+ icons.push({
+ id: "emoji",
+ kind: "emoji",
+ text: emojis[0].message,
+ });
+ }
+ }
+
+ // Embargo icon (theme dependent)
+ if (myPlayer?.hasEmbargo(player)) {
+ const embargoIcon = isDarkMode ? embargoWhiteIcon : embargoBlackIcon;
+ icons.push({ id: "embargo", kind: "image", src: embargoIcon });
+ }
+
+ // Nuke icon (different color depending on whether the local player is the target)
+ const nukesSentByOtherPlayer = game.units(...nukeTypes).filter((unit) => {
+ const isSendingNuke = player.id() === unit.owner().id();
+ const notMyPlayer = !myPlayer || unit.owner().id() !== myPlayer.id();
+ return isSendingNuke && notMyPlayer && unit.isActive();
+ });
+
+ const isMyPlayerTarget = nukesSentByOtherPlayer.some((unit) => {
+ const detonationDst = unit.targetTile();
+ if (!detonationDst || !myPlayer) return false;
+ const targetId = game.owner(detonationDst).id();
+ return targetId === myPlayer.id();
+ });
+
+ if (nukesSentByOtherPlayer.length > 0) {
+ const icon = isMyPlayerTarget ? nukeRedIcon : nukeWhiteIcon;
+ icons.push({ id: "nuke", kind: "image", src: icon });
+ }
+
+ return icons;
+}
diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts
index 3965c8922..649315cf6 100644
--- a/src/client/graphics/layers/NameLayer.ts
+++ b/src/client/graphics/layers/NameLayer.ts
@@ -1,29 +1,23 @@
-import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
-import allianceRequestBlackIcon from "../../../../resources/images/AllianceRequestBlackIcon.svg";
-import allianceRequestWhiteIcon from "../../../../resources/images/AllianceRequestWhiteIcon.svg";
-import crownIcon from "../../../../resources/images/CrownIcon.svg";
-import disconnectedIcon from "../../../../resources/images/DisconnectedIcon.svg";
-import embargoBlackIcon from "../../../../resources/images/EmbargoBlackIcon.svg";
-import embargoWhiteIcon from "../../../../resources/images/EmbargoWhiteIcon.svg";
-import nukeRedIcon from "../../../../resources/images/NukeIconRed.svg";
-import nukeWhiteIcon from "../../../../resources/images/NukeIconWhite.svg";
import shieldIcon from "../../../../resources/images/ShieldIconBlack.svg";
-import targetIcon from "../../../../resources/images/TargetIcon.svg";
-import traitorIcon from "../../../../resources/images/TraitorIcon.svg";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { Theme } from "../../../core/configuration/Config";
-import { AllPlayers, Cell, nukeTypes } from "../../../core/game/Game";
+import { Cell } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent } from "../../InputHandler";
import { createCanvas, renderNumber, renderTroops } from "../../Utils";
+import {
+ getFirstPlacePlayer,
+ getPlayerIcons,
+ PlayerIconId,
+} from "../PlayerIcons";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
class RenderInfo {
- public icons: Map = new Map(); // Track icon elements
+ public icons: Map = new Map(); // Track icon elements
constructor(
public player: PlayerView,
@@ -43,51 +37,18 @@ export class NameLayer implements Layer {
private rand = new PseudoRandom(10);
private renders: RenderInfo[] = [];
private seenPlayers: Set = new Set();
- private traitorIconImage: HTMLImageElement;
- private disconnectedIconImage: HTMLImageElement;
- private allianceRequestBlackIconImage: HTMLImageElement;
- private allianceRequestWhiteIconImage: HTMLImageElement;
- private allianceIconImage: HTMLImageElement;
- private targetIconImage: HTMLImageElement;
- private crownIconImage: HTMLImageElement;
- private embargoBlackIconImage: HTMLImageElement;
- private embargoWhiteIconImage: HTMLImageElement;
- private nukeWhiteIconImage: HTMLImageElement;
- private nukeRedIconImage: HTMLImageElement;
private shieldIconImage: HTMLImageElement;
private container: HTMLDivElement;
- private firstPlace: PlayerView | null = null;
private theme: Theme = this.game.config().theme();
private userSettings: UserSettings = new UserSettings();
private isVisible: boolean = true;
+ private firstPlace: PlayerView | null = null;
constructor(
private game: GameView,
private transformHandler: TransformHandler,
private eventBus: EventBus,
) {
- this.traitorIconImage = new Image();
- this.traitorIconImage.src = traitorIcon;
- this.disconnectedIconImage = new Image();
- this.disconnectedIconImage.src = disconnectedIcon;
- this.allianceIconImage = new Image();
- this.allianceIconImage.src = allianceIcon;
- this.allianceRequestBlackIconImage = new Image();
- this.allianceRequestBlackIconImage.src = allianceRequestBlackIcon;
- this.allianceRequestWhiteIconImage = new Image();
- this.allianceRequestWhiteIconImage.src = allianceRequestWhiteIcon;
- this.crownIconImage = new Image();
- this.crownIconImage.src = crownIcon;
- this.targetIconImage = new Image();
- this.targetIconImage.src = targetIcon;
- this.embargoBlackIconImage = new Image();
- this.embargoBlackIconImage.src = embargoBlackIcon;
- this.embargoWhiteIconImage = new Image();
- this.embargoWhiteIconImage.src = embargoWhiteIcon;
- this.nukeWhiteIconImage = new Image();
- this.nukeWhiteIconImage.src = nukeWhiteIcon;
- this.nukeRedIconImage = new Image();
- this.nukeRedIconImage.src = nukeRedIcon;
this.shieldIconImage = new Image();
this.shieldIconImage.src = shieldIcon;
}
@@ -172,12 +133,9 @@ export class NameLayer implements Layer {
if (this.game.ticks() % 10 !== 0) {
return;
}
- const sorted = this.game
- .playerViews()
- .sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
- if (sorted.length > 0) {
- this.firstPlace = sorted[0];
- }
+
+ // Precompute the first-place player for performance
+ this.firstPlace = getFirstPlacePlayer(this.game);
for (const player of this.game.playerViews()) {
if (player.isAlive()) {
@@ -404,251 +362,89 @@ export class NameLayer implements Layer {
".player-icons",
) as HTMLDivElement;
const iconSize = Math.min(render.fontSize * 1.5, 48);
- const myPlayer = this.game.myPlayer();
- const isDarkMode = this.userSettings.darkMode();
- // Crown icon
- const existingCrown = iconsDiv.querySelector('[data-icon="crown"]');
- if (render.player === this.firstPlace) {
- if (!existingCrown) {
- iconsDiv.appendChild(
- this.createIconElement(
- this.crownIconImage.src,
- iconSize,
- "crown",
- false,
- ),
- );
+ // Compute which icons should be shown for this player using shared logic
+ const icons = getPlayerIcons({
+ game: this.game,
+ player: render.player,
+ includeAllianceIcon: true,
+ firstPlace: this.firstPlace,
+ });
+
+ // Build a set of desired icon IDs
+ const desiredIconIds = new Set(icons.map((icon) => icon.id));
+
+ // Remove any icons that are no longer needed
+ for (const [id, element] of render.icons) {
+ if (!desiredIconIds.has(id)) {
+ element.remove();
+ render.icons.delete(id);
}
- } else if (existingCrown) {
- existingCrown.remove();
}
- // Traitor icon
- let existingTraitor = iconsDiv.querySelector('[data-icon="traitor"]');
- if (render.player.isTraitor()) {
- const remainingTicks = render.player.getTraitorRemainingTicks();
- // Use precise seconds (not rounded) for smoother transitions, rounded to 0.5s intervals
- const remainingSeconds = Math.round((remainingTicks / 10) * 2) / 2;
+ // Add or update icons that should be shown
+ for (const icon of icons) {
+ if (icon.kind === "emoji" && icon.text) {
+ let emojiDiv = render.icons.get(icon.id) as HTMLDivElement | undefined;
- if (!existingTraitor) {
- existingTraitor = this.createIconElement(
- this.traitorIconImage.src,
- iconSize,
- "traitor",
- );
- iconsDiv.appendChild(existingTraitor);
- }
+ if (!emojiDiv) {
+ emojiDiv = document.createElement("div");
+ emojiDiv.style.position = "absolute";
+ emojiDiv.style.top = "50%";
+ emojiDiv.style.transform = "translateY(-50%)";
+ iconsDiv.appendChild(emojiDiv);
+ render.icons.set(icon.id, emojiDiv);
+ }
- // Apply flashing animation - smooth speed increase starting at 15s
- if (existingTraitor instanceof HTMLImageElement) {
- if (remainingSeconds <= 15) {
- // Smooth transition: starts at 1s at 15 seconds, decreases to 0.2s at 0 seconds
- // Using cubic ease-out for slower, more gradual acceleration
- const clampedSeconds = Math.max(0, Math.min(15, remainingSeconds));
- const normalizedTime = clampedSeconds / 15; // 0 to 1 (1 = 15s remaining, 0 = 0s remaining)
+ emojiDiv.textContent = icon.text;
+ emojiDiv.style.fontSize = `${iconSize}px`;
+ } else if (icon.kind === "image" && icon.src) {
+ let imgElement = render.icons.get(icon.id) as
+ | HTMLImageElement
+ | undefined;
- // Cubic ease-out: slower acceleration, smoother transition
- const easedProgress = 1 - Math.pow(1 - normalizedTime, 3);
+ if (!imgElement) {
+ imgElement = this.createIconElement(icon.src, iconSize, icon.center);
+ iconsDiv.appendChild(imgElement);
+ render.icons.set(icon.id, imgElement);
+ }
- const maxDuration = 1.0; // Slow flash at 15 seconds
- const minDuration = 0.2; // Fast flash at 0 seconds
- const duration =
- minDuration + (maxDuration - minDuration) * easedProgress;
- const animationDuration = `${duration.toFixed(2)}s`;
+ // Update src if it changed (e.g., nuke red/white or dark-mode icons)
+ if (imgElement.src !== icon.src) {
+ imgElement.src = icon.src;
+ }
- existingTraitor.style.animation = `traitorFlash ${animationDuration} infinite`;
- existingTraitor.style.animationTimingFunction = "ease-in-out";
- } else {
- // Don't flash if more than 15 seconds remaining
- existingTraitor.style.animation = "none";
+ imgElement.style.width = `${iconSize}px`;
+ imgElement.style.height = `${iconSize}px`;
+
+ // Traitor flashing - smooth speed increase starting at 15s
+ if (icon.id === "traitor") {
+ const remainingTicks = render.player.getTraitorRemainingTicks();
+ // Use precise seconds (not rounded) for smoother transitions, rounded to 0.5s intervals
+ const remainingSeconds = Math.round((remainingTicks / 10) * 2) / 2;
+
+ if (remainingSeconds <= 15) {
+ // Smooth transition: starts at 1s at 15 seconds, decreases to 0.2s at 0 seconds
+ // Using cubic ease-out for slower, more gradual acceleration
+ const clampedSeconds = Math.max(0, Math.min(15, remainingSeconds));
+ const normalizedTime = clampedSeconds / 15; // 0 to 1 (1 = 15s remaining, 0 = 0s remaining)
+
+ // Cubic ease-out: slower acceleration, smoother transition
+ const easedProgress = 1 - Math.pow(1 - normalizedTime, 3);
+ const maxDuration = 1.0; // Slow flash at 15 seconds
+ const minDuration = 0.2; // Fast flash at 0 seconds
+ const duration =
+ minDuration + (maxDuration - minDuration) * easedProgress;
+ const animationDuration = `${duration.toFixed(2)}s`;
+
+ imgElement.style.animation = `traitorFlash ${animationDuration} infinite`;
+ imgElement.style.animationTimingFunction = "ease-in-out";
+ } else {
+ // Don't flash if more than 15 seconds remaining
+ imgElement.style.animation = "none";
+ }
}
}
- } else if (existingTraitor) {
- existingTraitor.remove();
- }
-
- // Disconnected icon
- const existingDisconnected = iconsDiv.querySelector(
- '[data-icon="disconnected"]',
- );
- if (render.player.isDisconnected()) {
- if (!existingDisconnected) {
- iconsDiv.appendChild(
- this.createIconElement(
- this.disconnectedIconImage.src,
- iconSize,
- "disconnected",
- ),
- );
- }
- } else if (existingDisconnected) {
- existingDisconnected.remove();
- }
-
- // Alliance icon
- const existingAlliance = iconsDiv.querySelector('[data-icon="alliance"]');
- if (myPlayer !== null && myPlayer.isAlliedWith(render.player)) {
- if (!existingAlliance) {
- iconsDiv.appendChild(
- this.createIconElement(
- this.allianceIconImage.src,
- iconSize,
- "alliance",
- ),
- );
- }
- } else if (existingAlliance) {
- existingAlliance.remove();
- }
-
- // Alliance request icon
- let existingRequestAlliance = iconsDiv.querySelector(
- '[data-icon="alliance-request"]',
- );
- const isThemeAllianceRequestIcon =
- existingRequestAlliance?.getAttribute("dark-mode") ===
- isDarkMode.toString();
- const AllianceRequestIconImageSrc = isDarkMode
- ? this.allianceRequestWhiteIconImage.src
- : this.allianceRequestBlackIconImage.src;
-
- if (myPlayer !== null && render.player.isRequestingAllianceWith(myPlayer)) {
- // Create new icon to match theme
- if (existingRequestAlliance && !isThemeAllianceRequestIcon) {
- existingRequestAlliance.remove();
- existingRequestAlliance = null;
- }
-
- if (!existingRequestAlliance) {
- iconsDiv.appendChild(
- this.createIconElement(
- AllianceRequestIconImageSrc,
- iconSize,
- "alliance-request",
- ),
- );
- }
- } else if (existingRequestAlliance) {
- existingRequestAlliance.remove();
- }
-
- // Target icon
- const existingTarget = iconsDiv.querySelector('[data-icon="target"]');
- if (
- myPlayer !== null &&
- new Set(myPlayer.transitiveTargets()).has(render.player)
- ) {
- if (!existingTarget) {
- iconsDiv.appendChild(
- this.createIconElement(
- this.targetIconImage.src,
- iconSize,
- "target",
- true,
- ),
- );
- }
- } else if (existingTarget) {
- existingTarget.remove();
- }
-
- // Emoji handling
- const existingEmoji = iconsDiv.querySelector('[data-icon="emoji"]');
- const emojis = render.player
- .outgoingEmojis()
- .filter(
- (emoji) =>
- emoji.recipientID === AllPlayers ||
- emoji.recipientID === myPlayer?.smallID(),
- );
-
- if (this.game.config().userSettings()?.emojis() && emojis.length > 0) {
- if (!existingEmoji) {
- const emojiDiv = document.createElement("div");
- emojiDiv.setAttribute("data-icon", "emoji");
- emojiDiv.style.fontSize = `${iconSize}px`;
- emojiDiv.textContent = emojis[0].message;
- emojiDiv.style.position = "absolute";
- emojiDiv.style.top = "50%";
- emojiDiv.style.transform = "translateY(-50%)";
- iconsDiv.appendChild(emojiDiv);
- }
- } else if (existingEmoji) {
- existingEmoji.remove();
- }
-
- // Embargo icon
- let existingEmbargo = iconsDiv.querySelector('[data-icon="embargo"]');
- const isThemeEmbargoIcon =
- existingEmbargo?.getAttribute("dark-mode") === isDarkMode.toString();
- const embargoIconImageSrc = isDarkMode
- ? this.embargoWhiteIconImage.src
- : this.embargoBlackIconImage.src;
-
- if (myPlayer?.hasEmbargo(render.player)) {
- // Create new icon to match theme
- if (existingEmbargo && !isThemeEmbargoIcon) {
- existingEmbargo.remove();
- existingEmbargo = null;
- }
-
- if (!existingEmbargo) {
- iconsDiv.appendChild(
- this.createIconElement(embargoIconImageSrc, iconSize, "embargo"),
- );
- }
- } else if (existingEmbargo) {
- existingEmbargo.remove();
- }
-
- const nukesSentByOtherPlayer = this.game.units().filter((unit) => {
- const isSendingNuke = render.player.id() === unit.owner().id();
- const notMyPlayer = !myPlayer || unit.owner().id() !== myPlayer.id();
- return (
- nukeTypes.includes(unit.type()) &&
- isSendingNuke &&
- notMyPlayer &&
- unit.isActive()
- );
- });
- const isMyPlayerTarget = nukesSentByOtherPlayer.find((unit) => {
- const detonationDst = unit.targetTile();
- if (detonationDst === undefined) return false;
- const targetId = this.game.owner(detonationDst).id();
- return myPlayer && targetId === myPlayer.id();
- });
- const existingNuke = iconsDiv.querySelector(
- '[data-icon="nuke"]',
- ) as HTMLImageElement;
-
- if (existingNuke) {
- if (nukesSentByOtherPlayer.length === 0) {
- existingNuke.remove();
- } else if (
- isMyPlayerTarget &&
- existingNuke.src !== this.nukeRedIconImage.src
- ) {
- existingNuke.src = this.nukeRedIconImage.src;
- } else if (
- !isMyPlayerTarget &&
- existingNuke.src !== this.nukeWhiteIconImage.src
- ) {
- existingNuke.src = this.nukeWhiteIconImage.src;
- }
- } else if (nukesSentByOtherPlayer.length > 0) {
- if (!existingNuke) {
- const icon = isMyPlayerTarget
- ? this.nukeRedIconImage.src
- : this.nukeWhiteIconImage.src;
- iconsDiv.appendChild(this.createIconElement(icon, iconSize, "nuke"));
- }
- }
- // Update all icon sizes
- const icons = iconsDiv.getElementsByTagName("img");
- for (const icon of icons) {
- icon.style.width = `${iconSize}px`;
- icon.style.height = `${iconSize}px`;
}
// Position element with scale
@@ -661,14 +457,12 @@ export class NameLayer implements Layer {
private createIconElement(
src: string,
size: number,
- id: string,
center: boolean = false,
): HTMLImageElement {
const icon = document.createElement("img");
icon.src = src;
icon.style.width = `${size}px`;
icon.style.height = `${size}px`;
- icon.setAttribute("data-icon", id);
icon.setAttribute("dark-mode", this.userSettings.darkMode().toString());
if (center) {
icon.style.position = "absolute";
diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts
index 83505eb28..12fd07f87 100644
--- a/src/client/graphics/layers/PlayerInfoOverlay.ts
+++ b/src/client/graphics/layers/PlayerInfoOverlay.ts
@@ -28,6 +28,7 @@ import {
renderTroops,
translateText,
} from "../../Utils";
+import { getFirstPlacePlayer, getPlayerIcons } from "../PlayerIcons";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { CloseRadialMenuEvent } from "./RadialMenu";
@@ -221,6 +222,33 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
return renderDuration(remainingSeconds);
}
+ private renderPlayerNameIcons(player: PlayerView) {
+ const firstPlace = getFirstPlacePlayer(this.game);
+ const icons = getPlayerIcons({
+ game: this.game,
+ player,
+ // Because we already show the alliance icon next to the alliance expiration timer, we don't need to show it a second time in this render
+ includeAllianceIcon: false,
+ firstPlace,
+ });
+
+ if (icons.length === 0) {
+ return html``;
+ }
+
+ return html`
+ ${icons.map((icon) =>
+ icon.kind === "emoji" && icon.text
+ ? html`${icon.text}`
+ : icon.kind === "image" && icon.src
+ ? html`
`
+ : html``,
+ )}
+ `;
+ }
+
private renderPlayerInfo(player: PlayerView) {
const myPlayer = this.game.myPlayer();
const isFriendly = myPlayer?.isFriendly(player);
@@ -306,7 +334,8 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
src=${"/flags/" + player.cosmetics.flag! + ".svg"}
/>`
: html``}
- ${player.name()}
+ ${player.name()}
+ ${this.renderPlayerNameIcons(player)}