Render custom crown cosmetics above player names (#4619)

## What

Renders each player's crown cosmetic in-game (follow-up to #4618). The
crown **skins the first-place crown**: when the tile leader owns a crown
cosmetic, their image replaces the default crown icon above their name —
on the map name plate and in the hover overlay. Players without the
cosmetic keep the default crown.

The image pipeline follows custom flags: the server-resolved crown URL
rides `PlayerStatic` into the GPU name pass and loads on demand into a
runtime `TEXTURE_2D_ARRAY` (layers deduped by URL, square 128×128
cells).

## Changes

**GL name pass**
- `FlagAtlasArray` cell size is now a constructor parameter; a second
instance holds crown images (square cells, no letterbox margins)
- Per-player data texture widens 8 → 9 columns (`PLAYER_DATA_COLS`);
column 8 carries the crown atlas layer
- `status-icon.vert/frag.glsl`: status slot 0 (first-place crown)
samples the crown atlas instead of the status atlas when the player has
a crown cosmetic — same size and position as the default crown
- `WebGLFrameBuilder.syncPlayers` resolves `cosmetics.crown.url` →
`PlayerStatic.crown`

**DOM**
- `getPlayerIcons` swaps the first-place crown icon src for the cosmetic
image (hover overlay)

**Dev hook**: `localStorage.setItem("dev-crown", "<image url>")` forces
a crown in singleplayer (mirrors `dev-pattern`)

## Verified

Drove a headless solo game (real-GPU WebGL via ANGLE Metal) with the
dev-crown hook, expanded until first place — the cosmetic replaces the
default crown above the name on the map and in the hover overlay
(default `CrownIcon.svg` absent, cosmetic present).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-07-15 15:24:25 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 8f1bbdd20e
commit 766fda991a
14 changed files with 227 additions and 52 deletions
+6
View File
@@ -302,6 +302,12 @@ export class UserSettings {
return data.startsWith(skinPrefix) ? data.slice(skinPrefix.length) : null;
}
// For development only. Crown image URL for testing, set in the console
// manually (localStorage "dev-crown"), like getDevOnlyPattern.
getDevOnlyCrown(): string | undefined {
return localStorage.getItem("dev-crown") ?? undefined;
}
/** Returns the selected crown name, or null if none is selected. */
getSelectedCrownName(): string | null {
return this.getCached(CROWN_KEY);