mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-28 14:27:54 +00:00
Add host cheats for streamers (Specifically Enzo) ⭐ (#3671)
## 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
This commit is contained in:
@@ -258,6 +258,20 @@ export const GameConfigSchema = z.object({
|
||||
playerTeams: TeamCountConfigSchema.optional(),
|
||||
goldMultiplier: z.number().min(0.1).max(1000).nullable().optional(),
|
||||
startingGold: z.number().int().min(0).max(1000000000).nullable().optional(),
|
||||
hostCheats: z
|
||||
.object({
|
||||
infiniteGold: z.boolean().optional(),
|
||||
infiniteTroops: z.boolean().optional(),
|
||||
goldMultiplier: z.number().min(0.1).max(1000).nullable().optional(),
|
||||
startingGold: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(1000000000)
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const TeamSchema = z.string();
|
||||
|
||||
@@ -128,7 +128,7 @@ export interface Config {
|
||||
defaultDonationAmount(sender: Player): number;
|
||||
unitInfo(type: UnitType): UnitInfo;
|
||||
tradeShipShortRangeDebuff(): number;
|
||||
tradeShipGold(dist: number): Gold;
|
||||
tradeShipGold(dist: number, player: Player | PlayerView): Gold;
|
||||
tradeShipSpawnRate(
|
||||
tradeShipSpawnRejections: number,
|
||||
numTradeShips: number,
|
||||
@@ -136,6 +136,7 @@ export interface Config {
|
||||
trainGold(
|
||||
rel: "self" | "team" | "ally" | "other",
|
||||
citiesVisited: number,
|
||||
player: Player | PlayerView,
|
||||
): Gold;
|
||||
trainSpawnRate(numPlayerFactories: number): number;
|
||||
trainStationMinRange(): number;
|
||||
|
||||
@@ -271,7 +271,7 @@ export class DefaultConfig implements Config {
|
||||
if (playerInfo.playerType === PlayerType.Bot) {
|
||||
return 0n;
|
||||
}
|
||||
return BigInt(this._gameConfig.startingGold ?? 0);
|
||||
return this.startingGoldFor(playerInfo);
|
||||
}
|
||||
|
||||
trainSpawnRate(numPlayerFactories: number): number {
|
||||
@@ -282,6 +282,7 @@ export class DefaultConfig implements Config {
|
||||
trainGold(
|
||||
rel: "self" | "team" | "ally" | "other",
|
||||
citiesVisited: number,
|
||||
player: Player | PlayerView,
|
||||
): Gold {
|
||||
// No penalty for the first 10 cities.
|
||||
citiesVisited = Math.max(0, citiesVisited - 9);
|
||||
@@ -300,7 +301,7 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
const distPenalty = citiesVisited * 5_000;
|
||||
const gold = Math.max(5000, baseGold - distPenalty);
|
||||
return toInt(gold * this.goldMultiplier());
|
||||
return toInt(gold * this.goldMultiplierFor(player));
|
||||
}
|
||||
|
||||
trainStationMinRange(): number {
|
||||
@@ -313,13 +314,12 @@ export class DefaultConfig implements Config {
|
||||
return 120;
|
||||
}
|
||||
|
||||
tradeShipGold(dist: number): Gold {
|
||||
tradeShipGold(dist: number, player: Player | PlayerView): Gold {
|
||||
// Sigmoid: concave start, sharp S-curve middle, linear end - heavily punishes trades under range debuff.
|
||||
const debuff = this.tradeShipShortRangeDebuff();
|
||||
const baseGold =
|
||||
75_000 / (1 + Math.exp(-0.03 * (dist - debuff))) + 50 * dist;
|
||||
const multiplier = this.goldMultiplier();
|
||||
return BigInt(Math.floor(baseGold * multiplier));
|
||||
return BigInt(Math.floor(baseGold * this.goldMultiplierFor(player)));
|
||||
}
|
||||
|
||||
// Probability of trade ship spawn = 1 / tradeShipSpawnRate
|
||||
@@ -396,7 +396,10 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.MIRV:
|
||||
info = {
|
||||
cost: (game: Game, player: Player) => {
|
||||
if (player.type() === PlayerType.Human && this.infiniteGold()) {
|
||||
if (
|
||||
player.type() === PlayerType.Human &&
|
||||
this.hasInfiniteGoldFor(player)
|
||||
) {
|
||||
return 0n;
|
||||
}
|
||||
return 25_000_000n + game.stats().numMirvsLaunched() * 15_000_000n;
|
||||
@@ -478,12 +481,55 @@ export class DefaultConfig implements Config {
|
||||
return info;
|
||||
}
|
||||
|
||||
private hasInfiniteGoldFor(player: Player | PlayerView): boolean {
|
||||
if (this.infiniteGold()) return true;
|
||||
const hc = this._gameConfig.hostCheats;
|
||||
return (hc?.infiniteGold ?? false) && player.isLobbyCreator();
|
||||
}
|
||||
|
||||
private hasInfiniteTroopsFor(player: Player | PlayerView): boolean {
|
||||
if (this.infiniteTroops()) return true;
|
||||
return (
|
||||
(this._gameConfig.hostCheats?.infiniteTroops ?? false) &&
|
||||
player.isLobbyCreator()
|
||||
);
|
||||
}
|
||||
|
||||
private hasInfiniteTroopsForInfo(playerInfo: PlayerInfo): boolean {
|
||||
if (this.infiniteTroops()) return true;
|
||||
return (
|
||||
(this._gameConfig.hostCheats?.infiniteTroops ?? false) &&
|
||||
playerInfo.isLobbyCreator
|
||||
);
|
||||
}
|
||||
|
||||
private goldMultiplierFor(player: Player | PlayerView): number {
|
||||
const base = this.goldMultiplier();
|
||||
const hc = this._gameConfig.hostCheats;
|
||||
if (hc?.goldMultiplier && player.isLobbyCreator()) {
|
||||
return hc.goldMultiplier;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
private startingGoldFor(playerInfo: PlayerInfo): Gold {
|
||||
const base = BigInt(this._gameConfig.startingGold ?? 0);
|
||||
const hc = this._gameConfig.hostCheats;
|
||||
if (hc?.startingGold && playerInfo.isLobbyCreator) {
|
||||
return base + BigInt(hc.startingGold);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
private costWrapper(
|
||||
costFn: (units: number) => number,
|
||||
...types: UnitType[]
|
||||
): (g: Game, p: Player) => bigint {
|
||||
return (game: Game, player: Player) => {
|
||||
if (player.type() === PlayerType.Human && this.infiniteGold()) {
|
||||
if (
|
||||
player.type() === PlayerType.Human &&
|
||||
this.hasInfiniteGoldFor(player)
|
||||
) {
|
||||
return 0n;
|
||||
}
|
||||
const numUnits = types.reduce(
|
||||
@@ -761,12 +807,12 @@ export class DefaultConfig implements Config {
|
||||
assertNever(this._gameConfig.difficulty);
|
||||
}
|
||||
}
|
||||
return this.infiniteTroops() ? 1_000_000 : 25_000;
|
||||
return this.hasInfiniteTroopsForInfo(playerInfo) ? 1_000_000 : 25_000;
|
||||
}
|
||||
|
||||
maxTroops(player: Player | PlayerView): number {
|
||||
const maxTroops =
|
||||
player.type() === PlayerType.Human && this.infiniteTroops()
|
||||
player.type() === PlayerType.Human && this.hasInfiniteTroopsFor(player)
|
||||
? 1_000_000_000
|
||||
: 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) +
|
||||
player
|
||||
@@ -833,7 +879,7 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
goldAdditionRate(player: Player): Gold {
|
||||
const multiplier = this.goldMultiplier();
|
||||
const multiplier = this.goldMultiplierFor(player);
|
||||
let baseRate: bigint;
|
||||
if (player.type() === PlayerType.Bot) {
|
||||
baseRate = 50n;
|
||||
|
||||
@@ -168,7 +168,9 @@ export class TradeShipExecution implements Execution {
|
||||
private complete() {
|
||||
this.active = false;
|
||||
this.tradeShip!.delete(false);
|
||||
const gold = this.mg.config().tradeShipGold(this.tilesTraveled);
|
||||
const gold = this.mg
|
||||
.config()
|
||||
.tradeShipGold(this.tilesTraveled, this.tradeShip!.owner());
|
||||
|
||||
if (this.wasCaptured) {
|
||||
this.tradeShip!.owner().addGold(gold, this._dstPort.tile());
|
||||
|
||||
@@ -735,7 +735,7 @@ export class NationStructureBehavior {
|
||||
}
|
||||
|
||||
const maxTradeGold = Math.max(
|
||||
Number(game.config().trainGold("ally", 0)),
|
||||
Number(game.config().trainGold("ally", 0, player)),
|
||||
1,
|
||||
);
|
||||
const result: Array<{
|
||||
@@ -746,7 +746,7 @@ export class NationStructureBehavior {
|
||||
|
||||
// Own structures — weighted by "self" trade gold.
|
||||
const selfWeight =
|
||||
Number(game.config().trainGold("self", 0)) / maxTradeGold;
|
||||
Number(game.config().trainGold("self", 0, player)) / maxTradeGold;
|
||||
for (const unit of player.units(
|
||||
UnitType.City,
|
||||
UnitType.Port,
|
||||
@@ -771,7 +771,8 @@ export class NationStructureBehavior {
|
||||
: player.isAlliedWith(neighbor)
|
||||
? "ally"
|
||||
: "other";
|
||||
const weight = Number(game.config().trainGold(relType, 0)) / maxTradeGold;
|
||||
const weight =
|
||||
Number(game.config().trainGold(relType, 0, player)) / maxTradeGold;
|
||||
for (const unit of neighbor.units(
|
||||
UnitType.City,
|
||||
UnitType.Port,
|
||||
|
||||
@@ -25,6 +25,7 @@ class TradeStationStopHandler implements TrainStopHandler {
|
||||
.trainGold(
|
||||
rel(trainOwner, stationOwner),
|
||||
trainExecution.tradeStopsVisited(),
|
||||
trainOwner,
|
||||
);
|
||||
// Share revenue with the station owner if it's not the current player
|
||||
if (trainOwner !== stationOwner) {
|
||||
|
||||
Reference in New Issue
Block a user