mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-23 20:20:51 +00:00
feat(client): highlight small players with a pulsing glow (#4525)
## Description: Adds a **"Highlight small players"** client setting (in the in-game Settings menu). When it's on, human players holding **0.2% or less of the map** get a soft red glow that breathes (a pulsing aura) around their territory, starting **one minute into the game**. This makes near-eliminated players easy to spot on a busy map, even when their territory is scattered in fragments or sitting under structures. How it works: - **Purely client-side**, no simulation or determinism impact. `SmallPlayerGlowPass` renders a tile-space bloom: extract a sub-tile mask of the small players' tiles, run a separable Gaussian blur, then composite the soft aura over the map additively. It's camera-independent (no shimmer when panning/zooming), so scattered tiles blur into one clean halo. The aura breathes: its intensity fades fully to 0 at the trough for clear contrast. - The set is recomputed each tick in `WebGLFrameBuilder`: alive human players with `tilesOwned / (landTiles - fallout) <= 0.002` (0.2%), suppressed during the spawn phase and the first minute. - Backed by a persisted `UserSettings` flag, toggleable live from the in-game Settings modal (with a new, distinct icon). - Drawn after the structure passes so buildings can't hide it. - Mirrors the existing FalloutBloom pipeline and reuses the shared blur shader and render-target helpers rather than duplicating them. Tunable via `render-settings.json` (`smallPlayerGlow`: color / alpha / pulseSpeed). UI (glow fades in and out): <img width="474" height="334" alt="image" src="https://github.com/user-attachments/assets/2b94f292-2cbb-43d3-82fb-f274d1afdedf" /> <img width="976" height="400" alt="image" src="https://github.com/user-attachments/assets/f1b972d7-e046-4f7a-ab43-da1eb758c7b5" /> ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: zixer._
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
} from "../core/CosmeticSchemas";
|
||||
import { decodePatternData } from "../core/PatternDecoder";
|
||||
import { PlayerType } from "../core/game/Game";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import { getCachedCosmetics } from "./Cosmetics";
|
||||
import { uploadFrameData } from "./render/frame/Upload";
|
||||
// Type-only: a value import would pull GPURenderer and its `.glsl?raw` shader
|
||||
@@ -40,6 +41,14 @@ import type { GameView } from "./view";
|
||||
|
||||
const PALETTE_SIZE = 4096;
|
||||
|
||||
// A human player counts as "small" (and glows) at or below this fraction of the
|
||||
// map; the glow is suppressed for a grace window after the game starts.
|
||||
const SMALL_PLAYER_MAX_MAP_FRACTION = 0.002; // 0.2%
|
||||
const SMALL_PLAYER_GLOW_GRACE_SECONDS = 60;
|
||||
// The set is a visual aid, not tick-critical, so rescan ~once a second
|
||||
// (10 ticks) instead of every tick.
|
||||
const SMALL_PLAYER_GLOW_RESCAN_TICKS = 10;
|
||||
|
||||
// The effect-palette block order: index = block (rows block·MAX_TRAIL_COLORS …).
|
||||
// trail.frag.glsl picks its block from the trail tile's nuke bit — block 0 =
|
||||
// transportShipTrail (nuke bit 0), block 1 = nukeTrail (nuke bit 1, set by
|
||||
@@ -187,12 +196,17 @@ export class WebGLFrameBuilder {
|
||||
this.view.refreshNames(displayNames);
|
||||
}
|
||||
|
||||
private readonly highlightSetBuf = new Uint8Array(PALETTE_SIZE);
|
||||
private readonly userSettings = new UserSettings();
|
||||
private glowRescanTick = 0;
|
||||
|
||||
update(gameView: GameView): void {
|
||||
this.syncPlayers(gameView);
|
||||
this.syncPlayerEffects(gameView);
|
||||
this.syncPlayerSpawns(gameView);
|
||||
this.syncLocalPlayer(gameView);
|
||||
this.syncSpawnOverlay(gameView);
|
||||
this.syncSmallPlayerGlow(gameView);
|
||||
this.syncTerrainDeltas(gameView);
|
||||
this.resolveDeadUnitExplosions(gameView);
|
||||
uploadFrameData(this.view, gameView.frameData());
|
||||
@@ -328,6 +342,48 @@ export class WebGLFrameBuilder {
|
||||
this.view.updateSpawnOverlay(true, centers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Small-player glow: when the client "Highlight small players" setting is on,
|
||||
* collect the alive human players holding <=0.2% of the map and push their
|
||||
* smallIDs so the glow pass radiates around their territory. Skips the first
|
||||
* minute of play so everyone's tiny starting territory doesn't glow.
|
||||
* Client-only view — toggle it live in the settings.
|
||||
*/
|
||||
private syncSmallPlayerGlow(gameView: GameView): void {
|
||||
if (
|
||||
!this.userSettings.highlightSmallPlayers() ||
|
||||
gameView.inSpawnPhase() ||
|
||||
gameView.elapsedGameSeconds() < SMALL_PLAYER_GLOW_GRACE_SECONDS
|
||||
) {
|
||||
this.view.updateSmallPlayerGlow(null);
|
||||
return;
|
||||
}
|
||||
// Throttle the per-player scan + upload; the glow keeps rendering the last
|
||||
// set between rescans. The off/spawn/grace checks above run every tick, so
|
||||
// toggling off takes effect on the next tick (deferred while the game is
|
||||
// paused, since ticks stop; it clears on unpause).
|
||||
if (this.glowRescanTick++ % SMALL_PLAYER_GLOW_RESCAN_TICKS !== 0) return;
|
||||
// "% of the map" uses the same denominator the leaderboard/win-check use.
|
||||
const denom = gameView.numLandTiles() - gameView.numTilesWithFallout();
|
||||
if (denom <= 0) {
|
||||
this.view.updateSmallPlayerGlow(null);
|
||||
return;
|
||||
}
|
||||
const set = this.highlightSetBuf;
|
||||
set.fill(0);
|
||||
let any = false;
|
||||
for (const p of gameView.players()) {
|
||||
if (!p.isPlayer() || p.type() !== PlayerType.Human || !p.isAlive()) {
|
||||
continue;
|
||||
}
|
||||
if (p.numTilesOwned() / denom <= SMALL_PLAYER_MAX_MAP_FRACTION) {
|
||||
set[p.smallID()] = 1;
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
this.view.updateSmallPlayerGlow(any ? set : null);
|
||||
}
|
||||
|
||||
private syncPlayers(gameView: GameView): void {
|
||||
if (!this.skinsInitialized) {
|
||||
this.skinsInitialized = true;
|
||||
|
||||
Reference in New Issue
Block a user