Commit Graph

718 Commits

Author SHA1 Message Date
FloPinguin 4cd6f50c03 Nations: Fix city farming + reactive defense posts + fix nuked territory capture 🛡️ (#3814)
## Description:

Would be very good to get these fixes last minute into v31.

- **Farming nations for cities is fixed**: Here you can see city farming
in action, vari farms Finland:
https://www.youtube.com/watch?v=J4J0ajlnSHs&t=137s - Lots of 125k gold
cities... Now nations will build defense posts instead of cities:

- **Nations now build defense posts reactively** instead of through the
normal structure build order. When a nation is under significant land
attack (incoming/own troops >= 35%), it places a defense post near the
attack front -- skipped on Easy, capped at 1 on Medium, and scaled by
the incoming troop ratio on Hard/Impossible. Posts spread along the
front. The old `defensePostValue()` placement path is removed.

- **Nations now capture nuked territory.**
`hasLandBorderWithTerraNullius()` was incorrectly filtering out tiles
with fallout, causing the `nuked` attack strategy to never dispatch a
attack. Big bug

.

- [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
2026-05-03 09:33:44 -06:00
Evan bcdc2126c6 bugfix: SAM was only reloading when not in cooldown (#3817)
## Description:

reloadMissile() was inside the isInCooldown() block. For level-2+ SAMs,
isInCooldown() returns queue.length === level, so after firing one of
two missiles (queue.length = 1 < level = 2) the SAM is not in cooldown —
meaning expired timers were never cleaned up. Stale queue entries caused
subsequent shots to be treated as still-cooling even after the cooldown
elapsed.

SAM execution now mirrors the missile silo execution

## Please complete the following:

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

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

evan
2026-05-02 09:05:10 -06:00
evanpelle 02353cf77d Fix nuke cancellation on alliance to use blast radius
cancelNukesBetweenAlliedPlayers previously only cancelled a nuke if its exact target tile was owned by the new ally. This meant nukes aimed at neutral or own tiles near allied territory would survive alliance formation and still land (and break the alliance).

Now uses wouldNukeBreakAlliance — the same blast-radius logic used by maybeBreakAlliances on impact — so a nuke is cancelled if its blast would have meaningfully hit the ally's tiles or structures. Also switches from the exhaustive listNukeBreakAlliance (scans all players) to wouldNukeBreakAlliance with a single-player allySmallIds set for early-exit performance.
2026-04-30 17:17:46 -06:00
Evan ccb80f4245 Fix warship diagonal chase and improve trade ship capture reliability (#3807)
## Description:

The warship pathfinder operates on a 2x downscaled mini-map, and
upscaling mini-map paths back to full coordinates produces diagonal
interpolated steps. At close range (< 20 tiles), the entire path
consists of these diagonal moves, causing the warship to approach the
trade ship at an awkward angle and never converge cleanly.
## Please complete the following:

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

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

evan
2026-04-30 16:37:44 -06:00
Zixer1 742a544a69 2661 PR 3/3 Warship Manual Override, Aggro Override, and Heal-at-Port Command (#3501)
Part of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661)
(split into 3 PRs so they are not too large..)

## Description:

Part 3/3 of
[#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661).

This PR adds the retreat control and override behavior for warships:

- Manual override: moving a warship manually cancels retreat and
suppresses auto-retreat for 5 seconds
- Aggro override: a retreating warship will aggro a nearby enemy
transport or warship before continuing retreat
- Heal-at-port command for sending a warship to a friendly port manually
- Friendly-port validation for HealAtPortExecution
- Regression tests for manual override, aggro override, and heal-at-port
behavior



## Please complete the following:

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

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

zixer._

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
Co-authored-by: evanpelle <evanpelle@gmail.com>
2026-04-30 13:54:28 -06:00
babyboucher bd6c63b6ea Nation ship improvements (#3724)
## Description:

Nations preform poorly on large water maps, these changes aims to
improve how they handle and react to water combat.

Adding the ability for hard and impossible nations to retaliate against
incoming transport ships.
Adding the ability for ships to be moved if the nation is not able to
build a new ship.

## Please complete the following:

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

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

babyboucher

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
Co-authored-by: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
2026-04-29 18:18:00 +00:00
FloPinguin 7654537a00 Much better river handling for nations and tribes! 🏞️ (#3786)
## Description:

In this example, the two nations DONT see each other as neighbors, but
as ISLANDERS. Because they dont have a direct border connection, there
is water in between.

<img width="526" height="329" alt="image"
src="https://github.com/user-attachments/assets/cf2c15b5-7793-4445-afd2-920d6cd50a2a"
/>

This is a big problem, because most of the logic in AiAttackBehavior
gets ignored. Only the "islander" strategy runs (late, because its a
not-important strat).

### Summary

- `PlayerImpl.neighbors()` now includes cross-water neighbors: a new
`shoreReachableNeighbors()` helper samples every 10th shore border tile
and looks up to 5 tiles in each cardinal direction across water, finding
land owners on the other side (covers rivers up to 4 tiles wide).

- `AiAttackBehavior.maybeAttack()` extends the `hasNonNukedTerraNullius`
check to also trigger on TN detected via `player.neighbors()`, so
nations notice and pursue TN that is only reachable across a river.

- `sendAttack()` uses a new `hasLandBorderWithTerraNullius()` land-only
adjacency check to decide between a land attack and a boat attack for
TN, rather than `sharesBorderWith()` which includes water tiles.

- Added `sendBoatAttackToNearbyTerraNullius()`: when no TN land is
directly adjacent, the AI scans its shore border tiles for unowned land
across water and dispatches a transport ship.

### Also works for Tribes!

Tribes can boat rivers now, really cool.


https://github.com/user-attachments/assets/382e85aa-c437-4e0c-afc2-0c381432da3d

## 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
2026-04-28 18:25:11 -06:00
FloPinguin 692028d033 Make nations more forgiving toward humans on Easy difficulty 🕊️ (#3791)
## Description:

- Nations on Easy no longer betray human allies
- Nations on Easy now attack humans only 25% of the time (down from 50%)
- Increased Easy reaction tick range (65-100, up from 65-80) so nations
respond more slowly
- Raised MIRV steamroll-stop thresholds on Easy (city gap multiplier 1.5
-> 2, min leader cities 15 -> 20) and slightly tuned Medium/Hard
thresholds

## 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
2026-04-28 13:56:05 -06:00
FloPinguin 408d0e4862 Improve TribeExecution deleteAllStructures 🏛️ (#3777)
## Description:

`deleteAllStructures` did not check for `isMarkedForDeletion`, not very
clean. Now its clean.
Also rename to `deleteNextStructure` because its not possible to delete
more than 1 structure at a time.

## 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
2026-04-26 19:10:27 -06:00
FloPinguin 8099b9fad7 Massive nation improvement 🤖 (#3761)
## Description:

- Hard / Impossible nations in team games auto-stop trading with all
enemies
- If there are a LOT of nations on the map (Enzo stream with 400 nation
HvN private games) they no longer start with a city, they start with eco
(port / factory) because they cannot gain much gold from bot-killing
- Impossible nations built way too many missile silos sometimes, caused
by the SAM overwhelming logic. Fixed now.
- In public HvN games with 5M starting gold, nations placed their
structures way too fast, which slowed down their expansion. And humans
could easily cause a lot of damage with one atom bomb. Now their first
structure is a SAM (on hard / impossible) and they wait between their
earlygame structure placements.
- Nations now spread out their port placements more evenly
- Nations are now able to attack much stronger enemies in team games
(They can expect donations)
- Improve performance a bit by adding more early-returns (Dont run any
nuking logic if nukes are disabled, no alliance logic if alliances are
disabled, no boating logic if transport boats are disabled, ...)
- Fix some of the "cannot send troops" messages in the console
(DonateTroopExecution)
- Nations build their first missile silo sooner, they should also build
more SAMs
- Nations spend their gold better after reaching the save-up-target
(previously they stopped nuking)
- Optimized save-up-targets for team games
- The richest impossible nation is nuking very dense players now (lot of
structure levels on a small island)

### How does a 5M gold HvN start look like now?


https://github.com/user-attachments/assets/e9da89c3-c0d4-4144-a741-3101746b16da

## 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
2026-04-26 18:43:45 -06:00
Zixer1 c0febacb8e 2661 PR 2/3 Warship Port Healing, Docking Capacity, and Waiting Behavior (#3499)
Part of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661)
(split into 3 PRs so they are not too large..)

## Description:

Part 2/3 of
[#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661).

This PR adds port-based healing and docking behavior:
- Passive healing near friendly ports
- Active docked healing pool scaled by port level and shared across
docked ships
- Docking radius and capacity-by-port-level behavior
- Waiting behavior near full ports until a slot opens
- Auto-undock once fully healed

For the active healing, it works like `ActiveHeal = (PortLevel * 5) /
DockedShipsAtThatPort`
Ex:
1 ship at level 1 port -> +5 HP/tick
1 ship at level 2 port → +10 HP/tick
2 ships at level 3 port → +7.5 HP/tick each

Includes regression tests covering healing math and docking/waiting
behavior.

## Please complete the following:

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

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

zixer._
2026-04-26 21:42:13 +00:00
Zixer1 37079e6a05 2661 PR 1/3 Warship Retreat Core, Blue UI Signal, and Transport-First Target Priority (#3498)
Part of #2661 (split into 3 PRs so they are not too large..)

## Description:

Part 1/3 of #2661.

This PR adds warship retreat basics, a blue retreating UI state, and
updates target priority.

Added:
- Retreat state handling
- Blue visual for retreating warships
- Target priority: transport > warship > trade 
- Tests for retreat and target priority

Example video:
https://youtu.be/2hE2qeOeY48
Ship retreating:
<img width="630" height="488" alt="image"
src="https://github.com/user-attachments/assets/56d3e6d5-08af-453d-afe5-ee21dd6f3414"
/>
Ship healing:
<img width="483" height="311" alt="image"
src="https://github.com/user-attachments/assets/aeaf2239-bb81-444f-84ef-62dbcb48fddf"
/>
Back to being deployed:
<img width="585" height="358" alt="image"
src="https://github.com/user-attachments/assets/875828a2-8a24-4593-ac76-26426bb81057"
/>

## Please complete the following:

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

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

zixer._
2026-04-24 08:26:14 -06:00
FloPinguin f7716c7d42 Little Console Cleanup 🧹 (#3741)
## Description:

Fixes these console warnings from bots:

<img width="591" height="94" alt="Screenshot 2026-04-19 033624"
src="https://github.com/user-attachments/assets/6ee79302-e2a7-4195-94e5-c1f455eb1799"
/>

Removes some spammy logs, they dont seem to be helpful?

<img width="271" height="174" alt="Screenshot 2026-04-19 033739"
src="https://github.com/user-attachments/assets/70122506-e8fb-4a72-b73e-08e72fe222bd"
/>



<img width="284" height="656" alt="Screenshot 2026-04-19 033646"
src="https://github.com/user-attachments/assets/4b4ebef2-e191-4947-9615-0e26cd9bf075"
/>

## Please complete the following:

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

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

FloPinguin
2026-04-22 15:59:42 -07:00
Ivan Batsulin 29a1e8dfda feat: multi-warship selection with Shift+drag box (#3677)
Resolves #3666

## Description:

Adds RTS-style box selection for warships. Hold Shift and drag (desktop)
or long-press and drag (touch/mobile) to draw a selection rectangle —
all player-owned warships inside get selected at once. A subsequent
click/tap on water sends them all to that location.

- `SelectionBoxLayer` — pixel-dashed rectangle in world-space, player
territory color; shared between desktop and touch
- `UILayer` — same pulsing selection outline on each box-selected
warship; clears correctly when switching between single/multi selection
- `UnitLayer` — finds warships in screen rect, filters inactive ships
before sending; touch support included
- `InputHandler` — Shift+drag and touch long-press+drag both emit
selection box events; cursor becomes crosshair on Shift; discards active
ghost structure on Shift press; configurable via `shiftKey` keybind
- `Transport` — single atomic `move_multiple_warships` intent (no split
on socket drop)
- `Schemas` + `ExecutionManager` + `MoveMultipleWarshipsExecution` —
server fans out atomic intent into individual `MoveWarshipExecution` per
ship
- `DynamicUILayer` — `MoveIndicatorUI` chevron animation on target tile
for both single and multi move
- `UnitDisplay` — warship tooltip Shift hint via `translateText`
- `HelpModal` — new hotkey row: Shift + drag → select multiple warships

## Please complete the following:

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

## UI update
### Mouse + Keyboard


https://github.com/user-attachments/assets/3f35ab5e-1f3c-4c5d-bc4f-aabccf64dc60

### Touch


https://github.com/user-attachments/assets/0d6aec3f-44fa-4fee-b5c6-b267b9b14d79

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

fghjk_60845
2026-04-21 14:06:07 -07:00
FloPinguin 12b06fa0b2 Pathfinding Fixes (Water Nukes / Lakes) 💧 (#3714)
## Description:

Fixes water-pathfinding errors that started appearing after the first
water nuke and persisted across the rest of the match.
Users reported warships "getting stuck" (stopped moving).

<img width="374" height="281" alt="image"
src="https://github.com/user-attachments/assets/de38b8f1-c4d8-469e-b3a7-d0cef4dfb772"
/>

### Summary

- The new `AbstractGraphBuilder.buildClusterConnectionsFromCache` was
buggy _(The cached edge costs reused by "clean" clusters were keyed by
tile pair without their original `(clusterX, clusterY)`, so a boundary
edge could be re-stamped with the wrong cluster and become untraversable
by the query-time single-cluster bounded A*. The cache now stores `{
cost, clusterX, clusterY }` and `buildClusterConnectionsFromCache`
preserves the original attribution when re-adding the edge.)_
- Warships: `findTargetUnit` now skips trade ships that are not in the
warship's water component, avoiding pathfinding to provably unreachable
targets.
- Warships: On `patrol` `NOT_FOUND`, clear `targetTile` so the warship
picks a new target. This is a defensive guard for the rare case where a
water nuke splits the component between target selection and pathfinding
- without it, the warship retries the same now-unreachable target every
tick and spams the log forever.

### Test

- Added a Warship test verifying that trade ships in a different water
component are not targeted.

## 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
2026-04-18 21:32:21 -07:00
FloPinguin bcd9cd6af5 Cache shared-water computation for nation port placement 💧 (#3696)
## Description:

- Cache `sharedWaterComponents` globally with a 30-tick (~3s) TTL so all
nations share one `O(total_border_tiles)` pass instead of each nation
re-scanning every other player's border on every call.
- Treat ocean as always-shared: any ocean neighbor short-circuits as a
valid port site, skipping the `getWaterComponent` lookup in both the
build pass and the per-tile port check.
- Exclude bots and mutually-embargoed players from the trade-partner
candidate set, so nations no longer avoid port sites that only "share"
water with a player they can never trade with.

Port placement is not time-critical, so the 3-second staleness is
acceptable and lets the expensive build amortize across many attack
cycles.

### Performance
Benchmarked on World map (2000×1000, 61 nations) with the realistic call
pattern of ~3 nations invoking `sharedWaterComponents` per tick:

- **Before (main):** ~414 μs per tick
- **After:** ~8 μs per tick amortized (29/30 ticks hit the warm cache;
1/30 rebuilds)
- **~50× faster** on this AI hot path

## 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
2026-04-17 17:34:45 -07:00
FloPinguin 0801cad0b5 Fix NaN coordinates in Warship patrol logic 🚢 (#3697)
## Description:

This PR fixes the `Invalid coordinates: NaN,NaN` crash during Warship
patrol execution.

### Root cause

`WarshipExecution.randomTile` picks a patrol destination inside
`warshipPatrolRange / 2` of the current patrol tile. When a search fails
to find a valid tile, the range expands by 50% per retry (`100 → 150 →
225 → 337`) and becomes odd. Once odd, `warshipPatrolRange / 2` is a
float (e.g. `112.5`), which is handed straight to
`PseudoRandom.nextInt`:

```ts
Math.floor(this.rng() * (max - min)) + min;
```

With a float `min`, this returns `integer + float` - a float. Despite
its name, `nextInt` was silently returning a non-integer. From there:

- `x = mg.x(patrolTile) + floatOffset` → float
- `mg.isValidCoord(floatX, floatY)` → `true` (only bounds were checked)
- `mg.ref(floatX, floatY)` → `yToRef[floatY] + floatX` → `undefined +
float` → `NaN`
- `hasWaterComponent(NaN, …)` → `miniMap.ref(NaN, NaN)` → **throw**

### Why this only started crashing recently

The float‑leaking `nextInt` bug has been latent since at least the
pathfinding refactor (#2866, January), which introduced the
`hasWaterComponent` check. It was invisible because the guard directly
above it short‑circuited on `NaN`:

```ts
if (!this.mg.isOcean(tile) || (!allowShoreline && this.mg.isShoreline(tile))) continue;
```

For a `NaN` tile ref, `terrain[NaN]` is `undefined`, so:

- `isOcean(NaN)` → `Boolean(undefined & OCEAN_BIT)` → **`false`**
- `isLand(NaN)` → **`false`**
- `isWater(NaN)` → `!isLand(NaN)` → **`true`**

Before: `!isOcean(NaN)` was `true`, execution hit `continue`, and the
poisoned ref never reached `hasWaterComponent`. The "Trading in lakes"
PR (#3653) relaxed that single line to allow patrol on lakes:

```diff
- if (!this.mg.isOcean(tile) || ...) continue;
+ if (!this.mg.isWater(tile) || ...) continue;
```

Because `isWater(NaN)` is `true`, `!isWater(NaN)` is now `false` -
execution falls through to `hasWaterComponent(NaN, …)` and crashes.
#3653 didn't introduce the bug; it just happened to remove the
accidental NaN filter that was hiding it.

### Changes

- **`PseudoRandom.nextInt`** - root‑cause fix. Floors both `min` and
`max` so `nextInt` always returns an integer regardless of what callers
pass. Future callers can't re‑trip this trap.
- **`WarshipExecution.randomTile`** - replaced the unsafe
`this.warship.patrolTile()!` non‑null assertion with a proper
`undefined` guard that returns early.
- **`GameMap.isValidCoord`** - defense in depth: also requires
`Number.isInteger(x)` and `Number.isInteger(y)`. Non‑integer coords can
still be produced outside `nextInt` (trig, arithmetic); this makes
`ref()` fail loudly at the boundary instead of silently producing `NaN`
refs.

### Original stacktrace

Please paste the following in your bug report in Discord:
Game crashed!
game id: gGicMpDh
client id: wXE5SpT2
Error: Invalid coordinates: NaN,NaN
Message: Error: Invalid coordinates: NaN,NaN
at at.ref
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:64853)
at r_.hasWaterComponent
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:243326)
at l_.hasWaterComponent
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:260740)
at b1.randomTile
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:92634)
at b1.patrol
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:91728)
at b1.tick
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:89996)
at
https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:251463
    at Array.forEach (<anonymous>)
at l_.executeNextTick
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:251383)
at p_.executeNextTick
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:271256)

Discord:
https://discord.com/channels/1284581928254701718/1494336024740888667

## 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
2026-04-17 17:34:12 -07:00
FloPinguin ae96eb7e98 Fix nation nuke crash when attacker has no remaining tiles 🛡️ (#3703)
## Description:

Fixes a game crash (`Error: array must not be empty`) thrown from
`PseudoRandom.randElement` when a nation tries to pick a nuke target
whose territory no longer exists.

## Root cause

`NationNukeBehavior.findBestNukeTarget` calls
`findIncomingAttackPlayer`, which iterates the player's
`_incomingAttacks`. `AttackImpl` instances can linger in this array past
the point where the attacker has lost all tiles — the attack is only
removed on explicit `delete()` and `removeOnDeath` cleanup isn't
guaranteed to run before other executions tick within the same turn. A
dead attacker gets returned as the nuke target, `randTerritoryTileArray`
samples their (empty) territory, and `randElement` throws on the empty
array.

## Fix

- `Player.incomingAttacks()` now filters out attacks whose attacker is
no longer alive, so consumers can't observe stale references from
mid-tick deaths.
- `randTerritoryTile` guards against `numTilesOwned() === 0` before
falling back to `randElement(p.tiles())` as a defense-in-depth safeguard
at the util level.
- `PlayerImpl.toUpdate()` now uses the `incomingAttacks()` /
`outgoingAttacks()` accessors (rather than the raw `_` arrays) so
serialized client state stays consistent with the server-side view.

`outgoingAttacks()` is intentionally left unfiltered, the engine relies
on seeing in-flight attacks during their retreat phase after a target is
conquered (attack merging/cancellation in `AttackExecution.init`).

### Bug report from discord

Please paste the following in your bug report in Discord:
Game crashed!
game id: LQDSWbh6
client id: JjwysSLN
Error: array must not be empty
Message: Error: array must not be empty
at sa.randElement
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:28:39166)
at H1
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:116429)
at Jc
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:116135)
at G1.maybeSendNuke
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:117597)
at n5.tick
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:171764)
at https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:251463
    at Array.forEach (<anonymous>)
at c.executeNextTick
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:251383)
at b.executeNextTick
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:271256)
at S_
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:366356)

## 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
2026-04-17 17:32:49 -07:00
FloPinguin 9821e8e041 Add host cheats for streamers (Specifically Enzo) (#3671)
## Description:

- Adds a "Host Cheats" toggle in the private lobby options section that
reveals a dedicated section with four host-only cheats: infinite gold,
infinite troops, gold multiplier, and starting gold
- Only the lobby creator receives the cheat effects in-game (checked via
`isLobbyCreator` in DefaultConfig)
- Joining players see active host cheats displayed as yellow badges in
the lobby UI
- Adds `hostCheats` optional object to `GameConfigSchema` and wires it
through the server config update whitelist
- Raises the intent size limit for `update_game_config` messages
(lobby-only, not stored in turn history) to prevent rate-limiter kicks
(I always got too-much-data-kicked after selecting "host cheats" lol)

<img width="861" height="525" alt="image"
src="https://github.com/user-attachments/assets/51e51ec4-c2e8-46ca-b258-11a93487964f"
/>


<img width="933" height="825" alt="image"
src="https://github.com/user-attachments/assets/5acbd38d-2097-42e1-ba78-0fb17d6afe82"
/>

## 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
2026-04-15 15:20:08 -07:00
FloPinguin 35a64fa0d9 Big Water-Nukes Performance Improvements 💧 (#3668)
## Description:

### 1. Water Magnitude Calculation Optimization (WaterManager.ts)
* Boxed BFS Approach: Refactored the water magnitude recomputation to
use "Dirty" and "Seed" boxes. Instead of a global update, the system now
only recalculates magnitudes within a specific radius of the affected
area, significantly reducing CPU load after water-nuke-explosions
* Shoreline Bit Optimization: Narrowed the scope for updating shoreline
bits to a 2-ring neighborhood around converted tiles, avoiding
unnecessary checks across the entire map.

Performance test on the world map:
- AtomBomb (r=30): 24ms (was 344ms with global BFS), 2,993 changed tiles
(was 630k)
- Massive (r=200): 178ms (was 378ms), 130k changed tiles (was 654k)

### 2. Pathfinding Rebuild Staggering (PathFinder.ts,
TradeShipExecution.ts, TransportShipExecution.ts)
* Distributed Rebuilds: Introduced a staggering mechanism in
WaterPathFinder. Ship pathfinders now wait a randomized/distributed
number of ticks (0 - 5 seconds) before rebuilding after a water graph
change.
* CPU Spike Mitigation: By spreading out these expensive A* rebuilds
over time, we prevent lag when hundreds of ships attempt to re-path
simultaneously
* Like Mole said it: "Pretty realistic I;d say the capitan needs a
second to realize the big nuke on the left opened a new path"

From a performance test on the big new Luna map:
Graph rebuild: 256.4ms
Pathfinder-Rebuild of 329 ships (Including other Executions): 1564.4ms
(No longer noticeable, spread over 5s)

###  3. Performance Refinements
* Simplified deep ocean magnitude logic within the optimized BFS flow.
   * Improved memory efficiency by utilizing clipped BFS wavefronts.

## 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
2026-04-14 16:17:15 -07:00
FloPinguin 17c1a6300f Trading in lakes 🚤 (#3653)
## Description:

- Widened port placement and warship spawn/patrol checks from
`isOcean`/`isOceanShore` to `isWater`/`isShore`, so ports can be built
on lake shores and ships can operate on lakes, we discussed it here:

<img width="996" height="423" alt="image"
src="https://github.com/user-attachments/assets/acf1e970-9631-4848-a0ed-6d0470616e1d"
/>

- Filtered `tradingPorts()` by water component so ports only attempt
trades with reachable ports - prevents silent path-not-found failures
across disconnected water bodies
- Applied the same water component filter when a captured trade ship
reroutes to its new owner's nearest port
- Removed the `WaterManager` fallback that force-marked isolated
water-nuked-tiles as ocean (no longer needed since lakes are now
navigable)
- Added a check to prevent nations from building ports on water bodies
that aren't accessible to other players

## Please complete the following:

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

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

FloPinguin

---------

Co-authored-by: Evan <evanpelle@gmail.com>
2026-04-12 17:18:52 -07:00
FloPinguin 7f7cbba12f Water-Nukes 💧 (#3604)
## Description:

Adds a new `waterNukes` game config option that causes nuclear
detonations to convert land tiles into water instead of just leaving
fallout. When enabled, nuked land tiles are batched and converted to
water each tick, with full terrain metadata updates including:

- Ocean bit propagation from adjacent ocean tiles (BFS flood fill)
- Magnitude recomputation via BFS from remaining coastlines
- Shoreline bit fix-up in a 2-ring neighborhood around converted tiles
- Minimap terrain sync (majority-rule downsampling)
- Throttled water navigation graph rebuild (every 20 ticks) for ship
pathfinding
- Ship executions detect graph rebuilds and refresh their pathfinders
- TransportShips auto-retreat if their destination becomes water
- Water nuke craters use a smoothed angular noise ring with a
bounding-box scan instead of the regular per-tile random coin flip with
BFS, producing clean blob-shaped craters without scattered land pixels
that players would have to boat to individually

The `TerrainLayer` now incrementally repaints tiles that changed terrain
type, and tile update packets encode the terrain byte alongside tile
state so clients can reflect water conversions in real time.

When `waterNukes` is disabled, behavior is unchanged (fallout only).

Includes a new test suite (WaterNukes.test.ts) covering the conversion
pipeline, ocean propagation, magnitude recalculation, shoreline updates,
and minimap sync.

Also adds a new public game modifier for the special rotation.

### The only problem
A bit of lag on impact. But otherwise it works great and is fun. Maybe
needs some followup improvements if it gets merged.
I think its very cool in baikal / four islands team games. Chip away the
territory of your opponents.
Its also fun to turn The Box / Alps into a water map (its actually
possible to boat-trade then)

### Media

Video does not show the updated craters


https://github.com/user-attachments/assets/aed8bf08-0e94-4484-b997-4de11ae313d9

Updated craters (no tiny islands after impact):

<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/e896870b-bc9d-493d-8bc8-b3a5427d69d3"
/>

<img width="1472" height="920" alt="image"
src="https://github.com/user-attachments/assets/677065aa-0159-48cd-af44-a91b0f57adfc"
/>

<img width="1296" height="892" alt="image"
src="https://github.com/user-attachments/assets/886ffaba-541f-4e46-97c6-ce963f632fe0"
/>

## Please complete the following:

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

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

FloPinguin
2026-04-08 20:56:02 -07:00
VariableVince 21c286189e Perf/fix(NukeExecution/NukeTrajectoryPreviewLayer): unnecessary fetch and pass to listNukeBreakAlliance (#3546)
## Description:

Perf/fix: listNukeBreakAlliance doesn't ask for allySmallIds and doesn't
do anything with it. But both NukeExecution and
NukeTrajectoryPreviewLayer do fetch and pass allySmallIds to it.

Make allySmallIds optional, have wouldNukeBreakAlliance (which does use
allySmallIds) handle it potentially being undefined, and remove fetch
and pass of allySmallIds to listNukeBreakAlliance.

## Please complete the following:

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

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

tryout33
2026-04-02 11:19:55 -07:00
VariableVince eb51853b05 Perf/Fix: spawn and other functions that need closest by unit (#3243)
## Description:

Performance improvements.

- **PlayerImpl**: for _nukeSpawn_, cache config to const.
- **Other files**: for nukeSpawn and other functions doing the same,
introduce findClosestBy function.
- for **TradeShipExecution**, with the move from _distSortUnit_ to
_findClosestBy_, also add check if port isActive, !_isMarkedForDeletion_
and !_isUnderConstruction_. These checks should have been there already,
so now do it in one go to make use of the predicate isCandidate in
findClosestBy.
- for **TradeShipExectution.test.ts**, add mock functions for
_isMarkedForDeletion_ and _isUnderConstruction_ because of the above.
Also, set Unit tiles and Pathfinding node to actual valid TileRefs for
the testing map. This prevents NaN as return value from manhattanDist.
This problem was already present with the use of distSortUnit, but that
function just did NaN - NaN, returned the first and only port unit in
the array and called it a day. For findClosestBy we have to make sure
the predicate manhattanDist actually returns a number instead of NaN so
we need actually valid tiles. We now have a working test instead of a
test that actually silently failed like before.
- **PlayerImpl**: _warshipSpawn_ and _nukeSpawn_: Make use of the
isCandidate predicate of findClosestBy to have warshipSpawn not return
ports under construction or (smaller change) inactive. This fixes a bug
i have seen right away (where Warship spawns from under construction
Port).
Same for _nukeSpawn_ silos, don't return inactive silo just to be sure
now that we can easily add it to isCandidate predicate anyway. This
costs no performance in the _nukeSpawn_ benchmarks actually. This should
as a by-effecft fix an edge case bug i have seen, where a nuke is sent
from a phantom silo.

Some of this goes along with PR #3220 since playerImpl buildableUnits
makes use of the underlying spawn functions via canBuild. Just like
ConstructionExecution does. But i didn't want to add more to PR 3220
since there's already a lot in there.

The new function _findClosestBy_ could also be applied to some other
parts of code to benefit of it being faster, so i did that.

_findClosestBy_ uses _findMinimumBy_, which is a little more generic in
name. I think _findMinimumBy_ could be used by other parts of code,
while _findClosestBy_ is more clear naming for what it does now. But we
could ditch _findMinimumBy_ and just leave findClosestBy?

Examples of synthetic benchmarks (not included in this PR):

**BEFORE CHANGES (before Scamiv's PR #3241)**
<img width="705" height="91" alt="image"
src="https://github.com/user-attachments/assets/d6d91c08-39f1-4387-9ccc-e51951caa539"
/>

<img width="751" height="101" alt="image"
src="https://github.com/user-attachments/assets/80d400ac-3408-4107-aa58-6d2a847311e9"
/>

**AFTER CHANGES (before  Scamiv's PR #3241)**
![findunittoupgrade for loop 5th
run](https://github.com/user-attachments/assets/b840111b-e7e0-49b5-ace1-299a322224b5)

![nukespawn for loop 3rd
run](https://github.com/user-attachments/assets/47cfc444-9549-4887-8c0e-007277d24485)


**BEFORE CHANGES (after Scamiv's PR #3241)**
![findunittoupgrade
BEFORE](https://github.com/user-attachments/assets/c51e2cec-6171-4204-ba3f-48ed282978eb)

![nukespawn
BEFORE](https://github.com/user-attachments/assets/f7ce9a33-32d6-4875-a529-41724fd4d89f)

**AFTER CHANGES (after Scamiv's PR #3241)**
<img width="717" height="96" alt="image"
src="https://github.com/user-attachments/assets/5b106843-bf6e-4448-a8e8-94448fb30ced"
/>

<img width="767" height="92" alt="image"
src="https://github.com/user-attachments/assets/e6714c7b-26c1-455b-adae-f0060f1cbc7b"
/>




_Also see more **BEFORE** and **AFTER** in this comment:_

https://github.com/openfrontio/OpenFrontIO/pull/3243#issuecomment-3949060395

_And here a comparison in the flame charts:_

- based on the same replay and tried to get the performance recording
going at the same speed and length but always end up with small
differences
- because of a bug in replays currently, it puts you in with the same
clientID/persistantID currently. This means we can also record part of
what is normally only recordable with live human input (the
playerActions/playerBuildables).


**BEFORE** flame chart with nukeSpawn (human player) and maybeSendNuke
(Nation players, uses nukeSpawn via canBuild):

![BEFORE nukespawn Schermafbeelding 2026-03-04
231707](https://github.com/user-attachments/assets/3de7de16-769e-4748-b201-d71c5b75e16e)

![BEFORE maybesendnuke B Schermafbeelding 2026-03-04
230009](https://github.com/user-attachments/assets/16924c77-21c2-4a2d-b784-a469dce15538)

![BEFORE main build Schermafbeelding 2026-03-04
222017](https://github.com/user-attachments/assets/67e99fd6-335c-4e12-a9dc-ad5ae7d74de4)


**AFTER** flame chart with nukeSpawn (human player) and maybeSendNuke
(Nation players, uses nukeSpawn via canBuild):

![AFTER nukespawn Schermafbeelding 2026-03-04
230613](https://github.com/user-attachments/assets/a4eec0ae-d654-44c9-bf89-61567203d748)

![AFTER maybesendnuke B Schermafbeelding 2026-03-04
230009](https://github.com/user-attachments/assets/80e2366d-406b-403a-854c-6fa156713abc)

![AFTER maybesendnuke C Schermafbeelding 2026-03-04
230009](https://github.com/user-attachments/assets/71497e8a-81d0-4722-80f7-427f09d9c21e)

![AFTER maybesendnuke D Schermafbeelding 2026-03-04
230009](https://github.com/user-attachments/assets/55f131cc-e6e5-48f2-9e8d-771c60280640)

![AFTER main build Schermafbeelding 2026-03-04
222017](https://github.com/user-attachments/assets/1927ecb6-d54d-4e1e-8aa4-4f97602e2234)


## 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
2026-03-23 16:23:49 -07:00
Mattia Migliorini 2ec12f0a3a Auto-reject alliance request when transport ship is sent to target player (#3477)
## Description:

When a player sends a transport ship toward another player's territory,
any pending alliance request from the target is now automatically
rejected.
This mirrors the behavior already in place for direct attacks,
preventing a player from exploiting a pending alliance request while
launching a naval invasion.

## 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
2026-03-20 10:56:36 -07:00
Ryan 1049b7e7dc Clan System Part 1 (#3276)
## Description:

Properly split out clantags and usernames, a clantag should not be part
of a username.

<img width="285" height="286" alt="image"
src="https://github.com/user-attachments/assets/8ac56e82-b12c-4fc0-9774-e445252a6e61"
/>

https://api.openfront.dev/game/ojkqZFb2


<img width="296" height="596" alt="image"
src="https://github.com/user-attachments/assets/85152f80-c111-4f87-b85b-8516c9c6137b"
/>


https://api.openfront.dev/game/MF32BkVc


requires;
https://github.com/openfrontio/infra/pull/264

## Please complete the following:

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

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

w.o.n
2026-03-17 15:55:47 -07:00
VariableVince 6952550014 Fix: player name and location on wrong spot on the map (#3455)
## Description:

Fixes https://github.com/openfrontio/OpenFrontIO/issues/1021

Fixes issue that has been there since the beginning. Player name and
location and conquest FX (swords) not being in the right place. It can
happen at any time during a game and can be game-breaking in that
regard.

This makes it hard to find players, especially when trying to eliminate
their last few tiles on some island. So when clicking name in
leaderboard > wrong tiles. And when seeing name > above wrong tiles. Bug
report:
https://discord.com/channels/1284581928254701718/1444669324571967680

Also, when removing those last tiles, the wait time between updates of
player location can make it frustrating to find and eliminate them fast.
You need 2-3 clicks on their name in leaderboard, before finally being
moved to their current location.


**Cause:**
largestClusterBoundingBox not being changed when last attack happened in
same tick removeClusters last ran.

**Fix:** 
Also call removeClusters, and therefore update largestClusterBoundingBox
, when LastTileChange was AT lastCalc tick.

**Also:** 
Run removeClusters if player owns less than 100 tiles, don't wait for
ticksPerClusterCalc in that case. This way, sniping off the last couple
of island tiles of the player is easier. So it doesn't take 2-3 clicks
bbut just 1 click on the player name in the Leaderboard before the
camera moves to the next little island they are on. Also their last
clusters are annexed faster, only helping with the faster cleanup.
I think this is an optional to the fix in this PR, but still an
important QoL fix for sniping those last tiles quickly.

**BEFORE:**

https://github.com/user-attachments/assets/0960a4d3-7f8b-4368-9531-8244356bff17

**AFTER:** (also notice how it now just takes 1 click in the leaderboard
to immediately go to their next location, not 2-3 clicks)
https://youtu.be/qXJPekjsrP4

## 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
2026-03-17 14:08:52 -07:00
VariableVince 0dc8fbf129 Fix inverse annexation (#3448)
## Description:

An inverse annexation could happen where the small player (even with
0,01% tiles owned) could fully annex the large player.

**TL;DR:** basically wrong use of calculateBoundingBox in
surroundedBySamePlayer, feeding it all bordertiles, making enemyBox far
bigger than it actually was in some cases. Which resulted in enemyBox of
small player with two small clusters at some distance from each other,
being seen as inscribing the largest cluster of the bigger player. While
that largest cluster is actually the border tiles of the bigger player
surrounding the main cluster of the small player. Instead of an
annexation of small by bigger, small would incorrectly annex bigger
completely.

**Situation:** bigger player fully surrounds main cluster of smaller
player. Those border tiles are also the largest cluster of the bigger
player, for which surroundedBySamePlayer is called.
SurroundedBySamePlayer finds the small player as the only bordering
enemy of this cluster. Then it needs to check which of the two players
is surrounded by the other one. EnemyBox uses calculateBoundingBox with
all border tiles of the small player as argument. The small player also
has at least one seperate cluster elsewhere, could be on another island,
which count as border tiles too. The enemyBox from the main cluster of
the small player to the seperate cluster elsewhere, can be huge. Now
inscribed() is called and it determines that largest cluster box of the
bigger player (which was in fact calculated correctly, also making use
of calculateBoundingBox) is surrounded by the bigger enemyBox. And so
the small surrounded player fully annexes the bigger player.

**Fix:** instead of a global enemyBox, we only need the localEnemyBox
that touches the largest cluster of the bigger player. With that,
inscribed() can correctly conclude that largest cluster box surrounds
the localEnemyBox. As a matter of fact isSurrounded() already used the
same method to calculate its enemyBox as introduced by @scamiv for v30:
https://github.com/openfrontio/OpenFrontIO/pull/3127/changes#diff-fb1101a2b50dd7c353d082ff7a3351cff5469b8249b3ebca91c10573a3dfaaf1

- Change in PlayerExecution
- Added test NoInverseAnnexation.test.ts, which fails before and passes
after the fix

The bug was introduced in this commit 10 months ago:
https://github.com/openfrontio/OpenFrontIO/commit/c4381a9ad3828b06764ab1a21fc1514e37aacfd7

It has probably led to some weird annexations happening since then. The
bug could seemingly happen on any map. But was noted recently a few
times on square islands (Sierpinski) or maps (The Box/The Alps), where
the circumstances probably highten the chances of the bug occuring.

**Bug reports:**

https://discord.com/channels/1359946986937258015/1481916231689703477/1481916231689703477

https://discord.com/channels/1359946986937258015/1481916231689703477/1481963273367851030

https://discord.com/channels/1284581928254701718/1479993924432171008/1479995658302652496

https://discord.com/channels/1284581928254701718/1479993924432171008/1481865495492956182
https://discord.com/channels/1284581928254701718/1483047153571201034

**BEFORE:**

https://github.com/user-attachments/assets/4440182b-f696-45cf-bb01-b10159df8763

**AFTER**, on the same replay but with the bugfix:

https://github.com/user-attachments/assets/5f461ab2-eb62-4cc3-ae07-e2224adbbc6a

## 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
2026-03-17 11:15:18 -07:00
FloPinguin aa59dd23be Rebalance HvN (#3433)
## Description:

For the next v30 fix version

<img width="868" height="364" alt="imaege"
src="https://github.com/user-attachments/assets/520a999c-67e7-4c57-8651-895ad9eeb73a"
/>

HvN balancing for the revamped difficulty steps of v30 sadly doesn't
really work out...
In medium difficulty games humans nearly always win (boring)
In hard difficulty games humans usually lose
It was intended differently...

So lets get rid of medium difficulty HvN, always use hard difficulty and
disable the donation-capability for public game nations.
That will tune the human winrate towards a middle ground at about 65% I
think. Which should be nice.
Easier than in v29 (was frustrating sometimes) but not as easy as it's
now.

We can only test this in prod lol

## 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
2026-03-15 21:28:40 -07:00
FloPinguin 3013133d08 Embrace the aftergame! 😄 (#3410)
## Description:

In v30 we have the following change to prevent teammates from destroying
your structures:

**Block nuking teammate structures** - Nukes blocked if they'd hit a
teammate's structure (that was possible by nuking oceans / rivers) (by
@FloPinguin)

Original idea was from Wonder.
I think it makes sense, but it has a side effect: The aftergame, which
many players love, will be dead because of this change.

<img width="835" height="103" alt="image"
src="https://github.com/user-attachments/assets/521b7915-be28-4d83-8d45-65835e7385ab"
/>

<img width="1101" height="105" alt="image"
src="https://github.com/user-attachments/assets/db74a9c6-da12-44a2-aa06-f042b8e58b8a"
/>

I think a lot of complaints will follow after v30 is live. 

So why not add a little bit of logic for the aftergame?
After a team wins/loses, players can nuke their teammates. No longer
need to aim for water. SAMs also intercept teammate nukes in this 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:

FloPinguin
2026-03-11 20:39:39 -07:00
Evan 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
2026-03-10 20:16:47 -07:00
PGray be9ea14fe9 refactor: rename Bot to Tribe in internal execution code (#3372)
## Description

Follows up on #3290 which renamed the user-facing "Bots" to "Tribes".
This renames the internal implementation to match:

- `BotExecution` → `TribeExecution`
- `BotSpawner` → `TribeSpawner`
- `BotNames` → `TribeNames` (`BOT_NAME_*` → `TRIBE_NAME_*`)

All callers updated accordingly. `PlayerType.Bot` and `ColoredTeams.Bot`
are intentionally left unchanged as they are serialised wire-format
values.

Closes #3335

## Please complete the following:
- [x] My changes do not break existing functionality
- [x] I have tested my changes

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

---------

Co-authored-by: PGray <PGrayCS@users.noreply.github.com>
2026-03-09 21:31:18 -07:00
Ryan 0eb23c0c8c clientId replay bugfix (was picking first clientID in the array) (#3369)
## Description:

clientId replay bugfix (was picking first clientID in the array)

https://discord.com/channels/1359946986937258015/1479543573404844042

## Please complete the following:

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

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

w.o.n
2026-03-06 13:58:10 -08:00
FloPinguin cf3e3a7a74 Fix bot/nation player ID collisions causing missing players 🔧 (#3354)
## Description:

BotSpawner used the same PRNG seed (simpleHash(gameID)) as
createGameRunner, causing bot IDs to collide with nation IDs. When a
bot's SpawnExecution found a nation with the same ID via hasPlayer(), it
silently reused that nation instead of creating a new player - resulting
in far fewer players than configured (e.g. ~670 instead of 800 with 400
bots + 400 nations) with no console warnings.

Offsets the BotSpawner seed by +2 to avoid the shared PRNG sequence
(matching the +1 pattern already used by Executor).

## 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
2026-03-04 16:54:29 -08:00
VariableVince 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>
2026-03-04 11:32:45 -08:00
Ryan 49b69b6fa1 [Bugfix] Force end 170mins (#3326)
## Description:

instead of just killing the server, lets save 10m before, then kill the
server at 3hr mark, so at least we have a proper savegame.

## Please complete the following:

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

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

w.o.n
2026-03-02 10:51:23 -08:00
Mattia Migliorini 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>
2026-03-01 21:33:41 +00:00
Ryan 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
2026-03-01 12:11:00 +00:00
Mattia Migliorini 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
2026-03-01 11:20:19 +00:00
scamiv 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
2026-02-27 20:54:42 -08:00
bijx 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
2026-02-24 21:31:06 -06:00
FloPinguin 339ace0bd6 v30 nuke wars preparation: Disable boats & Team spawn zones (#3263)
## Description:

Preparation for nuke wars, for v30.
Next PR will be adding the nuke wars modifier for public games, but
Wonders https://github.com/openfrontio/OpenFrontIO/pull/3224 needs to be
merged first to avoid merge conflicts.

### 1. Disable boats setting

It's possible to disable `UnitType.TransportShip` now. Because they are
not needed in nuke wars and can even be annoying.

<img width="720" height="320" alt="image"
src="https://github.com/user-attachments/assets/661bc10d-b204-4b4f-b876-ee7c9b92de8c"
/>

### 2. Team spawn zones for random spawn

Maps can have `teamGameSpawnAreas` in their json file now.
Spawn areas are currently active if 
- a supported map is chosen (Baikal Nuke Wars or Four Islands)
- a supported team size is chosen (2 teams on Baikal Nuke Wars or 2/4
teams on Four Islands)
- random spawn is enabled

## 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
2026-02-23 16:12:24 -06:00
VariableVince 4788316504 Small refactor: unnecessary Array.from (#3279)
## Description:

Array.from was performed on this.player.alliances(), which already
returns an array. Also it was saved in a const which isn't strictly
necessary, same goes for the array in the loop below it.

## Please complete the following:

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

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

tryout33
2026-02-23 09:17:15 +00:00
Mykola f7b39faca7 Spawn. Fix respawn near the previos spawn (#3278)
## Description:

Because spawning is prohibited on tiles that have an owner, this created
a problem when a person tried to spawn near the center of their previous
spawn. This was resolved by relinquishing all the tiles conquered by the
previous spawn.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [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
2026-02-22 22:10:00 +00:00
Mykola 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>
2026-02-22 15:51:05 +00:00
Mattia Migliorini 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>
2026-02-21 21:12:44 -06:00
FloPinguin 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
2026-02-20 23:16:03 -06:00
FloPinguin 07b5d6e41f Prevent self-retaliation crash when own nuke destroys own ship 🔧 (#3260)
## Description:

Noticed this in two singleplayer games:

When a nation's nuke destroys its own transport/trade ship in the blast
radius, `NationWarshipBehavior` incorrectly tries to retaliate against
itself, calling `updateRelation(self)` which throws (GameRunner tick
error).

Added a self-check in `maybeRetaliateWithWarship` to skip retaliation
when the destroyer is the player itself.

## 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
2026-02-20 17:44:57 +00:00
scamiv f6a08e16db Perf alloc (#3241)
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #(issue number)

## Description:

## PR Title
perf(core): reduce hot-path allocations & safe optimizations

This PR brings in a set of allocation-focused optimizations in core hot
paths

### Scope
- `src/core/execution/NukeExecution.ts`
- `src/core/execution/WarshipExecution.ts`
- `src/core/game/UnitGrid.ts`
- `src/core/game/PlayerImpl.ts`
- `src/core/configuration/DefaultConfig.ts`
- `src/core/execution/SAMLauncherExecution.ts`

### What Changed
- `NukeExecution.detonate`: reduced call overhead/allocations by caching
`mg`/`config`, avoiding repeated lookups, and using allocation-free
loops (no `forEach` closures) in the diminishing-effect pass.
- `WarshipExecution.findTargetUnit`: replaced allocate+sort flow with
single-pass best-target selection.
- `UnitGrid.nearbyUnits`: reduced call overhead and allocations via
single-type fast path and cached query coordinates.
- `PlayerImpl.units`: added fast paths for common small-arity type
queries (1-3 unit types).
- `DefaultConfig.unitInfo`: cached `UnitInfo` objects per `UnitType` to
avoid repeated object/closure creation.
- `SAMLauncherExecution` targeting: removed sort churn and streamlined
target selection with single-pass hydrogen prioritization.



### Rebase
- One conflict was resolved in `NukeExecution.detonate` by keeping
`main`'s diminishing-effect-per-impacted-tile behavior, while retaining
the allocation-reduction refactors.


## 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
2026-02-19 19:01:12 -06:00
DevelopingTom 031a17d880 Optimize nuke explosion (#3176)
## Description:

When a nuke detonates, the explosion is looping over each tiles. Among
other things, it filters the owner units to find the `TransportShips`
units.

This operation is cpu-intensive and repeated over each tiles can make
the tick noticably slower.

On this screenshot the detonation function took 100ms:
<img width="1617" height="488" alt="image"
src="https://github.com/user-attachments/assets/07009b18-4342-4caf-9e82-9ae5147b63f8"
/>
<img width="1645" height="375" alt="image"
src="https://github.com/user-attachments/assets/fe9ead87-550a-4166-96ab-092d0c08be82"
/>

I suspect it led to a few frame freeze when I MIRVed too.

Suggestion: loop over the impacted players rather than the tiles.

With this suggestion, the same nuke takes between 4ms to 20ms:
<img width="989" height="365" alt="image"
src="https://github.com/user-attachments/assets/25c0faf0-cc34-41b7-8091-b14bde6db595"
/>


However this changes the death formula used, as they were repeated over
each tiles with ever-smaller values, and with this change the operation
is done all-at-once. This will result in a different outcome.

In my opinion the performance gain is seductive enough to maybe tweak
the formula to make it work with this revised strategy.
What do you think?

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

IngloriousTom
2026-02-18 17:00:56 -06:00