Add black outline to alliance icon for terrain contrast (#4353)

## Problem

The green alliance icon above player names blends into similarly-colored
terrain — most notably irradiated land, which is the same green — making
it hard to spot allied players.

## Fix

Add a configurable dark outline to the alliance status icon, rendered in
the status-icon shader (the icons come from a pre-baked atlas with no
regeneration script, so this is done in-shader rather than by editing
the PNG).

- **Outline**: an alpha dilation gated to the alliance icon (slot 3).
8-direction sampling of the icon's alpha builds a black halo around its
silhouette; interior pixels and all other status icons are untouched.
- **No clipping**: the alliance icon's quad is grown outward into the
atlas cell's existing transparent padding so the halo isn't clipped at
the quad edge. The icon's on-screen size and position are unchanged; 8px
of the cell's 16px mipmap-safety padding is preserved.
- **Drain stays aligned**: the alliance-expiry drain effect's cut line
and faded-icon UVs are remapped into the expanded quad space so the
animation still lines up.
- **Tunable**: width is driven by `name.statusOutlineWidth` in
`render-settings.json` (default 6 texels; 0 disables), with a matching
"Status Outline Width" slider in the debug GUI.

## Testing

`tsc` and `eslint` pass. Verified in-game: the handshake now reads
clearly against irradiated terrain, with the outline rendering fully (no
edge clipping) and the drain animation still aligned.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-06-19 20:17:14 -07:00
committed by evanpelle
co-authored by Claude Opus 4.8
parent ad9380a35d
commit cb0d79ed6d
6 changed files with 79 additions and 7 deletions
@@ -40,6 +40,7 @@ export class StatusIconProgram {
private uStatusRowOffset: WebGLUniformLocation;
private uFadeOwnerID: WebGLUniformLocation;
private uHoverFadeAlpha: WebGLUniformLocation;
private uStatusOutlinePx: WebGLUniformLocation;
constructor(
gl: WebGL2RenderingContext,
@@ -83,6 +84,12 @@ export class StatusIconProgram {
gl.getUniformLocation(this.program, "uStatusPad")!,
sm.pad ?? 0,
);
// Texel size for the outline dilation sampling (static).
gl.uniform2f(
gl.getUniformLocation(this.program, "uStatusTexel")!,
1 / sm.width,
1 / sm.height,
);
// Flash window matches the alliance renewal prompt (10 ticks/sec)
gl.uniform1f(
gl.getUniformLocation(this.program, "uAllianceFlashWindowSec")!,
@@ -111,6 +118,10 @@ export class StatusIconProgram {
this.program,
"uHoverFadeAlpha",
)!;
this.uStatusOutlinePx = gl.getUniformLocation(
this.program,
"uStatusOutlinePx",
)!;
this.loadAtlas();
}
@@ -159,6 +170,7 @@ export class StatusIconProgram {
gl.uniform1f(this.uStatusRowOffset, ns.statusRowOffset);
gl.uniform1f(this.uFadeOwnerID, fadeOwnerID);
gl.uniform1f(this.uHoverFadeAlpha, ns.hoverFadeAlpha);
gl.uniform1f(this.uStatusOutlinePx, ns.statusOutlineWidth);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.playerDataTex);