Commit Graph

1008 Commits

Author SHA1 Message Date
VariableVince 496f1008bb Fix: icons structure icons and others at wrong location (#3453)
## Description:

Fix for all structure icons suddenly being displaced from the actual
structure location. And in some cases, structure itself created at wrong
location, or coordinate grid, nuke trajectory preview target, nuke
circles, naval landing target, or pop-up text of gold earned from train
or tradeship.

- Was triggered on iOS almost exclusively, but was also possible on
other devices when a top/left margin was present. Like when an ad was
shown. Why noticed almost only on iOS? Because of different behaviors
where it shifts the DOM elements around relative to the screen
temporarily, so we get a top/left offset on getBoundingClientRect for
the canvas. Possible culprit is overscroll which lets you scroll outside
of the viewport for several hundred pixels before snapping back. Which
was triggered by mistake by dragging instead of tapping somewhere, or
so.

- Some of the bug reports:
https://discord.com/channels/1284581928254701718/1451393982159523982,
https://discord.com/channels/1284581928254701718/1463548362526822649,
https://discord.com/channels/1284581928254701718/1378672255336189964,
fixes https://github.com/openfrontio/OpenFrontIO/issues/3406

- The fix brings a little performance win as well because we need to be
doing less calculations. It is basically "if drawing on a canvas, work
with canvas coordinates and not with screen coordinates". Was stress
tested by two players and me, see below for reproduction.

- (BTW. When researching if the current logic was intended in any way, I
found CodeRabbit had already noticed part of the bug twice. One of them
was
[resolved](https://github.com/openfrontio/OpenFrontIO/pull/2059#discussion_r2396277710),
the other [left
open](https://github.com/openfrontio/OpenFrontIO/pull/2059#discussion_r2370413213).
Another reminder that we need to heed Rabbits calls!)

**CONTAINS**
- StructureIconsLayer > computeNewLocation and StructureDrawingUtils >
createUnitContainer. In renderLayer, when TransformHandler.hasChanged
(after onZoom, goTo, onMove), computeNewLocation recalculates the
position of all structure icons. Sometimes all icons would suddenly be
displaced, not above their map position but somewhere else. New single
icons/levels/sprites would be placed wrongly too by computeNewLocation
and createUnitContainer.
-- Fix: don't use TransformHandler > worldToScreenCoordinates but the
new worldToCanvasCoordinates. Because worldToScreenCoordinates adds the
canvas boundingRect top/left offset. When the main canvas is already
shifted down with a margin, the icons also get shifted within the
canvas. So they're moved twice as much as they should be. We only need
to know at what canvas location to put the icons, the main GameRender
canvas already handles the overall displacement.

- TransformHandler > worldToCanvasCoordinates
-- Use the new worldToCanvasCoordinates too instead of
worldToScreenCoordinates in CoordinateGridLayer, MoveIncicatorUI,
NavalTarget, NukeTeleGraph, TextIndicator. They were affected by the
same bug as the shifting Structure icons because the boundingRect
top/left offset was applied twice, but it was noticed less. I have seen
reports of NavalTarget or MobveIndicatorUI (for warships) not being in
the correct spot though iirc. And just like for
StructureIconsLayer/StructureDrawingUtils, it's only logical. Since
we're drawing on the canvas, we only need to know where to place things
within that canvas.

- TransformHandler > worldToScreenCoordinates
-- Split in two sub-functions. New seperate function
worldToCanvasCoordinates was needed for the above fix. For
canvasToScreenCoordinates the reason is explained below.

- TransformHandler > screenToWorldCoordinates: this function already
subtracts the canvas boundingRect top/left offset. Some callers were
themselves getting the boundaryRect and subtracting top/left, before
calling screenToWorldCoordinates. Not only unnecessary. But also, when
there was more than 0 top/left offset, it would be subtracted twice from
the mouse/tap position. Meaning a (ghost) structure or nuke trajectory
preview target would not be put where the mouse/tap was. Same bug as
above but reversed.
-- Checked all callers. Most did it right. Fixes where needed in
StructureIconsLayer > createStructure and renderGhost, and in
NukeTrajectoryPreviewLayer > updateTrajectoryPreview and
updateTrajectoryPath.
-- Removed comment in screenToWorldCoordinates that talked about zoom.
It doesn't belong there because we do more than zooming there, it was
probably copied once from onZoom() which has the exact same comment and
it belongs in that function.
-- Small fix in caller BuildMenu when checking all callers of
screenToWorldCoordinates: it checked if clickedCell was null, but
screenToWorldCoordinates never returns null.

- TransformHandler > added public helper functions
screenToCanvasCoordinates and canvasToScreenCoordinates to make code
re-usable
-- screenToCanvasCoordinates is used in screenToWorldCoordinates and
onZoom in TransformHandler itself
-- screenToCanvasCoordinates is now also used also in moveGhost and
createGhostStructure in StructureIconsLayer. No bugs there, but the same
calculation was done (getting boundingRect, subtracting left/top from
mouse/tap position). So they now use the centralized function which also
adds to their readability.
-- canvasToScreenCoordinates is (for now) only used in
worldToScreenCoordinates in TransformHandler. It makes the function more
readable imo. And, since it has such a similar calculation to
screenToWorldCoordinates, it only seemed neat to have them both have
their own function.

**BEFORE** (only showing "all structure icons get displaced", but the
cause and fix is basically the same for all)
https://youtu.be/CfDdUwIRQeE

**AFTER**
https://youtu.be/w5w_wvh5V0g 

## Please complete the following:

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

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

tryout33
2026-03-23 16:27:46 -07:00
VariableVince eb51853b05 Perf/Fix: spawn and other functions that need closest by unit (#3243)
## Description:

Performance improvements.

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

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

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

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

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

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

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

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

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


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

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

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

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




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

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

_And here a comparison in the flame charts:_

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


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

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

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

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


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

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

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

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

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

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


## Please complete the following:

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

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

tryout33
2026-03-23 16:23:49 -07:00
scamiv 05e2bc9f0a Improve cacheability with content-hashed public assets and a cacheable app shell (#3494)
## Description:

This reworks asset delivery and cacheability across the app and moves
non-bundled public resources onto immutable, content-hashed URLs.

Vite bundle outputs continue to live under `/assets/**` and remain
content-hashed by Vite. Public resources that were previously fetched
from stable paths in `resources/` now go through a custom hashed
namespace under `/_assets/**`, backed by a generated asset manifest that
is available to the server, browser, and worker runtime.

In parallel, the root app shell is now cacheable shared HTML instead of
request-time `no-store` HTML. Dynamic and live routes remain explicitly
uncached.

## Why
- Improve browser and Cloudflare cacheability for static assets.
- Remove query-string and release-version cache busting for
runtime-fetched assets.
- Allow unchanged public assets to keep the same URL across releases.
- Reduce avoidable work on `/` by serving a shared app shell instead of
rendering HTML on every request.
- Make cache behavior explicit instead of relying on mixed framework
defaults and file-extension heuristics.

## What Changed

### 1. Content-hashed public asset pipeline
- Added a build-time public asset manifest and hashing pipeline for
non-Vite resources.
- Production now emits hashed public assets under `/_assets/**`.
- Added runtime manifest loading for Node so server-rendered paths
resolve against built hashed files instead of rebuilding from source at
runtime.
- Emitted the runtime asset manifest as an ESM module for server
consumption.

Result:
- `/assets/**` = Vite-managed hashed bundle outputs
- `/_assets/**` = custom content-hashed public resources

### 2. Runtime asset URL migration
- Added a shared `assetUrl(...)` resolution path.
- Migrated runtime references away from query-string versioning and
stable source paths.
- Updated browser, worker, and server-side rendering paths to resolve
through the asset manifest.
- Moved map manifests, map binaries, thumbnails, sprites, sounds, fonts,
flags, icons, screenshots, and other runtime-fetched resources onto
hashed URLs.

### 3. Map and preview fixes
- Fixed directory and per-file map asset resolution so map manifest and
binary fetches resolve to the correct hashed URLs.
- Updated preview metadata and map thumbnail paths to use the hashed
asset namespace.
- Fixed runtime manifest loading in prod after deployment.

### 4. Explicit cache policies
- Added explicit immutable cache headers for:
  - `/assets/**`
  - `/_assets/**`
  - worker-prefixed equivalents under `/wN/...`
- Added explicit `no-store` headers for live and dynamic APIs.
- Removed the old `/api/env` bootstrap request and baked `gameEnv` into
the HTML bootstrap instead.

### 5. Cacheable root app shell
- Refactored the root HTML path to serve a shared app shell with:
- `Cache-Control: public, max-age=0, s-maxage=300,
stale-while-revalidate=86400`
- `/` and the SPA fallback now serve shared cacheable HTML instead of
request-time `no-store` rendering.
- `/game/:id` remains dynamic and `no-store`, but now reuses the shared
shell before injecting preview tags.

### 6. Matchmaking instance handling
- Because the app shell is now cacheable, `INSTANCE_ID` was removed from
shared HTML.
- Added `/api/instance` as a temporary `no-store` runtime lookup used
only by matchmaking.
- This preserves correctness with the current random-per-boot
`INSTANCE_ID` model while keeping `/` cacheable, but it is not the
intended long-term design.

## Behavior Changes

### Asset URL contract
Production URLs for non-Vite public resources now change from stable
paths such as:
- `/maps/...`
- `/images/...`
- `/manifest.json`

to content-hashed paths under:
- `/_assets/...`

Examples:
- `/_assets/maps/<map>/manifest.<hash>.json`
- `/_assets/images/Favicon.<hash>.svg`

### Bootstrap/config
- `/api/env` is removed.
- `gameEnv` is now bootstrapped from HTML.

### HTML caching
- `/` and the SPA fallback are now cacheable shared HTML.
- `/game/:id` remains dynamic.

## Cache Matrix After This Branch
- `/_assets/**`: `public, max-age=31536000, immutable`
- `/assets/**`: `public, max-age=31536000, immutable`
- live `/api/**`: explicit `no-store`
- `/api/health`: explicit `no-store`
- `/api/instance`: explicit `no-store`
- `/game/:id`: explicit `no-store`
- `/` and SPA fallback: `public, max-age=0, s-maxage=300,
stale-while-revalidate=86400`

## Notes / Tradeoffs
- `/api/instance` is a temporary compromise. It exists because
`INSTANCE_ID` is currently random per boot, which is not safe to embed
into cacheable shared HTML.
- The current matchmaking flow still asks the client to provide
`instance_id` during `matchmaking/join`. That is functional, but it is
the wrong ownership boundary: instance selection should be handled by
the matchmaking service, not by the browser.
- The cleaner end-state would be:
- make `matchmaking/join` stop requiring `instance_id` from the client,
and let the matchmaking service select a healthy instance from worker
check-ins
- This branch makes the origin behavior edge-cache-friendly, but
Cloudflare still needs matching cache rules if HTML itself should be
cached at the edge.

## Validation
Verified during development with:
- `npx tsc --noEmit`
- `node node_modules\\vite\\bin\\vite.js build`
- `node node_modules\\vitest\\vitest.mjs run
tests/server/RenderHtml.test.ts tests/server/NoStoreHeaders.test.ts
tests/server/StaticAssetCache.test.ts
tests/core/configuration/ConfigLoader.test.ts`

Additional targeted tests added:
- `tests/AssetUrls.test.ts`
- `tests/core/game/FetchGameMapLoader.test.ts`
- `tests/core/configuration/ConfigLoader.test.ts`
- `tests/server/NoStoreHeaders.test.ts`
- `tests/server/StaticAssetCache.test.ts`
- `tests/server/RenderHtml.test.ts`

## Known Existing Warnings
The production build still reports pre-existing warnings that are not
addressed by this branch:
- inconsistent JSON import attributes for `resources/countries.json`
- inconsistent JSON import attributes for `resources/QuickChat.json`
- large chunk warnings from Vite

## Rollout Notes
- Cache rules should treat `/_assets/**` and `/assets/**` as immutable.
- Cloudflare will still classify HTML as dynamic after deploy unless
matching edge cache rules are configured for it.

## Follow-ups
- Remove `/api/instance` by changing `matchmaking/join` so the server
selects the target instance, or by making `INSTANCE_ID` deploy-stable if
the current contract must remain.


## Please complete the following:

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

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

DISCORD_USERNAME
2026-03-23 11:36:52 -07:00
VariableVince 13df5cf324 Perf/Cleanup/Fix(NameLayer): 40% better performance (#3475)
## Description:

TL;DR: NameLayer cleanup+ fix + about 40% faster. The potential move of
NameLayer to canvas is stalled so this is a welcome improvement until
then imo.

- It was previously attempted to move NameLayer from HTML to canvas. But
currently that work is stalled so it might take awhile. Therefore
optimizations to NameLayer are useful to merge in the meantime. Also
there's a fix in this PR (see point below) and some cleanup. Overall it
would probably be better to base future changes on this better version
of NameLayer.
Messages about attempt on Feb 6 and reference to having done that
attempt on March 3:

https://discord.com/channels/1359946986937258015/1381293863712591872/1469117172767784981

https://discord.com/channels/1359946986937258015/1381293863712591872/1469401090385641573

https://discord.com/channels/1359946986937258015/1381293863712591872/1469435973522686127

https://discord.com/channels/1359946986937258015/1359946989046989063/1478213329079242752

- Fix: TL;DR: Remove redundant comparison that unintentionally didn't
work and always resolved to true. Leading to scale always being
recalculated. It is now still always recalculated because otherwise name
may be too big for the territory for several seconds, which looks buggy.
(More on this: In renderPlayerInfo(), it cached render.location in
oldLocation. Then put new Cell() in render.location. Later on it would
strictly compare render.location against oldLocation, to decide if scale
should be changed. Which would always be true because render.location
would have a new Object (long ago they were compared non-strictly with
==, later on strictly when those checks were updated in the entire repo
to ===). With this comparison always returning true (even if
render.location x and y did not actually change), the scale would always
be updated by updating render.element.style.transform.
After the fix (removing the non-working comparison which always resolved
to true), scale updates happen at same frequency as before. I have not
kept a similar check like "positionChanged". Because in testing,
player/tribe name would be scaled as too big for their territory size
for several seconds. This felt buggy. Cause for this is two delays
sometimes overlap resulting in several seconds of delay before scale is
recalculated after name position changed: time in Namelayer per render
refresh inside renderLayer (renderRefreshRate 500ms) plus the waiting
time in PlayerExecution per recalculation of largestClusterBoundingBox
(every 20 ticks). I ultimately decided that it should not wait for
"positionChanged" and just be updated every 500ms (renderRefreshRate)
just like unintentionally happened before.)

- Remove redundantly re-adding the name, when a player name doesn't
change anyway. Only adding it when creating the element is enough
- Remove dead code for Shield
- Cache DOM lookups
- Use textContext instead of innerHTML for nameSpan
- Only transform container if it has updates
- Remove currently unused Canvas. Also from public renderLayer().
Layer.ts expects renderLayer(context: CanvasRenderingContext2) so i
could put it back, but it isn't needed per se and i think it makes more
clear that NameLayer doesn't use Canvas currently.
- Remove two unneeded/outdated comments, update others
- Move setting render.fontSize and render.fontColor after early return
- Pass baseSize to updateElementVisibility so as to not calculate it
twice
- Cache render.player.nameLocation() to re-use

BEFORE:
![BEFORE 610 PsvZSjuL for 20 2s Schermafbeelding 2026-03-13
003100](https://github.com/user-attachments/assets/33583004-8a01-4587-9908-dafd0816f2b4)

AFTER:
![AFTER 610 PsvZSjuL for 20 2s Schermafbeelding 2026-03-13
003100](https://github.com/user-attachments/assets/a8c70ef6-c16a-4204-82b3-d916ba3d6437)

## Please complete the following:

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

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

tryout33
2026-03-20 15:34:19 -07:00
Ralfi Salhon 015e3c7d19 feat: Attacking Troops Overlay (#3427)
## Description:

https://troop-advantage-layer.openfront.dev/

Hey OpenFront dev team, I've been really enjoying the game, and the
v0.30 changes have felt great so far. Happy to start contributing!

This PR introduces `AttackingTroopsOverlay`, a layer that renders live
attacker vs. defender troop counts directly on active front lines.
Players can immediately gauge combat strength without leaving the map
view.


![troop-advantage-layer](https://github.com/user-attachments/assets/9e862812-84b4-46cb-a0c6-65fa50320198)

A recent change updates the layer to just the # of attackers and a
symbol for attack/defence:

![visual-front-line](https://github.com/user-attachments/assets/46bc7117-2314-44c9-96fc-8a7e9c6ab5cd)

Left: Perspective of Anon 667 (Blue) | Right: Perspective of Anon332
(Red)

![ezgif-6261e6669d6b972b](https://github.com/user-attachments/assets/734d90c1-8f22-44dc-8f2f-b22e46676f46)

**How it works:**
- Attacker count shown for ground invasions. When attacking, your troop
count will display amber for disadvantageous, and green for advantageous
battles. When defending, the enemy troop count will switch to red if you
are at a severe disadvantage.
- Label position recalculates every tick at 200ms, tracking the front
line as it moves.
- Automatically hidden during Terrain view (spacebar)
- Labels clean up when an attack ends or its target becomes invalid

**Settings:** An "Attacking Troops Overlay" toggle is added to Settings,
enabled by default.
--> the screenshot is old, but the text has been updated
<img width="448" height="410" alt="Settings toggle"
src="https://github.com/user-attachments/assets/2df8ec7a-3f77-48b7-a9b5-ee4a6eed0412"
/>

## Checklist

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

## Discord
Radyus
2026-03-19 15:04:33 -07:00
evanpelle 6b9603d445 Merge branch 'v30' 2026-03-18 19:29:42 -07:00
Evan cb7164a52f Place footer ad during spawn phase (#3458)
## Description:

Show the "footer_ad" ad type during the spawn phase.

<img width="1855" height="914" alt="Screenshot 2026-03-17 at 7 01 37 PM"
src="https://github.com/user-attachments/assets/9c4c1730-95a0-4fc7-a983-4fe625d38c80"
/>

## Please complete the following:

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

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

evan
2026-03-17 19:02:55 -07:00
Ryan 1049b7e7dc Clan System Part 1 (#3276)
## Description:

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

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

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


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


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


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

## Please complete the following:

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

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

w.o.n
2026-03-17 15:55:47 -07:00
evanpelle 79c3deabd8 remove spawn video ad 2026-03-17 11:40:42 -07:00
Evan 51db6a2772 Add in game add to bottom left corner (#3446)
## Description:

Remove the header ad because that's where the player info overlay is,
and instead put it on the bottom left

Ad is not displayed on small screens.

<img width="1916" height="924" alt="Screenshot 2026-03-16 at 7 27 58 PM"
src="https://github.com/user-attachments/assets/e39c4be6-5270-4a77-bfbd-9521d15b9211"
/>

## Please complete the following:

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

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

evan
2026-03-16 19:36:02 -07:00
bijx f356f09f81 Feat: Game Speed + Pause keybinds (#3397)
## Description:

Can't tell you how many times I've been playing solo, I try to go change
the speed from `Max` to `x1` and before I've opened the speed controls
and clicked on one the AI completely wipes me. But not to worry, we now
have a pause and speed toggle up/down keybinds!


https://github.com/user-attachments/assets/48692c27-888f-40fb-837a-45e26f262441

Keybinds were added to "Menu Shortcuts" submenu:

<img width="1750" height="1099" alt="image"
src="https://github.com/user-attachments/assets/8c4500d5-f43e-4a1c-9940-04db75bf18cf"
/>

Tested on Solo, custom match, and public lobbies. Pause intent works
correctly on solo and private match (only if you are host), and neither
feature works in public matches, as expected.



## Please complete the following:

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

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

bijx
2026-03-16 23:16:59 +00:00
HulKiora b045608c89 ui: reduce HUD transparency for control, attacks, events, and hover panel (#3429)
## Description:

Reduce HUD transparency for better readability by making the Control
Panel, Attacks panel, Events/Chat panel, and Hover panel more opaque
while keeping a subtle blur effect.

## Please complete the following:

- [ x] 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:

HulKiora
Before :
<img width="518" height="134" alt="image"
src="https://github.com/user-attachments/assets/dd714a8e-5b8c-4754-b68d-a86a0f82afcf"
/>

After :
<img width="519" height="138" alt="image"
src="https://github.com/user-attachments/assets/828b88be-a071-474c-a4a2-80d1c679b3df"
/>
2026-03-15 14:59:34 -07:00
evanpelle 681aa98fb1 bugfix: don't remove player name renderer if name location is not set 2026-03-13 11:53:38 -07:00
evanpelle 28b2372ef5 shorten the control panel vertically a bit to increase center space for gameplay 2026-03-13 09:46:47 -07:00
Evan 37f4f606f6 reduce margin for player info & control panel (#3414)
## Description:

To give more space in the center of the screen remove the top & bottom
margin. Also make the control panel & player info panel slightly shorter
& longer.

<img width="1167" height="961" alt="Screenshot 2026-03-12 at 7 20 40 PM"
src="https://github.com/user-attachments/assets/15baf640-daf0-4394-9107-d175130e7754"
/>

## Please complete the following:

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

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

evan
2026-03-12 19:50:43 -07:00
evanpelle 36572b69b5 radial menu: make elements brighter on hover 2026-03-11 20:35:23 -07:00
Evan e922d336a1 update radial menu styling (#3404)
## Description:

Before:

<img width="215" height="239" alt="Screenshot 2026-03-10 at 10 29 12 PM"
src="https://github.com/user-attachments/assets/bb044425-eb2f-427c-afd6-6c9dd5d075aa"
/>

After:

<img width="240" height="207" alt="Screenshot 2026-03-10 at 10 27 33 PM"
src="https://github.com/user-attachments/assets/21ce4c3b-ab24-4af0-b608-6be5603320fb"
/>

## Please complete the following:

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

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

evan
2026-03-11 10:06:38 -07:00
FloPinguin 8329dd479e Mobile UI polish (#3401)
## Description:

### Fix attack percentage label causing slider width jitter on mobile by
setting a fixed width on the container


https://github.com/user-attachments/assets/41ea3b98-48fa-49f3-b22e-2a567f208dfb

### Reduce event panel max-height from 30vh to 15vh on mobile

Previous:

<img width="406" height="781" alt="Screenshot 2026-03-10 231050"
src="https://github.com/user-attachments/assets/ea83a75d-a0cb-4ce2-8714-99e822b028e9"
/>

After:

<img width="397" height="781" alt="Screenshot 2026-03-10 231414"
src="https://github.com/user-attachments/assets/153a4291-a3dc-4255-8a19-725bfdf6ff8b"
/>

### Change lobby buttons (Create/Ranked/Join) from `slate-700` to
`slate-600` for better contrast against the background

Previous:

<img width="411" height="387" alt="Screenshot 2026-03-10 232720"
src="https://github.com/user-attachments/assets/1fd8bc7a-dbaa-414a-8e74-a070c66c40c6"
/>

After:

<img width="408" height="495" alt="Screenshot 2026-03-10 232704"
src="https://github.com/user-attachments/assets/e7693a28-6560-4440-8581-91b1fd7a5cf5"
/>

## Please complete the following:

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

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

FloPinguin
2026-03-10 16:40:15 -07:00
Evan 3a195005d8 outgoing attacks (#3398)
## Description:

Show outgoing attacks on the PlayerInfoPanel. Also update the
AttacksDisplay so it uses the same icon for incoming & outgoing attacks

<img width="502" height="97" alt="Screenshot 2026-03-10 at 2 02 49 PM"
src="https://github.com/user-attachments/assets/de83bf4d-b1b9-4fec-a8c6-e25bbf82c942"
/>

<img width="483" height="167" alt="Screenshot 2026-03-10 at 2 03 02 PM"
src="https://github.com/user-attachments/assets/cf43106f-5f3a-4a78-abae-dff019de4fb4"
/>

<img width="509" height="203" alt="Screenshot 2026-03-10 at 2 03 28 PM"
src="https://github.com/user-attachments/assets/79db2081-dd93-4ff4-aaa7-75281c16c037"
/>


## Please complete the following:

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

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

evan
2026-03-10 14:08:31 -07:00
bijx 8eb879aff4 Fix: Coordinate Grid text is now white when dark mode is enabled (#3396)
## Description:

When the game was on dark mode, seeing the coordinate text on oceans
became impossible. Now we check for dark mode in the layer to change the
text color accordingly.

Light Mode:
<img width="3430" height="1669" alt="image"
src="https://github.com/user-attachments/assets/181d53f5-b218-4f85-9d06-7e80f5d20004"
/>

Dark Mode: 
<img width="3488" height="1706" alt="image"
src="https://github.com/user-attachments/assets/8a8e3951-31b4-448d-bc87-7fabd105e5fb"
/>


## Please complete the following:

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

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

bijx
2026-03-10 19:59:08 +00:00
Evan f61d2fb59f update control panel UI: larger text, troop colors (#3395)
## Description:

During the playtests people mentioned the UI was too small. Also change
the troop color because white & green was difficult to read.


<img width="559" height="196" alt="Screenshot 2026-03-10 at 12 42 58 PM"
src="https://github.com/user-attachments/assets/9ba5ccaa-3113-4998-a016-e9936269f5de"
/>

<img width="491" height="96" alt="Screenshot 2026-03-10 at 12 43 15 PM"
src="https://github.com/user-attachments/assets/54f7886f-b830-451e-9a7a-3a764efdfbcc"
/>

<img width="574" height="208" alt="Screenshot 2026-03-10 at 12 43 25 PM"
src="https://github.com/user-attachments/assets/2583b492-1844-4814-8fd8-b7216a1dad60"
/>

## Please complete the following:

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

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

evan
2026-03-10 12:59:53 -07:00
evanpelle aa24438edf Fix slider focus stealing keyboard shortcuts after interaction 2026-03-09 16:53:30 -07:00
evanpelle ce416f877c bugfix: don't show yellow alert frame when a bot is attacking 2026-03-09 12:41:27 -07:00
evanpelle ebd39e8ced cap attacks panel height and make it scrollable when overflow 2026-03-09 09:44:25 -07:00
Bartosz Woźniak 936928fed9 Enhance InputHandler to allow using NumPad (#3317)
## Description:

Adds **Enter** and **Numpad Enter** as confirmation for placing a ghost
structure after selecting a building with hotkeys (1–0 or numpad).
Players can cancel with Esc but previously had to click to confirm; they
can now confirm with Enter or Numpad Enter at the current cursor
position. This supports keyboard-only or mouse + numpad workflows (e.g.
one hand on numpad for select + confirm, one on mouse for aiming).

## Please complete the following:

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

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

.wozniakpl
2026-03-07 15:27:25 -08:00
FloPinguin fe89713f46 Fix UI (again) 🖌️ (#3379)
## Description:

 **Fix UI spacing and border radius across multiple screen sizes**

- Fix events panel missing right margin on wide screens
- Fix incorrect border radius on events panel and control panel at
various breakpoints
- Remove border radius from attack/boat elements on small screens
- Show running attacks above the events panel on mobile
- Add left/right margin to the homepage on tablet-sized screens
- Adjust lobby card spacing on mobile

Previous

<img width="410" height="124" alt="Screenshot 2026-03-07 203244"
src="https://github.com/user-attachments/assets/d3feb9fe-97a3-44d0-9aba-db04062f9911"
/>


After

<img width="417" height="117" alt="Screenshot 2026-03-07 203255"
src="https://github.com/user-attachments/assets/31b88145-8e92-40db-b9cc-f2a00754f900"
/>


Previous

<img width="828" height="123" alt="Screenshot 2026-03-07 203320"
src="https://github.com/user-attachments/assets/4e162cf5-7d82-4e87-9dd9-9ab1d3782f23"
/>


After

<img width="820" height="126" alt="Screenshot 2026-03-07 203337"
src="https://github.com/user-attachments/assets/a25121aa-603c-41c7-b335-406a38a62cf9"
/>


Previous

<img width="961" height="102" alt="Screenshot 2026-03-07 203353"
src="https://github.com/user-attachments/assets/22ba9770-88a3-4f49-aeb6-6d875006946b"
/>


After

<img width="954" height="78" alt="Screenshot 2026-03-07 203403"
src="https://github.com/user-attachments/assets/0d4e3b19-de1c-4211-b1e3-bd935025de33"
/>


Previous

<img width="557" height="154" alt="Screenshot 2026-03-07 203450"
src="https://github.com/user-attachments/assets/2cc8a747-3e68-4449-9746-62fcbca76510"
/>


After

<img width="602" height="146" alt="Screenshot 2026-03-07 203421"
src="https://github.com/user-attachments/assets/bae399a3-8969-4b7a-a77c-c73c4f775ca0"
/>


Previous

<img width="727" height="889" alt="Screenshot 2026-03-07 204707"
src="https://github.com/user-attachments/assets/bc53febf-9beb-4195-a994-858333f30f24"
/>


After

<img width="725" height="799" alt="Screenshot 2026-03-07 204714"
src="https://github.com/user-attachments/assets/9d600212-73ae-4566-b1c5-df83e8edb8e9"
/>


Previous

<img width="658" height="890" alt="Screenshot 2026-03-07 204633"
src="https://github.com/user-attachments/assets/6c935fcc-3e46-4706-8c9a-9840cc469b60"
/>


After

<img width="656" height="798" alt="Screenshot 2026-03-07 204639"
src="https://github.com/user-attachments/assets/8e490f29-cf50-4c1f-a97e-f550fd4f9a13"
/>

## Please complete the following:

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

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

FloPinguin
2026-03-07 13:01:18 -08:00
FloPinguin 3fca25f421 Skip multi-tab detection during replays 🛠️ (#3366)
## Description:

Multi-tab detection was incorrectly penalizing users watching replays.
Added `isReplay()` check to `MultiTabModal.tick()` so the detector is
never initialized when viewing a replay.

## Please complete the following:

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

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

FloPinguin
2026-03-06 20:02:03 -08:00
DevelopingTom 902a0b42ac Remove useless sprite setting (#3363)
## Description:

Redundant animated sprite setting: the sprite frame width can be
computed directly.

## Please complete the following:

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

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

IngloriousTom
2026-03-06 19:58:14 -08:00
Evan 815f1de67b Update control panel UI (#3357)
Relates to #2260

## Description:

Inspired by https://github.com/openfrontio/OpenFrontIO/pull/3359

This PR centers the control panel and combines it with the units
display. The reasoning is that the control panel contains the most
critical info so it should be in the center of the screen. Combining it
with the units display reduces the number of UI components on screen.

Also made the attack ratio bar persistent on mobile

<img width="618" height="216" alt="Screenshot 2026-03-06 at 2 06 34 PM"
src="https://github.com/user-attachments/assets/34b041c1-d78b-46b5-a7ab-f2a44145a7e2"
/>


<img width="941" height="343" alt="Screenshot 2026-03-06 at 2 06 55 PM"
src="https://github.com/user-attachments/assets/1e3b026c-8eb2-407c-be38-0e71e1ae426c"
/>

<img width="562" height="228" alt="Screenshot 2026-03-06 at 4 11 20 PM"
src="https://github.com/user-attachments/assets/56eac49f-c8a4-4ac1-a60a-f1bcb2fad2d0"
/>

<img width="939" height="357" alt="Screenshot 2026-03-06 at 4 11 32 PM"
src="https://github.com/user-attachments/assets/eb5591d5-3cc2-4182-944b-3a4b0b76852a"
/>


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

Co-authored-by: hkio120 <111693579+hkio120@users.noreply.github.com>
2026-03-06 18:32:01 -08:00
VariableVince e137fcaa6c Fix/Perf/Refactor: playerActions and buildableUnits, their callers and related types (#3220)
## Description:

TL;DR: it's faster.

buildableUnits is called via PlayerView.actions from UnitDisplay (each
tick without TileRef), BuildMenu (each tick when open), MainRadialMenu
(each tick when open), PlayerPanel (each tick when open),
StructureIconsLayer (when placing a building from build bar),
NukeTrajectoryPreviewLayer (when placing nuke, on tick when tile
changes), ClientGameRunner (on click to attack/auto-boat or hotkey B or
G).

After https://github.com/openfrontio/OpenFrontIO/pull/3213 got merged,
the change with largest impact in
https://github.com/openfrontio/OpenFrontIO/pull/3193 was done in such a
different way that a new PR was needed

The idea in 3193 was to not always ask for Transport Ship from
buildableUnits. In such a way that very little extra data was send to
the worker. This had the biggest impact on performance (the idea was
months older btw, see
https://github.com/openfrontio/OpenFrontIO/pull/2295). Now, we do it the
other way around, by telling buildableUnits all unit types we want. Or
we want them all (undefined). The downside is more data is send in the
worker message. The upside is we have more options and can add more in
this PR.

This PR implements some of the leftovers in 3193 on top of 3213 and adds
further improvements.

(Some unrelated refactor/perf changes where moved out of this PR and
into already merged
https://github.com/openfrontio/OpenFrontIO/pull/3233,
https://github.com/openfrontio/OpenFrontIO/pull/3234,
https://github.com/openfrontio/OpenFrontIO/pull/3235,
https://github.com/openfrontio/OpenFrontIO/pull/3236,
https://github.com/openfrontio/OpenFrontIO/pull/3237,
https://github.com/openfrontio/OpenFrontIO/pull/3238,
https://github.com/openfrontio/OpenFrontIO/pull/3239)

- **GameRunner**, **WorkerMessages**: _playerActions_ and
_PlayerActionsMessage ._ Option to ask for no buildable units (null). It
now has 3 modes: get all actions and all buildings (units undefined),
get all actions and no buildings (units null), or get all actions and
specific building (units contains Unit Types).

- **GameRunner**: _playerActions_. fixes wrong assumption in PR 3213:
that only if units was undefined, we have to know canAttack.
ClientGameRunner wants to know both, in case of a click on non-bordering
land, to decide if it should auto-boat using a Transport Ship. So units
is not undefined (we only ask for Transport Ship now which has a
positive effect on performance for each click/tap) but we need canAttack
still.
Solved by removing the unit === undefined check before _canAttack_ in
_playerActions_.

- **GameRunner**, **GameView**, **WorkerClient**, **WorkerMessages**,
**Worker.worker**: added _playerBuildables_ / _buildables_ next to
existing _playerActions_ / _actions_. With above solved, there was still
no option to only get buildable units when the actions are not needed.
While **StructureIconsLayer**, **NukeTrajectoryPreviewLayer**,
**BuildMenu** and **UnitDisplay** need only that. To not make
playerActions more convoluted with more params or so, i've added a new
function _playerBuildables_ in **GameView** to only get buildable units
(**GameRunner** _playerBuildables_). _playerBuildables_ has 2 modes: get
all buildings (units undefined) or get specific buildings (units
contains Unit Types). Also update some comments that mentioned .actions
in **NukeTrajectoryPreviewLayer**.

- **ClientGameRunner**, **PlayerPanel**, **BuildMenu**, **UnitDisplay**,
**StructureIconsLayer** and **NukeTrajectoryPreviewLayer**: Since PR
3213, **StructureIconsLayer** and **NukeTrajectoryPreviewLayer** ask for
specific types of units from **GameView** _actions_ (**GameRunner**
playerActions). Now have the other files do the same. For example
**BuildMenu** asks for the new _BuildMenuTypes_ when it calls
._buildables_ and **ClientGameRunner** asks for UnitType.TransportShip
when sending a boat

- **ClientGameRunner**: canBoatAttack now accepts BuildableUnit[]
instead of PlayerActions so we can send it either actions.buildableUnits
or just buildables. Have functions call myPlayer.buildables(tileRef,
[UnitType.TransportShip]) when we only need a buildable unit and no
actions. Or myPlayer.actions(tileRef, null) when we need actions but no
buildable units. Or myPlayer.actions(tileRef, [UnitType.TransportShip])
when we need both actions, like canAttack, and a buildable unit. Then if
needed send either actions.buildableUnits or buildables to to
_canAutoBoat_ / _canBoatAttack_.

- **MainRadialMenu**: needs all player buildable unit types including
Transport Ship, so the _actions_ call argument for unit types can stay
undefined (unchanged) there.

- **MainRadialMenu**: now that **BuildMenu** uses _playerBuildables_
instead of _playerActions_, we must put data in
_this.buildMenu.playerBuildables_. And since we're not putting the
(unneeded) full _actions_ in there anymore, we can now put only the
needed and expected _actions._buildableUnits_ in it.

- **Game**, **PlayerImpl**, **StructureIconsLayer**: Typesafety and some
added perf: new type _PlayerBuildableUnitType_ (see also the below point
for how it is formed). So callers of _buildableUnits_ can never ask for
the wrong type like e.g. UnitType.Train because it doesn't return data
for that type. This type is now used in **PlayerImpl**, **BuildMenu**,
**RadialMenuElements**, **StructureDrawingUtils** and **UnitDisplay**
for that reason. And **InputHandler**, **StructureIconsLayer** and
**UIState** (little more on that in point below).

- **InputHandler**, **StructureIconsLayer**, **UIState**: In order to
make type safety work for GhostUnit.buildableUnit.type too (line ~217 of
StructureIconsLayer), changed type of interface _BuildableUnit_ to
_PlayerBuildableType_. Which is only more accurate. Same for and
this.structures and uiState.ghostStructure and with the latter,
_renderUnitItem_ in **UnitDisplay** and _setGhostStructure_ in
**InputHandler**. All Structures are of PlayerBuildableType (there are
even some in PlayerBuildables that aren't Structures, but it is much
more confined than UnitType).

- **Game**: Typesafety and some added perf: added _BuildMenus_ and
_BuildableAttacks_ in the same fashion that the existing StructureTypes
was already used (simplified it a bit too, with it renamed
_StructureTypes_ to _Structures_ and removed _isStructureType_). They
can be used with .types or .has(). _BuildableAttacks_.has() is used in
**RadialMenuElements**. _BuildableAttacks_ and existing _Structures_ now
make up _BuildMenus_. Which is used in **BuildMenu**,
**StructureIconsLayer** and **UnitDisplay**. Then _BuildMenus_ together
with UnitType.TransportShip make up the _PlayerBuildables_. Which is
used in **PlayerImpl** _buildableUnits_ (see point below). And with
_PlayerBuildableUnits_ we get the new _PlayerBuildableUnitType_ (see
above point on Game / PlayerImpl).

- **RadialMenuElements**: replace non-central ATTACK_UNIT_TYPES in
**RadialMenuElements** with centralized _BuildableAttackTypes_ too. Use
_PlayerBuildableUnitType_ for more type safety (can't by mistake add
UnitType.Train to its build menu). Make use of _BuildableAttackTypes_
instead of adding items hardcoded line by line in _getAllEnabledUnits_,
just like we already did since PR 3239 with _StructureTypes_. And use
_BuildableAttacks.types_ in the same fashion that existing
_isStructureTypes_ (now Structures.types) was already used elsewhere.

- **PlayerImpl**: _buildableUnits_ 
-- would do Object.values(UnitTypes) on every call. Now for better perf
directly loop over player buildable units by using _PlayerBuildables_
(see above point). In this way we also exclude MIRVWarhead, TradeShip,
Train, SamMissile and Shell so there are less unit types to loop through
by default. Since a player doesn't build those by themselves, they are
only build by Executions which use _canBuild_ directly and not
_buildableUnits_.
-- for more performance, do for loop instead of using .map and .filter,
no intermediate array needed nor callback overhead. We just loop over
the given units (which if undefined will contain _PlayerBuildables_).
Also pre-allocate the results array to get the most out of it, even if
V8 might already be very good at this.
-- cache config, railNetwork and inSpawnPhase so they can be re-used
inside the for loop.
-- cache cost inside the loop
-- it would check twice for tile!==null to decide to call
findUnitToUpgrade and canBuild. Now once.
-- eliminated double/triple checks for the same thing. It called
_findUnitToUpgrade_ (and with that _canUpgradeUnit_) and then _canBuild_
which both check if player has enough gold for the cost of the unit
type. And they both check if the unit type is disabled. Now we call
private functions _canBuildUnitType_, _canUpgradeUnitType_ to first do
checks on unit type level for early returns, and
_findExistingUnitToUpgrade_ to find existing unit without doing anything
extra. in a specific order to check everything only once. The public
functions _findUnitToUpgrade_ and _canBuild_ have an unchanged
functionality and we don't call them from _buildableUnits_ anymore.
-- would get _overlappingRailRoads_ and _computeGhostRailPaths_ when
canBuild was true. But this data is only meant for
**StructureIconsLayer** and it logically only uses it when placing a new
unit, not when upgrading one. Which is also commented on line 351 of
**StructureIconsLayer**. So, we now only get overlapping railroads and
ghost rails if we're not hovering to upgrade an existing unit.

- **PlayerImpl**: _findUnitToUpgrade_: unchanged functionality, but have
it call new private function _findExistingUnitToUpgrade_ to find
existing unit.

- **PlayerImpl**: _canBuild_: unchanged functionality, but have it call
new private function _canBuildUnitType_ to do the checks it first did
itself. And then new private function _canSpawnUnitType_ for the rest of
the checks. This way we can call _canBuildUnitType_ and
_canSpawnUnitType_ from _buildableUnits_ in a specific order to prevent
double/triple checks.

- **PlayerImpl**: _canBuildUnitType_: new private function to be shared
by _buildableUnits_, _canBuild_ and _canUpgradeUnit_ to be able do unit
type level checks in a specific order to prevent double/triple checks.
Via parameter knownCost, _buildableUnits_ can send it the cost it
already fetched so that it doesn't have to be fetched again. For caller
_canUpgradeUnit_, the isAlive() check (which was previously only done in
canBuild) is new but harmless, maybe even better to have also check
isAlive() on upgrade now that Nations are also upgrading which might
prevent some edge case bugs.

- **PlayerImpl**: _canUpgradeUnitType_: new private function to be
shared by _buildableUnits_ and _canUpgradeUnit_ to be able do unit type
level checks in a specific order to prevent double/triple checks.

- **PlayerImpl**: _canSpawnUnitType_: new private function to be shared
by _buildableUnits_ and _canBuildUnit_ to be able do unit type level
checks in a specific order to prevent double/triple checks.

- **PlayerImpl**: _findExistingUnitToUpgrade_: new private function to
be shared by _buildableUnits_ and _findUnitToUpgrade_ to be able do unit
level checks in a specific order to prevent double/triple checks.

- **PlayerImpl**: _isUnitValidToUpgrade_: new private function to be
shared by _buildableUnits_ and _canUpgradeUnit_ to be able do unit level
checks in a specific order to prevent double/triple checks.

- **PlayerImpl.test.ts**: because of the isAlive() check in which is new
for _canUpgradeUnit_ (see above at _canBuildUnitType_), the tests needed
to have the players be alive at the start, in order to pass.

- **BuildMenu**: use .find instead of .filter in canBuildOrUpgrade, a
function we already needed to change. This is faster and prevents an
allocation.


**PERFORMANCE**
As for calling ._buildables_ instead of unnecessarily getting
._actions_, there is an obvious win because there's less to send
calculate and recieve.

Also asking for only the needed buildings helps a lot (especially if
TradeShip isn't needed, see the difference in benchmark in original
#3193).

But the real-world impact is hard to measure. gave it a try in #3193 and
those results should be even better now.

Now testing only _buildableUnits_ performance in a synthetic benchmark,
we get these results. This is after other performance improvments so the
base is already better than it was in original #3193:

**BEFORE** (only buildableUnits itself)
<img width="602" height="96" alt="image"
src="https://github.com/user-attachments/assets/7770c0fa-a35e-42fc-90de-1de83242ec23"
/>

**AFTER** (only buildableUnits itself)
<img width="603" height="91" alt="image"
src="https://github.com/user-attachments/assets/a1578382-7010-4160-937c-7117bad18beb"
/>


## Please complete the following:

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

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

tryout33

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-04 11:32:45 -08:00
scamiv 28bbd933a4 Revert "fix: resolve drawImage scaling penalty on non-square sprite height" (#3337)
Reverts openfrontio/OpenFrontIO#3320

doesnt do what it says

The #3320 description claimed it “resolves a performance parsing
penalty” and fixes “non-square sprite” scaling/ghosting issues.
In reality, the code change was limited to:

* **clearRect**: switched from clearing a `clearsize x clearsize` square
(`clearsize = sprite.width + 1`) around `lastX/lastY` to clearing a
**(sprite.width+2) x (sprite.height+2)** rect around **rounded**
`clearX/clearY` (with an extra 1px pad via `-1/+2`).
* **drawImage**: changed a single call’s destination height from
`sprite.width` → `sprite.height`.

### Why revert

For unit rendering, sprites are square, so the drawImage change is a
no-op in practice, and the main effect was **clearing more pixels per
frame**. Any theoretical gain from rounding coordinates is speculative,
and is outweighed by the increased clear area/overdraw.
2026-03-03 11:30:22 -08:00
VariableVince 1d1b076672 Rename/fix: change Bots to Tribes (#3290)
## Description:

Resolves #3285. As discussed on Discord.

However, in at least one instance "Tribes" feels a bit off: in Humans vs
Nations, team "Tribes" feels as human too while they are just bots.

This PR changes Bots to Tribes outwardly by 
- Changing default EN translation.
- Changing (untranslated) alt text in PlayerPanel.
- To change "Team Bot" into "Team Tribes" too in PlayerInfoOverlay and
TeamStats (team leaderboard in-game), translate team names in there from
now on too.
- This way we also fix a bug where team names were not translated yet in
there. To add to that fix, also translate team names in LobbyPlayerView
in the same way. For this we re-use the existing
getTranslatedPlayerTeamLabel function from GameLeftSideBar by moving it
to Utils.
- No translation key was present yet for Humans and Nations teams, so
added those to now be used in PlayerInfoOverlay, LobbyPlayerView and
TeamStats for completeness.
- No internal code changes so nothing breaks.

**BEFORE (showing old team name Bot and also that team names weren't
translated yet in TeamStats)**
![No translation yet in
TeamStats](https://github.com/user-attachments/assets/38f465bc-ef82-4474-806c-015bb640d233)

![No translation yet in TeamStats
2](https://github.com/user-attachments/assets/a4387f1e-0e80-491d-b57d-e52b3c616e2b)


**AFTER** (translations in Dutch only shown as proof here, did not
include nl.json in the PR)
![AFTER translated in TeamStats for Humans vs Nations as an example in
NL
json](https://github.com/user-attachments/assets/1a7dcf4e-4263-4d6b-a992-58cb08a4fa7b)
![AFTER Tribe as player type in
PlayerInfoPanel](https://github.com/user-attachments/assets/6fd09686-320e-4fee-9c0d-397e581aa676)
![AFTER translated Team name PlayerInfoPanel as an
example](https://github.com/user-attachments/assets/1b4bc684-9ef4-47a9-b91c-4ed5cda65e9e)
![AFTER Tribes in EN now that it is translated in TeamStats so fetched
from EN
json](https://github.com/user-attachments/assets/5ea6528b-7e3c-4c6e-abeb-2769fb0aedee)
![AFTER Instructions example of changed text
](https://github.com/user-attachments/assets/6c7a7ab7-1dea-4f11-bacf-3e2edcdb074b)



## Please complete the following:

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

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

tryout33
2026-03-02 18:20:10 -08:00
Skigim 60f69a6408 perf: remove O(n) StructureIconsLayer render lookups (#3305)
Begins work on #3207

## Description:

This PR is the first optimization slice for #3207: it removes O(n)
render lookups in `StructureIconsLayer` by replacing array-first render
state with a unit-id keyed map, and tightens hot-path execution to
reduce per-tick allocations.

### What changed
- Refactored render state from array-first to `rendersByUnitId:
Map<number, StructureRenderInfo>`.
- Replaced O(n) lookup/delete paths with O(1) `Map#get` / `Map#delete`.
- Replaced `seenUnits` object-identity tracking with `seenUnitIds:
Set<number>`.
- Removed `tick()` array/closure chain (`map(...).forEach(...)`) and
switched to index-based loop.
- Reduced ghost-path allocation pressure by reusing a layer-level `Set`
for connected ally IDs instead of allocating `filter` + `map` + `new
Set` per ghost query.
- Added dirty-flag caching for structure visibility focus
(`visibilityStateDirty`) so expensive visibility-state scans recompute
only when toggles change.

### Performance validation (before/after)
Benchmark added: `tests/perf/StructureIconsLayerLookupPerf.ts`

Command:
`npm run perf`

Observed result:
- `StructureIconsLayer BEFORE (array O(n) lookup/delete) x 0.33 ops/sec
±13.28%`
- `StructureIconsLayer AFTER (unit-id map O(1) lookup/delete) x 95.65
ops/sec ±2.46%`
- Fastest implementation: AFTER (unit-id map)

#### Profiler screenshots are too noisy to be useful for such a focused
change

### Verification
- `npx tsc --noEmit` 
- `npx eslint src/client/graphics/layers/StructureIconsLayer.ts
tests/perf/StructureIconsLayerLookupPerf.ts` 
- `npm run perf` 

## Please complete the following:

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

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

skigim
2026-03-02 17:40:15 -08:00
Skigim 17f32a590c fix: resolve drawImage scaling penalty on non-square sprite height (#3320)
## Description:

This PR resolves a performance parsing penalty in the `UnitLayer`
rendering loop by fixing two issues with non-square sprites:
1. `drawImage` was incorrectly using `sprite.width` for both width and
height, causing aspect ratio squashing and forcing the browser to
perform a scaling operation on the image instead of hitting the canvas
fast-path. It now correctly uses `sprite.height` for the vertical
dimension.
2. `clearUnitsCells` was previously configured to only clear a square
grid (`clearsize`) based solely on width, meaning taller sprites would
leave visual artifact "ghosts" on the map. The clearing bounds have been
corrected to leverage discrete `sprite.width` and `sprite.height`.
Additionally, these values are wrapped in `Math.round()` prior to offset
calculation to prevent subpixel anti-aliasing CPU penalties during
`clearRect`.

## Please complete the following:

- [x] I have added screenshots for all UI updates (not needed)
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file (no new text)
- [x] I have added relevant tests to the test directory (existing tests
suffice, change was minuscule and non-breaking)
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:
skigim
2026-03-02 10:56:37 -08:00
Skigim f7598369ed refactor: consolidate platform detection across client components (#3325)
## Description:

This PR consolidates ad hoc platform/environment/viewport detection into
a single shared utility. It is scoped to this refactor only, and serves
as groundwork for the mobile-focused feature work planned for the v31
milestone.

### What changed
- Introduced a shared `Platform` utility centralising:
  - OS detection (with `userAgentData` + UA fallback)
  - Electron environment detection
- Viewport breakpoint helpers (`isMobileWidth`, `isTabletWidth`,
`isDesktopWidth`)
- Replaced duplicated inline checks across client files with the shared
API.
- Normalised Mac detection to derive from the consolidated OS logic
rather than a separate regex.

### Why
- Multiple client files each independently ran `navigator.userAgent`
regexes or copy-pasted `isElectron` logic — this unifies all of that.
- Puts a stable, tested abstraction in place before v31 mobile work
lands, so mobile feature branches have a consistent surface to build
against.

## Please complete the following:

- [x] I have added screenshots for all UI updates (N/A: refactor only,
no visible UI changes)
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file (N/A: no new user-facing strings)
- [x] I have added relevant tests to the test directory (N/A: refactor
only)
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

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

skigim
2026-03-02 10:12:48 -08:00
scamiv 50197e7254 perf(PerformanceOverlay): reduce per-render overhead (#3295)
## Description:

Cache translated UI labels per language/translation load
Avoid per-frame layer breakdown sorting unless expanded
Use rolling sums instead of array reduce
Drop redundant requestUpdate() calls and object clones

## Please complete the following:

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

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

DISCORD_USERNAME
2026-03-01 20:15:31 -08:00
bijx 8754f5291f Feat: Alphanumeric Coordinate Grid on Alternate View (#2938)
## Description:

Adds a coordinate grid to the Alternate View (holding spacebar) using
numbers on the X-axis, and letters on the Y-axis. No more "he's
attacking you in that—well, the little peninsula thing... next to the
island! which island? uhh..." moments when playing with friends.
Optimally maps have letters A-J (just like in the Battleships board
game) but special maps like Amazon River dynamically resize to only have
2 letters so as to not have too many number columns. This feature
overall can be toggled via the settings menu.

Also saw it requested on the [official
discord](https://discord.com/channels/1359946986937258015/1457037351422263480)
a couple times, thought it was a neat idea.

### World Map
<img width="3809" height="1824" alt="image"
src="https://github.com/user-attachments/assets/dab56879-a34e-48ea-a588-2907d26feb45"
/>

### Scales correctly when zoomed in
<img width="3798" height="1874" alt="image"
src="https://github.com/user-attachments/assets/7e06a47f-d3d9-4f92-8e89-3eaf866e9b25"
/>

### Amazon River
<img width="3803" height="1595" alt="image"
src="https://github.com/user-attachments/assets/4797c576-20b2-4aa8-8b7a-107078ab6308"
/>

### Enable/Disable via settings


https://github.com/user-attachments/assets/ec9f4e07-70a1-4f9d-b137-c3c3d2a2540c

## Please complete the following:

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

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

bijx

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
2026-02-28 20:28:47 -08:00
scamiv c911bfb2d8 Packed unit updates / MotionPlans (#3292)
## Description:

Reduce per-step `Unit` update traffic by shipping packed motion plans
and letting the client advance plan-driven units locally.

Changes:
- Add packed motion plan records (`packedMotionPlans?: Uint32Array`) to
game updates and transfer the buffer worker -> main.
- Introduce `src/core/game/MotionPlans.ts` (schema + pack/unpack) for
grid + train motion plans.
- Extend `Game` with `recordMotionPlan(...)` and
`drainPackedMotionPlans()`, and implement buffering/packing in
`GameImpl`.
- Treat units with motion plans as “plan-driven”: suppress per-tile
`Unit` updates on `move()` and advance positions client-side.
- Emit motion plans from executions:
- `TradeShipExecution`: record/update grid motion plans and `touch()`
when changing target after capture.
- `TransportShipExecution`: record initial plan and update it when
destination changes.
  - `TrainExecution`: record a train plan on init (engine + cars).
- Client: apply motion plans in `GameView` and ensure `UnitLayer`
updates sprites for motion-planned units even when no `Unit` updates
arrived.

## Please complete the following:

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

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

DISCORD_USERNAME
2026-02-27 20:54:42 -08:00
evanpelle f09177f8fe Merge branch 'v29' 2026-02-26 17:40:57 -06:00
Mattia Migliorini 7b785ea79a Fix alliance renewal prompt incorrectly dismissed for both players (#3297)
## Description:

NOTE: Applies to current main / beta version. Needs to be included in
v30.

When a player clicked "Renew Alliance", the `AllianceExtensionUpdate`
broadcast caused both players' renewal prompts to be removed, even the
one who hadn't yet acted. This happened because
`onAllianceExtensionEvent` called `removeAllianceRenewalEvents`
unconditionally on every client.

This PR fixes the behavior by calling `removeAllianceRenewalEvents` only
for the player that executed the action.

## Please complete the following:

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

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

deshack_82603
2026-02-25 21:12:58 -06:00
Evan 7f03072e9b revert skin trials (#3293)
## Description:

Skin trials has been a failure, very low fill rate and cause a major
drop in sales.

reverts 


https://github.com/openfrontio/OpenFrontIO/commit/97d0a05d58e926e3de4ba46d8dd14a04d60d6698

## Please complete the following:

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

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

evan
2026-02-24 15:47:21 -06:00
scamiv 4b917c4153 Performance Overlay rework/redesign (#3274)
## Description:
updates the Performance Overlay to be more usable
(draggable/resizable/scrollable), adds tick-level metrics (TPS +
per-layer tick timings), and reduces overhead when the overlay is
hidden.

### UI/UX
- Overlay layout updated to a fixed, pixel-positioned panel (default
near top-left) with a dedicated drag handle.
- Overlay is touch-draggable (pointer events) and remains usable on
small viewports via internal scrolling.
- Overlay width is resizable with a right-edge handle; width is clamped
to viewport bounds.
- Render/tick layer breakdown sections are collapsible, with headers and
“last tick” summaries.

### New metrics
- Adds TPS reporting:
  - Current TPS (ticks in the last 1s).
- Average TPS over the last ~60s, computed using elapsed time so it’s
accurate before a full 60s passes.
- Adds per-layer tick profiling (“Tick Layers”) alongside render
profiling (“Render Layers”).
- Adds “render-per-tick” metrics so render-layer costs can be understood
per simulation tick (frames + per-layer totals).

### Performance / overhead
- Avoids profiling overhead when the overlay is hidden:
- `GameRenderer` only calls `FrameProfiler.clear()/consume()` and
per-layer `start/end` when profiling is enabled.
- Tick-layer duration tracking is only collected when profiling is
enabled.

### Settings plumbing
- `UserSettings` now dispatches a `user-settings-changed` `CustomEvent`
on `set()` / `setFloat()`.
- The overlay listens for `settings.performanceOverlay` changes so
visibility stays in sync even when toggled outside the overlay.

## Implementation notes (by file)

- `src/client/graphics/layers/PerformanceOverlay.ts`
  - Adds TPS tracking using a timestamp ring + moving heads (1s / 60s).
- Adds UI state for collapsibles, drag + resize pointer tracking, and
new breakdown models:
    - Render layers: EMA avg/max + per-tick render aggregation.
    - Tick layers: EMA avg/max + last-tick durations.
- Copy-to-clipboard snapshot now includes TPS, tick layers, and
render-per-tick last-tick details.

- `src/client/graphics/GameRenderer.ts`
  - Gates render-layer profiling behind `FrameProfiler.isEnabled()`.
- Accumulates per-render-layer timings across frames and publishes them
once per tick via `updateRenderPerTickMetrics(...)`.
- Measures tick-layer durations (per layer `tick()` call) and publishes
them via `updateTickLayerMetrics(...)`.

- `src/core/game/UserSettings.ts`
- Adds `emitChange(key, value)` to dispatch `user-settings-changed` to
`globalThis` (best-effort).

- `resources/lang/en.json`
- Adds/updates `performance_overlay.*` strings for TPS and the new
render/tick layer sections.

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

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-23 14:22:56 -06:00
Mattia Migliorini 90204f6628 Add alliance renewal action to Radial Menu (#3148)
## Description:

The following PR replaces the (disabled) alliance request button with an
alliance extension/renewal button when the alliance with the target
player is expiring.

Agreeing to renewal via radial menu also hides the message in the
EventsDisplay.

<img width="369" height="364" alt="image"
src="https://github.com/user-attachments/assets/d8040f5c-ad7b-47d0-852f-925ecbf273a8"
/>


https://github.com/user-attachments/assets/aa589edf-6505-46bf-88a3-aa4c2df9137f

Icon size adjusted:

<img width="294" height="252" alt="image"
src="https://github.com/user-attachments/assets/7ca63500-b1fb-427b-965c-cf121a5213da"
/>

## Please complete the following:

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

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

deshack_82603

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-19 19:47:57 -06:00
VariableVince 2a7db43db3 Small refactor/cleanup: RadialMenuElements and PlayerImpl (#3239)
## Description:

PR 7/x in effort to break up PR #3220. Follows on already merged #3238.

Please see if these can be merged for v30.

-**RadialMenuElements**: 
- _getAllEnabledUnits_: use camelCase instead of PascalCase so change
Units into units.
- _getAllEnabledUnits_: use StructureTypes to loop through instead of
having 6 individual lines of code to check if a structure is enabled.
StructureTypes contains and will keep containing the same structures as
those that are checked here. PR 3220 will later on also replace the
individual lines for the attack type units into a loop with a newly
introduced Types array, in the same way as we do in this PR with
StructureTypes.
- _getAllEnabledUnits_: rename the long named const
addStructureIfEnabled to just addIfEnabled, which is clear enough from
the context it is used in.

-**PlayerImpl**
- _buildableUnits_: removed unnecessary "as BuildableUnit" after the
in-loop return; the function itself already says it returns
BuildableUnit[].

## Please complete the following:

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

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

tryout33
2026-02-19 00:27:12 +00:00
Mattia Migliorini ba2a947061 Feat: Display ghost railways when building cities and ports (#3202)
## Description:

Based on [this suggestion on
Discord](https://discord.com/channels/1284581928254701718/1447110257196138577)
and feedback gathered in [this
thread](https://discord.com/channels/1359946986937258015/1469598906173227184).

Supersedes #3143 

This PR introduces "ghost railways": when you are going to place a city
or port, previews railway connections that will be made when actually
building the structure.

Ghost railways are skipped if the structure is going to be snapped to
existing railways (as in railway snapping functionality introduced in
#3156 ).

### Video


https://github.com/user-attachments/assets/ff8cf325-6501-4df8-801d-c8ae3ced3d0e


### Ghost rails color revisited

black with 40% opacity

<img width="695" height="430" alt="image"
src="https://github.com/user-attachments/assets/272efbcc-4185-426a-921c-7fae61f6c462"
/>


## Please complete the following:

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

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

deshack_82603
2026-02-18 21:44:08 +00:00
VariableVince f1cd478970 Cleanup/refactor: Remove some redundant checks (#3235)
## Description:

PR 3/x in effort to break up PR
https://github.com/openfrontio/OpenFrontIO/pull/3220. Follows on already
merged https://github.com/openfrontio/OpenFrontIO/pull/3233 and
https://github.com/openfrontio/OpenFrontIO/pull/3234.

Please see if these can be merged for v30.

- **ClientGameRunner**: removed two redundant myPlayer===null checks
since that was already done right above, instead use !.
- **BuildMenu**: just like in UnitDisplay, assign public PlayerActions
default value of null. So that in canCreateOrBuild, where we already do
a === null check on it btw, we can safely skip the assignment to const
buildableUnits and just directly loop over
this.playerActions.buildableUnits.
- **RadialMenuElements**: don't call canBuildOrUpgrade 3x in
CreateMenuElements for the .map on flattenedBuildTable, instead do it
once and re-use outcome.

## Please complete the following:

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

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

tryout33
2026-02-18 21:09:24 +00:00
VariableVince f4f7ae3929 Cleanup: comments in BuildMenu and error messages in worker.worker (#3233)
## Description:

PR 1/x in effort to break up PR
https://github.com/openfrontio/OpenFrontIO/pull/3220.
Please see if these can be merged for v30.

- **BuildMenu**: remove one redundant comment about replacing an icon
(which has been done long ago already). And fix typo in one other
comment.
- **Worker.worker**: correct some existing error messages.

## Please complete the following:

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

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

tryout33
2026-02-18 20:21:19 +00:00
VariableVince 52036cc20c Cleanup: remove unused code from four files (#3234)
## Description:

PR 2/x in effort to break up PR
https://github.com/openfrontio/OpenFrontIO/pull/3220.

Removes unused code and properties.

- **Game.ts** and **DefaultConfig** unitInfo: removed
_canBuildTrainStation_ and _expirimental_ properties, as they weren't
used anywhere anymore.

- **PlayerActionHandler**: remove unused getPlayerActions, the only
potential caller MainRadialMenu already just calls myPlayer.actions via
GameView directly.

- **StructureIconsLayer**: remove unused PlayerActions

## Please complete the following:

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

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

tryout33
2026-02-18 20:17:46 +00:00
FloPinguin f276a72635 Remove win modal animation 🖌️ (#3230)
## Description:

Win modal animation looks bugged (https://youtu.be/fmR4nZL5RLg?t=3839)
Just remove it, make the game feel snappier
Reported by Wonder

## Please complete the following:

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

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

FloPinguin

Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
2026-02-17 20:35:38 +00:00
FloPinguin 18f52c01bb Improve moble UI again (#3226)
## Description:

- Fix HeadsUpMessage appearing above Settings modal
- Fix HeadsUpMessage appearing above Leaderboard
- Remove PlayerInfoOverlay show/hide animation (we need quick access to
the data!)

- Close PlayerInfoOverlay on tap outside the map (gray area)

- Fix error when tapping gray area outside the map
- Close PlayerInfoOverlay on click/tap on itself


## Please complete the following:

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

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

FloPinguin
2026-02-16 23:28:43 +00:00