mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-23 19:20:18 +00:00
a2321eb824b53ed908d41a33300f0fbcbed44fed
28
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d07f7d46dd |
feat(highlight): replace the small-player highlight toggle with a strength slider (#4588)
## Description: Replaces the "Highlight small players" on/off toggle with a **0-500% Strength slider** controlling the glow's **width** (0 = off, 100% = the previous default). - `UserSettings`: `highlightSmallPlayers()`/toggle → `highlightGlowStrength()`/set, float-backed, clamped to [0, 5]. - `SmallPlayerGlowPass` takes the strength via a setter (pushed by `ClientGameRunner` on the settings-changed event), so it stays a pure consumer and the slider still updates live while the settings modal has the sim paused. Width = a non-linear blur-iteration count; intensity compensated so wider stays about as bright. - `MapRenderer` persists the strength across a WebGL context restore. - `SettingsModal`: the toggle row becomes a range input. Behavior note: with the toggle gone the glow **defaults ON at 100%** (old toggle defaulted OFF); can default to 0 (opt-in) if preferred. ## 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 |
||
|
|
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._ |
||
|
|
be77ab4fc9 |
feat: structures cosmetic effect (hover-shown gradient/transition recolor) (#4492)
## Description: Adds a new `structures` cosmetic effect type: an equippable effect that recolors the owner's structure icons (City, Port, Factory, Defense Post, SAM Launcher, Missile Silo) with gradient or transition color styles. The effect is **shown while the owner's territory is hovered** — structures otherwise keep their normal player colors, so the map stays readable. **Cosmetics / selection** - `StructuresEffectAttributesSchema` (`CosmeticSchemas.ts`): its own discriminated union (`gradient` / `transition`) — structurally identical to the trail attributes today, but structures aren't trails, so it's a separate schema free to diverge. - Slot = the effectType itself: `effectTypeForSlot` is generalized to map any non-nukeExplosion effect type to itself, so server privilege checks (`Privilege.ts`), client selection, and persistence all work with no per-type code. - Effects tab, Default tile, and the store preview (shared color swatch) come from `EFFECT_TYPES`; the only UI addition is the `effects.type.structures` label in `en.json`. **Rendering** - The shared per-player effect palette grows from 2 to 3 blocks (`EFFECT_PALETTE_BLOCKS`; structures = block 2, pinned by a build-breaking guard). `syncPlayerEffects` resolves the `structures` selection through the same `writeEffectEntry` used by trails. - `StructurePass` binds the effect texture plus `uTime` and `uHoverOwner` (fed from the existing `HoverHighlightController` → `setHighlightOwner` path, now forwarded to the pass). - `structure.frag.glsl` recolors the **fill only** — the border keeps the player color for ownership legibility; alt view and construction gray bypass the effect entirely. - Style semantics: - `gradient` — the palette spans each icon's diagonal once (a visible gradient across the shape), sliding one full cycle every `colorSize · 4 · count / movementSpeed` seconds (the trail-equivalent pace; world-space banding like the trail's would put a whole icon inside one band and read as a flat color) - `transition` — the whole icon is one color at a time, cross-fading at `frequency` colors/s - Glyph contrast: the inner icon's black/white decision is now a smooth luminance fade (`smoothstep(0.25, 0.45)`) instead of a hard flip at 0.25, so animated fills cross-fade the glyph instead of snapping it between black and white. ## Please complete the following: - [ ] 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b6317964a7 |
feat: sparkles nuke-explosion visual type (#4490)
## Description: Follow-up to #4485: adds a second nuke-explosion visual, `"sparkles"` — a firework burst of twinkling glints that start at the detonation point and ride outward with the expanding front, reaching the cosmetic's full `size` at fade-out. **Schema (`CosmeticSchemas.ts`)** - `NukeExplosionAttributesSchema` is now a discriminated union on `type` (`"shockwave" | "sparkles"`), matching `TrailEffectAttributesSchema`. Old clients drop sparkles entries via `lenientRecord` and render the default ring. - The sparkles member adds `density` (required, positive) — roughly the total number of glints in the burst. - Literal attribute semantics, consistent with shockwave: - `size` — final burst width (diameter) in world tiles at fade-out - `speed` — tiles/s the width grows; duration = size / speed, clamped 0.1–15 s - `thickness` — **average** sparkle size in tiles; each glint hash-varies ±50% around it - `density` — approximate glint count; renderer clamps to 2–5000 - `colors` + `transitionSpeed` — shared palette-cycle semantics, with a hashed per-glint palette offset on top **Rendering** - `NukeExplosionRenderParams` now carries the visual type through to the pass as a matching TS union (previously any cosmetic was hardwired to the EMP style — this closes that gap for future visuals). - Sparkles are style 2 in the same `FxShockwavePass` instance stream: one new float (grid cell pitch, derived CPU-side from density), no other layout changes. - Fragment shader: one hashed glint per rotated front-normalized grid cell (jittered, cell-confined so each fragment samples only its own cell, ~1/3 dropout for organic scatter), hashed birth stagger. Glints are **fully opaque** — twinkle modulates color brightness, not alpha — holding full opacity through life and fading only over the last quarter. - SAM interceptions, the classic ring, and EMP shockwaves are unchanged. **Store / selection** - New `<sparkles-swatch>` preview (burst scales from center, density-scaled dot count, size-varied dots, palette cycling), branched in `CosmeticButton` by `attributes.type`. **Verification** - Schema tests incl. the real `rgb_nuke_sparkles` catalog entry, missing/non-positive `density` rejection. - Verified in-game via headless Chromium: size-250 RGB burst renders opaque red/white/blue glints expanding from the detonation point; sparse (40) vs dense (400) density comparison; no page errors. ## Please complete the following: - [ ] 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6ff202afb5 |
feat: nuke-explosion cosmetic effects (per-bomb-type shockwave customization) (#4485)
## Description: Adds a new `nukeExplosion` cosmetic effect type: when a bomb detonates, every client renders the shockwave in the firing player's equipped effect for that bomb type. **Cosmetics / selection** - New `nukeExplosion` effect schema (`CosmeticSchemas.ts`) with per-bomb selection slots — a slot is the effectType for trails and the `nukeType` for explosions (`atom` / `hydro` / `mirvWarhead`), so players can equip a distinct explosion per bomb type. - Slot resolution + validation is one shared helper (`findEffectForSlot`) used by client selection, server privilege checks (`Privilege.ts`), and the renderer; a compile-time guard keeps the nukeType and effectType slot namespaces disjoint. - Effects picker gains an Atom / Hydrogen / MIRV sub-tab bar when browsing nuke explosions; selections persist per slot in UserSettings and are validated/dropped like other cosmetics. **Rendering** - `WebGLFrameBuilder` resolves each dead nuke's owner cosmetic onto the dead-unit event; `FxShockwavePass` renders an EMP-style procedural ring (jagged crackling front, rotating lightning arcs, inner energy fill) from per-instance attributes. SAM interceptions and players with no cosmetic keep the classic white ring. - Catalog attributes have literal units: - `size` — final ring width (diameter) in world tiles at fade-out, absolute — independent of the bomb's blast radius - `speed` — tiles/s the width grows; duration = size / speed, clamped to 0.1–15 s - `thickness` (required) — ring band thickness in tiles, constant while the ring expands - `colors` — palette of up to 4 colors, cycled at `transitionSpeed` steps/s (0 = static, negative = reverse; same semantics as trail transitions) - The shockwave quad is sized radius + thickness so the absolute-width band isn't clipped into a box while the ring is young. ## Please complete the following: - [ ] 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2794ab1270 |
feat: nuke-trail cosmetic effect + tabbed effects picker (#4466)
## What Adds a **`nukeTrail`** cosmetic effectType alongside `transportShipTrail`, so nukes leave a trail colored by their own gradient/transition effect — independent of the boat-trail effect (a player can run both). Also reorganizes the effects picker and store into per-effectType **tabs**. ## Rendering Boat and nuke trails are stamped into **one** trail texture keyed only by owner, so independent coloring needs a per-tile unit-class signal: - **Trail texture** `R8UI` → `R16UI`: texel = `ownerID(bits 0-11) | nukeBit(bit 12)`. `TrailManager` stamps the bit (and preserves it when repainting on unit death); the `Uint8Array`→`Uint16Array` ripple + `UNSIGNED_SHORT` uploads flow through `GpuResources`, `TrailPass`, `Upload`, `MapRenderer`, `Renderer`, `FrameData`. - **Effect texture** widened to two stacked blocks (`TRAIL_EFFECT_BLOCKS`): rows 0–7 = transportShipTrail, rows 8–15 = nukeTrail. `writeEffectEntry(…, rowBase)`; `syncPlayerEffects` resolves both effectTypes. - **Shader** masks the owner, derives `rowBase` from the nuke bit, offsets every row, and reuses the gradient/transition decode. - Bonus: the 12-bit owner mask lifts the old `R8UI` >255-player truncation. ## Schema / server / UI - Shared attributes schema renamed `TransportShipTrail…` → **`TrailEffectAttributesSchema`** (it's no longer ship-specific); `NukeTrailEffectSchema` added to `EffectSchema` + `CosmeticsSchema.effects`. `EFFECT_TYPES = [transportShipTrail, nukeTrail]`. - Server `Privilege`, selection, and the picker grid all iterate `EFFECT_TYPES`, so they handle the new type with **no per-type code**. - **Tabs:** the selection modal uses one tab per effectType (`BaseModal`'s native tabs); the **store's** EFFECTS panel gets an internal sub-tab bar (its top-level PACKS/EFFECTS tabs can't nest). Tabs are always present, so a type you own entirely still appears as an empty tab (previously the boat-trail section vanished from the store when you owned everything). ## Review A 3-angle adversarial review (bit-packing, type-ripple, GLSL/data-flow) **refuted** the correctness concerns — the R16UI format, masking, and block layout agree across `TrailManager` / shader / builder. The minor survivors (a preview that only resolved boat trails, stale comments) were fixed. ## Testing - `tsc --noEmit`, ESLint, Prettier, `build-prod` — all clean. - Schema/`Privilege` tests updated for `nukeTrail` (96 tests pass). - The GL trail + tab UI are visual — not yet verified in a running game. - The catalog (`cosmetics.json`, closed-source API) must ship the `effects.nukeTrail` block for the effect to appear in production. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
200f276ab2 |
feat: transport-ship trail transition effect + animated store swatch (#4455)
## What Adds a second transport-ship trail style, **transition**, alongside the existing **gradient** (#4454). Where `gradient` paints a spatial band of colors along the trail, `transition` makes the whole trail one color at a time, cross-fading through the color list over time. ```json "attributes": { "type": "transition", "colors": ["#002aff", "#4805ff"], "frequency": 1 } ``` ## How - **Schema** ([CosmeticSchemas.ts](src/core/CosmeticSchemas.ts)) — `TransportShipTrailAttributesSchema` is now a discriminated union on `type`: - `gradient`: `{ colors, colorSize, movementSpeed }` - `transition`: `{ colors, frequency }` — `frequency` = color changes per second. - **Renderer** — the effect texture gained a `styleId` discriminator (row 1's alpha; 0 = gradient, 1 = transition), with the gradient scalars shifted down a row. - [WebGLFrameBuilder.ts](src/client/WebGLFrameBuilder.ts) encodes `styleId` + the style's scalars. - [trail.frag.glsl](src/client/render/gl/shaders/map-overlay/trail.frag.glsl): for `transition`, the trail color is `mix(colors[i], colors[i+1], fract(t))` with `i = floor(uTime · frequency) mod count` — one color step every `1/frequency` seconds. - **Store/picker swatch** ([EffectPreview.ts](src/client/components/EffectPreview.ts)) — the swatch is now a `<trail-swatch>` Lit element. For `transition` it cross-fades through the colors via the Web Animations API, timed to match the shader (each step `1/frequency` s); gradient/solid stay static. The animation is canceled on disconnect. ## Notes - Animation is render-only (local time) — no simulation/determinism impact. - `gradient` swatches remain static (they don't scroll like the in-game trail) — easy to add later if wanted. ## Testing - `tsc --noEmit`, ESLint, Prettier, `build-prod` all clean. - Schema tests cover the transition member (parse + required `frequency`); 95 tests pass. - The animated swatch is visual-only (no automated coverage) and not yet verified in a running store. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7c151e76ad |
feat: render transport-ship trail cosmetic as a gradient (#4454)
## What
Renders the `transportShipTrail` cosmetic effect in-game. Transport
ships already left a trail, but it was always drawn in the player's
**territory color** — this wires the selected effect through to the
renderer so the trail shows the player's chosen **gradient**.
## How
- **Per-player effect texture** (`RGBA32F`, mirrors the palette texture)
keyed by `smallID`, sampled by the trail fragment shader. Each row holds
a gradient color; spare alpha channels carry the color count,
`colorSize`, and `movementSpeed`.
- **Shader**
([trail.frag.glsl](src/client/render/gl/shaders/map-overlay/trail.frag.glsl))
cycles a flowing gradient through the color list: 1 color → flat, 2+ →
animated bands scrolling along the trail. No effect (count 0) falls back
to the territory color; alt-view keeps affiliation colors.
- **WebGLFrameBuilder** resolves each player's catalog attributes (the
in-game cosmetic is only `{ name, effectType }`; the style/colors live
in the catalog) and encodes them. Resolution is decoupled from the
first-seen palette path so it retries until the catalog loads, and
unparseable colors are dropped so bad catalog data degrades to the
territory color rather than rendering black.
## Schema
Collapses the trail attributes to a single gradient shape:
```ts
{ type: "gradient", colors: string[], colorSize: number, movementSpeed: number }
```
- `colors` — solid = one color, rainbow = the spectrum, gradient = two
or more.
- `colorSize` — band width (tiles per color band; `1` is the default, ~4
tiles).
- `movementSpeed` — scroll rate along the trail (tiles/sec; `0` =
static).
## Notes
- Animation is render-only (local time), no simulation/determinism
impact.
- The catalog (`cosmetics.json`, served by the closed-source API) must
ship effects in this `{ type: "gradient", colors, colorSize,
movementSpeed }` shape.
- Band thickness (`4.0` base in the shader) and the gradient frequency
are visual constants picked without in-game verification — easy to tune.
## Testing
- `tsc --noEmit`, ESLint, Prettier, `build-prod` all clean.
- Schema + Privilege test suites updated for the gradient shape (92
tests pass).
- Not yet visually verified in a running game (effect selection is
flare-gated).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9881b118e4 |
Fix anonymous-names setting not hiding names on the map (#4345)
## Problem Enabling the **hidden names** (anonymous names) setting hid names in the leaderboard/HUD but **not on the map**. The GL name renderer (`NamePass`) drew `slot.static.displayName` — always the real name — and never consulted `userSettings.anonymousNames()`. The HUD works because it calls `PlayerView.displayName()` (which honors the setting) on each render, but the names baked into the GPU texture bypassed that path entirely. ## Fix Push the *resolved* name into the renderer instead of the raw static name: - **`WebGLFrameBuilder.syncPlayers`** registers each player with `displayName: p.displayName()` (honors the setting) instead of `static.displayName`. Covers enabling the setting before a game and players who join after a toggle. - **`WebGLFrameBuilder.refreshNames` → `MapRenderer` → `Renderer` → `NamePass.refreshNames`** is a new path that re-resolves cached names and forces a re-upload (resets `slot.nameLen = 0`, which also recomputes the name half-width so it stays centered). - **`ClientGameRunner`** listens for the `settings.anonymousNames` change event and calls `refreshNames`, mirroring the existing territory-patterns live toggle. ## Behavior - Enabled before a game → players register with anonymous names. - Toggled mid-game → map names flip to/from anonymous on the next sim tick (~100ms), matching the leaderboard. - Your own name is unaffected (unchanged — `PlayerView` maps the local player's anonymous name to their real name). ## Testing `tsc --noEmit` passes for all edited files. This is a WebGL rendering change with no straightforward unit test; verified by tracing the data flow (resolved name → cached `slot.static.displayName` → re-upload on dirty). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4149b3e4cb |
Pulse spawn ring white→team color for self in team games
In team games the local player's spawn breathing ring now pulses white→own team color (matching teammates' rings) instead of white→gold. Gold pulse is unchanged for teamless games (singleplayer/FFA). Self ring stays larger than teammates' via existing self/mate radii. |
||
|
|
aa4b490e68 |
Simplify WebGL renderer integration: remove dead extension code, untangle GameView naming (#4240)
## Summary The WebGL renderer was adapted from an external extension and carried a lot of machinery this integration never uses (replay playback, its own input/event system, a GL radial menu). This PR is two mechanical cleanup passes with **no behavior change**: delete the dead code, then untangle the `GameView` naming collision. **78 files, +142 / −2,197.** ### Pass 1 — remove dead extension baggage - **Replay/copy mode**: `FrameData.tileMode` was hard-coded `"live"`; the copy branches in `frame/Upload.ts`, `UploadOptions` (never passed), `applyFullFrame`/`applyFullTiles`/`applyDelta` on the facade and `GPURenderer`, `HeatManager.resetForSeek`, and the seek-upload methods on `TerritoryPass`/`TrailPass` were all unreachable. Also deletes `types/Replay.ts`, `types/FrameSource.ts`, `types/GameUpdates.ts`, `types/Game.ts` (imported only by the types barrel). - **FrameEvents**: trimmed from 14 fields to the 3 actually populated and read (`deadUnits`, `conquestEvents`, `bonusEvents`). The other 11 fed the extension's stats system and were never written or read here. - **GL radial menu**: `RadialMenuPass`, its 4 shaders, and ~10 API methods on facade + renderer had zero callers — the game uses the DOM/d3 radial menu in `hud/layers/RadialMenu.ts`. The pass was constructed and drawn every frame for nothing. - **Facade event system**: `GameViewEventMap` defined 10 event types (`click`, `hover`, `scroll`, …) but only `contextrestored` was ever emitted — input actually flows through `InputHandler` → EventBus → controllers. Replaced the listener map with a single `onContextRestored` callback and deleted `Events.ts`. Also fixed the stale header comment claiming the facade handles user interaction. - **Unused API surface**: removed ~20 facade/renderer methods with zero callers (camera passthroughs like `panTo`/`zoomTo`/`fitMap`/`screenToWorld`, hit-testing queries, SAM replay setters, `setSelectedUnit`, `clearFx`/`setFxTimeFn`, `onFrame`/`afterRender`/fps tracking). Deliberately left alone: `Camera`'s pan/zoom primitives (building blocks for a possible future camera unification) and the `timeFn` plumbing inside the FX passes (deeply embedded as defaults; only the dead renderer-level wrappers were removed). ### Pass 2 — untangle the three GameViews - `render/gl/GameView.ts` → **`MapRenderer.ts`** (class `MapRenderer`). Every importer was already aliasing it as `WebGLGameView` to dodge the collision with the simulation-mirror `GameView` in `client/view/`, so this removes aliasing rather than adding churn. `render/CLAUDE.md` updated. - Deleted the `src/core/game/GameView.ts` back-compat shim (its own TODO asked for this). All 51 importers now import from `src/client/view/` directly via a new 3-line barrel `view/index.ts`. ## Test plan - `tsc --noEmit` clean, `eslint` clean - Full test suite passes (1,385 + 65 server tests) - Manual verification via headless Chromium: started a singleplayer game and confirmed the renderer works end-to-end — terrain draws, spawn-phase overlay shows, territories fill with borders after spawning, player names/flags render, no renderer console errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aa22339f96 |
Add a main-thread perf harness for the worker → client update pipeline (#4243)
## What `npm run perf:client` — a headless harness (companion to `npm run perf:game` from #4228) that measures the **main-thread burst** the client runs every simulation tick. The sim ticks at 10Hz in a worker; each tick the main thread synchronously runs deserialization → `GameView.update()` → `WebGLFrameBuilder.update()` → HUD ticks. On low-end devices that burst exceeds the 16.7ms frame budget and shows up as a stutter every 100ms. Before optimizing that path, this gives us numbers. Per tick it runs the real pipeline end to end and times three stages: - **clone** — `structuredClone` of the `GameUpdateViewData` with the same transfer list `Worker.worker.ts` uses (serialize+deserialize, an upper bound on the main-thread share of the real `postMessage`) - **view** — the real client `GameView.update()`, including all `populateFrame()` derivations - **builder** — the real `WebGLFrameBuilder.update()` against a no-op GL stub that counts payload sizes It reports mean/p50/p95/p99/max per stage, slowest bursts with their tile counts, payload stats, a filtered V8 CPU profile table, and writes a `.cpuprofile`. Not covered (browser-only): CPU inside the WebGL view's `update*()` methods and HUD layer ticks. Same flags as `perf:game`: `--map --ticks --bots --nations --seed --top --no-cpu-profile`. ## Determinism - Prints the sim **Final hash**, which matches the `perf:game` references on all three standard configs (world/200t/100b → `5607618202213430`, default → `29309648281599524`, giantworldmap/600t → `39945089450032050`) — the harness's worker side is faithful. - Prints a **View hash** (FNV over the tile-state buffer, FrameData deriveds, and per-player/unit view state) — verified stable across runs. Client-side optimizations should keep it identical, the same workflow as the sim hash. ## Baseline (this machine; low-end devices are ~5–20× slower) Default run (world, 400 bots, 1800 ticks): | stage | mean | p50 | p95 | p99 | max | |---|---|---|---|---|---| | clone (serialize+deserialize) | 1.02ms | 0.96 | 1.53 | 2.11 | 9.15 | | GameView.update | 0.62ms | 0.58 | 0.93 | 1.25 | 5.09 | | WebGLFrameBuilder.update | 0.04ms | 0.04 | 0.05 | 0.07 | 0.17 | | **TOTAL burst** | **1.67ms** | **1.60** | **2.46** | **3.47** | **10.3** | giantworldmap/600t: TOTAL mean 2.54ms, p99 5.65ms, max 6.42ms. Notable: the clone is the largest stage (~60%) — the packed tile/motion buffers transfer for free, so the cost is structured-cloning the `updates` object (~278 partial player updates/tick on world, ~508 on giantworldmap). Inside `view`, the recurring cost is `populateFrame`'s derivations (`computePlayerStatus`, the O(players²) relation matrix, alliance clusters); tile apply dominates the land-grab spikes. ## Code changes outside the harness - `WebGLFrameBuilder`: the `./render/gl` import is now `import type` so the module loads under Node — a value import pulls `GPURenderer` and its `.glsl?raw` shader imports. No behavior change (the symbols were only used in type positions). - `tests/perf/client/Shims.ts`: an in-memory `localStorage` shim so `UserSettings`/theme code runs under Node (all settings resolve to defaults, which is also the deterministic choice). ## Verification - Sim + view hashes identical on repeat runs. - `npm test` (1474 tests), `eslint`, `prettier --check`, `tsc --noEmit` all pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
21776e81af |
Feature/colorblind mode (#4150)
**Add approved & assigned issue number here:** Resolves #2549 ## Description: Adds colorblind mode. Similar to dark mode, it exists as a toggle in settings. When enabled, it swaps the game's theme (which is refactored to extend from a theme base class) to use more colorblind-friendly colors and brightness variations. Borders are darkened, and terrarin is separated by lightness. Friendly/Foe colors and switched to blue/orange instead of red/green. The theme refactor supports adding new themes without having to reimplement the color distribution system. New themes can extend the BaseTheme and supply the data, such as palettes, team-color variations, and terrain. New setting: <img width="880" height="273" alt="Screenshot 2026-06-04 at 11 30 27 AM" src="https://github.com/user-attachments/assets/d5d573d5-cc64-4ac1-95c2-00627faf17cc" /> New color palette: <img width="1119" height="757" alt="Screenshot 2026-06-04 at 11 30 59 AM" src="https://github.com/user-attachments/assets/2bb15bc9-992b-41ae-ab0e-b01fe0c3c6bb" /> ## 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: jetaviz |
||
|
|
9189aac687 |
Improve railroad visibility: own-rail contrast color and thickness setting
Local-player rails previously rendered in the white focused-border color from the palette, making them hard to see on light territory. Rails now use a dedicated local rail color: white normally, flipped to black when the territory backdrop is too light for white to read against (patterns average their primary/secondary brightness). Also add a railThickness render setting (0.5-3, default 1), exposed in the Graphics Settings modal and the debug GUI, and persisted via GraphicsOverrides. In the medium-zoom LOD, rails are now drawn as screen-space anti-aliased lines around each tile's rail centerline, accumulated from the 3x3 neighborhood so thick lines spill cleanly into neighboring tiles; detailed mode scales its sub-grid band widths. - PlayerView: compute railColor() (white/black by backdrop brightness) - RailroadPass/shader: uLocalPlayerID, uLocalRailColor, uRailThickness - render-settings.json, RenderSettings, GraphicsOverrides, RenderOverrides: new railroad.railThickness knob - GraphicsSettingsModal: "Train track thickness" slider (+ en.json keys) - tests: schema + apply coverage for railroad overrides |
||
|
|
000f1442c4 |
Pulse local spawn ring white→gold for visibility on any terrain
The local player's spawn ring was a static near-white tint, which could wash out against light backgrounds. Drive the ring color from the existing breath animation in SpawnOverlayPass so it pulses between white and gold at 60fps — one end of the pulse always contrasts with the terrain. Remove the now-unused hardcoded self tint from WebGLFrameBuilder; the pass owns the self color now. |
||
|
|
611560a0b2 |
Restyle spawn-phase self highlight: gold instead of white
The local player's spawn ring was plain white, which was hard to see against white mountain terrain. Recolor it to a bright gold so it stays visible regardless of the terrain underneath, and make it stand out more overall during the spawn phase: - Recolor the self ring from white to a bright gold tint - Keep the ring center transparent so your territory shows through, ramping up to solid at the inner edge - Raise the breathing opacity floor (35% -> 65%) so the ring stays more solid through the dim part of the pulse - Speed up the breathing animation and enlarge the self ring radii |
||
|
|
aa3959bffe |
feat: territory png based skins (#4006)
## Description: Add image-based territory skins as a new cosmetic type, rendered alongside the existing 1-bit patterns. Skins render a single PNG centered on each player's spawn tile — opaque pixels show the skin (multiplied by team color in team games, raw colors in FFA), transparent pixels and tiles outside the image bounds fall through to the regular player palette color. **Cosmetic plumbing** - `SkinSchema` in `CosmeticSchemas.ts`, optional `skins` map on `CosmeticsSchema` - `PlayerSkin`, `PlayerCosmetics.skin`, `PlayerCosmeticRefs.skinName` in `Schemas.ts` - Server-side resolution: `PrivilegeCheckerImpl.isSkinAllowed` (gated by `skin:*` / `skin:<name>` flares) - Client persistence: stored under `PATTERN_KEY` (`pattern:` and `skin:` share one slot — they're mutually exclusive) - `getPlayerCosmeticsRefs` only emits a `skinName` when cosmetics are loaded, the skin exists in the catalog, and the user has the right flare — otherwise drops the ref and clears storage **Renderer** - `SkinAtlasArray` — fixed `TEXTURE_2D_ARRAY`, 1024×1024 per layer, exact layer count allocated once at game start from the locked-in player set. No resize, no callbacks, no retained `HTMLImageElement`. Zero GPU cost when no players have skins (1×1 placeholder). - `skinLayerTex` (R8UI 4096×1) — per-player `layer + 1` (`0` = no skin) - `skinAnchorTex` (RG16UI 4096×1) — per-player spawn tile, so the PNG center anchors at each player's spawn (re-uploads when the player re-picks during spawn phase) - `WebGLFrameBuilder.syncPlayers` collects unique skin URLs on first sync and calls `view.initSkinAtlas(urls)` once; `clearCaches()` resets so seek/replay re-initializes - `territory.frag.glsl`: skin branch is mutually exclusive with patterns; bounds-checks UVs against `[0, 1]` so the image is a single stamp, not tiled; alpha-blends against the player palette color so transparent pixels and out-of-bounds tiles render as the regular player color **Hover highlight (global UX change, not skin-scoped)** - Existing hover highlight changed from "brighten toward white" to "saturation boost." Applies to all players regardless of skin/pattern/flat-color — looks better across the board. **UI** - `CosmeticButton` renders skins as a single `<img>` (object-contain) - `TerritoryPatternsModal` merges patterns + skins into one grid; single "default" tile clears both - Selecting a pattern clears the skin and vice versa (mutually exclusive) - `Store` pattern tab includes skin entries (purchasable, not-yet-owned) - `PatternInput` lobby button previews the active skin when one is set **Memory** - 0 skin players → ~4 bytes (placeholder) + ~40 KB fixed per-player tables - 1 skin player → ~5.6 MB GPU - 5 skin players → ~28 MB GPU - 10 skin players → ~56 MB GPU **Tests** - `tests/Privilege.test.ts`: 13 new cases covering `isSkinAllowed` (wildcard, exact-match, missing flare, missing skin, forged refs) and `isAllowed` integration (allowed/forbidden paths, short-circuit when invalid skin is paired with valid other cosmetics) ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] 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 - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan |
||
|
|
19beab9a70 |
flags (#3985)
# Dynamic flag atlas (runtime TEXTURE_2D_ARRAY) Replaces the build-time `flag-atlas.png` with a runtime `TEXTURE_2D_ARRAY` populated on demand from each player's server-resolved flag URL. Layers are deduped by URL (every "Mercia" bot shares one slot), so the per-game working set is bounded by unique flags, not player count. ## Why The store will eventually ship hundreds of custom flags fetched from the CDN, which can't be baked into a static atlas. Moving to a runtime array also lets the catalog grow without bloating the client bundle. ## Side effect (bonus) Human players' country flags (`country:US`, etc.) now display next to their names in-game. The old atlas only contained nation names, so non-nation flags were silently dropped. ## Notes - Cell size is fixed at 128×85; loaded images are aspect-fit and centered. - Layer cap is 512 (clamped to `MAX_ARRAY_TEXTURE_LAYERS`). Past the cap, further flag requests render no icon. - Mipmaps are regenerated after each layer upload. - Recommend store pipeline caps custom flag uploads at SVG or PNG ≤ 256×170, ≤ 50 KB (decode-time RAM and bandwidth, not VRAM). ## 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 - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan |
||
|
|
7acf1aebc2 |
Restore on webgl context loss (#3968)
## Description: When WebGL context is lost, restore context and all elements. In GameView, handle potentially transient undefined states during context loss gracefully. Test with chrome://gpucrash from another tab, then return to the game tab to see it being restored (This fake gpucrash only works once sometimes. Because the second time the browser might reject the tab it thinks caused the gpu crash, access to hardware acceleration. And after even more tries even disables it browser-wide. A browser restart resets it in that case.) ## 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 - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 |
||
|
|
a2aa7823a4 |
Display player flags next to their names again (#3965)
## Description: Display flags again. ## 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 - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 |
||
|
|
17e3ac4b05 |
make spawn glow follow the player's currently selected spawn tile
Plumb spawnTile through PlayerUpdate / PlayerState / applyStateUpdate so the WebGL spawn overlay can read it directly. The glow was reading nameData.x/y (territory centroid for label placement) which only recomputes every 2 ticks and only when largestClusterBoundingBox has been updated by PlayerExecution — both lag the player's actual spawn click. Using spawnTile updates the same tick setSpawnTile() fires. Also adds spawnTile to diffPlayerUpdate / applyStateUpdate so changes after the initial full snapshot actually propagate (the recent diff-only PlayerUpdate path silently dropped any field not enumerated in those helpers). |
||
|
|
ed928db081 |
Display territory skins again (#3966)
## Description: Display territory skins (patterns) again. ## 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 - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 |
||
|
|
1dd00f6264 |
push terrain deltas to the WebGL view so water nukes show
Terrain was uploaded once at game start and treated as static — water nukes (land → water conversion) mutated the sim's terrain bytes but the rendered terrain stayed dry. Plumbed a delta path: TerrainPass and RailroadPass each get applyTerrainDelta(refs, bytes), Renderer + GameView forward, and WebGLFrameBuilder pushes each tick from gameView.recentlyUpdatedTerrainTiles(). Per-tile encoding is shared via the new encodeTerrainTile helper in ColorUtils so the startup full-map build and the per-tile delta updates can't drift. |
||
|
|
4936ae3d59 |
restore spawn-phase glow with a true breathing animation
SpawnOverlayPass had everything wired except a caller. WebGLFrameBuilder now collects spawned human players each tick during spawn phase and pushes their territory centroid + color through view.updateSpawnOverlay. myPlayer reads as white so the local-player ring stands out. Reshaped the shader animation: dropped the growing-disc effect, gave the ring a true breath — radius scales 0.5×→1.15× while opacity pulses 35%→100% in phase. Replaced the sharp inner-edge ramp with a smooth center-to-boundary fill so there's no hard cutoff or empty hole in the middle. animSpeed bumped to 0.0035 (~1 breath/sec). |
||
|
|
4cd22a9b5c |
rename render/ files to UpperCamelCase to match client convention
The render/ tree was the only place in the client still using kebab-case filenames. Brings ~80 files in line with the rest of src/client/ (BuildPreviewController, TransformHandler, etc.). Directories kept as they were (name-pass/, fx-pass/, passes/, utils/, debug/) since the codebase already mixes those. Two collisions surfaced and got resolved: render/types/ is a directory, not a file, so its imports kept the lowercase form; and the sed pass incidentally normalized core/pathfinding imports, which had to be reverted since that file is actually lowercase on disk despite some imports having referenced it as ./Types under macOS case-insensitive resolution. |
||
|
|
fb45c27d82 |
add subtle player-tile highlight on nation hover
The hover wiring already pushed setHighlightOwner into the border pass, but the WebGL canvas has pointer-events: none (post-migration to the inputOverlay div) so MapInteraction's pointermove listener never fired. Forward pointermove from the input overlay to view.handlePointerMove so hover actually triggers. While there, brighten every tile owned by the hovered player — the territory frag shader now reads uHighlightOwner / uHighlightBrighten and mixes toward white when the tile owner matches. Wired through territory-pass.ts; renderer.setHighlightOwner forwards to both border and territory passes. New highlightFillBrighten setting (0.15) keeps the fill tint tunable independently of the existing highlightBrighten border setting, which is dropped from 0.6 → 0.25 so neither effect blows out. |
||
|
|
5b663fae14 |
refactor: share renderer state shapes between game and WebGL renderer
PlayerView/UnitView now wrap renderer-shaped state objects (PlayerState, PlayerStatic, UnitState) directly instead of holding engine wire types. GameView owns a long-lived FrameData object kept in sync each tick: players/units/tiles/trail/railroad are mutated in place; derived buffers (playerStatus, relationMatrix, allianceClusters, nukeTelegraphs, attackRings) and events are recomputed in a final populateFrame() pass. The renderer reads gameView.frameData() and the same byte-identical state objects PlayerView/UnitView wrap. WebGLFrameBuilder shrinks from ~270 to ~70 LOC: palette management + a single uploadFrameData() call, no per-frame UnitState allocation on the hot path. Wiring: maxPlayers=1024 on RendererConfig (pre-sizes NamePass/palette/ relation matrix textures); NamePass disabled so HTML NameLayer remains the only on-screen player names. Also: 39 new tests covering PlayerView/GameView/FrameData behavior; replace .data field access in three layer call sites with accessor methods (betrayals(), type(), getTraitorRemainingTicks()). |
||
|
|
53cf2d43f8 | migrate away from canvas |