mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:00:43 +00:00
restart_test
251 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
741a38c62b | meta: only penalize train gold after 5 cities instead of 3 | ||
|
|
3e65d08942 |
reduce train gold after each city (#3400)
## Description: Now that cities snap to existing rails, it's possible to line up dozens of cities in a row, producing way too much gold. This PR reduces the gold after each stop to prevent that. Gold only starts decreasing after the 3rd city. ## 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 |
||
|
|
3838de1d30 |
Option to disable alliances + 2 new modifiers for variety 😄 (#3392)
## Description:
Rex had this idea: "It would be funny to have an option in private
lobbies to disable alliances."
I added it as an option.
Now people can choose to live in constant fear of their neighbors 😆
Also added two new public game modifiers for variety (only for the
special rotation):
- Alliances disabled (low probability)
- x2 gold multiplier (low probability)
Would be nice to squeeze this into v30, last minute?
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
|
||
|
|
936928fed9 |
Enhance InputHandler to allow using NumPad (#3317)
## Description: Adds **Enter** and **Numpad Enter** as confirmation for placing a ghost structure after selecting a building with hotkeys (1–0 or numpad). Players can cancel with Esc but previously had to click to confirm; they can now confirm with Enter or Numpad Enter at the current cursor position. This supports keyboard-only or mouse + numpad workflows (e.g. one hand on numpad for select + confirm, one on mouse for aiming). ## 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: .wozniakpl |
||
|
|
e137fcaa6c |
Fix/Perf/Refactor: playerActions and buildableUnits, their callers and related types (#3220)
## Description: TL;DR: it's faster. buildableUnits is called via PlayerView.actions from UnitDisplay (each tick without TileRef), BuildMenu (each tick when open), MainRadialMenu (each tick when open), PlayerPanel (each tick when open), StructureIconsLayer (when placing a building from build bar), NukeTrajectoryPreviewLayer (when placing nuke, on tick when tile changes), ClientGameRunner (on click to attack/auto-boat or hotkey B or G). After https://github.com/openfrontio/OpenFrontIO/pull/3213 got merged, the change with largest impact in https://github.com/openfrontio/OpenFrontIO/pull/3193 was done in such a different way that a new PR was needed The idea in 3193 was to not always ask for Transport Ship from buildableUnits. In such a way that very little extra data was send to the worker. This had the biggest impact on performance (the idea was months older btw, see https://github.com/openfrontio/OpenFrontIO/pull/2295). Now, we do it the other way around, by telling buildableUnits all unit types we want. Or we want them all (undefined). The downside is more data is send in the worker message. The upside is we have more options and can add more in this PR. This PR implements some of the leftovers in 3193 on top of 3213 and adds further improvements. (Some unrelated refactor/perf changes where moved out of this PR and into already merged https://github.com/openfrontio/OpenFrontIO/pull/3233, https://github.com/openfrontio/OpenFrontIO/pull/3234, https://github.com/openfrontio/OpenFrontIO/pull/3235, https://github.com/openfrontio/OpenFrontIO/pull/3236, https://github.com/openfrontio/OpenFrontIO/pull/3237, https://github.com/openfrontio/OpenFrontIO/pull/3238, https://github.com/openfrontio/OpenFrontIO/pull/3239) - **GameRunner**, **WorkerMessages**: _playerActions_ and _PlayerActionsMessage ._ Option to ask for no buildable units (null). It now has 3 modes: get all actions and all buildings (units undefined), get all actions and no buildings (units null), or get all actions and specific building (units contains Unit Types). - **GameRunner**: _playerActions_. fixes wrong assumption in PR 3213: that only if units was undefined, we have to know canAttack. ClientGameRunner wants to know both, in case of a click on non-bordering land, to decide if it should auto-boat using a Transport Ship. So units is not undefined (we only ask for Transport Ship now which has a positive effect on performance for each click/tap) but we need canAttack still. Solved by removing the unit === undefined check before _canAttack_ in _playerActions_. - **GameRunner**, **GameView**, **WorkerClient**, **WorkerMessages**, **Worker.worker**: added _playerBuildables_ / _buildables_ next to existing _playerActions_ / _actions_. With above solved, there was still no option to only get buildable units when the actions are not needed. While **StructureIconsLayer**, **NukeTrajectoryPreviewLayer**, **BuildMenu** and **UnitDisplay** need only that. To not make playerActions more convoluted with more params or so, i've added a new function _playerBuildables_ in **GameView** to only get buildable units (**GameRunner** _playerBuildables_). _playerBuildables_ has 2 modes: get all buildings (units undefined) or get specific buildings (units contains Unit Types). Also update some comments that mentioned .actions in **NukeTrajectoryPreviewLayer**. - **ClientGameRunner**, **PlayerPanel**, **BuildMenu**, **UnitDisplay**, **StructureIconsLayer** and **NukeTrajectoryPreviewLayer**: Since PR 3213, **StructureIconsLayer** and **NukeTrajectoryPreviewLayer** ask for specific types of units from **GameView** _actions_ (**GameRunner** playerActions). Now have the other files do the same. For example **BuildMenu** asks for the new _BuildMenuTypes_ when it calls ._buildables_ and **ClientGameRunner** asks for UnitType.TransportShip when sending a boat - **ClientGameRunner**: canBoatAttack now accepts BuildableUnit[] instead of PlayerActions so we can send it either actions.buildableUnits or just buildables. Have functions call myPlayer.buildables(tileRef, [UnitType.TransportShip]) when we only need a buildable unit and no actions. Or myPlayer.actions(tileRef, null) when we need actions but no buildable units. Or myPlayer.actions(tileRef, [UnitType.TransportShip]) when we need both actions, like canAttack, and a buildable unit. Then if needed send either actions.buildableUnits or buildables to to _canAutoBoat_ / _canBoatAttack_. - **MainRadialMenu**: needs all player buildable unit types including Transport Ship, so the _actions_ call argument for unit types can stay undefined (unchanged) there. - **MainRadialMenu**: now that **BuildMenu** uses _playerBuildables_ instead of _playerActions_, we must put data in _this.buildMenu.playerBuildables_. And since we're not putting the (unneeded) full _actions_ in there anymore, we can now put only the needed and expected _actions._buildableUnits_ in it. - **Game**, **PlayerImpl**, **StructureIconsLayer**: Typesafety and some added perf: new type _PlayerBuildableUnitType_ (see also the below point for how it is formed). So callers of _buildableUnits_ can never ask for the wrong type like e.g. UnitType.Train because it doesn't return data for that type. This type is now used in **PlayerImpl**, **BuildMenu**, **RadialMenuElements**, **StructureDrawingUtils** and **UnitDisplay** for that reason. And **InputHandler**, **StructureIconsLayer** and **UIState** (little more on that in point below). - **InputHandler**, **StructureIconsLayer**, **UIState**: In order to make type safety work for GhostUnit.buildableUnit.type too (line ~217 of StructureIconsLayer), changed type of interface _BuildableUnit_ to _PlayerBuildableType_. Which is only more accurate. Same for and this.structures and uiState.ghostStructure and with the latter, _renderUnitItem_ in **UnitDisplay** and _setGhostStructure_ in **InputHandler**. All Structures are of PlayerBuildableType (there are even some in PlayerBuildables that aren't Structures, but it is much more confined than UnitType). - **Game**: Typesafety and some added perf: added _BuildMenus_ and _BuildableAttacks_ in the same fashion that the existing StructureTypes was already used (simplified it a bit too, with it renamed _StructureTypes_ to _Structures_ and removed _isStructureType_). They can be used with .types or .has(). _BuildableAttacks_.has() is used in **RadialMenuElements**. _BuildableAttacks_ and existing _Structures_ now make up _BuildMenus_. Which is used in **BuildMenu**, **StructureIconsLayer** and **UnitDisplay**. Then _BuildMenus_ together with UnitType.TransportShip make up the _PlayerBuildables_. Which is used in **PlayerImpl** _buildableUnits_ (see point below). And with _PlayerBuildableUnits_ we get the new _PlayerBuildableUnitType_ (see above point on Game / PlayerImpl). - **RadialMenuElements**: replace non-central ATTACK_UNIT_TYPES in **RadialMenuElements** with centralized _BuildableAttackTypes_ too. Use _PlayerBuildableUnitType_ for more type safety (can't by mistake add UnitType.Train to its build menu). Make use of _BuildableAttackTypes_ instead of adding items hardcoded line by line in _getAllEnabledUnits_, just like we already did since PR 3239 with _StructureTypes_. And use _BuildableAttacks.types_ in the same fashion that existing _isStructureTypes_ (now Structures.types) was already used elsewhere. - **PlayerImpl**: _buildableUnits_ -- would do Object.values(UnitTypes) on every call. Now for better perf directly loop over player buildable units by using _PlayerBuildables_ (see above point). In this way we also exclude MIRVWarhead, TradeShip, Train, SamMissile and Shell so there are less unit types to loop through by default. Since a player doesn't build those by themselves, they are only build by Executions which use _canBuild_ directly and not _buildableUnits_. -- for more performance, do for loop instead of using .map and .filter, no intermediate array needed nor callback overhead. We just loop over the given units (which if undefined will contain _PlayerBuildables_). Also pre-allocate the results array to get the most out of it, even if V8 might already be very good at this. -- cache config, railNetwork and inSpawnPhase so they can be re-used inside the for loop. -- cache cost inside the loop -- it would check twice for tile!==null to decide to call findUnitToUpgrade and canBuild. Now once. -- eliminated double/triple checks for the same thing. It called _findUnitToUpgrade_ (and with that _canUpgradeUnit_) and then _canBuild_ which both check if player has enough gold for the cost of the unit type. And they both check if the unit type is disabled. Now we call private functions _canBuildUnitType_, _canUpgradeUnitType_ to first do checks on unit type level for early returns, and _findExistingUnitToUpgrade_ to find existing unit without doing anything extra. in a specific order to check everything only once. The public functions _findUnitToUpgrade_ and _canBuild_ have an unchanged functionality and we don't call them from _buildableUnits_ anymore. -- would get _overlappingRailRoads_ and _computeGhostRailPaths_ when canBuild was true. But this data is only meant for **StructureIconsLayer** and it logically only uses it when placing a new unit, not when upgrading one. Which is also commented on line 351 of **StructureIconsLayer**. So, we now only get overlapping railroads and ghost rails if we're not hovering to upgrade an existing unit. - **PlayerImpl**: _findUnitToUpgrade_: unchanged functionality, but have it call new private function _findExistingUnitToUpgrade_ to find existing unit. - **PlayerImpl**: _canBuild_: unchanged functionality, but have it call new private function _canBuildUnitType_ to do the checks it first did itself. And then new private function _canSpawnUnitType_ for the rest of the checks. This way we can call _canBuildUnitType_ and _canSpawnUnitType_ from _buildableUnits_ in a specific order to prevent double/triple checks. - **PlayerImpl**: _canBuildUnitType_: new private function to be shared by _buildableUnits_, _canBuild_ and _canUpgradeUnit_ to be able do unit type level checks in a specific order to prevent double/triple checks. Via parameter knownCost, _buildableUnits_ can send it the cost it already fetched so that it doesn't have to be fetched again. For caller _canUpgradeUnit_, the isAlive() check (which was previously only done in canBuild) is new but harmless, maybe even better to have also check isAlive() on upgrade now that Nations are also upgrading which might prevent some edge case bugs. - **PlayerImpl**: _canUpgradeUnitType_: new private function to be shared by _buildableUnits_ and _canUpgradeUnit_ to be able do unit type level checks in a specific order to prevent double/triple checks. - **PlayerImpl**: _canSpawnUnitType_: new private function to be shared by _buildableUnits_ and _canBuildUnit_ to be able do unit type level checks in a specific order to prevent double/triple checks. - **PlayerImpl**: _findExistingUnitToUpgrade_: new private function to be shared by _buildableUnits_ and _findUnitToUpgrade_ to be able do unit level checks in a specific order to prevent double/triple checks. - **PlayerImpl**: _isUnitValidToUpgrade_: new private function to be shared by _buildableUnits_ and _canUpgradeUnit_ to be able do unit level checks in a specific order to prevent double/triple checks. - **PlayerImpl.test.ts**: because of the isAlive() check in which is new for _canUpgradeUnit_ (see above at _canBuildUnitType_), the tests needed to have the players be alive at the start, in order to pass. - **BuildMenu**: use .find instead of .filter in canBuildOrUpgrade, a function we already needed to change. This is faster and prevents an allocation. **PERFORMANCE** As for calling ._buildables_ instead of unnecessarily getting ._actions_, there is an obvious win because there's less to send calculate and recieve. Also asking for only the needed buildings helps a lot (especially if TradeShip isn't needed, see the difference in benchmark in original #3193). But the real-world impact is hard to measure. gave it a try in #3193 and those results should be even better now. Now testing only _buildableUnits_ performance in a synthetic benchmark, we get these results. This is after other performance improvments so the base is already better than it was in original #3193: **BEFORE** (only buildableUnits itself) <img width="602" height="96" alt="image" src="https://github.com/user-attachments/assets/7770c0fa-a35e-42fc-90de-1de83242ec23" /> **AFTER** (only buildableUnits itself) <img width="603" height="91" alt="image" src="https://github.com/user-attachments/assets/a1578382-7010-4160-937c-7117bad18beb" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
0b9d43cb46 |
Configurable nation count 🤖 (#3338)
## Description: I hope we can get this into v30? The nation count is configurable now, just like the bot count. Replaced the "Disable Nations" toggle with a nations slider (0–400) in SinglePlayer and Host Lobby modals. <img width="710" height="121" alt="Screenshot 2026-03-03 021952" src="https://github.com/user-attachments/assets/c8d0f0c3-db51-4303-95fa-dbc770460ec2" /> Public games are staying exactly the same, this is just for singleplayer and private lobby fun. Youtubers could play HvN against 400 nations, for example. Singleplayer enjoyers no longer have to play against 1 nation in HvN, they can freely choose. `GameConfig.disableNations: boolean` got replaced by `nations: number (0-400, optional)` `undefined` = map default, `0` = disabled, number = custom count Nations slider defaults to the map's nation count, shows "(MAP DEFAULT)" label when unchanged Compact map toggle reduces nations to 25% when at default, restores when toggled off (just like we already do with bots) The nation count for HvN no longer automatically matches the human count in singleplayer and private games, only in public games. **What if there aren't enough nations configured for the map?** We just use the HvN logic (Generate random nations) ### Warning **This infra PR also needs to get merged: https://github.com/openfrontio/infra/pull/263 Otherwise players can set 0 nations and get achievements.** ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
60f69a6408 |
perf: remove O(n) StructureIconsLayer render lookups (#3305)
Begins work on #3207 ## Description: This PR is the first optimization slice for #3207: it removes O(n) render lookups in `StructureIconsLayer` by replacing array-first render state with a unit-id keyed map, and tightens hot-path execution to reduce per-tick allocations. ### What changed - Refactored render state from array-first to `rendersByUnitId: Map<number, StructureRenderInfo>`. - Replaced O(n) lookup/delete paths with O(1) `Map#get` / `Map#delete`. - Replaced `seenUnits` object-identity tracking with `seenUnitIds: Set<number>`. - Removed `tick()` array/closure chain (`map(...).forEach(...)`) and switched to index-based loop. - Reduced ghost-path allocation pressure by reusing a layer-level `Set` for connected ally IDs instead of allocating `filter` + `map` + `new Set` per ghost query. - Added dirty-flag caching for structure visibility focus (`visibilityStateDirty`) so expensive visibility-state scans recompute only when toggles change. ### Performance validation (before/after) Benchmark added: `tests/perf/StructureIconsLayerLookupPerf.ts` Command: `npm run perf` Observed result: - `StructureIconsLayer BEFORE (array O(n) lookup/delete) x 0.33 ops/sec ±13.28%` - `StructureIconsLayer AFTER (unit-id map O(1) lookup/delete) x 95.65 ops/sec ±2.46%` - Fastest implementation: AFTER (unit-id map) #### Profiler screenshots are too noisy to be useful for such a focused change ### Verification - `npx tsc --noEmit` ✅ - `npx eslint src/client/graphics/layers/StructureIconsLayer.ts tests/perf/StructureIconsLayerLookupPerf.ts` ✅ - `npm run perf` ✅ ## Please complete the following: ~~- [ ] I have added screenshots for all UI updates~~ ~~- [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file~~ - [x] I have added relevant tests to the test directory - [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: skigim |
||
|
|
f7598369ed |
refactor: consolidate platform detection across client components (#3325)
## Description: This PR consolidates ad hoc platform/environment/viewport detection into a single shared utility. It is scoped to this refactor only, and serves as groundwork for the mobile-focused feature work planned for the v31 milestone. ### What changed - Introduced a shared `Platform` utility centralising: - OS detection (with `userAgentData` + UA fallback) - Electron environment detection - Viewport breakpoint helpers (`isMobileWidth`, `isTabletWidth`, `isDesktopWidth`) - Replaced duplicated inline checks across client files with the shared API. - Normalised Mac detection to derive from the consolidated OS logic rather than a separate regex. ### Why - Multiple client files each independently ran `navigator.userAgent` regexes or copy-pasted `isElectron` logic — this unifies all of that. - Puts a stable, tested abstraction in place before v31 mobile work lands, so mobile feature branches have a consistent surface to build against. ## Please complete the following: - [x] I have added screenshots for all UI updates (N/A: refactor only, no visible UI changes) - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file (N/A: no new user-facing strings) - [x] I have added relevant tests to the test directory (N/A: refactor only) - [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: skigim |
||
|
|
417fa0fe09 |
For v30: Add new modifiers (Hard nations and 25M Starting Gold) 🙂 (#3316)
## Description: Adds two new public game modifiers for variety and improves compact map eligibility for team games. ### New Modifiers **Hard Nations (`isHardNations`)** - We need this modifier for HvN, because medium nations are easier now (will result in a much higher human winrate) - In a discord discussion we concluded that HvN should generally be easier (higher winrate than 50%, so players are less frustated) - Thats why only 20% of HvN games have the hard nations modifier (for now) - For PvPvE enjoyers, the modifier is also active in FFA games => (Only 2.5% chance, and 1 ticket in `SPECIAL_MODIFIER_POOL`) **25M Starting Gold (`startingGoldHigh`)** - Some people in the main discord wanted this modifier, and it will result in crazy games - Rare special-only modifier (1 ticket in pool); mutually exclusive with 5M starting gold via `MUTUALLY_EXCLUSIVE_MODIFIERS` - Disables nations (they lack PVP immunity, so 25M gold doesn't work well with them) - Excluded from HumansVsNations games (since it disables nations) - Spawn immunity set to **2 minutes 30 seconds** (vs 30s for 5M gold), so people can spend the gold and prepare ### Other Changes - **Improved `supportsCompactMapForTeams`**: Replaced the hard `smallest >= 50` land-tile cutoff with a per-team-config calculation that simulates worst-case compact player count and checks every team gets at least 2 players. - **HvN spawn immunity**: Always 5 seconds in both regular and special lobbies (to get rid of a confusing PVP immunity HeadsUpMessage in 5M starting gold games) - **Regular public lobby random spawn modifier probabilty**: Reduced from 10% to 5% (Because of the new modifier, so there aren't too many modifiers in non-special-lobbies, should only occur sometimes there) - Rebalanced `SPECIAL_MODIFIER_POOL` a bit ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
e1125e0c37 |
Fix: Nations reject alliance requests created pre-spawn (#3314)
## Description: This PR fixes an exploit that allows the player to request alliances to Nations, mostly in impossible mode, during spawn phase, with high chances for it to be accepted due to troop count parity. Nations now reject alliance requests during the spawn phase. `GameImpl.executeNextTick()` initializes ALL pending `unInitExecs` in one batch on the first post-spawn tick ( `numSpawnPhaseTurns() + 1` ). So every alliance request submitted during spawn phase is guaranteed to be created with `createdAt = numSpawnPhaseTurns() + 1` on the very first post-spawn tick. Therefore, we check for alliance requests created on the very first post-spawn tick and reject those. ## 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: deshack_82603 --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
802cc7f16d |
Revert "Fix: Nations reject alliance requests during spawn phase" (#3313)
## Description: Reverts openfrontio/OpenFrontIO#3312 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n |
||
|
|
a9c89e4f15 |
Fix: Nations reject alliance requests during spawn phase (#3312)
## Description: This PR fixes an exploit that allows the player to request alliances to Nations, mostly in impossible mode, during spawn phase, with high chances for it to be accepted due to troop count parity. Nations now reject alliance requests during the spawn phase. ## 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: deshack_82603 |
||
|
|
c911bfb2d8 |
Packed unit updates / MotionPlans (#3292)
## Description: Reduce per-step `Unit` update traffic by shipping packed motion plans and letting the client advance plan-driven units locally. Changes: - Add packed motion plan records (`packedMotionPlans?: Uint32Array`) to game updates and transfer the buffer worker -> main. - Introduce `src/core/game/MotionPlans.ts` (schema + pack/unpack) for grid + train motion plans. - Extend `Game` with `recordMotionPlan(...)` and `drainPackedMotionPlans()`, and implement buffering/packing in `GameImpl`. - Treat units with motion plans as “plan-driven”: suppress per-tile `Unit` updates on `move()` and advance positions client-side. - Emit motion plans from executions: - `TradeShipExecution`: record/update grid motion plans and `touch()` when changing target after capture. - `TransportShipExecution`: record initial plan and update it when destination changes. - `TrainExecution`: record a train plan on init (engine + cars). - Client: apply motion plans in `GameView` and ensure `UnitLayer` updates sprites for motion-planned units even when no `Unit` updates arrived. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME |
||
|
|
7b785ea79a |
Fix alliance renewal prompt incorrectly dismissed for both players (#3297)
## Description: NOTE: Applies to current main / beta version. Needs to be included in v30. When a player clicked "Renew Alliance", the `AllianceExtensionUpdate` broadcast caused both players' renewal prompts to be removed, even the one who hadn't yet acted. This happened because `onAllianceExtensionEvent` called `removeAllianceRenewalEvents` unconditionally on every client. This PR fixes the behavior by calling `removeAllianceRenewalEvents` only for the player that executed the action. ## 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: deshack_82603 |
||
|
|
7855e1b0e9 |
Feat: Troop transport retreats to closest owned tile v2 (#3286)
If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #1139 ## Description: New version of the #2789 PR that is cleaner after changes made to old pathfinding logic. Adds logic to troop transport retreat behaviour which retreats a transport to the closest owned tile instead of the source. Now if no shores are detected (you lost all your shoreline while the transport was out) we handle the return case same as if the original source was no longer your territory. <img width="2541" height="1593" alt="image" src="https://github.com/user-attachments/assets/4d2ff5e7-d10d-40f4-80e0-9f029cff61a2" /> ## Video example from previous PR (works the exact same way in this PR): https://github.com/user-attachments/assets/e43a3b10-e8b0-4f23-87f3-2dc4739de880 ## 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: bijx |
||
|
|
9b96b07820 |
test: add vitest-canvas-mock for local canvas support
Fixes UILayer tests failing locally due to the native canvas package not being compiled. vitest-canvas-mock provides a jsdom-compatible Canvas 2D API mock without requiring native build tools. |
||
|
|
097c42740c |
Random spawn. Avoid spawning near water. (#3009)
## Description: Fixing https://discord.com/channels/1359946986937258015/1360078040222142564/1463898386854973642 Now, if not all tiles on the spawn circle can be owned, the algorithm tries to select another random spawn tile. ## 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: nikolaj_mykola --------- Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com> |
||
|
|
6a30d2b38b |
Smarter factory placement for Nation AI 🤖 (#3244)
## Description: Introduces a dedicated `factoryValue()` scoring function for AI factory placement, replacing the generic `interiorStructureValue()` previously shared with cities and missile silos. Scoring criteria: - High elevation and spacing from other factories (unchanged from city/silo logic) - Rail connectivity: bonus per distinct rail cluster reachable within `trainStationMaxRange`, weighted by trade gold potential — allied clusters score highest (1.0), team/neutral clusters score ~0.71, own clusters ~0.29 (based on `config.tradeGold()` values). Based on difficulty - Cluster deduplication: connecting to the same cluster multiple times does not inflate the score - Embargoed and bot neighbors are excluded; all other non-embargoed neighbors are included The result is that the AI tends to place factories where they can bridge separate rail networks or connect to high-value trade partners, rather than deep in its own interior. ### EDIT Added a dedicated `cityValue()` scoring function that takes into account the connectivity score. This allows placement of cities in a "factory-aware" way, while also enforcing spreading structures (we want the network to grow, not a cluster of cities and factories all together). ## 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: deshack_82603 --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
05af154b58 |
feat(server): add health api endpoint for increased observability (#3264)
## Description: Adds an additional API endpoint to the server for health, using the master lobby service as the health metric. The master lobby service is considered healthy if the lobby service has started (i.e. it had enough ready workers to start), and the current amount of ready workers is more than half of the desired number. This means that we won't show as healthy until all the workers start, and then we will continue to show as healthy even if a few workers crash, as long as at least more than half are still running. Any less than that, and the service becomes unhealthy. This also is set to "no cache" in the nginx config. This is to ensure that any checks of the server health show the true value, and cannot show false/stale data served by nginx, cloudflare, or anything else. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: jish |
||
|
|
f09d9a3a5f |
Nations can overwhelm SAMs now 💥 (+ 3 little nation improvements) (#3246)
## Description: ### SAM Overwhelming (`NationNukeBehavior.ts`) On Impossible difficulty, nations can now destroy enemy SAMs by overwhelming them with coordinated atom bomb salvos. When no good nuke target is found (all trajectories intercepted by SAMs), the nations will: - Identify the easiest enemy SAM to destroy (lowest level first) - Calculate the total interception capacity of all covering SAMs and send enough bombs to overwhelm them (+1 extra per 5 needed to account for enemy building more SAMs during flight) - Plan launches in NukeExecution's Manhattan-distance silo order, tracking which silos have interceptable trajectories (wasted bombs) - Use a sliding window over parabolic flight times to find the best cluster of bombs that can arrive within half the SAM cooldown window - Compute per-bomb wait ticks to synchronize arrivals from silos at different distances - Skip launching if a salvo is already in flight - Upgrade the best SAM-protected silo when silo capacity is insufficient; wait and save gold when only gold is lacking https://github.com/user-attachments/assets/14fa592f-2902-4604-8e37-1eba2b2f0b85 ### 2-Player Endgame Handling (`NationNukeBehavior.ts`) - On Hard/Impossible with only 2 players remaining, `findBestNukeTarget()` directly targets the other player (bypasses all priority logic) - `getPerceivedNukeCost()` returns actual cost (no MIRV saving inflation) when only 2 players are left ### SAM Build Rate (`NationStructureBehavior.ts`) - Reduced SAM perceived cost increase per owned from 1.0 to 0.5, so nations build more SAMs ### Island Attack Variety (`AiAttackBehavior.ts`) - `findNearestIslandEnemy()` now collects up to 2 reachable candidates and has a 33% chance to pick the second-nearest, adding variety to boat attack targeting ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
90204f6628 |
Add alliance renewal action to Radial Menu (#3148)
## Description: The following PR replaces the (disabled) alliance request button with an alliance extension/renewal button when the alliance with the target player is expiring. Agreeing to renewal via radial menu also hides the message in the EventsDisplay. <img width="369" height="364" alt="image" src="https://github.com/user-attachments/assets/d8040f5c-ad7b-47d0-852f-925ecbf273a8" /> https://github.com/user-attachments/assets/aa589edf-6505-46bf-88a3-aa4c2df9137f Icon size adjusted: <img width="294" height="252" alt="image" src="https://github.com/user-attachments/assets/7ca63500-b1fb-427b-965c-cf121a5213da" /> ## 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: deshack_82603 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
ba2a947061 |
Feat: Display ghost railways when building cities and ports (#3202)
## Description: Based on [this suggestion on Discord](https://discord.com/channels/1284581928254701718/1447110257196138577) and feedback gathered in [this thread](https://discord.com/channels/1359946986937258015/1469598906173227184). Supersedes #3143 This PR introduces "ghost railways": when you are going to place a city or port, previews railway connections that will be made when actually building the structure. Ghost railways are skipped if the structure is going to be snapped to existing railways (as in railway snapping functionality introduced in #3156 ). ### Video https://github.com/user-attachments/assets/ff8cf325-6501-4df8-801d-c8ae3ced3d0e ### Ghost rails color revisited black with 40% opacity <img width="695" height="430" alt="image" src="https://github.com/user-attachments/assets/272efbcc-4185-426a-921c-7fae61f6c462" /> ## 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: deshack_82603 |
||
|
|
86e51ab790 |
Fix nation spawnkilling 🔧 (#3222)
## Description: As far as I can remember, in v28 the spawn immunity applied to both humans and nations. With the configurable spawn immunity (added for v29) the spawn immunity no longer applies to nations... Because its called PVP immunity now. So right now it's possible to spawnkill nations. This is a big problem for the 5M gold modifier games... And you can "cheat" in singleplayer. This PR changes two things: - Nations always have 5 seconds spawn immunity now, no matter whats configured for the PVP immunity - Nations attack TerraNullius earlier (Otherwise the easy nations would sometimes do their first attack after the 5 seconds are over, spawnkills would still be possible) ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin --------- Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com> |
||
|
|
52012e321b |
Fix: npm run perf errors on Windows (#3192)
## Description: Npm script 'perf' errors on Windows: "Error [ERR_MODULE_NOT_FOUND]: Cannot find module '(XXX)\OpenFrontIO\tests\perf\*.ts'". It probably worked fine on Linux or Mac, that i don't know. Replaced it with a file that also runs all tests in the folder, which is then simply ran by the script. There are possibly better ways to address this but this just 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 |
||
|
|
4bc168dffb |
make usernames linkable in news (#3200)
## Description: make usernames linkable in news now: <img width="399" height="153" alt="image" src="https://github.com/user-attachments/assets/39644fe2-9af1-4765-b839-9f8b5f9d0418" /> before: <img width="409" height="82" alt="image" src="https://github.com/user-attachments/assets/d7a1c37e-63cf-4417-ac61-c6db39a33851" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n Co-authored-by: iamlewis <lewismmmm@gmail.com> |
||
|
|
f362e47413 |
Cancel nukes when accepting alliance via radial menu (#3155)
Resolves #3154 ## Description: #2716 introduced nuke cancellation logic on alliance acceptance via `AllianceRequestReplyExecution`. The radial menu action, though, calls `AllianceRequestExecution` instead, which accepts the alliance if a request has already been made by the other player. This PR moves the nuke cancellation logic to `GameImpl`, hooking into the `acceptAllianceRequest` method, therefore accounting for every alliance acceptance, regardless of the specific action that brought to that. ## 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: deshack_82603 |
||
|
|
cb6e97ed11 |
Add Leaderboard refresh time (#3190)
## Description: I added a small refresh time text (see screenshots below). > I play ranked a lot since it's been added and I just reached the top 100 (yay !!), I was wondering what was the refresh time so after I found it in the code, I wanted to add a small text for easier understanding :) <details> <summary><h2>Open Screenshots "players" here</h2></summary> Before "players" : <img width="622" height="645" alt="image" src="https://github.com/user-attachments/assets/d3335954-8e16-4465-b09f-89d03defe643" /> After "players" : <img width="628" height="637" alt="image" src="https://github.com/user-attachments/assets/fd89df53-0942-4869-bfb5-9c7e7497af38" /> </details> This can be edited as you want but I did not added the text in the "clans" section. I did not added any test in the tests files since this is a minor UI improvement, but I can if needed, And I do tested everything locally myself to take the screenshots :) ## 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: @noleet |
||
|
|
07e13b3479 |
Fix: remove alliances on death (#3168)
## Description: - Remove alliances on death: after death, alliances would stay active including countdown timers and (when dead player kept spectating) icons. Now remove them when player becomes inActive. - Moved code to private method within PlayerExecution + added comments in NationExecution and BotExecution for more clarity as to where removals are performed from at death - Remove renewal request from Events Display when Alliance doesn't exist anymore (after death or otherwise). - Also cleanup this.alliancesCheckedAt when alliance doesn't exist anymore. Before, old/broken alliance id's would accumulate in it during a game. - Removed now-redundant isAlive check in EventsDisplay. Both the alliances array as the isAlive are updated in the same tick from PlayerUpdates so now alliance is removed from alliances array on player death, the other.isAlive() check is no longer needed. Of course we could keep it in just to be very safe, so just let me know when you're doubtful about this. - Attack.test.ts: fix failing test. Player B dies because of the attack, meaning the alliance now gets removed. Prevent this by gving both a different, adjecent, starting tile. And to be more clear about what is needed for the test to pass, add isAlive check for both of them after the attacks. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 |
||
|
|
6cc0ef7d14 |
Add PVP immunity to 5M starting gold modifier games 🔧 (#3180)
## Description: Adds 30 seconds of PVP immunity to 5M starting gold modifier games. So you cannot insta-nuke other players. Because I'm sure people would be confused "I cannot attack!!!!" I added a HeadsUpMessage which informs about the PVP immunity. We already have a ImmunityTimer progress bar but I don't think its enough. <img width="1270" height="745" alt="image" src="https://github.com/user-attachments/assets/0ee23dc4-1c7b-4d62-8b3d-8de214f03c2b" /> I had a second count in the HeadsUpMessage (seconds until PVP immunity is over) but it felt too busy. So I removed it. You can tell when PVP immunity is over by looking at the progress bar. ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin --------- Co-authored-by: Evan <evanpelle@gmail.com> |
||
|
|
12733900a4 |
test(i18n): validate ICU syntax across all translation files (#3170)
## Description: This PR adds a translation validation test to catch malformed ICU message syntax during test runs instead of only at runtime. ## What changed - Added `tests/LangIcuMessages.test.ts`. - The test scans all `resources/lang/*.json` files (excluding `metadata.json`). - It flattens nested translation objects into dot-keys. - It validates each translation string by compiling it with `IntlMessageFormat`. - It fails with explicit `file:key` errors for: - Invalid ICU syntax - Invalid translation value types (non-string leaves) ## Why Today malformed translation strings only surface as console warnings at runtime. This test moves detection into CI/test execution, giving fast and deterministic feedback. ## How to run ```bash npx vitest run tests/LangIcuMessages.test.ts ``` ## Notes The new test currently surfaces existing malformed ICU strings (not introduced by this PR), especially `send_troops_modal.slider_tooltip`, `send_troops_modal.capacity_note`, and `send_gold_modal.slider_tooltip` in multiple locale files. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME |
||
|
|
900cc89067 |
Better username censoring (#3122)
## Description: Many inapropriate names bypass the current filter. This PR does the following: 1. Moves name censoring to server side so inappropriate names are scrubbed before being sent to the client 2. Requests a list of profane words from the api, this allows us to quickly add new profane words in the admin panel without having to redeploy. ## 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 |
||
|
|
c212735f09 |
Orange betrayal button for no-debuff-betrayals 🖌️ (#3161)
Resolves #1276 ## Description: Orange betrayal button if the player is a traitor or disconnected. So people can easier tell that this is a betrayal without consequences. The color changes back to red without reopening the menu (live) when the traitor debuff ends or the player reconnects. <img width="268" height="257" alt="image" src="https://github.com/user-attachments/assets/276e91ce-e49d-474c-afaa-ffa18d45a2c7" /> ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin --------- Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com> |
||
|
|
c6c793f6b3 |
Highlight hovering railroad (#3156)
## Description:  The `RailroadLayer` simply displays tiles as instructed by the core worker. While it's practical for the layer to only care about the tiles, it also means it has no understanding of railroads as entities (their paths, connections, or identities). It also means that the core worker is responsible for rendering tasks such as tile orientation and construction animation, which is not expected. To support ID-based events and better separation of concerns, the rendering layer needs to be aware of complete railroads. With this change, the core worker can send the tiles once and subsequently reference railroads only by ID for all other events. #### Changes: - `RailroadLayer` now stores full railroad data instead of only individual tiles - `RailroadLayer` is responsible for animating newly built railroads - Add a new `RailroadSnapUpdate` sent when a new structure is built over an existing railroad. This event is used by `RailroadLayer` to keep railroad ID in sync. - When hovering over a railroad, the render worker is querying the core worker about overlapping railroads. Alternatively, RailroadLayer could compute overlaps itself now that it has full railroad knowledge, but this logic would need to be duplicated and kept in sync across workers. Keeping a single source of truth in the core worker is preferred. #### Edgecases: - When a structure snaps over a railroad, the original railroad is split into two new railroads. If the construction animation is still in progress, instead of resuming the animation at the correct point on the new railroads, all remaining tiles are rendered immediately - Previously, `RailroadUpdate` handled both construction and destruction. This no longer works with `RailroadSnapUpdate`, as event ordering is now pretty important and IDs may be lost before they are consumed. To address this, RailroadUpdate is split in two: `RailroadConstructionUpdate` and `RailroadDestructionUpdate`. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom --------- Co-authored-by: jrouillard <jon@rouillard.org> |
||
|
|
e7676b4260 |
check if translations are being used in the code (en.json test) (#3158)
## Description: Test if translation is being used from en.json test with a small bugfix for a regression that happened in an old 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n |
||
|
|
32adfa2f79 |
Add requeue button to Ranked victory/defeat modal (#3121)
## Description: Adds a "Play Again" requeue button to the victory/defeat modal for Ranked 1v1 games. When clicked, it navigates the player back to the homepage and automatically opens the matchmaking modal to queue for another ranked match. Changes: - WinModal.ts: Added isRankedGame state, purple "Play Again" button (only shown for ranked 1v1), and _handleRequeue() method - Main.ts: Added ?requeue URL parameter handling to trigger matchmaking modal on page load - en.json: Added "requeue": "Play Again" translation string - added tests to WinModal.test.ts Note: temporarily set isRanked flag to true to get the modal to pop in a solo match on dev server and confirmed that ?requeue URL parameter called _handleRequeue() correctly, which opened the sign in process since actually signing in and queuing for a ranked match isn't possible on dev server. <img width="771" height="364" alt="play-again" src="https://github.com/user-attachments/assets/6e3f5a02-f1ae-465a-9b28-656126c11d3d" /> ## 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: skigim |
||
|
|
8cc6c2c2aa |
Perf spawn train (#3130)
## Description: Train spawning hot-path optimization (trade destination selection) ## Summary This PR reduces per-tick overhead in train spawning by removing temporary allocations and reducing work in the destination-selection path. The change focuses on `Cluster` trade destination lookup and how `TrainStationExecution` picks a destination. ## What changed ### 1) Maintain a “trade-capable” station subset per cluster `src/core/game/TrainStation.ts` - `Cluster` now maintains: - `stations`: all stations in the cluster (unchanged) - `tradeStations`: maintained subset of stations that can act as trade endpoints (`City` or `Port`) - `tradeStations` is kept in sync in: - `addStation()` - `removeStation()` - `clear()` Impact: - Trade queries no longer scan every station in the cluster; they only scan `tradeStations`. ### 2) Add cheap eligibility helpers `src/core/game/TrainStation.ts` - `hasAnyTradeDestination(player)`: - Fast early-exit check: returns as soon as it finds any eligible trade destination. - `randomTradeDestination(player, random)`: - Picks a random eligible trade destination directly without materializing an intermediate `Set`. ### 3) Use reservoir sampling for single-pass random choice `src/core/game/TrainStation.ts` `Cluster.randomTradeDestination()` uses reservoir sampling: - Iterates `tradeStations` once. - Maintains a running count of eligible stations (`eligibleSeen`). - Replaces the selected station with probability `1/eligibleSeen`. Properties: - Uniform selection among eligible stations. - One pass instead of “count then pick by index” (two pass). - Allocation-free. - Returns `null` when no eligible destination exists. ### 4) Update train spawning to avoid temporary sets `src/core/execution/TrainStationExecution.ts` - Previously: `spawnTrain()` called `cluster.availableForTrade()` and then `random.randFromSet(...)`. - This built a new `Set` on the hot path. - Now: - Early-exit via `cluster.hasAnyTradeDestination(owner)`. - Destination via `cluster.randomTradeDestination(owner, random)`. Net effect: - Less per-tick work and no per-spawn temporary `Set` allocations. ## Why this helps Train spawning happens frequently and can become a hot path in large games / large rail clusters. Avoiding repeated allocations and reducing work inside `tick()` helps keep frame/update time predictable. ## notes - Trade rules are unchanged (`tradeAvailable(player)` still gates eligibility). - Destination selection remains random-uniform over eligible `City`/`Port` stations that satisfy `tradeAvailable(player)`. - `TrainStationExecution` now avoids calling `spawnTrain()` entirely when `spawnTrains` is falsy (it was already guarded inside). ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME |
||
|
|
c2663944e5 |
Stop getting gold from conquering inactive players 🔧 (#3020)
## Description:
Maybe for v29.
In the 5M starting gold modifier games you can conquer a inactive player
(spawned but didn't do anything) and get their 5M gold.
Huge unfair advantage.
I think that even without the starting gold modifier you should not get
the gold of inactive players because its unfair.
I identify inactive players (spawned but didn't do anything) by checking
the attack stats.
I added a translation for the displayMessage "Conquered {name}, received
{gold} gold". Why was that not translated?
I added a new message "Conquered {name} (Inactive player, received no
gold)".
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
---------
Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
|
||
|
|
294a1b4784 |
move lobby websockets to worker (#2974)
## Description: Currently only the master process sends public lobby updates to clients. This is not scalable since it could overload the master process. In this PR, the master uses IPC to send public lobby info to all workers. Then clients connect to a random worker to get public lobby updates via websocket. This way clients never connect directly to the master websocket. The flow looks like this: Every 100ms: 1. Master schedules a public game on a random worker if new games are needed 2. Master broadcasts public lobby info to all workers (all public games & num clients connected to each game) 3. Each worker responds to that update with the number of clients connected to its own public games 4. Master then updates its public lobby state so it knows how many clients are connected to each public game ## 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 |
||
|
|
e4280c28e1 |
Add Ranked 1v1 Leaderboard (#3008)
If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #(issue number) ## Description: @wraith4081 's pr updates the stats modal to show both 1v1 and clan stats ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n --------- Co-authored-by: Wraith <54374743+wraith4081@users.noreply.github.com> Co-authored-by: iamlewis <lewismmmm@gmail.com> |
||
|
|
0286224299 |
Move betrayal button, remove betrayal confirmation 🔧 (#3076)
## Description: - Move betrayal button to the boat-sending-button-location (you can't send boats to allies) to prevent missclicks - Remove betrayal confirmation <img width="260" height="248" alt="image" src="https://github.com/user-attachments/assets/0a25fc9c-c8a0-4ba9-a8c8-971d6a7a0511" /> ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
c54c73d157 | Merge branch 'v29' | ||
|
|
965dd0f482 |
Confirm alliance break ⚠️ (#3033)
## Description: People accidentally clicked the betray button because it's at the same position as the ally button. So let's add a small confirmation step. https://github.com/user-attachments/assets/754f2d33-7419-42fc-a732-197c3107236e ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
0cc58a8f5a |
fix: add validation for unknown flags in manifest.json (#3044)
Resolves #3041 ## Description: - Add a test to ensure an error is thrown when manifest.json specifies a non-existent flag. - Fix the underlying issue by removing the invalid flag specification (see error below). ``` resources/maps/straitofgibraltar/manifest.json -> nations[0].flag "Rif" does not exist in resources/flags resources/maps/straitofgibraltar/manifest.json -> nations[5].flag "Shilha" does not exist in resources/flags resources/maps/straitofgibraltar/manifest.json -> nations[6].flag "Andalusia" does not exist in resources/flags resources/maps/italia/manifest.json -> nations[0].flag "custom:Kingdom of the Two Sicilies" does not exist in resources/flags resources/maps/italia/manifest.json -> nations[3].flag "custom:Tuscany" does not exist in resources/flags resources/maps/italia/manifest.json -> nations[5].flag "custom:Modena" does not exist in resources/flags resources/maps/italia/manifest.json -> nations[6].flag "custom:Parma" does not exist in resources/flags resources/maps/italia/manifest.json -> nations[8].flag "custom:Kingdom of Sardinia" does not exist in resources/flags resources/maps/italia/manifest.json -> nations[11].flag "custom:Ottoman Empire2" does not exist in resources/flags resources/maps/britannia/manifest.json -> nations[19].flag "gb-nir" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[0].flag "quebec" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[1].flag "quebec" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[2].flag "quebec" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[4].flag "quebec" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[5].flag "quebec" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[6].flag "quebec" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[7].flag "quebec" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[8].flag "quebec" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[9].flag "quebec" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[10].flag "quebec" does not exist in resources/flags resources/maps/montreal/manifest.json -> nations[11].flag "quebec" does not exist in resources/flags ``` ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri |
||
|
|
1dac7bd2e8 |
Confirm alliance break ⚠️ (#3033)
## Description: People accidentally clicked the betray button because it's at the same position as the ally button. So let's add a small confirmation step. https://github.com/user-attachments/assets/754f2d33-7419-42fc-a732-197c3107236e ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
7942990037 |
Crowded modifier 😄 (#3023)
## Description: To increase variety a bit more I present: The "crowded" public game modifier :) It basically simulates a crazy youtuber lobby. Cramp a lot of players on a small map 😄 I think its fun, exciting and you actually need skill to manage the chaos. 5% of public games get this modifier, but because we remove the modifier for big maps its more like 2.5% (should be something special) | <img width="321" height="269" alt="Screenshot 2026-01-25 200427" src="https://github.com/user-attachments/assets/7d2e90c1-e6bc-40a8-a19e-a0849612f472" /> | <img width="317" height="264" alt="Screenshot 2026-01-25 200554" src="https://github.com/user-attachments/assets/8b4bd5da-bed1-4743-a107-9ce07fce3040" /> | <img width="317" height="244" alt="Screenshot 2026-01-25 200521" src="https://github.com/user-attachments/assets/16293de3-0fc4-431f-8151-31b4e11040fe" /> | |---|---|---| ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
9aed372425 |
Added afterEach cleanup to call inputHandler.destroy(), which clears the setInterval before jsdom tears down and removes window. (#3030)
## Description: Fixes the failing test:coverage ci. ## 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 |
||
|
|
ed9900e313 |
Added afterEach cleanup to call inputHandler.destroy(), which clears the setInterval before jsdom tears down and removes window. (#3030)
## Description: Fixes the failing test:coverage ci. ## 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 |
||
|
|
de3794313d |
feat: Kick player in game (#2969)
If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #2686 ## Description: - Implemented feature for lobby creator to kick players in game. - Added new moderation option for lobby creator, with a kick player option if they aren't the creator, a bot, and exist in game. - Includes a confirm kick option, and keeps track of kicked players so that the kick option changes to "Already Kicked" if the kicked player panel is opened again on the kicked player. Screenshot order: 1) Open player panel 2) Click on moderation 3) Click on kick player and confirm kick 4) Player is kicked, open same player panel again and observe change in kick status 5) Receiving player kick message <img width="1470" height="776" alt="Screenshot 2026-01-20 at 12 33 55 PM" src="https://github.com/user-attachments/assets/7c47b5a2-a0f8-4e92-833c-7b9732f751a8" /> <img width="1470" height="776" alt="Screenshot 2026-01-20 at 11 58 58 AM" src="https://github.com/user-attachments/assets/3aa026af-9a42-4512-91b8-916f146849a6" /> <img width="1470" height="776" alt="Screenshot 2026-01-20 at 12 31 46 PM" src="https://github.com/user-attachments/assets/5e1d271b-bf32-4335-8eb1-bcdf84aba8ce" /> <img width="1470" height="776" alt="Screenshot 2026-01-20 at 11 57 58 AM" src="https://github.com/user-attachments/assets/7cbd5ea6-bcb6-4a35-a003-ea0add936925" /> <img width="1470" height="776" alt="Screenshot 2026-01-20 at 11 57 39 AM" src="https://github.com/user-attachments/assets/4309b3e3-2fe6-48dd-8e0c-55036e567461" /> ## 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: mitchfz |
||
|
|
9415162f51 |
Split railroads when placing overlapping structures (#3003)
## Description: Players wrongly assume that building a structure over an existing railroad will connect it properly. What actually happens is that the structure will connect on the network with its own railroad, even if the new railroads are overlapping over the existing network. To address this issue, this PR splits the overlapping railroad into two segments when a structure is built over it, and inserts the structure as a new node in the rail graph. It does not alter the rail network visually because the same railroad tiles are used for the new segments. Railroad tiles are not stored directly in the map, they exist only as edges in the rail graph, so looking for nearby rails would be terribly inefficient. To address that, this PR introduces a new `RailSpatialGrid` class which indexes rails on a 4×4 grid, allowing fast spatial queries. Alternative considered: removing overlapping rails and rebuilding them from the new structure. It would visually modify the rail network, which may be unexpected for the player. It's still missing a visual indicator so the player knows that the structures has been connected properly. ### Line placement:  ### Multi-railroad overlap:  ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom |
||
|
|
8aa3e26e70 |
feat: Prevent GameServer from restarting after ending by introducin… (#2923)
If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #(issue number) #2919 In GameManager.tick(), when a game becomes active but hasn't started, a setTimeout for game.start() is scheduled with a 2-second delay. If the game finishes or is cancelled within those 2 seconds, game.end() is called, which clears the existing interval. However: 1.The 2-second timeout still fires. game.start() executes. 2. A NEW setInterval is created for turn execution. 3.Since the game is already ending/finished, it's removed from GameManager.games, but the interval continues to run forever in the background ## 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: codimo |