mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-28 16:04:15 +00:00
add station passenger demand and time-based payouts
- Interpret trainGold as per-level max gold and add trainGoldRefillTime with a 60-tick full refill baseline - Add per-station passenger pool with lazy tick-based refill and proportional depletion on train arrival - Make city and port train payouts depend on station level, owner relation, and current passenger demand instead of flat values - Expose getPassengerDemandScore for future logic - Update TrainStation tests for the new config and payout behavior
This commit is contained in:
@@ -144,7 +144,12 @@ export interface Config {
|
||||
numPlayerPorts: number,
|
||||
numPlayerTradeShips: number,
|
||||
): number;
|
||||
// Maximum gold a full train can earn at a level-1 station, per relation.
|
||||
// Actual payout scales with station level and current passenger demand.
|
||||
trainGold(rel: "self" | "team" | "ally" | "other"): Gold;
|
||||
// Number of ticks it takes for an empty station to fully refill its
|
||||
// passenger / demand pool (0 -> 100%).
|
||||
trainGoldRefillTime(): Tick;
|
||||
trainSpawnRate(numPlayerFactories: number): number;
|
||||
trainStationMinRange(): number;
|
||||
trainStationMaxRange(): number;
|
||||
|
||||
@@ -369,6 +369,10 @@ export class DefaultConfig implements Config {
|
||||
return 10_000n;
|
||||
}
|
||||
}
|
||||
trainGoldRefillTime(): Tick {
|
||||
// Baseline: full refill in 60 ticks
|
||||
return 60;
|
||||
}
|
||||
|
||||
trainStationMinRange(): number {
|
||||
return 15;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TrainExecution } from "../execution/TrainExecution";
|
||||
import { GraphAdapter } from "../pathfinding/SerialAStar";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { Game, Player, Unit, UnitType } from "./Game";
|
||||
import { Game, Gold, Player, Unit, UnitType } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
import { GameUpdateType, RailTile, RailType } from "./GameUpdates";
|
||||
import { Railroad } from "./Railroad";
|
||||
@@ -25,12 +25,21 @@ class CityStopHandler implements TrainStopHandler {
|
||||
): void {
|
||||
const stationOwner = station.unit.owner();
|
||||
const trainOwner = trainExecution.owner();
|
||||
const goldBonus = mg.config().trainGold(rel(trainOwner, stationOwner));
|
||||
const relation = rel(trainOwner, stationOwner);
|
||||
const perLevelMax = mg.config().trainGold(relation);
|
||||
const level = station.unit.level();
|
||||
const maxGoldForThisTrain = perLevelMax * BigInt(level);
|
||||
|
||||
const payout = station.consumePassengerPool(maxGoldForThisTrain);
|
||||
if (payout === 0n) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Share revenue with the station owner if it's not the current player
|
||||
if (trainOwner !== stationOwner) {
|
||||
stationOwner.addGold(goldBonus, station.tile());
|
||||
stationOwner.addGold(payout, station.tile());
|
||||
}
|
||||
trainOwner.addGold(goldBonus, station.tile());
|
||||
trainOwner.addGold(payout, station.tile());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +52,21 @@ class PortStopHandler implements TrainStopHandler {
|
||||
): void {
|
||||
const stationOwner = station.unit.owner();
|
||||
const trainOwner = trainExecution.owner();
|
||||
const goldBonus = mg.config().trainGold(rel(trainOwner, stationOwner));
|
||||
const relation = rel(trainOwner, stationOwner);
|
||||
const perLevelMax = mg.config().trainGold(relation);
|
||||
const level = station.unit.level();
|
||||
const maxGoldForThisTrain = perLevelMax * BigInt(level);
|
||||
|
||||
trainOwner.addGold(goldBonus, station.tile());
|
||||
const payout = station.consumePassengerPool(maxGoldForThisTrain);
|
||||
if (payout === 0n) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Train owner always gets the payout
|
||||
trainOwner.addGold(payout, station.tile());
|
||||
// Share revenue with the station owner if it's not the current player
|
||||
if (trainOwner !== stationOwner) {
|
||||
stationOwner.addGold(goldBonus, station.tile());
|
||||
stationOwner.addGold(payout, station.tile());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,11 +97,18 @@ export class TrainStation {
|
||||
// Quick lookup from neighboring station to connecting railroad
|
||||
private railroadByNeighbor: Map<TrainStation, Railroad> = new Map();
|
||||
|
||||
// 0–1 scalar representing how "full" the station is with paying passengers.
|
||||
private passengerFullness: number = 1;
|
||||
// Last tick at which we updated passengerFullness.
|
||||
private lastPassengerUpdateTick: number;
|
||||
|
||||
constructor(
|
||||
private mg: Game,
|
||||
public unit: Unit,
|
||||
) {
|
||||
this.stopHandlers = createTrainStopHandlers(new PseudoRandom(mg.ticks()));
|
||||
this.passengerFullness = 1;
|
||||
this.lastPassengerUpdateTick = mg.ticks();
|
||||
}
|
||||
|
||||
tradeAvailable(otherPlayer: Player): boolean {
|
||||
@@ -162,6 +187,63 @@ export class TrainStation {
|
||||
return this.cluster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily regenerate the passenger pool based on elapsed ticks.
|
||||
*/
|
||||
private updatePassengerPool() {
|
||||
const now = this.mg.ticks();
|
||||
const dt = now - this.lastPassengerUpdateTick;
|
||||
if (dt <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastPassengerUpdateTick = now;
|
||||
|
||||
const refillTime = this.mg.config().trainGoldRefillTime();
|
||||
if (refillTime <= 0) {
|
||||
this.passengerFullness = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
this.passengerFullness = Math.min(
|
||||
1,
|
||||
this.passengerFullness + dt / refillTime,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public view for UI / analytics: how strong is demand right now?
|
||||
*/
|
||||
getPassengerDemandScore(): number {
|
||||
this.updatePassengerPool();
|
||||
return this.passengerFullness * this.unit.level();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert current passenger pool into an actual gold payout, then
|
||||
* deplete the pool proportionally.
|
||||
*/
|
||||
consumePassengerPool(maxGoldForThisTrain: Gold): Gold {
|
||||
this.updatePassengerPool();
|
||||
|
||||
const maxGoldNum = Number(maxGoldForThisTrain);
|
||||
if (maxGoldNum <= 0) {
|
||||
return 0n;
|
||||
}
|
||||
|
||||
const payoutNum = Math.floor(maxGoldNum * this.passengerFullness);
|
||||
const payout = BigInt(payoutNum);
|
||||
|
||||
if (payoutNum > 0) {
|
||||
this.passengerFullness -= payoutNum / maxGoldNum;
|
||||
if (this.passengerFullness < 0) {
|
||||
this.passengerFullness = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return payout;
|
||||
}
|
||||
|
||||
onTrainStop(trainExecution: TrainExecution) {
|
||||
const type = this.unit.type();
|
||||
const handler = this.stopHandlers[type];
|
||||
|
||||
@@ -16,8 +16,8 @@ describe("TrainStation", () => {
|
||||
game = {
|
||||
ticks: jest.fn().mockReturnValue(123),
|
||||
config: jest.fn().mockReturnValue({
|
||||
trainGold: (isFriendly: boolean) =>
|
||||
isFriendly ? BigInt(1000) : BigInt(500),
|
||||
trainGold: () => 1000n,
|
||||
trainGoldRefillTime: () => 60,
|
||||
}),
|
||||
addUpdate: jest.fn(),
|
||||
addExecution: jest.fn(),
|
||||
@@ -28,6 +28,8 @@ describe("TrainStation", () => {
|
||||
id: 1,
|
||||
canTrade: jest.fn().mockReturnValue(true),
|
||||
isFriendly: jest.fn().mockReturnValue(false),
|
||||
isOnSameTeam: jest.fn().mockReturnValue(false),
|
||||
isAlliedWith: jest.fn().mockReturnValue(false),
|
||||
} as any;
|
||||
|
||||
unit = {
|
||||
|
||||
Reference in New Issue
Block a user