-
Notifications
You must be signed in to change notification settings - Fork 472
merge v25 into main #1880
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
merge v25 into main #1880
Conversation
## Description: This PR implements a new feature allowing automatic upgrade of the nearest building using the middle mouse button. This feature greatly simplifies the upgrade process that previously required a right-click + building recreation. ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: Kipstzz --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
## Description: Checks that each lang file’s lang_code matches its filename and that the flag SVG exists. Reports all errors in a single test run for easier debugging. ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri Co-authored-by: Drills Kibo <59177241+drillskibo@users.noreply.github.com>
## Description: Added a red delete button with trash can icon to the right-click radial menu that allows players to voluntarily delete their own units. ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: Kipstz <img width="286" height="209" alt="image" src="https://github.com/user-attachments/assets/85142be3-2aa5-4c84-ab30-0c68289c8f85" /> --------- Co-authored-by: Drills Kibo <59177241+drillskibo@users.noreply.github.com>
## Description: Strict mode is causing docker deployments to fail. This commit reverts only the strict mode setting itself, while keeping the code changes, so that we can re-enable it more easily in the future. ## 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 - [ ] I have read and accepted the CLA agreement (only required once).
Fixes #1559 ## Description: The current Mars map has several issues: - The map is very underdetailed - The coastlines are very smooth and visually unappealing - The map was created from an incompatible source image, which results in the height data being very wrong - As a result the map hardly resembles Mars at all. - There is too much land - The major landmass is almost entirely covered in plains - The ocean doesn't match the heightmap - The gameplay is boring - The map is very succeptable to snowballing issues - The map seems commonly disliked in the community I attempted to remedy this. - The water level and elevation are calculated from accurate height data - Water level is much higher making the map much more interesting, both in the gameplay and visual aspect. - The map has several contitents which should slow down snowballing - The map is much more detailed overall - I added a lot more NPC's. 8 for such a big map just wasn't enough. <img width="2500" height="1150" alt="Mars_Out2" src="https://github.com/user-attachments/assets/7b7009f7-6376-4059-b731-86931df8f926" /> - [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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: aceralex --------- Co-authored-by: Drills Kibo <59177241+drillskibo@users.noreply.github.com>
## Description: Switches away from ts-jest in favor of @swc/jest. On my local I observe a ten-fold decrease in how long it takes the test suite to run. No changes are required to how our existing tests are written. Benchmarking old: 24.658s new: 2.268s ts-jest (old) ``` Test Suites: 29 passed, 29 total Tests: 215 passed, 215 total Snapshots: 0 total Time: 24.658 s Ran all test suites. ``` swc-jest (new) ``` Test Suites: 29 passed, 29 total Tests: 215 passed, 215 total Snapshots: 0 total Time: 2.268 s Ran all test suites. ``` Fixes #1679 ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: slyty
## Description: This pull request adds support for ICU (Intl MessageFormat) syntax in the translation system. Existing translation files may need to be updated to fully leverage ICU features. ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME
## Description: Add frontend-approver and backend-approver to CODEOWNERS file ## 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 - [ ] I have read and accepted the CLA agreement (only required once).
Bumps the npm_and_yarn group with 2 updates in the / directory: [on-headers](https://github.com/jshttp/on-headers) and [compression](https://github.com/expressjs/compression). Updates `on-headers` from 1.0.2 to 1.1.0 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/jshttp/on-headers/releases">on-headers's releases</a>.</em></p> <blockquote> <h2>1.1.0</h2> <h2>Important</h2> <ul> <li>Fix <a href="https://www.cve.org/CVERecord?id=CVE-2025-7339">CVE-2025-7339</a> (<a href="https://github.com/jshttp/on-headers/security/advisories/GHSA-76c9-3jph-rj3q">GHSA-76c9-3jph-rj3q</a>)</li> </ul> <h2>What's Changed</h2> <ul> <li>Migrate CI pipeline to GitHub actions by <a href="https://github.com/carpasse"><code>@carpasse</code></a> in <a href="https://redirect.github.com/jshttp/on-headers/pull/12">jshttp/on-headers#12</a></li> <li>fix README.md badges by <a href="https://github.com/carpasse"><code>@carpasse</code></a> in <a href="https://redirect.github.com/jshttp/on-headers/pull/13">jshttp/on-headers#13</a></li> <li>add OSSF scorecard action by <a href="https://github.com/carpasse"><code>@carpasse</code></a> in <a href="https://redirect.github.com/jshttp/on-headers/pull/14">jshttp/on-headers#14</a></li> <li>fix: use <code>ubuntu-latest</code> as ci runner by <a href="https://github.com/UlisesGascon"><code>@UlisesGascon</code></a> in <a href="https://redirect.github.com/jshttp/on-headers/pull/19">jshttp/on-headers#19</a></li> <li>ci: apply OSSF Scorecard security best practices by <a href="https://github.com/UlisesGascon"><code>@UlisesGascon</code></a> in <a href="https://redirect.github.com/jshttp/on-headers/pull/20">jshttp/on-headers#20</a></li> <li>👷 add upstream change detection by <a href="https://github.com/ctcpip"><code>@ctcpip</code></a> in <a href="https://redirect.github.com/jshttp/on-headers/pull/31">jshttp/on-headers#31</a></li> <li>✨ add script to update known hashes by <a href="https://github.com/ctcpip"><code>@ctcpip</code></a> in <a href="https://redirect.github.com/jshttp/on-headers/pull/32">jshttp/on-headers#32</a></li> <li>💚 update CI - add newer node versions by <a href="https://github.com/ctcpip"><code>@ctcpip</code></a> in <a href="https://redirect.github.com/jshttp/on-headers/pull/33">jshttp/on-headers#33</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/carpasse"><code>@carpasse</code></a> made their first contribution in <a href="https://redirect.github.com/jshttp/on-headers/pull/12">jshttp/on-headers#12</a></li> <li><a href="https://github.com/UlisesGascon"><code>@UlisesGascon</code></a> made their first contribution in <a href="https://redirect.github.com/jshttp/on-headers/pull/19">jshttp/on-headers#19</a></li> <li><a href="https://github.com/ctcpip"><code>@ctcpip</code></a> made their first contribution in <a href="https://redirect.github.com/jshttp/on-headers/pull/31">jshttp/on-headers#31</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0">https://github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0</a></p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/jshttp/on-headers/blob/master/HISTORY.md">on-headers's changelog</a>.</em></p> <blockquote> <h1>1.1.0 / 2025-07-17</h1> <ul> <li>Fix <a href="https://www.cve.org/CVERecord?id=CVE-2025-7339">CVE-2025-7339</a> (<a href="https://github.com/jshttp/on-headers/security/advisories/GHSA-76c9-3jph-rj3q">GHSA-76c9-3jph-rj3q</a>)</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/jshttp/on-headers/commit/4b017af88f5375bbdf3ad2ee732d2c122e4f52b0"><code>4b017af</code></a> 1.1.0</li> <li><a href="https://github.com/jshttp/on-headers/commit/b636f2d08e6c1e0a784b53a13cd61e05c09bb118"><code>b636f2d</code></a> ♻️ refactor header array code</li> <li><a href="https://github.com/jshttp/on-headers/commit/3e2c2d46c3e9592f6a1c3a3a1dbe622401f95d39"><code>3e2c2d4</code></a> ✨ ignore falsy header keys, matching node behavior</li> <li><a href="https://github.com/jshttp/on-headers/commit/172eb41b99a5a290b27a2c43fe602ca33aa1c8ce"><code>172eb41</code></a> ✨ support duplicate headers</li> <li><a href="https://github.com/jshttp/on-headers/commit/c6e384908c9c6127d18831d16ab0bd96e1231867"><code>c6e3849</code></a> 🔒️ fix array handling</li> <li><a href="https://github.com/jshttp/on-headers/commit/6893518341bb4e5363285df086b3158302d3b216"><code>6893518</code></a> 💚 update CI - add newer node versions</li> <li><a href="https://github.com/jshttp/on-headers/commit/56a345d82b51a0dcb8d09f061f87b1fd1dc4c01e"><code>56a345d</code></a> ✨ add script to update known hashes</li> <li><a href="https://github.com/jshttp/on-headers/commit/175ab217155d525371a5416ff059f895a3a532a6"><code>175ab21</code></a> 👷 add upstream change detection (<a href="https://redirect.github.com/jshttp/on-headers/issues/31">#31</a>)</li> <li><a href="https://github.com/jshttp/on-headers/commit/ce0b2c8fcd313d38d3534fb731050dc16e105bf6"><code>ce0b2c8</code></a> ci: apply OSSF Scorecard security best practices (<a href="https://redirect.github.com/jshttp/on-headers/issues/20">#20</a>)</li> <li><a href="https://github.com/jshttp/on-headers/commit/1a38c543e75cd06217b449531de10b1758e35299"><code>1a38c54</code></a> fix: use <code>ubuntu-latest</code> as ci runner (<a href="https://redirect.github.com/jshttp/on-headers/issues/19">#19</a>)</li> <li>Additional commits viewable in <a href="https://github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0">compare view</a></li> </ul> </details> <details> <summary>Maintainer changes</summary> <p>This version was pushed to npm by <a href="https://www.npmjs.com/~ulisesgascon">ulisesgascon</a>, a new releaser for on-headers since your current version.</p> </details> <br /> Updates `compression` from 1.8.0 to 1.8.1 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/expressjs/compression/releases">compression's releases</a>.</em></p> <blockquote> <h2>v1.8.1</h2> <h2>What's Changed</h2> <ul> <li>fix(docs): update multiple links from http to https by <a href="https://github.com/Phillip9587"><code>@Phillip9587</code></a> in <a href="https://redirect.github.com/expressjs/compression/pull/222">expressjs/compression#222</a></li> <li>ci: add dependabot for github actions by <a href="https://github.com/bjohansebas"><code>@bjohansebas</code></a> in <a href="https://redirect.github.com/expressjs/compression/pull/207">expressjs/compression#207</a></li> <li>build(deps): bump github/codeql-action from 2.23.2 to 3.28.15 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/expressjs/compression/pull/228">expressjs/compression#228</a></li> <li>build(deps): bump ossf/scorecard-action from 2.3.1 to 2.4.1 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/expressjs/compression/pull/229">expressjs/compression#229</a></li> <li>build(deps-dev): bump eslint-plugin-import from 2.26.0 to 2.31.0 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/expressjs/compression/pull/230">expressjs/compression#230</a></li> <li>build(deps-dev): bump supertest from 6.2.3 to 6.3.4 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/expressjs/compression/pull/231">expressjs/compression#231</a></li> <li>[StepSecurity] ci: Harden GitHub Actions by <a href="https://github.com/step-security-bot"><code>@step-security-bot</code></a> in <a href="https://redirect.github.com/expressjs/compression/pull/235">expressjs/compression#235</a></li> <li>build(deps): bump github/codeql-action from 3.28.15 to 3.29.2 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/expressjs/compression/pull/243">expressjs/compression#243</a></li> <li>build(deps): bump actions/upload-artifact from 4.3.1 to 4.6.2 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/expressjs/compression/pull/239">expressjs/compression#239</a></li> <li>build(deps): bump ossf/scorecard-action from 2.4.1 to 2.4.2 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/expressjs/compression/pull/240">expressjs/compression#240</a></li> <li>build(deps): bump actions/checkout from 4.1.1 to 4.2.2 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/expressjs/compression/pull/241">expressjs/compression#241</a></li> <li>build(deps-dev): bump eslint-plugin-import from 2.31.0 to 2.32.0 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/expressjs/compression/pull/244">expressjs/compression#244</a></li> <li>deps: on-headers@1.1.0 by <a href="https://github.com/UlisesGascon"><code>@UlisesGascon</code></a> in <a href="https://redirect.github.com/expressjs/compression/pull/246">expressjs/compression#246</a></li> <li>Release: 1.8.1 by <a href="https://github.com/UlisesGascon"><code>@UlisesGascon</code></a> in <a href="https://redirect.github.com/expressjs/compression/pull/247">expressjs/compression#247</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] made their first contribution in <a href="https://redirect.github.com/expressjs/compression/pull/228">expressjs/compression#228</a></li> <li><a href="https://github.com/step-security-bot"><code>@step-security-bot</code></a> made their first contribution in <a href="https://redirect.github.com/expressjs/compression/pull/235">expressjs/compression#235</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/expressjs/compression/compare/1.8.0...v1.8.1">https://github.com/expressjs/compression/compare/1.8.0...v1.8.1</a></p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/expressjs/compression/blob/master/HISTORY.md">compression's changelog</a>.</em></p> <blockquote> <h1>1.8.1 / 2025-07-17</h1> <ul> <li>deps: on-headers@~1.1.0 <ul> <li>Fix <a href="https://www.cve.org/CVERecord?id=CVE-2025-7339">CVE-2025-7339</a> (<a href="https://github.com/expressjs/on-headers/security/advisories/GHSA-76c9-3jph-rj3q">GHSA-76c9-3jph-rj3q</a>)</li> </ul> </li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/expressjs/compression/commit/83a0c45fe190f4fcb8b515c18065db9cb9029dd1"><code>83a0c45</code></a> 1.8.1</li> <li><a href="https://github.com/expressjs/compression/commit/ce62713129f4b33eac4b833e1722410091646395"><code>ce62713</code></a> deps: on-headers@1.1.0 (<a href="https://redirect.github.com/expressjs/compression/issues/246">#246</a>)</li> <li><a href="https://github.com/expressjs/compression/commit/f4acb23985fa345318d34d4a96acf555a883efeb"><code>f4acb23</code></a> build(deps-dev): bump eslint-plugin-import from 2.31.0 to 2.32.0 (<a href="https://redirect.github.com/expressjs/compression/issues/244">#244</a>)</li> <li><a href="https://github.com/expressjs/compression/commit/6eaebe63f2ecac191d402c570bde140488435c4c"><code>6eaebe6</code></a> build(deps): bump actions/checkout from 4.1.1 to 4.2.2 (<a href="https://redirect.github.com/expressjs/compression/issues/241">#241</a>)</li> <li><a href="https://github.com/expressjs/compression/commit/37e062312fd270f84b5f50f7c6f88312609633f5"><code>37e0623</code></a> build(deps): bump ossf/scorecard-action from 2.4.1 to 2.4.2 (<a href="https://redirect.github.com/expressjs/compression/issues/240">#240</a>)</li> <li><a href="https://github.com/expressjs/compression/commit/bc436b26283c2f85a9711085dd0e4a580de50ba7"><code>bc436b2</code></a> build(deps): bump actions/upload-artifact from 4.3.1 to 4.6.2 (<a href="https://redirect.github.com/expressjs/compression/issues/239">#239</a>)</li> <li><a href="https://github.com/expressjs/compression/commit/2f9f5726751ecf12f7c46a9d1493bcd1966e09a7"><code>2f9f572</code></a> build(deps): bump github/codeql-action from 3.28.15 to 3.29.2 (<a href="https://redirect.github.com/expressjs/compression/issues/243">#243</a>)</li> <li><a href="https://github.com/expressjs/compression/commit/5f13b148d2a1a2daaa8647e03592214bb240bf18"><code>5f13b14</code></a> [StepSecurity] ci: Harden GitHub Actions (<a href="https://redirect.github.com/expressjs/compression/issues/235">#235</a>)</li> <li><a href="https://github.com/expressjs/compression/commit/76e094548125afbf8089a482d5982dc96c7ce398"><code>76e0945</code></a> build(deps-dev): bump supertest from 6.2.3 to 6.3.4 (<a href="https://redirect.github.com/expressjs/compression/issues/231">#231</a>)</li> <li><a href="https://github.com/expressjs/compression/commit/ae6ee809dc0cb40febaf2a5bff298465bd5a207f"><code>ae6ee80</code></a> build(deps-dev): bump eslint-plugin-import from 2.26.0 to 2.31.0 (<a href="https://redirect.github.com/expressjs/compression/issues/230">#230</a>)</li> <li>Additional commits viewable in <a href="https://github.com/expressjs/compression/compare/1.8.0...v1.8.1">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/openfrontio/OpenFrontIO/network/alerts). </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Description: Added 2-fingers control for map with trackpad ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: pierre_brtr
## Description: Enable the `sort-keys` eslint rule. Fixes #1629 ## 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 - [ ] I have read and accepted the CLA agreement (only required once).
## Description: Problem: attacking a player right before accepting an alliance request is very effective since the requester can't fight back or reclaim his territory without canceling the alliance and being penalized with the traitor debuff. Change: - Attacking a player after he requested an alliance automatically rejects the request - No changes to existing attacks in both directions, only new attacks affect the request ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom
## Description: The websocket message handler functions have gotten quite large. This change extracts them to functions in their own file, and extracts the `"join"` message acceptance logic into its own function allowing for all cases to be accounted for when we add error messages in #1447. ## 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 - [ ] I have read and accepted the CLA agreement (only required once).
## Description: Enable `@total-typescript/ts-reset` Fixes #1760 ## 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 - [ ] I have read and accepted the CLA agreement (only required once).
## Description: Update the code for strict 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 - [ ] I have read and accepted the CLA agreement (only required once).
## Description: Enable eslint warning for `@typescript-eslint/consistent-type-assertions`. While build warnings are generally a discouraged pattern, we are actively working to drive the number of warnings down to zero, at which point we will convert the warnings to errors. ## 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 - [ ] I have read and accepted the CLA agreement (only required once).
## Description: Since the in-game Settings became their own modal in v24, it blocks game control but the game goes on in the background. On mobile it is full screen so you don't even notice the game still playing in the background. Of course players understand that a Multiplayer game will move on. But for Single Player, the game mode played by beginners too, you'd expect the game to be paused and are surprised when it isn't. Same goes for Replays. This PR fixes it: - Pause when opening the in-game Settings modal during replay or single player game. - Unpause again when closing the Settings modal (if not already paused before opening the Settings). - The icon for pause/unpause isn't switched in GameRightSideBar during this, as the sidebar is blurred in the background anyway. ## 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 - [ ] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 --------- Co-authored-by: Drills Kibo <59177241+drillskibo@users.noreply.github.com>
## Description: Re-enable strict 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
## Description: Remove CLA from the PR description template and PR check. This is handled through a dedicated status check, so the description is not needed. ## 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
## Description: Describe the PR. This PR makes the playerInfoOverlay collapsible by clicking on the player name. This overlay was covering a decent chunk of the mobile screen.  Now the details can be toggled by just clicking on the name freeing a lot of the screen on mobile.  ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME tls_15 Co-authored-by: Drills Kibo <59177241+drillskibo@users.noreply.github.com>
## Description: Resolve #1652 1. Add the ability to toggle **gold donations** and **troop donations** for private lobbies ~2. Add relevant translations.~ 3. Refactor `canDonate` to be specific to gold and troop donations 4. Add placeholders for singleplayer mode if this is to be extended to support that too. 5. Add Tests for Donate logic ### Screenshots: <img width="1643" height="1788" alt="image" src="https://github.com/user-attachments/assets/82b93400-a1f0-45f0-8b2b-a7f78dc0c3e9" /> _Private Lobby_ ### Smoke Tests  _Testing Troop Send In Private Lobby_  _Troop Send Complete In Private Lobby_  Confirming that public teams still works ## 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 - [X] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME: cool_clarky --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> Co-authored-by: Drills Kibo <59177241+drillskibo@users.noreply.github.com>
## Description: Enable various eslint rules. ## 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
## Description: Enable eslint rule `@stylistic/ts/indent`. Fixes #1778 ## 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
## Description: Enable more stylistic rules. ## 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
## Description: Enable eslint rule `quote-props` ## 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
## Description: Enable enforcement of various eslint style rules. ## 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
## Description: Have at least 2 clients and majority vote to decide a winner ## 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 --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
## Description: Restore code that was accidentally deleted in the previous PR. ## 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
## Description: Prefer `type.array()` over `z.array(type)`. ## 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
The previous implementation had a bug that biased numbers away from 0, so random.chance(1500+) would always return false. This caused trains to not spawn at all when their spawn rate was sufficiently low. We should be using a library instead of implementing it from scratch anyways. - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced regression is found: evan
## Description: After a SAM was captured, SAMTargetingSystem's player was not updated, so it thought it was still owned by the original player. So it wouldn't shoot down nukes launched from the original owner. ## 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
## Description: I noticed after MIRVs, player names would be very small, or even not shown, because the MIRV would litter the player territory with radiation. This PR allows the name to go over irradiated land. It occasionally led to strange name placement, like if a small nation was h-bombed, then half the name could end up on the radiation, but it was still clear who the nation was. Overall it feels like an improvement. ## 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
## Description: seedrandom provides much better randomization (77fd82b) causing MIRV warheads to cover the target much more efficiently, and wiping out much more territory than in v24. This change makes the MIRV more similar to v24. ## 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
## Description: Fix the failing eslint check after cherry-picking v25 changes into main. ## 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
## Description: Remove the structure building limit for AI players, in favor of a simple cost heuristic. Fixes #1561 ## 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
## Description: The alliance icon was appearing squished in the radial menu because the SVG was being forced into a 32x32 square without proper aspect ratio preservation. Change: Added `viewBox="0 0 834 528"` and `preserveAspectRatio="xMidYMid meet"` to the SVG element, which maintains the original aspect ratio (1.58:1) when scaled to fit the allocated space. After changes: [ <img width="166" height="184" alt="Screenshot 2025-08-07 at 4 52 15 PM" src="https://github.com/user-attachments/assets/c1438b4d-9302-4a2b-8e25-c6d289e7148e" /> ](url) Refer to the issue for before. Fixes #1730 ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: phantom845 Co-authored-by: Kanishk-T <kanishk.tyagi@yugen.ai>
## Description: Smarter nation structure placement. Fixes #881 <img width="2464" height="1235" alt="image" src="https://github.com/user-attachments/assets/b8ec0041-6f12-4ff3-9279-f5e8529521e5" /> ## 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
## Description: Enable the `no-multiple-empty-lines` eslint rule. ## 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
## Description: Adds more nations to the default World Map and updates some `strength` values to work better. fixes #609 <details> <summary>Screenshots</summary> <img width="1440" height="798" alt="Screen Shot 2025-08-11 at 6 13 04 AM" src="https://github.com/user-attachments/assets/7c9fd017-862c-41d3-83e9-1de511edaf2d" /> <img width="1436" height="801" alt="Screen Shot 2025-08-11 at 6 14 29 AM" src="https://github.com/user-attachments/assets/801f7c2d-299c-4a53-b2c1-5a342623d024" /> <img width="1440" height="798" alt="Screen Shot 2025-08-11 at 6 15 30 AM" src="https://github.com/user-attachments/assets/e43c6f71-185d-4dd4-bf38-4d76264c3c32" /> <img width="1440" height="800" alt="Screen Shot 2025-08-11 at 6 16 26 AM" src="https://github.com/user-attachments/assets/30fc2ee2-5e44-4007-ac81-83a3aeaefe37" /> <img width="1440" height="801" alt="Screen Shot 2025-08-11 at 6 16 48 AM" src="https://github.com/user-attachments/assets/e624477b-3310-40e4-92cf-c9aea3b81e59" /> </details> ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: loymdayddaud
## Description: Enable the `object-shorthand` eslint rule. ## 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
## Description: Enable the `@typescript-eslint/prefer-readonly` eslint rule. ## 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
This reverts commit ec21a0f.
## Description: Enable the `prefer-destructuring` eslint rule. ## 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
## Description: Bots on the same team as a human player will no longer embargo them if they betray an alliance with an opposing team player. Fixes #1845 ## 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: sibyljudith
WalkthroughRefactors pattern handling to use patternName and new cosmetics schemas; updates client join/spawn payloads, privilege checks, and UI modals. Adds alliance request duration/status with revised execution and event cleanup. Overhauls trade/train spawn and rewards with new config APIs and sigmoid utility. Removes a map, tweaks tests, and updates lint rule. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Client as Client (Main/Transport)
participant Server as WS PreJoinHandler
participant Priv as PrivilegeChecker
participant Store as Cosmetics (by name)
User->>Client: Click Join Lobby
Client->>Client: Read userSettings.getSelectedPatternName()
Client->>Server: ClientJoin{ patternName, ... }
Server->>Priv: isPatternAllowed(name, flares)
alt allowed
Priv-->>Server: {type:"allowed", pattern}
Server->>Store: Resolve pattern by name
Store-->>Server: PatternInfo
Server-->>Client: Join OK
else forbidden
Priv-->>Server: {type:"forbidden", reason}
Server-->>Client: Close 1002 (reason)
else unknown
Priv-->>Server: {type:"unknown"}
Server-->>Client: Close 1002 (backend lookup)
end
sequenceDiagram
autonumber
participant P1 as Player A
participant P2 as Player B
participant Exec as AllianceRequestExecution
participant Game as Game (config/time)
participant UI as EventsDisplay
P1->>Exec: Add execution (request A→B)
Exec->>Game: init(mg)
alt B already requested A
Exec->>Game: accept(existing)
Exec-->>P1: deactivate
else can send
Exec->>Game: create request(A→B)
else cannot send
Exec-->>P1: deactivate
end
loop each tick
Exec->>Game: check status/time
alt status accepted/rejected
Exec-->>P1: deactivate
else expired (ticks > allianceRequestDuration)
Exec->>Game: reject(request)
Exec-->>P1: deactivate
end
end
UI->>UI: Filter events
UI->>Game: event.shouldDelete?(game)
note right of UI: Deletes when players become allied
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
cd5d75f
to
12a1fc5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
src/client/ClientGameRunner.ts (1)
50-62
: Migrate allpattern
fields in join payloads and handlers topatternName
Several places still use
pattern:
in the wire format or handler logic, which will be ignored by the server now that we’re only looking atpatternName
. Please update these to ensure the client’s selection makes it all the way through:• src/core/Schemas.ts (ClientJoinMessageSchema around line 488)
export const ClientJoinMessageSchema = z.object({ clientID: ID, flag: FlagSchema, - pattern: PatternSchema.optional(), patternName: z.string().optional(), token: TokenSchema, type: z.literal("join"), });
• src/server/GameServer.ts (around line 302)
Replace the outgoing join-complete payload’spattern: c.pattern
withpatternName: c.patternName
.• src/client/SinglePlayerModal.ts (around line 450)
When dispatching the “join” event for single-player, sendpatternName
instead ofpattern
:payload: { flag: flagInput.getCurrentFlag(), - pattern: pattern?.pattern, + patternName, },• src/server/Client.ts
Rename thepublic readonly pattern: string | undefined
property topatternName
(and update all its usages).• src/core/game/GameView.ts (around line 396)
Change{ flag: h.flag, pattern: h.pattern }
to{ flag: h.flag, patternName: h.patternName }
so the view schema aligns.After these changes, verify that:
- All
pattern:
occurrences in join messages/schema are removed.- All
ClientJoinMessage
handlers read fromclientMsg.patternName
only.- No client-to-server or server-to-client payloads still reference the old
pattern
field.This ensures the chosen pattern flows end-to-end.
src/client/Transport.ts (1)
373-384
: Ensure PreJoinHandler usespatternName
instead ofpattern
We still see one remaining reference to
clientMsg.pattern
in the server’s join flow. Update this to consume the newpatternName
field so that the server honors the client’s selected pattern rather than falling back to the old property.• Location to fix:
src/server/worker/websocket/handler/message/PreJoinHandler.ts
around line 255
• Change this snippet:// before clientMsg.flag, clientMsg.pattern, ); // after clientMsg.flag, clientMsg.patternName, );• Confirm downstream logic uses the correct
pattern
value (pulled frompatternName
). Update any other references or model types as needed to avoid having two different sources for the same data.src/core/Schemas.ts (1)
483-494
: EnsurepatternName
is strongly typed and mutually exclusive withpattern
The
PatternNameSchema
is already exported fromsrc/core/CosmeticSchemas.ts
(line 18), so we can strengthen typing and add a guard to prevent sending both fields at once.Please update
src/core/Schemas.ts
as follows:• Import
PatternNameSchema
alongsidePatternSchema
.
• Replace the plain-stringpatternName
withPatternNameSchema.optional()
.
• Add a.refine()
call to enforce that only one ofpattern
orpatternName
may be present.--- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -1,5 +1,5 @@ -import { PatternSchema } from "./CosmeticSchemas"; +import { PatternSchema, PatternNameSchema } from "./CosmeticSchemas"; export const ClientJoinMessageSchema = z.object({ clientID: ID, @@ -490,8 +490,8 @@ export const ClientJoinMessageSchema = z.object({ lastTurn: z.number(), // The last turn the client saw. - pattern: PatternSchema.optional(), - patternName: z.string().optional(), + pattern: PatternSchema.optional(), + patternName: PatternNameSchema.optional(), token: TokenSchema, // WARNING: PII type: z.literal("join"), username: UsernameSchema, @@ -494,4 +494,9 @@ export const ClientJoinMessageSchema = z.object({ }); + +// Ensure only one of `pattern` or `patternName` is provided. +export const ClientJoinMessageSchema = ClientJoinMessageSchema.refine( + (v) => !(v.pattern && v.patternName), + { message: "Provide either pattern or patternName, not both" } +);src/core/CosmeticSchemas.ts (1)
52-76
: Enforce key/name consistency to avoid drift.Right now, the key (record key) and
PatternInfoSchema.name
can diverge. Add a top-levelsuperRefine
to validate equality.-export const CosmeticsSchema = z.object({ +export const CosmeticsSchema = z.object({ /* eslint-disable sort-keys */ - patterns: z.record(z.string(), PatternInfoSchema), + patterns: z.record(PatternNameSchema, PatternInfoSchema), flag: z .object({ layers: z.record( z.string(), z.object({ name: z.string(), flares: z.string().array().optional(), }), ), color: z.record( z.string(), z.object({ color: z.string(), name: z.string(), flares: z.string().array().optional(), }), ), }) .optional(), /* eslint-enable sort-keys */ -}); +}).superRefine((data, ctx) => { + for (const [key, info] of Object.entries(data.patterns)) { + if (info.name !== key) { + ctx.addIssue({ + code: "custom", + message: `patterns["${key}"].name must equal "${key}"`, + path: ["patterns", key, "name"], + }); + } + } +});src/client/TerritoryPatternsModal.ts (1)
101-107
: Tooltip shows for unlocked patterns (null vs undefined bug).
product
isProduct | null
, neverundefined
. This condition is always true fornull
, so the tooltip appears when it shouldn’t.- if (this.hoveredPattern && this.hoveredPattern.product !== undefined) { + if (this.hoveredPattern && this.hoveredPattern.product !== null) {src/core/game/TrainStation.ts (1)
28-34
: Compare players by id(), not by object identity.Object identity can differ across references to the same player. Use stable PlayerID for equality in both revenue sharing and rel().
Apply this diff:
- const goldBonus = mg.config().trainGold(rel(trainOwner, stationOwner)); - // Share revenue with the station owner if it's not the current player - if (trainOwner !== stationOwner) { + const goldBonus = mg.config().trainGold(rel(trainOwner, stationOwner)); + // Share revenue with the station owner if it's not the current player + if (trainOwner.id() !== stationOwner.id()) { stationOwner.addGold(goldBonus, station.tile()); } trainOwner.addGold(goldBonus, station.tile());- const goldBonus = mg.config().trainGold(rel(trainOwner, stationOwner)); + const goldBonus = mg.config().trainGold(rel(trainOwner, stationOwner)); - trainOwner.addGold(goldBonus, station.tile()); - // Share revenue with the station owner if it's not the current player - if (trainOwner !== stationOwner) { + trainOwner.addGold(goldBonus, station.tile()); + // Share revenue with the station owner if it's not the current player + if (trainOwner.id() !== stationOwner.id()) { stationOwner.addGold(goldBonus, station.tile()); }-function rel(player: Player, other: Player): "self" | "friendly" | "other" { - if (player === other) { +function rel(player: Player, other: Player): "self" | "friendly" | "other" { + if (player.id() === other.id()) { return "self"; } if (player.isFriendly(other)) { return "friendly"; } return "other"; }Also applies to: 46-53, 236-244
🧹 Nitpick comments (38)
src/client/ClientGameRunner.ts (1)
50-56
: Rename to patternName aligns with the wider schema changes; prefer an optional field overstring | undefined
.Using an optional property reads cleaner in TypeScript and better reflects intent.
Apply this diff:
export type LobbyConfig = { serverConfig: ServerConfig; - patternName: string | undefined; + patternName?: string; flag: string; playerName: string;src/client/FlagInput.ts (1)
46-52
: Harden event parsing to avoid crashes if detail is missing.If someone dispatches
flag-change
withoutdetail.flag
,safeParse(e.detail.flag)
will throw due toe.detail
being undefined.Apply this diff:
- private readonly updateFlag = (ev: Event) => { - const e = ev as CustomEvent<{ flag: string }>; - if (!FlagSchema.safeParse(e.detail.flag).success) return; - if (this.flag !== e.detail.flag) { - this.flag = e.detail.flag; - } - }; + private readonly updateFlag = (ev: Event) => { + const e = ev as Partial<CustomEvent<{ flag: string }>>; + const candidate = e?.detail?.flag; + if (!FlagSchema.safeParse(candidate).success) return; + if (this.flag !== candidate) { + this.flag = candidate; + } + };src/core/game/UserSettings.ts (2)
114-116
: Consider migrating old stored values to the new name-based format.Older clients may have base64 pattern data under
territoryPattern
. Returning it as a “name” will fail lookups. Clear or migrate obviously non-name values.Apply this diff:
getSelectedPatternName(): string | undefined { - return localStorage.getItem(PATTERN_KEY) ?? undefined; + const raw = localStorage.getItem(PATTERN_KEY) ?? undefined; + // Naive migration: if it looks like legacy base64/blob (too long or contains '=' or '/'), + // drop it so the UI falls back to "no selection". + if (raw && (raw.length > 64 || /[=\/]/.test(raw))) { + localStorage.removeItem(PATTERN_KEY); + return undefined; + } + return raw; }
118-124
: Rename the parameter to reflect the new semantics (it’s a name, not base64).Also avoid leaking legacy wording into code.
Apply this diff:
- setSelectedPatternName(base64: string | undefined): void { - if (base64 === undefined) { + setSelectedPatternName(name: string | undefined): void { + if (name === undefined) { localStorage.removeItem(PATTERN_KEY); } else { - localStorage.setItem(PATTERN_KEY, base64); + localStorage.setItem(PATTERN_KEY, name); } }src/client/Cosmetics.ts (2)
21-31
: Avoid mutating the cosmetics catalog when the player has access.
patternData.product = null
changes shared data. Prefer a shallow copy.Apply this diff:
- const patternData = cosmetics.patterns[name]; + const patternData = cosmetics.patterns[name]; const hasAccess = playerFlares.has(`pattern:${name}`); if (hasAccess) { // Remove product info because player already has access. - patternData.product = null; - patterns.set(name, patternData); + patterns.set(name, { ...patternData, product: null }); } else if (patternData.product !== null) { // Player doesn't have access, but product is available for purchase. - patterns.set(name, patternData); + patterns.set(name, { ...patternData }); }
80-96
: Exporting getCosmetics() is fine; consider caching to cut network chatter.Low-priority: a simple in-memory cache would avoid repeated fetch+parse when multiple views open.
Example (outside this hunk):
let cosmeticsCache: Cosmetics | undefined; export async function getCosmetics(): Promise<Cosmetics | undefined> { if (cosmeticsCache) return cosmeticsCache; try { // ...fetch/parse... cosmeticsCache = result.data; return cosmeticsCache; } catch (e) { return undefined; } }src/client/Main.ts (1)
432-439
: Localize user-facing alerts for purchase flow.These strings should go through
translateText()
per PR checklist.Apply this diff:
- alertAndStrip("purchase succeeded"); + alertAndStrip(translateText("purchase.succeeded")); return; } else if (params.get("purchase-completed") === "false") { - alertAndStrip("purchase failed"); + alertAndStrip(translateText("purchase.failed")); return; }src/client/SinglePlayerModal.ts (2)
5-16
: Imports look good; keep the API surface small and typed.
- Nice: using translateText for UI strings and typed imports (TeamCountConfig).
- Consider importing a PatternName type (if exported) for local variables where you handle patternName to make intent explicit and help refactors later.
407-477
: Async pattern resolution is correct; add resilience and prefetch to cut latency.
- If cosmetics fetch fails, pattern is undefined (good), but there’s no user feedback. Optional: log and proceed is fine for Singleplayer.
- To reduce click-to-start latency, prefetch cosmetics on modal open and reuse them.
Example patch:
public open() { this.modalEl?.open(); this.useRandomMap = false; + // Warm-up cosmetics so startGame avoids a network hop. + void getCosmetics(); }Optionally, cache the result in a field to avoid multiple fetches.
src/server/Privilege.ts (1)
9-18
: Rename the parameter to reflect new semantics.The API now takes a pattern name, not base64. Rename for clarity to avoid misuse at call sites.
export type PrivilegeChecker = { isPatternAllowed( - base64: string, + name: string, flares: readonly string[] | undefined, ): PatternResult;src/core/CosmeticSchemas.ts (4)
18-21
: PatternName should forbid empty strings.Add a minimal length to prevent
""
from passing validation.export const PatternNameSchema = z .string() .regex(/^[a-z0-9_]+$/) - .max(32); + .min(1) + .max(32);
23-44
: Avoid noisy logging inside schema validation and remove the “magic number”.
- Validation should not
console.error
on expected bad input; it’ll spam logs client- and server-side.- The hardcoded
1403
is a magic number; make it a named constant with a short rationale.Apply this diff to quiet validation and reference a named constant:
export const PatternSchema = z .string() - .max(1403) - .base64url() + .max(MAX_PATTERN_B64URL_LEN, { message: "Pattern too large" }) + // If your Zod version supports it, `.base64url()` can be kept in addition to the refine. .refine( (val) => { try { new PatternDecoder(val, base64url.decode); return true; } catch (e) { - if (e instanceof Error) { - console.error(JSON.stringify(e.message, null, 2)); - } else { - console.error(String(e)); - } return false; } }, { message: "Invalid pattern", }, );Add this constant near the top of the file (outside the shown range):
// Upper bound to cap decode/preview work. 3 header bytes + up to 128x128 bits (scaled) ≈ 1403 chars base64url. // Keep aligned with PatternDecoder expectations and UI preview limits. export const MAX_PATTERN_B64URL_LEN = 1403 as const;
55-55
: UsePatternNameSchema
for record keys (stronger typing).Keys are pattern names; reflect that in the schema.
- patterns: z.record(z.string(), PatternInfoSchema), + patterns: z.record(PatternNameSchema, PatternInfoSchema),
1-3
: Use the standard Zod importI’ve confirmed that your project is already on Zod v4 (package.json shows
^4.0.5
), so there’s no build-break risk from using v3. Also, I didn’t find any calls to a chained.base64url()
on Zod schemas—your code importsbase64url
fromjose
instead.To keep imports consistent across the repo, please update:
• In src/core/CosmeticSchemas.ts, line 2
- import { z } from "zod/v4"; + import { z } from "zod";No other changes are needed here.
src/client/TerritoryPatternsModal.ts (4)
14-25
: Single source of truth for button width.You introduced
BUTTON_WIDTH
(nice), butbuttonWidth
remains a redundant instance field. Either remove it or have it reference the constant to avoid divergence.- public previewButton: HTMLElement | null = null; - public buttonWidth = 150; + public previewButton: HTMLElement | null = null; + public buttonWidth = BUTTON_WIDTH; // keep for backwards compatibility, but delegate to constant
176-179
: Iterate Map values directly for clarity.Small readability win; avoids the unused
_
.- for (const [_, pattern] of this.patterns) { + for (const pattern of this.patterns.values()) { const { name } = pattern; if (!this.showChocoPattern && name === "choco") continue;
355-369
: Fallback to a safe default preview on decode errors.Returning an empty string yields a broken image icon. Decode the DEFAULT pattern instead so the UI stays clean.
- let decoder: PatternDecoder; - try { - decoder = new PatternDecoder(pattern, base64url.decode); - } catch (e) { - console.error("Error decoding pattern", e); - return ""; - } + let decoder: PatternDecoder; + try { + decoder = new PatternDecoder(pattern, base64url.decode); + } catch (e) { + console.warn("Error decoding pattern; falling back to default", e); + try { + decoder = new PatternDecoder(DEFAULT_PATTERN_B64, base64url.decode); + pattern = DEFAULT_PATTERN_B64; // keep cache key consistent with actual preview + } catch { + return ""; + } + }
345-407
: Bound the preview cache to prevent unbounded growth.
patternCache
is global and unbounded. If patterns or sizes vary a lot, memory can grow without limit. Add a simple cap (e.g., 100 entries) and evict oldest.Example helper outside the function:
function setCacheEntry(cache: Map<string, string>, key: string, value: string, max = 100) { if (cache.size >= max) { const firstKey = cache.keys().next().value; cache.delete(firstKey); } cache.set(key, value); }Then replace the setter:
- patternCache.set(patternLookupKey, dataUrl); + setCacheEntry(patternCache, patternLookupKey, dataUrl, 128);src/core/game/GameView.ts (1)
388-399
: No remaining_hunans
usages; rename is completeI ran a full search for the old
_hunans
identifier and found zero hits—your rename to_humans
is clean and final.Regarding the null-guard on
clientID
:
- The Zod
PlayerSchema
enforcesclientID
as a non-nullID
(a safe string) when you loadgameStart.players
.- In
GameView
you only map over those human players, soh.clientID
cannot benull
orundefined
at runtime.Optional hardening: if you ever extend
PlayerInfo
to allow bots or missing IDs, you could apply your.filter(...)
+.map(...)
diff insrc/core/game/GameView.ts
(around lines 392–397). Otherwise, no changes are strictly needed here.src/core/Util.ts (1)
292-298
: Sigmoid is correct; document inputs and handle edge cases (decayRate = 0, non-finite)Implementation is the standard logistic. Add minimal guards + JSDoc for clarity and robustness.
Apply this diff:
+/** + * Logistic/sigmoid helper. + * value: x-axis input. + * decayRate: steepness (> 0 recommended). If 0, returns 0.5. + * midpoint: x where output is 0.5. + * Returns a number in [0, 1]. + */ export function sigmoid( value: number, decayRate: number, midpoint: number, ): number { - return 1 / (1 + Math.exp(-decayRate * (value - midpoint))); + if (!Number.isFinite(value) || !Number.isFinite(decayRate) || !Number.isFinite(midpoint)) { + return 0.5; + } + if (decayRate === 0) return 0.5; + const z = -decayRate * (value - midpoint); + const out = 1 / (1 + Math.exp(z)); + // Clamp to [0,1] to avoid tiny FP drift. + return Math.min(1, Math.max(0, out)); }src/core/game/Game.ts (2)
1-1
: Avoid disabling max-lines; split types and enums into focused modulesLarge files are harder to review and maintain. Prefer composition and smaller modules (e.g., move enums and type aliases into dedicated files).
Happy to suggest a cut plan if you want it.
352-359
: Use a named union for AllianceRequest status
To make thestatus
type clear and easy to reuse, replace the inline string-literal union with a shared alias. I found two spots still using the literal union and no existing alias in the codebase:• src/core/game/Game.ts (line 358)
• src/core/game/AllianceRequestImpl.ts (line 15)Please:
- Add this alias near your other game/diplomacy types (for example, above the
AllianceRequest
interface in Game.ts):export type AllianceRequestStatus = "pending" | "accepted" | "rejected";- Update the interface in Game.ts:
export type AllianceRequest = { ── status(): "pending" | "accepted" | "rejected"; ++ status(): AllianceRequestStatus; };
- Update the implementation in AllianceRequestImpl.ts:
export class AllianceRequestImpl implements AllianceRequest { ── status(): "pending" | "accepted" | "rejected" { ++ status(): AllianceRequestStatus { return this._status; } }
This change is optional but will improve type clarity and make future refactors or tests easier.
src/client/graphics/layers/EventsDisplay.ts (2)
196-200
: Minor: evaluate shouldDelete first to drop stale events earlyFunctionally correct. For efficiency and intent, check shouldDelete before duration. It avoids needless tick math and reads clearer.
Apply this diff:
- let remainingEvents = this.events.filter((event) => { - const shouldKeep = - this.game.ticks() - event.createdAt < (event.duration ?? 600) && - !event.shouldDelete?.(this.game); + let remainingEvents = this.events.filter((event) => { + const shouldKeep = + !(event.shouldDelete?.(this.game) ?? false) && + this.game.ticks() - event.createdAt < (event.duration ?? 600);
461-466
: Clamp alliance request duration; avoid magic tick constantGood use of config(). To be safe against misconfig, clamp to non-negative. Also, the -20 assumes “10 ticks = 1s”. Prefer a helper if available; otherwise clamp now.
Apply this diff:
- duration: this.game.config().allianceRequestDuration() - 20, // 2 second buffer + duration: Math.max(0, this.game.config().allianceRequestDuration() - 20), // 2s buffer; clampIf you want, I can add a small util like secondsToTicks(s: number): Tick to remove the magic 20/10s from UI code.
src/core/configuration/Config.ts (1)
124-139
: Add explicit JSDoc for timing and spawn‐rate methodsPlease enrich the new configuration signatures with clear JSDoc so callers know exactly what units and semantics they’re working with:
•
allianceRequestDuration(): Tick
– How many game ticks an alliance request stays actionable before expiring.•
tradeShipSpawnRate(numTradeShips: number, numPlayerPorts: number): number
– Chance of spawning a trade ship per tick (e.g. expected spawns/tick, normalized per port, bounded [0,1]).•
trainGold(rel: "self" | "friendly" | "other"): Gold
– Amount of gold awarded per train spawn or per delivery (specify which).•
trainSpawnRate(numPlayerFactories: number): number
– Expected number of train spawns per tick (describe cap or decay behavior).Adding these comments in
src/core/configuration/Config.ts
above each signature will prevent magic constants and misuse in callers.tests/core/game/TrainStation.test.ts (1)
64-67
: Future-proof the config mock to match the new relation-based API.The production API is now
trainGold(rel: "self" | "friendly" | "other")
, but the mock still accepts a boolean. This passes today because strings are truthy, but it can hide bugs and weakens type-safety.Suggested mock for clarity:
config: jest.fn().mockReturnValue({ trainGold: (rel: "self" | "friendly" | "other") => rel === "friendly" || rel === "self" ? 1000n : 500n, }),If desired, I can add a small test that verifies self/friendly/other all call the mock with the right rel.
src/core/game/AllianceRequestImpl.ts (1)
6-7
: Good move: use a typed-union status on the request.This is clean, composable, and easy to reason about. Consider whether you also need to represent “expired” in the lifecycle, or ensure expired requests are removed so clients never see stale
"pending"
. If clients need to render status, consider adding it to the update payloads (only if the wire type supports it).Would you like me to scan usages to confirm whether expiry is handled purely by removal vs. state?
Also applies to: 15-17
src/core/execution/PortExecution.ts (1)
81-85
: Confirm whether tradeShipSpawnRate expects count-of-ports or port-level capacity.
unitCount(UnitType.Port)
sums levels, not the number of port units. If the config function expects a simple count, usethis.player.units(UnitType.Port).length
. If it expects “capacity”, keeping sum-of-levels is fine but please rename the variable accordingly to avoid confusion.Two options:
- If config expects number of ports (recommended for clarity):
- const numPlayerPorts = this.player.unitCount(UnitType.Port); + const numPlayerPorts = this.player.units(UnitType.Port).length;
- If config expects a capacity-like value (sum of port levels), just rename:
- const numPlayerPorts = this.player.unitCount(UnitType.Port); + const playerPortCapacity = this.player.unitCount(UnitType.Port); - const spawnRate = this.mg - .config() - .tradeShipSpawnRate(numTradeShips, numPlayerPorts); + const spawnRate = this.mg + .config() + .tradeShipSpawnRate(numTradeShips, playerPortCapacity);I can grep DefaultConfig to verify the intent and update this spot plus its docstring.
src/core/execution/TrainStationExecution.ts (2)
51-55
: Double-check whether trainSpawnRate expects factory count vs. capacity (sum of levels).
unitCount(UnitType.Factory)
returns the sum of factory levels. If the config treats the parameter as “number of factories,” prefer a plain count:- const spawnRate = this.mg - .config() - .trainSpawnRate(this.unit.owner().unitCount(UnitType.Factory)); + const numFactories = this.unit.owner().units(UnitType.Factory).length; + const spawnRate = this.mg.config().trainSpawnRate(numFactories);If capacity is intended, consider documenting it or renaming a local variable for clarity in both Train and Port logic to keep semantics consistent.
78-78
: Spawn decision now ignores cluster size — confirm balance impact.Previously, cluster size influenced spawn probability. With the new config-driven approach, you still gate on
availableForTrade.size > 0
, which is good. If pacing feels off during playtests, consider feeding a small cluster factor into the config function rather than re-introducing complex logic here (composition over inheritance, keep config as the policy).Happy to prototype a
trainSpawnRate(capacity: number, clusterSize: number)
signature if needed.src/core/game/TrainStation.ts (3)
38-38
: Remove unused PseudoRandom from PortStopHandler and handler factory.random is not used anymore; keep API simple and avoid dead params/imports.
Apply this diff:
-import { PseudoRandom } from "../PseudoRandom"; +// PseudoRandom no longer needed here -class PortStopHandler implements TrainStopHandler { - constructor(private readonly random: PseudoRandom) {} +class PortStopHandler implements TrainStopHandler { onStop(-export function createTrainStopHandlers( - random: PseudoRandom, -): Partial<Record<UnitType, TrainStopHandler>> { +export function createTrainStopHandlers(): Partial< + Record<UnitType, TrainStopHandler> +> { return { [UnitType.City]: new CityStopHandler(), - [UnitType.Port]: new PortStopHandler(random), + [UnitType.Port]: new PortStopHandler(), [UnitType.Factory]: new FactoryStopHandler(), }; }- constructor( - private readonly mg: Game, - public unit: Unit, - ) { - this.stopHandlers = createTrainStopHandlers(new PseudoRandom(mg.ticks())); - } + constructor(private readonly mg: Game, public unit: Unit) { + this.stopHandlers = createTrainStopHandlers(); + }Also applies to: 64-72, 80-86
61-61
: FactoryStopHandler is a no-op; ensure this is deliberate.If factories are supposed to have stop effects later, leave a TODO with context to avoid regressions.
28-34
: Deduplicate stop award logic.City and Port handlers do the same thing; prefer a tiny helper to keep logic in one place and reduce bugs.
Example:
+function awardStopGold( + mg: Game, + station: TrainStation, + trainOwner: Player, + stationOwner: Player, +): void { + const amount = mg.config().trainGold(rel(trainOwner, stationOwner)); + trainOwner.addGold(amount, station.tile()); + if (trainOwner.id() !== stationOwner.id()) { + stationOwner.addGold(amount, station.tile()); + } +}Also applies to: 46-53
src/core/execution/alliance/AllianceRequestExecution.ts (1)
48-62
: Optional: use ticks param for testability, or document choice.You ignore the ticks param and read mg.ticks(). That's okay, but pick one source and document, so executions behave uniformly.
tests/AllianceRequestExecution.test.ts (1)
62-76
: Good expiry test; add two edge cases for robustness.Consider adding:
- Missing recipient deactivates immediately (should not linger).
- Cannot send (cooldown or already allied) keeps requests at 0.
I can draft tests if helpful. Example skeletons:
test("Missing recipient deactivates immediately", () => { // use an invalid id game.addExecution(new AllianceRequestExecution(player1, "does-not-exist")); game.executeNextTick(); expect(player1.outgoingAllianceRequests()).toHaveLength(0); }); test("Cannot send when already allied or on cooldown", () => { // Pre-create alliance or simulate cooldown; then: game.addExecution(new AllianceRequestExecution(player1, player2.id())); game.executeNextTick(); expect(player1.outgoingAllianceRequests()).toHaveLength(0); });src/core/configuration/DefaultConfig.ts (3)
340-344
: trainSpawnRate comment mismatches the math.trainSpawnRate returns a “rate” used as 1/p; the comment about expected number is misleading. Clarify units.
- // hyperbolic decay, midpoint at 10 factories - // expected number of trains = numPlayerFactories / trainSpawnRate(numPlayerFactories) + // Hyperbolic-like decay, midpoint ~10 factories. + // Probability of spawning a train each tick is 1 / trainSpawnRate(numPlayerFactories). return (numPlayerFactories + 10) * 20;
671-706
: Attack debuffs: extract magic numbers and document rationale.Values like 100_000 tiles, exponents 0.7/0.6 are important tuning knobs. Consider top-level named constants to keep balance intent explicit.
1-1
: Style: prefer composition over small handler classes where possible.DefaultConfig remains monolithic; for stop handlers elsewhere, consider a simple Record<UnitType, (ctx) => void> instead of tiny classes to reduce boilerplate and improve readability.
Also applies to: 214-222
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (3)
resources/sprites/trainCarriage.png
is excluded by!**/*.png
resources/sprites/trainCarriageLoaded.png
is excluded by!**/*.png
resources/sprites/trainEngine.png
is excluded by!**/*.png
📒 Files selected for processing (28)
eslint.config.js
(0 hunks)src/client/ClientGameRunner.ts
(1 hunks)src/client/Cosmetics.ts
(3 hunks)src/client/FlagInput.ts
(1 hunks)src/client/Main.ts
(2 hunks)src/client/SinglePlayerModal.ts
(5 hunks)src/client/TerritoryPatternsModal.ts
(12 hunks)src/client/Transport.ts
(1 hunks)src/client/graphics/layers/EventsDisplay.ts
(3 hunks)src/core/CosmeticSchemas.ts
(2 hunks)src/core/Schemas.ts
(3 hunks)src/core/Util.ts
(1 hunks)src/core/configuration/Config.ts
(2 hunks)src/core/configuration/DefaultConfig.ts
(12 hunks)src/core/execution/PortExecution.ts
(1 hunks)src/core/execution/TrainStationExecution.ts
(3 hunks)src/core/execution/alliance/AllianceRequestExecution.ts
(1 hunks)src/core/game/AllianceRequestImpl.ts
(2 hunks)src/core/game/Game.ts
(2 hunks)src/core/game/GameView.ts
(1 hunks)src/core/game/PlayerImpl.ts
(1 hunks)src/core/game/TrainStation.ts
(5 hunks)src/core/game/UserSettings.ts
(1 hunks)src/server/MapPlaylist.ts
(0 hunks)src/server/Privilege.ts
(2 hunks)src/server/worker/websocket/handler/message/PreJoinHandler.ts
(2 hunks)tests/AllianceRequestExecution.test.ts
(1 hunks)tests/core/game/TrainStation.test.ts
(1 hunks)
💤 Files with no reviewable changes (2)
- src/server/MapPlaylist.ts
- eslint.config.js
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
Applied to files:
src/client/Main.ts
src/client/SinglePlayerModal.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as `z.infer<typeof PlayerStatsSchema>` where PlayerStatsSchema has `.optional()` applied at the object level, making PlayerStats a union type that already includes undefined (PlayerStats | undefined).
Applied to files:
src/core/Schemas.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as a type inferred from a Zod schema that is marked as optional, which means PlayerStats already includes undefined as a possible type (PlayerStats | undefined).
Applied to files:
src/core/Schemas.ts
📚 Learning: 2025-05-21T04:10:59.706Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:44-53
Timestamp: 2025-05-21T04:10:59.706Z
Learning: The PlayerStats type from ArchiveSchemas already includes undefined in its definition, making explicit union types with undefined (PlayerStats | undefined) redundant.
Applied to files:
src/core/Schemas.ts
📚 Learning: 2025-08-16T16:16:00.414Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#1758
File: src/core/ApiSchemas.ts:63-72
Timestamp: 2025-08-16T16:16:00.414Z
Learning: In Zod v4, z.enum() can accept TypeScript native enums directly (e.g., z.enum(MyEnum)). The z.nativeEnum() function was deprecated in v4 and replaced by the overloaded z.enum(). This means z.enum(GameType), z.enum(GameMode), etc. are valid in Zod v4.
Applied to files:
src/core/Schemas.ts
📚 Learning: 2025-08-14T10:07:44.588Z
Learnt from: woodydrn
PR: openfrontio/OpenFrontIO#1811
File: src/client/FlagInput.ts:0-0
Timestamp: 2025-08-14T10:07:44.588Z
Learning: The codebase uses FlagSchema from "../core/Schemas" for validating flag values. The validation is done using FlagSchema.safeParse(value).success pattern, which is preferred over custom regex validation for flag input validation.
Applied to files:
src/core/Schemas.ts
📚 Learning: 2025-05-18T23:36:12.847Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:143-159
Timestamp: 2025-05-18T23:36:12.847Z
Learning: In this codebase, NukeType is a union type derived from specific UnitType enum values (AtomBomb, HydrogenBomb, MIRV, MIRVWarhead). This means that comparisons in switch statements between a NukeType parameter and UnitType enum values are valid and will work correctly at runtime.
Applied to files:
src/core/configuration/DefaultConfig.ts
📚 Learning: 2025-05-18T23:36:12.847Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:143-159
Timestamp: 2025-05-18T23:36:12.847Z
Learning: In this codebase, NukeType is a union type derived from UnitType values (specifically bomb-related values like AtomBomb, HydrogenBomb, MIRV, and MIRVWarhead) rather than a separate enum. This means comparing NukeType values against UnitType values in switch statements is valid and intentional.
Applied to files:
src/core/configuration/DefaultConfig.ts
📚 Learning: 2025-05-31T18:15:03.445Z
Learnt from: 1brucben
PR: openfrontio/OpenFrontIO#977
File: src/core/execution/AttackExecution.ts:123-125
Timestamp: 2025-05-31T18:15:03.445Z
Learning: The removeTroops function in PlayerImpl.ts already prevents negative troop counts by using minInt(this._troops, toInt(troops)) to ensure it never removes more troops than available.
Applied to files:
src/core/configuration/DefaultConfig.ts
📚 Learning: 2025-06-22T05:48:19.241Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#786
File: src/client/TerritoryPatternsModal.ts:337-338
Timestamp: 2025-06-22T05:48:19.241Z
Learning: In src/client/TerritoryPatternsModal.ts, the bit shifting operators (<<) used in coordinate calculations with decoder.getScale() are intentional and should not be changed to multiplication. The user scottanderson confirmed this is functioning as intended.
Applied to files:
src/client/TerritoryPatternsModal.ts
🧬 Code Graph Analysis (13)
src/core/game/GameView.ts (2)
src/core/game/Game.ts (1)
Player
(500-634)src/core/Schemas.ts (1)
Player
(109-109)
tests/AllianceRequestExecution.test.ts (4)
src/core/game/Game.ts (2)
Game
(636-720)Player
(500-634)tests/util/Setup.ts (2)
setup
(23-87)playerInfo
(89-91)src/core/execution/alliance/AllianceRequestExecution.ts (1)
AllianceRequestExecution
(9-72)src/core/execution/alliance/AllianceRequestReplyExecution.ts (1)
AllianceRequestReplyExecution
(3-56)
src/core/game/AllianceRequestImpl.ts (1)
src/core/game/Game.ts (1)
Player
(500-634)
src/core/configuration/Config.ts (1)
src/core/game/Game.ts (2)
Tick
(16-16)Gold
(17-17)
src/client/Cosmetics.ts (1)
src/core/CosmeticSchemas.ts (1)
Pattern
(6-6)
src/client/graphics/layers/EventsDisplay.ts (3)
src/core/game/GameView.ts (1)
GameView
(364-696)src/core/game/AllianceRequestImpl.ts (2)
requestor
(19-21)recipient
(23-25)src/core/game/AllianceImpl.ts (2)
requestor
(26-28)recipient
(30-32)
src/core/execution/alliance/AllianceRequestExecution.ts (1)
src/core/game/Game.ts (5)
Execution
(324-329)AllianceRequest
(352-359)Game
(636-720)Player
(500-634)PlayerID
(15-15)
src/core/CosmeticSchemas.ts (1)
src/core/PatternDecoder.ts (1)
PatternDecoder
(1-56)
src/core/game/TrainStation.ts (1)
src/core/game/Game.ts (1)
Player
(500-634)
src/client/SinglePlayerModal.ts (1)
src/client/Cosmetics.ts (1)
getCosmetics
(80-96)
src/core/configuration/DefaultConfig.ts (2)
src/core/game/Game.ts (3)
Gold
(17-17)Player
(500-634)Tick
(16-16)src/core/Util.ts (2)
sigmoid
(292-298)within
(36-38)
src/client/TerritoryPatternsModal.ts (3)
src/core/CosmeticSchemas.ts (1)
Pattern
(6-6)src/client/Cosmetics.ts (1)
patterns
(9-35)src/core/PatternDecoder.ts (1)
PatternDecoder
(1-56)
src/server/Privilege.ts (1)
src/core/PatternDecoder.ts (1)
PatternDecoder
(1-56)
🔇 Additional comments (27)
src/core/game/UserSettings.ts (1)
114-124
: No old API usages foundI ran the Ripgrep search for both
getSelectedPattern(
andsetSelectedPattern(
and found no matches. All legacy calls have been removed—no further action needed.src/client/Cosmetics.ts (1)
9-16
: All call sites now use Map methods; safe to approveI checked the two consumer files mentioned and found no array-style calls (
.find
,.map
,.filter
) on the result ofpatterns()
. BothTerritoryPatternsModal
and anySinglePlayerModal
usages either aren’t present or correctly handle theMap<string, Pattern>
return (e.g., via.get
,.has
, or iteration).Great work—this change stands as is!
src/client/Main.ts (2)
221-224
: Switch toterritoryModal.refresh()
looks good; it simplifies layout sync.Assuming
refresh()
encapsulates preview sizing + data reload, this is a clean improvement over the previous two-step flow.
461-473
: patternName is already optional—no change neededThe call
this.userSettings.getSelectedPatternName()
returnsstring | undefined
(seeUserSettings.getSelectedPatternName()
:localStorage.getItem(...) ?? undefined
) and the transport schema declarespatternName: z.string().optional()
incore/Schemas.ts
– it already accepts an undefined value. The server handler inPreJoinHandler.ts
checksclientMsg.patternName !== undefined
before using it. At runtimeJSON.stringify
will omit keys withundefined
, so adding?? undefined
has no effect.No diff required here—please leave the code as is.
src/server/worker/websocket/handler/message/PreJoinHandler.ts (2)
15-15
: Solid: assertNever import for exhaustiveness.Good defensive addition to force handling of all PatternResult variants in the switch.
245-256
: Apply resolved pattern in Client constructorThe search shows that
clientMsg.pattern
only appears here, so updating this spot covers all uses.
- File:
src/server/worker/websocket/handler/message/PreJoinHandler.ts
Lines 253–256: pass the locally computedpattern
instead ofclientMsg.pattern
const client = new Client( clientMsg.clientID, persistentId, claims, roles, flares, ip, clientMsg.username, ws, clientMsg.flag, - clientMsg.pattern, + pattern, );src/core/Schemas.ts (2)
1-1
: Good: centralizing PatternSchema in CosmeticSchemas.This reduces duplicate validation logic and keeps pattern rules in one place.
383-387
: Player.pattern optional is correct for fail-open and singleplayer paths.This aligns with letting players join without a resolved pattern and avoids over-validating.
src/server/Privilege.ts (3)
27-34
: Minor: early “not found” return is fine; consider distinguishing unlisted vs. unknown at source.Current “pattern not found” maps to forbidden here; PreJoinHandler maps it to “Pattern unlisted” in its return. That’s consistent. No code change needed.
126-132
: Fail-open checker returns unknown, which pairs well with the PreJoinHandler fail-open change.After adopting the PreJoinHandler change to allow join without pattern on “unknown”, this path behaves as intended during backend outages.
41-49
: Incorrect flare check suggestionThe cosmetics patterns in
resources/cosmetics/cosmetics.json
all include a"name"
field (e.g."ABMIVVU": { "name": "stripes_v" }
), sofound.name
is defined and matches the requestedname
. Swappingfound.name
for the localname
variable is a no-op in practice and isn’t needed.No changes required here—please ignore this comment.
Likely an incorrect or invalid review comment.
src/core/CosmeticSchemas.ts (1)
46-50
: Schema looks good. Ensureproduct
is strictly nullable, not undefined.This matches downstream usage that checks
product !== null
. Good alignment.src/client/TerritoryPatternsModal.ts (1)
116-128
: Purchase/button gating looks correct.Good: selection is blocked via both styling and a click guard when
product !== null
. Matches Product being nullable in the schema.Also applies to: 155-167
src/client/graphics/layers/EventsDisplay.ts (1)
68-69
: New shouldDelete hook is a clean, composable additionThis keeps event lifecycle logic local to the event and avoids side-effects. Good move.
tests/core/game/TrainStation.test.ts (1)
64-67
: Allied gold expectations updated correctly to 1000n.The assertions now reflect the new config. Looks good.
src/core/game/PlayerImpl.ts (1)
400-403
: No change needed:outgoingAllianceRequests
only returns pending requestsThe game’s request list (
mg.allianceRequests
) is pruned on both accept and reject, so non-pending requests are removed immediately. That means:
- In GameImpl.acceptAllianceRequest(request) and rejectAllianceRequest(request), the request is filtered out of
allianceRequests
outgoingAllianceRequests()
simply returns the remaining items, which are all still “pending”Because of this, the extra
ar.status() === "pending"
check is redundant.src/core/execution/TrainStationExecution.ts (1)
1-2
: Imports look correct and minimal.Brings in
UnitType
andTrainStation
needed by the new spawn logic. No issues.src/core/game/TrainStation.ts (2)
28-34
: Confirm intent: both trainOwner and stationOwner get full trainGold().With current config, a friendly stop injects 2 × trainGold("friendly") per stop. If trainGold is meant as total-per-stop, this doubles economy. If it is per-recipient, all good. Please confirm balance and update name/docs if needed (e.g., trainGoldPerRecipient).
Also applies to: 46-53
216-220
: Trading eligibility now excludes Factory stations.Filtering to City/Port only changes reachable trade graph. If UI/AI rely on factory-as-trade-node, this is a breaking change. Validate downstream behavior.
tests/AllianceRequestExecution.test.ts (1)
37-49
: Nice coverage for reply and reciprocal flows.The “reply” and “send back” paths both look solid and readable.
src/core/configuration/DefaultConfig.ts (7)
345-354
: Relation-based trainGold looks good.Clear and typed union; values are BigInt and consistent.
366-372
: tradeShipGold: confirm behavior at 0 ports.With numPorts=0, bonus drops to 0.5× baseGold. If trade ships can only exist with ≥1 port, fine; otherwise this halves earnings unexpectedly. Verify call sites.
408-433
: costWrapper multi-type counting is sensible; watch cost growth across types.Counting the min(owned, constructed) across multiple types (e.g., Port with Factory; Factory with Port) can accelerate costs compared to previous single-type curves. Make sure this is intended and reflected in balance docs.
Also applies to: 466-476, 493-510
534-549
: costWrapper: readable and safe.The function is side-effect free and clamps nicely using min(owned, constructed).
572-574
: allianceRequestDuration addition: nice.Good central knob for diplomacy timing.
845-850
: Bot goldAdditionRate increased; ensure tests cover bot economy.A quick regression test around bot pacing would help catch unexpected snowballing.
23-23
: API Changes Fully AppliedAll call sites for the updated trainSpawnRate and trainGold signatures have been updated—no leftover boolean arguments or mismatched parameters remain.
• trainGold(rel)
– No calls withtrue
/false
were found. Both calls in src/core/game/TrainStation.ts now pass a"self" | "friendly" | "other"
union via rel(trainOwner, stationOwner).
• trainSpawnRate(numPlayerFactories)
– The call in src/core/execution/TrainStationExecution.ts passes a number (unit count) as expected.
• Configuration signatures in src/core/configuration/Config.ts and their implementations in src/core/configuration/DefaultConfig.ts match the new method shapes.No further updates are required.
src/client/Cosmetics.ts
Outdated
const patterns: Map<string, Pattern> = new Map(); | ||
const playerFlares = new Set(userMe?.player.flares); | ||
|
||
for (const name in cosmetics.patterns) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Crash when userMe is null: new Set(undefined)
throws.
Guard with a fallback empty array.
Apply this diff:
- const playerFlares = new Set(userMe?.player.flares);
+ const playerFlares = new Set(userMe?.player.flares ?? []);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const patterns: Map<string, Pattern> = new Map(); | |
const playerFlares = new Set(userMe?.player.flares); | |
for (const name in cosmetics.patterns) { | |
const patterns: Map<string, Pattern> = new Map(); | |
const playerFlares = new Set(userMe?.player.flares ?? []); | |
for (const name in cosmetics.patterns) { |
🤖 Prompt for AI Agents
In src/client/Cosmetics.ts around lines 18 to 21, constructing new
Set(userMe?.player.flares) can throw when userMe or player.flares is
null/undefined; change to use a safe fallback (e.g. new
Set(userMe?.player?.flares ?? [])) so the set is created from an empty array
when flares are missing.
disconnectedCallback() { | ||
super.disconnectedCallback(); | ||
window.removeEventListener("flag-change", this.updateFlag as EventListener); | ||
window.addEventListener("flag-change", this.updateFlag as EventListener); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: re-adding the listener in disconnectedCallback leaks handlers and duplicates updates.
disconnectedCallback
should only remove listeners.
Apply this diff:
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("flag-change", this.updateFlag as EventListener);
- window.addEventListener("flag-change", this.updateFlag as EventListener);
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
disconnectedCallback() { | |
super.disconnectedCallback(); | |
window.removeEventListener("flag-change", this.updateFlag as EventListener); | |
window.addEventListener("flag-change", this.updateFlag as EventListener); | |
} | |
disconnectedCallback() { | |
super.disconnectedCallback(); | |
window.removeEventListener("flag-change", this.updateFlag as EventListener); | |
} |
🤖 Prompt for AI Agents
In src/client/FlagInput.ts around lines 61-65, disconnectedCallback currently
removes the listener then immediately re-adds it, causing event handler leaks
and duplicate updates; modify the method so it only removes the listener (delete
the window.addEventListener("flag-change", this.updateFlag as EventListener)
line) and keep the existing window.removeEventListener call using the same
this.updateFlag reference so the handler is correctly detached.
src/client/TerritoryPatternsModal.ts
Outdated
public async open() { | ||
this.isActive = true; | ||
this.requestUpdate(); | ||
await this.refresh(); | ||
window.addEventListener("keydown", this.handleKeyDown); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Don’t auto-open the modal from refresh()
; it causes flicker and unintended opens.
refresh()
calls this.modalEl.open()
unconditionally. It runs in open()
, selectPattern()
, and onUserMe()
. This re-opens the modal when selecting a pattern and when user data loads, causing UX glitches.
- Make
refresh(openModal = false)
and only open the modal when explicitly requested. - Call
await this.refresh(true)
fromopen()
. - Call
await this.refresh(false)
fromonUserMe()
andselectPattern()
.
Apply these diffs:
- public async open() {
+ public async open() {
this.isActive = true;
- await this.refresh();
+ await this.refresh(true);
window.addEventListener("keydown", this.handleKeyDown);
}
- private selectPattern(pattern: Pattern | undefined) {
+ private async selectPattern(pattern: Pattern | undefined) {
this.userSettings.setSelectedPatternName(pattern?.name);
this.selectedPattern = pattern;
- this.refresh();
+ await this.refresh(false);
this.close();
}
- public async refresh() {
+ public async refresh(openModal = false) {
const preview = this.renderPatternPreview(
this.selectedPattern?.pattern,
48,
48,
);
this.requestUpdate();
// Wait for the DOM to be updated and the o-modal element to be available
await this.updateComplete;
- // Now modalEl should be available
- if (this.modalEl) {
- this.modalEl.open();
- } else {
- console.warn("modalEl is still null after updateComplete");
- }
+ if (openModal) {
+ if (this.modalEl) {
+ this.modalEl.open();
+ } else {
+ console.warn("modalEl is still null after updateComplete");
+ }
+ }
if (this.previewButton === null) return;
render(preview, this.previewButton);
this.requestUpdate();
}
async onUserMe(userMeResponse: UserMeResponse | null) {
if (userMeResponse === null) {
this.userSettings.setSelectedPatternName(undefined);
this.selectedPattern = undefined;
}
this.patterns = await patterns(userMeResponse);
const storedPatternName = this.userSettings.getSelectedPatternName();
if (storedPatternName) {
this.selectedPattern = this.patterns.get(storedPatternName);
}
- this.refresh();
+ await this.refresh(false);
}
Also applies to: 246-251, 305-325, 51-62
🤖 Prompt for AI Agents
In src/client/TerritoryPatternsModal.ts around lines 51-62, 234-238, 246-251 and
305-325, change refresh() to accept a boolean flag (e.g., refresh(openModal =
false)) and remove the unconditional this.modalEl.open() so the modal is only
opened when openModal is true; then update calls accordingly: in open() call
await this.refresh(true) and in onUserMe() and selectPattern() call await
this.refresh(false) (or just await this.refresh()), and ensure any other
internal calls use the default false unless they explicitly should open the
modal.
// Probability of trade ship spawn = 1 / tradeShipSpawnRate | ||
tradeShipSpawnRate(numTradeShips: number, numPlayerPorts: number): number { | ||
// Geometric mean of base spawn rate and port multiplier | ||
const combined = Math.sqrt( | ||
this.tradeShipBaseSpawn(numTradeShips) * | ||
this.tradeShipPortMultiplier(numPlayerPorts), | ||
); | ||
|
||
let totalMultiplier = 1; | ||
for (let i = 0; i < numPorts; i++) { | ||
totalMultiplier += basePortBonus * Math.pow(diminishingFactor, i); | ||
} | ||
return Math.floor(12 / combined); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Guard against divide-by-zero and revisit port multiplier wiring.
- For large numTradeShips, tradeShipBaseSpawn() can be exactly 0 (sigmoid→1), making combined=0 and rate=Infinity. Clamp to a small epsilon to avoid Infinity.
- The comment says “expected spawn rate ∝ numPlayerPorts × multiplier,” but you only multiply by multiplier. As ports increase, multiplier decreases, which increases rate (since rate = 12 / combined) and lowers spawn probability — opposite of intuition. Consider incorporating a saturating portFactor = numPlayerPorts × multiplier into combined.
Minimal fix (safety):
- const combined = Math.sqrt(
- this.tradeShipBaseSpawn(numTradeShips) *
- this.tradeShipPortMultiplier(numPlayerPorts),
- );
-
- return Math.floor(12 / combined);
+ const base = this.tradeShipBaseSpawn(numTradeShips);
+ const mult = this.tradeShipPortMultiplier(numPlayerPorts);
+ let combined = Math.sqrt(base * mult);
+ combined = Math.max(combined, 1e-6); // avoid division by 0/Infinity
+ return Math.floor(12 / combined);
Optional follow-up (make more ports never reduce spawn probability):
- const combined = Math.sqrt(
- this.tradeShipBaseSpawn(numTradeShips) *
- this.tradeShipPortMultiplier(numPlayerPorts),
- );
+ const base = this.tradeShipBaseSpawn(numTradeShips);
+ const portFactor = numPlayerPorts * this.tradeShipPortMultiplier(numPlayerPorts); // increases and saturates
+ let combined = Math.sqrt(base * (1 + portFactor));
To validate monotonicity quickly:
Also applies to: 385-396
🏁 Script executed:
#!/bin/bash
# Explore tradeShipSpawnRate monotonicity vs ports and ships
python - << 'PY'
import math
def sigmoid(value, decayRate, midpoint):
return 1/(1+math.exp(-decayRate*(value-midpoint)))
def base(numTradeShips):
return 1 - sigmoid(numTradeShips, math.log(2)/30, 100)
def mult(numPorts):
return 1/(1 + (1/10)*numPorts)
def rate(numShips, numPorts):
combined = math.sqrt(max(base(numShips)*mult(numPorts), 1e-12))
return math.floor(12/combined)
for ships in (0, 25, 50, 100, 200):
row = [ships]
for ports in (0,1,2,5,10,20,50):
row.append(rate(ships, ports))
print("ships=",ships," rates=",row[1:],"(ports=[0,1,2,5,10,20,50])")
PY
Length of output: 381
Guard against divide-by-zero and correct port multiplier behavior in tradeShipSpawnRate
The current implementation can underflow to zero (when base * mult
becomes 0), causing tradeShipSpawnRate
to return Infinity
. It also inverts the intended port effect: as you add ports, the spawn rate increases (spawn probability decreases).
• File: src/core/configuration/DefaultConfig.ts
, method tradeShipSpawnRate
– Clamp combined
to a small ε before dividing
– Adjust multiplier logic if you want more ports to raise spawn probability
Minimal fix (prevent Infinity):
@@ src/core/configuration/DefaultConfig.ts:374
- const combined = Math.sqrt(
- this.tradeShipBaseSpawn(numTradeShips) *
- this.tradeShipPortMultiplier(numPlayerPorts),
- );
- return Math.floor(12 / combined);
+ const base = this.tradeShipBaseSpawn(numTradeShips);
+ const mult = this.tradeShipPortMultiplier(numPlayerPorts);
+ let combined = Math.sqrt(base * mult);
+ combined = Math.max(combined, 1e-6); // avoid division by zero/Infinity
+ return Math.floor(12 / combined);
Optional refactor (make more ports boost spawn probability):
+ const portFactor = numPlayerPorts * this.tradeShipPortMultiplier(numPlayerPorts);
+ combined = Math.sqrt(base * (1 + portFactor));
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Probability of trade ship spawn = 1 / tradeShipSpawnRate | |
tradeShipSpawnRate(numTradeShips: number, numPlayerPorts: number): number { | |
// Geometric mean of base spawn rate and port multiplier | |
const combined = Math.sqrt( | |
this.tradeShipBaseSpawn(numTradeShips) * | |
this.tradeShipPortMultiplier(numPlayerPorts), | |
); | |
let totalMultiplier = 1; | |
for (let i = 0; i < numPorts; i++) { | |
totalMultiplier += basePortBonus * Math.pow(diminishingFactor, i); | |
} | |
return Math.floor(12 / combined); | |
} | |
// Probability of trade ship spawn = 1 / tradeShipSpawnRate | |
tradeShipSpawnRate(numTradeShips: number, numPlayerPorts: number): number { | |
// Geometric mean of base spawn rate and port multiplier | |
const base = this.tradeShipBaseSpawn(numTradeShips); | |
const mult = this.tradeShipPortMultiplier(numPlayerPorts); | |
let combined = Math.sqrt(base * mult); | |
combined = Math.max(combined, 1e-6); // avoid division by zero/Infinity | |
return Math.floor(12 / combined); | |
} |
🤖 Prompt for AI Agents
In src/core/configuration/DefaultConfig.ts around lines 374 to 383, the computed
combined value can underflow to zero and produce Infinity on the division, and
the current port multiplier inverts the intended effect (more ports should
increase spawn probability); fix this by clamping combined to a small epsilon
before dividing (e.g., combined = Math.max(combined, 1e-6)) and update the port
multiplier logic so it increases spawn probability as numPlayerPorts grows (for
example make tradeShipPortMultiplier return a value >=1 that grows with
numPlayerPorts such as 1 + k * numPlayerPorts or return the reciprocal if
current implementation decreases with ports), then compute spawn rate using the
clamped combined value.
private active = true; | ||
private recipient: Player | null = null; | ||
private mg: Game; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix strict property initialization for mg.
Mark mg as definitely assigned to satisfy TS when strictPropertyInitialization is on.
- private mg: Game;
+ private mg!: Game;
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
private mg: Game; | |
private mg!: Game; |
🤖 Prompt for AI Agents
In src/core/execution/alliance/AllianceRequestExecution.ts around line 12, the
property "mg" lacks a definite assignment and fails TS
strictPropertyInitialization; mark it as definitely assigned by changing the
declaration to use the definite assignment assertion (e.g., declare it as
private mg!: Game;) or ensure it is assigned in the class constructor before any
use (assign this.mg = <Game instance>), so the compiler knows it will be
initialized.
if (!mg.hasPlayer(this.recipientID)) { | ||
console.warn( | ||
`AllianceRequestExecution recipient ${this.recipientID} not found`, | ||
); | ||
this.active = false; | ||
return; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not linger when recipient is missing; deactivate immediately.
Otherwise this execution stays active until duration elapses, wasting ticks.
Apply this diff:
if (!mg.hasPlayer(this.recipientID)) {
console.warn(
`AllianceRequestExecution recipient ${this.recipientID} not found`,
);
- return;
+ this.active = false;
+ return;
}
🤖 Prompt for AI Agents
In src/core/execution/alliance/AllianceRequestExecution.ts around lines 21-27,
the execution currently just logs and returns when the recipient is missing, but
it should be deactivated immediately to avoid wasting ticks; update the branch
so that after detecting !mg.hasPlayer(this.recipientID) you call the execution
deactivation routine (e.g., this.deactivate() or the project’s canonical method
to mark executions finished/removed from the manager) and then return, ensuring
the execution is no longer active and will not consume further ticks.
const incoming = recipient | ||
.outgoingAllianceRequests() | ||
.find((r) => r.recipient() === this.requestor); | ||
if (incoming) { | ||
// If the recipient already has pending alliance request, | ||
// then accept it instead of creating a new one. | ||
this.active = false; | ||
incoming.accept(); | ||
} else { | ||
this.req = this.requestor.createAllianceRequest(recipient); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Handle createAllianceRequest() possibly returning null.
When it returns null, this exec will idle until expiry. Fail fast and log.
- } else {
- this.req = this.requestor.createAllianceRequest(recipient);
- }
+ } else {
+ this.req = this.requestor.createAllianceRequest(recipient);
+ if (this.req === null) {
+ console.warn("createAllianceRequest returned null");
+ this.active = false;
+ }
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const incoming = recipient | |
.outgoingAllianceRequests() | |
.find((r) => r.recipient() === this.requestor); | |
if (incoming) { | |
// If the recipient already has pending alliance request, | |
// then accept it instead of creating a new one. | |
this.active = false; | |
incoming.accept(); | |
} else { | |
this.req = this.requestor.createAllianceRequest(recipient); | |
} | |
} | |
const incoming = recipient | |
.outgoingAllianceRequests() | |
.find((r) => r.recipient() === this.requestor); | |
if (incoming) { | |
// If the recipient already has pending alliance request, | |
// then accept it instead of creating a new one. | |
this.active = false; | |
incoming.accept(); | |
} else { | |
this.req = this.requestor.createAllianceRequest(recipient); | |
if (this.req === null) { | |
console.warn("createAllianceRequest returned null"); | |
this.active = false; | |
} | |
} | |
} |
🤖 Prompt for AI Agents
In src/core/execution/alliance/AllianceRequestExecution.ts around lines 34 to
45, createAllianceRequest(...) may return null which currently leaves this
execution idling until expiry; update the code to handle a null return
immediately by logging an error with context (requestor and recipient ids),
marking this.active = false and setting an explicit failure state (or throwing a
controlled error) instead of assigning null to this.req so the exec fails fast
and does not hang.
// If the recipient already has pending alliance request, | ||
// then accept it instead of creating a new one. | ||
this.active = false; | ||
incoming.accept(); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Keep relation bumps consistent on auto-accept path.
AllianceRequestReplyExecution adds +100 relation on accept; the auto-accept path here does not. Align behaviors to avoid divergent diplomacy states.
if (incoming) {
// If the recipient already has pending alliance request,
// then accept it instead of creating a new one.
this.active = false;
- incoming.accept();
+ incoming.accept();
+ this.requestor.updateRelation(recipient, 100);
+ recipient.updateRelation(this.requestor, 100);
} else {
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// If the recipient already has pending alliance request, | |
// then accept it instead of creating a new one. | |
this.active = false; | |
incoming.accept(); | |
} else { | |
// If the recipient already has pending alliance request, | |
// then accept it instead of creating a new one. | |
this.active = false; | |
incoming.accept(); | |
this.requestor.updateRelation(recipient, 100); | |
recipient.updateRelation(this.requestor, 100); | |
} else { |
🤖 Prompt for AI Agents
In src/core/execution/alliance/AllianceRequestExecution.ts around lines 38–42
the auto-accept branch marks the request inactive and calls incoming.accept()
but does not apply the +100 relation bump that AllianceRequestReplyExecution
applies on accept; update this auto-accept path to call the same relation-bump
logic (or the shared helper) used by AllianceRequestReplyExecution so the +100
relation is applied, preserve this.active = false and incoming.accept(), and
ensure any persistence/events triggered by the reply path are also executed so
diplomacy state remains consistent.
src/core/game/AllianceRequestImpl.ts
Outdated
accept(): void { | ||
this.status_ = "accepted"; | ||
this.game.acceptAllianceRequest(this); | ||
} | ||
reject(): void { | ||
this.status_ = "rejected"; | ||
this.game.rejectAllianceRequest(this); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Make accept/reject idempotent to avoid double-processing.
Right now, repeated calls re-invoke game handlers. Guarding on "pending"
prevents accidental re-entry and aligns with a simple state machine.
Apply this diff:
accept(): void {
- this.status_ = "accepted";
- this.game.acceptAllianceRequest(this);
+ if (this.status_ !== "pending") return;
+ this.status_ = "accepted";
+ this.game.acceptAllianceRequest(this);
}
reject(): void {
- this.status_ = "rejected";
- this.game.rejectAllianceRequest(this);
+ if (this.status_ !== "pending") return;
+ this.status_ = "rejected";
+ this.game.rejectAllianceRequest(this);
}
I can also add a tiny unit test to assert idempotency on accept/reject.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
accept(): void { | |
this.status_ = "accepted"; | |
this.game.acceptAllianceRequest(this); | |
} | |
reject(): void { | |
this.status_ = "rejected"; | |
this.game.rejectAllianceRequest(this); | |
} | |
accept(): void { | |
if (this.status_ !== "pending") return; | |
this.status_ = "accepted"; | |
this.game.acceptAllianceRequest(this); | |
} | |
reject(): void { | |
if (this.status_ !== "pending") return; | |
this.status_ = "rejected"; | |
this.game.rejectAllianceRequest(this); | |
} |
🤖 Prompt for AI Agents
In src/core/game/AllianceRequestImpl.ts around lines 31 to 38, accept() and
reject() are not idempotent and re-invoke game handlers on repeated calls;
modify both methods to first check that this.status_ === "pending" and only then
set the status_ ("accepted" or "rejected") and call
this.game.acceptAllianceRequest(this) / this.game.rejectAllianceRequest(this),
otherwise return/do nothing so repeated calls have no effect.
let pattern: string | undefined; | ||
// Check if the pattern is allowed | ||
if (clientMsg.pattern !== undefined) { | ||
const allowed = privilegeRefresher | ||
if (clientMsg.patternName !== undefined) { | ||
const result = privilegeRefresher | ||
.get() | ||
.isPatternAllowed(clientMsg.pattern, flares); | ||
if (allowed !== true) { | ||
log.warn(`Pattern ${allowed}: ${clientMsg.pattern}`); | ||
return { | ||
code: 1002, | ||
error: `The pattern you have selected is ${allowed}.`, | ||
reason: `Pattern ${allowed}`, | ||
success: false, | ||
}; | ||
.isPatternAllowed(clientMsg.patternName, flares); | ||
switch (result.type) { | ||
case "allowed": { | ||
const { pattern: resultPattern } = result; | ||
pattern = resultPattern; | ||
break; | ||
} | ||
case "unknown": | ||
// Api could be down, so allow player to join but disable pattern. | ||
log.warn(`Pattern ${clientMsg.patternName} unknown`); | ||
ws.close( | ||
1002, | ||
"Could not look up pattern, backend may be offline", | ||
); | ||
return; | ||
case "forbidden": | ||
log.warn(`Pattern ${clientMsg.patternName}: ${result.reason}`); | ||
ws.close( | ||
1002, | ||
`Pattern ${clientMsg.patternName}: ${result.reason}`, | ||
); | ||
return; | ||
default: | ||
assertNever(result); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pattern handling flow: unknown path closes the socket; mismatch with comment and inconsistent error contract.
- The comment says “allow player to join but disable pattern,” yet the code closes the connection on “unknown”.
- Elsewhere in this handler you surface join failures by returning a { success:false, code, reason, error } object; here you bypass that and call ws.close() directly. This breaks observability and consistency.
- Recommended: fail-open on “unknown” (let join continue with no pattern) and map “forbidden” cases to the existing reason enums via a returned failure, not a direct close.
Apply:
- const result = privilegeRefresher
+ const check = privilegeRefresher
.get()
- .isPatternAllowed(clientMsg.patternName, flares);
- switch (result.type) {
+ .isPatternAllowed(clientMsg.patternName, flares);
+ switch (check.type) {
case "allowed": {
- const { pattern: resultPattern } = result;
+ const { pattern: resultPattern } = check;
pattern = resultPattern;
break;
}
- case "unknown":
- // Api could be down, so allow player to join but disable pattern.
- log.warn(`Pattern ${clientMsg.patternName} unknown`);
- ws.close(
- 1002,
- "Could not look up pattern, backend may be offline",
- );
- return;
- case "forbidden":
- log.warn(`Pattern ${clientMsg.patternName}: ${result.reason}`);
- ws.close(
- 1002,
- `Pattern ${clientMsg.patternName}: ${result.reason}`,
- );
- return;
+ case "unknown": {
+ // Fail-open: let player in without a pattern.
+ log.warn(`Pattern ${clientMsg.patternName} unknown; allowing join without pattern`);
+ pattern = undefined;
+ break;
+ }
+ case "forbidden": {
+ log.warn(`Pattern ${clientMsg.patternName}: ${check.reason}`);
+ const reason =
+ check.reason === "invalid pattern"
+ ? "Pattern invalid"
+ : check.reason === "pattern not found"
+ ? "Pattern unlisted"
+ : "Pattern restricted";
+ return {
+ success: false,
+ code: 1002,
+ reason,
+ error: `Pattern ${clientMsg.patternName}: ${check.reason}`,
+ };
+ }
default:
- assertNever(result);
+ assertNever(check);
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
let pattern: string | undefined; | |
// Check if the pattern is allowed | |
if (clientMsg.pattern !== undefined) { | |
const allowed = privilegeRefresher | |
if (clientMsg.patternName !== undefined) { | |
const result = privilegeRefresher | |
.get() | |
.isPatternAllowed(clientMsg.pattern, flares); | |
if (allowed !== true) { | |
log.warn(`Pattern ${allowed}: ${clientMsg.pattern}`); | |
return { | |
code: 1002, | |
error: `The pattern you have selected is ${allowed}.`, | |
reason: `Pattern ${allowed}`, | |
success: false, | |
}; | |
.isPatternAllowed(clientMsg.patternName, flares); | |
switch (result.type) { | |
case "allowed": { | |
const { pattern: resultPattern } = result; | |
pattern = resultPattern; | |
break; | |
} | |
case "unknown": | |
// Api could be down, so allow player to join but disable pattern. | |
log.warn(`Pattern ${clientMsg.patternName} unknown`); | |
ws.close( | |
1002, | |
"Could not look up pattern, backend may be offline", | |
); | |
return; | |
case "forbidden": | |
log.warn(`Pattern ${clientMsg.patternName}: ${result.reason}`); | |
ws.close( | |
1002, | |
`Pattern ${clientMsg.patternName}: ${result.reason}`, | |
); | |
return; | |
default: | |
assertNever(result); | |
} | |
let pattern: string | undefined; | |
// Check if the pattern is allowed | |
if (clientMsg.patternName !== undefined) { | |
const check = privilegeRefresher | |
.get() | |
.isPatternAllowed(clientMsg.patternName, flares); | |
switch (check.type) { | |
case "allowed": { | |
const { pattern: resultPattern } = check; | |
pattern = resultPattern; | |
break; | |
} | |
case "unknown": { | |
// Fail-open: let player in without a pattern. | |
log.warn(`Pattern ${clientMsg.patternName} unknown; allowing join without pattern`); | |
pattern = undefined; | |
break; | |
} | |
case "forbidden": { | |
log.warn(`Pattern ${clientMsg.patternName}: ${check.reason}`); | |
const reason = | |
check.reason === "invalid pattern" | |
? "Pattern invalid" | |
: check.reason === "pattern not found" | |
? "Pattern unlisted" | |
: "Pattern restricted"; | |
return { | |
success: false, | |
code: 1002, | |
reason, | |
error: `Pattern ${clientMsg.patternName}: ${check.reason}`, | |
}; | |
} | |
default: | |
assertNever(check); | |
} | |
} |
|
Description:
Merge v25 into main
Please complete the following:
Please put your Discord username so you can be contacted if a bug or regression is found:
evan