Files
OpenFrontIO/tests/UserSettings.test.ts
T
Zixer1 f5ef306d99 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._
2026-07-08 12:30:37 -07:00

89 lines
3.2 KiB
TypeScript

import { EFFECTS_KEY, UserSettings } from "../src/core/game/UserSettings";
describe("UserSettings effect selection", () => {
beforeEach(() => {
localStorage.clear();
// UserSettings keeps a static in-memory cache; reset it too so each test
// reads fresh from the (cleared) localStorage.
(
UserSettings as unknown as { cache: Map<string, string | null> }
).cache.clear();
});
it("sets and reads a per-effectType selection", () => {
const s = new UserSettings();
s.setSelectedEffectName("transportShipTrail", "spectrum");
expect(s.getSelectedEffectName("transportShipTrail")).toBe("spectrum");
});
it("returns null when nothing is selected", () => {
expect(
new UserSettings().getSelectedEffectName("transportShipTrail"),
).toBeNull();
});
it("clearing the last selection removes the storage key", () => {
const s = new UserSettings();
s.setSelectedEffectName("transportShipTrail", "spectrum");
s.setSelectedEffectName("transportShipTrail", undefined);
expect(s.getSelectedEffectName("transportShipTrail")).toBeNull();
expect(localStorage.getItem(EFFECTS_KEY)).toBeNull();
});
it("clearing one effectType leaves other types intact", () => {
const s = new UserSettings();
// Seed two types directly (only one real effectType exists today).
localStorage.setItem(
EFFECTS_KEY,
JSON.stringify({ transportShipTrail: "spectrum", future: "x" }),
);
s.setSelectedEffectName("transportShipTrail", undefined);
expect(s.getSelectedEffects()).toEqual({ future: "x" });
});
it("returns an empty map for a corrupt blob", () => {
localStorage.setItem(EFFECTS_KEY, "not json");
expect(new UserSettings().getSelectedEffects()).toEqual({});
});
it("keeps per-nukeType nuke-explosion slots independent", () => {
const s = new UserSettings();
s.setSelectedEffectName("atom", "atom_boom");
s.setSelectedEffectName("hydro", "hydro_boom");
expect(s.getSelectedEffectName("atom")).toBe("atom_boom");
expect(s.getSelectedEffectName("hydro")).toBe("hydro_boom");
// Clearing one bomb's slot leaves the others intact.
s.setSelectedEffectName("atom", undefined);
expect(s.getSelectedEffectName("atom")).toBeNull();
expect(s.getSelectedEffectName("hydro")).toBe("hydro_boom");
});
});
describe("UserSettings highlight small players", () => {
beforeEach(() => {
localStorage.clear();
(
UserSettings as unknown as { cache: Map<string, string | null> }
).cache.clear();
});
it("defaults to off", () => {
expect(new UserSettings().highlightSmallPlayers()).toBe(false);
});
it("toggles on and off", () => {
const s = new UserSettings();
s.toggleHighlightSmallPlayers();
expect(s.highlightSmallPlayers()).toBe(true);
s.toggleHighlightSmallPlayers();
expect(s.highlightSmallPlayers()).toBe(false);
});
it("shares state across instances via the static cache", () => {
// The settings modal and the renderer's frame builder each hold their own
// UserSettings; a toggle in one must be visible to the other.
new UserSettings().toggleHighlightSmallPlayers();
expect(new UserSettings().highlightSmallPlayers()).toBe(true);
});
});