mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:10:42 +00:00
Flash alliance icon when renewal prompt is active
When an alliance is within the renewal-prompt window, the alliance icon above the player's name now pulses, ramping from 2 Hz to 5 Hz as expiry approaches (same effect as the traitor flash). The flash window is driven by allianceExtensionPromptOffset() — the same Config value that triggers the "renew alliance" prompt in the actionable events display — so the two always stay in sync. The shader only knew the alliance fraction, not absolute time, so computePlayerStatus now also emits allianceRemainingTicks, packed into the free pd7.w slot of the player-data texture.
This commit is contained in:
@@ -90,6 +90,7 @@ export function computePlayerStatus(
|
||||
let embargo = false;
|
||||
let allianceReq = false;
|
||||
let allianceFraction = 0;
|
||||
let allianceRemainingTicks = 0;
|
||||
|
||||
// Nukes: show during replay too, except the nukeTargetsMe flag
|
||||
for (const u of units.values()) {
|
||||
@@ -143,6 +144,7 @@ export function computePlayerStatus(
|
||||
0,
|
||||
Math.min(1, remainingTicks / Math.max(1, opts.allianceDuration)),
|
||||
);
|
||||
allianceRemainingTicks = remainingTicks;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,6 +173,7 @@ export function computePlayerStatus(
|
||||
nukeTargetsMe,
|
||||
traitorRemainingTicks,
|
||||
allianceFraction,
|
||||
allianceRemainingTicks,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,7 +466,13 @@ export class GPURenderer {
|
||||
);
|
||||
this.structureLevelPass = new StructureLevelPass(gl, header, this.settings);
|
||||
this.unitPass = new UnitPass(gl, header, this.paletteTex, this.settings);
|
||||
this.namePass = new NamePass(gl, header, paletteData, this.settings);
|
||||
this.namePass = new NamePass(
|
||||
gl,
|
||||
header,
|
||||
paletteData,
|
||||
this.settings,
|
||||
config,
|
||||
);
|
||||
this.fxPass = new FxPass(gl, header, this.settings, config);
|
||||
this.barPass = new BarPass(gl, header, this.settings, config);
|
||||
this.worldTextPass = new WorldTextPass(gl, this.settings, config);
|
||||
|
||||
@@ -46,6 +46,7 @@ export class StatusIconProgram {
|
||||
atlas: ParsedAtlas,
|
||||
playerDataTex: WebGLTexture,
|
||||
maxPlayers: number,
|
||||
allianceFlashWindowTicks: number,
|
||||
) {
|
||||
this.gl = gl;
|
||||
this.playerDataTex = playerDataTex;
|
||||
@@ -82,6 +83,11 @@ export class StatusIconProgram {
|
||||
gl.getUniformLocation(this.program, "uStatusPad")!,
|
||||
sm.pad ?? 0,
|
||||
);
|
||||
// Flash window matches the alliance renewal prompt (10 ticks/sec)
|
||||
gl.uniform1f(
|
||||
gl.getUniformLocation(this.program, "uAllianceFlashWindowSec")!,
|
||||
allianceFlashWindowTicks / 10,
|
||||
);
|
||||
|
||||
// Dynamic uniform locations
|
||||
this.uCamera = gl.getUniformLocation(this.program, "uCamera")!;
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface PlayerSlot {
|
||||
nukeTargetsMe: boolean;
|
||||
traitorRemainingTicks: number;
|
||||
allianceFraction: number;
|
||||
allianceRemainingTicks: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* - types — shared interfaces + constants
|
||||
*/
|
||||
|
||||
import type { Config } from "../../../../../core/configuration/Config";
|
||||
import type {
|
||||
NameEntry,
|
||||
PlayerState,
|
||||
@@ -117,6 +118,7 @@ export class NamePass {
|
||||
header: RendererConfig,
|
||||
paletteData: Float32Array,
|
||||
settings: RenderSettings,
|
||||
config: Config,
|
||||
) {
|
||||
this.gl = gl;
|
||||
this.settings = settings;
|
||||
@@ -194,6 +196,7 @@ export class NamePass {
|
||||
atlas,
|
||||
this.playerDataTex,
|
||||
this.maxPlayers,
|
||||
config.allianceExtensionPromptOffset(),
|
||||
);
|
||||
this.debugProgram = new DebugProgram(
|
||||
gl,
|
||||
@@ -325,6 +328,7 @@ export class NamePass {
|
||||
nukeTargetsMe: false,
|
||||
traitorRemainingTicks: 0,
|
||||
allianceFraction: 0,
|
||||
allianceRemainingTicks: 0,
|
||||
};
|
||||
this.slots.set(p.id, slot);
|
||||
this.resolveSlotFlag(slot);
|
||||
@@ -441,6 +445,7 @@ export class NamePass {
|
||||
const nukeTargetsMe = sd?.nukeTargetsMe ?? false;
|
||||
const traitorRemainingTicks = sd?.traitorRemainingTicks ?? 0;
|
||||
const allianceFraction = sd?.allianceFraction ?? 0;
|
||||
const allianceRemainingTicks = sd?.allianceRemainingTicks ?? 0;
|
||||
|
||||
if (
|
||||
crown !== slot.crown ||
|
||||
@@ -453,7 +458,8 @@ export class NamePass {
|
||||
nukeActive !== slot.nukeActive ||
|
||||
nukeTargetsMe !== slot.nukeTargetsMe ||
|
||||
traitorRemainingTicks !== slot.traitorRemainingTicks ||
|
||||
allianceFraction !== slot.allianceFraction
|
||||
allianceFraction !== slot.allianceFraction ||
|
||||
allianceRemainingTicks !== slot.allianceRemainingTicks
|
||||
) {
|
||||
slot.crown = crown;
|
||||
slot.traitor = traitor;
|
||||
@@ -466,6 +472,7 @@ export class NamePass {
|
||||
slot.nukeTargetsMe = nukeTargetsMe;
|
||||
slot.traitorRemainingTicks = traitorRemainingTicks;
|
||||
slot.allianceFraction = allianceFraction;
|
||||
slot.allianceRemainingTicks = allianceRemainingTicks;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
@@ -560,11 +567,11 @@ export class NamePass {
|
||||
d[off + 26] = slot.embargo ? 1.0 : 0.0;
|
||||
d[off + 27] = slot.nukeActive ? 1.0 : 0.0;
|
||||
|
||||
// Column 7: nukeTargetsMe, traitorRemainingTicks, allianceFraction, [free]
|
||||
// Column 7: nukeTargetsMe, traitorRemainingTicks, allianceFraction, allianceRemainingTicks
|
||||
d[off + 28] = slot.nukeTargetsMe ? 1.0 : 0.0;
|
||||
d[off + 29] = slot.traitorRemainingTicks;
|
||||
d[off + 30] = slot.allianceFraction;
|
||||
d[off + 31] = 0;
|
||||
d[off + 31] = slot.allianceRemainingTicks;
|
||||
|
||||
this.playerDataDirty = true;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ uniform float uStatusRowOffset; // row Y offset (multiples of uFontBase * nameW
|
||||
|
||||
uniform float uFadeOwnerID; // smallID of player whose name plate the cursor is over (0 = none)
|
||||
uniform float uHoverFadeAlpha; // alpha multiplier applied to that player's name plate
|
||||
uniform float uAllianceFlashWindowSec; // seconds before expiry the alliance icon flashes (= renewal prompt offset)
|
||||
|
||||
out vec2 vUV;
|
||||
out vec2 vLocalUV; // 0..1 within the icon cell
|
||||
@@ -90,7 +91,7 @@ void main() {
|
||||
vec4 pd0 = texelFetch(uPlayerData, ivec2(0, playerIdx), 0); // srcX, srcY, srcScale, startTime
|
||||
vec4 pd1 = texelFetch(uPlayerData, ivec2(1, playerIdx), 0); // tgtX, tgtY, tgtScale, alive
|
||||
vec4 pd4 = texelFetch(uPlayerData, ivec2(4, playerIdx), 0); // flagIdx, emojiIdx, smallID, [free]
|
||||
vec4 pd7 = texelFetch(uPlayerData, ivec2(7, playerIdx), 0); // nukeTargetsMe, traitorRemainingTicks, allianceFraction, [free]
|
||||
vec4 pd7 = texelFetch(uPlayerData, ivec2(7, playerIdx), 0); // nukeTargetsMe, traitorRemainingTicks, allianceFraction, allianceRemainingTicks
|
||||
|
||||
// Early out: dead player OR emoji is active
|
||||
if (pd1.w <= 0.0 || pd4.y >= 0.0) {
|
||||
@@ -226,5 +227,18 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Alliance expiry flash: slot 3 = alliance icon
|
||||
// Window matches the renewal prompt offset so the icon flashes exactly
|
||||
// while the prompt is up. Same pulse as the traitor flash (2 Hz → 5 Hz).
|
||||
if (iconSlot == 3) {
|
||||
float window = uAllianceFlashWindowSec;
|
||||
float remainingSec = pd7.w / 10.0; // ticks → seconds
|
||||
if (window > 0.0 && remainingSec <= window && remainingSec > 0.0) {
|
||||
float elapsed = window - remainingSec;
|
||||
float phase = uTime * 2.0 + elapsed * elapsed * (1.5 / window);
|
||||
vFlashAlpha = 0.3 + 0.7 * (0.5 + 0.5 * cos(phase * 6.2832));
|
||||
}
|
||||
}
|
||||
|
||||
vDiscard = 0;
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ export interface PlayerStatusData {
|
||||
nukeTargetsMe: boolean;
|
||||
traitorRemainingTicks: number;
|
||||
allianceFraction: number;
|
||||
allianceRemainingTicks: number;
|
||||
}
|
||||
|
||||
/** Ghost structure preview data for build-mode visualization. */
|
||||
|
||||
@@ -333,4 +333,30 @@ describe("computePlayerStatus — live mode (localPlayerSmallID set)", () => {
|
||||
expect(status.get(2)?.allianceReq).toBe(false);
|
||||
expect(status.get(2)?.allianceFraction).toBe(0);
|
||||
});
|
||||
|
||||
it("allianceFraction and allianceRemainingTicks come from the alliance expiry", () => {
|
||||
const players = playersMap(
|
||||
ps({ smallID: 1, allies: [2] }),
|
||||
ps({
|
||||
smallID: 2,
|
||||
alliances: [
|
||||
{
|
||||
id: 1,
|
||||
other: "me",
|
||||
createdAt: 100,
|
||||
expiresAt: 700,
|
||||
hasExtensionRequest: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const status = computePlayerStatus(players, unitsMap(), {
|
||||
localPlayerSmallID: 1,
|
||||
localPlayerID: "me",
|
||||
tick: 400,
|
||||
allianceDuration: 600,
|
||||
});
|
||||
expect(status.get(2)?.allianceFraction).toBe(0.5);
|
||||
expect(status.get(2)?.allianceRemainingTicks).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user