Factory spawns trains (#1408)

## Description:

- Change trains so it spawns from factories only
- Increase train frequency as they will now spawn from a single
structure.
- Factory will spawn more trains depending on its level
- Fix port to connect to nearby railroads
- Add factory description

## 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: Tom Rouillard <trouilla@mathworks.com>
Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
DevelopingTom
2025-07-14 17:59:51 +02:00
committed by GitHub
parent 1e73293d4f
commit 29b587cdae
7 changed files with 74 additions and 31 deletions
+5 -2
View File
@@ -90,6 +90,8 @@
"build_desc": "Description",
"build_city": "City",
"build_city_desc": "Increases your max population. Useful when you can't expand your territory or you're about to hit your population limit.",
"build_factory": "Factory",
"build_factory_desc": "Creates railroads automatically with nearby structures, and spawns trains sporadically.",
"build_defense": "Defense Post",
"build_defense_desc": "Increases defenses around nearby borders, which show a checkered pattern. Attacks from enemies are slower and have more casualties.",
"build_port": "Port",
@@ -409,8 +411,9 @@
"sam_launcher": "Defends against incoming nukes",
"warship": "Captures trade ships, destroys ships and boats",
"port": "Sends trade ships to generate gold",
"defense_post": "Increase defenses of nearby borders",
"city": "Increase max population"
"defense_post": "Increases defenses of nearby borders",
"city": "Increases max population",
"factory": "Creates railroads and spawns trains"
},
"not_enough_money": "Not enough money"
},
+5
View File
@@ -369,6 +369,11 @@ label.option-card:hover {
mask: url("../../resources/images/CityIconWhite.svg") no-repeat center / cover;
}
#helpModal .factory-icon {
mask: url("../../resources/images/FactoryIconWhite.svg") no-repeat center /
cover;
}
#helpModal .defense-post-icon {
mask: url("../../resources/images/ShieldIconWhite.svg") no-repeat center /
cover;
+1 -1
View File
@@ -326,7 +326,7 @@ export class DefaultConfig implements Config {
return this._gameConfig.infiniteTroops;
}
trainSpawnRate(numberOfStations: number): number {
return Math.min(1400, Math.round(70 * Math.pow(numberOfStations, 0.8)));
return Math.min(1400, Math.round(20 * Math.pow(numberOfStations, 0.5)));
}
trainGold(): Gold {
return BigInt(10_000);
+3 -3
View File
@@ -51,11 +51,11 @@ export class FactoryExecution implements Execution {
this.game.config().trainStationMaxRange(),
[UnitType.City, UnitType.Port, UnitType.Factory],
);
// Use different seeds or trains will spawn simultaneously
let seed = 0;
this.game.addExecution(new TrainStationExecution(this.factory, true));
for (const { unit } of structures) {
if (!unit.hasTrainStation()) {
this.game.addExecution(new TrainStationExecution(unit, ++seed));
this.game.addExecution(new TrainStationExecution(unit));
}
}
}
+16
View File
@@ -2,6 +2,7 @@ import { Execution, Game, Player, Unit, UnitType } from "../game/Game";
import { TileRef } from "../game/GameMap";
import { PseudoRandom } from "../PseudoRandom";
import { TradeShipExecution } from "./TradeShipExecution";
import { TrainStationExecution } from "./TrainStationExecution";
export class PortExecution implements Execution {
private active = true;
@@ -36,6 +37,7 @@ export class PortExecution implements Execution {
return;
}
this.port = this.player.buildUnit(UnitType.Port, spawn, {});
this.createStation();
}
if (!this.port.isActive()) {
@@ -84,4 +86,18 @@ export class PortExecution implements Execution {
}
return false;
}
createStation(): void {
if (this.port !== null) {
const nearbyFactory = this.mg.hasUnitNearby(
this.port.tile()!,
this.mg.config().trainStationMaxRange(),
UnitType.Factory,
this.player.id(),
);
if (nearbyFactory) {
this.mg.addExecution(new TrainStationExecution(this.port));
}
}
}
}
+42 -16
View File
@@ -6,13 +6,17 @@ import { TrainExecution } from "./TrainExecution";
export class TrainStationExecution implements Execution {
private mg: Game;
private active: boolean = true;
private random: PseudoRandom | null = null;
private random: PseudoRandom;
private station: TrainStation | null = null;
private numCars: number = 5;
private lastSpawnTick: number = 0;
private ticksCooldown: number = 10; // Minimum cooldown between two trains
constructor(
private unit: Unit,
private randomSeed?: number,
) {}
private spawnTrains?: boolean, // If set, the station will spawn trains
) {
this.unit.setTrainStation(true);
}
isActive(): boolean {
return this.active;
@@ -20,8 +24,9 @@ export class TrainStationExecution implements Execution {
init(mg: Game, ticks: number): void {
this.mg = mg;
this.random = new PseudoRandom(mg.ticks() + (this.randomSeed ?? 0));
this.unit.setTrainStation(true);
if (this.spawnTrains) {
this.random = new PseudoRandom(mg.ticks());
}
}
tick(ticks: number): void {
@@ -36,36 +41,57 @@ export class TrainStationExecution implements Execution {
this.station = new TrainStation(this.mg, this.unit);
this.mg.railNetwork().connectStation(this.station);
}
if (!this.station.isActive() || !this.random) {
if (!this.station.isActive()) {
this.active = false;
return;
}
const cluster = this.station.getCluster();
this.spawnTrain(this.station, ticks);
}
private shouldSpawnTrain(clusterSize: number): boolean {
const spawnRate = this.mg.config().trainSpawnRate(clusterSize);
for (let i = 0; i < this.unit!.level(); i++) {
if (this.random.chance(spawnRate)) {
return true;
}
}
return false;
}
private spawnTrain(station: TrainStation, currentTick: number) {
if (
!this.spawnTrains ||
currentTick - this.lastSpawnTick < this.ticksCooldown
) {
return;
}
const cluster = station.getCluster();
if (cluster === null) {
return;
}
const availableForTrade = cluster.availableForTrade(this.unit.owner());
if (
availableForTrade.size === 0 ||
!this.random.chance(
this.mg.config().trainSpawnRate(availableForTrade.size),
)
) {
if (availableForTrade.size === 0) {
return;
}
if (!this.shouldSpawnTrain(availableForTrade.size)) {
return;
}
// Pick a destination randomly.
// Could be improved to pick a lucrative trip
const destination = this.random.randFromSet(availableForTrade);
if (destination !== this.station) {
const destination: TrainStation =
this.random.randFromSet(availableForTrade);
if (destination !== station) {
this.mg.addExecution(
new TrainExecution(
this.mg.railNetwork(),
this.unit.owner(),
this.station,
station,
destination,
this.numCars,
),
);
this.lastSpawnTick = currentTick;
}
}
+2 -9
View File
@@ -45,20 +45,13 @@ class PortStopHandler implements TrainStopHandler {
}
class FactoryStopHandler implements TrainStopHandler {
private factor: bigint = BigInt(2);
onStop(
mg: Game,
station: TrainStation,
trainExecution: TrainExecution,
): void {
const goldBonus = mg.config().trainGold();
station.unit.owner().addGold(goldBonus);
mg.addUpdate({
type: GameUpdateType.BonusEvent,
tile: station.tile(),
gold: Number(goldBonus),
workers: 0,
troops: 0,
});
station.unit.owner().addGold(mg.config().trainGold(), station.tile());
}
}