Skip to content

Highlight hovering railroad#3156

Merged
evanpelle merged 8 commits intoopenfrontio:mainfrom
DevelopingTom:rail_layer
Feb 9, 2026
Merged

Highlight hovering railroad#3156
evanpelle merged 8 commits intoopenfrontio:mainfrom
DevelopingTom:rail_layer

Conversation

@DevelopingTom
Copy link
Contributor

@DevelopingTom DevelopingTom commented Feb 8, 2026

Description:

rail_snap

The RailroadLayer simply displays tiles as instructed by the core worker. While it's practical for the layer to only care about the tiles, it also means it has no understanding of railroads as entities (their paths, connections, or identities).

It also means that the core worker is responsible for rendering tasks such as tile orientation and construction animation, which is not expected.

To support ID-based events and better separation of concerns, the rendering layer needs to be aware of complete railroads. With this change, the core worker can send the tiles once and subsequently reference railroads only by ID for all other events.

Changes:

  • RailroadLayer now stores full railroad data instead of only individual tiles

  • RailroadLayer is responsible for animating newly built railroads

  • Add a new RailroadSnapUpdate sent when a new structure is built over an existing railroad. This event is used by RailroadLayer to keep railroad ID in sync.

  • When hovering over a railroad, the render worker is querying the core worker about overlapping railroads.
    Alternatively, RailroadLayer could compute overlaps itself now that it has full railroad knowledge, but this logic would need to be duplicated and kept in sync across workers. Keeping a single source of truth in the core worker is preferred.

Edgecases:

  • When a structure snaps over a railroad, the original railroad is split into two new railroads. If the construction animation is still in progress, instead of resuming the animation at the correct point on the new railroads, all remaining tiles are rendered immediately
  • Previously, RailroadUpdate handled both construction and destruction. This no longer works with RailroadSnapUpdate, as event ordering is now pretty important and IDs may be lost before they are consumed.
    To address this, RailroadUpdate is split in two: RailroadConstructionUpdate and RailroadDestructionUpdate.

Please complete the following:

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

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

IngloriousTom

@DevelopingTom DevelopingTom self-assigned this Feb 8, 2026
@DevelopingTom DevelopingTom added the UI/UX UI/UX changes including assets, menus, QoL, etc. label Feb 8, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Refactors railroad handling to event-driven updates: adds RailroadView for per-tile direction and incremental drawing, splits updates into Construction/Snap/Destruction with per-tile RailTileChangedEvent, integrates overlappingRailroads into UIState/build flow, removes RailroadExecution, and updates RailroadLayer, FxLayer, and core railroad models.

Changes

Cohort / File(s) Summary
Renderer & layers
src/client/graphics/GameRenderer.ts, src/client/graphics/layers/FxLayer.ts, src/client/graphics/layers/RailroadLayer.ts, src/client/graphics/layers/RailroadSprites.ts
RailroadLayer now accepts uiState; FxLayer now accepts eventBus and listens for per-tile RailTileChangedEvent; RailroadLayer reworked to use RailroadView, emit per-tile events, and manage animated pending railroads.
Rail visualization
src/client/graphics/layers/RailroadView.ts
New module: RailType, RailTile, computeRailTiles, computeDirection, and RailroadView for incremental/tick-based painting and tile sequencing.
Core updates & models
src/core/game/GameUpdates.ts, src/core/game/Railroad.ts, src/core/game/RailNetwork.ts, src/core/game/RailNetworkImpl.ts
Replaced unified RailroadUpdate with RailroadConstruction/Destruction/Snap events; Railroad gained id; RailNetworkImpl adds nextId, emits construction/snap/destruction events, and implements overlappingRailroads(tile).
Build / UI state
src/client/graphics/UIState.ts, src/client/graphics/layers/StructureIconsLayer.ts, src/core/game/Game.ts, src/core/game/PlayerImpl.ts
Added UIState.overlappingRailroads: number[]; BuildableUnit now carries overlappingRailroads; StructureIconsLayer propagates/clears this state during build/upgrade flows.
Removed execution
src/core/execution/RailroadExecution.ts
Deleted RailroadExecution; progression logic moved to RailroadView and layer handling.
Tests & inputs
tests/core/game/TrainStation.test.ts, tests/InputHandler.test.ts
Tests updated to expect RailroadDestructionEvent payload and to include overlappingRailroads: [] in fixtures.
Minor signatures/imports
src/client/graphics/layers/*, src/core/game/*
Adjusted imports and constructor signatures across layers and sprites to match new event-driven API and view types.

Sequence Diagram(s)

sequenceDiagram
  participant RailNetwork as RailNetworkImpl
  participant Game as Game (update bus)
  participant EventBus as EventBus
  participant RailroadLayer as RailroadLayer
  participant FxLayer as FxLayer
  participant UI as UIState

  RailNetwork->>Game: emit RailroadConstructionEvent / RailroadSnapEvent / RailroadDestructionEvent
  Game->>EventBus: publish game update
  EventBus->>RailroadLayer: deliver construction/snap/destruction update
  RailroadLayer->>RailroadLayer: create/update RailroadView, start pending animation
  RailroadLayer->>EventBus: emit RailTileChangedEvent (per tile painted)
  EventBus->>FxLayer: deliver RailTileChangedEvent(tile)
  FxLayer->>FxLayer: spawn Dust FX at tile coords
  RailroadLayer->>UI: read UIState.overlappingRailroads to draw highlights
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🚂 Tiles whisper, rails unwind,
Events step out, one tile at a time,
Views draw slow while FX play,
Overlaps glow to show the way,
Old runner rests — the rails align.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title "Highlight hovering railroad" directly describes the main user-facing feature: visual highlighting when users hover over railroads.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining architectural improvements to RailroadLayer, event splitting, and the hover highlighting feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/graphics/layers/RailroadLayer.ts (1)

221-235: ⚠️ Potential issue | 🔴 Critical

Bug: highlight is painted before the railroad image, so it will be hidden underneath.

highlightOverlappingRailroads draws green rectangles on context at line 223, then drawImage paints the off-screen railroad canvas on top at line 224. The green overlay ends up behind the railroad sprites — users will not see it.

Move the highlight call to after drawImage so the green overlay is visible on top.

Proposed fix
     context.save();
     context.globalAlpha = alpha;
-    this.highlightOverlappingRailroads(context);
     context.drawImage(
       this.canvas,
       srcX,
       srcY,
       srcW,
       srcH,
       dstX,
       dstY,
       visWidth,
       visHeight,
     );
+    this.highlightOverlappingRailroads(context);
     context.restore();
🤖 Fix all issues with AI agents
In `@src/client/graphics/layers/RailroadView.ts`:
- Around line 1-2: The imports in RailroadView.ts use bare "src/..." paths;
update them to project-relative imports by replacing the top-level imports for
TileRef and GameView (the lines importing TileRef from "src/core/game/GameMap"
and GameView from "src/core/game/GameView") with the correct relative paths
(e.g., "../../core/game/GameMap" and "../../core/game/GameView" or the relative
path that matches this file's directory) so the file uses relative module paths
consistent with FxLayer.ts and the rest of the project.
- Around line 18-49: computeRailTiles crashes for 0 or 1 element arrays because
it unconditionally indexes neighbors; update computeRailTiles to early-return or
guard short arrays: if tiles.length === 0 return []; if tiles.length === 1
return [{ tile: tiles[0], type: RailType.VERTICAL }] (or otherwise avoid passing
undefined into computeExtremityDirection). Also adjust the two extremity
branches that call computeExtremityDirection (the first push and the final push)
to only reference tiles[1] or tiles[tiles.length-2] when tiles.length > 1 so
computeExtremityDirection and computeDirection never receive undefined.

In `@src/client/graphics/layers/StructureIconsLayer.ts`:
- Around line 336-344: The uiState.overlappingRailroads array is left stale when
hovering unbuildable tiles or when there is no unit, so update
StructureIconsLayer to clear uiState.overlappingRailroads in both the
early-return path where no unit is found and inside the unit.canBuild === false
branch; specifically, in the method handling hover/ghost updates (referencing
unit, ghostUnit, and uiState.overlappingRailroads), set
uiState.overlappingRailroads = [] before returning when unit is null/undefined
and also assign an empty array in the canBuild === false branch alongside
applying the red OutlineFilter.

In `@src/core/game/RailNetwork.ts`:
- Line 11: The interface method overlappingRailroads lacks a return type and
should be declared to return number[]; update the signature of
overlappingRailroads(tile: TileRef) to overlappingRailroads(tile: TileRef):
number[] so implementations and callers (e.g., the assignment in PlayerImpl
where the result is stored into a number[] field) have correct static typing.
🧹 Nitpick comments (6)
tests/core/game/TrainStation.test.ts (1)

99-120: Test mock is missing the id field on railRoad.

The railRoad mock (line 103–107) does not include an id property, but removeNeighboringRails reads toRemove.id (see TrainStation.ts). The assertion only checks type, so it passes — but the emitted update has id: undefined. Consider adding id to the mock and asserting on it for a stronger test.

Suggested improvement
     const railRoad = {
       from: stationA,
       to: stationB,
       tiles: [{ x: 1, y: 1 }],
+      id: 42,
     } as any;
     expect(game.addUpdate).toHaveBeenCalledWith(
       expect.objectContaining({
         type: GameUpdateType.RailroadDestructionEvent,
+        id: 42,
       }),
     );
src/client/graphics/layers/RailroadView.ts (1)

74-113: computeDirection falls through to the warning for diagonal straight-line movement.

If prev → current → next is a straight diagonal (e.g., dx1=1, dy1=1, dx2=1, dy2=1), the straight-line block (line 93) is entered but neither dx1 !== 0 triggers HORIZONTAL nor dy1 !== 0 triggers VERTICAL — it falls through, then the turn block condition (line 99) is false, and you hit the warning on line 111.

If the pathfinder only produces cardinal moves, this is not reachable. But the fallback to VERTICAL with a console.warn is a safe default.

Optional: handle straight diagonals explicitly
   // Straight line
   if (dx1 === dx2 && dy1 === dy2) {
     if (dx1 !== 0) return RailType.HORIZONTAL;
     if (dy1 !== 0) return RailType.VERTICAL;
+    // Diagonal straight line — shouldn't happen with cardinal pathing
+    return RailType.VERTICAL;
   }
src/client/graphics/layers/FxLayer.ts (1)

1-2: Import style is inconsistent but not a build issue.

Lines 1–2 use bare src/ paths while lines 3–8 use relative paths. The tsconfig has baseUrl: "." with bundler module resolution, so both styles resolve correctly and the codebase widely adopts the src/ pattern (seen in MapLandTiles.ts, NukeTelegraph.ts, NavalTarget.ts, RailroadView.ts, and others). For consistency within this file, align all imports to one style—either all relative or all bare src/.

src/client/graphics/layers/RailroadLayer.ts (3)

167-187: Early return when overlappingRailroads is undefined — but the type says number[].

Line 170 checks this.uiState.overlappingRailroads === undefined, yet the UIState interface declares it as number[] (not optional). If this field can truly be absent at runtime, the type should reflect that. Otherwise, just check .length === 0 and drop the undefined guard.

Also a minor note: consider using a distinct highlight color or outline rather than a filled green rectangle, so the railroad sprites remain visible under the overlay. Right now, rgba(0, 255, 0, 0.4) with a 2.5 × 2.5 fill may obscure small rail details.


238-270: Snap handler uses dummy RailType.HORIZONTAL for all tiles — fragile if these views are used beyond ID tracking.

The comment explains the tiles are already painted, so direction does not matter. That holds today because highlightOverlappingRailroads only reads .tile and removeRailroad calls clearRailroad which also ignores direction. But any future code that iterates drawnTiles() and relies on .type will silently get wrong data.

A short inline comment on lines 251–258 noting why the type is throwaway would help future readers. Low priority — just a readability note.


372-394: paintRail is public — is that intentional?

All other paint methods (paintRailTile, paintRailRects, paintBridge) are private. paintRail has no access modifier, so it defaults to public in TypeScript. If nothing outside the class calls it, mark it private for consistency.

Proposed fix
-  paintRail(railTile: RailTile) {
+  private paintRail(railTile: RailTile) {

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Feb 8, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 8, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/game/RailNetworkImpl.ts (1)

138-200: ⚠️ Potential issue | 🟡 Minor

Snap logic looks correct — one edge case to verify at boundary index

The snap flow correctly:

  • Preserves the originalId before unregistering the old rail
  • Creates two new segments with fresh IDs
  • Re-wires stations and the spatial grid
  • Emits RailroadSnapEvent with all needed data

One thing to double-check: when closestRailIndex === rail.tiles.length - 1 (last tile), newRailTo gets a single-tile segment (rail.tiles.slice(closestRailIndex)[lastTile]). A 1-tile railroad between station and to may cause odd rendering or train pathing. The guard on line 150 filters === 0 but not the symmetric end case.

Consider also filtering the last index
-      if (closestRailIndex === 0 || closestRailIndex >= rail.tiles.length) {
+      if (
+        closestRailIndex === 0 ||
+        closestRailIndex >= rail.tiles.length - 1
+      ) {
🤖 Fix all issues with AI agents
In `@src/client/graphics/layers/RailroadView.ts`:
- Around line 62-70: The branch that handles diagonal neighbors currently falls
through to returning RailType.VERTICAL when dx !== 0 && dy !== 0; update the
function (the block that inspects dx and dy and returns RailType constants) to
explicitly handle diagonals: either determine and return the correct diagonal
rail sprite if available, or at minimum emit a warning (matching the style of
computeDirection) when dx !== 0 && dy !== 0 and then return a clear default; use
the dx/dy check and the RailType enum names (e.g., RailType.VERTICAL /
RailType.HORIZONTAL) to locate the logic and add the warning/log call and/or the
explicit diagonal case so diagonal steps are not silently treated as VERTICAL.
🧹 Nitpick comments (3)
src/client/graphics/layers/RailroadView.ts (2)

90-94: Straight-line check does not return when both deltas are zero.

If two consecutive tiles are identical (dx1 === 0 && dy1 === 0), the condition on line 91 is true but neither inner if matches, so execution falls through to the corner-case block and eventually hits the warning on line 109. This is a minor edge case (duplicate tiles should not appear in practice), but adding an explicit guard would make the intent clearer.

Suggested guard
   // Straight line
   if (dx1 === dx2 && dy1 === dy2) {
+    if (dx1 === 0 && dy1 === 0) {
+      console.warn("Duplicate tiles in rail segment");
+      return RailType.VERTICAL;
+    }
     if (dx1 !== 0) return RailType.HORIZONTAL;
     if (dy1 !== 0) return RailType.VERTICAL;
   }

145-153: drawnTiles() creates new arrays on every call.

slice + concat allocates two intermediate arrays each call. If this is called frequently (every render frame), consider caching the result or using a single-pass approach. Not urgent — just noting for future optimization if profiling shows it matters.

src/core/game/RailNetworkImpl.ts (1)

202-206: Rename overlappingRailroads to nearbyRailroads for clarity

The method returns railroads within a 3-tile radius, not railroads that overlap the exact tile. The name overlappingRailroads is misleading. Since the radius (3 tiles) matches the train station connection distance and is used for UI hints during placement preview, renaming to nearbyRailroads would accurately reflect the actual behavior.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 8, 2026
@DevelopingTom DevelopingTom marked this pull request as ready for review February 8, 2026 17:00
@DevelopingTom DevelopingTom requested a review from a team as a code owner February 8, 2026 17:00
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 8, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/graphics/layers/RailroadLayer.ts (1)

229-243: ⚠️ Potential issue | 🟠 Major

Highlight is drawn before the railroad canvas is composited — it ends up behind the rails.

highlightOverlappingRailroads paints directly onto context at line 231, then drawImage paints the offscreen rail canvas on top at line 232. The green overlay will be occluded by the railroad pixels. If the intent is for the highlight to be visible on top of the rails, move the call after drawImage.

     context.save();
     context.globalAlpha = alpha;
     context.drawImage(
       this.canvas,
       srcX,
       srcY,
       srcW,
       srcH,
       dstX,
       dstY,
       visWidth,
       visHeight,
     );
+    this.highlightOverlappingRailroads(context);
     context.restore();
-    this.highlightOverlappingRailroads(context);

If the "glow behind" effect is intentional, please add a short comment so future readers know.

🤖 Fix all issues with AI agents
In `@src/client/graphics/layers/RailroadLayer.ts`:
- Around line 259-275: The submitted snap code creates RailroadView instances
for update.newId1/update.newId2 using placeholder RailType.HORIZONTAL for every
tile (from update.tiles1/update.tiles2), which can silently break later code
paths that read tile.type (e.g., drawnTiles() used by
highlightOverlappingRailroads or removeRailroad); change the creation to compute
real directions for each tile (call computeRailTiles or equivalent to produce
correct RailTile.type entries) before constructing new RailroadView, or if
computing directions is impossible here, add a clear doc comment on RailroadView
construction that snapped views must not have their tile.type read and ensure
removeRailroad/clearRailroad only rely on TileRef rather than type.
- Around line 316-334: The existing paintRailTile silently updates stored
direction by assigning railRef.tile = railTile without repainting, causing the
offscreen canvas to retain the old direction; in paintRailTile, before
overwriting railRef.tile compare the previous tile/direction (railRef.tile) to
the incoming railTile and if they differ call paintRail(railTile) after updating
the record (and still increment railRef.numOccurence and set lastOwnerId) so the
visual and bookkeeping stay in sync; use the existing symbols paintRailTile,
paintRail, existingRailroads, railRef, numOccurence, and lastOwnerId to locate
and implement this change.
🧹 Nitpick comments (5)
src/client/graphics/layers/RailroadLayer.ts (5)

171-195: Highlight rect size (2.5 × 2.5) does not match the offscreen canvas 2× scale.

Rail tiles are painted on the offscreen canvas at x * 2, y * 2 coordinates. But the highlight fillRect at line 191 uses game-space coordinates (x + offsetX) with a 2.5 × 2.5 rect. Since the main context is in game-space (the transform handler scales it), the 2.5 size may be correct visually — but it is a magic number that is easy to get wrong. A short inline comment explaining the chosen size relative to the tile scale would help future readers.


84-104: Deleting from a Set while iterating — works per spec but worth a note.

JavaScript guarantees safe deletion during for...of on a Set, so this is correct. A one-line comment would prevent a future contributor from "fixing" it.


34-36: RailTileChangedEvent is a simple value event — clean and minimal.

One small note: this class is exported from a layer file, which means other layers (like FxLayer) import from a sibling layer. If more events accumulate here, consider a shared events.ts to avoid circular or awkward cross-layer imports.


380-402: paintRail is public (no private keyword) — is that intentional?

Every other paint helper in this class is private. If nothing outside the class calls paintRail, mark it private for consistency and to shrink the public surface.

Proposed fix
-  paintRail(railTile: RailTile) {
+  private paintRail(railTile: RailTile) {

29-33: Export the snappable structure list from a shared module to keep client and core in sync.

The client layer defines SNAPPABLE_STRUCTURES at lines 29–33, but the core layer in RailNetworkImpl.ts (line 212) has an equivalent list [UnitType.City, UnitType.Factory, UnitType.Port] used for train station snapping logic. These lists already show drift (different order: Port, City, Factory vs City, Factory, Port). Move the canonical list to a shared module and import it in both places.

Comment on lines +259 to +275
const directions1: RailTile[] = update.tiles1.map((tile) => ({
tile,
type: RailType.HORIZONTAL,
}));
const directions2: RailTile[] = update.tiles2.map((tile) => ({
tile,
type: RailType.HORIZONTAL,
}));
// The rails are already painted, consider them complete
this.railroads.set(
update.newId1,
new RailroadView(update.newId1, directions1, true),
);
this.railroads.set(
update.newId2,
new RailroadView(update.newId2, directions2, true),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Snap creates RailroadViews with fake RailType.HORIZONTAL for every tile.

The comment says "the rails are already painted" so directions do not matter now. But these RailroadView instances are stored in this.railroads and later used by drawnTiles() in highlightOverlappingRailroads and removeRailroad. If removeRailroad is ever called on a snapped railroad and the removal path starts reading type (e.g. to clear bridge rects of a specific orientation), the wrong direction will produce incorrect clearing.

Even today, if a snapped railroad is later destroyed, clearRailroad only uses TileRef so it works — but this is fragile. Consider computing actual directions via computeRailTiles for the two new segments, or at minimum add a clear doc comment explaining the invariant that type on snapped views must never be read.

🤖 Prompt for AI Agents
In `@src/client/graphics/layers/RailroadLayer.ts` around lines 259 - 275, The
submitted snap code creates RailroadView instances for
update.newId1/update.newId2 using placeholder RailType.HORIZONTAL for every tile
(from update.tiles1/update.tiles2), which can silently break later code paths
that read tile.type (e.g., drawnTiles() used by highlightOverlappingRailroads or
removeRailroad); change the creation to compute real directions for each tile
(call computeRailTiles or equivalent to produce correct RailTile.type entries)
before constructing new RailroadView, or if computing directions is impossible
here, add a clear doc comment on RailroadView construction that snapped views
must not have their tile.type read and ensure removeRailroad/clearRailroad only
rely on TileRef rather than type.

Comment on lines +316 to 334
private paintRailTile(railTile: RailTile) {
const currentOwner = this.game.owner(railTile.tile)?.id() ?? null;
const railRef = this.existingRailroads.get(railTile.tile);

if (railTile) {
railTile.numOccurence++;
railTile.tile = railRoad;
railTile.lastOwnerId = currentOwner;
if (railRef) {
railRef.numOccurence++;
railRef.tile = railTile;
railRef.lastOwnerId = currentOwner;
} else {
this.existingRailroads.set(railRoad.tile, {
tile: railRoad,
this.existingRailroads.set(railTile.tile, {
tile: railTile,
numOccurence: 1,
lastOwnerId: currentOwner,
});
this.railTileIndex.set(railRoad.tile, this.railTileList.length);
this.railTileList.push(railRoad.tile);
this.paintRail(railRoad);
this.railTileIndex.set(railTile.tile, this.railTileList.length);
this.railTileList.push(railTile.tile);
this.paintRail(railTile);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Overlapping tile skips repaint — stored direction silently diverges from what is painted.

When two railroads share a tile, the second call to paintRailTile increments numOccurence and overwrites railRef.tile (new direction) but does not call paintRail. The visual on the offscreen canvas still shows the first railroad's direction while the bookkeeping now stores the second one. This mismatch can cause a wrong sprite on the next color-update repaint (line 132 calls paintRail(railRef.tile) with the silently-replaced direction).

Consider repainting when the direction changes:

Proposed fix
     if (railRef) {
       railRef.numOccurence++;
+      const directionChanged = railRef.tile.type !== railTile.type;
       railRef.tile = railTile;
       railRef.lastOwnerId = currentOwner;
+      if (directionChanged) {
+        this.paintRail(railTile);
+      }
     } else {
🤖 Prompt for AI Agents
In `@src/client/graphics/layers/RailroadLayer.ts` around lines 316 - 334, The
existing paintRailTile silently updates stored direction by assigning
railRef.tile = railTile without repainting, causing the offscreen canvas to
retain the old direction; in paintRailTile, before overwriting railRef.tile
compare the previous tile/direction (railRef.tile) to the incoming railTile and
if they differ call paintRail(railTile) after updating the record (and still
increment railRef.numOccurence and set lastOwnerId) so the visual and
bookkeeping stay in sync; use the existing symbols paintRailTile, paintRail,
existingRailroads, railRef, numOccurence, and lastOwnerId to locate and
implement this change.

Copy link
Collaborator

@evanpelle evanpelle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

@evanpelle evanpelle added this to the v30 milestone Feb 9, 2026
@evanpelle evanpelle merged commit c6c793f into openfrontio:main Feb 9, 2026
6 of 7 checks passed
@github-project-automation github-project-automation bot moved this from Development to Complete in OpenFront Release Management Feb 9, 2026
github-merge-queue bot pushed a commit that referenced this pull request Feb 18, 2026
## 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

UI/UX UI/UX changes including assets, menus, QoL, etc.

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

3 participants

Comments