mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-24 09:44:00 +00:00
52bcae5106f813c9a462b9863951b6f0fb6893d5
702
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
52bcae5106 |
Replace dark mode with player-adjustable lighting (#4280)
## What Removes the binary **dark mode** feature and replaces it with a player-adjustable **Lighting** section in graphics settings. ### In-game settings - Removed the Dark Mode toggle from both `SettingsModal` and `UserSettingModal`, and `darkMode()`/`toggleDarkMode()`/`DARK_MODE_KEY` from `UserSettings`. ### New Lighting section (Graphics Settings) - **Ambient light** slider (1–3): mapped to the renderer's ambient as `ambient = 1 / level`. **1.0 = no effect (unchanged look), 3.0 = darkest with the strongest structure glow.** - **Light falloff** slider (1–3): writes straight to `lighting.falloffPower`. - Lighting auto-enables only when ambient < 1, so the default (slider at 1) has zero GPU cost — off by default. ### Removed dark-mode overrides - Deleted `applyDarkModeOverride()` + `DARK_AMBIENT` and their wiring in `ClientGameRunner`, `gl/index.ts`, and the `DARK_MODE_KEY` listener. - Removed the `.dark` HUD-class toggle in `Main.ts` and the `userSettings.darkMode()` read in `PlayerIcons`. ### Train glow - `UT_TRAIN` light reduced (intensity `2.0 → 0.5`, radius `8 → 6`) so structures dominate the glow. ## Notes - Removing the dark-mode setting also retires the HUD's Tailwind dark theme (same setting). The dormant `dark:` CSS variants and unused white-icon assets are left in place (out of scope). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e494f83e8e |
New and updated categories for maps (#4254)
Resolves #4250 ## Description: Huge update for the map categories: https://github.com/user-attachments/assets/b7dc6344-efdc-4073-b15a-92b6dccdcc19 **New Categories** - Re-adds Continental category, with the 7 traditional continents - Re-adds the category of Arcade along all its maps. - Renames "Other" to "Fictional", so that tag is more specific and feels more in-theme with the others. The info.json's of the maps that had the Other category got changed to Fictional **Map Category changes** - **achiran**: adds Europe (while the map is fictional, it is made up of real islands from ireland. (Since world includes Dyslexdria and Antarctica has Deglaciated Antarctica, both fictional , i figured for consistency we could include these mash-up maps too) - **aegean**: adds Asia category (Turkey is in Asia) - **arctic**: adds Asia category - **choppingblock**: updated "other" to "fictional", added to "new" - **deglaciatedantarctica**: updated "other" to "fictional" - **didier**: re-added to Arcade - **didierfrance**: re-added to Arcade - **dyslexdria**: updated "other" to "fictional" - **fourislands**: updated "other" to "fictional" - **hawaii**: remove north_america tag (while part of the US, hawaii is geographically only in Oceania) - **labyrinth**: added to new, re-added to Arcade - **marenostrum**: added africa and asia tags, the continents which the mediterranean borders - **onion**: re-added to Arcade - **pangaea**: updated "other" to "fictional" - **passage**: updated "other" to "fictional" - **sierpinski** re-added to Arcade - **surrounded**: updated "other" to "fictional" - **svalmel**: updated "other" to "fictional", added to europe and north_america (same logic as achiran) - **thebox**: re-added to Arcade - **tradersdream**: updated "other" to "fictional" - **worldinverted**: updated "other" to "fictional", added to "new" - **africa**: added to Continental - **antarctica**: added to Continental - **asia**: added to Continental - **europe**: added to Continental - **northamerica**: added to Continental - **southamerica**: added to Continental - **oceania**: added to Continental - **mississippiriver**: added to "new" - **korea**: added to "new" - **middleeast**: added to "new" - **balkans**: added to "new" - **indiansubcontinent**: added to "new" - **taiwanstrait**: added to "new" - **northwestpassage**: added to "new" - **southeastasia**: added to "new" - **venice**: added to "new" - **yellowsea**: added to "new" - **hongkong**: added to "new" - **titan**: added to "new" - **caribbean**: added to "new" - **juandefucastrait**: added to "new" - **danishstraits**: added to "new" ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: tri.star1011 |
||
|
|
5102805d77 |
Adds Warship Warship map (with additional nations and team spawn) (#4261)
> **Before opening a PR:** discuss new features on [Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small improvements as [issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose). You must be assigned to an `approved` issue — unsolicited PRs will be auto-closed. **Add approved & assigned issue number here:** Resolves #4259 ## Description: 2 Warship shaped islands next to each other. Trade ships and land attacks can go through the corners. This can be either a 2 teams or a ffa map. Size is 3000x1396 with 29% land. This will complete the 20th map for v32 before it releases in 2 days. There are 10 nations with 23 additional nations (with ai generated names). The nations are made up similarly to the ones in traders dream but they are piracy themed and theres also a meme "Evil island man" nation (rex reference) It is based on a meme when Ultimus-Rex says "warship warship" when deploying warships and now people spam "warship warship" in the comments, especially this user named @warshipwarship who comments warship warship on every video. [https://youtu.be/DGMIji0bQQM](https://github.com/openfrontio/OpenFrontIO/issues/url) <img width="3000" height="1396" alt="image" src="https://github.com/user-attachments/assets/4bf6d708-afbc-41ea-be7c-cf43fdf69cbc" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME crunchybbbbb |
||
|
|
f4840a1cfd | Add Giant World Map to the multiplayer queue after recent performance improvements. I'm hitting 40fps on a 10 year old chromebook | ||
|
|
bca980f572 |
Shrink the per-tick worker → main update payload by ~90% (#4244)
Stacked on #4243 (the `perf:client` harness) — first step of fixing the every-100ms main-thread stutter: make the per-tick burst small before spreading what remains across frames. ## Problem The harness showed the main-thread burst was dominated by `structuredClone` of the `updates` object, and the clone was dominated by two kinds of per-tick churn that re-sent object payloads every tick: - `gold` / `troops` / `tilesOwned` change for nearly every alive player every tick → ~278 partial `PlayerUpdate` objects per tick (world/400 bots), ~508 on giantworldmap. - Attack troop counts tick down every tick → whole `outgoingAttacks`/`incomingAttacks` arrays re-cloned for every fighting player every tick. - `playerNameViewData` (an all-players record) was cloned every tick but only recomputed every 30 ticks. ## Change Three additions to the worker → main protocol (all transferable, zero-clone): 1. **`packedPlayerUpdates`** — `[smallID, tilesOwned, gold, troops]` float64 quads for players whose stats changed. These fields no longer appear in `PlayerUpdate` diffs (first emissions still carry the full snapshot). Gold is exact in a float64 (game values ≪ 2^53). 2. **`packedAttackUpdates`** — `[ownerSmallID, direction, index, troops]` quads. Attack arrays are only resent when membership/order/retreating changes — which is exactly the condition that keeps the patch indexes valid (a tick either resends an array or patches it, never both). 3. **`playerNameViewData` is now optional** — attached only on placement-rebuild ticks (spawn ticks, first ticks, every 30th, spawn end). The client keeps the last applied values; dead players' name placements freeze at death (matching the previous effective behavior). On the client, `GameView.populateFrame` now also rebuilds `names` / `relationMatrix` / `allianceClusters` only when their inputs changed that tick — field presence on a partial `PlayerUpdate` marks them dirty. (`playerStatus`, nuke telegraphs, and attack rings still recompute every tick; they're tick- or unit-dependent.) ## Results (perf:client, this machine; low-end devices ~5–20× slower) Default run (world, 400 bots, 1800 ticks): | stage | before | after | |---|---|---| | clone (serialize+deserialize) | 1.02ms | **0.09ms** | | GameView.update | 0.62ms | **0.29ms** | | WebGLFrameBuilder.update | 0.04ms | 0.04ms | | **TOTAL burst mean** | **1.67ms** | **0.42ms** | | TOTAL p99 / max | 3.47 / 10.3ms | **1.21 / 3.92ms** | giantworldmap/600t: 2.54 → 0.68ms mean. Player update objects: 278 → 6.5 per tick (world), 508 → 12 (giant). The remaining burst is mostly tile apply + per-tick derivations — the part that frame-spreading (next step) addresses. ## Verification - **Sim final hash unchanged** on all three reference configs (`5607618202213430`, `29309648281599524`, `39945089450032050`) — no simulation behavior change. - **View hash unchanged** on all three configs (`942106e9`, `a3aae227`, `cbaaf265`) — the rendered view state is provably identical tick-for-tick, including the name-freeze semantics. - New tests: `tests/PackedPlayerUpdates.test.ts` (drain + GameRunner cadence), packed-channel and freeze-at-death cases in `tests/client/view/GameView.test.ts`, `packAttackTroopDeltas` unit tests and updated diff contract in `tests/GameUpdateUtils.test.ts` / `tests/PlayerUpdateDiff.test.ts`. - `npm test` (1490 tests), `eslint`, `prettier`, `tsc --noEmit` all pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aa4b490e68 |
Simplify WebGL renderer integration: remove dead extension code, untangle GameView naming (#4240)
## Summary The WebGL renderer was adapted from an external extension and carried a lot of machinery this integration never uses (replay playback, its own input/event system, a GL radial menu). This PR is two mechanical cleanup passes with **no behavior change**: delete the dead code, then untangle the `GameView` naming collision. **78 files, +142 / −2,197.** ### Pass 1 — remove dead extension baggage - **Replay/copy mode**: `FrameData.tileMode` was hard-coded `"live"`; the copy branches in `frame/Upload.ts`, `UploadOptions` (never passed), `applyFullFrame`/`applyFullTiles`/`applyDelta` on the facade and `GPURenderer`, `HeatManager.resetForSeek`, and the seek-upload methods on `TerritoryPass`/`TrailPass` were all unreachable. Also deletes `types/Replay.ts`, `types/FrameSource.ts`, `types/GameUpdates.ts`, `types/Game.ts` (imported only by the types barrel). - **FrameEvents**: trimmed from 14 fields to the 3 actually populated and read (`deadUnits`, `conquestEvents`, `bonusEvents`). The other 11 fed the extension's stats system and were never written or read here. - **GL radial menu**: `RadialMenuPass`, its 4 shaders, and ~10 API methods on facade + renderer had zero callers — the game uses the DOM/d3 radial menu in `hud/layers/RadialMenu.ts`. The pass was constructed and drawn every frame for nothing. - **Facade event system**: `GameViewEventMap` defined 10 event types (`click`, `hover`, `scroll`, …) but only `contextrestored` was ever emitted — input actually flows through `InputHandler` → EventBus → controllers. Replaced the listener map with a single `onContextRestored` callback and deleted `Events.ts`. Also fixed the stale header comment claiming the facade handles user interaction. - **Unused API surface**: removed ~20 facade/renderer methods with zero callers (camera passthroughs like `panTo`/`zoomTo`/`fitMap`/`screenToWorld`, hit-testing queries, SAM replay setters, `setSelectedUnit`, `clearFx`/`setFxTimeFn`, `onFrame`/`afterRender`/fps tracking). Deliberately left alone: `Camera`'s pan/zoom primitives (building blocks for a possible future camera unification) and the `timeFn` plumbing inside the FX passes (deeply embedded as defaults; only the dead renderer-level wrappers were removed). ### Pass 2 — untangle the three GameViews - `render/gl/GameView.ts` → **`MapRenderer.ts`** (class `MapRenderer`). Every importer was already aliasing it as `WebGLGameView` to dodge the collision with the simulation-mirror `GameView` in `client/view/`, so this removes aliasing rather than adding churn. `render/CLAUDE.md` updated. - Deleted the `src/core/game/GameView.ts` back-compat shim (its own TODO asked for this). All 51 importers now import from `src/client/view/` directly via a new 3-line barrel `view/index.ts`. ## Test plan - `tsc --noEmit` clean, `eslint` clean - Full test suite passes (1,385 + 65 server tests) - Manual verification via headless Chromium: started a singleplayer game and confirmed the renderer works end-to-end — terrain draws, spawn-phase overlay shows, territories fill with borders after spawning, player names/flags render, no renderer console errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2e6f70c098 |
Speed up the core sim: inline sfc32 PRNG and allocation-free player updates (#4233)
## Summary Follow-up to #4230. Two more core-sim optimizations — these are **behavior-affecting in controlled ways** (unlike #4230, which was hash-identical), so both come with dedicated test coverage written before the change. Combined results (`npm run perf:game`, same machine, before → after): | run | mean tick | ticks/sec | p99 | peak heap | |---|---|---|---|---| | default (world, 400 bots, 1800 ticks) | 7.98 → **6.96 ms** | 125 → **144** | 21.2 → **19.0 ms** | 438 → **294 MB** | | giantworldmap, 600 ticks | 17.4 → **15.2 ms** | 58 → **66** | 32.6 → 30.5 ms | | Cumulative with #4230 vs. the original baseline: default run mean 9.04 → 6.96 ms (111 → 144 ticks/sec); giantworldmap 22.5 → 15.2 ms (44 → 66 ticks/sec, max tick 52.8 → 40.1 ms). ### 1. `PseudoRandom`: seedrandom ARC4 → inline sfc32 - ARC4 was ~4% of profiled self time. The new engine is sfc32 with splitmix32 seed expansion and a warmup, using only 32-bit integer ops — sequences are identical across platforms. The class API is unchanged. - This **removes the `seedrandom` dependency entirely**, making `src/core` actually dependency-free (the import was the only violation of that rule). - ⚠️ **The random stream differs, so the deterministic game-state hash changes.** All clients run the same code, so cross-client sync is unaffected; the harness reproduces the same hash on repeated runs per seed. New reference hashes: - `--map world --ticks 200 --bots 100` → `5607618202213430` - default run → `29309648281599524` - `--map giantworldmap --ticks 600` → `39945089450032050` - New `tests/PseudoRandom.test.ts` (15 tests) pins the engine-agnostic contract: per-seed determinism, ranges, uniformity, adjacent-seed decorrelation, and every API method. The tests were verified green against the old engine first, then the swap. - The stream change exposed a test that passed **by RNG luck**: in `AiAttackBehavior.test.ts`, "nation cannot attack allied player" was actually being blocked by the difficulty dice gate in `shouldAttack`, not the alliance check — hiding that the test's `AiAttackBehavior` was constructed without its `NationEmojiBehavior`. The test now supplies one and verifies the real protection layer (`AttackExecution`'s alliance check), robust to any dice outcome. ### 2. `PlayerImpl.toFullUpdate`: allocation-free empty collections - `toFullUpdate` runs for every player every tick and allocated ~10 collections each (allies, embargoes Set, attacks, alliance views, …) even when all were empty — the common case for most of 472 players. Because `lastSentUpdate` retains each snapshot for a full tick, these objects survived minor GC, got promoted, and accumulated as old-space garbage between major GCs — that's the peak-heap drop. - Empty collections now reuse shared **frozen** module-level singletons, so `diffPlayerUpdate`'s existing `a === b` fast paths skip structural comparison entirely. Non-empty collections build in single passes. Freezing makes accidental in-worker mutation throw loudly instead of silently corrupting every player; consumers across the worker boundary get mutable structured clones as before. (`Set` cannot be frozen — `EMPTY_EMBARGOES` is documented as never-mutate.) - Value-identical: the game-state hash is unchanged by this part (verified against the post-PRNG baseline). - New `tests/PlayerUpdateDiff.test.ts` (8 tests): full-snapshot shape, null-when-unchanged, embargo/alliance/target/attack diffs through the real tick pipeline, and the freeze contract. ### Verification - Full suite passes: 124 files / 1408 tests (23 new) + server tests; lint and prettier clean. - Hash reproducibility confirmed: repeated runs with identical args produce identical hashes on all three configs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
182d008ddd |
Generate a single MapInfo list; move SPECIAL_TEAM_MAPS and en.json map names into info.json (#4231)
**Add approved & assigned issue number here:** N/A — maintainer follow-up to #4227. ## Description: Follow-up to #4227, finishing the "info.json is the single source of truth" refactor. **Maps.gen.ts now generates one `MapInfo` interface and a `maps` list** instead of parallel lookup records. `mapCategories`, `mapTranslationKeys`, and `multiplayerFrequency` are gone — consumers read the list directly (`map.categories`, `map.translationKey`, `map.multiplayerFrequency`). MapPicker got simpler in the process: it renders from `MapInfo` objects, so the reverse `Object.entries(GameMapType)` lookup to recover the enum key is gone. The featured-rank sort moved out of the Go codegen into the picker, where the presentation concern belongs. **`SPECIAL_TEAM_MAPS` moves into info.json** as an optional `special_team_count` field (set on the same 17 maps with the same values). MapPlaylist derives its map from the generated list; `SPECIAL_TEAM_FORCE_CHANCE` and the frequency multiplier behavior are unchanged. **The en.json `map` section is now generated.** A new optional `display_name` field in info.json (defaulting to `name`) is written to `resources/lang/en.json` by the generator, preserving the section's non-map UI keys (`map`, `featured`, `all`, `favorites`, `random`). The 8 maps whose English display name intentionally differs from the frozen enum value (e.g. `MENA`, `Milky Way`, `Europe (Classic)`, `Baikal (Nuke Wars)`) declare it via `display_name`, so no display text changes. The section is emitted alphabetically; since #4232 already sorted en.json and every value matches, regeneration is byte-identical and this PR has no en.json diff. Other languages remain Crowdin-managed. The generator also now validates `translation_key` is exactly `map.<folder>` and `special_team_count >= 2`. MapConsistency tests compare info.json directly against the generated list and the en.json section, and fail with a "run `npm run gen-maps`" message on drift. No behavior changes: enum values, playlist frequencies, special-team counts, featured order, and display names are all byte-identical. ## Please complete the following: - [x] I have added screenshots for all UI updates (no UI changes — internal refactor, rendering output identical) - [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: evanpelle 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2789db8b96 |
Optimize core simulation hot paths (no behavior change) (#4230)
## Summary Pure performance optimizations to the attack/conquer/cluster hot paths in `src/core`, driven by the full-game perf harness from #4228. **No behavior change**: the final game-state hash is identical before/after on every config tested — world quick run (2 different seeds), giantworldmap, and the default 1800-tick run. ### Changes - **Flat-arithmetic neighbor iteration**: `forEachNeighbor` / `forEachNeighborWithDiag` / `isBorder` / `isOceanShore` are now implemented inside `GameMapImpl` using raw `ref±1` / `ref±width` index math, skipping the per-neighbor `ref()` coordinate validation (`Number.isInteger` etc.). `GameImpl` and `GameView` delegate. - **New `neighbors4(ref, out)`**: zero-allocation, callback-free neighbor query for hot loops (W, E, N, S — same order as `forEachNeighbor`). - **`AttackExecution`**: the per-tile closures in `tick()` / `addNeighbors()` are replaced with reusable neighbor buffers, a cached `GameMap` reference, and integer `smallID()` owner comparisons instead of owner-object lookups. - **`GameImpl`**: the per-conquer `updateBorders` closure is hoisted to a method with a reusable buffer; `removeInactiveExecutions` compacts the executions array in place instead of allocating a new ~4200-element array every tick. - **`PlayerExecution`**: `surroundedBySamePlayer` / `isSurrounded` / `getCapturingPlayer` de-closured (`neighbors4` + integer compares; neighbor visit order preserved, so `getCapturingPlayer`'s Map-insertion-order tie-breaking is unchanged); flood-fill visit closure hoisted out of the while loop. - **`FlatBinaryHeap.dequeue`**: returns the tile directly instead of allocating a `[tile, priority]` tuple per dequeued tile (AttackExecution is the only caller). ### Performance (`npm run perf:game`, same machine, before → after) | run | mean tick | ticks/sec | max tick | |---|---|---|---| | default (world, 400 bots, 1800 ticks) | 9.04 → **7.98 ms** | 111 → **125** | 31.7 → 35.7 ms | | giantworldmap, 600 ticks | 22.5 → **17.4 ms** | 44 → **58** | 52.8 → **36.2 ms** | The giantworldmap tail improvement (max tick −31%) is the most relevant for the 100 ms tick budget. ### Determinism verification Identical `Final hash` before and after on all configs: | config | hash | |---|---| | `--map world --ticks 200 --bots 100` | `5455008589403520` | | same + `--seed second-seed-check` | `5580840142777488` | | `--map giantworldmap --ticks 600` | `37373734953428430` | | default run | `26773450321979388` | ### Tests - New `tests/NeighborIteration.test.ts` pins the exact neighbor iteration orders (W,E,N,S cardinal; dx-major diagonal — conquest order and RNG consumption depend on them) and conquer/border-tile invariants checked mid-battle. - New `tests/FlatBinaryHeap.test.ts` covers heap ordering, clear, and growth. - Full suite passes (122 files / 1386 tests + server tests); lint and prettier clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3de5fb4204 |
Move map metadata into info.json and generate map TypeScript from it (#4227)
**Add approved & assigned issue number here:** N/A — maintainer refactor. ## Description: Makes each map's `info.json` the single source of truth for map metadata — adding a map is now a folder with `image.png` + `info.json`, a `gen-maps` run, and an en.json display name. **info.json / manifest.json carry full map metadata.** Every `map-generator/assets/maps/<map>/info.json` declares `id` (the `GameMapType` enum key), `name` (the enum value — wire format, unchanged for all 94 maps), `translation_key`, `categories`, and `multiplayer_frequency` (the public-playlist weight that used to be the `FREQUENCY` record in MapPlaylist.ts). The generator validates everything and mirrors it into `resources/maps/<map>/manifest.json`. 23 stale info.json `name` values were normalized to the canonical enum value; enum values are byte-identical, so replays and stored game configs are unaffected. **The generator emits the TypeScript and discovers maps itself.** New `map-generator/codegen.go` generates `src/core/game/Maps.gen.ts` (`GameMapType`, `GameMapName`, `mapCategories`, `mapTranslationKeys`, `multiplayerFrequency` — now a full `Record<GameMapName, number>`, killing the old `Partial`) on every run; `Game.ts` re-exports it. The hardcoded map registry in `main.go` is gone — maps are auto-discovered from the `assets/maps` / `assets/test_maps` directories. MapConsistency tests fail with a "run `npm run gen-maps`" message if info.json, manifest.json, and Maps.gen.ts drift. The tracked `map-generator/map-generator` binary is rebuilt to match. **New categories: continents + world/cosmic/tournament/other, multi-category support.** `continental`/`regional`/`fantasy`/`arcade` are replaced by `featured`, `world`, `europe`, `asia`, `north_america`, `africa`, `south_america`, `oceania`, `antarctica`, `cosmic`, `tournament`, and `other`. Maps can list multiple categories, so straddlers (Black Sea, Bosphorus, Caucasus, Between Two Seas, Bering Sea/Strait, Mena, Strait of Gibraltar, Hawaii, Arctic) appear under both regions. Featured is itself a category (same 7 maps as before). MapPlaylist keeps its arcade exclusion via an explicit set. **Map picker UI.** Two tabs: **Featured** (default — featured maps plus a Favorites section when maps are starred) and **All** (one prominent collapsible bar per category with a map count, collapsed by default). The selected map is prepended to the featured grid when it lives elsewhere. `getMapName()` resolves through the generated `mapTranslationKeys`, which also fixes tourney maps never resolving a valid translation key. ## Please complete the following: - [ ] I have added screenshots for all UI updates (maintainer change — picker described above) - [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: evanpelle 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7405339ea7 |
Add Titan map with random spawn nations - along new Cosmic map category (#4183)
Resolves #4182 ## Description: Adds "Titan" (real moon of Saturn with methane seas) map . Uses new random spawn nation feature by FloPinguin. https://github.com/openfrontio/OpenFrontIO/pull/4156 Also adds new Cosmic map category. The "Other" map category has become a wastebasket of unrelated maps, and with increasing number of maps, i think its a good addition to have better categories for these maps. I figured these 2 changes should go together since im adding a cosmic map, and a cosmic category. proof of nations spawning randomly and how the cosmic category looks in the menu: https://github.com/user-attachments/assets/b84bd3ef-6b8f-46fe-a6ea-ea5e79c6dc00 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: tri.star1011 --------- Co-authored-by: Evan <evanpelle@gmail.com> |
||
|
|
af2849a2d7 |
Adds "Juan De Fuca Strait" map - 3 way team map (#4215)
Resolves #4148 ## Description: Adds "Juan de Fuca Strait" map. This is the Strait in Washington and British Columbia: https://en.wikipedia.org/wiki/Strait_of_Juan_de_Fuca This map is meant to be a brand new 3-team way map, since all the team maps we have are either made for 2 or 4 teams. The map is bumped towards this gamemode similar to how Baikal is bumped to 2 teams. Map also has Additional Nations, for a total fof 62, for Human vs Nations and solo games <img width="1365" height="602" alt="image" src="https://github.com/user-attachments/assets/9cb86727-db06-4fcb-bee4-85e7b5d47d15" /> <img width="1319" height="488" alt="image" src="https://github.com/user-attachments/assets/13fd9a01-7ec6-49ab-81c3-40b566cbf6e0" /> data from OpenTopography, already credited ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: tri.star1011 |
||
|
|
b0e7d04f6e |
Add help notification system to control panel ℹ️ (#4212)
Resolves https://github.com/openfrontio/OpenFrontIO/issues/3445 ## Description: I copied the PR #3743 from @luctrate (Add army limit warning indicator for team games) to this PR because he didn't respond to requested changes but I thought it's important. I expanded on it, now its a full help message system: **Warnings (orange):** - Army limit: shown in team games with donations when troops exceed 80% of max - Low troops: shown when troops drop below 1k (=> new noob player who clicks too much) <img width="764" height="251" alt="582494157-cf19b13e-a0a9-44e4-8de8-86c007fe9c79" src="https://github.com/user-attachments/assets/6b4996d9-1993-4d2c-98ba-afba17a5ca4d" /> **Info messages (blue):** - Borders a traitor ally: "You can betray traitors without becoming a traitor yourself" (Because its not obvious for new players) - Borders an allied AFK player: "You can attack disconnected players even if you are allied with them" (Because its not obvious for new players) - Borders an AFK teammate: "You can attack disconnected teammates" (Because its not obvious for new players) Info messages only appear when the player has not attacked the relevant neighbor for at least 15 seconds, so they do not show up without reason. <img width="524" height="141" alt="image" src="https://github.com/user-attachments/assets/88d74661-d47e-45a7-9f91-d4f5361114b7" /> New "Help Messages" toggle in settings (default: on) <img width="409" height="105" alt="image" src="https://github.com/user-attachments/assets/24bc8bed-777b-4f72-9451-02116ac39db0" /> Implementation details: - Border detection uses async borderTiles() refreshed every 1s, cached in a Set of nearby player smallIDs - Outgoing attacks are tracked per-target to compute the 15-second idle threshold - New armyLimitWarningThreshold() on Config (returns 0.8) - All user-facing strings go through translateText() with en.json entries AI Model used: MiMo 2.5 Pro ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
5cb155267d |
Only notify on warship and transport ship destruction
displayMessageOnDeleted fired a "Your {unit} was destroyed" message for
nearly every unit type (denylisting only MIRV warheads and train cars),
which spammed the event feed with low-stakes losses like ports, cities,
and trade ships.
Flip it to an allowlist: only surface destruction of warships and
transport ships, the two losses with real tactical weight (lost naval
control, lost army at sea). Everything else is either visible on the map
or not worth a notification.
|
||
|
|
9396df1ca4 |
Remove unit capture event messages
Drop the "Your {unit} was captured by {name}" and "Captured {unit} from
{name}" display messages on unit ownership change in UnitImpl. They fired
on every capture — dominated by warships taking trade ships — and were
too spammy to be useful, so players tuned them out.
Also clean up the now-unused pieces:
- Remove the UNIT_CAPTURED_BY_ENEMY message type, its category mapping,
and its case in getMessageTypeClasses.
- Remove the orphaned unit_captured_by_enemy and captured_enemy_unit
en.json keys.
CAPTURED_ENEMY_UNIT is kept — still used by the trade-ship gold message.
|
||
|
|
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 |
||
|
|
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> |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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)
|
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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) |
||
|
|
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 |
||
|
|
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 |
||
|
|
2cb5244ad4 |
Adds Map of the Yellow Sea (#4026)
## Description: "A high-stakes naval theater where empires clash over narrow corridors, bottleneck straits, and heavily fortified shorelines." Modeled to the exact strategic proportions of the classic Black Sea map, Yellow Sea shifts the focus of global conflict to East Asia. The map is defined by its massive central body of water, making naval dominance absolutely essential for survival. However, unlike wide-open oceans, control of the Yellow Sea is entirely dictated by its unique coastal geography. The Shandong And Liaoning Peninsulas are The definitive feature of the map. Two massive, opposing peninsulas project deep into the sea, acting as natural, heavily contestable daggers. They create tight naval choke points in the central waters while forcing land-based players into brutal, linear frontlines where every pixel of territory is bought with blood. The Continental Rim: A sprawling mainland coast wraps around the northern and western edges of the map, offering expansive land routes for players who prefer sweeping land invasions over amphibious assaults. Scale Class: Medium Gameplay Style: Naval/Land Hybrid, Tactical Choke Points, Frontline Bottlenecks Nations: 8 North Korea South Korea Liaoning Shandong Beijing Hebei Tianjin Jilin description mostly generated by google gemini ai ## 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: DISCORD_USERNAME crunchybbbbb <img width="1660" height="1266" alt="Screenshot 2026-05-24 220103" src="https://github.com/user-attachments/assets/800c6732-677d-44f1-ba5c-c60da5f199e0" /> <img width="1500" height="1152" alt="yellow_sea2" src="https://github.com/user-attachments/assets/9b3ba34a-3f9c-4485-9235-f953fd07be4c" /> Game play video https://youtu.be/IcRPTM0rHM0 --------- Co-authored-by: RickD004 <realtacoco@gmail.com> |
||
|
|
10bf2be102 |
Remove unused Structure Sprites setting
The structureSprites toggle was only read by the toggle UIs themselves — no rendering code ever consulted it. Drops the getter/setter from UserSettings, both toggle rows (SettingsModal + UserSettingModal), and the en.json keys. Other-language entries are left for Crowdin to reap. |
||
|
|
20bc311caf |
Add Graphics Settings modal with live name-label tuning (#4065)
## Description:
- Add a user-facing **Graphics Settings** modal accessible from the
in-game Settings menu, with live preview as sliders change.
- First two knobs: **Name Scale** and **Minimum Name Size** (the
name-cull threshold).
- Overrides stored as a single JSON blob in `localStorage` under
`settings.graphics`, validated by a Zod schema
(`GraphicsOverridesSchema`). Future graphics knobs just extend the
schema + slider list.
## How it fits together
- `generateRenderSettings(overrides)` (`RenderSettings.ts`) — pure
function: clones `render-settings.json` defaults, layers overrides on
top, returns a fresh `RenderSettings`.
- `UserSettings.graphicsOverrides()` / `setGraphicsOverrides()` —
read/write the blob; falls back to `{}` on a missing/corrupt entry.
- `ClientGameRunner` listens for
`USER_SETTINGS_CHANGED_EVENT:settings.graphics`, regenerates, and
`Object.assign`s each category into the live `view.getSettings()` slice
so passes pick up the new values on the next frame (no renderer
reconstruction).
- Modal reads defaults straight from `render-settings.json` so there's
no duplication.
<img width="599" height="515" alt="Screenshot 2026-05-28 at 11 18 43 AM"
src="https://github.com/user-attachments/assets/263d7d91-10d8-4a66-a069-10015c735d60"
/>
## 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
|
||
|
|
aa3959bffe |
feat: territory png based skins (#4006)
## Description: Add image-based territory skins as a new cosmetic type, rendered alongside the existing 1-bit patterns. Skins render a single PNG centered on each player's spawn tile — opaque pixels show the skin (multiplied by team color in team games, raw colors in FFA), transparent pixels and tiles outside the image bounds fall through to the regular player palette color. **Cosmetic plumbing** - `SkinSchema` in `CosmeticSchemas.ts`, optional `skins` map on `CosmeticsSchema` - `PlayerSkin`, `PlayerCosmetics.skin`, `PlayerCosmeticRefs.skinName` in `Schemas.ts` - Server-side resolution: `PrivilegeCheckerImpl.isSkinAllowed` (gated by `skin:*` / `skin:<name>` flares) - Client persistence: stored under `PATTERN_KEY` (`pattern:` and `skin:` share one slot — they're mutually exclusive) - `getPlayerCosmeticsRefs` only emits a `skinName` when cosmetics are loaded, the skin exists in the catalog, and the user has the right flare — otherwise drops the ref and clears storage **Renderer** - `SkinAtlasArray` — fixed `TEXTURE_2D_ARRAY`, 1024×1024 per layer, exact layer count allocated once at game start from the locked-in player set. No resize, no callbacks, no retained `HTMLImageElement`. Zero GPU cost when no players have skins (1×1 placeholder). - `skinLayerTex` (R8UI 4096×1) — per-player `layer + 1` (`0` = no skin) - `skinAnchorTex` (RG16UI 4096×1) — per-player spawn tile, so the PNG center anchors at each player's spawn (re-uploads when the player re-picks during spawn phase) - `WebGLFrameBuilder.syncPlayers` collects unique skin URLs on first sync and calls `view.initSkinAtlas(urls)` once; `clearCaches()` resets so seek/replay re-initializes - `territory.frag.glsl`: skin branch is mutually exclusive with patterns; bounds-checks UVs against `[0, 1]` so the image is a single stamp, not tiled; alpha-blends against the player palette color so transparent pixels and out-of-bounds tiles render as the regular player color **Hover highlight (global UX change, not skin-scoped)** - Existing hover highlight changed from "brighten toward white" to "saturation boost." Applies to all players regardless of skin/pattern/flat-color — looks better across the board. **UI** - `CosmeticButton` renders skins as a single `<img>` (object-contain) - `TerritoryPatternsModal` merges patterns + skins into one grid; single "default" tile clears both - Selecting a pattern clears the skin and vice versa (mutually exclusive) - `Store` pattern tab includes skin entries (purchasable, not-yet-owned) - `PatternInput` lobby button previews the active skin when one is set **Memory** - 0 skin players → ~4 bytes (placeholder) + ~40 KB fixed per-player tables - 1 skin player → ~5.6 MB GPU - 5 skin players → ~28 MB GPU - 10 skin players → ~56 MB GPU **Tests** - `tests/Privilege.test.ts`: 13 new cases covering `isSkinAllowed` (wildcard, exact-match, missing flare, missing skin, forged refs) and `isAllowed` integration (allowed/forbidden paths, short-circuit when invalid skin is paired with valid other cosmetics) ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan |
||
|
|
172113193f |
Add May Labyrinth (#4002)
## Description: Labyrinth is a maze type map. My attempt at making a more chess style board for play. Games with bots appear stable at over 45min average run times. The map has been setup for team spawn zones for 2, 3, 4, 5, 6, and 7 teams. Some of the team spawns for odd numbers are experimental and I would like to see how they play out with live players. Additional nation names included. There are other design factors like each of the large squares being within the blast radius of a hydro; small islands are within the blast radius of nukes. This is meant as a slower playing game. My intentions are to get some sort of literal rotation of the map in the future if easily implemented. That way every time players load the game there would be some randomization. As an additional note one of my last edit to the map was the "+" shape to the islands to allow train passage. Zooming out I can see now that the pattern is squares and + through out. Did not fully intend on that, but it felt like good vibes. https://discord.com/channels/1284581928254701718/1293201128858587207/threads/1497062552784605316 https://www.youtube.com/watch?v=e8c-TylT4hs https://www.youtube.com/watch?v=0-yqrfr3nv0 ## 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 PlaysBadly --------- Co-authored-by: RickD004 <realtacoco@gmail.com> |
||
|
|
b086881a4e |
Add Korea Map (#3977)
## Description: Add map of korean peninsula. Size 1092x2149 Nations: 35 based on provinces ## 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: DISCORD_USERNAME crunchybbbbb @crunchybbbbb_59469 <img width="1092" height="2149" alt="Korea-2" src="https://github.com/user-attachments/assets/874100e8-4a68-4f57-b2f7-49aa87b8194d" /> two teams nations war video https://www.youtube.com/watch?v=n4h7GAfAHTM --------- Co-authored-by: Ricky G.P. <realtacoco@gmail.com> |
||
|
|
5a2c0504eb |
adds map of the Balkans (using Additional nations feature) (#3998)
## Description: Adds map of the Balkan Peninsula and surroundings. Heavily requested map with multiple posts on the Discord all with over 10 or 20 upvotes. 23 NPC/Nations based on countries and relevant regions of the area. Adds an extra 39 nations for crowded Humans vs Nations gamemode for a total of 62 NPCs, based on regions of multiple countries. Also some flags for some regions. Source from NASA DEM, already credited Photo of base map, and 62 HvN: <img width="614" height="588" alt="Captura de pantalla 2026-05-24 030105" src="https://github.com/user-attachments/assets/5742a4c3-1b1f-4ca7-858d-91529861dd81" /> <img width="548" height="547" alt="image" src="https://github.com/user-attachments/assets/758d8ad0-1515-41b8-8d42-14e76cdd54ed" /> This map completes the quartet row of "polemic" maps for v32 <img width="678" height="119" alt="image" src="https://github.com/user-attachments/assets/9e6f4ef1-f0cc-48ea-a59f-b7ff69033b73" /> ## 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 |
||
|
|
db501c68d2 |
Put friends on the same team (#3994)
Fixes #3911 ## Description: - Server captures `publicId` and `friends` from `getUserMe()` and includes each player's in-game friend `clientID`s in `PlayerSchema` on game start - Team assignment treats friends as a **soft preference** (best-effort): a non-clan player goes to the team where the most of their friends already are; if that team is full they spill to the next-emptiest team rather than getting kicked - Clans remain strict (kick overflow) since clan membership is an explicit opt-in; friends are implicit, so a friend-of-friend chain that doesn't fit shouldn't bench anyone - Friendship is symmetric — an edge from either direction counts, which keeps things working when one side's `getUserMe` is stale - Lobby preview unchanged — friend grouping only takes effect once the game actually starts (avoids exposing friend lists in the lobby payload) ## 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 |
||
|
|
b1ec3ac70f |
Adds Indian Subcontinent map (#3975)
## Description: Adds Map of the Indian Subcontinent, with indian and pakistani states and surrounding countries, important rivers like the Ganges, Brahmaputra and Indus, and Tibet/ theHimalayas 2M land pixels and 52 Nations (i think its fitting that India has the most nations of a regional map, only continental maps have more) Should be nice to boost whatever indian playerbase this game might have. This region also doesnt have any representation aside from continental maps <img width="584" height="598" alt="image" src="https://github.com/user-attachments/assets/4089049a-800b-4e37-ab34-2afc5de821e8" /> <img width="418" height="462" alt="image" src="https://github.com/user-attachments/assets/a68e2424-5972-4105-86c9-0312ab095024" /> Elevation data from NASA DEM, already credited in CREDITS.md No reference test is needed, the test suite automatically iterates over all GameMapType enum values — no map is hardcoded by name in the 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: tri.star1011 |
||
|
|
2ee2fb97e3 |
WebGL: return of factory/defence post radii, and railroad highlighting when placing city/port right on top (#3981)
## Description: Show factory and defence post radius for ghost structure when placing structures from build bar (unitdisplay). Show when city/port is placed directly over existing railroad, by highlighting the railroad green. The railroad is not highlighted when instead a city/port nearby the ghost structure will be upgraded instead of placing it on the railroad. This works with the existing code in buildableUnits in PlayerImpl: it would already return an empty array [] for overlappingRailroads and for ghostRailPaths when canUpgrade is false. So the old checks for uiState for Canvas2D in BuildPreviewController weren't even needed per se, they followed the same logic as buildableUnits in PlayerImpl already did. Both changes emulate how it worked before the move to WebGL. - OverlappingRailroads now returns TileRefs instead of a railroad ID, and it does so with less allocations than the previous code. It's a determistic outcome, sorted and deduplicated. In doubt about this a bit, because it's better also in case we ever do desync checks using this data, but for the rendering it isn't needed per se and could be more performant without allocations. - Also: Cleanup obsolete Canvas2D rail highlighting state (UIState) that was superseded by GhostPreviewData. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 |
||
|
|
41ef675e98 |
Improve Notification Panel (#3913)
Resolves #3910 ## Description: - Split the events HUD into two components: a new **`<actionable-events>`** that owns alliance prompts (request / renew) and a slimmed-down **`<events-display>`** for everything else. - Reworked `<events-display>` into two visual tiers: dim/scrolling tier 2 on top (trade results, unit losses, donations, alliance status), prominent tier 1 anchored at the bottom (inbound nukes, naval invasion, attack requests, alliance broken, conquered player, chat). Tier 2 caps at the 4 newest entries; events expire after 8s. - Added a transient **+gold pip** above the gold pill in `<control-panel>`, animated with a small fade-in. Fires for trade ships, trains, donations, and conquest. Trade-ship and train arrivals are removed from the events scroll since they're surfaced here instead. - New `MessageType.NUKE_DETONATED` and a server-side emission in `NukeExecution.detonate` — once an inbound nuke lands or gets intercepted, the inbound warning vanishes and a "detonated" entry takes its place. - `displayMessage` gained optional `unitID` and `focusPlayerID` params so events can link to a unit or a player. Unit captures and destructions now navigate to the unit's last tile when clicked; donations navigate to the other player. - ActionableEvents card width matches `<events-display>`; cards persist until the user clicks Accept/Reject/Renew/Ignore or the server-side request timeout expires. - Removed the in-events category filter UI and the gold-amount banner — `<events-display>` is now a lightweight log that hides entirely when empty. <img width="570" height="444" alt="Screenshot 2026-05-21 at 1 42 30 PM" src="https://github.com/user-attachments/assets/f103efb3-0e11-4b72-a11b-91ff6896177c" /> <img width="430" height="296" alt="Screenshot 2026-05-21 at 1 41 34 PM" src="https://github.com/user-attachments/assets/ae58475a-b252-4aa6-9ce5-99dea7575ce3" /> ## 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 |
||
|
|
6591b055c3 |
Adds map of Venice 🛶 (#3935)
## Description: Adds map of Venice. A relatively small map (similar land area to World) for heavy trade and lots of boating. Because of the very low difference of elevation of the zone, terrain is instead used to show buildings. Map source from OpenStreetMap, already credited in CREDITS.md Very requested map, with 2 discord posts suggesting it with +15 upvotes each <img width="794" height="569" alt="image" src="https://github.com/user-attachments/assets/ca7d44f2-cfc9-4e93-b7d4-43dbe62f74d4" /> ## 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 |
||
|
|
17e3ac4b05 |
make spawn glow follow the player's currently selected spawn tile
Plumb spawnTile through PlayerUpdate / PlayerState / applyStateUpdate so the WebGL spawn overlay can read it directly. The glow was reading nameData.x/y (territory centroid for label placement) which only recomputes every 2 ticks and only when largestClusterBoundingBox has been updated by PlayerExecution — both lag the player's actual spawn click. Using spawnTile updates the same tick setSpawnTile() fires. Also adds spawnTile to diffPlayerUpdate / applyStateUpdate so changes after the initial full snapshot actually propagate (the recent diff-only PlayerUpdate path silently dropped any field not enumerated in those helpers). |
||
|
|
62e15d2794 |
Cut worker→main bandwidth ~3.3× by switching PlayerUpdate to deltas (#3967)
## Description: Cut worker→main bandwidth ~3.3× by switching PlayerUpdate from a full per-tick snapshot to a field-level diff. PlayerImpl.toUpdate() now caches the last sent update and returns only changed fields, or null if nothing changed. The client-side applyStateUpdate() merges instead of overwriting. Per-tick total dropped from ~297 KB to ~89 KB; the Player bucket alone went from 258 KB/tick to 50 KB/tick. Diff/apply logic lives in a new GameUpdateUtils.ts module with unit 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: evan |
||
|
|
5b663fae14 |
refactor: share renderer state shapes between game and WebGL renderer
PlayerView/UnitView now wrap renderer-shaped state objects (PlayerState, PlayerStatic, UnitState) directly instead of holding engine wire types. GameView owns a long-lived FrameData object kept in sync each tick: players/units/tiles/trail/railroad are mutated in place; derived buffers (playerStatus, relationMatrix, allianceClusters, nukeTelegraphs, attackRings) and events are recomputed in a final populateFrame() pass. The renderer reads gameView.frameData() and the same byte-identical state objects PlayerView/UnitView wrap. WebGLFrameBuilder shrinks from ~270 to ~70 LOC: palette management + a single uploadFrameData() call, no per-frame UnitState allocation on the hot path. Wiring: maxPlayers=1024 on RendererConfig (pre-sizes NamePass/palette/ relation matrix textures); NamePass disabled so HTML NameLayer remains the only on-screen player names. Also: 39 new tests covering PlayerView/GameView/FrameData behavior; replace .data field access in three layer call sites with accessor methods (betrayals(), type(), getTraitorRemainingTicks()). |
||
|
|
53cf2d43f8 | migrate away from canvas | ||
|
|
7dc5d472a7 |
Change name of map "The Straits" into "Danish Straits" (#3929)
## Description: Renames TheStraits map. The people that suggested this map told me they would prefer a more specific name for the map, rather than the generic one it has right now. So im renaming it into Danish Straits This map is for v32, it has not been released, it should be fine to rename ## 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 |
||
|
|
7359e2bc3b |
Adds Northwest Passage map (using new additionalNations feature) (#3920)
## Description: Adds map "Northwest Passage", map of the Canadian Arctic Archipelago , Greenland and surroundings. "Northwest Passage" (NWP) is the sea lane between the Atlantic and Pacific oceans (https://en.wikipedia.org/wiki/Northwest_Passage) . 21 default nations, based on the towns of the region. This map uses the brand new additionalNations feature made by FloPinguin https://github.com/openfrontio/OpenFrontIO/pull/3902 . Adds 39 extra nations for a total of 60 nations (so that in gamemodes like Humans vs Nations all the nations have names of real places) Comparison: - Map with default nations - Map with extra named nations, tested by raising the number of nations in Solo <img width="1050" height="412" alt="image" src="https://github.com/user-attachments/assets/12ed94f1-0615-4fb3-b0d0-dcecb65006ea" /> <img width="1089" height="436" alt="image" src="https://github.com/user-attachments/assets/6e7c11bf-7382-4e36-9433-229a9d463b68" /> Terrain source from OpenTopography, already credited in CREDITS.md ## 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 |
||
|
|
834a9757d7 |
Adds map "The Straits" (#3896)
## Description: **Adds "The Straits" map:** A map located around Denmark and the many surrounding straits: Kattegat, Skagerrak and the Danish straits (thus the name, meant to be a creative name like "Between Two Seas" and "Gateway to the Atlantic"). This map is themed in the early 1900s, the nations/NPCs are traditional and historical regions of Sweden-Norway, Denmark and the Germany. Relatively small map with ~700k land tiles, similar to World Inspired by this Discord thread with nearly 20 upvotes: https://canary.discord.com/channels/1284581928254701718/1482089104110911634/1482089104110911634 <img width="365" height="506" alt="image" src="https://github.com/user-attachments/assets/5ee16218-34c0-4b8b-9f9b-d33f219760b0" /> ## 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 |