mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 00:15:23 +00:00
38bbef6ecf7de7cee96722e94cdeeb2d1320ca3d
970 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
38bbef6ecf | update structure icon filename to bust cache, previous assets had bad headers | ||
|
|
8a638a3842 |
perf(UnitLayer): batch trail clears to fix O(n²) cost on mass nuke explosions (#3808)
## Description: When multiple nukes detonated in the same tick, clearTrail was called once per dying unit. Each call scanned all remaining units to repaint overlapping trail tiles — O(dead × alive × trail_len) per tick. Replace with a deferred batch: dying units are queued into pendingTrailClears during drawUnitsCells, then flushTrailClears() processes them all at once after the draw pass. All trail tiles are cleared in a single loop (skipping duplicates), followed by one repaint scan of surviving units — O((dead + alive) × trail_len). Also fixes a minor bug in the original: the surviving unit's relationship is now used when repainting its trail (previously the dying unit's relationship was used, which gave wrong colors in alternate-view mode). ## 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 |
||
|
|
4f20d2b332 |
TypeScript update to 6.0.3 (#3806)
## Description: Updating TypeScript to 6.0.3. Updating TypeScript-eslint to 8.59.1 for TS6 support. Concurrently needed to get updated as well to remove deprecated warning. Most things deleted are now just defaults. ## 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: Babyboucher |
||
|
|
742a544a69 |
2661 PR 3/3 Warship Manual Override, Aggro Override, and Heal-at-Port Command (#3501)
Part of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661) (split into 3 PRs so they are not too large..) ## Description: Part 3/3 of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661). This PR adds the retreat control and override behavior for warships: - Manual override: moving a warship manually cancels retreat and suppresses auto-retreat for 5 seconds - Aggro override: a retreating warship will aggro a nearby enemy transport or warship before continuing retreat - Heal-at-port command for sending a warship to a friendly port manually - Friendly-port validation for HealAtPortExecution - Regression tests for manual override, aggro override, and heal-at-port behavior ## 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: zixer._ --------- Co-authored-by: iamlewis <lewismmmm@gmail.com> Co-authored-by: evanpelle <evanpelle@gmail.com> |
||
|
|
1776ae4f35 |
go to player on spawn start (#3802)
## Description: Some new players were having trouble finding themselves on game start * Emits a GoToPlayerEvent (zoom=8) on the first turn after the spawn phase, using a hasGoneToPlayer flag to ensure it only fires once per session * Adds a zoom parameter to GoToPlayerEvent so callers can specify a target zoom level * Adds smooth zoom animation to TransformHandler — the camera now eases to the target scale alongside the existing position easing, with screen-center correction to avoid visual jumping on mobile (where canvas and map dimensions differ) * Moves GoToPlayerEvent, GoToPositionEvent, and GoToUnitEvent out of Leaderboard.ts into TransformHandler.ts, where they logically belong ## 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 |
||
|
|
f304141338 |
Fix/refactor/optim(StructureIconsLayer): restore structure icons after context loss, use WebGL/WebGPU/Canvas, and some improvements (#3654)
## Description: StructureIconsLayer and StructureDrawingUtils fixes and improvements. Most notably have it restore structure icons after webGL context loss. Inspired by @Skigim's https://github.com/openfrontio/OpenFrontIO/pull/3339, https://github.com/openfrontio/OpenFrontIO/pull/3480. Fixes his https://github.com/openfrontio/OpenFrontIO/issues/3207, contains only those fixes from the Issue that are actually valid and needed fixes, on top of his earlier merged PR. ### CONTAINS (partly written by AI, excuse the exaggerated language) **1.** * ** AutoDetectRenderer: ** now, if Hardware Acceleration is unavailable or disabled, Structure Icons will be displayed using Canvas renderer. Otherwise it will use either WebGL or WebGPU, depeding on which is available. PixiJS currently prefers WebGL but it will switch this to WebGPU at one point. We can also force it to WebGPU as explained in the comment. * ** Canvas: ** on Canvas, what doesn't work is gracefully skipped. The non-working parts will be fixed, see this issue in their repo, but until then it will work fine for us anyway: https://github.com/pixijs/pixijs/issues/11981 * **WebGPU Context Loss:** PixiJS doesn't restore this context itself. Instead we do it by calling setupRenderer again on device loss. * **WebGL Context Loss:** To know when we need to restore the layer, don't use native event (`webglcontextrestored`) but use PixiJS's internal hook (`this.renderer.runners.contextChange`). This prevents our cache-clearing commands from interrupting Pixi while it's still busy rebuilding its internal GL State Machine buffer. With links severed, we need to clear and rebuild all icons to restore them. * **WebGL Context existance Check (`this.renderer.context?.isLost`):** This prevents a crash in PixiJS. Fixes black map background and all graphics frozen, which has been reported a few times. Issue created in their repo: https://github.com/pixijs/pixijs/issues/12032. * **Redraw:** for Canvas context restore or on Alt-R, a call from GameRenderer now actually restores icons. Also called for WebGPU device loss and after contextChange WebGL restoration. Checks for WebGL context.isLost so a calls from Alt-R etc won't meddle while GL context is lost. * **Orphaned Object Leaks:** In PixiJS v8, `Container.destroy()` does *not* recursively destroy its children. This PR explicitly adds `.destroy({ children: true })` inside icon deletion states. This stops thousands of `PIXI.Sprite` and `PIXI.BitmapText` child nodes from leaking and choking Pixi when it attempts a WebGL restore. * **Texture Lifecycle:** Invalidate caching logic in `SpriteFactory` now correctly executes `.destroy(true)` on `PIXI.Texture` objects. Previously, they were only deleted from the textureCache Map, retaining a phantom grip on GPU memory buffers. * **Don't remove PIXI.Texture.EMPTY from textureCache: `createTexture()` in `SpriteFactory` stores `PIXI.Texture.EMPTY` (a singleton) in `textureCache` when a structure type has no known shape. When not preventing removal of the EMPTY texture, `clearCache()` would call `texture.destroy(true)` on PixiJS's shared global empty texture, breaking all sprites in the renderer that fall back to it. **2. Small Memory/Perf Optimizations** * **The Shared 2D Canvas Optimization:** To prevent allocating endless tiny `<canvas>` elements every time a structure color is loaded, `SpriteFactory` now utilizes a cleanly shared `colorCanvas` singleton. To keep this safe from hardware acceleration crashes (where the 2D context dies alongside WebGL), it accurately nullifies itself in `clearCache()` and lazily instantiates on the next call (`getImageColored()`). * **Bypassing Inefficient Textures Cache:** Now passing the `skipCache: true` argument implicitly to dynamic UI elements via `PIXI.Texture.from(structureCanvas, true)`. * **Zero-Allocation Filters (No more GC Stutters):** `renderGhost()` previously spawned numerous `new OutlineFilter(...)` WebGL shaders when hovering over invalid tiles, compounding to many leaked Shader Programs. We hoisted these filters to static class properties initialized once, and went a step further: hoisted the wrapping Array structures too (`filterRedArray`, `filterGreenArray`). This eliminates many pointless micro-allocations and GC sweeps entirely. **BEFORE, for webGL:** https://youtu.be/durJxNFNePs **AFTER, for WebGL:** https://youtu.be/VnYEFMx4gfM **AFTER, for Canvas:** https://youtu.be/zT720oKxcaI **AFTER, for WebGPU:** https://youtu.be/J09Wee2qTs8 The performance optimizations weren't well measurable in my tests but there's no downgrade at least. WebGPU should bee better than WebGL when we would force it but again, currently PixiJS prefers WebGL hardcoded so only if we disallow WebGL will it use WebGPU if it is available, otherwise fallback gracefully to Canvas still. Canvas skips parts gracefully, as long as the non-breaking issue exists in PixiJS (as explained above): <img width="952" height="705" alt="AFTER Canvas in Firefox skips non-supported gracefully" src="https://github.com/user-attachments/assets/17e8d8ab-05dc-47cb-ab11-f0f4d015a42a" /> ## 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 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> |
||
|
|
d00425871d |
Fix cross-browser CSS-mask CORS failures for OpenFrontLogo and SoldierIcon (#3792)
## Description: Cross-origin CSS-mask icons were failing on Chrome and Safari because mask: url(...) triggers a CORS-mode fetch (unlike plain <img>), and stale browser caches without ACAO break per-user. Instead change the svgs with the appropriate colors so we don't need to mask them ## 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 |
||
|
|
70f425e354 | emit sound when structure starts building not on completion | ||
|
|
01b3cbe332 |
Set crossOrigin = "anonymous" on canvas-bound icon images (#3789)
## Description: StructureLayer.loadIcon and StructureDrawingUtils.loadIcon hand-roll new Image() and feed the result into a canvas. With assets now served from a cross-origin CDN, the default no-cors fetch tainted the canvas, and WebGL's texImage2D rejected the upload `Uncaught SecurityError: Tainted canvases may not be loaded`. Setting crossOrigin = "anonymous" before src switches to a CORS-checked fetch (R2 already returns ACAO), so the canvas stays clean and the texture upload succeeds. Other new Image() and <img> sites in the codebase don't need the change — they're either DOM-only or read naturalWidth/naturalHeight only. ## 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 |
||
|
|
fc45410ee5 | Remove the ticks() % 5 gate in TerritoryLayer.spawnHighlight() so the spawn highlight redraws every frame instead of every 5th frame. | ||
|
|
0c0f9c2a81 |
Update attack labels (#3784)
## Description: The motivation behind this PR is to standardize colors & icons for incoming and outgoing attacks. Outgoing attacks are always aquarious and incoming are red. This also makes it much easier to see which attacks are incoming vs outgoing at a glance, as previously the color changed depending on attack effictiveness. Instead, show a small bar on the left side that displays attack effectiveness. <img width="498" height="456" alt="Screenshot 2026-04-27 at 12 58 53 PM" src="https://github.com/user-attachments/assets/ea6928b3-5dfa-47fa-84d2-63e1e81ef6a4" /> Updates the in-game attack labels to match AttacksDisplay: a single soldier icon recolored via CSS filters, aquarius for outgoing and red-400 for incoming. Color is now purely directional — the previous attacker-vs-defender comparison (and the troopAttackColor / troopDefenceColor helpers that drove it) is gone, along with the defenderTroops plumbing. Also adds zoom-aware sizing via a new computeLabelScale(zoom) (full screen size when zoomed in, linear shrink with a floor so labels never disappear), bumps font/padding/snap-jump threshold for readability, and moves immutable per-label DOM writes (icon src/filter, color) into element creation so the per-tick path only updates the troop count. Also fixes a bug where the labels kept swapping when 2 clusters where similar size ## 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 |
||
|
|
4aeece4aef |
fix: render spawn highlight on 1/5 frames instead of 4/5 (#3782)
Resolves #3590 ## Description: The spawnHighlight() function in TerritoryLayer.ts was using `=== 0` as the condition to return early, which caused the spawn highlight to render on 4 out of every 5 frames instead of the intended 1 out of 5. Changed `=== 0` to `!== 0` so the function skips rendering on 4/5 frames, improving performance especially on large maps. ## 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 - [x ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced |
||
|
|
62299c9714 |
standardize UI colors to fit brand guidelines (#3754)
## Description: We have brand colors: <img width="738" height="900" alt="Screenshot 2026-04-25 at 12 52 29 PM" src="https://github.com/user-attachments/assets/aac69e87-91f2-4c3f-9f1e-f69f48f5943e" /> So update the homepage & in-game UI to use those colors: <img width="1185" height="946" alt="Screenshot 2026-04-25 at 12 51 06 PM" src="https://github.com/user-attachments/assets/89a0b12c-2db1-43d4-9500-fcf405c0f7ff" /> Also updated buttons to use the o-button element ## 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 |
||
|
|
37079e6a05 |
2661 PR 1/3 Warship Retreat Core, Blue UI Signal, and Transport-First Target Priority (#3498)
Part of #2661 (split into 3 PRs so they are not too large..) ## Description: Part 1/3 of #2661. This PR adds warship retreat basics, a blue retreating UI state, and updates target priority. Added: - Retreat state handling - Blue visual for retreating warships - Target priority: transport > warship > trade - Tests for retreat and target priority Example video: https://youtu.be/2hE2qeOeY48 Ship retreating: <img width="630" height="488" alt="image" src="https://github.com/user-attachments/assets/56d3e6d5-08af-453d-afe5-ee21dd6f3414" /> Ship healing: <img width="483" height="311" alt="image" src="https://github.com/user-attachments/assets/aeaf2239-bb81-444f-84ef-62dbcb48fddf" /> Back to being deployed: <img width="585" height="358" alt="image" src="https://github.com/user-attachments/assets/875828a2-8a24-4593-ac76-26426bb81057" /> ## 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: zixer._ |
||
|
|
66bbbc664b |
Colored SVG smiley icons next to nation names based on relation 😊 (#3746)
## Description: Instead of coloring nation names based on diplomatic relation, small inline SVG face icons are now shown next to the nation name in the player info overlay: - 😠 **Hostile** - red angry face (furrowed brows, downturned mouth) - 😟 **Distrustful** - orange slightly-sad face (flat mouth) - 😊 **Friendly** - green happy face (upturned smile) - **Neutral** - no icon shown <img width="509" height="80" alt="Screenshot 2026-04-23 013151" src="https://github.com/user-attachments/assets/85dc3f29-0a84-45d1-902e-e75c6cad4a44" /> <img width="511" height="82" alt="Screenshot 2026-04-23 012809" src="https://github.com/user-attachments/assets/7a37c8a3-08d0-448e-9eaa-16f254a296ad" /> <img width="511" height="88" alt="Screenshot 2026-04-23 012741" src="https://github.com/user-attachments/assets/d617f8ca-2315-467a-85f5-63f769bd0341" /> No longer conflicts with green text color because of alliance now. ## 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: FloPinguin |
||
|
|
236f611f61 |
Cap RailroadLayer Maximum Texture Size (#3584)
Resolves #3582 ## Description: Almost exactly the same fix as #3574 , just to RailroadLayer instead of StuctureLayer. While browsers like Firefox will report their maximum texture size of 16384, going over 8192 causes extreme VRAM usage and massive FPS drops. This issue is slightly more elusive as the RailroadLayer texture is not rendered until the first railroad is created, meaning FPS will suddenly drop mid-game. This PR sets the RailroadLayer texture size to cap at 8192, while keeping near-exact scales. The result is increased performance, reduced VRAM Usage, (especially in larger maps), and the resolution of the unplayable performance issues when RailroadLayer is present, with zero noticeable degradation. All tested on Giant World, where the issues were first spotted, but applies to all maps. ## 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 Discord: @EnderBoy9217 |
||
|
|
4fd162415a |
Add fullscreen support: HUD button (desktop/Android) + iOS Add to Home Screen banner (#3688)
Resolves #3685 ## Description: Adds fullscreen support for both desktop and mobile: **Desktop / Android** — a fullscreen toggle button in the in-game HUD (right sidebar), next to the settings button. Icon switches between expand/compress depending on current state, synced with `fullscreenchange` event (works with F11 too). Hidden on browsers that don't support `document.fullscreenEnabled`. **iOS** — since Safari doesn't support the Fullscreen API, a dismissible banner is shown on the main screen (above the lobby cards) explaining how to add the game to the Home Screen for a fullscreen experience. The banner includes: - **How** button — opens a step-by-step guide modal with iOS version detection (iOS 26+ shows updated steps for the new ··· menu location, including the extra Share step inside the menu) - **Later** — hides until next visit - **Never** — permanently dismisses via localStorage - **Click here** button styled as a speech bubble with a tail pointing toward the Share button location (center for iOS ≤18, right for iOS 26+) All user-facing strings are wired through `translateText()` with keys added to `en.json`. ## 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 ## UI changes: ### For [Fullscreen API supported browsers](https://caniuse.com/?search=fullscreen+api): https://github.com/user-attachments/assets/026e6a67-d070-4a7e-897b-52396a43191e ### For safari on ios: (add to homescreen modal) <img width="375" height="667" alt="IMG_2242" src="https://github.com/user-attachments/assets/9d0a6454-8512-44cf-b6ed-989de3ff02bc" /> <img width="648" height="1292" alt="CleanShot 2026-04-22 at 11 29 27@2x" src="https://github.com/user-attachments/assets/dba1c218-2b73-4bc0-ac7d-14962eb79327" /> ## Please put your Discord username so you can be contacted if a bug or regression is found: fghjk_60845 --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
f7716c7d42 |
Little Console Cleanup 🧹 (#3741)
## Description: Fixes these console warnings from bots: <img width="591" height="94" alt="Screenshot 2026-04-19 033624" src="https://github.com/user-attachments/assets/6ee79302-e2a7-4195-94e5-c1f455eb1799" /> Removes some spammy logs, they dont seem to be helpful? <img width="271" height="174" alt="Screenshot 2026-04-19 033739" src="https://github.com/user-attachments/assets/70122506-e8fb-4a72-b73e-08e72fe222bd" /> <img width="284" height="656" alt="Screenshot 2026-04-19 033646" src="https://github.com/user-attachments/assets/4b4ebef2-e191-4947-9615-0e26cd9bf075" /> ## 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: FloPinguin |
||
|
|
0801798fbd |
Feat: Alliance and betrayal hotkeys (#3110)
Original Feature request by @FloPinguin Resolves #3077 ## Description: Adds hotkeys for Requesting alliances and breaking alliances. This allows for players to send or break alliances whose tile is under the cursor, without opening the radial menu. Keybinds: New "Ally Keybinds" section in Settings -> Keybinds Request alliance: Default: K - sends an alliance request to the player/bot/nation under the cursor Break alliance: Default: L - breaks the alliance with the player at the cursor Behavior: - Cursor must be over a tile owned by the target player. The action runs only when the game allows it, following the same logic as the radial menu. (canSendAllianceRequest and canBreakAlliance) - When an alliance request is sent, the events log shows: "Alliance request sent to [target]" for confirmation. No extra message for breaking an alliance (betrayal/debuff message already exists and is sent upon breaking an alliance) ## Screenshots: Keybind menu: <img width="739" height="595" alt="image" src="https://github.com/user-attachments/assets/ee958eab-fd50-4971-85c5-dfd49c6f0bdc" /> In game logs: <img width="373" height="232" alt="image" src="https://github.com/user-attachments/assets/2cf6bb07-5f0d-425a-82d3-65a44fef99c5" /> ## 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 Discord username: _Dave9595_ |
||
|
|
eedb90ffb5 |
Fix game ID display for team games 🪪 (#3734)
## Description: Game ID display was looking weird in team games... Before: <img width="389" height="321" alt="image" src="https://github.com/user-attachments/assets/cf39b490-cfba-4c3a-86af-8f9498380eae" /> After: <img width="394" height="323" alt="image" src="https://github.com/user-attachments/assets/9e828169-b267-4627-85eb-548dca224a8a" /> ## 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: FloPinguin |
||
|
|
29a1e8dfda |
feat: multi-warship selection with Shift+drag box (#3677)
Resolves #3666 ## Description: Adds RTS-style box selection for warships. Hold Shift and drag (desktop) or long-press and drag (touch/mobile) to draw a selection rectangle — all player-owned warships inside get selected at once. A subsequent click/tap on water sends them all to that location. - `SelectionBoxLayer` — pixel-dashed rectangle in world-space, player territory color; shared between desktop and touch - `UILayer` — same pulsing selection outline on each box-selected warship; clears correctly when switching between single/multi selection - `UnitLayer` — finds warships in screen rect, filters inactive ships before sending; touch support included - `InputHandler` — Shift+drag and touch long-press+drag both emit selection box events; cursor becomes crosshair on Shift; discards active ghost structure on Shift press; configurable via `shiftKey` keybind - `Transport` — single atomic `move_multiple_warships` intent (no split on socket drop) - `Schemas` + `ExecutionManager` + `MoveMultipleWarshipsExecution` — server fans out atomic intent into individual `MoveWarshipExecution` per ship - `DynamicUILayer` — `MoveIndicatorUI` chevron animation on target tile for both single and multi move - `UnitDisplay` — warship tooltip Shift hint via `translateText` - `HelpModal` — new hotkey row: Shift + drag → select multiple warships ## 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 ## UI update ### Mouse + Keyboard https://github.com/user-attachments/assets/3f35ab5e-1f3c-4c5d-bc4f-aabccf64dc60 ### Touch https://github.com/user-attachments/assets/0d6aec3f-44fa-4fee-b5c6-b267b9b14d79 ## ## Please put your Discord username so you can be contacted if a bug or regression is found: fghjk_60845 |
||
|
|
c3d7d0373e |
Improve ingame moderation for admins (#3678)
## Description: Players with the `admin` flare can now kick players from any game (including public lobbies), not just the lobby creator in private lobbies. ## 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 |
||
|
|
4e04bed44c |
Show game ID ingame 🪪 (#3674)
## Description: Instead of begging youtubers to share their game id to be able to debug: Display the current game ID in the top-right corner of the in-game leaderboard panel (there was unused space) <img width="391" height="326" alt="image" src="https://github.com/user-attachments/assets/8b0aa7c2-fc8c-48e5-ae11-edd60fd40de9" /> ## 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: FloPinguin |
||
|
|
565060f346 | Merge branch 'v30' | ||
|
|
adeb6a306a |
Update homepage & in-game promos (#3602)
## Description: * Replace the static footer ad (HomeFooterAd component) with a Playwire bottom_rail ad that loads on lobby join and persists into the spawn phase * Expand in-game ad slots from 1 to 3 (standard_iab_left1, left3, left4) with a timer-based visibility check to show a background container when ads render * Remove the resize-based footer ad height logic and gutter ad vertical offset adjustments that depended on it <img width="1828" height="961" alt="Screenshot 2026-04-16 at 12 14 00 PM" src="https://github.com/user-attachments/assets/50bfd0de-dd54-4f8b-b75e-04b720a1841b" /> <img width="1286" height="939" alt="Screenshot 2026-04-16 at 11 59 18 AM" src="https://github.com/user-attachments/assets/e0fb0762-82e7-444f-8706-5908aad0f094" /> ## 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 |
||
|
|
41c72a0f9e |
UI Updates (#3616)
## Description: Updates Favicon and other key UI elements ## 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 --------- Co-authored-by: iamharry <harrylong0905@gmail.com> Co-authored-by: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Co-authored-by: evanpelle <evanpelle@gmail.com> |
||
|
|
318d1e2c44 |
Refactor/Fix(UserSettings): last localstorage calls now use UserSettings, and Keybinds fixes (#3619)
## Description: 1) Have last localstorage calls for keybinds and attack ratio also use UserSettings cache instead, after #3481. Remaining calls to localstorage are for different things than user settings, so they are left as is. 2) Consolidate and centralize keybinds logic. And three fixes for it. - **UnitDisplay** and **UserSettingsModal**: _parsedUserKeybinds_ is introduced in **UserSettings** to centralize their logic. It is also used by _normalizedUserKeybinds_, see point below. - **UserSettingsModal** -- replaced unwanted cast `as SettingKeybind` by a typed QuerySelector. -- renamed this.keybinds to this.userKeybinds for more clarity, and distinction from defaultKeybinds. -- state private _userKeybinds_: remove type string[] since loadKeybindsFromStorage replaces a value array by its first string element, so it can not contain string[] anymore. -- _handleKeybindChange_ and _getKeyValue_: no need to check for Array.isArray anymore, see above reason. -- **Fix**: checks after calling _parsedUserKeybinds_ are improved a bit: don't delete all keybinds and print a console warning when finding just one invalid keybind and (i think i have seen people complaining about things being removed). Instead it now migrates or throws out the invalid ones but keeps the valid ones. Also works with the "Null" value expected and removed within **UserSettingsModal**._handleyKeybindChange_() and in **HelpModal**. When legacy value is an array and key is empty, don't put value as key but get first array element or empty string as key name. So that check on line 68 is true. - **HelpModal** and **InputHandler**: Also centralize/consolidate their logic more, by having __keybinds()_ from **UserSettings** perform fetching _getDefaultKeybinds_ and _normalizedUserKeybinds_. -- Functionality in _normalizedUserKeybinds_ is the same: Where HelpModal did return [k, v.value] if typeof (v as any).value === "string", this is now handled by lines 309-310 of normalizedKeybinds still the same but with less lines. Same for old HelplModal if (typeof v === "string") return [k, v], this is stil returned by line 112 of normalizedKeybinds. And return [k, undefined] when (typeof val !== "string") as was done in InputHandler, isn't needed as values that weren't strings were already filtered out right after which we still do on line 314 of normalizedKeybinds. -- **Fix** in _normalizedUserKeybinds_: added one extra thing that was a discrepancy between **HelpModal**/**InputHandler** and **UserSettingsModal** before: **UserSettingsModal** would handle array values, and normalize them by picking only the first value if it is a string. Now have _normalizedKeybinds_ do the same. Otherwise it would have thrown those values out while **UserSettingsModal** would have kept the first value. This may still help a returning player who hasn't played in the last version (i think i have seen people complaining about things being removed, but that may not have been about this). And makes the logic more consistent between **UserSettingsModal** and **HelpModal**/**InputHandler**. - **UserSettings**: -- _getDefaultKeybinds_: centralized/consolidated logic, accepts Platform.isMac parameter. In **HelpModal**, **InputHandler** and **UserSettingsModal** the same list with default keybinds was hardcoded. Now they all read from _getDefaultKeybinds_. The list of default keybinds in **HelpModal** was a little shorter, but that doesn't matter since its _render_() function has hardcoded which of the hotkeys **HelpModal** shows. Have thought about putting default keybinds in **DefaultConfig** but with all the logic handled through **UserSettings**, this seemed the better place in the current refactor. -- _removeCached_: make public, now that **InputHandler.test.ts** needs to be able to call it. We could instead make a public function like removeKeybinds() and keep removeCached() private, but went with this for now. -- _parsedUserKeybinds_: centralized/consolidated logic for **UserSettingsModal**/**UserDisplay**. Always returns an object, even an empty one if the JSON wasn't parsable. -- _normalizedKeybinds_: centralized/consolidated logic. Used by _keybinds_() which is now called by **HelpModal**/**InputHandler**. -- _keybinds_: now uses getDefaultKeybinds() and normalizedKeybinds() to get the default and user changed keybinds. -- **Fix** in _keybinds_: it now removes a key if it is Unbound by the user in **UserSettingsModal**. Instead of first loading the parsedUserKeybinds, removing "Null" keys from it, and then merging that with defaultKeybinds (so default key would overwrite an unbound key), we now merge parsedUserKeybinds with defaultKeybinds and after that remove "Null" keys from it (so that unbound key stays removed). For example if Boat Attack Up is set to "None" ("Null") by clicking Unbind, there is now no hotkey working for it anymore. Even when the default is "B". Why? This prevents the user from being confused, they have deliberately Unbound it, they don't understand why it still works (have seen bug reports and game feedback about this)? Also more importantly: they used to now be able to bind "B" to another action. Effectively making key "B" bound to two actions: the user choosen one and Boat Attack. This also makes the logic more consistent. Because building hotkeys in **UnitDisplay** already didn't work when unbound, eg. when Build Missile Silo was Unbound, the "5" key did not do anything anymore (there is a fallback in **UnitDisplay** in case the key is actually null, but it does respect "Null" as it should). -- _setKeybinds_: have it accept an object, it stringifies it itself. Callers UserSettingsModal and InputHandler.test.ts now just send either a string or an object. - **InputHandler.test.ts**: -- use **UserSettings** functions instead of localStorage for more real-world testing. -- change test "ignores non-string values and preserves defaults, removes 'Null' for unbound keys". As explained above, as a fix we no longer preserve unbound ("Null") keys within InputHandler. UserSettings.keybinds() now removes "Null" keys as explained above. - ControlPanel: use UserSettings to fetch initial attack ratio. ## 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 |
||
|
|
616ba1c794 |
Add support to purchase cosmetics with in-game currency (#3648)
## Description: Caps & Plutonium can be used to purchase different cosmetics. * The cosmetic button can display pluto/caps/dollars * Create a "purchaseCosmetic" helper function that handles purchase logic ## 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 |
||
|
|
17c1a6300f |
Trading in lakes 🚤 (#3653)
## Description: - Widened port placement and warship spawn/patrol checks from `isOcean`/`isOceanShore` to `isWater`/`isShore`, so ports can be built on lake shores and ships can operate on lakes, we discussed it here: <img width="996" height="423" alt="image" src="https://github.com/user-attachments/assets/acf1e970-9631-4848-a0ed-6d0470616e1d" /> - Filtered `tradingPorts()` by water component so ports only attempt trades with reachable ports - prevents silent path-not-found failures across disconnected water bodies - Applied the same water component filter when a captured trade ship reroutes to its new owner's nearest port - Removed the `WaterManager` fallback that force-marked isolated water-nuked-tiles as ocean (no longer needed since lakes are now navigable) - Added a check to prevent nations from building ports on water bodies that aren't accessible to other players ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [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: FloPinguin --------- Co-authored-by: Evan <evanpelle@gmail.com> |
||
|
|
d5a2cc0fca |
cosmetic refactor (#3628)
## Description: The motivation is to have a single "cosmetic-button" element, so we can abstract out the cosmetic types. This will make it much easier to add new cosmetic types in the future. Unifies PatternButton and FlagButton into a single CosmeticButton component. Extracts a resolveCosmetics() function that flattens patterns × color palettes + flags into a ResolvedCosmetic[] with relationship status pre-computed, replacing duplicated resolution logic across four callers. * New CosmeticButton — renders patterns or flags based on ResolvedCosmetic.type * New resolveCosmetics() — centralizes ownership/purchase/blocked resolution * Extracted PatternPreview — canvas rendering split into its own module * Added type: "pattern" | "flag" discriminator to Zod cosmetic schemas * Deleted FlagButton.ts and PatternButton.ts * Added 320-line test suite for resolveCosmetics ## 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 |
||
|
|
7f7cbba12f |
Water-Nukes 💧 (#3604)
## Description: Adds a new `waterNukes` game config option that causes nuclear detonations to convert land tiles into water instead of just leaving fallout. When enabled, nuked land tiles are batched and converted to water each tick, with full terrain metadata updates including: - Ocean bit propagation from adjacent ocean tiles (BFS flood fill) - Magnitude recomputation via BFS from remaining coastlines - Shoreline bit fix-up in a 2-ring neighborhood around converted tiles - Minimap terrain sync (majority-rule downsampling) - Throttled water navigation graph rebuild (every 20 ticks) for ship pathfinding - Ship executions detect graph rebuilds and refresh their pathfinders - TransportShips auto-retreat if their destination becomes water - Water nuke craters use a smoothed angular noise ring with a bounding-box scan instead of the regular per-tile random coin flip with BFS, producing clean blob-shaped craters without scattered land pixels that players would have to boat to individually The `TerrainLayer` now incrementally repaints tiles that changed terrain type, and tile update packets encode the terrain byte alongside tile state so clients can reflect water conversions in real time. When `waterNukes` is disabled, behavior is unchanged (fallout only). Includes a new test suite (WaterNukes.test.ts) covering the conversion pipeline, ocean propagation, magnitude recalculation, shoreline updates, and minimap sync. Also adds a new public game modifier for the special rotation. ### The only problem A bit of lag on impact. But otherwise it works great and is fun. Maybe needs some followup improvements if it gets merged. I think its very cool in baikal / four islands team games. Chip away the territory of your opponents. Its also fun to turn The Box / Alps into a water map (its actually possible to boat-trade then) ### Media Video does not show the updated craters https://github.com/user-attachments/assets/aed8bf08-0e94-4484-b997-4de11ae313d9 Updated craters (no tiny islands after impact): <img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/e896870b-bc9d-493d-8bc8-b3a5427d69d3" /> <img width="1472" height="920" alt="image" src="https://github.com/user-attachments/assets/677065aa-0159-48cd-af44-a91b0f57adfc" /> <img width="1296" height="892" alt="image" src="https://github.com/user-attachments/assets/886ffaba-541f-4e46-97c6-ce963f632fe0" /> ## 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: FloPinguin |
||
|
|
646d7ecaf6 |
Disable Radial Menu during spawn phase, just left click or tap to pick spawn point (#3611)
## Description: During spawn phase, disable Radial Menu further. Its options where already greyed out or non-responding on purpose, except for the attack button (middle button) which could only be used to select a spawn point but two clicks for that is convoluted. It was mostly a nuisance, especially on mobile where you where forced to go through the Radial Menu, so tap and then tap again to pick a spawn point. - Now, left click directly places a spawn point. Even if "Left click opens menu" is enabled. - And right click does not open Radial menu anymore. Which had no use anyway. And also makes touch screen and mouse players more alike in that they now both have no access to the Radial Menu (which didn't have a purpose anyway except picking spawn point in a convoluted way with two clicks). - On touch screen during spawn phase, the Radial Menu also doesn't open anymore. Instead of two taps (open Radial Menu > tap attack button), now one tap suffices to pick a spawn point just like one left mouse click now does. Fixes https://github.com/openfrontio/OpenFrontIO/issues/3609 Also from UnitLayer > onTouch: - remove redundant isValidRef check. Since isValidCoord check was added in PR 3226 above it, we know it is a correct coord and with that correct ref, already. - remove redundant comment about isValidCoord/Ref not being checked there yet intentionally, because it is actually being checked there since PR 3226. ## 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 --------- Co-authored-by: iamlewis <lewismmmm@gmail.com> |
||
|
|
105404ca50 |
Fix: Player Panel shown on top of Win Modal (#3618)
## Description: Put Win Modal on top of other modals, namely the Player Panel (z-index 10001) and EmojiTable (z-index 10002). Because currently if Player Panel is open when the Win Modal (z-index 9999) appears on death/win, the Panel is incorrectly shown on top of the Win Modal. Fix is to up Win Modal z-index to 10010, which also leaves room for other (newly added) panels/modals below it still. Fixes: https://discord.com/channels/1284581928254701718/1284581928833388619/1491504813521895534  ## 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 |
||
|
|
50bd075b1c |
Fix deselected host lobby settings persisting for joiners 🐛 (#3607)
## Description: ### Problem When a host toggled off certain settings (game length, PVP immunity, starting gold, gold multiplier, disable alliances) in the host lobby modal, joiners still saw the old values. The settings appeared "stuck" once enabled. ### Root Cause `putGameConfig()` sent `undefined` for disabled settings, but `JSON.stringify` strips `undefined` properties entirely. The server's `!== undefined` guard never fired, so the old value was never cleared. ### Fix - **HostLobbyModal**: Send `null` instead of `undefined` when these settings are toggled off (`null` survives JSON serialization) - **Schemas**: Add `.nullable()` to the five affected fields (`maxTimerValue`, `spawnImmunityDuration`, `goldMultiplier`, `startingGold`, `disableAlliances`) - **GameServer**: Use `?? undefined` (nullish coalescing) to convert incoming `null` back to `undefined` when storing on the config Other settings are unaffected. Booleans like `infiniteGold` always send `true`/`false`, and fields like `bots`/`gameMap` always have a concrete value.. ## 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: FloPinguin --------- Co-authored-by: Evan <evanpelle@gmail.com> |
||
|
|
1cbee79cc7 |
Reduce Attacking Troops Overlay Reflows (#3608)
## Description: Vimacs on Discord pointed out a heavier than needed DOM load from the [AttackingTroopsOverlay PR](https://github.com/openfrontio/OpenFrontIO/pull/3427) - Caches a single `labelTemplate` in `AttackingTroopsOverlay`, built once on init and cloned per label instead of recreating it each time - Removes redundant inline style assignments that were repeated on every label creation - Simplifies `updateLabelContent` by accessing template-guaranteed children directly by index ## 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: Radyus |
||
|
|
18da7134c8 |
Implement FX sound effects (#3394)
## Description: Adds sound effects for approved events from the [sound asset pack](https://drive.google.com/drive/folders/1KpGYJkmLxipy8XmTeyHf40XDC4P--Ck8?usp=sharing). 15 new sound effects triggered from `FxLayer`, `EventsDisplay`, and `RadialMenu`. Sounds play even when visual FX are off, so disabling explosions doesn't kill audio. Unapproved sounds are included as assets but not wired up yet. ### SoundManager architecture Reworked `SoundManager` per [maintainer feedback](https://github.com/openfrontio/OpenFrontIO/issues/1893#issuecomment-4184649434) and [follow-up review](https://github.com/openfrontio/OpenFrontIO/pull/3394): - No more singleton. `SoundManager` is instantiated in `createClientGame()` with `EventBus` and `UserSettings` - Layers emit events (`PlaySoundEffectEvent`, `SetBackgroundMusicVolumeEvent`, `SetSoundEffectsVolumeEvent`) via EventBus instead of holding a `SoundManager` reference - `SoundManager` subscribes to these events in its constructor - `SoundEffect` is a type union (not an enum), per project convention - All sound configuration (type, URL mapping, events) lives in `Sounds.ts` - Sound effects are lazy-loaded on first play - Channel limit of 8 concurrent sounds. New sounds always play; when at the limit, the oldest active sound gets stopped - `SoundManager` bootstraps volume from `UserSettings` in its constructor - All Howler calls are wrapped in try/catch with error logging, so sound failures never crash the game - `dispose()` method unsubscribes from EventBus and unloads all Howl instances on game shutdown - Sound code stays entirely in `src/client/`, nothing in `core/` touches it ## Sound approval status (per [spreadsheet](https://drive.google.com/drive/folders/1KpGYJkmLxipy8XmTeyHf40XDC4P--Ck8?usp=sharing)) ### Approved, wired up in this PR | Event | Sound file | Trigger location | |-------|-----------|-----------------| | Message sent/received | `message.mp3` | EventsDisplay | | Menu open/select | `click.mp3` | RadialMenu | | Atom bomb launch | `atom-launch.mp3` | FxLayer (unit created) | | Atom bomb / MIRV hit | `atom-hit.mp3` | FxLayer (reached target) | | Hydrogen launch | `hydrogen-launch.mp3` | FxLayer (unit created) | | Hydrogen hit | `hydrogen-hit.mp3` | FxLayer (reached target) | | MIRV launch | `mirv-launch.mp3` | FxLayer (unit created) | | Alliance suggested | `alliance-suggested.mp3` | EventsDisplay | | Alliance broken | `alliance-broken.mp3` | EventsDisplay | | Port built | `build-port.mp3` | FxLayer (construction complete) | | City built | `build-city.mp3` | FxLayer (construction complete) | | Defense post built | `build-defense-post.mp3` | FxLayer (construction complete) | | Warship built | `build-warship.mp3` | FxLayer (unit created) | | SAM built | `sam-built.mp3` | FxLayer (construction complete) | ### Waiting for approval, sound files included but NOT wired up | Event | Sound file | Notes | |-------|-----------|-------| | Missile Silo built | `silo-built.mp3` | Waiting for Approval | | SAM shoot | `sam-shoot.mp3` | Waiting for Approval | | SAM hit | - | Waiting for Approval, no sound file assigned | | Warship sunk | - | Waiting for Approval, no sound file assigned | | Warship shoot | - | Waiting for Approval, no sound file assigned | ### Not done, no sound files exist yet | Event | Notes | |-------|-------| | Looted player | "Not sure if needed" | | Invaded | - | | Ship invasion incoming | - | | Ship sent | - | | Menu theme song | - | | Ambience | "Not sure if needed" | ## Test plan - [x] Start a private game and launch atom/hydrogen/MIRV nukes, verify launch and detonation sounds - [x] Build structures (city, port, defense post, SAM), verify build completion sounds - [x] Build a warship, verify warship built sound - [x] Receive an alliance request, verify alliance suggested sound - [x] Break an alliance, verify alliance broken sound - [ ] Receive a chat message, verify message sound - [x] Open the radial menu and click items, verify click sound - [x] Disable visual FX in settings, verify sounds still play - [x] Adjust SFX volume slider, verify it affects all new sounds - [x] Verify no audio issues with rapid/overlapping events - [x] Verify SoundManager responds to EventBus events and unsubscribes cleanly on dispose - [x] Verify SoundManager swallows Howler errors without crashing the game - [x] Verify channel limit of 8, oldest sound stopped when at cap ## Checklist - [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 Resolves #1893 ## Please put your Discord username so you can be contacted if a bug or regression is found: cool_clarky |
||
|
|
341f344ce5 |
Perf/Refactor(UserSettings): caching makes it 10-20x faster (#3481)
## Description: Skip slow and blocking LocalStorage reads, replace by a Map. Also some refactoring. ### Contains - No out-of-sync issue between main and worker thread: Earlier PRs got a comment from evan about main & worker.worker thread having their own version of usersettings and possibly getting out-of-sync (see https://github.com/openfrontio/OpenFrontIO/pull/760#pullrequestreview-2845155737, https://github.com/openfrontio/OpenFrontIO/pull/896#pullrequestreview-2871836979 and https://github.com/openfrontio/OpenFrontIO/pull/1266. But userSettings is not used in files ran by worker.worker, not even 10 months after evan's first comment about it. In GameRunner, createGameRunner sends NULL to getConfig as argument for userSettings. And DefaultConfig guards against userSettings being null by throwing an error, but it has never been thrown which points to worker.worker thread not using userSettings. So we do not need to worry about syncing between the threads currently. (If needed in the future after all, we could quite easily sync it, by loading the userSettings cache on worker.worker and listening to the "user-settings-changed" event @scamiv to keep it synced (changes in WorkerMessages and WorkerClient etc would be needed to handle this). - Went with cache in UserSettings, not with listening to "user-settings-changed" event: "user-settings-changed" was added by @scamiv and is used in PerformanceOverlay. Which is great for single files that need the very best performance. But having to add that same system to any file reading settings, scales poorly and would lead to messy code. Also, a developer could make the mistake of not listening to the event and it would end up just reading LocalStorage again just like now. Also a developer might forget removing the listener or so etc. The cache is a central solution and fast, without changes to other files needed and future-proof. - Make sure each setting is cached: UserSettingsModal was using LocalStorage directly by itself for some things. Made it use the central UserSettings methods instead so we avoid LocalStorage reads as much as possible. For this, changed get() and set() in UserSettings to getBool() and setBool(), to introduce a getString() and setString() for use in UserSettingsModal while keeping getCached() and setCached() private within UserSettings. - Remove unused 'focusLocked' and 'toggleFocusLocked' from UserSettings: was last changed 11 months ago to just return false. Since then we've moved to different ways of highlighting and this setting isn't used anymore. No existing references or callers are left. - Other files: -- Have callers call the renamed functions (see point above) -- Remove userSettings from UILayer and Territorylayer: the variable is unused in those files. Also remove from GameRenderer when it calls TerritoryLayer. -- Cache calls to defaultconfig Theme (which in turn calls dark mode setting)/Config better in: GameView and Terrainlayer. ### Update on Contents later on It wasn't really in scope of this PR but further consolidation was called for. These changes could also pave the way for UserSettingsModal (main menu) perhaps being partly mergable with SettingsModal (in-game) one day as it begins to look more like it. Even though UserSettingsModal still does things its own way, and does console.log where SettingsModal doesn't, etc. They both have partially different content and settings but also have a large overlap. - UserSettings: Removed localStorage call from clearFlag() and setFlag() which were added after creation of this PR, and were neatly merged in silence without merge conflicts so i wasn't aware of them yet until now. - UserSettings: added key constants, exported to use both inside UserSettings and in files that listen to its events. - UserSettings 'emitChange': now done from setCached, removed from setBool, setFlag etc. Also removed from the new setFlag. And from setPattern even though it emitted "pattern" instead of key name "territoryPattern"; now it emits the default "territoryPattern" from PATTERN_KEY which is re-used in Store, TerritoryPatternsModal and PatternInput. - UserSettingsModal: made UserSettingsModal call existing toggle functions in UserSettings, or new or existing getter or setter. We do not need CustomEvent: checked anymore. In UserSettingsModal, its toggle functions did not all actually toggle, some like toggleLeftClickOpensMenu actually just set a value. Based on the 'checked' value of the CustomEvent. But we don't need that 'checked' value anymore and none of the checks for it inside the toggle functions in UserSettingsModal, now that we just directly call toggleLeftClickOpensMenu and others in UserSettings. - SettingToggle: continuing about not needing CustomEvent anymore: the old way actually fired two events. The native change event from <input> and our own CustomEvent from handleChange in SettingToggle. It prevented handling both events by checking e.detail?.checked === undefined. But now, the native <input> event is all we need to show the visual toggle change and trigger @changed in UserSettingsModal which calls the toggle function. - Use the toggle functions too from CopyButton and PerformanceOverlay.ts. In PerformanceOverlay, change in onUserSettingsChanged was needed because of how setBool works. - UserSettingsModal 'toggleDarkMode': in UserSettingsModal, removed the event from toggleDarkMode in UserSettingsModal; nothing is listening to this event anymore after DarkModeButton.ts was removed some time ago. Also both UserSettingsModal an UserSettings added/removed "dark" from the document element. Now that UserSettingsModal calls toggleDarkMode in UserSettings, we could centralize that. But UserSettings is in core, not in client like UserSettingsModal. But now that we emit "user-settings-changed", we could handle it even more centralized and not have UserSettingsModal or UserSettings touch the element directly. Instead have Main.ts listen to the event and change it dark mode from there. - UserSettings: added claryfing comment to attackRatioIncrement and the new attackRatio setters/getters, to explain their difference. Noticed a small omitment in its description and fixed that right away in en.json: you can change attack ratio increment by shift+mouse wheel scroll or by hotkey. So made "How much the attack ratio keybinds change per press" also mention "/scroll." **BEFORE** (with getDisplayName added back to NameLayer as a fix i will do soon) get > getItem in UserSettings  renderLayer in NameLayer (with getDisplayName added back to NameLayer as a fix i will do soon)  **AFTER** (with getDisplayName added back to NameLayer as a fix i will do soon) getCached in UserSettings  renderLayer in NameLayer (with getDisplayName added back to NameLayer as a fix i will do soon)  ## 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 |
||
|
|
b5ca0f9d8f |
Perf/refactor/fix(NameLayer): about 10% extra improvement (#3540)
## Description: NameLayer perf part 2 after https://github.com/openfrontio/OpenFrontIO/pull/3475 with thanks to @scamiv. Shaves off another 10% or thereabouts, even doing something extra for a fix (see below). Also refactor/fixes around NameLayer and PlayerIcons, which is used by both NameLayer and PlayerInfoOverlay, and underlying function in GameView. This would go well with other PR https://github.com/openfrontio/OpenFrontIO/pull/3481, since this layer reads multiple settings. Reasoning to not use events and instead rely on fast caching is explained in that PR. ### Contents - Fixes: -- Fixes bug on .dev introduced by wrong assumption by me in previous PR https://github.com/openfrontio/OpenFrontIO/pull/3475. displayName CAN change during game, when Hidden Names is toggled from settings, so needs to be put back in renderPlayerInfo. -- Fixes longer existing bug: it was assumed Dark Mode didn't change after creation of icon element. Now it also sets Dark Mode attribute when updating icons elements. -- Fixes target mark icons not being shown to team members, while the icons were shown to normal allies. And EventsDisplay displayed message "XX requests you attack XX" to both team members and allies already. So why is the icon not shown to both if the message already is. While we improve performance of GameView > PlayerView > transitiveTargets (which is only used by NameLayer/PlayerIcons so only in this context). We can add team members' targets to it in one go. So previously transitiveTargets returned: your own targets and allies' targets. Now transitiveTargets is faster and returns: your own targets and allies' targets and team members' targets. - NameLayer: -- renderLayer: for target icons, getPlayerIcons used to fetch myPlayer.transitiveTargets each time. While that doesn't change per player we're rendering for. So now, we fetch myPlayer.transitiveTargets once per call to renderPlayerInfo, which passes it on to getPlayerIcons. So now we check it 1x each 100ms (renderCheckRate) inside of renderLayer. Instead of up to 100s of times each 500ms (renderRefreshRate) inside of getPlayerIcons inside of renderPlayerInfo loop. -- createBasePlayerElement and renderPlayerInfo: use cloneNode where possible with templates -- createPlayerElement: only find the elements and set font and flag. Leave the rest to renderLayer > renderPlayerInfo which fills displayName and troops and font color very soon after anyway. I haven't noticed a difference in testing. -- cache game.config() and others -- renderPlayerInfo: remove check if render.flagDiv exists, we know it exists. Check if fontColor changed before assigning it (it never changes, currently, be it dark mode or light mode). Don't check if troops or size changes, that happens so often that the overhead for checking would be smaller than the win, probably. -- We don't require nameLocation to be changed to change scale (see previous Namelayer perf PR for the reason). But it seems good to check if the transform changed before 'overwriting' it, so do that now instead. -- Remove Alliance icon DOM traversals. Only do it once, for each time an alliance icon is displayed. To this end, also made NameLayer more agnostic on Alliance icon stuff. By moving more code to PlayerIcons. See below. -- Use cached allianceDuration instead of fetching this static value every time -- Re-use from PlayerIcons: ALLIANCE_ICON_ID, TRAITOR_ICON_ID etc -- create more sub-functions to make the icons loop in renderLayer more readable: handleEmojiIcon, handleAllianceIcons, createOrUpdateIconElement (createIconElement already existed, now combined), handleTraitorIcon. For Alliance icons, this was already done in PlayerIcons.ts through createAllianceProgressIcon (now createAllianceProgressIconRefs), and more now to skip some DOM traversals. But most of this belongs in NameLayer itself when it comes to seperation of concern. - cache dark mode (as boolean and as string) - use dark mode to update (alliance) icons too, not only on create, since the setting can change after icon element creation and before it is removed -- for getPlayerIcons, add this.alliancesDisabled. If disabled, getPlayerIcons won't fetch Alliance icon and Alliance Request icon. - PlayerIcons: -- use cloneNode where possible -- added check for alliances disabled: then skip alliance (request) icon checks -- See point under NameLayer about the move of Alliance icon code to PlayerIcons. To make NameLayer even more agnostic on it and keep it in one place. -- getPlayerIcons: skip creating a new Set from myPlayer.transitiveTargets() each time getPlayerIcons is called. One allocation less. Just do .includes on the returned array. Probably just as fast in this case, also because not many Targets are present many times anyway. -- getPlayerIcons: on outgoingEmojis(), use .find() instead of .filter() since we only use the first result anyway and it saves us another allocation. -- getPlayerIcons: for nukes, only fetch the ones from the player we're rendering for, not all game nukes. Also don't use .filter() and just a normal loop to skip an allocation. Logic outcome is the same. -- getPlayerIcons: for target icons, it used to fetch myPlayer.transitiveTargets each time. While that doesn't change per player we're rendering for. So now, NameLayer fetches myPlayer.transitiveTargets once per call to renderPlayerInfo, which passes it on to getPlayerIcons. -- Remove the need for querySelector and getElementsByTagName("img") alltogether. Since this would be done for every time an alliance was (re-)created, here in createAllianceProgressIconRefs in PlayerIcons it makes more sense to not do DOM traversal than in createPlayerElement in NameLayer where we only do it once per player per game anyway. We assume updateAllianceProgressIconRefs just knows of all image names in createAllianceProgressIconRefs. This is a bit less dynamic and maintainable maybe, but i think worth the win. And the functions are all one-purpose and not meant to be used dynamically by another caller anyway. -- So instead of updateAllianceProgressIconRefs looping through refs.images, now just update the different images each. See point above. - PlayerInfoOverlay: also re-use the new exported consts from PlayerIcons. Since we put those in PlayerIcons anyway, need to be consistent. Even though PlayerInfoOverlay is outside of the scope of this PR otherwise. -- for getPlayerIcons, add this.alliancesDisabled here too. If disabled, getPlayerIcons won't fetch Alliance icon and Alliance Request icon. We also send includeAllianceIcon = false, which means Alliance icon will already be excluded but Alliance Request icon is normally still fetched and shown. - GameView > PlayerView: for transitiveTargets (only used in NameLayer/PlayerIcons so only in this context), improve performance. It did several allocations. Now it loops directly over the arrays we need. Also (as mentioned under Fixes) previously transitiveTargets returned: your own targets and allies' targets. Now transitiveTargets is faster and returns: your own targets and allies' targets and team members' targets. **BEFORE**  **AFTER** (including now getting team members' targets for myPlayer.transitiveTargets)  ## 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 |
||
|
|
494b409f55 |
Cap StructureLayer Maximum Texture Size (#3574)
Resolves #3569 ## Description: While browsers like Firefox will report their maximum texture size of 16384, going over 8192 causes extreme VRAM usage and massive FPS drops, even when no structures are actually being rendered (I experienced ~60ms rendering time on this layer with no structures present). This PR sets the StructureLayer texture size to cap at 8192, while keeping near-exact scales. The result is increased performance, reduced VRAM Usage, (especially in larger maps), and the resolution of the unplayable performance issues when StructureLayer is present, with zero noticeable degradation. VRAM Usage also no longer rises when zoomed in (Sitting at around a constant 40MB no matter zoom level, previously it would rise to over 160MB when StructureLayer was present). All tested on Giant World, where the issues were first spotted, but applies to all maps. Discord: @enderboy9217 |
||
|
|
21c286189e |
Perf/fix(NukeExecution/NukeTrajectoryPreviewLayer): unnecessary fetch and pass to listNukeBreakAlliance (#3546)
## Description: Perf/fix: listNukeBreakAlliance doesn't ask for allySmallIds and doesn't do anything with it. But both NukeExecution and NukeTrajectoryPreviewLayer do fetch and pass allySmallIds to it. Make allySmallIds optional, have wouldNukeBreakAlliance (which does use allySmallIds) handle it potentially being undefined, and remove fetch and pass of allySmallIds to listNukeBreakAlliance. ## 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 |
||
|
|
b88c3a3bdd | Merge branch 'v30' | ||
|
|
54e29c5b4e |
Revert "Add new bouncing trajectory for april fools (#3534)"
This reverts commit
|
||
|
|
c92895620f |
Add new bouncing trajectory for april fools (#3534)
## Description: For April Fools day, the nukes can randomly bounce (1/10 chance). Changes: - New BouncingParabola trajectory - Generic "Text Event" to display a `Boing!` where the bounce happens - The complete trajectory, with or without bounce, is used by the SAM interceptor so there should be no defense gap.  ## 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: IngloriousTom |
||
|
|
e7b4317718 |
fix: radial build sub-menu items stay grayed out after gaining enough gold (#3415)
## Description: canBuildOrUpgrade was captured once at sub-menu open time, so disabled/color never updated while the menu was open. Evaluate canBuildOrUpgrade dynamically inside the disabled and color callbacks so the menu reflects current gold on each refresh tick. ## 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 |
||
|
|
130315cba1 | Merge branch 'v30' | ||
|
|
73016bb56b |
Add bottom left ad in crazygames (#3526)
## Description: If on crazy games, shows an in-game ad on the bottom left corner ## 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 |
||
|
|
7fdda33fb9 | Merge branch 'v30' | ||
|
|
39ad547c04 |
support for unlockable flags (#3479)
## Description: Add support for purchasable/gated flags. * Create a new "Store" modal that renders both skins & flags * move all store related logic out of TerritoryPatternsModal * use nation:code for existing nation flags & flag:key for gated flags * check if user has the appropriate flags before purchasing ## 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 |
||
|
|
496f1008bb |
Fix: icons structure icons and others at wrong location (#3453)
## Description: Fix for all structure icons suddenly being displaced from the actual structure location. And in some cases, structure itself created at wrong location, or coordinate grid, nuke trajectory preview target, nuke circles, naval landing target, or pop-up text of gold earned from train or tradeship. - Was triggered on iOS almost exclusively, but was also possible on other devices when a top/left margin was present. Like when an ad was shown. Why noticed almost only on iOS? Because of different behaviors where it shifts the DOM elements around relative to the screen temporarily, so we get a top/left offset on getBoundingClientRect for the canvas. Possible culprit is overscroll which lets you scroll outside of the viewport for several hundred pixels before snapping back. Which was triggered by mistake by dragging instead of tapping somewhere, or so. - Some of the bug reports: https://discord.com/channels/1284581928254701718/1451393982159523982, https://discord.com/channels/1284581928254701718/1463548362526822649, https://discord.com/channels/1284581928254701718/1378672255336189964, fixes https://github.com/openfrontio/OpenFrontIO/issues/3406 - The fix brings a little performance win as well because we need to be doing less calculations. It is basically "if drawing on a canvas, work with canvas coordinates and not with screen coordinates". Was stress tested by two players and me, see below for reproduction. - (BTW. When researching if the current logic was intended in any way, I found CodeRabbit had already noticed part of the bug twice. One of them was [resolved](https://github.com/openfrontio/OpenFrontIO/pull/2059#discussion_r2396277710), the other [left open](https://github.com/openfrontio/OpenFrontIO/pull/2059#discussion_r2370413213). Another reminder that we need to heed Rabbits calls!) **CONTAINS** - StructureIconsLayer > computeNewLocation and StructureDrawingUtils > createUnitContainer. In renderLayer, when TransformHandler.hasChanged (after onZoom, goTo, onMove), computeNewLocation recalculates the position of all structure icons. Sometimes all icons would suddenly be displaced, not above their map position but somewhere else. New single icons/levels/sprites would be placed wrongly too by computeNewLocation and createUnitContainer. -- Fix: don't use TransformHandler > worldToScreenCoordinates but the new worldToCanvasCoordinates. Because worldToScreenCoordinates adds the canvas boundingRect top/left offset. When the main canvas is already shifted down with a margin, the icons also get shifted within the canvas. So they're moved twice as much as they should be. We only need to know at what canvas location to put the icons, the main GameRender canvas already handles the overall displacement. - TransformHandler > worldToCanvasCoordinates -- Use the new worldToCanvasCoordinates too instead of worldToScreenCoordinates in CoordinateGridLayer, MoveIncicatorUI, NavalTarget, NukeTeleGraph, TextIndicator. They were affected by the same bug as the shifting Structure icons because the boundingRect top/left offset was applied twice, but it was noticed less. I have seen reports of NavalTarget or MobveIndicatorUI (for warships) not being in the correct spot though iirc. And just like for StructureIconsLayer/StructureDrawingUtils, it's only logical. Since we're drawing on the canvas, we only need to know where to place things within that canvas. - TransformHandler > worldToScreenCoordinates -- Split in two sub-functions. New seperate function worldToCanvasCoordinates was needed for the above fix. For canvasToScreenCoordinates the reason is explained below. - TransformHandler > screenToWorldCoordinates: this function already subtracts the canvas boundingRect top/left offset. Some callers were themselves getting the boundaryRect and subtracting top/left, before calling screenToWorldCoordinates. Not only unnecessary. But also, when there was more than 0 top/left offset, it would be subtracted twice from the mouse/tap position. Meaning a (ghost) structure or nuke trajectory preview target would not be put where the mouse/tap was. Same bug as above but reversed. -- Checked all callers. Most did it right. Fixes where needed in StructureIconsLayer > createStructure and renderGhost, and in NukeTrajectoryPreviewLayer > updateTrajectoryPreview and updateTrajectoryPath. -- Removed comment in screenToWorldCoordinates that talked about zoom. It doesn't belong there because we do more than zooming there, it was probably copied once from onZoom() which has the exact same comment and it belongs in that function. -- Small fix in caller BuildMenu when checking all callers of screenToWorldCoordinates: it checked if clickedCell was null, but screenToWorldCoordinates never returns null. - TransformHandler > added public helper functions screenToCanvasCoordinates and canvasToScreenCoordinates to make code re-usable -- screenToCanvasCoordinates is used in screenToWorldCoordinates and onZoom in TransformHandler itself -- screenToCanvasCoordinates is now also used also in moveGhost and createGhostStructure in StructureIconsLayer. No bugs there, but the same calculation was done (getting boundingRect, subtracting left/top from mouse/tap position). So they now use the centralized function which also adds to their readability. -- canvasToScreenCoordinates is (for now) only used in worldToScreenCoordinates in TransformHandler. It makes the function more readable imo. And, since it has such a similar calculation to screenToWorldCoordinates, it only seemed neat to have them both have their own function. **BEFORE** (only showing "all structure icons get displaced", but the cause and fix is basically the same for all) https://youtu.be/CfDdUwIRQeE **AFTER** https://youtu.be/w5w_wvh5V0g ## 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 |
||
|
|
eb51853b05 |
Perf/Fix: spawn and other functions that need closest by unit (#3243)
## Description: Performance improvements. - **PlayerImpl**: for _nukeSpawn_, cache config to const. - **Other files**: for nukeSpawn and other functions doing the same, introduce findClosestBy function. - for **TradeShipExecution**, with the move from _distSortUnit_ to _findClosestBy_, also add check if port isActive, !_isMarkedForDeletion_ and !_isUnderConstruction_. These checks should have been there already, so now do it in one go to make use of the predicate isCandidate in findClosestBy. - for **TradeShipExectution.test.ts**, add mock functions for _isMarkedForDeletion_ and _isUnderConstruction_ because of the above. Also, set Unit tiles and Pathfinding node to actual valid TileRefs for the testing map. This prevents NaN as return value from manhattanDist. This problem was already present with the use of distSortUnit, but that function just did NaN - NaN, returned the first and only port unit in the array and called it a day. For findClosestBy we have to make sure the predicate manhattanDist actually returns a number instead of NaN so we need actually valid tiles. We now have a working test instead of a test that actually silently failed like before. - **PlayerImpl**: _warshipSpawn_ and _nukeSpawn_: Make use of the isCandidate predicate of findClosestBy to have warshipSpawn not return ports under construction or (smaller change) inactive. This fixes a bug i have seen right away (where Warship spawns from under construction Port). Same for _nukeSpawn_ silos, don't return inactive silo just to be sure now that we can easily add it to isCandidate predicate anyway. This costs no performance in the _nukeSpawn_ benchmarks actually. This should as a by-effecft fix an edge case bug i have seen, where a nuke is sent from a phantom silo. Some of this goes along with PR #3220 since playerImpl buildableUnits makes use of the underlying spawn functions via canBuild. Just like ConstructionExecution does. But i didn't want to add more to PR 3220 since there's already a lot in there. The new function _findClosestBy_ could also be applied to some other parts of code to benefit of it being faster, so i did that. _findClosestBy_ uses _findMinimumBy_, which is a little more generic in name. I think _findMinimumBy_ could be used by other parts of code, while _findClosestBy_ is more clear naming for what it does now. But we could ditch _findMinimumBy_ and just leave findClosestBy? Examples of synthetic benchmarks (not included in this PR): **BEFORE CHANGES (before Scamiv's PR #3241)** <img width="705" height="91" alt="image" src="https://github.com/user-attachments/assets/d6d91c08-39f1-4387-9ccc-e51951caa539" /> <img width="751" height="101" alt="image" src="https://github.com/user-attachments/assets/80d400ac-3408-4107-aa58-6d2a847311e9" /> **AFTER CHANGES (before Scamiv's PR #3241)**   **BEFORE CHANGES (after Scamiv's PR #3241)**   **AFTER CHANGES (after Scamiv's PR #3241)** <img width="717" height="96" alt="image" src="https://github.com/user-attachments/assets/5b106843-bf6e-4448-a8e8-94448fb30ced" /> <img width="767" height="92" alt="image" src="https://github.com/user-attachments/assets/e6714c7b-26c1-455b-adae-f0060f1cbc7b" /> _Also see more **BEFORE** and **AFTER** in this comment:_ https://github.com/openfrontio/OpenFrontIO/pull/3243#issuecomment-3949060395 _And here a comparison in the flame charts:_ - based on the same replay and tried to get the performance recording going at the same speed and length but always end up with small differences - because of a bug in replays currently, it puts you in with the same clientID/persistantID currently. This means we can also record part of what is normally only recordable with live human input (the playerActions/playerBuildables). **BEFORE** flame chart with nukeSpawn (human player) and maybeSendNuke (Nation players, uses nukeSpawn via canBuild):    **AFTER** flame chart with nukeSpawn (human player) and maybeSendNuke (Nation players, uses nukeSpawn via canBuild):      ## 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 |