mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-28 13:00:29 +00:00
Add trains (#1159)
## Description: Add a rail network to handle train stations/railroad between structures. Changes: - `RailNetwork` is responsible for the train station graph. Use it to connect new `TrainStations` - A `RailRoad` connects two `TrainStation` - No loop possible in the rail network - Train stations handles its railroads - Added a layer to draw the railroads under the structures #### Clusters - To speed up computations, each `TrainStation` references its own cluster - A cluster is a list of `TrainStation` connected with each other, created by the `RailNetwork` when connecting the station - Train stations spawn trains randomly depending on its current cluster size - A `TrainStation` decides randomly of the train destination by picking one from the cluster #### Production building: - Added a factory which has no gameplay impact currently. _To be discussed._ #### Train stops: - When a train reaches a factory, it's filled with a "cargo". The loaded trains has no impact currently. _To be discussed._ - When a train reaches a city, the player earn 10k gold - When a train reaches a port, it sends a new tradeship if possible - If a destination/source is destroyed, the train & railroad are deleted too https://github.com/user-attachments/assets/42375c17-9e04-4a42-98d0-708c81ffd609 https://github.com/user-attachments/assets/fbecdb53-a516-4df8-87fb-1f9a62c4efa0 ## 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 --------- Co-authored-by: Scott Anderson <scottanderson@users.noreply.github.com>
This commit is contained in:
co-authored by
Scott Anderson
parent
0f2008a68d
commit
43397779fa
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
TrainType,
|
||||
Unit,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { RailNetwork } from "../game/RailNetwork";
|
||||
import { getOrientedRailroad, OrientedRailroad } from "../game/Railroad";
|
||||
import { TrainStation } from "../game/TrainStation";
|
||||
|
||||
export class TrainExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game | null = null;
|
||||
private train: Unit | null = null;
|
||||
private cars: Unit[] = [];
|
||||
private hasCargo: boolean = false;
|
||||
private currentTile: number = 0;
|
||||
private spacing = 2;
|
||||
private usedTiles: TileRef[] = []; // used for cars behind
|
||||
private stations: TrainStation[] = [];
|
||||
private currentRailroad: OrientedRailroad | null = null;
|
||||
private speed: number = 3;
|
||||
|
||||
constructor(
|
||||
private railNetwork: RailNetwork,
|
||||
private player: Player,
|
||||
private source: TrainStation,
|
||||
private destination: TrainStation,
|
||||
private numCars: number,
|
||||
) {
|
||||
this.hasCargo = source.unit.type() === UnitType.Factory;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
const stations = this.railNetwork.findStationsPath(
|
||||
this.source,
|
||||
this.destination,
|
||||
);
|
||||
if (!stations || stations.length <= 1) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.stations = stations;
|
||||
const railroad = getOrientedRailroad(this.stations[0], this.stations[1]);
|
||||
if (railroad) {
|
||||
this.currentRailroad = railroad;
|
||||
} else {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const spawn = this.player.canBuild(UnitType.Train, this.stations[0].tile());
|
||||
if (spawn === false) {
|
||||
console.warn(`cannot build train`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.train = this.createTrainUnits(spawn);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.train === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (!this.train.isActive() || !this.activeSourceOrDestination()) {
|
||||
this.deleteTrain();
|
||||
return;
|
||||
}
|
||||
|
||||
const tile = this.getNextTile();
|
||||
if (tile) {
|
||||
this.updateCarsPositions(tile);
|
||||
} else {
|
||||
this.targetReached();
|
||||
this.deleteTrain();
|
||||
}
|
||||
}
|
||||
|
||||
loadCargo() {
|
||||
if (this.hasCargo || this.train === null) {
|
||||
return;
|
||||
}
|
||||
this.hasCargo = true;
|
||||
// Starts at 1: don't load tail engine
|
||||
for (let i = 1; i < this.cars.length; i++) {
|
||||
this.cars[i].setLoaded(true);
|
||||
}
|
||||
}
|
||||
|
||||
private targetReached() {
|
||||
if (this.train === null) {
|
||||
return;
|
||||
}
|
||||
this.train.setReachedTarget();
|
||||
this.cars.forEach((car: Unit) => {
|
||||
car.setReachedTarget();
|
||||
});
|
||||
}
|
||||
|
||||
private createTrainUnits(tile: TileRef): Unit {
|
||||
const train = this.player.buildUnit(UnitType.Train, tile, {
|
||||
targetUnit: this.destination.unit,
|
||||
trainType: TrainType.Engine,
|
||||
});
|
||||
// Tail is also an engine, just for cosmetics
|
||||
this.cars.push(
|
||||
this.player.buildUnit(UnitType.Train, tile, {
|
||||
targetUnit: this.destination.unit,
|
||||
trainType: TrainType.Engine,
|
||||
}),
|
||||
);
|
||||
for (let i = 0; i < this.numCars; i++) {
|
||||
this.cars.push(
|
||||
this.player.buildUnit(UnitType.Train, tile, {
|
||||
trainType: TrainType.Carriage,
|
||||
loaded: this.hasCargo,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return train;
|
||||
}
|
||||
|
||||
private deleteTrain() {
|
||||
this.active = false;
|
||||
if (this.train?.isActive()) {
|
||||
this.train.delete(false);
|
||||
}
|
||||
for (const car of this.cars) {
|
||||
if (car.isActive()) {
|
||||
car.delete(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private activeSourceOrDestination(): boolean {
|
||||
return (
|
||||
this.stations.length > 1 &&
|
||||
this.stations[1].isActive() &&
|
||||
this.stations[0].isActive()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the tiles the train go through so the cars can reuse them
|
||||
* Don't simply save the tiles the engine uses, otherwise the spacing will be dictated by the train speed
|
||||
*/
|
||||
private saveTraversedTiles(from: number, speed: number) {
|
||||
if (!this.currentRailroad) {
|
||||
return;
|
||||
}
|
||||
let tileToSave: number = from;
|
||||
for (
|
||||
let i = 0;
|
||||
i < speed && tileToSave < this.currentRailroad.getTiles().length;
|
||||
i++
|
||||
) {
|
||||
this.saveTile(this.currentRailroad.getTiles()[tileToSave]);
|
||||
tileToSave = tileToSave + 1;
|
||||
}
|
||||
}
|
||||
|
||||
private saveTile(tile: TileRef) {
|
||||
this.usedTiles.push(tile);
|
||||
if (this.usedTiles.length > this.cars.length * this.spacing + 3) {
|
||||
this.usedTiles.shift();
|
||||
}
|
||||
}
|
||||
|
||||
private updateCarsPositions(newTile: TileRef) {
|
||||
if (this.cars.length > 0) {
|
||||
for (let i = this.cars.length - 1; i >= 0; --i) {
|
||||
const carTileIndex = (i + 1) * this.spacing + 2;
|
||||
if (this.usedTiles.length > carTileIndex) {
|
||||
this.cars[i].move(this.usedTiles[carTileIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.train !== null) {
|
||||
this.train.move(newTile);
|
||||
}
|
||||
}
|
||||
|
||||
private nextStation() {
|
||||
if (this.stations.length > 2) {
|
||||
this.stations.shift();
|
||||
const railRoad = getOrientedRailroad(this.stations[0], this.stations[1]);
|
||||
if (railRoad) {
|
||||
this.currentRailroad = railRoad;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private canTradeWithDestination() {
|
||||
return (
|
||||
this.stations.length > 1 && this.stations[1].tradeAvailable(this.player)
|
||||
);
|
||||
}
|
||||
|
||||
private getNextTile(): TileRef | null {
|
||||
if (this.currentRailroad === null || !this.canTradeWithDestination()) {
|
||||
return null;
|
||||
}
|
||||
this.saveTraversedTiles(this.currentTile, this.speed);
|
||||
this.currentTile = this.currentTile + this.speed;
|
||||
const leftOver = this.currentTile - this.currentRailroad.getTiles().length;
|
||||
if (leftOver >= 0) {
|
||||
// Station reached, pick the next station
|
||||
this.stationReached();
|
||||
if (!this.nextStation()) {
|
||||
return null; // Destination reached (or no valid connection)
|
||||
}
|
||||
this.currentTile = leftOver;
|
||||
this.saveTraversedTiles(0, leftOver);
|
||||
}
|
||||
return this.currentRailroad.getTiles()[this.currentTile];
|
||||
}
|
||||
|
||||
private stationReached() {
|
||||
if (this.mg === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
this.stations[1].onTrainStop(this);
|
||||
return;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user