mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 16:50:54 +00:00
6353a5d6f7
## Description:
Train stations are now built automatically when a factory is
constructed.
Changes:
- When a factory is built, nearby structures are connected to the rail
network
- When a city is built near a factory, it is connected to the rail
network
- All structures behave the same when a train stops: to be defined
- Removed station badge
- Gold income is now related to the structure's level
## 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
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors
## Please put your Discord username so you can be contacted if a bug or
regression is found:
IngloriousTom
265 lines
7.6 KiB
TypeScript
265 lines
7.6 KiB
TypeScript
import { RailroadExecution } from "../execution/RailroadExecution";
|
|
import { PathFindResultType } from "../pathfinding/AStar";
|
|
import { MiniAStar } from "../pathfinding/MiniAStar";
|
|
import { SerialAStar } from "../pathfinding/SerialAStar";
|
|
import { Game, Unit, UnitType } from "./Game";
|
|
import { TileRef } from "./GameMap";
|
|
import { RailNetwork } from "./RailNetwork";
|
|
import { Railroad } from "./Railroad";
|
|
import { Cluster, TrainStation, TrainStationMapAdapter } from "./TrainStation";
|
|
|
|
/**
|
|
* The Stations handle their own neighbors so the graph is naturally traversable,
|
|
* but it would be expensive to look through the graph to find a station.
|
|
* This class stores the existing stations for quick access
|
|
*/
|
|
export interface StationManager {
|
|
addStation(station: TrainStation): void;
|
|
removeStation(station: TrainStation): void;
|
|
findStation(unit: Unit): TrainStation | null;
|
|
getAll(): Set<TrainStation>;
|
|
}
|
|
|
|
export class StationManagerImpl implements StationManager {
|
|
private stations: Set<TrainStation> = new Set();
|
|
|
|
addStation(station: TrainStation) {
|
|
this.stations.add(station);
|
|
}
|
|
|
|
removeStation(station: TrainStation) {
|
|
this.stations.delete(station);
|
|
}
|
|
|
|
findStation(unit: Unit): TrainStation | null {
|
|
for (const station of this.stations) {
|
|
if (station.unit === unit) return station;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getAll(): Set<TrainStation> {
|
|
return this.stations;
|
|
}
|
|
}
|
|
|
|
export interface RailPathFinderService {
|
|
findTilePath(from: TileRef, to: TileRef): TileRef[];
|
|
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
|
|
}
|
|
|
|
class RailPathFinderServiceImpl implements RailPathFinderService {
|
|
constructor(private game: Game) {}
|
|
|
|
findTilePath(from: TileRef, to: TileRef): TileRef[] {
|
|
const astar = new MiniAStar(
|
|
this.game.map(),
|
|
this.game.miniMap(),
|
|
from,
|
|
to,
|
|
5000,
|
|
20,
|
|
false,
|
|
3,
|
|
);
|
|
return astar.compute() === PathFindResultType.Completed
|
|
? astar.reconstructPath()
|
|
: [];
|
|
}
|
|
|
|
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[] {
|
|
const stationAStar = new SerialAStar(
|
|
from,
|
|
to,
|
|
5000,
|
|
20,
|
|
new TrainStationMapAdapter(this.game),
|
|
);
|
|
return stationAStar.compute() === PathFindResultType.Completed
|
|
? stationAStar.reconstructPath()
|
|
: [];
|
|
}
|
|
}
|
|
|
|
export function createRailNetwork(game: Game): RailNetwork {
|
|
const stationManager = new StationManagerImpl();
|
|
const pathService = new RailPathFinderServiceImpl(game);
|
|
return new RailNetworkImpl(game, stationManager, pathService);
|
|
}
|
|
|
|
export class RailNetworkImpl implements RailNetwork {
|
|
private maxConnectionDistance: number = 4;
|
|
|
|
constructor(
|
|
private game: Game,
|
|
private stationManager: StationManager,
|
|
private pathService: RailPathFinderService,
|
|
) {}
|
|
|
|
connectStation(station: TrainStation) {
|
|
this.stationManager.addStation(station);
|
|
this.connectToNearbyStations(station);
|
|
}
|
|
|
|
removeStation(unit: Unit): void {
|
|
const station = this.stationManager.findStation(unit);
|
|
if (!station) return;
|
|
|
|
const neighbors = station.neighbors();
|
|
this.disconnectFromNetwork(station);
|
|
this.stationManager.removeStation(station);
|
|
|
|
const cluster = station.getCluster();
|
|
if (!cluster) return;
|
|
if (neighbors.length === 1) {
|
|
cluster.removeStation(station);
|
|
} else if (neighbors.length > 1) {
|
|
for (const neighbor of neighbors) {
|
|
const stations = this.computeCluster(neighbor);
|
|
const newCluster = new Cluster();
|
|
newCluster.addStations(stations);
|
|
}
|
|
}
|
|
station.unit.setTrainStation(false);
|
|
}
|
|
|
|
/**
|
|
* Return the intermediary stations connecting two stations
|
|
*/
|
|
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[] {
|
|
return this.pathService.findStationsPath(from, to);
|
|
}
|
|
|
|
private connectToNearbyStations(station: TrainStation) {
|
|
const neighbors = this.game.nearbyUnits(
|
|
station.tile(),
|
|
this.game.config().trainStationMaxRange(),
|
|
[UnitType.City, UnitType.Factory, UnitType.Port],
|
|
);
|
|
|
|
const editedClusters = new Set<Cluster>();
|
|
neighbors.sort((a, b) => a.distSquared - b.distSquared);
|
|
|
|
for (const neighbor of neighbors) {
|
|
if (neighbor.unit === station.unit) continue;
|
|
const neighborStation = this.stationManager.findStation(neighbor.unit);
|
|
if (!neighborStation) continue;
|
|
|
|
const distanceToStation = this.distanceFrom(
|
|
neighborStation,
|
|
station,
|
|
this.maxConnectionDistance,
|
|
);
|
|
|
|
const neighborCluster = neighborStation.getCluster();
|
|
if (neighborCluster === null) continue;
|
|
const connectionAvailable =
|
|
distanceToStation > this.maxConnectionDistance ||
|
|
distanceToStation === -1;
|
|
if (
|
|
connectionAvailable &&
|
|
neighbor.distSquared > this.game.config().trainStationMinRange() ** 2
|
|
) {
|
|
if (this.connect(station, neighborStation)) {
|
|
neighborCluster.addStation(station);
|
|
editedClusters.add(neighborCluster);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If multiple clusters own the new station, merge them into a single cluster
|
|
if (editedClusters.size > 1) {
|
|
this.mergeClusters(editedClusters);
|
|
} else if (editedClusters.size === 0) {
|
|
// If no cluster owns the station, creates a new one for it
|
|
const newCluster = new Cluster();
|
|
newCluster.addStation(station);
|
|
}
|
|
}
|
|
|
|
private disconnectFromNetwork(station: TrainStation) {
|
|
for (const rail of station.getRailroads()) {
|
|
rail.delete(this.game);
|
|
}
|
|
station.clearRailroads();
|
|
const cluster = station.getCluster();
|
|
if (cluster !== null && cluster.size() === 1) {
|
|
this.deleteCluster(cluster);
|
|
}
|
|
}
|
|
|
|
private deleteCluster(cluster: Cluster) {
|
|
for (const station of cluster.stations) {
|
|
station.setCluster(null);
|
|
}
|
|
cluster.clear();
|
|
}
|
|
|
|
private connect(from: TrainStation, to: TrainStation) {
|
|
const path = this.pathService.findTilePath(from.tile(), to.tile());
|
|
if (path.length > 0 && path.length < this.game.config().railroadMaxSize()) {
|
|
const railRoad = new Railroad(from, to, path);
|
|
this.game.addExecution(new RailroadExecution(railRoad));
|
|
from.addRailroad(railRoad);
|
|
to.addRailroad(railRoad);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private distanceFrom(
|
|
start: TrainStation,
|
|
dest: TrainStation,
|
|
maxDistance: number,
|
|
): number {
|
|
if (start === dest) return 0;
|
|
|
|
const visited = new Set<TrainStation>();
|
|
const queue: Array<{ station: TrainStation; distance: number }> = [
|
|
{ station: start, distance: 0 },
|
|
];
|
|
|
|
while (queue.length > 0) {
|
|
const { station, distance } = queue.shift()!;
|
|
if (visited.has(station)) continue;
|
|
visited.add(station);
|
|
|
|
if (distance >= maxDistance) continue;
|
|
|
|
for (const neighbor of station.neighbors()) {
|
|
if (neighbor === dest) return distance + 1;
|
|
if (!visited.has(neighbor)) {
|
|
queue.push({ station: neighbor, distance: distance + 1 });
|
|
}
|
|
}
|
|
}
|
|
|
|
// If destination not found within maxDistance
|
|
return -1;
|
|
}
|
|
|
|
private computeCluster(start: TrainStation): Set<TrainStation> {
|
|
const visited = new Set<TrainStation>();
|
|
const queue = [start];
|
|
|
|
while (queue.length > 0) {
|
|
const current = queue.shift()!;
|
|
if (visited.has(current)) continue;
|
|
visited.add(current);
|
|
|
|
for (const neighbor of current.neighbors()) {
|
|
if (!visited.has(neighbor)) queue.push(neighbor);
|
|
}
|
|
}
|
|
|
|
return visited;
|
|
}
|
|
|
|
private mergeClusters(clustersToMerge: Set<Cluster>) {
|
|
const merged = new Cluster();
|
|
for (const cluster of clustersToMerge) {
|
|
merged.merge(cluster);
|
|
}
|
|
}
|
|
}
|