mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 10:10:55 +00:00
83cd86401855b36c18c5c9171ddc7cdb9b5c7ca1
4051 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
83cd864018 |
Show rail ghost for initial factory 🚂 (#4294)
## Problem Fixes #4284. When you build a factory in an area with **no pre-existing factory** (e.g. just a city nearby), no rail ghost preview appeared — even though building the factory *would* lay rail lines connecting it to that city. ## Root cause `computeGhostRailPaths` in `RailNetworkImpl.ts` had two factory-hostile assumptions: 1. It bailed out early unless a `Factory` was already in range (`hasUnitNearby(..., UnitType.Factory)`). 2. It only matched neighbors that were *already* train stations (`findStation(...)` → skipped if null). But a **Factory** always becomes a station itself and *promotes* nearby City/Port/Factory into the rail network (see `FactoryExecution`). So it needs no pre-existing factory, and its neighbors won't be stations yet on first build. A **City/Port** only joins the network when a factory already exists (`CityExecution`/`PortExecution`) — so their behavior is correctly left unchanged. ## Fix - Skip the "factory must be nearby" gate when the placed unit is itself a `Factory`. - For a factory build, pathfind to nearby City/Port/Factory even if they aren't stations yet. City/Port keep connecting only to existing stations. ## Tests Added two cases to `RailNetwork.test.ts` (factory connects with no pre-existing factory; city still doesn't without one). All 25 tests pass. ## Note on scope As @Katokoda noted on the issue, a fully build-exact preview (neighboring structures also connecting to *each other*, merging existing networks, etc.) is larger and order-dependent. This PR resolves the reported bug — the initial factory now shows its rail ghost — and leaves the exact-match cascade as a separate follow-up. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
678112492c |
Fix per-frame layout jank when focusing a toggle-input-card field (#4314)
## Problem
Focusing the number field of a `toggle-input-card` (Game Timer / Gold
Multiplier / Starting Gold, in both the single-player and host-lobby
modals) cost several ms of layout/paint **every tick** for as long as
the field stayed focused.
## Root cause
The input was rendered **conditionally** — `${this.checked ?
html`…<input>…` : nothing}`. Enabling a toggle therefore **freshly
inserts** the `<input>` into the DOM, and **focusing a just-inserted
input** is what forced the per-frame layout/paint. An input that was
already present in the DOM doesn't do this.
## Fix
Keep the input **permanently mounted** and toggle a `hidden` class when
unchecked, instead of conditionally rendering it. Focusing it is then
always focusing an element that was already there. Because both modals
share `<toggle-input-card>`, this single change fixes both.
Also restores the **autofocus + select** of the field on enable (it had
been removed earlier while chasing this bug) — safe now that the input
isn't freshly inserted.
No other UX change: the toggle behavior, checkmark, styling, and all
three cards behave identically.
## Testing
Hard-reload, then in both the Solo and Host-lobby modals, enable each of
Game Timer / Gold Multiplier / Starting Gold, type a value, and keep the
field focused — smooth, no per-frame jank, and the field autofocuses on
enable.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
bb464538d0 |
Add Fallout effects toggle to graphics settings ☢️ (#4313)
## Summary Adds a **"Fallout effects"** toggle to the *Effects* section of the graphics settings modal, letting players disable the nuclear fallout visuals (useful for performance). Fallout is rendered by two passes — the broiling green **bloom** on irradiated territory and its additive **light** contribution in day/night mode. The bloom pass was already gated by `passEnabled.falloutBloom`, but the light pass had no gate. This adds a `passEnabled.falloutLight` flag and a single user-facing `passEnabled.fallout` graphics override that drives both together. ## Changes - **`RenderSettings.ts` / `render-settings.json`** — new `passEnabled.falloutLight` flag (default `true`). - **`LightmapPass.ts`** — gate the fallout light pass behind `passEnabled.falloutLight`. - **`GraphicsOverrides.ts`** — add `fallout: z.boolean()` to the `passEnabled` override group. - **`RenderOverrides.ts`** — apply `passEnabled.fallout` to both `falloutBloom` and `falloutLight`. - **`GraphicsSettingsModal.ts`** — `currentFallout()` / `onToggleFallout()` + a toggle button (mirrors the existing Special Effects toggle). - **`en.json`** — `graphics_setting.fallout_label` / `fallout_desc`. ## Testing - `tsc --noEmit` passes; JSON files validated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e6256e6269 |
Flip structure icon glyph to light on dark territory 🌑 (#4312)
## Problem
In the structure render pass, when **classic icons** are enabled the
inner icon glyph is tinted with a *darkened version of the player's fill
color* (`uIconDarken = 0.3`). When a player's territory color is already
dark, both the structure shape and its glyph render dark, so the icon
blends into the shape and the dark territory behind it — making it
effectively unreadable.
(With non-classic icons the glyph is already the light `uIconColor`, so
only the classic path was affected.)
## Fix
In `structure.frag.glsl`, when classic icons are active, compute the
fill's perceptual luminance and flip the glyph to the light icon color
(`uIconColor`, white by default) when the fill is too dark:
```glsl
vec3 glyphColor = uIconColor;
if (uIconDarken > 0.0) {
float fillLum = dot(fillColor.rgb, vec3(0.299, 0.587, 0.114));
glyphColor = fillLum < 0.25 ? uIconColor : darken(fillColor.rgb, uIconDarken);
}
```
The non-classic path is unchanged. The change is contained to the shader
— no new uniforms or plumbing.
## Notes
- The `0.25` luminance threshold is hardcoded in the shader to keep the
change surgical. It could be promoted to a `render-settings.json` knob
if preferred.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
6c84919801 |
Smooth nuke point-light position per frame in ambient mode (#4311)
## Summary Follow-up to #4255. That PR made nuke **sprites** glide per render frame — `UnitPass.drawMissiles` lerps each nuke's `lastPos→pos` by wall-clock progress through the current tick. But in ambient/night mode the glow *behind* a nuke comes from a separate pass, `PointLightPass`, whose instance buffer is packed once per tick in `updateLights()` from the raw `unit.pos`. Its per-frame `draw()` (run every frame via `LightmapPass`) only set uniforms and issued the instanced draw — it never repositioned the lights. So the sprite moved at 60fps while its light jumped once per 100ms tick. ## Fix Mirror `UnitPass`'s smoothing in `PointLightPass`: - `updateLights()` records a `smoothSegs` tuple `(lightIdx, lastX, lastY, x, y)` for each `SMOOTHED_NUKE_TYPES` unit whose `lastPos !== pos`, and stamps `lastUnitsUpdateMs`. - A new `applySmoothing()`, called at the top of `draw()`, lerps those lights by wall-clock tick progress (`(now - lastUnitsUpdateMs) / tickIntervalMs`, clamped to 1) and re-uploads **only** the affected instances. Unlike `UnitPass` (which re-uploads its tiny missile buffer wholesale), the light buffer can hold thousands of static structure lights, so a full per-frame re-upload would be wasteful. - `tickIntervalMs` comes from a new `config` constructor param, wired through in `Renderer.ts` (the same `config` already passed to `UnitPass`). The light now uses the exact same `lastPos→pos` endpoints and alpha as the sprite, so the two track together. ## Test plan - `npx tsc --noEmit`, eslint, and prettier all clean. - `npx vitest tests/client/render --run` — 40 passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8a8079b979 |
Fix parallel bot boat attacks of nations 🐛 (#4297)
## Description: When a nation attacked multiple bots via boat attacks in parallel, each boat attack computed its troop allocation independently using `player.troops() / 5` without subtracting `botAttackTroopsSent`. The cumulative troop commitment could exceed the nation's actual troop count, and when the queued `AttackExecution`s ran `init()`, they drained the nation to zero. Planetary Realignment found this bug by accident, here Russia has only 39 troops: <img width="1189" height="654" alt="image" src="https://github.com/user-attachments/assets/07b85e00-6734-4ddd-a16e-fe53309e0ef8" /> The land attack path already handled this correctly. The bug in `sendBoatAttack` was introduced by #3786, which made nations see and attack enemies across rivers via boats, and changed `attackBots()` from `.neighbors()` to `.nearby()`. So the bug was on prod for the entirety of v31. This fix extracts the shared attack troop calculation (reserve, bot-aware allocation, troopSendCap, isAttackTooWeak, emoji) into a new `calculateAttackTroops` method, with a callback for the non-bot troop default (land: `player.troops() - targetTroops`, boat: `player.troops() / 5`). Bot targets in both paths now go through the same reserve-aware calculation. ## 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: FloPinguin |
||
|
|
6833cef7bc |
Tweak render settings to look more like v31
Lower territory saturation, highlight thickness, and border darkening to bring the rendered map closer to the v31 look. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1ad71b9cfa |
Fix hover-highlight inner border lagging during tile changes (#4303)
## Problem
When you hover over a territory, it highlights with a band that is
`highlightThicken` (default **2**) tiles deep — the edge plus 2 interior
rings, computed via a Chebyshev expansion in `border-compute.frag.glsl`.
Starting a hover triggers a full border recompute, which paints the band
correctly. But while you keep hovering and tiles change owner (territory
growing/shrinking, combat at the front), only the cheap **incremental**
scatter path runs. `BorderScatterPass.pushWithNeighbors` repainted only
the changed tile **+ its 4 cardinal neighbors** (radius 1) — fine for
normal borders, but not for the highlight band. A changed tile affects
the thickening of *every* highlight-owner tile within `highlightThicken`
of it, and those interior tiles were never repainted, so the **inner
edge of the highlight band stayed stale** ("the inside border is not
getting updated"). This was a documented trade-off in the class comment.
## Fix
When a highlight is active, `pushWithNeighbors` now repaints a Chebyshev
**box of radius `highlightThicken`** around each changed tile (the box
subsumes the cardinal cross, so normal borders still update). With no
highlight active it stays on the cheap 5-point cross, preserving the
pass's O(dirty-tiles) scaling. The extra cost (~25 vs 5 points/tile at
default) only applies while actually hovering.
## Testing
Hover over a territory while it grows/shrinks (early-game expansion or a
war front) and confirm the inner edge of the highlight band now tracks
the moving border instead of lagging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
0639cdb29b |
Fix nuclear fallout covering UI overlays (#4302)
## Problem Nuclear fallout was rendering on top of UI overlays (most visibly the SAM launcher range circles), hiding them. ## Cause In `renderOverlays()` (`src/client/render/gl/Renderer.ts`), the fallout bloom pass was drawn near the end of the overlay sequence — after the SAM radius, range circles, structures, bars, etc. — so it painted over all of them. ## Fix Moved `bloomPass.draw(...)` (fallout bloom) to draw right after the ground units and before all UI overlays. Fallout is a ground-contamination effect, so it now sits above the terrain/units but below every UI overlay, which all render on top. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
10ca2d1230 |
Restore gray in-game background to match v31 (#4301)
## Summary The in-game map background changed from gray (v31) to near-black after the WebGL renderer rewrite. This restores the gray. The renderer rewrite hardcoded the base-layer clear color to `(0.04, 0.04, 0.06)` in `drawBaseLayer` (`src/client/render/gl/Renderer.ts`). v31 set the background via `PastelTheme.backgroundColor()`, which returned `rgb(60,60,60)`. This change sets the clear color back to that gray. ## Notes - The old theme-based `backgroundColor()` system was removed in the rewrite, so this hardcoded clear color is now the single source for the map background. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
0904ad76d1 |
Fix per-frame render jank from toggle-input-card focus (#4300)
## Problem
Enabling **Starting Gold** (or **Game Timer** / **Gold Multiplier**) in
the single-player / host-lobby modal made the page spend several ms of
"Render" time **every frame** — for as long as the toggle stayed
enabled. Disabling the option made it stop.
## Root cause
Each `toggle-input-card`, on enable, auto-focused and selected its
number input so you could type immediately:
```ts
input.focus();
input.select();
```
A focused/selected editable inside the modal keeps the browser doing
layout/paint work every frame for as long as it stays focused. It
reproduces for any of the toggle-input cards because they all auto-focus
on enable, which is why Starting Gold, Game Timer, and Gold Multiplier
all triggered it.
> **Note on the earlier revision of this PR:** the first attempt passed
`{ preventScroll: true }` to `focus()`, on the theory that
scroll-into-view was the cause. It successfully stopped the scroll
(verified: modal scroll container `scrollTop 0 → 0`), but the per-frame
render cost remained. That ruled out scroll-into-view and proved the
focused editable itself — not the scroll — was the trigger.
## Fix
Remove the auto-focus entirely. Enabling a toggle no longer focuses its
number input, and the per-frame render cost is gone.
## Trade-off
You no longer get type-to-replace on enable — click the field before
typing the value. Worth it to eliminate the per-frame render.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
5be72db060 |
Reapply graphics overrides on debug render GUI reset (#4287)
## Problem The debug render GUI's **"Reset to Defaults"** restored bare `createRenderSettings()` defaults, wiping the user's graphics overrides (colorblind theme, ocean color, lighting, name scaling, etc.) from the live render settings. The per-prop right-click "reset to default" and the modified-indicators had the same flaw — their captured defaults were raw, ignoring overrides. ## Fix Thread the existing `resolveRenderSettings` (`createRenderSettings()` + `applyGraphicsOverrides()`) into the debug GUI as the defaults provider, so reset restores the same settings the renderer was actually built with. - **`debug/index.ts`** — added a `resolveDefaults` param (defaults to `createRenderSettings` to keep the module decoupled). The captured `defaults` now include overrides, fixing the per-prop reset and modified indicators too. - **`debug/Wiring.ts`** — `wireActions` takes `resolveDefaults`; the reset handler `deepAssign`s `resolveDefaults()` instead of `createRenderSettings()`. - **`ClientGameRunner.ts`** — passes `resolveRenderSettings` into `createDebugGui`, and extracts a `refreshDerivedGraphics` helper (terrain rebuild + re-theme/palette) from `onGraphicsChanged`, wired as the GUI's `onSettingsChanged` so the reapplied terrain/colorblind overrides become *visible* after reset (they're baked into GPU textures and aren't picked up per-frame). Side benefit: editing terrain/theme settings in the debug GUI now refreshes those textures live too (that callback was previously never wired). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
094aa766ce |
Improve "Better troop management for nations 🤖" (#4278)
## Description: **Allow Hard/Impossible nations to retaliate and expand freely** Previously, nations on Hard/Impossible difficulty could be stuck unable to fight back if their `troopSendCap` or `isAttackTooWeak` checks blocked them from sending enough troops. **@legan320** on the main discord noticed it. Now: - `troopSendCap` raises the cap to at least the total incoming attack troops, so nations can match the force being used against them - `isAttackTooWeak` bypasses the 20% minimum check entirely when under attack - `troopSendCap` no longer applies when attacking Terra Nullius, so nations can always expand into unowned land with full troops All checks still apply normally for unprovoked attacks against other players. ## 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: FloPinguin |
||
|
|
5161d78d84 |
Fix nuke fallout embers flickering forever ☢️
The ember/particle flicker in the fallout effect was gated only by the fallout bit, which is permanent on tiles that stay unowned. It also ramped to full strength as the per-tile heat decayed to 0 and animated on the global tick, so it kept flickering indefinitely after the blast had cooled — visible both as the bloom dots and (more prominently) as the ambient ember light when dynamic lighting is enabled. Fade both with heat so they vanish along with the glow: - extract.frag.glsl: bloom dots multiplied by the glow's opacity - fallout-light.frag.glsl: ember light multiplied by heat Heat decay timing is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b997099dfe |
Add map search 🔍 (#4283)
## Description: Add a search input field to the map picker section header, allowing users to quickly filter maps by name. - Place transparent search input on the right side of the "Maps" section header - Filter maps by translated name and map ID as the user types - Hide Featured/All/Favourites tab buttons while search is active - Show filtered results with a count heading, or a "no results" empty state - Clear button appears when search input has text <img width="857" height="463" alt="Screenshot 2026-06-15 001415" src="https://github.com/user-attachments/assets/35e1101a-177e-4923-bb1d-34eb683c6f80" /> No search results: <img width="855" height="454" alt="Screenshot 2026-06-15 001433" src="https://github.com/user-attachments/assets/bf27211d-5891-4739-a92f-0fc44b3c9c61" /> ## 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: FloPinguin |
||
|
|
6c8ce958b2 |
Fix nations being blocked by PVP immunity 🛡️ (#4282)
## Description: ### Problem PVP immunity (the extended spawn immunity setting) was incorrectly preventing AI nations from attacking human players. The intent of PVP immunity is to protect human-vs-human combat only, but nations were subject to the same restriction. ### Root Cause In `canAttackPlayer()`, only `PlayerType.Bot` was exempt from checking target immunity. Nations fell through to the same path as humans, so when a nation tried to attack an immune human, `player.isImmune()` returned true and the attack was blocked. ### Fix Changed the immunity bypass condition from `this.type() === PlayerType.Bot` to `this.type() !== PlayerType.Human`. Now only human attackers check target immunity. Both bots and nations bypass it (they only check alliance status). This does not affect nation spawn immunity (`nationSpawnImmunityDuration`), which is a separate mechanism that protects newly spawned nations from all attackers and continues to work independently. ## 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: FloPinguin |
||
|
|
f7ce58a49f | Meta update: increase nuke speed from 10=>12 | ||
|
|
6a8900fac2 |
feat: Support direct clan detail links (#3928)
## Description: Add support for opening clan details directly with `clan=<tag>` ## 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: aotumuri |
||
|
|
769da27257 |
Fix railroad glowing green for non-snapping structures (#4281)
## Problem When placing a building near a railroad, the railroad glows green to show the building would snap to it. This should only apply to **City**, **Port**, and **Factory** — but missile silos, SAMs, and defense posts (which cannot be placed on railroads) were also triggering the green highlight. ## Root cause The core's `overlappingRailroads()` populated snap tiles for *every* buildable type. In v31 the green highlight didn't leak because the client renderer (`RailroadLayer.ts`) gated it with a `SNAPPABLE_STRUCTURES = [Port, City, Factory]` allowlist: ```ts if (!SNAPPABLE_STRUCTURES.includes(this.uiState.ghostStructure)) return; ``` That guard was lost when the rendering was rewritten into the WebGL `RailroadPass`, which now unconditionally highlights every tile in `overlappingRailroads`. The data was always there; only the renderer's filter was protecting it. ## Fix Filter by unit type inside `overlappingRailroads()`, mirroring the existing guard in `computeGhostRailPaths()`. This keeps the snap-eligible type list defined once in the core (`RailNetworkImpl`) and fixes the leak regardless of which renderer consumes the data — rather than re-adding a client-side allowlist a future rewrite could drop again. ## Tests Updated `tests/core/game/RailNetwork.test.ts` for the new signature and added a case asserting `MissileSilo`/`DefensePost`/`SAMLauncher` return `[]` (and don't even query the rail grid). All 23 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
52bcae5106 |
Replace dark mode with player-adjustable lighting (#4280)
## What Removes the binary **dark mode** feature and replaces it with a player-adjustable **Lighting** section in graphics settings. ### In-game settings - Removed the Dark Mode toggle from both `SettingsModal` and `UserSettingModal`, and `darkMode()`/`toggleDarkMode()`/`DARK_MODE_KEY` from `UserSettings`. ### New Lighting section (Graphics Settings) - **Ambient light** slider (1–3): mapped to the renderer's ambient as `ambient = 1 / level`. **1.0 = no effect (unchanged look), 3.0 = darkest with the strongest structure glow.** - **Light falloff** slider (1–3): writes straight to `lighting.falloffPower`. - Lighting auto-enables only when ambient < 1, so the default (slider at 1) has zero GPU cost — off by default. ### Removed dark-mode overrides - Deleted `applyDarkModeOverride()` + `DARK_AMBIENT` and their wiring in `ClientGameRunner`, `gl/index.ts`, and the `DARK_MODE_KEY` listener. - Removed the `.dark` HUD-class toggle in `Main.ts` and the `userSettings.darkMode()` read in `PlayerIcons`. ### Train glow - `UT_TRAIN` light reduced (intensity `2.0 → 0.5`, radius `8 → 6`) so structures dominate the glow. ## Notes - Removing the dark-mode setting also retires the HUD's Tailwind dark theme (same setting). The dormant `dark:` CSS variants and unused white-icon assets are left in place (out of scope). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
44f5e14a0f |
Fix captured trade ships rendering yellow in alt-view
In alt-view, trade ships are colored by their owner's affiliation: self green, ally yellow, enemy red. The FLAG_TRADE_FRIENDLY override recolors an enemy ship red→yellow when it's heading to a self/allied port. That flag was decided solely from the destination port's owner, ignoring who owns the ship — so a captured trade ship (now owned by us, heading to our port) got flagged yellow instead of keeping its green affiliation color. Gate FLAG_TRADE_FRIENDLY on the ship being enemy-owned, since self/allied ships already render the correct color without the override. Also fixes our own trade ships heading to an ally's port flipping green→yellow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bb5e7dc954 |
Apply perceptual curve to volume sliders (#4272)
## Problem Players reported having to turn the volume slider down to ~20% before noticing any change in loudness. The sliders fed their linear 0–1 position straight to Howler's `volume()`, which is linear amplitude gain. Human loudness perception is roughly logarithmic, so the top ~80% of the slider all sounds nearly identical — the classic linear-fader problem. ## Fix Square the slider position into a perceptual (audio-taper) gain inside `SoundManager`. The stored setting and the displayed `%` remain the intuitive linear slider position; only the gain handed to Howler is curved. | Slider | Old gain (linear) | New gain (x²) | |--------|-------------------|---------------| | 100% | 1.00 | 1.00 | | 90% | 0.90 | 0.81 | | 80% | 0.80 | 0.64 | | 50% | 0.50 | 0.25 | | 20% | 0.20 | 0.04 | Lowering the slider from 100→80 now produces an audible drop instead of nothing until ~20%. ## Notes - Quadratic (x²) was chosen as a balanced, conservative taper. Cubic (x³) would make the top-end drop-off even more immediate if preferred. - Existing saved settings are unaffected; the same slider position will simply sound slightly quieter, which is the intended correction. ## Tests Updated `SoundManager.test.ts` to assert the curved gain and added a dedicated test locking in the top-of-range behavior. All 18 tests pass. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1c5122e2d2 |
[Fix] Pathfinding bug in Warship Warship (#4274)
> **Before opening a PR:** discuss new features on [Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small improvements as [issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose). You must be assigned to an `approved` issue — unsolicited PRs will be auto-closed. **Add approved & assigned issue number here:** Resolves #4273 ## Description: Minor pathfinding bugs to do with the weird corners in Warship Warship. Boats are unable to exit some of the corners for no reason. This bugfix simply adds 2 blue pixels to all the glitched corners. Credit to @RickD004 for adding the pixels <img width="1265" height="674" alt="Screenshot 2026-06-13 223641" src="https://github.com/user-attachments/assets/5802d5ae-14cb-4159-ab70-454e1c73dfae" /> <img width="1262" height="688" alt="Screenshot 2026-06-13 223702" src="https://github.com/user-attachments/assets/c3d5c1d5-98f6-4322-87b0-134cfc916d1d" /> ## 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: DISCORD_USERNAME crunchybbbbb |
||
|
|
8b9bda1c8b |
Add ocean color override to graphics settings (#4269)
## What Adds a **Terrain** section to the graphics settings modal with a color picker and a hex-code text field (paste a `#rrggbb` code) for the **ocean** (deep water) color. ## Details - The picked color sets the *shallow-water base*; the existing per-depth brightness gradient is preserved (deeper water still darkens). - Only deep water is affected — shoreline water and land are untouched. - Follows the same override pattern as every other graphics setting: the default lives in `render-settings.json` (`terrain.oceanColor`), the override is a field in `GraphicsOverrides`, and `applyGraphicsOverrides` copies it into the live `RenderSettings`. - Rebased on #4271 (settings resolved before renderer construction): the terrain texture **bakes the resolved ocean color at construction**, so a saved override shows on load with no special-casing. Terrain is baked into a GPU texture rather than read per-frame, so a *live* change still triggers an explicit `view.rebuildTerrain()`. - Resetting graphics overrides clears it back to the default ocean color. ## Testing Verified live in a headless singleplayer game: - A **saved** ocean override renders green deep-water on load, baked at construction with no settings-change event fired. - A mid-game color change recolors the deep ocean instantly, gradient preserved, shoreline/land untouched. `tsc` and ESLint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
54a7042303 |
Resolve render settings before renderer construction (#4271)
## What
The client now resolves render settings (defaults + user overrides) **up
front** and passes the result into the renderer, instead of the renderer
constructing defaults itself and the client re-applying overrides
afterward.
```
before: new GPURenderer(...) // this.settings = createRenderSettings() (defaults)
view.getSettings() → deepAssign(defaults) → applyGraphicsOverrides(...) // patch after the fact
after: const settings = createRenderSettings(); applyGraphicsOverrides(settings, ...); applyDarkModeOverride(settings, ...)
new GPURenderer(..., settings) // built with the final values
```
## Why
- **Removes the construct-with-defaults / re-apply-overrides dance.**
Every pass — including texture-baking ones like terrain that read their
settings *once* at build time rather than every frame — is now built
with the final values on the first try. (This is the cleanup that
motivated the change, surfaced while adding a terrain color override in
a separate PR.)
- **Fixes a latent context-restore bug.** On WebGL context loss/restore
the renderer was rebuilt via `createRenderSettings()` → fresh
**defaults**, silently dropping any user overrides until the next
settings change. `MapRenderer` now holds the resolved settings object
and hands the same one to the recreated `GPURenderer`, so overrides
survive a restore.
Live setting changes still work exactly as before:
`regenerateRenderSettings()` re-resolves and `deepAssign`s onto the
renderer's live settings object in place (passes hold a reference, so
they pick it up next frame).
## Changes
- `Renderer.ts` (`GPURenderer`) — constructor takes a `settings:
RenderSettings`; drops the internal `createRenderSettings()` call.
- `MapRenderer.ts` — holds the resolved settings and passes it through
on construction and on context-restore re-init.
- `ClientGameRunner.ts` — new `resolveRenderSettings()` helper used both
at construction and by `regenerateRenderSettings()`; `createWebGLView`
takes the resolved settings; the now-redundant initial
`regenerateRenderSettings()` call is removed.
## Testing
Verified live in a headless singleplayer game:
- A saved `nameScaleFactor` override is present in `getSettings()`
immediately after game start, with no settings-change event fired
(construction path).
- A mid-game override change is reflected in the live settings
(regenerate/in-place path).
- The map renders correctly through spawn phase.
`tsc` and ESLint clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
3a8249dfd1 |
Add structure icon size graphics override (#4270)
## Summary Adds a new **Structure icon size** option to `GraphicsOverrides`, exposed as a slider in the Graphics Settings modal. Players can now scale how large structure icons are drawn on the map. ## Changes - **`GraphicsOverrides.ts`** — add `iconSize: z.number()` to the `structure` override schema. - **`RenderOverrides.ts`** — apply the override onto `settings.structure.iconSize` (consumed by `StructurePass`/`StructureLevelPass` shaders). - **`GraphicsSettingsModal.ts`** — add a slider (range 20–120, step 5) in the "Structure Icons" section, with getter/handler following the existing pattern. Falls back to the `render-settings.json` default of 60 when unset. - **`resources/lang/en.json`** — add `icon_size_label` / `icon_size_desc` (English only, per i18n rules). - **`tests/GraphicsOverrides.test.ts`** — schema-validation cases plus application tests (override sets the value; absence keeps the default). The setting persists via the existing `userSettings.graphicsOverrides()` localStorage flow and takes effect live through the existing `regenerateRenderSettings` wiring. ## Testing - `npx vitest tests/GraphicsOverrides.test.ts --run` — 35 passed - `tsc --noEmit` — no new type errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f76f133589 |
Structure level numbers: classic bitmap font by default + graphics toggle (#4264)
## What Structure **level numbers** now render in the **`round_6x6_modified`** bitmap font by default (matching the old PIXI-based `StructureLayer` / `v31`), with a graphics setting to switch back to the smooth `overpass-bold` MSDF font. Two commits: 1. **Default to the classic bitmap font** — `StructureLevelPass` drew level digits from the `overpass-bold` MSDF atlas (the one `NamePass` uses for player names); switch the default to the `round_6x6_modified` pixel font (white digits with a baked-in dark outline). 2. **Add a runtime toggle** — load both fonts and switch between them live via a new `Classic level numbers` graphics setting. ## How - `StructureLevelPass` loads both atlases up front and selects one per frame from `settings.structureLevel.classicFont`, re-laying-out the digits when the toggle flips (digit advances differ between the fonts). The fragment shader is a single program with a `uClassic` branch: direct bitmap sample (white fill + baked outline) vs. MSDF median + synthesized outline. - New override `structure.classicNumbers` in `GraphicsOverrides` (default `true` = classic), applied onto `settings.structureLevel.classicFont` in `applyGraphicsOverrides` — so it switches live, like the existing colorblind/classic-icons toggles. - `GraphicsSettingsModal` gets a `Classic level numbers` toggle next to `Classic icons` (with `en.json` strings). ## Testing - `tsc --noEmit`, ESLint, Prettier, and `npm run build-prod` all pass. - Ran the game headless, built/upgraded cities to level 2–3, and confirmed: the classic toggle renders the pixel font, flipping it renders the smooth MSDF font, and flipping back restores the pixel font — switching live with no shader errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f4db4a33c8 |
Send nukes as motion plans and render them smoothly per frame (#4255)
## Summary Follow-up to #4244's payload work: nukes were the last per-tick movers flooding the worker → main update stream. - **Core**: nuke trajectories are fully determined at launch (precomputed parabola), so `NukeExecution` now records a `GridPathPlan` when the nuke is built — same mechanism trade ships use — and the client derives the position each tick. Per-tick `UnitUpdate`s for nukes in flight are suppressed; only targetable flips and deletion (interception/detonation) still emit. This covers atom bombs, hydrogen bombs, and MIRV warheads (dozens of per-tick movers per MIRV separation). - The plan path replays a separate pathfinder rather than reusing the stored trajectory array: the curve's cached points don't advance exactly one index per tick, and the plan must match the movement pathfinder's exact per-tick tile sequence. - `startTick` accounts for MIRV warheads' staggered `waitTicks`. - **Render**: `UnitPass.drawMissiles` now lerps each nuke's instance position `lastPos→pos` by wall-clock progress through the current tick, so nukes glide along their arc at render framerate instead of jumping once per 100ms tick. Both endpoints are real simulated positions — the rendered nuke trails the sim by at most one tick and settles exactly on it when ticks stop. Plan-driven units sync `lastPos` on path-stall ticks so the lerp never replays a segment. Shells keep their existing two-instance trail; SAM missiles are unchanged. ## Test plan - New `tests/nukes/NukeMotionPlan.test.ts`: tick-exact alignment between the recorded plan and core nuke position over the whole flight (mirroring `GameView.advanceMotionPlannedUnits` math), `waitTicks` offset, and that no per-tick unit updates are emitted in flight except targetable flips and deletion. - Full suite passes (1452 + 65), tsc/eslint/prettier clean. - Verified in-game (headless Chromium, real WebGL): atom bomb arcs from silo to target with the client position driven by the plan, missile sprite renders intact while the smoothing rewrites the instance buffer every frame, detonation FX land at the target. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e494f83e8e |
New and updated categories for maps (#4254)
Resolves #4250 ## Description: Huge update for the map categories: https://github.com/user-attachments/assets/b7dc6344-efdc-4073-b15a-92b6dccdcc19 **New Categories** - Re-adds Continental category, with the 7 traditional continents - Re-adds the category of Arcade along all its maps. - Renames "Other" to "Fictional", so that tag is more specific and feels more in-theme with the others. The info.json's of the maps that had the Other category got changed to Fictional **Map Category changes** - **achiran**: adds Europe (while the map is fictional, it is made up of real islands from ireland. (Since world includes Dyslexdria and Antarctica has Deglaciated Antarctica, both fictional , i figured for consistency we could include these mash-up maps too) - **aegean**: adds Asia category (Turkey is in Asia) - **arctic**: adds Asia category - **choppingblock**: updated "other" to "fictional", added to "new" - **deglaciatedantarctica**: updated "other" to "fictional" - **didier**: re-added to Arcade - **didierfrance**: re-added to Arcade - **dyslexdria**: updated "other" to "fictional" - **fourislands**: updated "other" to "fictional" - **hawaii**: remove north_america tag (while part of the US, hawaii is geographically only in Oceania) - **labyrinth**: added to new, re-added to Arcade - **marenostrum**: added africa and asia tags, the continents which the mediterranean borders - **onion**: re-added to Arcade - **pangaea**: updated "other" to "fictional" - **passage**: updated "other" to "fictional" - **sierpinski** re-added to Arcade - **surrounded**: updated "other" to "fictional" - **svalmel**: updated "other" to "fictional", added to europe and north_america (same logic as achiran) - **thebox**: re-added to Arcade - **tradersdream**: updated "other" to "fictional" - **worldinverted**: updated "other" to "fictional", added to "new" - **africa**: added to Continental - **antarctica**: added to Continental - **asia**: added to Continental - **europe**: added to Continental - **northamerica**: added to Continental - **southamerica**: added to Continental - **oceania**: added to Continental - **mississippiriver**: added to "new" - **korea**: added to "new" - **middleeast**: added to "new" - **balkans**: added to "new" - **indiansubcontinent**: added to "new" - **taiwanstrait**: added to "new" - **northwestpassage**: added to "new" - **southeastasia**: added to "new" - **venice**: added to "new" - **yellowsea**: added to "new" - **hongkong**: added to "new" - **titan**: added to "new" - **caribbean**: added to "new" - **juandefucastrait**: added to "new" - **danishstraits**: added to "new" ## 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: tri.star1011 |
||
|
|
ccec87943f |
Update GraphicsOverrides tests for classic-icons-by-default
Commit
|
||
|
|
82b68d16a1 |
Fix "Better troop management for nations 🤖" (#4265)
## Description: There was a check missing... The troop management stuff should be disabled for team games because nations can expect donations in that case, and its mainly relevant for FFAs. ## 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: FloPinguin |
||
|
|
49a12519d7 |
Tune structure rendering to match the previous version's look
Bring the WebGL structure renderer closer to the old canvas render: larger icons, classic icon styling by default, and more prominent, better-positioned level numbers. |
||
|
|
de84f0e867 |
mls (v5.5) (#4263)
## Description: Version identifier within MLS: v5.5 [Changed languages] - eo - fa - fr - hu - ja - ru - uk [Change volume] - Changed languages: 7 - Changed files: 7 - Changed lines: 17353 - metadata.json: unchanged Final reviewer: name This PR was generated by the PR sender tool, then checked and submitted by the final reviewer. ## 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: aotumuri |
||
|
|
03b1e0e5e7 |
Update Map Dyslexdria (#4257)
**Add approved & assigned issue number here:** Resolves #4217 ## Description: - Add addition nations. All world nations with flags and funny names. - Minor changes to map. Please do not notate this publicly. Continuous changes to Dyslexdria per its theme. ## 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: PlaysBadly |
||
|
|
5102805d77 |
Adds Warship Warship map (with additional nations and team spawn) (#4261)
> **Before opening a PR:** discuss new features on [Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small improvements as [issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose). You must be assigned to an `approved` issue — unsolicited PRs will be auto-closed. **Add approved & assigned issue number here:** Resolves #4259 ## Description: 2 Warship shaped islands next to each other. Trade ships and land attacks can go through the corners. This can be either a 2 teams or a ffa map. Size is 3000x1396 with 29% land. This will complete the 20th map for v32 before it releases in 2 days. There are 10 nations with 23 additional nations (with ai generated names). The nations are made up similarly to the ones in traders dream but they are piracy themed and theres also a meme "Evil island man" nation (rex reference) It is based on a meme when Ultimus-Rex says "warship warship" when deploying warships and now people spam "warship warship" in the comments, especially this user named @warshipwarship who comments warship warship on every video. [https://youtu.be/DGMIji0bQQM](https://github.com/openfrontio/OpenFrontIO/issues/url) <img width="3000" height="1396" alt="image" src="https://github.com/user-attachments/assets/4bf6d708-afbc-41ea-be7c-cf43fdf69cbc" /> ## 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: DISCORD_USERNAME crunchybbbbb |
||
|
|
f4840a1cfd | Add Giant World Map to the multiplayer queue after recent performance improvements. I'm hitting 40fps on a 10 year old chromebook | ||
|
|
7ec26df4b4 |
Fix three high-impact renderer performance issues (#4251)
## Summary A performance review of `src/client/render/` found three issues where per-tick work silently defeated existing optimizations. All three are surgical fixes with no behavior change. ### 1. Relation matrix forced a full-map border recompute every tick `buildRelationMatrix` ran unconditionally every tick and `updateRelations` was pushed unconditionally, so every tick paid: - a 1 MB `fill(0)` + rebuild on the CPU, - a 1 MB `texSubImage2D` upload (~10 MB/s steady-state), - a **full map-resolution border fragment pass** via `globalDirty` — which also called `scatter.clear()`, making the incremental `BorderScatterPass`/`patchTile` path dead code during live play. Now the matrix is rebuilt and uploaded only when alliances/embargoes actually change. `PlayerUpdate`s are delta-encoded (`diffPlayerUpdate` content-compares `allies`/`embargoes`), so field presence is a reliable change signal. The WebGL context-restore path force-pushes relations, matching the existing structures/railroads pattern. ### 2. Heat decay pass + full-map blit ran every frame, forever `HeatManager.decayHeat()` set `heatActive = true` on every tick regardless of whether any fallout existed. With `heatDecayPerTick: 1` the drain window (255 ticks) was always re-armed before expiring, so the map-sized decay/transition fragment pass **plus a full-map R16UI `blitFramebuffer`** ran at 60 Hz for the entire game — even if no nuke was ever fired. On large maps this was likely the biggest fixed GPU cost in the renderer. Now `TerritoryPass` flags FALLOUT-bit flips at GPU-write time (delta, drip-drain, and conservatively on full uploads), and the renderer activates the heat pipeline only then. While inactive, `updateHeat()` does no GL work at all. Skipping the prev-tile blit while inactive is safe because the transition shader only reads the fallout bit, and every fallout flip activates the pipeline before its tile flush reaches the GPU. ### 3. `computePlayerStatus` was O(players × units) per tick The per-player loop scanned **all units** looking for that player's nukes (~1M+ iterations/tick at scale). Inverted to a single pass over units building per-owner `nukeActive`/`nukeTargetsMe` sets, then O(1) lookups in the player loop. ## Testing - Full suite passes (1386 + 65 tests), including the 19 existing `computePlayerStatus` behavior tests; `tsc --noEmit` and ESLint clean. - Verified in a live singleplayer game (headless Chromium): territory fill, borders, names/troop counts, and leaderboard all render correctly. - Fallout path verified end-to-end: built a missile silo, launched an atom bomb (1235 fallout tiles in tile state), and the fallout glow rendered at the impact site — under the new gating that glow can only appear if the `falloutTouched → activate()` chain works. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bca980f572 |
Shrink the per-tick worker → main update payload by ~90% (#4244)
Stacked on #4243 (the `perf:client` harness) — first step of fixing the every-100ms main-thread stutter: make the per-tick burst small before spreading what remains across frames. ## Problem The harness showed the main-thread burst was dominated by `structuredClone` of the `updates` object, and the clone was dominated by two kinds of per-tick churn that re-sent object payloads every tick: - `gold` / `troops` / `tilesOwned` change for nearly every alive player every tick → ~278 partial `PlayerUpdate` objects per tick (world/400 bots), ~508 on giantworldmap. - Attack troop counts tick down every tick → whole `outgoingAttacks`/`incomingAttacks` arrays re-cloned for every fighting player every tick. - `playerNameViewData` (an all-players record) was cloned every tick but only recomputed every 30 ticks. ## Change Three additions to the worker → main protocol (all transferable, zero-clone): 1. **`packedPlayerUpdates`** — `[smallID, tilesOwned, gold, troops]` float64 quads for players whose stats changed. These fields no longer appear in `PlayerUpdate` diffs (first emissions still carry the full snapshot). Gold is exact in a float64 (game values ≪ 2^53). 2. **`packedAttackUpdates`** — `[ownerSmallID, direction, index, troops]` quads. Attack arrays are only resent when membership/order/retreating changes — which is exactly the condition that keeps the patch indexes valid (a tick either resends an array or patches it, never both). 3. **`playerNameViewData` is now optional** — attached only on placement-rebuild ticks (spawn ticks, first ticks, every 30th, spawn end). The client keeps the last applied values; dead players' name placements freeze at death (matching the previous effective behavior). On the client, `GameView.populateFrame` now also rebuilds `names` / `relationMatrix` / `allianceClusters` only when their inputs changed that tick — field presence on a partial `PlayerUpdate` marks them dirty. (`playerStatus`, nuke telegraphs, and attack rings still recompute every tick; they're tick- or unit-dependent.) ## Results (perf:client, this machine; low-end devices ~5–20× slower) Default run (world, 400 bots, 1800 ticks): | stage | before | after | |---|---|---| | clone (serialize+deserialize) | 1.02ms | **0.09ms** | | GameView.update | 0.62ms | **0.29ms** | | WebGLFrameBuilder.update | 0.04ms | 0.04ms | | **TOTAL burst mean** | **1.67ms** | **0.42ms** | | TOTAL p99 / max | 3.47 / 10.3ms | **1.21 / 3.92ms** | giantworldmap/600t: 2.54 → 0.68ms mean. Player update objects: 278 → 6.5 per tick (world), 508 → 12 (giant). The remaining burst is mostly tile apply + per-tick derivations — the part that frame-spreading (next step) addresses. ## Verification - **Sim final hash unchanged** on all three reference configs (`5607618202213430`, `29309648281599524`, `39945089450032050`) — no simulation behavior change. - **View hash unchanged** on all three configs (`942106e9`, `a3aae227`, `cbaaf265`) — the rendered view state is provably identical tick-for-tick, including the name-freeze semantics. - New tests: `tests/PackedPlayerUpdates.test.ts` (drain + GameRunner cadence), packed-channel and freeze-at-death cases in `tests/client/view/GameView.test.ts`, `packAttackTroopDeltas` unit tests and updated diff contract in `tests/GameUpdateUtils.test.ts` / `tests/PlayerUpdateDiff.test.ts`. - `npm test` (1490 tests), `eslint`, `prettier`, `tsc --noEmit` all pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <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. |
||
|
|
81c5fcfb16 |
Fix events display showing troop donation amounts 10x too high
Troops are stored internally at 10x their displayed value, but the donation event message formatted the raw amount with renderNumber instead of renderTroops. Gold is unscaled and was already correct. |
||
|
|
ac6d8d739a |
Make attack ring size tunable and increase it to 30px
The transport-target ring size was hardcoded as RING_SCREEN_PX in attack-ring.vert.glsl. Promote it to a uRingScreenPx uniform fed from a new fx.attackRingScreenPx entry in render-settings.json, with an "Attack Ring Size (px)" slider in the debug GUI's FX folder. Also bump the size from 20 to 30 screen px so the ring is easier to spot. The inner/outer ring fractions (0.5/0.8 of the quad) stay shader constants. |
||
|
|
03b405eea7 |
Color nuke telegraph circles by launcher relation (self/ally/enemy)
The blast-radius warning circle was always red, so players couldn't tell who launched an incoming nuke. Now it's green for your own nukes, yellow for ally/teammate nukes, and red for everyone else's. Each telegraph carries a relation (0=self, 1=friendly, 2=enemy), classified from the per-tick relation matrix — the same friend/foe logic alt-view uses — and passed to the shader as a per-instance attribute. Replay/spectator mode (no local player) stays all red. Colors are tunable via the nukeTelegraph slice in render-settings.json. |
||
|
|
32011d2ed2 |
Fix a river in Balkans not connecting to the sea (#4249)
Resolves #4248 ## Description: Fix river not connected in Balkans map along the map border. The map generator accidentally deleted some columns of pixels along the map limits, and it disconnected a river. <img width="588" height="482" alt="image" src="https://github.com/user-attachments/assets/2c78b6bd-d669-4aef-bc1d-c69d4aeed162" /> Updated version <img width="290" height="311" alt="image" src="https://github.com/user-attachments/assets/f315bdfc-bcca-400d-95a7-876c14e47400" /> ## 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: tri.star1011 |
||
|
|
b85d1fc372 |
Fix alt-view coloring teammates as enemies in team games (#4247)
## Problem In team games, alternate view (space-hold) colored teammates' units red (enemy color) instead of yellow (ally color). Teammates' territory borders had the same problem. ## Root cause `buildRelationMatrix()` in `src/client/render/frame/derive/RelationMatrix.ts` already supports an optional `teams` map that marks same-team pairs as `RELATION_FRIENDLY`, but the call site in `GameView.populateFrame()` never passed it (the companion `buildTeamMap` helper was dead code). Only explicit alliances were marked friendly, so a teammate without a formal alliance read as neutral — and the alt-view unit palette maps neutral to the enemy color. ## Fix - `GameView` now tracks a `smallID → team` map, populated when each `PlayerView` is first created (team is a static field, so once per player is enough). - The map is passed through to `buildRelationMatrix()`, which feeds both the `AffiliationPalette` (unit colors) and `BorderComputePass` (border colors). ## Testing - New regression test in `tests/client/view/GameView.test.ts`: same-team players are `RELATION_FRIENDLY` in `frame.relationMatrix`, cross-team players stay neutral. - All 36 GameView tests pass; typecheck clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9e2648f80c | increase nuke trajectory line width from 1.25=>2.5 so it's more visible | ||
|
|
5648a37317 |
Classic icons: darken player color for icon glyph instead of black (#4246)
## Summary The "classic icons" graphics setting currently renders structure icon glyphs as flat black. In the v0.31 canvas renderer, classic icons used `structureColors().dark` — a darkened version of the owning player's territory color. This PR restores that look in the WebGL renderer. - New `structure.iconDarken` render setting (HSV value multiplier on the player fill color; `0` = off, default). - New `uIconDarken` uniform in `structure.frag.glsl`: when > 0, the glyph color is `darken(playerFill, uIconDarken)` instead of the flat `uIconColor`. - Classic mode (`classicIcons: true`) now sets `iconDarken = 0.45` instead of `iconR/G/B = 0`. Border darken, fill, and the 0.75 translucency are unchanged. - Default (non-classic) icons are unaffected (white glyph, `iconDarken = 0`). Under-construction structures keep the gray fill, so their glyph darkens to a darker gray — matching v31's construction styling. ## Verification Drove a solo game headlessly with classic icons on and built structures: glyphs render as darkened versions of each player's color (dark purple on a purple player, per-bot hues on bot structures). Pixel-sampled the screenshot: glyph measured `rgb(89,58,142)` vs `rgb(84,50,139)` predicted for the 0.45-darkened player color at 0.75 alpha (flat black would measure `rgb(38,26,60)`). Control run with classic off shows the unchanged white glyph. `tests/GraphicsOverrides.test.ts` updated; all pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
769d0c687f |
Extend run-openfront skill with headless in-game WebGL testing (#4245)
## What
Extends the `run-openfront` Claude Code skill so agents can test the
*whole game* headlessly, not just the home page and modals. New
`game.mjs` driver plays an actual singleplayer game end-to-end:
- start a solo game with chosen options (bots, map, difficulty, …)
- spawn, attack/expand, open the radial build menu
- read ground-truth sim state (`ticks`, `inSpawnPhase`, `myPlayer`
troops/gold/tiles, `outgoingAttacks`) instead of guessing from pixels
- take real WebGL screenshots (SwiftShader renders the map fine
headless)
`node .claude/skills/run-openfront/game.mjs` runs a ~2 min smoke flow
that asserts territory growth after an expansion attack and that the
radial menu opens.
## How
No game-code changes were needed:
- `hud/GameRenderer.ts` already assigns the `GameView` and
`TransformHandler` onto the `<build-menu>` element, so page JS reaches
live sim state and world↔screen conversion through it.
- `launch({ rafIntervalMs })` stubs `requestAnimationFrame` to one frame
per interval. SwiftShader needs seconds of CPU per frame, and an
unthrottled frame loop starves the main thread — the singleplayer turn
loop drops from 10 ticks/s to ~0.3. Throttled, the sim runs near full
speed while frames still render for screenshots.
- `clickWorld()` absorbs the canvas-click pitfalls discovered while
testing: aims at tile centers (corner clicks floor onto the neighboring
tile), refuses to click through HUD elements covering
`#game-input-overlay`, and freezes the post-spawn camera animation so
computed coordinates don't go stale.
## Testing
Smoke flow run repeatedly on a headless 4-core box: game starts (123
players), spawn lands on the clicked tile, expansion attack grows
territory 52 → ~275 tiles, radial menu opens, screenshots show the
rendered map.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
19db66f424 |
Delayed lobby start (#4184)
Resolves #4169 ## Description: Adds a delayed lobby start option. Utilizes the same system as for public lobbies. The default for the option is for lobbies to take 3 seconds to start, however this can easily be changed. The current setting is controlled through an enable-disable slider, however there are multiple other options for how to control this. For example we could do a slider, an input field, a dropdown etc. And i dont necessarily know if the currently implemented option is the best. Furhtermore im not sure if i have used the language file completely correctly. There is now a duplicate field for both private and public lobby. However there is not category shared between the two. So i decided to reuse the field from public for private games, as this simplified the code a bit. **Host video** https://github.com/user-attachments/assets/6f3db6e4-7323-4fad-8544-efb8cef4d969 **Non-host video** https://github.com/user-attachments/assets/ee02a072-1f42-4dde-a5d9-120fda862eb7 ## 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: FrederikJA |