mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-24 15:35:18 +00:00
290d6922ebcff2e7c0e8c76f089e0951b5f71606
754
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a2321eb824 |
balance(doomsday): floor the drain at 5% of max instead of wiping to zero (#4587)
## Description: The Doomsday Clock drain wiped a caught side's troops (and warship health) to **zero**, an effective death sentence, so a brief dip below the bar was unrecoverable. Floor the drain at **5% of each max** (`drainFloorPercent`) for both troops and warships: a doomed side is crippled, not eliminated, and recovers if it climbs back above the bar. The rising territory bar still forces a finish (a 5%-strength side is trivial to conquer, and the leader is drain-exempt). - Config: `drainFloorPercent` (5), applied to both drains. - Execution: clamp troop + warship-health removal to what sits above the floor (integer-only, deterministic). - HUD: the Collapsing rate is shown net of the floor. - Tests: drain-to-zero / warship-scuttle cases updated to the floor + added coverage. ## 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 |
||
|
|
6a6c142388 |
Fixed pirating not being disabled when a warship's owner has no port in that body of water (#4458)
> **Before opening a PR:** discuss new features on [Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small improvements as [issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose). You must be assigned to an `approved` issue — unsolicited PRs will be auto-closed. **Add approved & assigned issue number here:** Resolves #4291 ## Description: Fixes pirating not being disabled when a warship's owner has no port in that body of water ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: tktk1234567 |
||
|
|
4ce57efbe2 |
Rebalance Doomsday Clock: late-game stalemate-breaker (10min grace + wave squeeze), slower troop drain, gentler-but-steeper warship attrition (#4518)
Resolves #<add your approved & assigned issue number> ## Description: Rebalances the Doomsday Clock so it acts as a **late-game stalemate-breaker** rather than an early-game culler, softens how fast it removes troops and warships, and makes the HUD countdown clearer. **Clock schedule (all presets):** - A flat **10-minute grace** at 0% required share — the early game is decided by combat, not the clock. - Then a 6-wave squeeze at accelerating levels (4 / 9 / 16 / 26 / 40 / 55%) with short pauses, reaching the final 55% at each preset's cap: **45 / 35 / 25 / 15 min** for slow / normal / fast / veryfast. - `WaveSchedule` `rampSeconds`/`pauseSeconds` are now **per-wave arrays**, so the curve can be shaped (gentle early, steeper late) instead of one uniform ramp. `requiredBasisPoints` and the HUD companion `doomsdayClockWaveState` walk the per-wave segments in lockstep. **Troop drain:** warn window `10s → 30s`, drain eased (`2%→5%` over `90s`), so a caught side takes ~2 minutes to wipe instead of ~1. **Warship attrition:** warships get their own gentler start plus a **convex** decay curve — a ship caught when its side is first doomed lasts about as long as troops, but the rate ramps up steeply so a side at full attrition still loses its fleet in ~2s. Adds a `curveExponent` argument to `doomsdayClockDrain` (1 = linear, used for troops; higher = convex, used for ships). **Determinism:** the drain curve is **integer-only** (fixed-point power, no floats), so the floored per-tick loss is bit-identical on every client in the lockstep sim. The linear troop path keeps its exact existing integer form; only the convex warship path is reshaped. **HUD countdown clarity:** the clock readout now shows a live countdown in both states — `Will reach 16% in M:SS` while the bar is actively climbing, and `Starts rising to 26% in M:SS` during a pause (previously `Next 26% in …`, which read as if it jumped there instantly). Backed by a new `secondsToTarget` field on the shared wave state so the sim and HUD stay in agreement. All display text goes through `translateText()` / `en.json`. <img width="278" height="116" alt="image" src="https://github.com/user-attachments/assets/e0be3d2c-bb88-46be-b344-34d63e4859bd" /> <img width="304" height="171" alt="image" src="https://github.com/user-attachments/assets/da74bd05-0b9a-4aec-bbf5-e7380fbda88e" /> **Danger skull:** while a side is below the bar in the warn window, its on-map skull now blinks progressively faster as the countdown runs out (accelerating to the moment the drain begins), then holds steady once it is actually draining — a clearer "you are about to be hit" cue. Rationale: the previous schedule removed players heavily in the first half of a match and could leave a drawn-out endgame. Holding the clock at 0% early keeps the opening about fighting, and concentrating its pressure in the back half reserves it for actually breaking stalemates. Values are tuning starting points and easy to adjust in `DoomsdayClock.ts` / the config defaults. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: <add your Discord username> |
||
|
|
7fa81c6bb9 |
perf: reduce core live-memory footprint by 45% on large maps (#4507)
## Summary
Reduces the simulation's steady-state memory footprint. On Giant World
Map at 20 game-minutes (12 000 ticks, 400 bots, seed `perf-default`),
live memory after a full GC drops **293 MB → 161 MB (−45%)**; unforced
peak heap drops **326 MB → 165 MB**. The simulation also runs ~10%
faster (85 → 94 ticks/s). The final game-state hash is **bit-identical**
(`57830793797434300`) — no behavior change.
## Measurement (first commit)
The full-game perf harness gains a footprint mode:
- `--footprint` — forces a full GC at every `--window` boundary and
records the live heap / ArrayBuffer / RSS curve across the game
(requires `NODE_OPTIONS=--expose-gc`).
- `--snapshot-at 0,2000,12000` — writes V8 `.heapsnapshot` files at
chosen ticks.
- `HeapSnapshotRetainers.ts` — attributes every heap node to its nearest
meaningfully-named retainer (e.g. `PlayerImpl._tiles`), plus prints
retainer chains for all nodes ≥128 KB. `HeapSnapshotSummary.ts` is a
streaming fallback for snapshots too large to `JSON.parse`.
Baseline attribution at tick 12 000: player `_tiles`/`_borderTiles` Sets
**83 MB**, GameMap `refToX`/`refToY` lookup tables **38 MB**, two
duplicate 30.5 MB visited-scratch arrays, trade-ship stepper paths **15
MB**, a construction-only flood-fill queue **9.5 MB**.
## Optimizations
**Map-sized buffers (second commit):**
- `GameMap.x()/y()` compute `ref % width` / `(ref / width) | 0` instead
of reading two per-tile Uint16 tables (−38 MB). The arithmetic is
cheaper than the tables' random-access cache misses — this is where the
speedup comes from.
- `PlayerExecution` and `SpatialQuery` each kept their own per-game
generation-stamped visited `Uint32Array`; both now share one via
`TileTraversalScratch` (−30 MB).
- `PathFinderStepper` stores numeric paths as `Uint32Array` (half the
bytes; steppers hold their full path for a unit's whole journey).
- `ConnectedComponents` frees its flood-fill queue after `initialize()`.
**Player tile sets (third commit):**
- New `TileSet`: insertion-ordered set of tile refs backed by a dense
`Uint32Array` plus an open-addressing hash index — ~12 bytes/element vs
~34 for a native `Set<number>`. Deletes tombstone; compaction is
deferred while iteration is in progress so positions never shift under
an iterator.
- Iteration semantics match `Set` exactly (insertion order, entries
added mid-iteration visited, deleted ones skipped, delete+re-add moves
to end) — the simulation relies on this order for determinism, and the
unchanged hash confirms it.
- `Player.borderTiles()` now returns `ReadonlyTileSet` (a native `Set`
still satisfies it structurally); `GameRunner.playerBorderTiles` copies
into a real `Set` since that result crosses the worker boundary via
structured clone.
## Footprint curve (giant world map, live MB after forced GC)
| checkpoint | before | after |
|---|---|---|
| spawn end | 20 + 100 buf | 20 + 55 buf |
| tick 6301 | 119 + 161 buf | 29 + 127 buf |
| tick 12301 | 130 + 161 buf | 32 + 129 buf |
## Validation
- Final hash `57830793797434300` identical across baseline / round 1 /
round 2 runs (12 000 ticks).
- Full suite passes (1798 + 126 tests), including new `TileSet` tests:
order semantics, mutation-during-iteration parity with `Set`, tombstone
compaction, and a 20 000-op randomized differential test against native
`Set`.
- Runs recorded in
`tests/perf/output/footprint-{baseline,round1,round2}-giant.txt`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
66063d6178 |
feat(doomsday-clock): decay warships alongside troops for doomed sides (#4499)
## Description: Follow-up to #4469. The Doomsday Clock drains a doomed side's troops but leaves its navy untouched, so a coastal or island turtle can sit below the bar indefinitely on warship defense, exactly the stall the clock is meant to break. This decays the warships of a flagged (sub-threshold, non-leader) side on the same ramp as its troops: - Each warship loses a percentage of its (veterancy-adjusted) max health per second, reusing `doomsdayClockDrain`, so the fleet and the army bleed in lockstep and reach zero together (~55s from full at the default rate). - Destruction passes **no attacker**, so it routes through `UnitImpl.delete` as an environmental loss: no kill credit, no boat-destroy stats, no veterancy granted. Scoring integrity is preserved. - Healing is suppressed for a flagged owner (`WarshipExecution.healWarship` early-returns), so the decay actually sinks the fleet instead of being out-healed at a port. Inert when the mode is off, since the mark is never set. - The leader's fleet is spared, same as its troops. No new config: warships reuse the existing drain curve. No HUD change, since warships count as part of the side's forces alongside troops. Tested: 4 new unit tests (same-ramp decay, no-kill-credit destruction, leader spared, warn-window grace), the full `DoomsdayClockExecution` and `Warship` suites, the whole test suite (1784 passing), `build-prod`, and a headless full-game sim run (resolves cleanly with the decay live, deterministic). |
||
|
|
20c81ca5f6 |
perf: cut core-sim GC churn another 36% (75% cumulative) (#4498)
## Summary Round 3 of GC-churn reduction (follow-up to #4494 and #4496). All changes are **behavior-preserving** — final game-state hash unchanged on three seeded runs. ### Changes | Site | Change | Churn target | |---|---|---| | `MiniMapTransformer` | Path upscaling works in pure numeric coordinates and emits main-map `TileRef`s directly, replacing three intermediate `Cell`-object arrays per path (cell path → scaled path → smoothed path → final map). Identical arithmetic, so identical rounding and identical tiles. | ~7.1 GB | | `ShoreCoercingTransformer` | Reused neighbor buffers instead of `neighbors()` arrays; no per-call `{water, original}` objects. Tie-breaking preserved (helpers share the unified N,S,W,E order since #4495). | ~1.5 GB | | `diffPlayerUpdate` | Allocation-free all-equal fast path. Runs per player per tick and usually returns `null` (gold/troops/tiles travel via packed arrays), but previously allocated the diff object + a closure first. Field list matches the diff exactly. | ~2.6 GB | | Large-`Set` iteration | `for..of` over a `Set` allocates an iterator-result object per element — significant on 100k-tile border sets. `calculateClusters`, `calculateBoundingBox` (indexed fast path for arrays too) and `getAttackFrontTiles` (also dropped its `neighbors()` arrays) now use `Set.forEach`. | ~3.6 GB | ### Results (Giant World Map, 400 bots, 12,000 ticks, seed `perf-default`) | Metric | Before | After | vs. original (pre-#4494) | |---|---|---|---| | Sampled allocations (incl. collected) | 37.8 GB | **24.1 GB (−36%)** | 97.7 GB (**−75%**) | | Ticks/sec | 82 | **88** | 66 (+33%) | | Mean / p99 tick | 12.2 / 36.0 ms | 11.3 / 34.8 ms | 15.2 / 49.9 ms | | Peak heap | 762 MB | 529 MB | 758 MB | ## Determinism Final hash unchanged on all three reference runs: - Giant World Map 12,000 ticks: `57830793797434300` ✓ - Giant World Map 2,000 ticks: `55125379638382860` ✓ - World 1,800 ticks: `32337437717390864` ✓ ## Test plan - [x] Full suite green (1,906 tests; the `getAttackFrontTiles` test stub gained a `neighbors4` implementation to match the real interface) - [x] Hash equality on 3 seeded headless runs (2 maps) - [x] Before/after 20-min GC benchmarks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9e9c608053 |
perf: cut core-sim GC churn another 36% (61% cumulative) (#4496)
## Summary Round 2 of GC-churn reduction, attacking the next tier of allocation sources found by the profiling harness from #4494. All changes are **behavior-preserving** — the simulation is bit-identical (final hash unchanged on three seeded runs). ### Changes | Site | Change | Churn target | |---|---|---| | `Player.units()` / `Game.units()` | Rest parameter → fixed-arity + array overloads (`units()`, `units(types[])`, `units(t1, t2?, t3?)`). The rest array was allocated on **every call** of one of the hottest functions in the sim. Spread call sites (`units(...Structures.types)`) now pass the array directly. `GameImpl.units()` builds one flat array instead of `Array.from().flatMap()` per-player intermediates. | ~18 GB | | `PlayerExecution` cluster flood fill | Results are plain `TileRef[]` in mark order instead of `Set<TileRef>` — the generation-stamped visited array already deduplicates, and consumers only iterate/measure. DFS stack reused across fills. | ~3.7 GB | | `SpatialQuery.bfsNearest` | Fused generation-stamped BFS with per-game scratch buffers (`WeakMap`-keyed, same pattern as `PlayerExecution`) instead of materializing a `Set` of the entire search area per query. Identical traversal and tie-breaking. | ~2.2 GB | | `NationWarshipBehavior` ship tracking | Single-pass loops instead of `filter().forEach()`; dropped defensive `Array.from(set)` copies (deleting the current entry while iterating a `Set` is well-defined). | ~1.4 GB | ### Results (Giant World Map, 400 bots, 12,000 ticks ≈ 20 game-min, seed `perf-default`) | Metric | Before | After | vs. pre-#4494 | |---|---|---|---| | Sampled allocations (incl. collected) | 59.2 GB | **37.8 GB (−36%)** | 97.7 GB (**−61%**) | | GC count / total pause | 1,076 / 1,830 ms | 772 / 1,442 ms | 1,682 / 3,313 ms | | Ticks/sec | 73 | **82** | 66 (+24%) | | Mean / p99 tick | 13.6 / 39.2 ms | 12.2 / 36.0 ms | 15.2 / 49.9 ms | `units()` no longer appears in the top-30 allocator list at all. The remaining leaders (possible round 3): the minimap pathfinding `Cell` pipeline (~8.5 GB), `diffPlayerUpdate`/`toFullUpdate` per-tick serialization (~4.6 GB), and iterator allocations (~3.3 GB). ## Determinism Final game-state hash unchanged on all three reference runs: - Giant World Map 12,000 ticks: `57830793797434300` ✓ - Giant World Map 2,000 ticks: `55125379638382860` ✓ - World 1,800 ticks: `32337437717390864` ✓ ## Test plan - [x] Full suite green (1,905 tests), including updated `units()` semantics tests (array overload, snapshot isolation, insertion order) - [x] Hash equality on 3 seeded headless runs (2 maps) - [x] Before/after 20-min GC benchmarks on the same commit base 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
22d5aba5ae |
refactor: standardize cardinal-neighbor iteration on neighbors() N,S,W,E order (#4495)
## Summary Follow-up to #4494. That PR added `forEachNeighborNSWE` as a third neighbor iterator because the existing allocation-free helpers (`forEachNeighbor`, `neighbors4`) visit in W,E,N,S order while `neighbors()` visits N,S,W,E — and substituting one for the other changes simulation behavior at order-sensitive call sites. This PR removes that duplication by standardizing on **one order everywhere**: `forEachNeighbor` and `neighbors4` now visit in the same N,S,W,E order as `neighbors()`, and `forEachNeighborNSWE` is deleted. ## ⚠️ Intentional behavior change Callers of the flipped helpers that are order-sensitive now make different (equally valid) decisions: - `AttackExecution.addNeighbors` — PRNG values are drawn per neighbor while building the conquest frontier, so attack expansion patterns differ - `AttackExecution.handleDeadDefender` — a dead defender's tiles go to the *first-visited* adjacent player - `WarshipExecution.bestNeighborToward` — distance ties break by visit order - `PlayerExecution` surrounded-cluster flood fill — set insertion order propagates to conquer order Game outcomes for a given seed differ from previous builds (verified: the 12k-tick reference run ends with 31 players alive vs 24 before). Determinism across clients *within* a build is unaffected — all clients run the same code, so there is no desync risk. Replays/verification pinned to old hashes will not match this build. New reference hashes for the headless perf harness (seed `perf-default`): | Run | Final hash | |---|---| | giantworldmap, 12,000 ticks | `57830793797434300` | | giantworldmap, 2,000 ticks | `55125379638382860` | | world, 1,800 ticks | `32337437717390864` | ## Verification - [x] Full suite green (1,901 tests), including new exact-order contract tests: `forEachNeighbor` and `neighbors4` must match `neighbors()` contents **and order** for every tile - [x] 20-game-minute Giant World Map benchmark: no perf regression (73 ticks/sec, GC 1.2% of wall, allocation profile unchanged) - [x] Order-sensitivity audit of every `forEachNeighbor`/`neighbors4` call site (sensitive ones listed above; the rest are booleans, counts, or min/max accumulations) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5e4b2791aa |
perf: reduce core-sim GC churn 42% and add GC-churn profiling to the perf harness (#4494)
## Summary
Reduces core-simulation GC churn by **42%** on a 20-game-minute Giant
World Map run, and extends the headless full-game perf harness so churn
is measurable and regressions are visible.
### 1. GC-churn measurement (`tests/perf/fullgame/GcProfiler.ts`)
`npm run perf:game` now reports:
- **GC pauses** by kind (minor/major/incremental) via a
`PerformanceObserver` on `'gc'` entries, bucketed into tick windows by
timestamp (V8 only delivers these entries on a timer task, so they're
flushed after the run)
- **Allocation rate** per `--window N` ticks (default 1000) from
used-heap deltas sampled every tick, so churn can be tracked across game
phases
- **Top allocating functions** from the V8 sampling heap profiler with
`includeObjectsCollectedBy{Major,Minor}GC` — i.e. actual churn including
short-lived garbage, not live memory — plus a `.heapprofile` loadable in
Chrome DevTools (Memory → Allocation sampling)
New flags: `--window N`, `--no-gc-profile`, `--no-alloc-profile`.
### 2. Allocation reductions in the hot paths it found
| Site | Change |
|---|---|
| `GameMap.bfs` | inline neighbor enumeration instead of an array per
visited tile |
| `GameMap`/`Game` | new `forEachNeighborNSWE` — allocation-free
iterator matching `neighbors()` N,S,W,E order for order-sensitive
callers (`forEachNeighbor` visits W,E,N,S, so substituting it would
change sim behavior) |
| `PlayerImpl.nearby` / `sharesBorderWith` / `shoreReachableNeighbors` |
no per-call neighbor arrays; no materialized shore-tile array |
| `PlayerImpl.units(types)` | gather into a reusable scratch buffer,
return one exact-size slice (still a fresh snapshot array per call) |
| `AiAttackBehavior.maybeAttack` | single pass over border neighbors
replacing the `flatMap`/`filter`/`map` chain over every border tile |
| `AiAttackBehavior.isBorderingNukedTerritory` | reusable `neighbors4`
buffer with early exit |
| `SharedWaterCache.build` | allocation-free neighbor iteration |
| `SpatialQuery.bfsNearest` | first-minimum scan instead of
collect-then-stable-sort (identical result incl. tie-breaking) |
### Results (Giant World Map, 400 bots, 12,000 ticks ≈ 20 game-minutes,
seed `perf-default`)
| Metric | Before | After |
|---|---|---|
| Sampled allocations (incl. collected) | 97.7 GB | **56.9 GB (−42%)** |
| GC count / total pause | 1,682 / 3,313 ms (1.8% of wall) | 1,058 /
2,087 ms (1.2%) |
| Ticks/sec | 66 | 70 |
| p99 / max tick | 49.9 ms / 988 ms | 43.5 ms / 689 ms |
| Ticks over 100 ms budget | 31 | 19 |
## Determinism
Every rewrite preserves exact iteration order (the new NSWE iterator
exists precisely for the order-sensitive sites). Verified by identical
final game-state hashes on three runs: Giant World Map 12,000 ticks
(`67286276735690560`), Giant World Map 2,000 ticks, and World 1,800
ticks.
## Test plan
- [x] Full suite green (1,896 tests)
- [x] New tests: `forEachNeighborNSWE` order contract vs `neighbors()`
over every tile; `units()` filtering semantics (insertion order,
fresh-array guarantee, duplicate types, Set path)
- [x] Final-hash equality on 3 seeded headless runs (2 maps)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
78ef7b56fd |
feat(doomsday-clock): battle-royale style zone gamemode (#4469)
Resolves Issue #4463 ## Description: An optional game mode that (almost) guarantees a finish instead of letting late-game stalemates drag on. Originally called sudden death, renamed to Doomsday clock Once enabled, every side (each player in FFA, each whole team in team modes) must hold a rising share of the map. A side below the bar is skulled; after a short warn its troops bleed to zero, forcing consolidation to a winner. ### How it works - **Rising zone:** a grace period, then the required share ramps up linearly to each level with 30s pauses between (a battle-royale "zone"). Levels track the ofstats FFA territory median (3/5/10/20/30%). - **Four speed presets** (slow / normal / fast / very fast) change only the pace: normal ends ~30 min, very fast ~15. - **Troop decay:** a linear ramp as a % of max capacity, ~50s from caught to zero (10s warn + ~50s ≈ 1 min total). - **UI:** a HUD panel (live share vs target, wave/decay countdowns, red/orange cues) and an on-map skull above flagged players (blinks in danger, steady while draining). ### Notes for review - Off by default; no effect on existing games. However, as discussed we can add it to the modifier pool for public games to see how popular the gamemode is vs normal play. - Sim is deterministic (integer-only, in `src/core`), covered by unit + integration tests. - One-line addition to `GameServer.updateGameConfig` so the setting survives the host → server → client round-trip. - Status is packed into the existing name-pass data slot (`pd4.w`: 0/1/2 = none/danger/draining); the skull is composited into the icon atlas at load. ### Testing `npm test`, `npm run lint`, `npx prettier --check .`, `npm run build-prod` all pass. ### UI: <img width="243" height="100" alt="Image" src="https://github.com/user-attachments/assets/c4c9eeb0-4feb-437d-9aac-b2786a841b74" /> Dropdown between slow, normal, fast, very fast Before zone: <img width="302" height="175" alt="Image" src="https://github.com/user-attachments/assets/7359a1ea-4951-446d-a23c-0711fe06cc5d" /> Zone started, player not affected the pannel also blinks orange for 10s: <img width="297" height="175" alt="Image" src="https://github.com/user-attachments/assets/fcc565a5-d5d0-47a7-97ea-d0ba9d9ad899" /> Player affected, grace period (Danger): <img width="314" height="170" alt="Image" src="https://github.com/user-attachments/assets/ff96d21e-96f3-4ef9-8190-48eecc7aac0f" /> Skull icon blinking over player (everyone sees it) - older screenshot, the clipping has been fixed <img width="462" height="145" alt="Image" src="https://github.com/user-attachments/assets/53899211-33b1-40e1-83f2-77f2096f0cad" /> Player affected, grace period ended (Draining): <img width="360" height="159" alt="Image" src="https://github.com/user-attachments/assets/4b226d57-da4d-4866-ab5f-db48e4ed1ea2" /> Skull icon no longer blinking, everyone can see you are in a state of decay, and troops are draining: <img width="732" height="146" alt="image" src="https://github.com/user-attachments/assets/cd10fedb-6e87-4dfc-9fbf-55d3945a7901" /> Skull is visible like alliances icon also on player tab <img width="558" height="81" alt="Image" src="https://github.com/user-attachments/assets/6acdbe91-bdd0-40c7-942b-3990d4dae87f" /> (just UI example, best way to see it is to hop on a solo game and play against AI) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: zixer._ |
||
|
|
006f1690a5 |
Warship veterancy (#4433)
## Description: Warship veterancy! This is an idea inspired by the unit veterancy feature of games like C&C: Red Alert 2 in which unit eliminations increases the level of individual units. I've been trying to build this mechanic for months with different ideas, and I finally landed on this being one of the more balanced implementation. Warships can earn up to three levels, represented by the gold bar insignia in the bottom right of their warship sprite. <img width="622" height="202" alt="image" src="https://github.com/user-attachments/assets/a8c31a45-4ae9-41a9-b054-9c4a7f4ab1f1" /> A veterancy bar grants 20% health from the base amount, and a 20% increase in shell damage applied _after_ the random damage roll. For example, a level 3 warship will apply a 60% damage boost on top of the random shell damage value (something between 200-325. If the random value is 250, the final damage output will be `250 * 1.60 = 400`. There are three ways to achieve a veteran level: 1. **Eliminate another warship:** any time a warship neutralizes another warship, it immediately get's a veterancy increase. https://github.com/user-attachments/assets/6a9e0958-5171-4ca3-94f6-9c2300a12f8b 2. **Eliminate transport boats:** Destroying 10 transport boats will level a warship to the next veterancy bar. https://github.com/user-attachments/assets/619ce0c0-033c-4e0b-9c64-b41eabaa791b 3. **Steal trade ships:** If the warship captures 25 trade ships, it will earn a veterancy bar. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: bijx |
||
|
|
ccd0745ad4 |
Prevent AI from placing ports on small lakes 🚢 (#4429)
## Description: AI nations were placing ports on small decorative ponds scattered across maps (Missisipi for example), wasting structure slots on strategically useless water bodies. This fix adds a water component size check to the port placement logic so the AI skips lakes that are too small for meaningful port use. We already had a check for available trade partners, but trading in small lakes is usually stupid. **How it works:** - `ConnectedComponents` now tracks component sizes during its existing flood-fill (zero extra cost - counts tiles as they're visited) - `AbstractGraph`, `WaterManager`, and the `Game` interface expose `getWaterComponentSize(tile)` so callers can query the size of any water body - `NationStructureBehavior.randCoastalTileArray()` filters out non-ocean water components below `MIN_PORT_WATER_COMPONENT_SIZE` (3000 minimap tiles, ~12000 full-map tiles) - Ocean tiles bypass the check entirely since they're always large enough ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
6b95a23606 |
Fix anonymize-names desync: seed cluster-recalc offset from id() not name() (#4426)
## Description Fixes a hard desync that hit anonymous-names lobbies (e.g. the OFM tournament): in an anonymized FFA, roughly half the clients desynced. ### Root cause `PlayerExecution.init()` seeded the per-player phase offset that schedules `removeClusters()` from the player's **username**: ```ts this.lastCalc = ticks + (simpleHash(this.player.name()) % this.ticksPerClusterCalc); ``` `removeClusters()` is not display-only — it both sets `largestClusterBoundingBox` (read by `AiAttackBehavior` for targeting) **and removes disconnected/surrounded territory, mutating tile ownership**. The anonymize-names option (#4318) sends each client a *different* username for the same player (`anonName(target + viewer)`). So `simpleHash(name()) % 20` differs per client → `removeClusters()` runs on different ticks per client → `numTilesOwned()` diverges → the every-10-tick state hash (`simpleHash(id) * (troops + numTilesOwned) + units`) diverges → **desync**. **Why only half the lobby:** clients granted real-name reveal (host / admins / casters via `nameReveals` / `nameRevealPublicIds`) all see real names, compute identical offsets, and stay in sync with each other and the server record. Every non-granted (anonymized) client sees a unique random name per player and diverges. Hence the clean split. This line has been latent and harmless since 2024 (`f01949f0`) because `name()` used to be identical on every client; #4318 was the first feature to feed a per-client value into the core. The PR comment in `GameServer.ts` even states the assumption — *"username … neither of which the simulation reads"* — which turned out to be false. ### Fix Seed the offset from `id()` instead of `name()`. Player `id()` is assigned from a `gameID`-seeded PRNG by spawn order, so it is identical on every client while still spreading cluster recalculation across players (the line's original load-balancing intent). One-line sim change; no schema/wire change. ### Verification / scope - Audited every `name()`/`username` read in `src/core/`: this was the **only** state-affecting one (all others are `console.log` / error strings / display-only update fields). So this closes the whole desync class. - Confirmed player order, ids, config, `clanTag`/`friends` and cosmetics are all client-identical under anonymize-names — `username` was the sole per-client field reaching the sim. - Swept the other recent core commits (impassable terrain, rail network, nations AI, inline sfc32 PRNG, troop/economy changes) for independent determinism regressions — none found. ### Follow-up `name()` should be removed from the core `Player` surface entirely so a per-client username can never re-enter the sim again (the remaining reads are logging + the display payload the renderer needs). Tracked separately. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
71d70dfb0e |
fix: prevent client from bypassing random spawn selection 🛡️ (#4428)
## Description: When random spawn mode is active, players are supposed to receive randomly chosen spawns rather than choosing their own. However, `SpawnExecution.getSpawn()` checks `center !== undefined` first, which means if a player manually injects coordinates into the spawn intent (bypassing the client-side UI guard), the random selection logic is completely bypassed and the player gets their chosen coordinates. This was fully exploitable in singleplayer (where no pre-created `SpawnExecution` objects exist) and was a defense-in-depth gap in multiplayer (relying on execution order of pre-created spawns to block it via the `hasSpawned()` guard). The fix forces `center` to `undefined` in `getSpawn()` when random spawns are enabled, ensuring the random selection code path is always taken regardless of what the client sends. ## Changes: - `src/core/execution/SpawnExecution.ts`: Pass `undefined` to `getSpawn()` when `isRandomSpawn()` is true, ignoring any client-specified tile - `tests/core/execution/SpawnExecution.test.ts`: Added test verifying that a client-specified tile is ignored when 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
170f506200 |
Fix transport ship's troop count to update when a hydro hits the player. (#4381)
**Add approved & assigned issue number here:** Resolves #4308 ## Description: When nuclear damage reduces a player's troop count, it also affects any transports ships in the water. This works well and is useful to avoid exploiting tranports to avoid hydro damages. `UnitImpl.setTroops()` changes the transports troop count without queuing a unit update. the core value changes, but the client never receives a fresh UnitUpdate unless something else touches the ship. - UnitImpl.ts now emits a UnitUpdate when a unit troop count actually changes. - NukeExecution.ts batches transport ship nuke losses, then applies one final troop update per ship. - Attack.test.ts now asserts the nuke tick includes a transport UnitUpdate with the reduced troop count. ## Please complete the following: - [N/A] I have added screenshots for all UI updates - [N/A] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: elliotlepley |
||
|
|
805f0968b1 |
Add impassable terrain 🗺️ (#4340)
## Description: Relates to #3725 Adds a new **Impassable** terrain type that enables non-rectangular maps and creates impassable barriers on the map. Painted with pure black (`#000`) in the map editor's `image.png`. **Encoding:** Impassable terrain is encoded in the binary format as `isLand=1, magnitude=31` (previously unused). The Go map generator detects `#000` pixels and produces this encoding. The map generator's minimap downscaling gives impassable highest priority (Impassable > Water > Land). Thumbnails render impassable as transparent so the map picker background shows through. **Rendering:** Impassable tiles render as the map background colour (`rgb(60, 60, 60)`, matching `gl.clearColor` in `Renderer.ts`), making them visually indistinguishable from the area outside the map quad. This enables maps to appear non-rectangular. **Gameplay restrictions:** Impassable terrain cannot be: - Owned (`conquer()` throws) - Attacked (`AttackExecution` skips impassable tiles in both `tick()` and `addNeighbors()`) - Nuked (targeting rejected in `nukeSpawn()`, blast radius filtered in `tilesToDestroy()`) - Spawned on (nations, human players, and structures all reject impassable tiles) - Converted to water (guarded in `WaterManager` and `setWater()`) **Nuke trajectories:** Nuke trajectories cannot cross impassable terrain, matching the existing map-border enforcement. This is checked at launch time in `NukeExecution.tick()`. The client-side trajectory preview turns red with a red X where the arc crosses impassable terrain (reusing the existing SAM-intercept visual pipeline in `NukeTrajectory.ts`). The nuke ghost preview is completely hidden when hovering over impassable terrain (same as hovering outside the map). https://github.com/user-attachments/assets/ff131146-9749-41e0-892a-617e5cd16c54 Impassable terrain is transparent on the thumbnail: <img width="213" height="152" alt="Screenshot 2026-06-18 211640" src="https://github.com/user-attachments/assets/ede16f8c-9239-4ab1-be5d-0ba81cce5e9e" /> Tested with water nukes, made sure there is no water depth gradient near the impassable terrain, just like at the world border: <img width="774" height="771" alt="Screenshot 2026-06-18 212348" src="https://github.com/user-attachments/assets/4429069d-911b-48e8-91e3-7307d42c9397" /> Models used: GLM 5.2 and MiMo 2.5 Pro 😄 ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
21291b9fa3 |
Add trade ship captured event with toggle setting (#4344)
## What Notify a player when one of their trade ships is captured. The alert appears in the **less-important (top) events tier** and is gated behind a new in-game setting (on by default). ## Why Previously there was no notification to the player who *lost* a trade ship — only the capturer got a transient +gold pip on the ship's arrival. This surfaces the loss to the victim, while letting players opt out if they find it noisy. ## Changes - **`src/core/execution/TradeShipExecution.ts`** — On capture detection, emit a display message (`events_display.trade_ship_captured`, type `UNIT_DESTROYED`) to the original owner. Fires once, guarded by the existing `wasCaptured` flag. `UNIT_DESTROYED` is not a Tier-1 type, so it lands in the top/less-important tier. - **`src/client/hud/layers/EventsDisplay.ts`** — Suppress the message when the setting is off, following the existing key-based filter pattern. - **`src/core/game/UserSettings.ts`** — New `tradeShipCapturedEvents()` getter (default `true`) + `toggleTradeShipCapturedEvents()`. - **`src/client/hud/layers/SettingsModal.ts`** — New toggle in the in-game settings modal. - **`resources/lang/en.json`** — New `events_display.trade_ship_captured` and `user_setting.trade_ship_captured_label`/`_desc` keys. - **`tests/core/executions/TradeShipExecution.test.ts`** — Tests that the notification is sent to the original owner with the right args and only once across ticks. ## Notes - The setting is gated client-side (in `EventsDisplay`), keeping `src/core` free of client-local localStorage settings — consistent with how display events are already filtered there. - Reused `MessageType.UNIT_DESTROYED` (red/"loss" styling) rather than adding a new message type, to keep the change minimal. Happy to add a dedicated type/color if preferred. ## Testing - `npx vitest tests/core/executions/TradeShipExecution.test.ts --run` — 7 passed - lint clean, no type errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8a8079b979 |
Fix parallel bot boat attacks of nations 🐛 (#4297)
## Description: When a nation attacked multiple bots via boat attacks in parallel, each boat attack computed its troop allocation independently using `player.troops() / 5` without subtracting `botAttackTroopsSent`. The cumulative troop commitment could exceed the nation's actual troop count, and when the queued `AttackExecution`s ran `init()`, they drained the nation to zero. Planetary Realignment found this bug by accident, here Russia has only 39 troops: <img width="1189" height="654" alt="image" src="https://github.com/user-attachments/assets/07b85e00-6734-4ddd-a16e-fe53309e0ef8" /> The land attack path already handled this correctly. The bug in `sendBoatAttack` was introduced by #3786, which made nations see and attack enemies across rivers via boats, and changed `attackBots()` from `.neighbors()` to `.nearby()`. So the bug was on prod for the entirety of v31. This fix extracts the shared attack troop calculation (reserve, bot-aware allocation, troopSendCap, isAttackTooWeak, emoji) into a new `calculateAttackTroops` method, with a callback for the non-bot troop default (land: `player.troops() - targetTroops`, boat: `player.troops() / 5`). Bot targets in both paths now go through the same reserve-aware calculation. ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
094aa766ce |
Improve "Better troop management for nations 🤖" (#4278)
## Description: **Allow Hard/Impossible nations to retaliate and expand freely** Previously, nations on Hard/Impossible difficulty could be stuck unable to fight back if their `troopSendCap` or `isAttackTooWeak` checks blocked them from sending enough troops. **@legan320** on the main discord noticed it. Now: - `troopSendCap` raises the cap to at least the total incoming attack troops, so nations can match the force being used against them - `isAttackTooWeak` bypasses the 20% minimum check entirely when under attack - `troopSendCap` no longer applies when attacking Terra Nullius, so nations can always expand into unowned land with full troops All checks still apply normally for unprovoked attacks against 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
f4db4a33c8 |
Send nukes as motion plans and render them smoothly per frame (#4255)
## Summary Follow-up to #4244's payload work: nukes were the last per-tick movers flooding the worker → main update stream. - **Core**: nuke trajectories are fully determined at launch (precomputed parabola), so `NukeExecution` now records a `GridPathPlan` when the nuke is built — same mechanism trade ships use — and the client derives the position each tick. Per-tick `UnitUpdate`s for nukes in flight are suppressed; only targetable flips and deletion (interception/detonation) still emit. This covers atom bombs, hydrogen bombs, and MIRV warheads (dozens of per-tick movers per MIRV separation). - The plan path replays a separate pathfinder rather than reusing the stored trajectory array: the curve's cached points don't advance exactly one index per tick, and the plan must match the movement pathfinder's exact per-tick tile sequence. - `startTick` accounts for MIRV warheads' staggered `waitTicks`. - **Render**: `UnitPass.drawMissiles` now lerps each nuke's instance position `lastPos→pos` by wall-clock progress through the current tick, so nukes glide along their arc at render framerate instead of jumping once per 100ms tick. Both endpoints are real simulated positions — the rendered nuke trails the sim by at most one tick and settles exactly on it when ticks stop. Plan-driven units sync `lastPos` on path-stall ticks so the lerp never replays a segment. Shells keep their existing two-instance trail; SAM missiles are unchanged. ## Test plan - New `tests/nukes/NukeMotionPlan.test.ts`: tick-exact alignment between the recorded plan and core nuke position over the whole flight (mirroring `GameView.advanceMotionPlannedUnits` math), `waitTicks` offset, and that no per-tick unit updates are emitted in flight except targetable flips and deletion. - Full suite passes (1452 + 65), tsc/eslint/prettier clean. - Verified in-game (headless Chromium, real WebGL): atom bomb arcs from silo to target with the client position driven by the plan, missile sprite renders intact while the smoothing rewrites the instance buffer every frame, detonation FX land at the target. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
82b68d16a1 |
Fix "Better troop management for nations 🤖" (#4265)
## Description: There was a check missing... The troop management stuff should be disabled for team games because nations can expect donations in that case, and its mainly relevant for FFAs. ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
aa4b490e68 |
Simplify WebGL renderer integration: remove dead extension code, untangle GameView naming (#4240)
## Summary The WebGL renderer was adapted from an external extension and carried a lot of machinery this integration never uses (replay playback, its own input/event system, a GL radial menu). This PR is two mechanical cleanup passes with **no behavior change**: delete the dead code, then untangle the `GameView` naming collision. **78 files, +142 / −2,197.** ### Pass 1 — remove dead extension baggage - **Replay/copy mode**: `FrameData.tileMode` was hard-coded `"live"`; the copy branches in `frame/Upload.ts`, `UploadOptions` (never passed), `applyFullFrame`/`applyFullTiles`/`applyDelta` on the facade and `GPURenderer`, `HeatManager.resetForSeek`, and the seek-upload methods on `TerritoryPass`/`TrailPass` were all unreachable. Also deletes `types/Replay.ts`, `types/FrameSource.ts`, `types/GameUpdates.ts`, `types/Game.ts` (imported only by the types barrel). - **FrameEvents**: trimmed from 14 fields to the 3 actually populated and read (`deadUnits`, `conquestEvents`, `bonusEvents`). The other 11 fed the extension's stats system and were never written or read here. - **GL radial menu**: `RadialMenuPass`, its 4 shaders, and ~10 API methods on facade + renderer had zero callers — the game uses the DOM/d3 radial menu in `hud/layers/RadialMenu.ts`. The pass was constructed and drawn every frame for nothing. - **Facade event system**: `GameViewEventMap` defined 10 event types (`click`, `hover`, `scroll`, …) but only `contextrestored` was ever emitted — input actually flows through `InputHandler` → EventBus → controllers. Replaced the listener map with a single `onContextRestored` callback and deleted `Events.ts`. Also fixed the stale header comment claiming the facade handles user interaction. - **Unused API surface**: removed ~20 facade/renderer methods with zero callers (camera passthroughs like `panTo`/`zoomTo`/`fitMap`/`screenToWorld`, hit-testing queries, SAM replay setters, `setSelectedUnit`, `clearFx`/`setFxTimeFn`, `onFrame`/`afterRender`/fps tracking). Deliberately left alone: `Camera`'s pan/zoom primitives (building blocks for a possible future camera unification) and the `timeFn` plumbing inside the FX passes (deeply embedded as defaults; only the dead renderer-level wrappers were removed). ### Pass 2 — untangle the three GameViews - `render/gl/GameView.ts` → **`MapRenderer.ts`** (class `MapRenderer`). Every importer was already aliasing it as `WebGLGameView` to dodge the collision with the simulation-mirror `GameView` in `client/view/`, so this removes aliasing rather than adding churn. `render/CLAUDE.md` updated. - Deleted the `src/core/game/GameView.ts` back-compat shim (its own TODO asked for this). All 51 importers now import from `src/client/view/` directly via a new 3-line barrel `view/index.ts`. ## Test plan - `tsc --noEmit` clean, `eslint` clean - Full test suite passes (1,385 + 65 server tests) - Manual verification via headless Chromium: started a singleplayer game and confirmed the renderer works end-to-end — terrain draws, spawn-phase overlay shows, territories fill with borders after spawning, player names/flags render, no renderer console errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d96c055df1 |
Better troop management for nations 🤖 (#4239)
## Description:
When human pro players have non-allied players with similar troops next
to them, they wouldn't send out a big attack.
But nations are doing exactly that.
With this PR, they no longer do. On hard and impossible.
On easy and medium they are stupid 😀
```
1. Troop send cap: the nation must retain a minimum fraction of its
strongest non-allied neighbor's troop count (Hard: 75%, Impossible:
90%). Attacks that would drop below this floor are scaled down or
skipped entirely. Allied and same-team neighbors are ignored since
they pose no threat. The cap applies to land attacks, boat attacks,
and random boat attacks.
2. Minimum attack strength: if the capped troop count is less than 20%
of the target's troop count, the attack is skipped as too weak to be
worthwhile. Only applies on Hard and Impossible.
```
_Coded by MiMo 2.5 Pro, reviewed by MiniMax M3_
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
|
||
|
|
2789db8b96 |
Optimize core simulation hot paths (no behavior change) (#4230)
## Summary Pure performance optimizations to the attack/conquer/cluster hot paths in `src/core`, driven by the full-game perf harness from #4228. **No behavior change**: the final game-state hash is identical before/after on every config tested — world quick run (2 different seeds), giantworldmap, and the default 1800-tick run. ### Changes - **Flat-arithmetic neighbor iteration**: `forEachNeighbor` / `forEachNeighborWithDiag` / `isBorder` / `isOceanShore` are now implemented inside `GameMapImpl` using raw `ref±1` / `ref±width` index math, skipping the per-neighbor `ref()` coordinate validation (`Number.isInteger` etc.). `GameImpl` and `GameView` delegate. - **New `neighbors4(ref, out)`**: zero-allocation, callback-free neighbor query for hot loops (W, E, N, S — same order as `forEachNeighbor`). - **`AttackExecution`**: the per-tile closures in `tick()` / `addNeighbors()` are replaced with reusable neighbor buffers, a cached `GameMap` reference, and integer `smallID()` owner comparisons instead of owner-object lookups. - **`GameImpl`**: the per-conquer `updateBorders` closure is hoisted to a method with a reusable buffer; `removeInactiveExecutions` compacts the executions array in place instead of allocating a new ~4200-element array every tick. - **`PlayerExecution`**: `surroundedBySamePlayer` / `isSurrounded` / `getCapturingPlayer` de-closured (`neighbors4` + integer compares; neighbor visit order preserved, so `getCapturingPlayer`'s Map-insertion-order tie-breaking is unchanged); flood-fill visit closure hoisted out of the while loop. - **`FlatBinaryHeap.dequeue`**: returns the tile directly instead of allocating a `[tile, priority]` tuple per dequeued tile (AttackExecution is the only caller). ### Performance (`npm run perf:game`, same machine, before → after) | run | mean tick | ticks/sec | max tick | |---|---|---|---| | default (world, 400 bots, 1800 ticks) | 9.04 → **7.98 ms** | 111 → **125** | 31.7 → 35.7 ms | | giantworldmap, 600 ticks | 22.5 → **17.4 ms** | 44 → **58** | 52.8 → **36.2 ms** | The giantworldmap tail improvement (max tick −31%) is the most relevant for the 100 ms tick budget. ### Determinism verification Identical `Final hash` before and after on all configs: | config | hash | |---|---| | `--map world --ticks 200 --bots 100` | `5455008589403520` | | same + `--seed second-seed-check` | `5580840142777488` | | `--map giantworldmap --ticks 600` | `37373734953428430` | | default run | `26773450321979388` | ### Tests - New `tests/NeighborIteration.test.ts` pins the exact neighbor iteration orders (W,E,N,S cardinal; dx-major diagonal — conquest order and RNG consumption depend on them) and conquer/border-tile invariants checked mid-battle. - New `tests/FlatBinaryHeap.test.ts` covers heap ordering, clear, and growth. - Full suite passes (122 files / 1386 tests + server tests); lint and prettier clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c6296c0bb1 |
Fix/warship freezing no path (#4151)
**Add approved & assigned issue number here:** Resolves #4113 ## Description: Warships now reject the PatrolTile change when the new one is a different water component. Adds a test ensuring this behavior. ## Please complete the following: - [x] I have added screenshots for all UI updates There are none - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file No texts - [x] I have added relevant tests to the test directory I also have tested in game and tested that the test does indeed fail if my fix is not present. ## Please put your Discord username so you can be contacted if a bug or regression is found: Katokoda |
||
|
|
775ae77e0a |
Fix nations not spawning when random spawn is enabled 🤖 (#4117)
## Description: When random spawn is active, human SpawnExecutions are pre-created in GameRunner.init() and fire on the same tick as NationExecution. Because humans were added first, their SpawnExecution ticked first, called endSpawnPhase() (in singleplayer), and NationExecution then saw inSpawnPhase()=false, found the nation not alive, and deactivated it before ever queuing a SpawnExecution. Two changes fix this: 1. GameRunner.init(): Move nationExecutions() before spawnPlayers() so NationExecution ticks first and queues its SpawnExecution before the human SpawnExecution can end the spawn phase. 2. NationExecution.tick(): After the spawn-phase block, add a guard that waits when spawnExecAdded is true but the nation hasn't actually spawned yet. This prevents NationExecution from deactivating on the very next tick (via !isAlive()) before its queued SpawnExecution has had a chance to fire and give the nation territory. I tested it in singleplaye with and without random spawn and also in public lobbies. Nations now always 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin |
||
|
|
f3ba95574c |
fix(core): prevent bots from invading/attacking themselves (#3865) (#4014)
Resolves #4094 ## Description: In Free-For-All (FFA) mode where teams default to 0, player isOnSameTeam checks returned false for oneself, allowing players to attack themselves. Consequently, if a bot conquered the targeted tile between queueing a transport ship action and its actual initialization, the target became itself, causing the bot to execute a self-invasion. This fix adds a reflexive check in PlayerImpl.ts's isFriendly method to always treat oneself as friendly. It also adds a safety guard in TransportShipExecution.ts's init method to abort ship execution if the target has shifted to the attacker. ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: barfires |
||
|
|
413efed895 |
Add per-recipient cooldown to QuickChatExecution (#4012)
`QuickChatExecution` had no cooldown, allowing a player to spam quick-chat intents and flood a recipient's chat UI. This could bury incoming alliance request notifications, preventing them from being seen or accepted. This fix mirrors the existing emoji cooldown pattern: - Added `quickChatCooldown()` to `Config` (default: 30 ticks / 3 seconds) - Added `canSendQuickChat(recipient)` and `recordQuickChat(recipient)` to `Player` / `PlayerImpl`, tracking outgoing chats per recipient - `QuickChatExecution.tick()` now checks `canSendQuickChat` before displaying and records before the display calls (so the cooldown is always written even if display throws) |
||
|
|
38f0709e53 |
fix(core): destroy defense posts on tile capture instead of downgrading and transferring ownership (#1563) (#4016)
## Description: Capturing defense posts previously demoted their level by 1, and transferred ownership to the invading player if their level was still above 0. The expected strategic behavior is that defense posts should always be destroyed (deleted) upon capture. This fix updates PlayerExecution.ts's structure tick loop to immediately destroy the Defense Post unit via u.delete(true, captor) instead of transferring ownership. It also rewrites the corresponding unit tests in PlayerExecution.test.ts to verify the complete destruction of Defense Posts of all levels (including level 2+) when the tile owner changes. ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: barfires |
||
|
|
cbf38ffb81 |
Fix destroyed cooldown structures reappearing in game (#3997)
## Description: Stop deleted missile silos from reappearing after cooldown expiry before https://github.com/user-attachments/assets/714d3580-b60d-479e-8e45-6ee065b79d54 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri |
||
|
|
41ef675e98 |
Improve Notification Panel (#3913)
Resolves #3910 ## Description: - Split the events HUD into two components: a new **`<actionable-events>`** that owns alliance prompts (request / renew) and a slimmed-down **`<events-display>`** for everything else. - Reworked `<events-display>` into two visual tiers: dim/scrolling tier 2 on top (trade results, unit losses, donations, alliance status), prominent tier 1 anchored at the bottom (inbound nukes, naval invasion, attack requests, alliance broken, conquered player, chat). Tier 2 caps at the 4 newest entries; events expire after 8s. - Added a transient **+gold pip** above the gold pill in `<control-panel>`, animated with a small fade-in. Fires for trade ships, trains, donations, and conquest. Trade-ship and train arrivals are removed from the events scroll since they're surfaced here instead. - New `MessageType.NUKE_DETONATED` and a server-side emission in `NukeExecution.detonate` — once an inbound nuke lands or gets intercepted, the inbound warning vanishes and a "detonated" entry takes its place. - `displayMessage` gained optional `unitID` and `focusPlayerID` params so events can link to a unit or a player. Unit captures and destructions now navigate to the unit's last tile when clicked; donations navigate to the other player. - ActionableEvents card width matches `<events-display>`; cards persist until the user clicks Accept/Reject/Renew/Ignore or the server-side request timeout expires. - Removed the in-events category filter UI and the gold-amount banner — `<events-display>` is now a lightweight log that hides entirely when empty. <img width="570" height="444" alt="Screenshot 2026-05-21 at 1 42 30 PM" src="https://github.com/user-attachments/assets/f103efb3-0e11-4b72-a11b-91ff6896177c" /> <img width="430" height="296" alt="Screenshot 2026-05-21 at 1 41 34 PM" src="https://github.com/user-attachments/assets/ae58475a-b252-4aa6-9ce5-99dea7575ce3" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan |
||
|
|
0eb8578996 |
Fix nations not spawning in singleplayer when player picks fast
NationExecution gated its first SpawnExecution by the same attackRate/attackTick throttle used for AI actions, so a nation could wait up to ~100 ticks before scheduling its spawn. In singleplayer the human's spawn ends the spawn phase immediately, stranding any nation that hadn't yet reached its attackTick — on the next tick its NationExecution sees inSpawnPhase()=false and isAlive()=false (no tiles), and deactivates itself. First spawn now fires on tick 1, gated by a one-shot flag to avoid queuing duplicates. The attackRate cadence is preserved for subsequent re-spawns so nations still hop locations during the spawn phase. |
||
|
|
2e17fb5184 |
fix: remove double x() dereference in MIRV separation point calc (#3940)
mg.x() takes a TileRef and returns a number (x coordinate). The code was calling mg.x(mg.x(tile)), feeding the numeric result back into x() which expects a TileRef. This produces an incorrect midpoint for MIRV warhead separation, causing warheads to spread from a wrong position on the map. If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #(issue number) ## Description: Describe the PR. ## 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 |
||
|
|
f1d162825e |
feat: remove spawn timer on singleplayer (#3199)
Resolves #1041 ## Description: Remove the singleplayer spawn countdown so the game starts when the player spawns, spawn nations immediately after player spawn, and align game timer/max-timer timing with the new start point. Added a singleplayer regression test for spawn-immunity timing (GameImpl.test.ts) and updated spawn-phase loop tests to use gameType: GameType.Public where singleplayer behavior is not under test (e.g. MIRV/AI/Spawn/WinCheck-related suites), eliminating inSpawnPhase() timeout hangs after the new singleplayer start logic. https://github.com/user-attachments/assets/c07a585f-1153-490e-88ca-a91fc7ae5756 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri |
||
|
|
23b8eaa04f |
perf(AttackExecution): migrate hot loops to forEachNeighbor (#3820)
Resolves [#3815 ](https://github.com/openfrontio/OpenFrontIO/issues/3815) ## Description: Migrates the five `mg.neighbors(tile)` call sites in `src/core/execution/AttackExecution.ts` to the existing zero-allocation `forEachNeighbor(tile, cb)` helper (`src/core/game/GameImpl.ts:1107`). `neighbors()` allocates a fresh `TileRef[]` of length up to 4 on every call; `forEachNeighbor` invokes a callback with no allocation. The helper is already used in `PlayerExecution`, `GameImpl.updateBorders`, and similar hot paths — this PR finishes that rollout in the hottest attack loop. Affected sites in `src/core/execution/AttackExecution.ts`: | Original line | Context | | --- | --- | | 277 | `tick()` border check around `tileToConquer` | | 326 | `addNeighbors()` outer neighbor loop | | 335 | `addNeighbors()` nested `numOwnedByMe` count | | 370 | `handleDeadDefender()` border test | | 375 | `handleDeadDefender()` neighbor scan | Notes: - `handleDeadDefender` previously narrowed `this.target` from `Player | TerraNullius` to `Player` via the early-return check. Inside an arrow callback that narrowing isn't preserved by TypeScript, so the function now captures `target: Player` once after the check. Same pattern as `tick()`'s `targetPlayer` cache. - For loops that previously used `break` or `.some()`, I used a small flag variable to short-circuit the callback body. With at most 4 neighbors per tile the extra callback invocations are negligible compared to the eliminated array allocation. Behavioral guarantees: - Iteration order is identical (`forEachNeighbor` enumerates the same tiles in the same order as `neighbors()`). - No RNG, no float math, no wire-format changes. - Determinism preserved. Verification: - `npm test` — all 994 tests pass across 104 files. The 22 attack-related tests in `Attack.test.ts`, `AttackStats.test.ts`, and `AiAttackBehavior.test.ts` exercise the migrated code paths end-to-end. - `npx tsc --noEmit` — clean for the changed file. - `npx prettier --check` — clean. Briefly flagged in `#development` per the contribution guidelines before opening. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: sxndrexe |
||
|
|
eca5794ebb |
Chore(deps): Update and remove dependencies (#3819)
## Description:
Only mentioning removals/major updates/notable changes below, not all
minor upgrades.
### Removed:
- "@aws-sdk/client-s3": not used anywhere (was used in Archive.ts
previously)
- chai, "@types/chai", sinon-chai: not used anywhere, probably leftover.
Vitest uses a bundled version of Chai for its expect asserations under
the hood too.
- protobufjs, "@types/google-protobuf": not used anywhere, probably left
from evan's experiment with it? Removed from vite.config.ts too.
- "@types/jquery": not used anywhere, probably leftover
- sinon, "@types/sinon": not used anywhere just like chai, probably
leftover. And Vitest provides us with the same functionality.
- "@types/systeminformation": dependency systeminformation was removed
last year, this is an unneeded, deprecated and unmaintained remainder.
- vite-tsconfig-paths: removed, and removed the import and usage in
vite.config.ts and replaced it by adding `tsconfigPaths: true` to the
`resolve` block. Because of this message displayed on running the tests:
"The plugin "vite-tsconfig-paths" is detected. Vite now supports
tsconfig paths resolution natively via the resolve.tsconfigPaths option.
You can remove the plugin and set resolve.tsconfigPaths: true in your
Vite config instead."
- vite-plugin-static-copy: removed, we don't use it anymore (was used in
our vite.config.ts once,, probably before Vite natively supported
copying static assets via its publicDir configuration)
### Updated:
- color.js: v0.5 > v0.6, no breaking change affecting us
- cross-env: v7 > v10. It's a publicly archived repo since Nov 2025. But
before that he got it up-to-date from June 2025, porting to TS, dropping
old Node versions, dependencies etc. Seems still good to use for some
amount of time to come.
- dotenv: v16 > v17, now logs an informational message by default when
it loads an environment file. Can be disabled by using
dotenv.config({quite: true}) if needed.
- ejs: v3 > v5: security patches mostly. Vite still uses v3 btw.
- eslint: v9 > v10. Newly enabled rules by default:
'no-unassigned-vars', 'no-useless-assignment' and
'preserve-caught-error'. Mostly faster and minimum support moved to
higher node versions, which shouldn't be a problem.
- "@eslint/compat": v1 > v2. Minimum supported Node versions, which
should not be a problem.
- intl-messageformat: v10 > v11 no breaking changes that affect us
- jdom: v27 > v29. Faster. Most notably minimum support moved to higher
node v22 version, which should not be a problem. Also, see types/node,
kind of expecting v24 to be installed now.
- nanoid: from v3 to v5, no breaking changes that affect us
- "@opentelemetry/sdk-logs": now that addLogRecordProcessor is removed,
changed Logger.ts to pass an (empty) provider array directly to the
LoggerProvider constructor. Follows the changes in
https://github.com/open-telemetry/opentelemetry-js/pull/5588
- "@tailwindcss/vite": supports vite v8 from 4.2.2, and a fix for it in
4.2.4
- tailwindcss: supports vite v8 from 4.2.2
-- in 4.1.15 (we were already above this version) break-words was
deprecated in favor of wrap-break-word. But break-words, which we use in
15 places, will still work as expected
(https://github.com/tailwindlabs/tailwindcss/pull/19157). Same goes for
also deprecated "order-none".
- "@types/node": from v22 to v24, assuming most now use node 24
- vite v7 > v8:
-- is now on 8.0.10 so first bugs are out of it, while v8 itself also
fixed a big number of bugs.
-- in vite.config.ts, fixed Ts error/compilation issue by changing the
manualChunks option in build.rollupOptions.output to use the function
syntax, which is required by the updated types instead of the object
syntax.
- zod: no changes that affect us
### Prettier:
Updated only because of (new because of update?) Prettier errors for
files untouched in this PR originally:
- PathFinder.Parabola.ts
- WorkerMessages.ts
- ClanModal.handlers.test.ts
- ClanModal.rendering.test.ts
- CONTRIBUTING.md
- README.md
### ESLint:
Fixes needed to silence errors coming from newly enabled recommended
rules 'no-useless-assignment' and 'preserve-caught-error':
For 'no-useless-assignment' (default assignment never used because of
unreachable code or they are guaranteed to get a value, so they can be
undefinedat the start. Exception was AttackExecution, so made the
default value of 0 the default case in the switch statement):
- ClientGameRunner
- GameModeSelector
- NameBoxCalculator
- StructureDrawingUtils
- TerritoryLayer
- Diagnostics
- GameRunner
- ColorAllocator
- DefaultConfig
- AttackExecution
- AiAttackBehavior
- Worker.worker
- GamePreviewBuilder
For 'preserve-caught-error', disabled the rule here because the possible
fix `{cause: error}` was introduced in ES2022 while we're still on
target ES2020 currently:
- GameServer
- Privilege
_Error: The value assigned to 'gameMap' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'timeDisplay' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'scalingFactor' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'radius' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'teamColor' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'gl' is not used in subsequent statements.
(no-useless-assignment)
Error: The value assigned to 'power' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'tickExecutionDuration' is not used in
subsequent statements. (no-useless-assignment)
Error: The value assigned to 'selectedIndex' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'mag' is not used in subsequent statements.
(no-useless-assignment)
Error: The value assigned to 'speed' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'matchesCriteria' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'shouldContinue' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'description' is not used in subsequent
statements. (no-useless-assignment)
Error: There is no `cause` attached to the symptom error being thrown.
(preserve-caught-error)
Error: There is no `cause` attached to the symptom error being thrown.
(preserve-caught-error)_
All tests pass. TypeScript and ESLint errors resolved.
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
tryout33
---------
Co-authored-by: Copilot <copilot@github.com>
|
||
|
|
58ec8b280f |
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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._ |
||
|
|
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._ |
||
|
|
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 |
||
|
|
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 |