mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 19:06:39 +00:00
Added NameLayer-Icons to PlayerInfoOverlay ✨ (#2446)
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. <img width="215" height="355" alt="Screenshot 2025-11-14 024435" src="https://github.com/user-attachments/assets/2e581ef9-0330-4c9d-9c52-5f943a58e64b" /> <img width="203" height="337" alt="Screenshot 2025-11-14 024731" src="https://github.com/user-attachments/assets/0c2bf278-b8ca-43c2-b466-ea7a83577b25" /> <img width="193" height="288" alt="Screenshot 2025-11-14 024639" src="https://github.com/user-attachments/assets/be114bc6-f3a8-4b8d-b267-025587c9eafe" /> 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 <evanpelle@gmail.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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<string, HTMLImageElement> = new Map(); // Track icon elements
|
||||
public icons: Map<PlayerIconId, HTMLElement> = 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<PlayerView> = 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";
|
||||
|
||||
@@ -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`<span class="flex items-center gap-1 ml-1 shrink-0">
|
||||
${icons.map((icon) =>
|
||||
icon.kind === "emoji" && icon.text
|
||||
? html`<span class="text-sm shrink-0" translate="no"
|
||||
>${icon.text}</span
|
||||
>`
|
||||
: icon.kind === "image" && icon.src
|
||||
? html`<img src=${icon.src} alt="" class="w-4 h-4 shrink-0" />`
|
||||
: html``,
|
||||
)}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
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()}
|
||||
<span>${player.name()}</span>
|
||||
${this.renderPlayerNameIcons(player)}
|
||||
</button>
|
||||
|
||||
<!-- Collapsible section -->
|
||||
|
||||
Reference in New Issue
Block a user