mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 06:10:42 +00:00
43397779fa
## 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>
282 lines
8.1 KiB
TypeScript
282 lines
8.1 KiB
TypeScript
import {
|
|
Execution,
|
|
Game,
|
|
isUnit,
|
|
OwnerComp,
|
|
Unit,
|
|
UnitParams,
|
|
UnitType,
|
|
} from "../game/Game";
|
|
import { TileRef } from "../game/GameMap";
|
|
import { PathFindResultType } from "../pathfinding/AStar";
|
|
import { PathFinder } from "../pathfinding/PathFinding";
|
|
import { PseudoRandom } from "../PseudoRandom";
|
|
import { ShellExecution } from "./ShellExecution";
|
|
|
|
export class WarshipExecution implements Execution {
|
|
private random: PseudoRandom;
|
|
private warship: Unit;
|
|
private mg: Game;
|
|
private pathfinder: PathFinder;
|
|
private lastShellAttack = 0;
|
|
private alreadySentShell = new Set<Unit>();
|
|
|
|
constructor(
|
|
private input: (UnitParams<UnitType.Warship> & OwnerComp) | Unit,
|
|
) {}
|
|
|
|
init(mg: Game, ticks: number): void {
|
|
this.mg = mg;
|
|
this.pathfinder = PathFinder.Mini(mg, 5000);
|
|
this.random = new PseudoRandom(mg.ticks());
|
|
if (isUnit(this.input)) {
|
|
this.warship = this.input;
|
|
} else {
|
|
const spawn = this.input.owner.canBuild(
|
|
UnitType.Warship,
|
|
this.input.patrolTile,
|
|
);
|
|
if (spawn === false) {
|
|
console.warn(
|
|
`Failed to spawn warship for ${this.input.owner.name()} at ${this.input.patrolTile}`,
|
|
);
|
|
return;
|
|
}
|
|
this.warship = this.input.owner.buildUnit(
|
|
UnitType.Warship,
|
|
spawn,
|
|
this.input,
|
|
);
|
|
}
|
|
}
|
|
|
|
tick(ticks: number): void {
|
|
if (this.warship.health() <= 0) {
|
|
this.warship.delete();
|
|
return;
|
|
}
|
|
const hasPort = this.warship.owner().units(UnitType.Port).length > 0;
|
|
if (hasPort) {
|
|
this.warship.modifyHealth(1);
|
|
}
|
|
|
|
this.warship.setTargetUnit(this.findTargetUnit());
|
|
if (this.warship.targetUnit()?.type() === UnitType.TradeShip) {
|
|
this.huntDownTradeShip();
|
|
return;
|
|
}
|
|
|
|
this.patrol();
|
|
|
|
if (this.warship.targetUnit() !== undefined) {
|
|
this.shootTarget();
|
|
return;
|
|
}
|
|
}
|
|
|
|
private findTargetUnit(): Unit | undefined {
|
|
const hasPort = this.warship.owner().units(UnitType.Port).length > 0;
|
|
const patrolRangeSquared = this.mg.config().warshipPatrolRange() ** 2;
|
|
|
|
const ships = this.mg.nearbyUnits(
|
|
this.warship.tile()!,
|
|
this.mg.config().warshipTargettingRange(),
|
|
[UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip],
|
|
);
|
|
const potentialTargets: { unit: Unit; distSquared: number }[] = [];
|
|
for (const { unit, distSquared } of ships) {
|
|
if (
|
|
unit.owner() === this.warship.owner() ||
|
|
unit === this.warship ||
|
|
unit.owner().isFriendly(this.warship.owner()) ||
|
|
this.alreadySentShell.has(unit)
|
|
) {
|
|
continue;
|
|
}
|
|
if (unit.type() === UnitType.TradeShip) {
|
|
if (
|
|
!hasPort ||
|
|
unit.isSafeFromPirates() ||
|
|
unit.targetUnit()?.owner() === this.warship.owner() || // trade ship is coming to my port
|
|
unit.targetUnit()?.owner().isFriendly(this.warship.owner()) // trade ship is coming to my ally
|
|
) {
|
|
continue;
|
|
}
|
|
if (
|
|
this.mg.euclideanDistSquared(
|
|
this.warship.patrolTile()!,
|
|
unit.tile(),
|
|
) > patrolRangeSquared
|
|
) {
|
|
// Prevent warship from chasing trade ship that is too far away from
|
|
// the patrol tile to prevent warships from wandering around the map.
|
|
continue;
|
|
}
|
|
}
|
|
potentialTargets.push({ unit: unit, distSquared });
|
|
}
|
|
|
|
return potentialTargets.sort((a, b) => {
|
|
const { unit: unitA, distSquared: distA } = a;
|
|
const { unit: unitB, distSquared: distB } = b;
|
|
|
|
// Prioritize Transport Ships above all other units
|
|
if (
|
|
unitA.type() === UnitType.TransportShip &&
|
|
unitB.type() !== UnitType.TransportShip
|
|
)
|
|
return -1;
|
|
if (
|
|
unitA.type() !== UnitType.TransportShip &&
|
|
unitB.type() === UnitType.TransportShip
|
|
)
|
|
return 1;
|
|
|
|
// Then prioritize Warships.
|
|
if (
|
|
unitA.type() === UnitType.Warship &&
|
|
unitB.type() !== UnitType.Warship
|
|
)
|
|
return -1;
|
|
if (
|
|
unitA.type() !== UnitType.Warship &&
|
|
unitB.type() === UnitType.Warship
|
|
)
|
|
return 1;
|
|
|
|
// If both are the same type, sort by distance (lower `distSquared` means closer)
|
|
return distA - distB;
|
|
})[0]?.unit;
|
|
}
|
|
|
|
private shootTarget() {
|
|
const shellAttackRate = this.mg.config().warshipShellAttackRate();
|
|
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
|
|
if (this.warship.targetUnit()?.type() !== UnitType.TransportShip) {
|
|
// Warships don't need to reload when attacking transport ships.
|
|
this.lastShellAttack = this.mg.ticks();
|
|
}
|
|
this.mg.addExecution(
|
|
new ShellExecution(
|
|
this.warship.tile(),
|
|
this.warship.owner(),
|
|
this.warship,
|
|
this.warship.targetUnit()!,
|
|
),
|
|
);
|
|
if (!this.warship.targetUnit()!.hasHealth()) {
|
|
// Don't send multiple shells to target that can be oneshotted
|
|
this.alreadySentShell.add(this.warship.targetUnit()!);
|
|
this.warship.setTargetUnit(undefined);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private huntDownTradeShip() {
|
|
for (let i = 0; i < 2; i++) {
|
|
// target is trade ship so capture it.
|
|
const result = this.pathfinder.nextTile(
|
|
this.warship.tile(),
|
|
this.warship.targetUnit()!.tile(),
|
|
5,
|
|
);
|
|
switch (result.type) {
|
|
case PathFindResultType.Completed:
|
|
this.warship.owner().captureUnit(this.warship.targetUnit()!);
|
|
this.warship.setTargetUnit(undefined);
|
|
this.warship.move(this.warship.tile());
|
|
return;
|
|
case PathFindResultType.NextTile:
|
|
this.warship.move(result.node);
|
|
break;
|
|
case PathFindResultType.Pending:
|
|
this.warship.touch();
|
|
break;
|
|
case PathFindResultType.PathNotFound:
|
|
console.log(`path not found to target`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private patrol() {
|
|
if (this.warship.targetTile() === undefined) {
|
|
this.warship.setTargetTile(this.randomTile());
|
|
if (this.warship.targetTile() === undefined) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const result = this.pathfinder.nextTile(
|
|
this.warship.tile(),
|
|
this.warship.targetTile()!,
|
|
);
|
|
switch (result.type) {
|
|
case PathFindResultType.Completed:
|
|
this.warship.setTargetTile(undefined);
|
|
this.warship.move(result.node);
|
|
break;
|
|
case PathFindResultType.NextTile:
|
|
this.warship.move(result.node);
|
|
break;
|
|
case PathFindResultType.Pending:
|
|
this.warship.touch();
|
|
return;
|
|
case PathFindResultType.PathNotFound:
|
|
console.warn(`path not found to target tile`);
|
|
this.warship.setTargetTile(undefined);
|
|
break;
|
|
}
|
|
}
|
|
|
|
isActive(): boolean {
|
|
return this.warship?.isActive();
|
|
}
|
|
|
|
activeDuringSpawnPhase(): boolean {
|
|
return false;
|
|
}
|
|
|
|
randomTile(allowShoreline: boolean = false): TileRef | undefined {
|
|
let warshipPatrolRange = this.mg.config().warshipPatrolRange();
|
|
const maxAttemptBeforeExpand: number = 500;
|
|
let attempts: number = 0;
|
|
let expandCount: number = 0;
|
|
while (expandCount < 3) {
|
|
const x =
|
|
this.mg.x(this.warship.patrolTile()!) +
|
|
this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2);
|
|
const y =
|
|
this.mg.y(this.warship.patrolTile()!) +
|
|
this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2);
|
|
if (!this.mg.isValidCoord(x, y)) {
|
|
continue;
|
|
}
|
|
const tile = this.mg.ref(x, y);
|
|
if (
|
|
!this.mg.isOcean(tile) ||
|
|
(!allowShoreline && this.mg.isShoreline(tile))
|
|
) {
|
|
attempts++;
|
|
if (attempts === maxAttemptBeforeExpand) {
|
|
expandCount++;
|
|
attempts = 0;
|
|
warshipPatrolRange =
|
|
warshipPatrolRange + Math.floor(warshipPatrolRange / 2);
|
|
}
|
|
continue;
|
|
}
|
|
return tile;
|
|
}
|
|
console.warn(
|
|
`Failed to find random tile for warship for ${this.warship.owner().name()}`,
|
|
);
|
|
if (!allowShoreline) {
|
|
// If we failed to find a tile on the ocean, try again but allow shoreline
|
|
return this.randomTile(true);
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|