## Problem
In the nuke trajectory preview, the SAM-intercept **"X"** marker was
drawn over **teammates'** SAMs — implying their SAM would shoot down
your missile. It shouldn't: like allies, a teammate's SAM never engages
your nuke. The bug only affected teammates; allies already worked.
## Cause
The preview built its threat set from `myPlayer.allies()` only — formal
alliances — and never considered teammates. That diverged from the sim
([`SAMLauncherExecution.ts`](src/core/execution/SAMLauncherExecution.ts#L118-L134)),
which skips any nuke whose owner it's `isFriendly()` with (**same team
OR allied**).
## Fix
`samThreatensNukePreview` now takes a teammate set and excludes
teammates **unconditionally**.
The subtlety: allies keep the existing *betrayal* exception — a strike
close enough to break the alliance makes that ally's SAM engage at
launch (`listNukeBreakAlliance`, the same function the sim uses).
Teammates get **no** such exception, because a strike can break an
alliance but never a team relationship. So even a player who is both a
teammate *and* a betrayed ally is correctly left off the threat set.
## Notes
- The sim has an "aftergame fun" exception where teammate SAMs *do*
target teammate nukes once there's a winner. The preview only appears
while aiming a buildable mid-game (no winner yet), so that case doesn't
apply here.
## Tests
Updated `samThreatensNukePreview` unit tests for the new signature and
added coverage for: teammate excluded, and teammate stays excluded even
when listed as betrayed. All 11 tests pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## 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>
## Summary
Fixes#4226 (Release Blocker, V32 regression).
The WebGL nuke trajectory preview built its SAM threat set by
unconditionally excluding own + allied SAMs
(`BuildPreviewController.updateNukeTrajectoryPreview`). But when the
strike targets allied territory, the alliance breaks at launch —
`NukeExecution.maybeBreakAlliances()` — so the betrayed ally's SAMs
**do** engage the nuke. The preview therefore showed a fully white
trajectory with no intercept X over an allied SAM, even though the bomb
would be shot down (V31 previewed this correctly).
## Fix
- Compute the would-be-betrayed player set with
`listNukeBreakAlliance()` — the exact function the sim uses at launch,
so preview and sim can't drift.
- Keep an allied SAM in the threat set iff its owner is in that set
(extracted as pure `samThreatensNukePreview()`).
- Other (non-betrayed) allies' SAMs remain excluded, matching sim
behavior where only alliances over the blast threshold break.
Both missing artifacts in the issue (red post-intercept segment and X
marker) come from `tSamIntercept` staying at 1.0 because no SAM was
supplied, so this one change restores both.
Cost note: this adds one `circleSearch` per throttled ghost update
(50ms) when the player has allies — same order as the existing
`wouldNukeBreakAlliance` call for the red warning circle.
## Testing
- Unit tests for the new threat-set predicate (4 cases) in
`tests/client/controllers/BuildPreviewController.test.ts`
- `tsc --noEmit`, ESLint, Prettier clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The preview arc could show a nuke originating from a silo the game would
never fire from. The renderer's silo selection had diverged from the
authoritative PlayerImpl.nukeSpawn that NukeExecution uses:
- Eligibility: the renderer only excluded inactive silos, but the game also
excludes silos that are reloading (isInCooldown) or under construction.
- Distance: the renderer used Euclidean distance; the game uses Manhattan.
Add isInCooldown() to UnitView mirroring UnitImpl, and update the trajectory
preview to filter on isActive && !isInCooldown && !isUnderConstruction and
pick the nearest silo by Manhattan distance. When no silo is eligible the
trajectory clears, matching canBuild returning false.
The build-ghost icon already tracked the cursor at sub-tile precision, but
the range circle (defense post / SAM / nuke radius) and the nuke trajectory
arc still snapped to the hover tile, making them look jagged as the cursor
moved.
Range circle: cursorLoop now smooths radiusTileX/Y the same way as the icon,
except when upgrading an existing structure (the circle stays anchored to
that structure's real tile).
Nuke trajectory: split the work by cadence. The throttled renderGhost path
caches the static inputs (nearest silo + threatening SAMs) in
nukeTrajectoryStatic; cursorLoop rebuilds the Bezier each frame with the
live cursor as the destination. Source stays on the silo's tile; only the
endpoint follows the cursor.
All three previews now use the same tile-center (+0.5) convention.
> **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#4059
## Description:
Makes ghost radius centred on building being upgrading (if upgrading
building)
<img width="1920" height="1080" alt="Screenshot from 2026-06-01
15-45-37"
src="https://github.com/user-attachments/assets/9cf19e59-0e20-4b43-b65f-35eebb37fa8e"
/>
<img width="1920" height="1080" alt="Screenshot from 2026-06-01
15-45-24"
src="https://github.com/user-attachments/assets/1953238f-5156-4bf7-aa18-8278c27d1a68"
/>
## 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:
tktk123456
Wire targetingAlly through GhostPreviewData.rangeWarning so RangeCirclePass
can color the blast-radius circle red instead of white when the ghosted nuke
would trigger a traitor penalty. Restores behavior lost in the canvas→WebGL
migration.
## Description:
# Ghost structure cost label
Renders the gold cost of the currently-selected build under the ghost
structure cursor, with color-coded affordability/placement state. Honors
the
existing `cursorCostLabel` user setting (legacy name `ghostPricePill`,
already
shipping ON by default).
## Behavior
| State | Color |
|---|---|
| Can afford + valid placement | white |
| Can afford + can't place here (port on land, overlap, …) | gray |
| Can't afford | red |
The number is formatted via `renderNumber` (project-wide convention —
`1.5K`,
`1.23M`, etc.) and rendered as MSDF text at a fixed world-space scale,
centered
under the ghost icon.
## Implementation
The cost was already plumbed end-to-end on
[`GhostPreviewData.cost`](src/client/render/types/Renderer.ts) but never
visualized. This PR:
- Extends [`GhostPreviewData`](src/client/render/types/Renderer.ts) with
`showCost` (from setting) and `canAfford` (gold
vs. cost check, computed in
[BuildPreviewController](src/client/controllers/BuildPreviewController.ts)).
- Adds a `setGhostCostLabel(...)` channel to the MSDF text pass — one
persistent,
non-animated text instance alongside the existing ephemeral popups. No
new
pass, no new shader.
- Wires
[`Renderer.updateGhostPreview`](src/client/render/gl/Renderer.ts) to
push the label whenever a ghost is active.
- Renames `ConquestPopupPass` →
[`WorldTextPass`](src/client/render/gl/passes/WorldTextPass.ts) (and its
shader dir
`conquest-popup/` → `world-text/`) since it now handles conquest popups,
bonus popups, and the ghost cost label. Done with `git mv` so history is
preserved.
https://github.com/user-attachments/assets/c5b21bf3-f440-4c28-9b94-843df9bf6a37
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
evan
## Description:
Show factory and defence post radius for ghost structure when placing
structures from build bar (unitdisplay).
Show when city/port is placed directly over existing railroad, by
highlighting the railroad green. The railroad is not highlighted when
instead a city/port nearby the ghost structure will be upgraded instead
of placing it on the railroad. This works with the existing code in
buildableUnits in PlayerImpl: it would already return an empty array []
for overlappingRailroads and for ghostRailPaths when canUpgrade is
false. So the old checks for uiState for Canvas2D in
BuildPreviewController weren't even needed per se, they followed the
same logic as buildableUnits in PlayerImpl already did.
Both changes emulate how it worked before the move to WebGL.
- OverlappingRailroads now returns TileRefs instead of a railroad ID,
and it does so with less allocations than the previous code. It's a
determistic outcome, sorted and deduplicated. In doubt about this a bit,
because it's better also in case we ever do desync checks using this
data, but for the rendering it isn't needed per se and could be more
performant without allocations.
- Also: Cleanup obsolete Canvas2D rail highlighting state (UIState) that
was superseded by GhostPreviewData.
## 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
The render/ tree was the only place in the client still using kebab-case
filenames. Brings ~80 files in line with the rest of src/client/
(BuildPreviewController, TransformHandler, etc.). Directories kept as
they were (name-pass/, fx-pass/, passes/, utils/, debug/) since the
codebase already mixes those.
Two collisions surfaced and got resolved: render/types/ is a directory,
not a file, so its imports kept the lowercase form; and the sed pass
incidentally normalized core/pathfinding imports, which had to be
reverted since that file is actually lowercase on disk despite some
imports having referenced it as ./Types under macOS case-insensitive
resolution.
NukeTrajectoryPass and the rangeRadius pipe existed but had no caller —
trajectory arc and outer-blast circle never appeared during build mode.
BuildPreviewController now picks the closest active player silo as the
launch source, collects non-allied SAMs as threats, and pushes a
NukeTrajectoryData each preview tick. rangeRadius is set to
nukeMagnitudes(type).outer for AtomBomb / HydrogenBomb so the existing
RangeCirclePass renders the blast radius at the target.
Bypass the snap-to-tile in TransformHandler by adding
screenToWorldCoordinatesFloat. Each render frame, BuildPreviewController
re-emits the ghost preview at the cursor's exact world position
(adjusted by -0.5 to cancel the shader's tile-center offset). Buildable
validation still runs on the snapped tile at the 50ms throttle, but the
icon now follows the cursor 1:1 instead of stepping tile-to-tile.
graphics/ was a canvas2D-era directory name — TransformHandler, UIState,
and the Controller interface aren't graphics, they're cross-cutting
client state. Hoist them to src/client/ so the path matches what they
are. GhostStructureChangedEvent had three emitters and zero listeners;
removed.
BuildPreviewController and WarshipSelectionController now take the WebGL
view in their constructor and call view.updateGhostPreview /
view.setSelectedUnits themselves instead of emitting bus events that
ClientGameRunner forwarded. Splits the old mountWebGLDebugRenderer in
two — createWebGLView builds the view up front so the renderer can wire
controllers to it, mountWebGLDebugRenderer does the per-frame plumbing
after the transformHandler exists. GhostPreviewUpdatedEvent had no
remaining consumers and is removed.
UILayer → WarshipSelectionController and StructureIconsLayer →
BuildPreviewController. These are the two real Controller implementations
(state + click handling, no rendering) — the new names + location reflect
what they actually do now that all rendering lives in WebGL passes.