mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-13 12:44:01 +00:00
Fading handshake (#2474)
## Description: Add dynamic alliance icon with time-based fill and extension request indicator - Implement bottom-up green fill on alliance icon proportional to remaining time - Use AllianceIconFaded.svg as base layer with green overlay clipped from top - Add 20-82.40% clip range to account for icon vertical offset ## 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 <img width="1132" height="631" alt="Screenshot 2025-11-18 205205" src="https://github.com/user-attachments/assets/4af71ddc-f847-4460-9046-167275efc773" /> <img width="1387" height="792" alt="Screenshot 2025-11-18 205532" src="https://github.com/user-attachments/assets/9dd0e018-323f-4de1-bae8-2633c09fe867" /> ## Please put your Discord username so you can be contacted if a bug or regression is found: hauke4707 --------- Co-authored-by: Evan <evanpelle@gmail.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import allianceIcon from "../../../resources/images/AllianceIcon.svg";
|
||||
import allianceIconFaded from "../../../resources/images/AllianceIconFaded.svg";
|
||||
import allianceRequestBlackIcon from "../../../resources/images/AllianceRequestBlackIcon.svg";
|
||||
import allianceRequestWhiteIcon from "../../../resources/images/AllianceRequestWhiteIcon.svg";
|
||||
import crownIcon from "../../../resources/images/CrownIcon.svg";
|
||||
@@ -7,6 +8,7 @@ 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 questionMarkIcon from "../../../resources/images/QuestionMarkIcon.svg";
|
||||
import targetIcon from "../../../resources/images/TargetIcon.svg";
|
||||
import traitorIcon from "../../../resources/images/TraitorIcon.svg";
|
||||
import { AllPlayers, nukeTypes } from "../../core/game/Game";
|
||||
@@ -152,3 +154,69 @@ export function getPlayerIcons(
|
||||
|
||||
return icons;
|
||||
}
|
||||
|
||||
export function createAllianceProgressIcon(
|
||||
size: number,
|
||||
fraction: number,
|
||||
hasExtensionRequest: boolean,
|
||||
darkMode: boolean,
|
||||
): HTMLDivElement {
|
||||
// Wrapper
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.setAttribute("data-icon", "alliance");
|
||||
wrapper.setAttribute("dark-mode", darkMode.toString());
|
||||
wrapper.style.position = "relative";
|
||||
wrapper.style.width = `${size}px`;
|
||||
wrapper.style.height = `${size}px`;
|
||||
wrapper.style.display = "inline-block";
|
||||
|
||||
// Base faded icon (full)
|
||||
const base = document.createElement("img");
|
||||
base.src = allianceIconFaded;
|
||||
base.style.width = `${size}px`;
|
||||
base.style.height = `${size}px`;
|
||||
base.style.display = "block";
|
||||
base.setAttribute("dark-mode", darkMode.toString());
|
||||
wrapper.appendChild(base);
|
||||
|
||||
// Overlay container for green portion, clipped from the top via clip-path
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "alliance-progress-overlay";
|
||||
overlay.style.position = "absolute";
|
||||
overlay.style.left = "0";
|
||||
overlay.style.top = "0";
|
||||
overlay.style.width = "100%";
|
||||
overlay.style.height = "100%";
|
||||
overlay.style.clipPath = computeAllianceClipPath(fraction);
|
||||
|
||||
const colored = document.createElement("img");
|
||||
colored.src = allianceIcon; // green icon
|
||||
colored.style.width = `${size}px`;
|
||||
colored.style.height = `${size}px`;
|
||||
colored.style.display = "block";
|
||||
colored.setAttribute("dark-mode", darkMode.toString());
|
||||
overlay.appendChild(colored);
|
||||
|
||||
wrapper.appendChild(overlay);
|
||||
|
||||
// Question mark overlay (shown when there's a pending extension request)
|
||||
const questionMark = document.createElement("img");
|
||||
questionMark.className = "alliance-question-mark";
|
||||
questionMark.src = questionMarkIcon;
|
||||
questionMark.style.position = "absolute";
|
||||
questionMark.style.left = "0";
|
||||
questionMark.style.top = "0";
|
||||
questionMark.style.width = `${size}px`;
|
||||
questionMark.style.height = `${size}px`;
|
||||
questionMark.style.display = hasExtensionRequest ? "block" : "none";
|
||||
questionMark.style.pointerEvents = "none";
|
||||
questionMark.setAttribute("dark-mode", darkMode.toString());
|
||||
wrapper.appendChild(questionMark);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
export function computeAllianceClipPath(fraction: number): string {
|
||||
const topCut = 20 + (1 - fraction) * 80 * 0.78; // min 20%, max 82.40%
|
||||
return `inset(${topCut.toFixed(2)}% -2px 0 -2px)`;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { AlternateViewEvent } from "../../InputHandler";
|
||||
import { createCanvas, renderNumber, renderTroops } from "../../Utils";
|
||||
import {
|
||||
computeAllianceClipPath,
|
||||
createAllianceProgressIcon,
|
||||
getFirstPlacePlayer,
|
||||
getPlayerIcons,
|
||||
PlayerIconId,
|
||||
@@ -51,6 +53,8 @@ export class NameLayer implements Layer {
|
||||
) {
|
||||
this.shieldIconImage = new Image();
|
||||
this.shieldIconImage.src = shieldIcon;
|
||||
this.shieldIconImage = new Image();
|
||||
this.shieldIconImage.src = shieldIcon;
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
@@ -399,6 +403,69 @@ export class NameLayer implements Layer {
|
||||
emojiDiv.textContent = icon.text;
|
||||
emojiDiv.style.fontSize = `${iconSize}px`;
|
||||
} else if (icon.kind === "image" && icon.src) {
|
||||
// Special handling for alliance icon with progress indicator
|
||||
if (icon.id === "alliance") {
|
||||
let allianceWrapper = render.icons.get(icon.id) as
|
||||
| HTMLDivElement
|
||||
| undefined;
|
||||
|
||||
const myPlayer = this.game.myPlayer();
|
||||
const allianceView = myPlayer
|
||||
?.alliances()
|
||||
.find((a) => a.other === render.player.id());
|
||||
|
||||
let fraction = 0;
|
||||
let hasExtensionRequest = false;
|
||||
if (allianceView) {
|
||||
const remaining = Math.max(
|
||||
0,
|
||||
allianceView.expiresAt - this.game.ticks(),
|
||||
);
|
||||
const duration = Math.max(1, this.game.config().allianceDuration());
|
||||
fraction = Math.max(0, Math.min(1, remaining / duration));
|
||||
hasExtensionRequest = allianceView.hasExtensionRequest;
|
||||
}
|
||||
|
||||
if (!allianceWrapper) {
|
||||
allianceWrapper = createAllianceProgressIcon(
|
||||
iconSize,
|
||||
fraction,
|
||||
hasExtensionRequest,
|
||||
this.userSettings.darkMode(),
|
||||
);
|
||||
iconsDiv.appendChild(allianceWrapper);
|
||||
render.icons.set(icon.id, allianceWrapper);
|
||||
} else {
|
||||
// Update existing alliance icon
|
||||
allianceWrapper.style.width = `${iconSize}px`;
|
||||
allianceWrapper.style.height = `${iconSize}px`;
|
||||
|
||||
const overlay = allianceWrapper.querySelector(
|
||||
".alliance-progress-overlay",
|
||||
) as HTMLDivElement | null;
|
||||
if (overlay) {
|
||||
overlay.style.clipPath = computeAllianceClipPath(fraction);
|
||||
}
|
||||
|
||||
const questionMark = allianceWrapper.querySelector(
|
||||
".alliance-question-mark",
|
||||
) as HTMLImageElement | null;
|
||||
if (questionMark) {
|
||||
questionMark.style.display = hasExtensionRequest
|
||||
? "block"
|
||||
: "none";
|
||||
}
|
||||
|
||||
// Update inner image sizes
|
||||
const imgs = allianceWrapper.getElementsByTagName("img");
|
||||
for (const img of imgs) {
|
||||
img.style.width = `${iconSize}px`;
|
||||
img.style.height = `${iconSize}px`;
|
||||
}
|
||||
}
|
||||
continue; // Skip regular image handling
|
||||
}
|
||||
|
||||
let imgElement = render.icons.get(icon.id) as
|
||||
| HTMLImageElement
|
||||
| undefined;
|
||||
|
||||
@@ -179,6 +179,7 @@ export interface AllianceView {
|
||||
other: PlayerID;
|
||||
createdAt: Tick;
|
||||
expiresAt: Tick;
|
||||
hasExtensionRequest: boolean;
|
||||
}
|
||||
|
||||
export interface AllianceRequestUpdate {
|
||||
|
||||
@@ -171,6 +171,10 @@ export class PlayerImpl implements Player {
|
||||
other: a.other(this).id(),
|
||||
createdAt: a.createdAt(),
|
||||
expiresAt: a.expiresAt(),
|
||||
hasExtensionRequest:
|
||||
a.expiresAt() <=
|
||||
this.mg.ticks() +
|
||||
this.mg.config().allianceExtensionPromptOffset(),
|
||||
}) satisfies AllianceView,
|
||||
),
|
||||
hasSpawned: this.hasSpawned(),
|
||||
|
||||
Reference in New Issue
Block a user