Commit Graph

3964 Commits

Author SHA1 Message Date
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
Ryan 312b38fda5 Disable game buttons (clan tag + username) (#4170)
## Description:

disables buttons, instead of emitting a warning 
<img width="1017" height="677" alt="image"
src="https://github.com/user-attachments/assets/7af4e0e1-df22-4cfe-bc8b-6fae5e62f9b6"
/>


<img width="1006" height="668" alt="image"
src="https://github.com/user-attachments/assets/d8e5291c-4ecd-4f8d-8471-e5a547c30eda"
/>


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

w.o.n
2026-06-05 14:18:31 -07:00
Katokoda c6296c0bb1 Fix/warship freezing no path (#4151)
**Add approved & assigned issue number here:**
Resolves #4113

## Description:

Warships now reject the PatrolTile change when the new one is a
different water component.
Adds a test ensuring this behavior.

## 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
No texts
- [x] I have added relevant tests to the test directory
I also have tested in game and tested that the test does indeed fail if
my fix is not present.

## Please put your Discord username so you can be contacted if a bug or
regression is found:
Katokoda
2026-06-05 13:42:52 -07:00
Evan 075547b7b6 Incremental GPU scatter recompute for tile borders (#4166)
## Description:

Incremental GPU border recompute — sequel to #4159.

On 10 yo low-end chrome book this increased performance by ~5fps. I'm
now able to get 40fps on GWM.

`BorderComputePass` previously re-ran its fragment shader over every
tile on
the map every time any input changed (tile flip, highlight, relation,
defense post). Cost was O(mapW × mapH) per invalidation, and tile flips
invalidate it ~every render frame in live play.

This PR adds `BorderScatterPass`, which runs the same fragment shader
but
rasterizes only one POINT per dirty tile (plus its 4 cardinal neighbors,
to
cover the cardinal-neighbor read in the border shader). Cost is O(dirty
tiles) regardless of map size or spatial distribution.

### What changed

- New `BorderScatterPass` — owns its own FBO, VAO, and instance buffer;
  shares the border fragment shader with `BorderComputePass` so the two
  paths can't diverge in output.
- `BorderComputePass.draw()` now picks per frame:
- **Full recompute** — when `globalDirty` is set by highlight / relation
/
    defense-post changes (those affect tiles across the whole map).
  - **Scatter** — when only per-tile patches have been queued via
    `patchTile()`.
- `TerritoryPass.flushTileTexture()` now returns `"none" | "full" |
  "scatter"` instead of `boolean`, so the renderer can pick the right
  downstream invalidation:
- `"full"` → `borderPass.markGlobalDirty()` (full tile upload supersedes
    per-tile patches).
  - `"scatter"` → no-op; per-tile patches were already pushed via the
    wired `borderPatchConsumer` callback during drip drain.
- Renderer wires `territoryPass.setBorderPatchConsumer((x, y) =>
borderPass.patchTile(x, y))` so every per-tile scatter write to
`tileTex`
  also schedules an incremental border recompute for that tile + its
  neighbors.

### Known limitation

Highlight-thicken rings (within `uHighlightThicken` of a changed tile)
are
NOT incrementally repainted — they'll lag visually until the next full
recompute. In practice this is short-lived (the next highlight change or
seek triggers a full recompute) and not visible during normal play; the
trade is documented in the `BorderScatterPass` header.


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

evan
2026-06-05 13:29:50 -07:00
Evan d1ce199a52 Upload tile delta to GPU (#4159)
## Description

Reduces the amount of tile data sent to the gpu each tick, roughly
~10fps rate increase on 10 year old chromebook.

Two changes to the territory rendering path:

### 1. Split `passEnabled.mapOverlay` into four flags

The single `mapOverlay` toggle controlled four unrelated passes
(territory fill, border compute, border stamp, trail). Splits it into
`territory`, `borderCompute`, `borderStamp`, `trail` so each can be
toggled independently in the debug GUI. Pure rename — default behavior
is unchanged (all four default to `true`).

### 2. GPU scatter for per-frame tile texture updates

Replaces the dirty-row bbox `texSubImage2D` upload in `TerritoryPass`
with a new `TileScatterPass` that uploads a small attribute buffer of
`(x, y, state)` patches and runs a single `POINTS` draw into an FBO
bound to `tileTex`. Each patch rasterizes as a 1×1 point into exactly
its target texel.

**Why:** the old path's cost scaled with the bounding box of the dirty
rows, not the number of changed tiles. In typical play, tile changes are
spread across the whole map (multiple players fighting in different
regions, scattered trails/fallout), so the bbox covered most of the
map's rows and we re-uploaded mostly-unchanged data every frame. The new
path is constant cost in patch count regardless of spatial distribution,
and no longer scales with map size.

The full-upload path (initial load / seek / spawn-phase flush) is
unchanged. `fullUploadPending` correctly supersedes any queued scatter
patches.

## Please complete the following:

- [x] I have added screenshots for all UI updates *(N/A — no UI
changes)*
- [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)*
- [x] I have added relevant tests to the test directory *(renderer code,
not covered by unit tests; verified visually)*

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

evan
2026-06-05 07:07:03 -07:00
Evan 2c2390d0cb Downsample fallout bloom + light extract for fillrate-bound GPUs (#4157)
## Description:

On low-end machines, the fillrate was too high causing framerate to
drop. The graphical difference is pretty negligible since fallout &
light are meant to be blurred anyways.

Reduces fillrate cost of the fallout bloom and fallout-light passes on
low-end GPUs:

- Extract step now renders at `mapW/8 × mapH/8` (64× fewer fragments).
Output is heavily blurred + LINEAR-magnified, so the visual difference
is minimal.
- Bloom blur reduced from 2× 9-tap to 1× 5-tap Gaussian (the smaller
kernel is sufficient given the lower-res source).


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

evan
2026-06-04 16:53:03 -07:00
evanpelle 00a7b6d14d Fix PrGateRules tests for checkRepoAccess refactor
Update test imports, mock data, and evaluate calls to match the
checkOrgMember/author_association → checkRepoAccess/getRepoPermission
rename in 0ab437ed5.
2026-06-04 16:45:46 -07:00
RickD004 37b7a781c2 Huge map update: Re-make of the terrain of multiple maps for better gameplay (#4139)
Resolves #4099

## Description:

Remake the terrain of various maps with green gaps in their terrain,
example below (strait of gibraltar)
<img width="608" height="383" alt="image"
src="https://github.com/user-attachments/assets/9272cf00-6620-4711-81a9-64ebf6a7990d"
/>

### Why?

In maps with continuous brown and white terrain, it is easier for a
player to defend against an attacker, as these types of terrains slow
down troops. However in maps with green gaps like the one in the
example, the troops will just rush in the green gaps and surround all
the white terrain, instantly absorbing them, which makes the terrain
mostly useless against defending.

I believe that this new type of terrain (using proper elevation data
instead of "hillshade like the ones used in these maps) is better for
gameplay as it adds more complexity to attacks and defense, and also
brings diversity to spawns, as right now these maps have functionally
all-green terrain all across them

I also changed the coastline of some maps that had them very pixelated.
This sometimes caused some rivers to dissapear and some islands to be
merged into blobs.

I kept the rivers of all maps even if slightly updated, to not change
naval gameplay.

The maps changed were:

Black Sea (terrain and coastline)
Gateway to the Atlantic (terrain only)
Between 2 seas (terrain only)
Iceland (terrain and coastline)
East Asia (terrain only)
Falklands (terrain and coastline)
Halkidiki (terrain only)
Strait of Gibraltar (terrain and coastline)
Italia (terrain and coastline)
Japan (terrain only)
Two Lakes (terrain only)

<img width="1058" height="536" alt="Captura de pantalla 2026-06-02
213132"
src="https://github.com/user-attachments/assets/58fb357b-8738-4388-bbc5-d6c9c1ac4699"
/>
<img width="625" height="583" alt="Captura de pantalla 2026-06-02
213251"
src="https://github.com/user-attachments/assets/99ead586-f790-4088-baef-ad179f3c0119"
/>
<img width="535" height="537" alt="Captura de pantalla 2026-06-02
213336"
src="https://github.com/user-attachments/assets/66755b25-e362-4ef0-b7ff-48b51fdabbbd"
/>
<img width="639" height="561" alt="Captura de pantalla 2026-06-02
231736"
src="https://github.com/user-attachments/assets/22802e9a-5989-4204-9002-61afef22696b"
/>
<img width="680" height="543" alt="Captura de pantalla 2026-06-02
231804"
src="https://github.com/user-attachments/assets/bfae63b6-81e3-4d53-bf17-962332b2c9b0"
/>
<img width="742" height="545" alt="Captura de pantalla 2026-06-02
212309"
src="https://github.com/user-attachments/assets/cdb933c7-5361-4db5-aa4f-c641d0fc4662"
/>
<img width="613" height="546" alt="Captura de pantalla 2026-06-02
212531"
src="https://github.com/user-attachments/assets/352148be-01fb-491b-ae39-746c54d3c278"
/>
<img width="908" height="548" alt="Captura de pantalla 2026-06-02
212709"
src="https://github.com/user-attachments/assets/f6a1ec03-fed8-4872-9a65-0a1ef4268035"
/>
<img width="718" height="536" alt="Captura de pantalla 2026-06-02
212839"
src="https://github.com/user-attachments/assets/5ec1e581-142c-45d6-af0d-e642989e1be1"
/>
<img width="516" height="544" alt="Captura de pantalla 2026-06-02
212937"
src="https://github.com/user-attachments/assets/9843ef29-81f1-45c3-ba7e-49ad2693571d"
/>
<img width="801" height="537" alt="Captura de pantalla 2026-06-02
213028"
src="https://github.com/user-attachments/assets/252822b5-2f2d-456e-a207-85367fab8e02"
/>

Terrain sources from NASA and OpenTopography, both already credited in
CREDITS

## 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-04 15:59:30 -07:00
Patrick Plays Badly b4058b5a58 Add map chopping block (#4143)
Resolves #4080

## Description:

Add Map Chopping Block
https://youtu.be/NpX73lHiKO8

Increased multiplier for 4 player team games and water nukes (plug in
center among other shortcuts). This map was made as a faster alternative
to Labyrinth. Map has been modified since last submission to be 'less
crazy'.

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

## Discord username:

PlaysBadly
2026-06-04 15:58:34 -07:00
FloPinguin 74b3bd275b Allow mappers to omit nation coordinates in manifest.json for random spawn 🎲 (#4156)
## Description:

Previously, every nation in a map's manifest.json required explicit
coordinates. Additional nations already supported optional coordinates
to trigger random spawn placement, but regular nations did not.

Idea from PlaysBadly.

Reasoning (copied off discord):

> I've been working on World Inverted by adding realistic 'nations' in
the form sunken ship names with their flags and location. However after
searching around for other possible nation locations that are ocean
related I realised that I might not have enough info for proper
'realisitc' coverage of the map. Currently Im at ~170 nations with
cordinates. This is not including the additional nations with no
locations. This will be reduced to ~62 as the default with the rest
turning into additional nations.
> 
> The problem is the end process is proving difficult. Trying to blance
the nation placment on the map is a little much at this volume. So being
able to add a few no-cordinate nations would be a great way to fill in
the map.

This PR also improves the MapConsistency test to check the additional
nations too.

## 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-04 15:57:00 -07:00
evanpelle bf648a2a58 Gate Vite dev server LAN binding behind VITE_HOST=lan
`npm run dev` now binds to 127.0.0.1 by default; use `npm run dev:host`
to expose the dev server on the local network for phone/device testing.
2026-06-04 12:50:24 -07:00
evanpelle 0ab437ed54 Fix PR gate trusting author_association for org membership
author_association comes back as CONTRIBUTOR or NONE for team-based
contributors (e.g. members of the Contributor team), so the gate was
auto-closing PRs from people who clearly have write access.

Replace the author_association check with a live permission lookup via
repos.getCollaboratorPermissionLevel, which resolves direct, team, and
org access in one call. PRs from anyone with write/maintain/admin now
bypass the gate.
2026-06-04 11:51:53 -07:00
dependabot[bot] 7e1d352469 Bump the updates group with 2 updates (#4140)
Bumps the updates group with 2 updates:
[actions/github-script](https://github.com/actions/github-script) and
[actions/stale](https://github.com/actions/stale).

Updates `actions/github-script` from 8 to 9
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/github-script/releases">actions/github-script's
releases</a>.</em></p>
<blockquote>
<h2>v9.0.0</h2>
<p><strong>New features:</strong></p>
<ul>
<li><strong><code>getOctokit</code> factory function</strong> —
Available directly in the script context. Create additional
authenticated Octokit clients with different tokens for multi-token
workflows, GitHub App tokens, and cross-org access. See <a
href="https://github.com/actions/github-script#creating-additional-clients-with-getoctokit">Creating
additional clients with <code>getOctokit</code></a> for details and
examples.</li>
<li><strong>Orchestration ID in user-agent</strong> — The
<code>ACTIONS_ORCHESTRATION_ID</code> environment variable is
automatically appended to the user-agent string for request
tracing.</li>
</ul>
<p><strong>Breaking changes:</strong></p>
<ul>
<li><strong><code>require('@actions/github')</code> no longer works in
scripts.</strong> The upgrade to <code>@actions/github</code> v9
(ESM-only) means <code>require('@actions/github')</code> will fail at
runtime. If you previously used patterns like <code>const { getOctokit }
= require('@actions/github')</code> to create secondary clients, use the
new injected <code>getOctokit</code> function instead — it's available
directly in the script context with no imports needed.</li>
<li><code>getOctokit</code> is now an injected function parameter.
Scripts that declare <code>const getOctokit = ...</code> or <code>let
getOctokit = ...</code> will get a <code>SyntaxError</code> because
JavaScript does not allow <code>const</code>/<code>let</code>
redeclaration of function parameters. Use the injected
<code>getOctokit</code> directly, or use <code>var getOctokit =
...</code> if you need to redeclare it.</li>
<li>If your script accesses other <code>@actions/github</code> internals
beyond the standard <code>github</code>/<code>octokit</code> client, you
may need to update those references for v9 compatibility.</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>Add ACTIONS_ORCHESTRATION_ID to user-agent string by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/695">actions/github-script#695</a></li>
<li>ci: use deployment: false for integration test environments by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/712">actions/github-script#712</a></li>
<li>feat!: add getOctokit to script context, upgrade
<code>@​actions/github</code> v9, <code>@​octokit/core</code> v7, and
related packages by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/700">actions/github-script#700</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/695">actions/github-script#695</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v8.0.0...v9.0.0">https://github.com/actions/github-script/compare/v8.0.0...v9.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/github-script/commit/3a2844b7e9c422d3c10d287c895573f7108da1b3"><code>3a2844b</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/700">#700</a>
from actions/salmanmkc/expose-getoctokit + prepare re...</li>
<li><a
href="https://github.com/actions/github-script/commit/ca10bbdd1a7739de09e99a200c7a59f5d73a4079"><code>ca10bbd</code></a>
fix: use <code>@​octokit/core/</code>types import for v7
compatibility</li>
<li><a
href="https://github.com/actions/github-script/commit/86e48e20ac85c970ed1f96e718fd068173948b7b"><code>86e48e2</code></a>
merge: incorporate main branch changes</li>
<li><a
href="https://github.com/actions/github-script/commit/c1084728b5b935ec4ddc1e4cee877b01797b3ff9"><code>c108472</code></a>
chore: rebuild dist for v9 upgrade and getOctokit factory</li>
<li><a
href="https://github.com/actions/github-script/commit/afff112e4f8b57c718168af75b89ce00bc8d091d"><code>afff112</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/712">#712</a>
from actions/salmanmkc/deployment-false + fix user-ag...</li>
<li><a
href="https://github.com/actions/github-script/commit/ff8117e5b78c415f814f39ad6998f424fee7b817"><code>ff8117e</code></a>
ci: fix user-agent test to handle orchestration ID</li>
<li><a
href="https://github.com/actions/github-script/commit/81c6b7876079abe10ff715951c9fc7b3e1ab389d"><code>81c6b78</code></a>
ci: use deployment: false to suppress deployment noise from integration
tests</li>
<li><a
href="https://github.com/actions/github-script/commit/3953caf8858d318f37b6cc53a9f5708859b5a7b7"><code>3953caf</code></a>
docs: update README examples from <a
href="https://github.com/v8"><code>@​v8</code></a> to <a
href="https://github.com/v9"><code>@​v9</code></a>, add getOctokit docs
and v9 brea...</li>
<li><a
href="https://github.com/actions/github-script/commit/c17d55b90dcdb3d554d0027a6c180a7adc2daf78"><code>c17d55b</code></a>
ci: add getOctokit integration test job</li>
<li><a
href="https://github.com/actions/github-script/commit/a047196d9a02fe92098771cafbb98c2f1814e408"><code>a047196</code></a>
test: add getOctokit integration tests via callAsyncFunction</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/github-script/compare/v8...v9">compare
view</a></li>
</ul>
</details>
<br />

Updates `actions/stale` from 10.2.0 to 10.3.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/stale/releases">actions/stale's
releases</a>.</em></p>
<blockquote>
<h2>v10.3.0</h2>
<h2>What's Changed</h2>
<h3>Bug Fix</h3>
<ul>
<li>Enhancement: ignore stale labeling events by <a
href="https://github.com/shamoon"><code>@​shamoon</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1311">actions/stale#1311</a></li>
</ul>
<h3>Dependency Updates</h3>
<ul>
<li>Upgrade dependencies (<code>@​actions/core</code>,
<code>@​octokit/plugin-retry</code>, <a
href="https://github.com/typescript-eslint"><code>@​typescript-eslint</code></a>)
by <a href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1335">actions/stale#1335</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/shamoon"><code>@​shamoon</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1311">actions/stale#1311</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/stale/compare/v10...v10.3.0">https://github.com/actions/stale/compare/v10...v10.3.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/stale/commit/eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899"><code>eb5cf3a</code></a>
chore: upgrade dependencies and bump version to 10.3.0 (<a
href="https://redirect.github.com/actions/stale/issues/1335">#1335</a>)</li>
<li><a
href="https://github.com/actions/stale/commit/db5d06a4c82d5e94513c09c406638111df61f63e"><code>db5d06a</code></a>
Enhancement: ignore stale labeling events (<a
href="https://redirect.github.com/actions/stale/issues/1311">#1311</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/stale/compare/b5d41d4e1d5dceea10e7104786b73624c18a190f...eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 11:50:00 -07:00
evanpelle 986f0b61bf Fix WorldTextPass labels scaling with device-pixel-ratio
Attack troop labels, the ghost-cost chip, and the bonusPopup
minScreenScale floor were dividing by `zoom`, which Camera.ts stores
as device-pixels-per-world-unit (canvasW = cssWidth * dpr). The result
was constant device-pixel size — labels rendered ~2x larger on DPR=1
displays than on retina. Multiply each screen-relative scale by dpr so
the on-screen size stays constant in CSS pixels.

Retune the now-correct sizes: attack labels 34 -> 17, ghost-cost
screenScale 30 -> 18, screenYOffset 50 -> 25.
2026-06-03 21:40:00 -07:00
evanpelle 96c032850d Fix start game button regression in lobby modals
The Start Game button in SinglePlayerModal and HostLobbyModal was
scrolling away with the body instead of staying pinned at the bottom.

HostLobbyModal's body lacked overflow-y-auto/min-h-0, so the body
couldn't scroll independently and the footer scrolled off-screen.
SinglePlayerModal was missing the flex flex-col h-full wrapper entirely
(and had an unmatched closing div), so the footer never had a sticky
column to anchor to.

Restore the v31 layout: outer flex-col h-full, body is the scroll
container (flex-1 min-h-0 overflow-y-auto custom-scrollbar), footer is
shrink-0.
2026-06-03 20:51:28 -07:00
evanpelle 88697955b7 Add SoundEffectController to restore v31 sound effects
Ports the sound-emission logic from v31's deleted FxLayer into a new
controller registered with the game renderer. Per-tick it scans the
worker's update stream and emits PlaySoundEffectEvent for unit launches,
nuke detonations, structure/warship builds (local player only), and
conquests where the local player is the conqueror.
2026-06-03 20:43:12 -07:00
Ryan 9c2ac05506 clantag part 1 (#4066)
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #(issue number)

## Description:

adds a check to see if you're in a clan or not. if not, checks to see if
the clan exists, if it does, warns the user, if it doesn't, lets them
use it.

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

w.o.n
2026-06-03 14:25:55 -07:00
Arkadiusz Sygulski 297e1f579e Fix AStar overflowing the priority queue on twisted paths (#4149)
Resolves pathfinding issue:
https://discord.com/channels/1359946986937258015/1458870041964445706

## Description:

BucketQueue requires `maxF` to be defined. Estimating it is much more
complicated than the code assumed. This caused the bucket to overflow on
certain paths, mostly (a) twisty paths - defined as one which must
traverse both ways along the same axis, (b) maps where height > width,
because we used `width ** 2` to estimate size instead of `width *
height` (iirc height was not easily accessible).

This PR replcaes BucketQueue with already existing MinHeap.
`AStar.AbstractGraph` is already specialized in traversing
`AbstractGraph`, so I have dropped the `useMinHeap` option and instead
made it the default path. Otherwise we'd be leaving dead code. The max
priority was also fixed to account for duplicate connections, abstract
graph is already very small so it should not affect (and in my testing
does not) affect performance.

**Before**
<img width="1813" height="1781" alt="image"
src="https://github.com/user-attachments/assets/63648db1-a293-441d-8ea8-eaf98694f04a"
/>

**After**
<img width="1734" height="1702" alt="image"
src="https://github.com/user-attachments/assets/b4ec2f68-b945-42e1-bab4-53ba19fa2bbf"
/>


## Please complete the following:

- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] 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:

moleole
2026-06-03 12:32:17 -07:00
Evan 48609fa70a Reduce lobby broadcast bandwidth via counts-only deltas (#4116)
## Description:

- The lobby WebSocket broadcast (`/lobbies`) was re-sending the full
`PublicGames` snapshot — including each lobby's `gameConfig` — to every
connected client every 500ms. Almost nothing in that payload changes
tick-to-tick; only `numClients` moves.
- `WorkerLobbyService` now tracks the sorted set of `gameID`s it last
sent as a full snapshot. On each incoming broadcast it sends a `full`
only when that set changes; otherwise it sends a `counts` delta carrying
just `{gameID → numClients}`.
- This relies on the master-side coupling at
[MasterLobbyService.ts:140-159](src/server/MasterLobbyService.ts#L140-L159):
when master finds a lobby without `startsAt`, it both sets `startsAt`
AND schedules a fresh lobby on the same tick, so the gameID change
brings the `startsAt` (and `gameConfig`) along with it.
- New WS connections are primed with the worker's cached last `full` so
late joiners don't have to wait for the next structural change.
- `LobbySocket` parses the new discriminated union (`PublicLobbyMessage
= full | counts`), keeps the last full snapshot in memory, and merges
counts into it before invoking the existing callback. `GameModeSelector`
is unchanged.
- Master → worker IPC is unchanged — still sends the full snapshot every
500ms. The optimization only applies to the worker → WS-client boundary,
which is the fan-out point.

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

evan
2026-06-02 15:52:14 -07:00
FloPinguin 775ae77e0a Fix nations not spawning when random spawn is enabled 🤖 (#4117)
## Description:

When random spawn is active, human SpawnExecutions are pre-created in
GameRunner.init() and fire on the same tick as NationExecution. Because
humans were added first, their SpawnExecution ticked first, called
endSpawnPhase() (in singleplayer), and NationExecution then saw
inSpawnPhase()=false, found the nation not alive, and deactivated it
before ever queuing a SpawnExecution.

Two changes fix this:

1. GameRunner.init(): Move nationExecutions() before spawnPlayers() so
NationExecution ticks first and queues its SpawnExecution before the
human SpawnExecution can end the spawn phase.

2. NationExecution.tick(): After the spawn-phase block, add a guard that
waits when spawnExecAdded is true but the nation hasn't actually spawned
yet. This prevents NationExecution from deactivating on the very next
tick (via !isAlive()) before its queued SpawnExecution has had a chance
to fire and give the nation territory.

I tested it in singleplaye with and without random spawn and also in
public lobbies. Nations now always spawn.

## 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-02 15:25:29 -07:00
evanpelle 431f22ac94 Always render player name when under the cursor 2026-06-02 12:04:05 -07:00
Evan f1045a2022 Update & refactor dark mode (#4114)
## Description:

- The renderer no longer knows what "dark mode" is.
`RenderSettings.dayNight.mode` (`"light" | "dark"`) is gone — passes
read neutral values (`lighting.ambient: number`, `lighting.enabled:
boolean`).
- `render-settings.json` holds the light-mode baseline. Dark mode is
just another override layer, applied the same way as graphics settings
(`darkNames`, `classicIcons`, etc.).
- New `src/client/render/gl/RenderOverrides.ts` exposes two in-place
mutators with matching shapes:
- `applyGraphicsOverrides(settings, overrides)` — replaces the old
`generateRenderSettings`
  - `applyDarkModeOverride(settings, isDark)`
- `ClientGameRunner` regenerates the live settings each time the user
setting changes via `deepAssign(live, createRenderSettings())` + the
override chain. No per-slice copy list, no intermediate object — adding
a new override that touches a new section just works.
- Renamed `dayNight` → `lighting`; collapsed `nightAmbient`/`dayAmbient`
into single `ambient`; renamed `enableLightCompositing` → `enabled`.
- Bumped dark-mode ambient from 0.15 → 0.35 so terrain stays readable.

<img width="1250" height="846" alt="Screenshot 2026-06-02 at 11 47
28 AM"
src="https://github.com/user-attachments/assets/b41e8ffb-6011-4ba0-9e1f-c2a21ff90794"
/>

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

evan
2026-06-02 11:48:52 -07:00
Aotumuri 2386b4b38a Restore dev-only localStorage pattern override (#3999)
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #(issue number)

## Description:

Restore the localStorage-driven dev pattern override for development
environments


## 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
2026-06-02 09:50:27 -07:00
noahschmal 2c8a66625c Feature/Move theme system from core to client-side ThemeProvider (#4108)
**Add approved & assigned issue number here:** 

Resolves #2549

## Description:

Themes are purely for the client's rendering, and the server doesn't
need context on them. This PR moves `Theme.ts` from
`src/core/configuration` to `src/client/theme` and moves affiliation
colors to `render-settings.json`.

This is to support the ability to add additional themes more quickly,
such as colorblind-friendly themes. No visible changes occur from this
refactor.

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

---------

Co-authored-by: Josh Harris <josh@wickedsick.com>
2026-06-02 09:32:08 +00:00
evanpelle d8c127a462 Add deterministic PR gate and label-triggered auto-close
- pr-gate.yml: closes PRs that don't fit the contribution workflow.
  First match wins: bypass-pr-check label, org/repo member, linked
  `approved` issue with author assigned, or ≤50-line diff. Defaults to
  dry-run via the PR_GATE_DRY_RUN repo variable.
- pr-close-on-label.yml: closes a PR with a polite comment when a
  maintainer applies a triage label (initially `appears-ai-generated`,
  extensible via the COMMENTS lookup).
- scripts/pr-gate/: TypeScript implementation with scoped install
  (Octokit + tsx) so the workflow doesn't pull the full game dep tree.
  35 unit tests cover parseLinkedIssues, each rule, and priority
  ordering.
- PULL_REQUEST_TEMPLATE.md: points contributors at Discord (features)
  and issues (bugs) and surfaces the `approved` requirement.
2026-06-01 22:17:21 -07:00