Commit Graph

3988 Commits

Author SHA1 Message Date
evanpelle cdcc774793 Hide subscriptions in store and account modal behind a feature flag
Subscriptions aren't ready yet. Add SUBSCRIPTIONS_ENABLED (currently
false) in Cosmetics.ts to gate the Subscriptions store tab, the
subscription panel on the account modal, and its cosmetics fetch.
Flip the flag to true to re-enable.
2026-06-11 17:07:25 -07:00
evanpelle f50456c688 Fix crash when HeadsUpMessage renders before game is assigned 2026-06-11 15:48:38 -07:00
evanpelle 74fc239f96 Dim untargetable nukes so players can tell SAMs can't hit them
Nukes flying outside SAM-targetable range now render at reduced alpha
(unit.untargetableAlpha, default 0.6), including the hydrogen bomb's
glow halo. Adds a FLAG_FLICKER_UNTARGETABLE instance flag in UnitPass
driven by the existing UnitState.targetable field.

Also fixes the alt-view trade-friendly check to match its flag exactly,
so retreating warships (flag 4) no longer render ally-yellow in alt view.
2026-06-11 15:44:02 -07:00
evanpelle 03a5d691ee Add white glow behind hovered player's name
The hovered player (tile owner under the cursor, already tracked via
uHighlightOwnerID for cull bypass) now gets a soft white glow behind
their name. The glow is derived from the MSDF distance field: a white
band past the outline with quadratic falloff, composited behind the
glyph and clamped to the SDF margin so it never clips at quad edges.

Glow size and strength are tunable via hoverGlowWidth/hoverGlowAlpha
in render-settings.json, exposed as sliders in the graphics settings
modal (persisted as graphics overrides) and in the debug GUI.

Includes schema and apply tests for the new override fields,
covering the 0 edge case (0 disables the glow, not "unset").
2026-06-11 15:27:56 -07:00
evanpelle 2d747d0f8b Flash alliance icon when renewal prompt is active
When an alliance is within the renewal-prompt window, the alliance
icon above the player's name now pulses, ramping from 2 Hz to 5 Hz
as expiry approaches (same effect as the traitor flash).

The flash window is driven by allianceExtensionPromptOffset() — the
same Config value that triggers the "renew alliance" prompt in the
actionable events display — so the two always stay in sync.

The shader only knew the alliance fraction, not absolute time, so
computePlayerStatus now also emits allianceRemainingTicks, packed
into the free pd7.w slot of the player-data texture.
2026-06-11 14:41:46 -07:00
evanpelle 95d5a0439b Make SAM intercept X marker 5x larger
Bump markerXRadius from 8 to 40 px in render-settings.json so the
intercept point on nuke trajectories is easier to spot. The marker
shader draws in normalized quad space, so the stroke and outline
scale with it automatically. Also raise the debug GUI slider max
for the X marker from 16 to 64 to keep the new value tunable.
2026-06-11 14:12:12 -07:00
Evan 1db02acdc2 Move theme data into the render-settings JSON pipeline (#4223)
**Add approved & assigned issue number here:**

N/A — maintainer refactor.

## Description:

Replaces the theme class hierarchy
(`BaseTheme`/`PastelTheme`/`ColorblindTheme`) with theme JSON files —
`default-theme.json` and `colorblind-theme.json` — combined with
`render-settings.json` at runtime into a single graphics-configuration
pipeline (`settings.theme`). One `SettingsTheme` class keeps the
algorithms (color allocation, team-variation generation, LAB-contrast
structure colors) and reads all data from `ThemeSettings`; adding a
theme is now just adding a JSON file.

Colorblind mode (#4150) is fully preserved:

- Same palettes — the 32-color CVD-safe pool and Okabe-Ito team colors
are baked into `colorblind-theme.json`
- The relative border rule (`l × 0.6`) is expressed as a
`borderLightnessScale` knob alongside the default theme's absolute
`borderDarken`
- The mid-game re-theme wiring (`refreshPlayerColors`/`refreshPalette`)
and the affiliation/friend-foe tint overrides are unchanged;
`applyGraphicsOverrides` now also swaps the `settings.theme` slice
- `deepAssign` replaces arrays wholesale so differing palette lengths
survive theme switches

Verified against the previous implementation with an equivalence test
(since removed): default-theme colors are byte-identical including
allocation order; colorblind team/derived colors are byte-identical, and
FFA assignment may permute within the same palette (hex baking rounds
upstream's fractional-RGB colord objects, which can flip the allocator's
greedy delta-E ordering — rendered colors round identically either way).

Also removes dead theme surface (`terrainColor`, `backgroundColor`,
`falloutColor`, `font`, `textColor`, spawn-highlight variants,
`PastelThemeDark`) — GL terrain colors and dark mode were already
handled in the renderer. Note this means the colorblind terrain bands
from #4150 were dead code (nothing calls `terrainColor`; GL terrain
comes from `ColorUtils.encodeTerrainTile`); wiring CVD-safe terrain into
the terrain texture would be a follow-up.

## Please complete the following:

- [x] I have added screenshots for all UI updates — N/A, no UI changes
(verified color-identical)
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file — N/A, no user-visible text
- [x] I have added relevant tests to the test directory —
`tests/Colors.test.ts` updated for the new pipeline (team colors from
theme JSON, colorblind palette/border tests)

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evanpelle

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:50:50 -07:00
evanpelle 3c0ff7a6f2 Fail open on clan tag ownership checks when API is unavailable
The clan-tag ownership check previously failed closed: when the API
service was unreachable (e.g. during local development), the client
dropped the tag with a "couldn't verify" error and the server's
FailOpenPrivilegeChecker treated every unverifiable tag as reserved.
This made clan tags unusable whenever the API was down.

- Client: checkClanTagOwnership keeps the tag when the existence
  probe is inconclusive; the server still re-checks authoritatively.
- Server: FailOpenPrivilegeChecker passes tags through instead of
  dropping non-member tags; decideClanTag now takes a non-nullable
  reserved set since the null case is gone.
- Remove the now-unused username.tag_check_failed translation key.
- Update Privilege and ClanApiQueries tests for fail-open behavior.

Trade-off: if the reserved-tag list is unavailable in production,
real clan tags can be impersonated until the first successful
PrivilegeRefresher load; after that the last good checker is retained.
2026-06-11 12:22:33 -07:00
a-happy-goose 625d54c128 [small-fix 20 lines] Add FFA collusion warning (#4107)
Resolves #3900

## Description:
During the spawn phase in FFA games, display a collusion warning to
clearly communicate to new users that pre-game agreement is not allowed.
<img width="1362" height="662" alt="2026-06-01_20-31"
src="https://github.com/user-attachments/assets/bd083e91-280a-41e1-a11a-d69d5f16bc8a"
/>

- [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:

goose126

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
2026-06-11 12:15:34 -07:00
RickD004 7405339ea7 Add Titan map with random spawn nations - along new Cosmic map category (#4183)
Resolves #4182

## Description:

Adds "Titan" (real moon of Saturn with methane seas) map . Uses new
random spawn nation feature by FloPinguin.
https://github.com/openfrontio/OpenFrontIO/pull/4156

Also adds new Cosmic map category. The "Other" map category has become a
wastebasket of unrelated maps, and with increasing number of maps, i
think its a good addition to have better categories for these maps.

I figured these 2 changes should go together since im adding a cosmic
map, and a cosmic category.

proof of nations spawning randomly and how the cosmic category looks in
the menu:


https://github.com/user-attachments/assets/b84bd3ef-6b8f-46fe-a6ea-ea5e79c6dc00

## 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

---------

Co-authored-by: Evan <evanpelle@gmail.com>
2026-06-11 10:55:46 -07:00
noahschmal 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
2026-06-11 10:53:03 -07:00
Evan 7137347b7d Fade player names under the cursor, with a graphics setting to tune it (#4221)
## Description:

Player name plates can block the view of what's underneath them
(structures, units, terrain). This PR fades the entire name plate —
name, troop count, flag, and emoji/status row — to 25% opacity while the
cursor is over it, so you can see and click what's behind it.

**How it works:**

- `HoverHighlightController` pushes the cursor's world position into the
renderer on mouse move.
- `NamePass` hit-tests the cursor against each player's name plate
bounds on the CPU (mirroring the lerp/sizing math in `name.vert.glsl`)
and passes the matched player's ID to the text, icon, and status-icon
programs, which apply the alpha multiplier in their shaders.

**Graphics setting:**

- New "Name opacity under cursor" slider in the Graphics Settings modal
(Name Labels section), range 0–1, default 0.25. Setting it to 1 disables
the fade entirely.
- Wired through the existing `GraphicsOverrides` pipeline: changes apply
live and are cleared by "Reset to defaults".
- Tuning knob exposed as `name.hoverFadeAlpha` in `render-settings.json`
and the debug GUI.

## 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
- [ ] 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:

evan
2026-06-11 09:25:13 -07:00
RickD004 af2849a2d7 Adds "Juan De Fuca Strait" map - 3 way team map (#4215)
Resolves #4148

## Description:

Adds "Juan de Fuca Strait" map. This is the Strait in Washington and
British Columbia: https://en.wikipedia.org/wiki/Strait_of_Juan_de_Fuca

This map is meant to be a brand new 3-team way map, since all the team
maps we have are either made for 2 or 4 teams.
The map is bumped towards this gamemode similar to how Baikal is bumped
to 2 teams.

Map also has Additional Nations, for a total fof 62, for Human vs
Nations and solo games
<img width="1365" height="602" alt="image"
src="https://github.com/user-attachments/assets/9cb86727-db06-4fcb-bee4-85e7b5d47d15"
/>
<img width="1319" height="488" alt="image"
src="https://github.com/user-attachments/assets/13fd9a01-7ec6-49ab-81c3-40b566cbf6e0"
/>
data from OpenTopography, already credited

## 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
2026-06-10 20:00:53 -07:00
evanpelle a39413c947 Show newest actionable events on top to prevent misclicks
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:04:34 -07:00
evanpelle 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
2026-06-10 18:57:02 -07:00
FloPinguin b0e7d04f6e Add help notification system to control panel ℹ️ (#4212)
Resolves https://github.com/openfrontio/OpenFrontIO/issues/3445

## Description:

I copied the PR #3743 from @luctrate (Add army limit warning indicator
for team games) to this PR because he didn't respond to requested
changes but I thought it's important.

I expanded on it, now its a full help message system:

**Warnings (orange):**
- Army limit: shown in team games with donations when troops exceed 80%
of max
- Low troops: shown when troops drop below 1k (=> new noob player who
clicks too much)

<img width="764" height="251"
alt="582494157-cf19b13e-a0a9-44e4-8de8-86c007fe9c79"
src="https://github.com/user-attachments/assets/6b4996d9-1993-4d2c-98ba-afba17a5ca4d"
/>

**Info messages (blue):**
- Borders a traitor ally: "You can betray traitors without becoming a
traitor yourself" (Because its not obvious for new players)
- Borders an allied AFK player: "You can attack disconnected players
even if you are allied with them" (Because its not obvious for new
players)
- Borders an AFK teammate: "You can attack disconnected teammates"
(Because its not obvious for new players)

Info messages only appear when the player has not attacked the relevant
neighbor for at least 15 seconds, so they do not show up without reason.

<img width="524" height="141" alt="image"
src="https://github.com/user-attachments/assets/88d74661-d47e-45a7-9f91-d4f5361114b7"
/>

New "Help Messages" toggle in settings (default: on)

<img width="409" height="105" alt="image"
src="https://github.com/user-attachments/assets/24bc8bed-777b-4f72-9451-02116ac39db0"
/>

Implementation details:
- Border detection uses async borderTiles() refreshed every 1s, cached
in a Set of nearby player smallIDs
- Outgoing attacks are tracked per-target to compute the 15-second idle
threshold
- New armyLimitWarningThreshold() on Config (returns 0.8)
- All user-facing strings go through translateText() with en.json
entries

AI Model used: MiMo 2.5 Pro

## 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
2026-06-10 17:00:24 -07:00
evanpelle 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.
2026-06-10 16:54:05 -07:00
evanpelle 9e80d534fb Align nuke trajectory preview silo selection with NukeExecution
The preview arc could show a nuke originating from a silo the game would
never fire from. The renderer's silo selection had diverged from the
authoritative PlayerImpl.nukeSpawn that NukeExecution uses:

- Eligibility: the renderer only excluded inactive silos, but the game also
  excludes silos that are reloading (isInCooldown) or under construction.
- Distance: the renderer used Euclidean distance; the game uses Manhattan.

Add isInCooldown() to UnitView mirroring UnitImpl, and update the trajectory
preview to filter on isActive && !isInCooldown && !isUnderConstruction and
pick the nearest silo by Manhattan distance. When no silo is eligible the
trajectory clears, matching canBuild returning false.
2026-06-10 16:26:29 -07:00
evanpelle dceb2798b3 Smooth build-ghost range circle and nuke trajectory to cursor
The build-ghost icon already tracked the cursor at sub-tile precision, but
the range circle (defense post / SAM / nuke radius) and the nuke trajectory
arc still snapped to the hover tile, making them look jagged as the cursor
moved.

Range circle: cursorLoop now smooths radiusTileX/Y the same way as the icon,
except when upgrading an existing structure (the circle stays anchored to
that structure's real tile).

Nuke trajectory: split the work by cadence. The throttled renderGhost path
caches the static inputs (nearest silo + threatening SAMs) in
nukeTrajectoryStatic; cursorLoop rebuilds the Bezier each frame with the
live cursor as the destination. Source stays on the silo's tile; only the
endpoint follows the cursor.

All three previews now use the same tile-center (+0.5) convention.
2026-06-10 16:20:14 -07:00
evanpelle 5cb155267d Only notify on warship and transport ship destruction
displayMessageOnDeleted fired a "Your {unit} was destroyed" message for
nearly every unit type (denylisting only MIRV warheads and train cars),
which spammed the event feed with low-stakes losses like ports, cities,
and trade ships.

Flip it to an allowlist: only surface destruction of warships and
transport ships, the two losses with real tactical weight (lost naval
control, lost army at sea). Everything else is either visible on the map
or not worth a notification.
2026-06-10 16:00:29 -07:00
evanpelle 9396df1ca4 Remove unit capture event messages
Drop the "Your {unit} was captured by {name}" and "Captured {unit} from
{name}" display messages on unit ownership change in UnitImpl. They fired
on every capture — dominated by warships taking trade ships — and were
too spammy to be useful, so players tuned them out.

Also clean up the now-unused pieces:
- Remove the UNIT_CAPTURED_BY_ENEMY message type, its category mapping,
  and its case in getMessageTypeClasses.
- Remove the orphaned unit_captured_by_enemy and captured_enemy_unit
  en.json keys.

CAPTURED_ENEMY_UNIT is kept — still used by the trade-ship gold message.
2026-06-10 15:58:25 -07:00
evanpelle 042820d56c Fix alliance request event display edge cases
Suppress the "Alliance request sent" event when the action actually
accepts an existing incoming request rather than sending a new one.
This happens both when clicking "Accept" on a request card and when
sending a request to a player who already requested one from us — in
both cases EventsDisplay now checks whether the recipient is already
requesting an alliance with us and skips the confirmation.

Also clear stale incoming alliance request cards in ActionableEvents
once the request is resolved: handle AllianceRequestReply to remove the
card when accepted/rejected, and drop ALLIANCE_REQUEST cards in tick()
whose requestor is no longer requesting an alliance with us.
2026-06-10 15:40:41 -07:00
Katokoda 90efb84168 Fix/expiration window staying after expiration (#4213)
Resolves #4209

## Description:

While filtering events in the src/client/hud/layers/ActionableEvents.ts
tick function, we remove from the event list any event where the
requestor is not requesting an alliance anymore. This excludes alliance
requests where the requestor attacked (or bombed) AND alliance requests
where the recipient accepted through the radial menu.

## 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:
Katokoda
2026-06-10 13:53:26 -07:00
bijx fe0b79ef21 Feat: Favourite maps tab (#4207)
Resolves #4202 

## Description:

As suggested in some suggestions in the main OF server
[[thread](https://discord.com/channels/1284581928254701718/1472496670267805782)],
we should have a map favouriting system since there are over 70 maps
already. People (myself included) have some maps we constantly play
during solo/private matches, so a favourite tab would be huge.

This feature adds the favourites tab to the solo and private match
selection screens. It works using localStorage for saving (device
persistence) but I can just as easily implement an infra update where
players have a 1-many relation with a `FavouriteMaps` table. That can be
a future solution. Video example right now:


https://github.com/user-attachments/assets/e8e278ab-d305-499a-81a9-d570e05db051


## 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:

bijx
2026-06-10 13:51:37 -07:00
Katokoda 9e9708468c Fix/nation names special caracters (#4195)
> **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 #4165

## Description:

This PR update the test checking validity of Nation Names to include the
new character constraint explained below.
It also fixes the 10 Nations that invalid characters (that did not
render correctly on the map).

**The new character constraint**
According to testing, the game map renders correctly all safe
Extended-ASCII characters (non colored in www.ascii-code.com =
[0x20–0x7E] or [0xA0-0xFF]). Other characters, when present in Nation
Names, are rendered correctly in the rest of the game but not on the
map, where they are trimmed to the last byte, which is then interpreted
as Extended-ASCII and rendered if possible.


**How to quickly check my assertion**
1. Change the file resources/maps/world/manifest.json, renaming one of
the countries to "a.á.आ!š!慢!".
2. Start a game on the world map without any bots
3. Verify that the nation name is well displayed in its overlay but is
shown as "a.á.!a!b!" on the map.
(characters before a point are preserved, but characters before an
exclamation mark are missing/changed).
4. run `npm run test` and notice that the NationName test fails and
lists the three non-valid characters.

Explanation: The string is represented in UNICODE-16 as
\u0061\u002e\u00e1\u002e\u0906\u0021\u0161\u0021\u6162\u0021.
Which, when we keep only the right-most byte of each character gives:
61 2e e1 2e 06 21 61 21 62 21
And, converted in Extended-ASCII gives:
a.á.�!a!b!
(which matches the showed name if we discard the control character).

**The 10 Nations which needed a fix**
Utqiaġvik from the Bearing Strait.
Ar Rayyān from the Strait of Hormuz.
6 Nations in the Bosphorus Straits.
2 Easter-egg Nations from Luna.

The 8 real-world Nations were adapted by simply removing the diacritics
(after confirmation from a speaker of arabic and turkish, but sadly none
for the Utqiaġvik Nation).
The Secret Base from Luna was renamed "T0Þ $e¢®ët Mi|¡tªr¥ ß@§£", all
within Extended-ASCII, keeping the same spirit as the original name.

However, the Monolith Nation (previously named ▊, without any flag) has
changed quite a lot and needs some explanation.

**Easter-egg Nation Monolith**
The new name is "ΜΟΝΟʟΙȚΗ", which is entirely outside of the valid
character zone but in a way that entirely disappears on the map (as the
आ character in the example above). This means that on the map, the
Nation has no name and only its Monolith-flag.
However, in all other places (leaderboard, overlay, alliances, warnings,
etc.) the name is displayed correctly.
The included test excludes this precise name from its violation list.

<img width="1512" height="632" alt="image"
src="https://github.com/user-attachments/assets/998693f2-edb4-417c-9054-35dc4819a57d"
/>
The Monolith Nation without its name but with a Monolith flag.

## 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:

Katokoda
2026-06-10 13:44:37 -07:00
Aotumuri dda47b0813 Make clan tag warning clickable (#4163)
> **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 #4154

## Description:

Adds a join path from reserved clan tag warnings to the clan detail
modal.


https://github.com/user-attachments/assets/cc0f4cb8-be8e-414a-8147-7a744069999e


## 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:

aotumuri
2026-06-10 13:42:22 -07:00
Cameron Clark e38b25f206 Fix missing boat sprite icon in attacks panel (#4141)
Resolves #4100

## Description:

The boat row in the attacks panel (bottom-right UI) rendered an empty
slot where the tinted boat sprite icon should appear, for both incoming
and outgoing transport boats.

Root cause: `loadAllSprites()` in `SpriteLoader.ts` was never called. It
was previously invoked by a canvas layer that has since been deleted, so
the sprite map stayed empty. As a result `getColoredSprite()` threw,
`AttacksDisplay.getBoatSpriteDataURL()` caught the error and returned
`""`, and the icon rendered blank.

This fix calls `loadAllSprites()` from `AttacksDisplay.init()`
(currently the only consumer of the sprite loader), so the sprite map is
populated at startup.

### Demo after fix:
<img width="800" height="572" alt="CleanShot 2026-06-03 at 18 51 01"
src="https://github.com/user-attachments/assets/e64a1ef7-da48-4662-a8c4-7234a8307730"
/>

## 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 (N/A — no user-facing text added;
only a console error log)
- [x] I have added relevant tests to the test directory (smoke tested
locally, see demo recording above)

## Please put your Discord username so you can be contacted if a bug or
regression is found:

cool_clarky
2026-06-10 13:39:53 -07:00
FloPinguin 3aaf0ea05d Remove lakes from the game 🌊 (#4214)
## Description:

Nametags look weird here because on the left is a lake:

<img width="954" height="765" alt="Screenshot 2026-06-10 170116"
src="https://github.com/user-attachments/assets/2b679a68-fab3-458e-8e29-e12b9a4f281b"
/>

I removed isLake from the nametag position calculation

Because isLake was unused then, I removed it completely.

Full changelog:

- Remove isLake() from GameMap interface, GameMapImpl, GameImpl, and
GameView
- Remove TerrainType.Lake enum value
- terrainType() now returns Ocean for all water tiles (previously
distinguished lake vs ocean, but nothing treated them differently)
- Remove Lake case from PastelTheme and PastelThemeDark (already fell
through to Ocean)
- Exclude lakes from nametag placement grid in NameBoxCalculator

Maybe as a next step also remove lakes metadata from the map generator?

AI Model used: MiMo 2.5 Pro

## 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
2026-06-10 13:20:52 -07:00
evanpelle f17ca9bd65 meta: increase trade ship sigmoid midpoint 200 => 400 2026-06-10 10:39:59 -07:00
evanpelle 3552b08f7a Color player name labels by player type
Name text fill now darkens based on player type so human players stand
out from AI: human = black, nation = a bit gray, bot = greyer. Shades are
tunable in render-settings.json (nameShadeNation, nameShadeBot; human is
always 0).

Repurpose the previously-unused pd3.z slot (was isHuman, dead in the
fragment shader) to carry a per-player grayscale shade, and use it as the
name fill color directly so it applies in both day and night.
2026-06-09 19:58:46 -07:00
evanpelle b5840d7887 Fix troop count precision in name labels, throttle/stagger updates
Replace the hand-rolled formatTroops() in the name-pass with the canonical
renderTroops() so map name labels match troop precision used elsewhere in
the UI (Leaderboard, PlayerPanel, etc.).

Also refresh each player's troop string at most every 500ms instead of
every simulation tick, staggered by slot index so GPU string uploads
spread across the window rather than bursting on a single tick.
2026-06-09 19:35:32 -07:00
evanpelle cb9cab9aca Keep static spawn timer for singleplayer games
PR #4198 made the spawn-phase timer count down numSpawnPhaseTurns(), but
singleplayer never adds SpawnTimerExecution (GameRunner.ts), so its spawn
phase doesn't end on a timer — it ends when the player spawns. The
countdown would tick to 0 at ~10s while the phase kept going.

In GameRightSidebar.tick(), restore the old static display (maxTimerValue
* 60, or 0 when unset) during spawn phase for Singleplayer games, leaving
the countdown for all other game types. Uses an explicit gameType check
rather than _isSinglePlayer so replays of multiplayer games still count
down.
2026-06-09 19:22:11 -07:00
evanpelle 2d28d5463b Add territory saturation and opacity graphics settings
Expose two new user-configurable map-overlay controls in the graphics
settings modal: territory saturation (mutes fill colors toward grayscale)
and territory opacity (lets terrain show through the fill).

The territory fragment shader blends the fill toward its luminance based
on uSaturation and applies uTerritoryAlpha as the absolute fill opacity.
Both are wired through RenderSettings, the GraphicsOverrides schema,
applyGraphicsOverrides, the debug Layout sliders, and TerritoryPass
uniforms, with defaults (saturation 1, alpha 0.588) in render-settings.json.
Adds the corresponding en.json label/description strings.
2026-06-09 19:16:04 -07:00
crunchybbb 855695b78e Adds Hong Kong map (#4191)
> **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 #4152(issue number)

## Description:


- Adds a map of Hong Kong. The size is 2781x1997 with land area of 41%
(2.2mil pixels). The islands, straits, harbors, coastlines and
peninsulas make for some very intersting gameplay.
- HK is the second densest place on earth. To simulate this, there are
71 nations based on districts, parks, islands, etc. (Kowloon and HK
Island are so crowded with nations, there may be only 1-2 tribes that
spawn there!)
- Large coastal plains, passes and mountain ranges across islands and
the mainland

map image
<img width="2781" height="1997" alt="hk-improvedriver"
src="https://github.com/user-attachments/assets/ef324fca-88f7-487c-adb0-fa31fc370458"
/>

showcase https://www.youtube.com/watch?v=DosBDttQVmE

## 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

---------

Co-authored-by: RickD004 <realtacoco@gmail.com>
2026-06-09 18:39:48 -07:00
Blake Girardet 90e4dd0677 Fixes malformed flag svg url in playerRow (#4203)
Resolves #4194 

## Description:

Fixes the malformed flag svg link when viewing the player row component.

This has been tested by temporarily registering a route to the game-info
modal locally and confirming the flag svg now loads.

Local before

<img width="698" height="500" alt="image"
src="https://github.com/user-attachments/assets/a5bd0958-e4f2-4ab6-9203-b49e42a34ca7"
/>

---
Local after

<img width="770" height="573" alt="Screenshot 2026-06-09 at 6 56 17 PM"
src="https://github.com/user-attachments/assets/ffc64c50-f0d9-4c22-9325-34924b68c985"
/>

## 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:

Caidora
2026-06-09 18:39:19 -07:00
evanpelle 65e99b25e7 Add retreating warship indicator and warship 2-color treatment
Warships now render with a dedicated center accent band so their state
reads at a glance:
- Normal: center + outer ring share the territory color (2-color look),
  hull uses the border color.
- Angry (attacking): outer ring and center turn red.
- Retreating to repair: the center blinks black.

The warship sprite center moved to its own gray value (100) in the unit
atlas so the shader can drive it via a new fourth replacement band, with
no per-unit-type branching — the missiles' shared 130 blend band is
untouched.

Warship repair-retreat (warshipState.state === "retreating") now feeds
the existing UnitState.retreating boolean in UnitView, which UnitPass
maps to a FLAG_RETREATING instance flag.
2026-06-08 17:32:21 -07:00
evanpelle 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
2026-06-08 16:04:28 -07:00
evanpelle 99a20ac032 Shrink warship shell sprite to a single pixel
The shell (unit-atlas.png col 7) was a centered 3×3 white square, so it
rendered as a 3×3-world-tile block. Replace it with a single centered
white pixel so shells render as one pixel, matching the original
pixel-shell look. The atlas is sampled with NEAREST and unitSize is 13
(1 atlas px ≈ 1 world tile), so the lone pixel stays crisp.

Update the UnitPass header comments that described the shell as 3×3.
2026-06-08 14:31:02 -07:00
evanpelle ea95069604 Move special effects toggle to graphics settings, wire to renderer
The special effects toggle wrote settings.specialEffects but nothing in
the WebGL pipeline read it — the FX pass is gated on passEnabled.fx. The
setting was orphaned when the old canvas renderer was removed, so the
toggle had no visual effect.

Move the toggle into the graphics settings modal (under a new Effects
section) and remove it from the in-game settings modal and the homepage
user settings modal. Rewire it to a passEnabled.fx graphics override so
it actually toggles the FX pass, applied live via the existing graphics
override listener.

Delete the now-dead fxLayer()/toggleFxLayer() from UserSettings.

Note: users who previously disabled special effects will reset to on,
since the old settings.specialEffects key is no longer read.
2026-06-08 14:15:07 -07:00
evanpelle 1c1728f6fa Add map hover/railroad graphics overrides and fix territory highlight
Extend GraphicsOverrides with a mapOverlay group (territory highlight,
border highlight amount, border highlight thickness) and a railroad
group (train track draw distance), wired through the schema,
applyGraphicsOverrides, and new sliders in the graphics settings modal.

Fix the territory hover highlight: the shader received uHighlightBrighten
but ignored it, applying a hardcoded saturation boost so the setting had
no effect. It now drives a contrast boost (push channels away from
mid-gray), with 0 disabling the effect.

The train track slider is presented as a "draw distance" (inverted
railMinZoom) so higher = tracks stay visible when more zoomed out.

Also move the Graphics settings button to the top of the settings modal.
2026-06-08 14:03:01 -07:00
Patrick Plays Badly 1e3f50436c Add map world inverted (#4189)
Resolves #4187

## Description:
Add Map - World Inverted

1248x2500, 1,561,000 land tiles
~100 standard. Over 250+ total.
https://www.youtube.com/watch?v=w2LVZQXZoaU
https://discord.com/channels/1284581928254701718/1509034328766812210

## 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
2026-06-08 13:43:55 -07:00
tnhnblgl 7921261ac9 Countdown before game start (#4198)
Resolves #4178 

## Description:

Let's the timer countdown remaining time to start in spawn phase

<img width="343" height="82" alt="Screenshot_2026-06-06-11-24-26-193_com
android chrome"
src="https://github.com/user-attachments/assets/e5827db4-a6d5-485f-b504-d8b64b7c6ba7"
/>

## 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:

Dovg
2026-06-08 12:58:18 -07:00
Katokoda 01ddc1d6c6 Fix aliance renewal question (extension-prompt) staying open (#4196)
> **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 #4164

## Description:

Single line change, making sure the ActionableEvent is updated after
removing the alliance that just ended.

## Please complete the following:

- [x] I have added screenshots for all UI updates
-# There are none
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
-# There are none
- [x] I have added relevant tests to the test directory
-# I do not know how I would to it.

## Please put your Discord username so you can be contacted if a bug or
regression is found:

Katokoda
2026-06-08 12:52:37 -07:00
Evan 26d8a314ae Scale defense-post border + fill rendering to thousands of posts (#4181)
## Description

Scales the defense-post border effect so it works with **thousands** of
Defense Posts instead of silently capping at 64.

### Problem
The border "checkerboard" (drawn on a player's border tiles when a
same-owner Defense Post is within range) was computed per-pixel: for
every border fragment, the shader looped over a `uniform vec4
uDefensePosts[64]` array doing a distance test. Two issues:
- **Hard cap of 64** — posts beyond the first 64 were dropped, so their
checkerboard never appeared.
- **Wrong cost shape** — work was `border_tiles × posts`; every added
post made every border pixel slower.

### Solution: invert the loop into a coverage texture
New `DefenseCoveragePass` stamps one instanced circle per post into a
map-resolution `R8` coverage texture (`1.0` = tile is within range of a
**same-owner** post; the owner check samples `tileTex` at stamp time, so
enemy posts never light up your border). It's a single
`drawArraysInstanced` regardless of post count — the same instancing
pattern `UnitPass`/`StructurePass` already use. The border-stamp shader
now reads one texel of that texture instead of looping; the old uniform
array, the 64-cap, and the per-fragment scan are removed from
`border-compute`/`BorderStampPass`/`BorderScatterPass`.

### Incremental re-stamping (dirty-block grid)
Coverage depends on tile ownership, which drips every frame during
combat, so a full re-stamp every frame would be wasteful at high post
counts. Because a tile changing owner only changes *its own* coverage,
the pass tracks a grid of dirty **blocks** and re-stamps only the blocks
containing changed tiles, scissored to each block (`gl.scissor` confines
the clear + draw to the changed region). Post add/remove and full tile
uploads fall back to a whole-map stamp; so does a frame where most
blocks are dirty. Per-frame cost tracks *how much changed*, not *how
many posts exist*, and scattered fronts (e.g. opposite corners) become
independent small block draws.

### Territory-fill darkening
The coverage texture marks every same-owner in-range tile (interior
included, not just borders), so `TerritoryPass` now also samples it to
darken the territory **fill** around posts. New tunable
`mapOverlay.territoryDefenseDarken` (live-editable in the graphics debug
GUI alongside `defenseCheckerDarken`).

### Performance
Tested with ~1,000 posts blanketing a map — smooth, including on a
low-end (~10-year-old) Chromebook.

## Files
- **New:** `passes/DefenseCoveragePass.ts`,
`shaders/defense-coverage/defense-coverage.{vert,frag}.glsl`
- **Edited:** `Renderer.ts`, `BorderStampPass.ts`,
`BorderComputePass.ts`, `BorderScatterPass.ts`, `TerritoryPass.ts`,
`border-stamp.frag.glsl`, `border-compute.frag.glsl`,
`territory.frag.glsl`, `RenderSettings.ts`, `render-settings.json`,
`debug/Layout.ts`

## Notes
- No user-facing text (no `translateText`/`en.json` changes needed).
- No `src/core` changes — purely client rendering, so no simulation
tests; verified via `tsc`, ESLint, `build-prod`, and in-game.
2026-06-08 10:18:02 -07:00
crunchybbb 8a510977ba [Fix] Adds flags to nations on Lemnos, Hormuz, Two Lakes, other minor fixes (#4173)
> **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 #4160 

## Description:

Adds missing flags to nations on Strait of Hormuz, Two Lakes, and
Lemnos.
Also adds a Bahrain npc



## 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
2026-06-08 10:02:09 -07:00
RickD004 27517e3698 Adds Mississippi River map - vertical pipe map (#4176)
Resolves #4153 

## Description:

Add Mississippi river, inspired by Amazon, but vertical. Pipe-type map
along Amazon and Passage. 11 nations, with 51 additional nations for a
total of 62 for Humans vs Nations gamemode.


https://github.com/user-attachments/assets/6596a7bf-b529-442a-99b1-815493ee0e96


https://github.com/user-attachments/assets/5bb4959b-8ef3-428a-8e3a-94c424fa092b


https://github.com/user-attachments/assets/e4d4622e-ea42-4edf-9d86-d9d00c0fdde4

## 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
2026-06-06 19:26:34 -07:00
TKTK123456 8115b755a2 Fix rebinded keys (#4175)
> **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 #4174 

## Description:

I have fixed the rebinding of the keys for the build menu modifier and
the emoji menu modifier in the settings so that they actually work.
And cleaned up the code

## 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:

tktk1234567
2026-06-06 18:29:16 -07:00
evanpelle 385b4dd686 Add steady glow effect beneath hydrogen bomb
Render a soft radial glow underneath the hydrogen bomb sprite in
UnitPass. H-bomb instances draw an enlarged quad (hBombGlowScale) so
there's room for the halo; a cell-space UV remap keeps the sprite at its
normal size while the margin becomes glow area. The glow is a steady
(non-pulsing) radial falloff in a warm amber, alpha-blended underneath
the sprite and suppressed in alt/affiliation view.

Detection uses a HYDROGEN_BOMB_COL shader define derived from
UNIT_ORDER, so it tracks the atlas layout rather than hard-coding the
column. All other units are unaffected (scale 1, same fillrate); this
stays a single program / two instanced draw calls.

Glow color, scale, strength, and falloff are exposed in
render-settings.json for live tuning via the debug GUI.
2026-06-05 19:47:07 -07:00
Evan ee8c28331b Perf: Maintain a per-player alliance list (#4172)
# Maintain a per-player alliance list (drop O(all-alliances) scan)

## Summary

`PlayerImpl.alliances()` was implemented as a full scan of the global
alliance
list on every call:

```ts
alliances(): MutableAlliance[] {
  return this.mg.alliances_.filter(
    (a) => a.requestor() === this || a.recipient() === this,
  );
}
```

This is O(all-alliances-in-game) **per call**, and it's called a lot —
most
notably twice per player per tick from `PlayerImpl.toFullUpdate()` (once
for
`allies`, once for `alliances`), which runs for every player every tick
on the
worker/core thread.

This PR makes each player own its alliance list: a per-player
`_alliances`
array (mirroring the existing `_incomingAttacks` / `_outgoingAttacks`
pattern),
maintained incrementally as alliances form/break/expire, so
`alliances()`
becomes an O(1) field read.

It turned out the global `mg.alliances_` list was only ever read by this
scan —
the `Game`-level `alliances()` getter had **zero callers** (all 17
`.alliances()`
callsites use the player-level accessor), and the list isn't used in
serialization. So rather than keep two structures in sync, this removes
the
global list entirely and makes the per-player lists the single source of
truth.

## Motivation

Profiling the worker/core thread showed `player.toFullUpdate` at ~**4%
of CPU**.
Breaking down where that time goes (microbenchmark, 100 players, ~100
alliances):

| Component | µs/tick | Share |
| --- | --- | --- |
| FULL (current: alliance scan ×2 + allocate collections) | 61.5 | 100%
|
| Alliance scan only (the two global `.filter()`s) | 41.7 | **~68%** |
| Allocation only (build arrays/objects, per-player list, no scan) | 6.4
| ~10% |

The global alliance scan — not the object allocation — is the dominant
cost, and
it gets *worse* with game size: the scan is O(players × total-alliances)
while
allocation is only O(players × own-alliances). Removing the scan targets
the
dominant ~2/3 of `toFullUpdate`'s cost.

It also speeds up `alliances()` everywhere, not just `toFullUpdate` —
it's called
in **17 places**, including AI hot paths (`NationAllianceBehavior`,
`PlayerExecution`).

> Note: this builds on the already-merged `diffPlayerUpdate`
typed-comparison
> change (commit `be87c76`), which addressed the diff/serialization
cost. This PR
> addresses the snapshot-construction cost.

## Changes

- **`PlayerImpl`**: add `public _alliances: MutableAlliance[]`;
`alliances()`
  returns it directly.
- **`GameImpl`**: remove the global `alliances_` field and the unused
`alliances()` getter. Maintain the per-player lists at the mutation
sites:
  - **add** — `acceptAllianceRequest` pushes the new alliance onto both
    participants.
  - **remove** — `breakAlliance`, `expireAlliance`, and
    `removeAlliancesByPlayerSilently` all funnel through a small
`detachAlliance()` helper that removes the alliance from both
participants.
- **`Game` interface**: drop `alliances(): MutableAlliance[]` (no
callers).

## Correctness notes

- `alliances()` now returns the internal array by reference. This
matches the
existing `outgoingAttacks()` / `incomingAttacks()` accessors, which
already do
the same. All 17 callsites were checked — none mutate the returned
array.
- `detachAlliance` reassigns the array (`filter`) rather than splicing
in place,
so the `for (const alliance of player.alliances())` loop in
`PlayerExecution`
(which can expire alliances mid-iteration) iterates a stable snapshot
and is
safe. `removeAlliancesByPlayerSilently` likewise snapshots the player's
list
  before detaching.

## Tests

New `tests/PlayerAllianceList.test.ts` asserts both participants' lists
stay in
sync through every mutation path:

- forming an alliance adds it to both lists
- `alliances()` agrees with `isAlliedWith` / `allianceWith`
- breaking removes it from both lists
- expiring removes it from both lists
- a player tracks multiple alliances independently (breaking one keeps
the other)
- `removeAllAlliances` clears the player and every partner

Full suite green: **1360 tests / 120 files**.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-05 17:34:46 -07:00
evanpelle be87c7658f Speed up diffPlayerUpdate with typed field comparisons
diffPlayerUpdate runs once per player per tick on the worker thread. The
array/object fields (outgoingAttacks, incomingAttacks, alliances,
outgoingEmojis) were compared via JSON.stringify — two string allocations per
field, run on every call even when nothing changed. This made the cost flat at
~3.4µs/call regardless of what actually changed.

Replace jsonEqual with three typed structural comparators (attackArrayEqual,
allianceArrayEqual, emojiArrayEqual) that short-circuit on reference/length,
compare known fields with ===, early-exit on the first difference, and
allocate nothing — matching the existing numberArrayEqual/stringArrayEqual
style. ~9-10x faster across all cases (276k -> 2.4M ops/sec when unchanged).

Add tests/perf/DiffPlayerUpdatePerf.ts (BEFORE/AFTER benchmark, run via
npm run perf) and warnings on PlayerUpdate and diffPlayerUpdate noting that new
fields must be wired into the diff/apply functions or their changes are
silently dropped after the first emission.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:59:58 -07:00