Commit Graph

3940 Commits

Author SHA1 Message Date
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
evanpelle ac63315449 Move contributing docs to CONTRIBUTING.md and require approved issues
Consolidate the contribution workflow, governance, and translation
sections from README.md into CONTRIBUTING.md, leaving the README with a
single pointer.

Document the new process: contributors must work from issues labelled
`approved` (or open one and wait for the label), comment to be assigned,
and link the issue from their PR. PRs without an approved issue will be
auto-closed (except small bug fixes), and PRs that appear AI-generated
without genuine author understanding will be closed.
2026-06-01 21:44:16 -07:00
TKTK123456 6ca06a6f6f Sam/factory radius ghost upgrade fix (#4104)
> **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 #4059

## Description:

Makes ghost radius centred on building being upgrading (if upgrading
building)
<img width="1920" height="1080" alt="Screenshot from 2026-06-01
15-45-37"
src="https://github.com/user-attachments/assets/9cf19e59-0e20-4b43-b65f-35eebb37fa8e"
/>
<img width="1920" height="1080" alt="Screenshot from 2026-06-01
15-45-24"
src="https://github.com/user-attachments/assets/1953238f-5156-4bf7-aa18-8278c27d1a68"
/>


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

tktk123456
2026-06-01 17:39:52 -07:00
TKTK123456 c722b026db Sync factory effective distance and railroad max length and add railroad ghost for factories (#4079)
> **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 #2668 

## Description:
Makes radius that factories effect equal to the max distance that a
railroad can be and enable factory ghosts
<img width="1920" height="1080" alt="Screenshot from 2026-06-01
20-13-58"
src="https://github.com/user-attachments/assets/1e2f4de8-79fc-4034-b6ad-3c71255c0410"
/>


## 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-01 17:39:26 -07:00
RickD004 95377f0361 Adds map of Southeast Asia (#4105)
Resolves #4098

## Description:

Adds Southeast Asia map for v32. Very requested map. 31 default nations
(with an extra 31 named for HvN).

Map for intense warship and naval warfare with many, many islands. Also
adds flags of the region to be used by nations in the map. More info
specified in issue


https://github.com/user-attachments/assets/b4151db4-825a-4c1c-8bf8-7b760ae056d2

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

tri.star1011
2026-06-01 17:29:56 -07:00
evanpelle 5802c19859 Remove "thoroughly tested" checkbox from PR description requirements 2026-06-01 13:53:04 -07:00
evanpelle 82bfae981f Allow shallow ocean tiles in name box grid for larger island player names 2026-06-01 11:22:34 -07:00
a-happy-goose 8dfa2b4cd9 [small-fix 25 lines] Add boat ETA calculation and display in AttacksDisplay (#4097)
Resolves #1793 

## Note:
@camclark also has a PR (#4073) for this issue and his design is really
nice. I hope his PR gets merged. I opened this PR because I already had
a simple solution which I made a few days ago (just 25 lines). If for
some reason his PR doesn't get merged I propose this as an alternative.
Thanks.

## Description:
Add real-time countdown ETA for boats. 
<img width="516" height="70" alt="2026-06-01_00-06"
src="https://github.com/user-attachments/assets/82d7ee7c-b0a3-44c8-9999-799e483c2f69"
/>

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

goose126
2026-05-31 20:34:16 -07:00
Berk f3ba95574c fix(core): prevent bots from invading/attacking themselves (#3865) (#4014)
Resolves #4094

## Description:

In Free-For-All (FFA) mode where teams default to 0, player isOnSameTeam
checks returned false for oneself, allowing players to attack
themselves. Consequently, if a bot conquered the targeted tile between
queueing a transport ship action and its actual initialization, the
target became itself, causing the bot to execute a self-invasion.

This fix adds a reflexive check in PlayerImpl.ts's isFriendly method to
always treat oneself as friendly. It also adds a safety guard in
TransportShipExecution.ts's init method to abort ship execution if the
target has shifted to the attacker.

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

barfires
2026-05-31 20:05:51 -07:00
Aotumuri b38f8ed1f8 mls (v5.4) (#4086)
## Description:

Version identifier within MLS: v5.4

[Changed languages]
- bg
- eo
- fa
- fr
- hu
- ja
- ru
- uk
- zh-CN

[Change volume]
- Changed languages: 9
- Changed files: 9
- Changed lines: 17653
- metadata.json: unchanged

Final reviewer: meow

This PR was generated by the PR sender tool, then checked and submitted
by the final reviewer.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:
aotumuri
2026-05-31 19:55:50 -07:00
Josh Harris 07b8f05cf2 Forward-port v31 → main (#4087)
**Add approved & assigned issue number here:**

Resolves #(issue number)

> Forward-port of changes already merged on the **v31** live-release
branch into **main** (next / v32). Source PRs: #3917, #3907, #4012, plus
the "clan tab on crazy games" change. Strait of Malacca (#3914) was
already on main and is excluded. No new feature work — please confirm
the tracking issue number above.

## Description:

Carries over v31 changes that never made it into main:

- **ToS / legal-docs update (#3917)** — updates
`resources/privacy-policy.html` and `resources/terms-of-service.html`,
plus the "OpenFront LLC → OpenFront Inc." entity rename and 2026
copyright bumps across `LICENSE-ASSETS`, `LICENSING.md`, and
`proprietary/LICENSE`.
- **Don't show clan tab on crazy games** — hides the clan tab on the
CrazyGames platform.
- **Show bonus amount on currency packs (#3907)** — displays the bonus
amount on currency packs; `en.json` conflict resolved by keeping both
sibling keys.
- **Per-recipient cooldown for QuickChatExecution (#4012)** — adapted to
main's refactored config (main folded `DefaultConfig.ts` into a single
`class Config`), adding `quickChatCooldown()` directly to the class.

Verification: `tsc --noEmit` clean, ESLint clean on changed files, full
suite passes (1319 + 65 tests).

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

jish

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
Co-authored-by: evanpelle <evanpelle@gmail.com>
2026-05-31 15:39:37 +01:00
Josh Harris 413efed895 Add per-recipient cooldown to QuickChatExecution (#4012)
`QuickChatExecution` had no cooldown, allowing a player to spam
quick-chat intents and flood a recipient's chat UI. This could bury
incoming alliance request notifications, preventing them from being seen
or accepted.

This fix mirrors the existing emoji cooldown pattern:

- Added `quickChatCooldown()` to `Config` (default: 30 ticks / 3 seconds)
- Added `canSendQuickChat(recipient)` and `recordQuickChat(recipient)`
to `Player` / `PlayerImpl`, tracking outgoing chats per recipient
- `QuickChatExecution.tick()` now checks `canSendQuickChat` before
displaying and records before the display calls (so the cooldown is
always written even if display throws)
2026-05-31 15:20:46 +01:00
Evan 712b2bc473 Show bonus amount on currency packs (#3907)
Show bonus amount on currency packs

- Add `bonusAmount` field to `PackSchema` (non-negative int)
- Render a rotated green corner ribbon (`+X FREE!`) on pack tiles when
`bonusAmount > 0`
- Add `cosmetics.free` translation key with `numFree` param

<img width="720" height="359" alt="Screenshot 2026-05-12 at 7 40 12 AM"
src="https://github.com/user-attachments/assets/3dd70fc4-c922-47f4-aee6-055047b58563"
/>

Describe the PR.

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

regression is found:

evan
2026-05-31 15:09:36 +01:00
evanpelle 450f2944c9 don't show clan tab on crazy games 2026-05-31 15:09:08 +01:00
iamlewis 14ac5ab3c6 Tos update (#3917)
## Description:

Bump "Last Updated" dates to 5/13/2026 and replace OpenFront LLC
references with OpenFront Inc. across Terms of Service and Privacy
Policy. Update corporate designation to a Delaware corporation, change
registered agent/address to Paracorp Incorporated (2140 South Dupont
Hwy, Camden, DE), and switch governing law from California to Delaware.
Minor wording clarifications in the introduction and contact/legal
sections.

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

iamlewis
2026-05-31 15:09:08 +01:00
evanpelle 885e31460c issue-lifecycle: greet new unmilestoned issues with a comment
When an issue is opened without a milestone, post a friendly comment
alongside the not-approved label so the reporter knows it's queued for
triage. Scoped to the opened event only to avoid spamming old issues on
cron reconciliation.
2026-05-30 09:19:47 -07:00
Evan f366f762cc Issue Lifecycle Actions (#4071)
## Description:

# Issue Lifecycle Actions

Adds two GitHub Actions workflows that enforce OpenFront's
issue-lifecycle invariants. No LLM calls — only the default
`GITHUB_TOKEN`. Layer B (Claude-powered triage) will build on this
foundation.

## Summary

- **Stale closer** — daily cron. Unmilestoned issues get warned at 5
days of inactivity, auto-closed at 10. Exempt: milestoned or
`keep-open`. Bot comments don't reset the timer.
- **Assignment invariant** — event + cron backstop. You cannot assign
anyone to an unmilestoned issue. Violators are unassigned automatically
with an explanatory comment.
- **Approval label sync** — event + cron backstop. The `not-approved`
(red) and `approved` (green) labels are derived from milestone state.
These labels are *only* ever touched by this Action.

## Rollout

Both workflows ship gated by `vars.ISSUE_LIFECYCLE_DRY_RUN` (defaults to
`'true'`). They log decisions but do not mutate anything until the
maintainer flips that variable in **Settings → Variables**.

Suggested rollout:
1. Merge with dry-run on.
2. Watch the cron logs for ~1 week. Verify the action list matches
expectations.
3. Flip `ISSUE_LIFECYCLE_DRY_RUN=false` to go live.

## File layout

```
.github/workflows/
  issue-lifecycle-cron.yml      # daily 06:00 UTC + workflow_dispatch
  issue-lifecycle-events.yml    # issues: [opened, assigned, milestoned, demilestoned]
scripts/issue-lifecycle/
  config.ts                     # labels, colors, thresholds, comment templates
  github.ts                     # Octokit wrapper, Action applier, label idempotent-creation
  rules/
    approval-label-sync.ts      # pure function — idempotent
    assignment-invariant.ts     # pure function
    stale-closer.ts             # async — reads comment history, filters bots
  cron.ts                       # daily sweep orchestrator
  events.ts                     # event-mode dispatcher
  index.ts                      # entrypoint, CLI arg parser
  README.md
```

Structure mirrors `scripts/pr-gate/` from Unit 2 — same Octokit/Action
patterns, same dry-run convention.

## Self-installing labels

On every run, the Action ensures the six labels exist (`not-approved`,
`approved`, `stale`, `keep-open`, `needs-info`, `auto-closed-stale`)
with the correct colors and descriptions. No manual setup required.

## Local testing

```bash
cd scripts/issue-lifecycle
npm install
export GITHUB_TOKEN=ghp_...

# Full cron sweep, dry-run (default for CLI):
npx tsx index.ts --mode cron

# Simulate an event:
EVENT_NAME=assigned npx tsx index.ts --mode event --issue 1234
```

CLI invocations are dry-run unless `--no-dry-run` is passed explicitly.

## 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-05-29 20:01:57 -07:00
RickD004 c049a81b86 Adds map of the Caribbean 🏴‍☠️ (#4067)
## Description:

Fixes #4069

Adds map of the Caribbean sea and its islands. Archipelago map with lots
of islands, lots of water and a lot of trade.

This map has multiple large landmasses of similar size to prevent
steamrolls (the largest islands and landmasses are around 30%), and
many, many small islands where players can survive and trade. Players
will have to island hop in order to win. 34 nations of Caribbean
countries and territories, with an extra 28 AdditionalNations for a
total of 62 nations for crowded HvN.

Heavy Island maps are very popular in the broader community and we dont
have one for v32, so i figured it would be nice to have a very requested
and popular world location

570k land tiles, fairly small for a map, would be right placed before
World (600k tiles). Also adds some flags of caribbean regions.


https://github.com/user-attachments/assets/9eae81ec-58eb-4594-89fd-2f95742f8b3a

Terrain source from OpenTopography, already credited. No modification to
the tests are needed for new maps added in Game.ts

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

tri.star1011
2026-05-29 19:23:21 -07:00
Evan b56e9438d1 github PR gate (#4070)
## Description:

## Summary

Adds a GitHub Action that auto-closes PRs which don't follow the
contribution workflow, so maintainer review time goes to legitimate
contributions instead of off-roadmap or AI-generated submissions.

Triggered on `pull_request_target: [opened, reopened]` and **defaults to
dry-run** so it's safe to merge before flipping live.

## Gate logic (first match wins)

1. **Maintainer bypass** — PR carries `bypass-pr-check` label → pass.
2. **Org/repo member** — `author_association` is `OWNER` / `MEMBER` /
`COLLABORATOR` → pass.
3. **Approved work** — PR body links an issue (`Closes/Fixes/Resolves
#N`) that carries the `approved` label and the PR author is in the
issue's assignees → pass.
4. **Small fix** — `additions + deletions ≤ 50` → pass + apply
`small-fix` label.
5. **Otherwise** — apply `auto-closed-needs-issue` label, post rejection
comment, close.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

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

evan
2026-05-29 18:28:52 -07:00
TKTK123456 9d4080fbe8 Adds onion map (#4057)
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #4055 

## Description:

Adds a 512*512 onion map with 3 nations (Leafer Confederation, Outer
Enclave and Inner Tribe)

<img width="128" height="128" alt="thumbnail"
src="https://github.com/user-attachments/assets/8d97d8dc-6286-4e79-a459-767c936d49ec"
/>


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

tktk1234567
2026-05-29 15:58:02 -07:00
evanpelle 326b882cf2 fix: pass NUM_WORKERS and TURNSTILE_SITE_KEY to release deploy steps
Both vars are written to the container env file by deploy.sh but were
missing from the env: blocks of all four deploy jobs in release.yml,
causing the server to crash on boot with "NUM_WORKERS not set".
2026-05-29 13:16:48 -07:00
Evan 6ed1483127 Share water pathfinder chain across ships (~150 MB savings) (#4068)
## Description

Each `TradeShipExecution` / `WarshipExecution` /
`TransportShipExecution` constructed its own `WaterPathFinder`, which
built a full transformer chain wrapping the (already-shared)
`AStarWaterHierarchical`. The chain's `SmoothingWaterTransformer`
allocates its own `AStarWaterBounded` with a 100×100 scratch (~480 KB:
four typed arrays + a MinHeap). With ~300 concurrent ships, that's ~150
MB of duplicated scratch buffers serving identical purposes.

Heap snapshot before:
- `WaterPathFinder` ×309 → 151 MB retained
- `AStarWaterBounded` ×312 (= 3 from the shared HPA + 309 from per-ship
smoothers) → 152 MB retained
- Worker total: 230 MB

## Fix

Cache the transformer chain in a module-level `WeakMap<Game, {version,
chain}>` in `PathFinder.ts`, keyed by `Game` and invalidated when
`waterGraphVersion()` changes. `PathFinding.Water` /
`PathFinding.WaterSimple` and the per-ship `WaterPathFinder` all wrap a
fresh (cheap) `PathFinderStepper` around the shared chain. Each ship
keeps its own stepper for its private path cache.

## Why sharing is safe

- The worker is single-threaded; `findPath` runs synchronously, so no
two callers touch the chain's scratch buffers at the same time.
- `AStarWaterBounded.searchBounded` already uses a `stamp++` pattern to
invalidate stale data — it doesn't care whether the previous caller was
the same instance or a different one.
- All transformers in the chain are either stateless or use the same
stamp-protected pattern.

## Stagger preserved

The per-ship stagger after a water-graph rebuild (so 300 ships don't
re-A* in the same tick) is intact. The chain itself rebuilds once per
version bump; each `WaterPathFinder` still counts down its own
`_staggerCountdown` before replacing its stepper (which invalidates its
cached path and forces re-A* against the new chain).

## Heap snapshot after

- `WaterPathFinder` no longer in top retainers
- `AStarWaterBounded` folded into the single 9 MB
`AStarWaterHierarchical`
- Worker total: 80 MB (≈150 MB freed)

## Please complete the following:

- [x] I have added screenshots for all UI updates (N/A — internal
refactor)
- [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 (existing tests
cover the behavior — all 1279 still pass)
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Discord

evan
2026-05-29 12:49:25 -07:00
evanpelle 475a7ab8af bugfix: port construction bar completes early; renderer now reads durations from Config
The renderer kept a parallel CONSTRUCTION_DURATIONS table in
src/client/render/GameConstants.ts that had drifted from Config: port
showed as 20 ticks but the simulation builds it in 50, so the bar hit
100% and idled for 30 ticks. SAM/silo cooldown constants were also
stale (120/75 vs Config's 90/90), making the missile-readiness bar
slightly wrong too.

Delete GameConstants.ts entirely. Thread the Config instance through
WebGLGameView → GPURenderer → BarPass / FxPass / FxSpritePass /
WorldTextPass; passes call config.unitInfo(...).constructionDuration,
config.SAMCooldown(), config.deletionMarkDuration(), config.msPerTick()
directly. Add Config.msPerTick() since no method existed for it.

Move the visual-only NUKE_EXPLOSION_RADII (not a game-logic value)
into FxSpritePass where it's used.
2026-05-29 12:15:49 -07:00
evanpelle 7c75df8426 bugfix: mirv skull eyes not showing 2026-05-29 10:50:00 -07:00
RickD004 b043dc6c15 Team Maps Expansion: New team spawnzones for multiple maps (#4058)
## Description:

Lets give Teams and HvN gamemodes some attention. Adds team spawnzones
to the following maps, and boosts them to appear more frequently as this
gamemode:

- Straitofgibraltar - 2 teams
- Aegean - 2 teams
- Beringsea - 2 teams
- Beringstrait - 2 teams
- Bosphorusstraits - 2 teams
- Conakry - 2 teams
- Falklandislands - 2 teams
- Straitofhormuz - 2 teams
- Tradersdream - 2 teams
- Surrounded - 2 teams & 4 teams
- Pluto - 2 teams
- Gulf of St. Lawrence - 3 teams

These maps (especially the ones for 2 teams) are all very symmetrical
and would be nice gift for the playerbase, which enjoys these kind of
games like FourIslands4Teams and Baikal2Teams. This is also nice for
HvN, as it centralizes the players and gives them a better chance at
defeating the nations.

Screenshots of the maps with the new team spawnzones:


<img width="1320" height="486" alt="Captura de pantalla 2026-05-28
001558"
src="https://github.com/user-attachments/assets/e0b4bea6-d1b7-4793-a995-ec2a139a5af6"
/>
<img width="1177" height="528" alt="Captura de pantalla 2026-05-28
001913"
src="https://github.com/user-attachments/assets/28ec5bf8-3a02-4660-ba62-3edbcabeaf51"
/>
<img width="1147" height="531" alt="Captura de pantalla 2026-05-28
002032"
src="https://github.com/user-attachments/assets/b148f1ae-473a-4505-b0f4-ca8820fbbb55"
/>
<img width="1219" height="536" alt="Captura de pantalla 2026-05-28
002348"
src="https://github.com/user-attachments/assets/89af4d27-eadf-447c-9bde-d0dcfe1ff757"
/>
<img width="923" height="524" alt="Captura de pantalla 2026-05-28
002704"
src="https://github.com/user-attachments/assets/50ad1b11-1685-41fb-b14d-088a2f0db88b"
/>
<img width="1307" height="456" alt="Captura de pantalla 2026-05-28
002859"
src="https://github.com/user-attachments/assets/4ef18da9-336a-4698-8af0-2769467148b4"
/>
<img width="1219" height="548" alt="Captura de pantalla 2026-05-28
003134"
src="https://github.com/user-attachments/assets/d0a514bf-e6e6-43f6-89b7-2168bc395010"
/>
<img width="1200" height="538" alt="Captura de pantalla 2026-05-28
003449"
src="https://github.com/user-attachments/assets/c1672296-db4d-4baf-9992-4bb380fab4e9"
/>
<img width="1032" height="501" alt="Captura de pantalla 2026-05-28
003650"
src="https://github.com/user-attachments/assets/8dd5ee07-3ac3-4f03-a56e-31c01d612655"
/>
<img width="1074" height="525" alt="Captura de pantalla 2026-05-28
003951"
src="https://github.com/user-attachments/assets/e140706b-3f1c-4e09-b70c-efc3e6536c60"
/>
<img width="914" height="513" alt="Captura de pantalla 2026-05-28
004632"
src="https://github.com/user-attachments/assets/e0dd6820-62f4-48b6-8356-df20c0e6ed8f"
/>
<img width="988" height="509" alt="Captura de pantalla 2026-05-28
005518"
src="https://github.com/user-attachments/assets/0da95c41-1191-4de4-a3ce-873839c00605"
/>
<img width="986" height="514" alt="Captura de pantalla 2026-05-28
000505"
src="https://github.com/user-attachments/assets/4eb20c73-56ba-4f9f-90af-8a047aa399eb"
/>


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

tri.star1011
2026-05-28 15:34:26 -07:00