From 7708d7cc6bbe960e9e190842497210994b28f43b Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 11 Jul 2025 21:19:53 +0200 Subject: [PATCH 1/3] Factories spawn trains --- resources/lang/en.json | 7 ++- src/client/styles.css | 5 ++ src/core/configuration/DefaultConfig.ts | 2 +- src/core/execution/FactoryExecution.ts | 6 +-- src/core/execution/PortExecution.ts | 15 ++++++ src/core/execution/TrainStationExecution.ts | 58 +++++++++++++++------ src/core/game/TrainStation.ts | 13 ++--- 7 files changed, 75 insertions(+), 31 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index debc8de8e1..d02ef470ab 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -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" }, diff --git a/src/client/styles.css b/src/client/styles.css index 26fa476581..8ad4560663 100644 --- a/src/client/styles.css +++ b/src/client/styles.css @@ -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; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 6705282ef0..5582808807 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -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); diff --git a/src/core/execution/FactoryExecution.ts b/src/core/execution/FactoryExecution.ts index cbd1d16672..fd24de674d 100644 --- a/src/core/execution/FactoryExecution.ts +++ b/src/core/execution/FactoryExecution.ts @@ -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)); } } } diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index a7d0705627..5e8a876903 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -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; @@ -84,4 +85,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)); + } + } + } } diff --git a/src/core/execution/TrainStationExecution.ts b/src/core/execution/TrainStationExecution.ts index fc8563da12..7a0af8f15c 100644 --- a/src/core/execution/TrainStationExecution.ts +++ b/src/core/execution/TrainStationExecution.ts @@ -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; + 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; } } diff --git a/src/core/game/TrainStation.ts b/src/core/game/TrainStation.ts index 027ddd2763..a657d24381 100644 --- a/src/core/game/TrainStation.ts +++ b/src/core/game/TrainStation.ts @@ -45,20 +45,15 @@ 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, - }); + const level = BigInt(station.unit.level() + 1); + const goldBonus = (mg.config().trainGold() * level) / this.factor; + station.unit.owner().addGold(goldBonus, station.tile()); } } From 2e866d3bf5d8d6c924ea2cb26621d70862fd2390 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 11 Jul 2025 22:51:05 +0200 Subject: [PATCH 2/3] Add missing function call --- src/core/execution/PortExecution.ts | 1 + src/core/execution/TrainStationExecution.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 5e8a876903..4f2bdce38d 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -37,6 +37,7 @@ export class PortExecution implements Execution { return; } this.port = this.player.buildUnit(UnitType.Port, spawn, {}); + this.createStation(); } if (!this.port.isActive()) { diff --git a/src/core/execution/TrainStationExecution.ts b/src/core/execution/TrainStationExecution.ts index 7a0af8f15c..ee0174bcc9 100644 --- a/src/core/execution/TrainStationExecution.ts +++ b/src/core/execution/TrainStationExecution.ts @@ -9,7 +9,7 @@ export class TrainStationExecution implements Execution { private random: PseudoRandom; private station: TrainStation | null = null; private numCars: number = 5; - private lastSpawnTick: number; + private lastSpawnTick: number = 0; private ticksCooldown: number = 10; // Minimum cooldown between two trains constructor( private unit: Unit, From 8ce8d8aa460734a7ce642c2cb8d2f5c273719efa Mon Sep 17 00:00:00 2001 From: Tom Rouillard Date: Sun, 13 Jul 2025 23:22:21 +0200 Subject: [PATCH 3/3] Revert factory gold bonus --- src/core/game/TrainStation.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/game/TrainStation.ts b/src/core/game/TrainStation.ts index a657d24381..b0149e598b 100644 --- a/src/core/game/TrainStation.ts +++ b/src/core/game/TrainStation.ts @@ -51,9 +51,7 @@ class FactoryStopHandler implements TrainStopHandler { station: TrainStation, trainExecution: TrainExecution, ): void { - const level = BigInt(station.unit.level() + 1); - const goldBonus = (mg.config().trainGold() * level) / this.factor; - station.unit.owner().addGold(goldBonus, station.tile()); + station.unit.owner().addGold(mg.config().trainGold(), station.tile()); } }