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:
FloPinguin
2026-04-15 15:20:08 -07:00
committed by GitHub
parent f32994fbc7
commit 9821e8e041
13 changed files with 388 additions and 30 deletions
+14
View File
@@ -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();
+2 -1
View File
@@ -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;
+56 -10
View File
@@ -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;
+3 -1
View File
@@ -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,
+1
View File
@@ -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) {