mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-25 02:17:00 +00:00
## Description: - Adds a "Host Cheats" toggle in the private lobby options section that reveals a dedicated section with four host-only cheats: infinite gold, infinite troops, gold multiplier, and starting gold - Only the lobby creator receives the cheat effects in-game (checked via `isLobbyCreator` in DefaultConfig) - Joining players see active host cheats displayed as yellow badges in the lobby UI - Adds `hostCheats` optional object to `GameConfigSchema` and wires it through the server config update whitelist - Raises the intent size limit for `update_game_config` messages (lobby-only, not stored in turn history) to prevent rate-limiter kicks (I always got too-much-data-kicked after selecting "host cheats" lol) <img width="861" height="525" alt="image" src="https://github.com/user-attachments/assets/51e51ec4-c2e8-46ca-b258-11a93487964f" /> <img width="933" height="825" alt="image" src="https://github.com/user-attachments/assets/5acbd38d-2097-42e1-ba78-0fb17d6afe82" /> ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin
263 lines
6.4 KiB
TypeScript
263 lines
6.4 KiB
TypeScript
import { TrainExecution } from "../execution/TrainExecution";
|
|
import { PseudoRandom } from "../PseudoRandom";
|
|
import { Game, Player, Unit, UnitType } from "./Game";
|
|
import { TileRef } from "./GameMap";
|
|
import { GameUpdateType } from "./GameUpdates";
|
|
import { Railroad } from "./Railroad";
|
|
|
|
/**
|
|
* Handle train stops at various station types
|
|
*/
|
|
interface TrainStopHandler {
|
|
onStop(mg: Game, station: TrainStation, trainExecution: TrainExecution): void;
|
|
}
|
|
|
|
class TradeStationStopHandler implements TrainStopHandler {
|
|
onStop(
|
|
mg: Game,
|
|
station: TrainStation,
|
|
trainExecution: TrainExecution,
|
|
): void {
|
|
const stationOwner = station.unit.owner();
|
|
const trainOwner = trainExecution.owner();
|
|
const gold = mg
|
|
.config()
|
|
.trainGold(
|
|
rel(trainOwner, stationOwner),
|
|
trainExecution.tradeStopsVisited(),
|
|
trainOwner,
|
|
);
|
|
// Share revenue with the station owner if it's not the current player
|
|
if (trainOwner !== stationOwner) {
|
|
stationOwner.addGold(gold, station.tile());
|
|
mg.stats().trainExternalTrade(stationOwner, gold);
|
|
}
|
|
trainOwner.addGold(gold, station.tile());
|
|
mg.stats().trainSelfTrade(trainOwner, gold);
|
|
}
|
|
}
|
|
|
|
class FactoryStopHandler implements TrainStopHandler {
|
|
onStop(
|
|
mg: Game,
|
|
station: TrainStation,
|
|
trainExecution: TrainExecution,
|
|
): void {}
|
|
}
|
|
|
|
export function createTrainStopHandlers(
|
|
random: PseudoRandom,
|
|
): Partial<Record<UnitType, TrainStopHandler>> {
|
|
return {
|
|
[UnitType.City]: new TradeStationStopHandler(),
|
|
[UnitType.Port]: new TradeStationStopHandler(),
|
|
[UnitType.Factory]: new FactoryStopHandler(),
|
|
};
|
|
}
|
|
|
|
export class TrainStation {
|
|
id: number = -1; // assigned by StationManager
|
|
private readonly stopHandlers: Partial<Record<UnitType, TrainStopHandler>> =
|
|
{};
|
|
private cluster: Cluster | null = null;
|
|
private railroads: Set<Railroad> = new Set();
|
|
// Quick lookup from neighboring station to connecting railroad
|
|
private railroadByNeighbor: Map<TrainStation, Railroad> = new Map();
|
|
|
|
constructor(
|
|
private mg: Game,
|
|
public unit: Unit,
|
|
) {
|
|
this.stopHandlers = createTrainStopHandlers(new PseudoRandom(mg.ticks()));
|
|
}
|
|
|
|
tradeAvailable(otherPlayer: Player): boolean {
|
|
const player = this.unit.owner();
|
|
return otherPlayer === player || player.canTrade(otherPlayer);
|
|
}
|
|
|
|
clearRailroads() {
|
|
this.railroads.clear();
|
|
this.railroadByNeighbor.clear();
|
|
}
|
|
|
|
addRailroad(railRoad: Railroad) {
|
|
this.railroads.add(railRoad);
|
|
const neighbor = railRoad.from === this ? railRoad.to : railRoad.from;
|
|
this.railroadByNeighbor.set(neighbor, railRoad);
|
|
}
|
|
|
|
removeRailroad(railRoad: Railroad) {
|
|
this.railroads.delete(railRoad);
|
|
const neighbor = railRoad.from === this ? railRoad.to : railRoad.from;
|
|
this.railroadByNeighbor.delete(neighbor);
|
|
}
|
|
|
|
removeNeighboringRails(station: TrainStation) {
|
|
const toRemove = [...this.railroads].find(
|
|
(r) => r.from === station || r.to === station,
|
|
);
|
|
if (toRemove) {
|
|
this.mg.addUpdate({
|
|
type: GameUpdateType.RailroadDestructionEvent,
|
|
id: toRemove.id,
|
|
});
|
|
this.removeRailroad(toRemove);
|
|
}
|
|
}
|
|
|
|
neighbors(): TrainStation[] {
|
|
const neighbors: TrainStation[] = [];
|
|
for (const r of this.railroads) {
|
|
if (r.from !== this) {
|
|
neighbors.push(r.from);
|
|
} else {
|
|
neighbors.push(r.to);
|
|
}
|
|
}
|
|
return neighbors;
|
|
}
|
|
|
|
tile(): TileRef {
|
|
return this.unit.tile();
|
|
}
|
|
|
|
isActive(): boolean {
|
|
return this.unit.isActive();
|
|
}
|
|
|
|
getRailroads(): Set<Railroad> {
|
|
return this.railroads;
|
|
}
|
|
|
|
getRailroadTo(station: TrainStation): Railroad | null {
|
|
return this.railroadByNeighbor.get(station) ?? null;
|
|
}
|
|
|
|
setCluster(cluster: Cluster | null) {
|
|
// Properly disconnect cluster if it's already set
|
|
if (this.cluster !== null) {
|
|
this.cluster.removeStation(this);
|
|
}
|
|
this.cluster = cluster;
|
|
}
|
|
|
|
getCluster(): Cluster | null {
|
|
return this.cluster;
|
|
}
|
|
|
|
onTrainStop(trainExecution: TrainExecution) {
|
|
const type = this.unit.type();
|
|
const handler = this.stopHandlers[type];
|
|
if (handler) {
|
|
handler.onStop(this.mg, this, trainExecution);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cluster of connected stations
|
|
*/
|
|
export class Cluster {
|
|
public stations: Set<TrainStation> = new Set();
|
|
private tradeStations: Set<TrainStation> = new Set();
|
|
|
|
private isTradeStation(station: TrainStation): boolean {
|
|
const type = station.unit.type();
|
|
return type === UnitType.City || type === UnitType.Port;
|
|
}
|
|
|
|
has(station: TrainStation) {
|
|
return this.stations.has(station);
|
|
}
|
|
|
|
addStation(station: TrainStation) {
|
|
this.stations.add(station);
|
|
if (this.isTradeStation(station)) {
|
|
this.tradeStations.add(station);
|
|
}
|
|
station.setCluster(this);
|
|
}
|
|
|
|
removeStation(station: TrainStation) {
|
|
this.stations.delete(station);
|
|
this.tradeStations.delete(station);
|
|
}
|
|
|
|
addStations(stations: Set<TrainStation>) {
|
|
for (const station of stations) {
|
|
this.addStation(station);
|
|
}
|
|
}
|
|
|
|
merge(other: Cluster) {
|
|
for (const s of other.stations) {
|
|
this.addStation(s);
|
|
}
|
|
}
|
|
|
|
hasAnyTradeDestination(player: Player): boolean {
|
|
for (const station of this.tradeStations) {
|
|
if (station.tradeAvailable(player)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
randomTradeDestination(
|
|
player: Player,
|
|
random: PseudoRandom,
|
|
): TrainStation | null {
|
|
let selected: TrainStation | null = null;
|
|
let eligibleSeen = 0;
|
|
|
|
for (const station of this.tradeStations) {
|
|
if (!station.tradeAvailable(player)) continue;
|
|
eligibleSeen++;
|
|
|
|
// Reservoir sampling: keep each eligible station with probability 1/eligibleSeen.
|
|
if (random.nextInt(0, eligibleSeen) === 0) {
|
|
selected = station;
|
|
}
|
|
}
|
|
|
|
return selected;
|
|
}
|
|
|
|
availableForTrade(player: Player): Set<TrainStation> {
|
|
const tradingStations = new Set<TrainStation>();
|
|
for (const station of this.tradeStations) {
|
|
if (station.tradeAvailable(player)) {
|
|
tradingStations.add(station);
|
|
}
|
|
}
|
|
return tradingStations;
|
|
}
|
|
|
|
size() {
|
|
return this.stations.size;
|
|
}
|
|
|
|
clear() {
|
|
this.stations.clear();
|
|
this.tradeStations.clear();
|
|
}
|
|
}
|
|
|
|
function rel(
|
|
player: Player,
|
|
other: Player,
|
|
): "self" | "team" | "ally" | "other" {
|
|
if (player === other) {
|
|
return "self";
|
|
}
|
|
if (player.isOnSameTeam(other)) {
|
|
return "team";
|
|
}
|
|
if (player.isAlliedWith(other)) {
|
|
return "ally";
|
|
}
|
|
return "other";
|
|
}
|