Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/client/UIState.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { PlayerBuildableUnitType } from "../core/game/Game";
import { TileRef } from "../core/game/GameMap";

export interface UIState {
attackRatio: number;
ghostStructure: PlayerBuildableUnitType | null;
overlappingRailroads: number[];
ghostRailPaths: TileRef[][];
rocketDirectionUp: boolean;
}
34 changes: 16 additions & 18 deletions src/client/controllers/BuildPreviewController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,6 @@ export class BuildPreviewController implements Controller {

this.ghostUnit.buildableUnit = unit;

if (unit.canUpgrade || unit.canBuild === false) {
// No rail-snap overlap for upgrades or invalid placements.
this.uiState.overlappingRailroads = [];
this.uiState.ghostRailPaths = [];
} else {
this.uiState.overlappingRailroads = unit.overlappingRailroads;
this.uiState.ghostRailPaths = unit.ghostRailPaths;
}

if (this.pendingConfirm !== null) {
const ev = this.pendingConfirm;
this.pendingConfirm = null;
Expand Down Expand Up @@ -326,14 +317,22 @@ export class BuildPreviewController implements Controller {
// Range circle: SAM placement preview shows targetable radius; nuke
// previews show the outer blast radius at the target tile.
let rangeRadius = 0;
if (u.type === UnitType.SAMLauncher) {
const level = this.resolveGhostRangeLevel(u) ?? 1;
rangeRadius = this.game.config().samRange(level);
} else if (
u.type === UnitType.AtomBomb ||
u.type === UnitType.HydrogenBomb
) {
rangeRadius = this.game.config().nukeMagnitudes(u.type).outer;
switch (u.type) {
case UnitType.SAMLauncher: {
const level = this.resolveGhostRangeLevel(u) ?? 1;
rangeRadius = this.game.config().samRange(level);
break;
}
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
rangeRadius = this.game.config().nukeMagnitudes(u.type).outer;
break;
case UnitType.Factory:
rangeRadius = this.game.config().trainStationMaxRange();
break;
case UnitType.DefensePost:
rangeRadius = this.game.config().defensePostRange();
break;
}

return {
Expand Down Expand Up @@ -428,7 +427,6 @@ export class BuildPreviewController implements Controller {
private clearGhostStructure() {
this.pendingConfirm = null;
this.ghostUnit = null;
this.uiState.ghostRailPaths = [];
this.lastGhostData = null;
this.view.updateGhostPreview(null);
this.view.updateNukeTrajectory(null);
Expand Down
2 changes: 0 additions & 2 deletions src/client/hud/GameRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ export function createRenderer(
const uiState: UIState = {
attackRatio: 20,
ghostStructure: null,
overlappingRailroads: [],
ghostRailPaths: [],
rocketDirectionUp: true,
};

Expand Down
6 changes: 4 additions & 2 deletions src/client/render/types/Renderer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { TileRef } from "../../../core/game/GameMap";

/** TrainType enum — numeric values matching UnitState.trainType. */
export enum TrainType {
Engine = 0,
Expand Down Expand Up @@ -152,8 +154,8 @@ export interface GhostPreviewData {
canBuild: boolean; // Valid placement?
canUpgrade: boolean; // Upgrading existing structure?
cost: number; // Gold cost
ghostRailPaths: number[][]; // TileRef paths (City/Port only)
overlappingRailroads: number[]; // Rail IDs in snap zone
ghostRailPaths: TileRef[][]; // TileRef paths (City/Port only)
overlappingRailroads: TileRef[]; // TileRefs containing rails in snap zone
ownerID: number; // Player's smallID (for color)
/** Tile position of existing structure being upgraded (null if fresh build). */
upgradeTargetTile: number | null;
Expand Down
2 changes: 1 addition & 1 deletion src/core/game/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,7 @@ export interface BuildableUnit {
canUpgrade: number | false;
type: PlayerBuildableUnitType;
cost: Gold;
overlappingRailroads: number[];
overlappingRailroads: TileRef[];
ghostRailPaths: TileRef[][];
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/game/RailNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface RailNetwork {
removeStation(unit: Unit): void;
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
stationManager(): StationManager;
overlappingRailroads(tile: TileRef): number[];
overlappingRailroads(tile: TileRef): TileRef[];
computeGhostRailPaths(unitType: UnitType, tile: TileRef): TileRef[][];
recomputeClusters(): void;
}
12 changes: 8 additions & 4 deletions src/core/game/RailNetworkImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,14 @@ export class RailNetworkImpl implements RailNetwork {
return editedClusters.size !== 0;
}

overlappingRailroads(tile: TileRef): number[] {
return [...this.railGrid.query(tile, this.stationRadius)].map(
(railroad: Railroad) => railroad.id,
);
overlappingRailroads(tile: TileRef): TileRef[] {
const tiles = new Set<TileRef>();
for (const railroad of this.railGrid.query(tile, this.stationRadius)) {
for (const t of railroad.tiles) {
tiles.add(t);
}
}
return Array.from(tiles).sort((a, b) => a - b);
}

private canSnapToExistingRailway(tile: TileRef): boolean {
Expand Down
18 changes: 0 additions & 18 deletions tests/InputHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ describe("InputHandler AutoUpgrade", () => {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
},
mockCanvas,
eventBus,
Expand Down Expand Up @@ -541,8 +539,6 @@ describe("InputHandler AutoUpgrade", () => {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
Expand Down Expand Up @@ -597,8 +593,6 @@ describe("InputHandler AutoUpgrade", () => {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
Expand Down Expand Up @@ -649,8 +643,6 @@ describe("InputHandler AutoUpgrade", () => {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
Expand All @@ -671,8 +663,6 @@ describe("InputHandler AutoUpgrade", () => {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
Expand All @@ -699,8 +689,6 @@ describe("InputHandler AutoUpgrade", () => {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
Expand All @@ -724,8 +712,6 @@ describe("InputHandler AutoUpgrade", () => {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
Expand All @@ -752,8 +738,6 @@ describe("InputHandler AutoUpgrade", () => {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
});

Expand Down Expand Up @@ -892,8 +876,6 @@ describe("Warship box selection (Shift+drag)", () => {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
Expand Down
27 changes: 27 additions & 0 deletions tests/core/game/RailNetwork.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,33 @@ describe("RailNetworkImpl", () => {
expect(neighborStation.setCluster).toHaveBeenCalled();
});

describe("overlappingRailroads", () => {
test("returns deterministic deduplicated TileRef array", () => {
const tile = 42 as any;
const railGridMock = {
query: vi.fn(
() => new Set([{ tiles: [50, 42, 60] }, { tiles: [60, 45, 42] }]),
),
};
(network as any).railGrid = railGridMock;

const result = network.overlappingRailroads(tile);

expect(railGridMock.query).toHaveBeenCalledWith(tile, 3);
expect(result).toEqual([42, 45, 50, 60]); // Deduplicated and sorted
});

test("returns empty array when no railroads overlap", () => {
const tile = 42 as any;
const railGridMock = { query: vi.fn(() => new Set()) };
(network as any).railGrid = railGridMock;

const result = network.overlappingRailroads(tile);

expect(result).toEqual([]);
});
});

Comment thread
VariableVince marked this conversation as resolved.
describe("computeGhostRailPaths", () => {
test("returns empty when snappable rails exist nearby", () => {
const tile = 42 as any;
Expand Down
Loading