Skip to content
6 changes: 4 additions & 2 deletions src/client/graphics/GameRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ export function createRenderer(
const transformHandler = new TransformHandler(game, eventBus, canvas);
const userSettings = new UserSettings();

const uiState = {
const uiState: UIState = {
attackRatio: 20,
ghostStructure: null,
overlappingRailroads: [],
ghostRailPaths: [],
rocketDirectionUp: true,
} as UIState;
};

//hide when the game renders
const startingModal = document.querySelector(
Expand Down
2 changes: 2 additions & 0 deletions src/client/graphics/UIState.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { UnitType } from "../../core/game/Game";
import { TileRef } from "../../core/game/GameMap";

export interface UIState {
attackRatio: number;
ghostStructure: UnitType | null;
overlappingRailroads: number[];
ghostRailPaths: TileRef[][];
rocketDirectionUp: boolean;
}
80 changes: 65 additions & 15 deletions src/client/graphics/layers/RailroadLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,6 @@ export class RailroadLayer implements Layer {
if (scale <= 1) {
return;
}
if (this.existingRailroads.size === 0) {
return;
}
this.updateRailColors();
const rawAlpha = (scale - 1) / (2 - 1); // maps 1->0, 2->1
const alpha = Math.max(0, Math.min(1, rawAlpha));
Expand All @@ -228,21 +225,74 @@ export class RailroadLayer implements Layer {

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

this.renderGhostRailroads(context);

if (this.existingRailroads.size > 0) {
this.highlightOverlappingRailroads(context);

context.drawImage(
this.canvas,
srcX,
srcY,
srcW,
srcH,
dstX,
dstY,
visWidth,
visHeight,
);
}

context.restore();
}

private renderGhostRailroads(context: CanvasRenderingContext2D) {
if (
this.uiState.ghostStructure !== UnitType.City &&
this.uiState.ghostStructure !== UnitType.Port
)
return;
if (this.uiState.ghostRailPaths.length === 0) return;

const offsetX = -this.game.width() / 2;
const offsetY = -this.game.height() / 2;
context.fillStyle = "rgba(0, 0, 0, 0.4)";

for (const path of this.uiState.ghostRailPaths) {
const railTiles = computeRailTiles(this.game, path);
for (const railTile of railTiles) {
const x = this.game.x(railTile.tile);
const y = this.game.y(railTile.tile);

if (this.game.isWater(railTile.tile)) {
context.save();
context.fillStyle = "rgba(197, 69, 72, 0.4)";
const bridgeRects = getBridgeRects(railTile.type);
for (const [dx, dy, w, h] of bridgeRects) {
context.fillRect(
x + offsetX + dx / 2,
y + offsetY + dy / 2,
w / 2,
h / 2,
);
}
context.restore();
}

const railRects = getRailroadRects(railTile.type);
for (const [dx, dy, w, h] of railRects) {
context.fillRect(
x + offsetX + dx / 2,
y + offsetY + dy / 2,
w / 2,
h / 2,
);
}
}
}
}

private onRailroadSnapEvent(update: RailroadSnapUpdate) {
const original = this.railroads.get(update.originalId);
if (!original) {
Expand Down
5 changes: 5 additions & 0 deletions src/client/graphics/layers/StructureIconsLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,13 +335,16 @@ export class StructureIconsLayer implements Layer {
}
// No overlapping when a structure is upgradable
this.uiState.overlappingRailroads = [];
this.uiState.ghostRailPaths = [];
} else if (unit.canBuild === false) {
this.ghostUnit.container.filters = [
new OutlineFilter({ thickness: 2, color: "rgba(255, 0, 0, 1)" }),
];
this.uiState.overlappingRailroads = [];
this.uiState.ghostRailPaths = [];
} else {
this.uiState.overlappingRailroads = unit.overlappingRailroads;
this.uiState.ghostRailPaths = unit.ghostRailPaths;
}

const scale = this.transformHandler.scale;
Expand Down Expand Up @@ -461,6 +464,7 @@ export class StructureIconsLayer implements Layer {
canUpgrade: false,
cost: 0n,
overlappingRailroads: [],
ghostRailPaths: [],
},
};
const showPrice = this.game.config().userSettings().cursorCostLabel();
Expand All @@ -480,6 +484,7 @@ export class StructureIconsLayer implements Layer {
this.potentialUpgrade.dotContainer.filters = [];
this.potentialUpgrade = undefined;
}
this.uiState.ghostRailPaths = [];
}

private removeGhostStructure() {
Expand Down
1 change: 1 addition & 0 deletions src/core/game/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,7 @@ export interface BuildableUnit {
type: UnitType;
cost: Gold;
overlappingRailroads: number[];
ghostRailPaths: TileRef[][];
}

export interface PlayerProfile {
Expand Down
4 changes: 4 additions & 0 deletions src/core/game/PlayerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,10 @@ export class PlayerImpl implements Player {
canBuild !== false
? this.mg.railNetwork().overlappingRailroads(canBuild)
: [],
ghostRailPaths:
canBuild !== false
? this.mg.railNetwork().computeGhostRailPaths(u, canBuild)
: [],
} as BuildableUnit;
});
}
Expand Down
3 changes: 2 additions & 1 deletion src/core/game/RailNetwork.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Unit } from "./Game";
import { Unit, UnitType } from "./Game";
import { TileRef } from "./GameMap";
import { StationManager } from "./RailNetworkImpl";
import { TrainStation } from "./TrainStation";
Expand All @@ -9,5 +9,6 @@ export interface RailNetwork {
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
stationManager(): StationManager;
overlappingRailroads(tile: TileRef): number[];
computeGhostRailPaths(unitType: UnitType, tile: TileRef): TileRef[][];
recomputeClusters(): void;
}
61 changes: 61 additions & 0 deletions src/core/game/RailNetworkImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,67 @@ export class RailNetworkImpl implements RailNetwork {
);
}

private canSnapToExistingRailway(tile: TileRef): boolean {
return this.railGrid.query(tile, this.stationRadius).size > 0;
}

computeGhostRailPaths(unitType: UnitType, tile: TileRef): TileRef[][] {
// Factories already show their radius, so we'll exclude from ghost rails
// in order not to clutter the interface too much.
if (![UnitType.City, UnitType.Port].includes(unitType)) {
return [];
}

if (this.canSnapToExistingRailway(tile)) {
return [];
}

const maxRange = this.game.config().trainStationMaxRange();
const minRangeSquared = this.game.config().trainStationMinRange() ** 2;
const maxPathSize = this.game.config().railroadMaxSize();

// Cannot connect if outside the max range of a factory
if (!this.game.hasUnitNearby(tile, maxRange, UnitType.Factory)) {
return [];
}

const neighbors = this.game.nearbyUnits(tile, maxRange, [
UnitType.City,
UnitType.Factory,
UnitType.Port,
]);
neighbors.sort((a, b) => a.distSquared - b.distSquared);

const paths: TileRef[][] = [];
const connectedStations: TrainStation[] = [];
for (const neighbor of neighbors) {
// Limit to the closest 5 stations to avoid running too many pathfinding calls.
if (paths.length >= 5) break;
if (neighbor.distSquared <= minRangeSquared) continue;

const neighborStation = this._stationManager.findStation(neighbor.unit);
if (!neighborStation) continue;

const alreadyReachable = connectedStations.some(
(s) =>
this.distanceFrom(
neighborStation,
s,
this.maxConnectionDistance - 1,
) !== -1,
);
if (alreadyReachable) continue;

const path = this.pathService.findTilePath(tile, neighborStation.tile());
if (path.length > 0 && path.length < maxPathSize) {
paths.push(path);
connectedStations.push(neighborStation);
}
}

return paths;
}

private connectToNearbyStations(station: TrainStation) {
const neighbors = this.game.nearbyUnits(
station.tile(),
Expand Down
1 change: 1 addition & 0 deletions tests/InputHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe("InputHandler AutoUpgrade", () => {
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
},
mockCanvas,
eventBus,
Expand Down
Loading
Loading