diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 49f2fc5a0..f4b1ddc68 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -28,9 +28,11 @@ export interface LobbyConfig { gameID: GameID; map: GameMapType | null; difficulty: Difficulty | null; - disableBots: boolean | null; + infiniteGold: boolean | null; + infiniteTroops: boolean | null; + instantBuild: boolean | null; + bots: number | null; disableNPCs: boolean | null; - creativeMode: boolean | null; } export function joinLobby( @@ -53,9 +55,11 @@ export function joinLobby( gameType: GameType.Singleplayer, gameMap: lobbyConfig.map, difficulty: lobbyConfig.difficulty, - disableBots: lobbyConfig.disableBots, disableNPCs: lobbyConfig.disableNPCs, - creativeMode: lobbyConfig.creativeMode, + bots: lobbyConfig.bots, + infiniteGold: lobbyConfig.infiniteGold, + infiniteTroops: lobbyConfig.infiniteTroops, + instantBuild: lobbyConfig.instantBuild, }; } diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 7747d861b..d6d82ff81 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -13,8 +13,10 @@ export class HostLobbyModal extends LitElement { @state() private selectedMap: GameMapType = GameMapType.World; @state() private selectedDifficulty: Difficulty = Difficulty.Medium; @state() private disableNPCs = false; - @state() private disableBots = false; - @state() private creativeMode = false; + @state() private bots: number = 400; + @state() private infiniteGold: boolean = false; + @state() private infiniteTroops: boolean = false; + @state() private instantBuild: boolean = false; @state() private lobbyId = ""; @state() private copySuccess = false; @state() private players: string[] = []; @@ -301,6 +303,10 @@ export class HostLobbyModal extends LitElement { border: 1px solid rgba(255, 255, 255, 0.2); } + #bots-count { + width: 80%; + } + @media screen and (max-width: 768px) { .modal-content { max-height: calc(100vh - 42px); @@ -397,18 +403,20 @@ export class HostLobbyModal extends LitElement {
Options
-
@@ -485,9 +521,11 @@ export class HostLobbyModal extends LitElement { }, map: this.selectedMap, difficulty: this.selectedDifficulty, - disableBots: this.disableBots, disableNPCs: this.disableNPCs, - creativeMode: this.creativeMode, + bots: this.bots, + infiniteGold: this.infiniteGold, + infiniteTroops: this.infiniteTroops, + instantBuild: this.instantBuild, }, bubbles: true, composed: true, @@ -516,9 +554,24 @@ export class HostLobbyModal extends LitElement { this.putGameConfig(); } - private async handleDisableBotsChange(e: Event) { - this.disableBots = Boolean((e.target as HTMLInputElement).checked); - consolex.log(`updating disable bots to ${this.disableBots}`); + private handleBotsChange(e: Event) { + const value = parseInt((e.target as HTMLInputElement).value); + if (isNaN(value) || value < 0 || value > 400) { + return; + } + this.bots = value; + this.putGameConfig(); + } + private handleInstantBuildChange(e: Event) { + this.instantBuild = Boolean((e.target as HTMLInputElement).checked); + this.putGameConfig(); + } + private handleInfiniteGoldChange(e: Event) { + this.infiniteGold = Boolean((e.target as HTMLInputElement).checked); + this.putGameConfig(); + } + private handleInfiniteTroopsChange(e: Event) { + this.infiniteTroops = Boolean((e.target as HTMLInputElement).checked); this.putGameConfig(); } @@ -528,12 +581,6 @@ export class HostLobbyModal extends LitElement { this.putGameConfig(); } - private async handleCreativeModeChange(e: Event) { - this.creativeMode = Boolean((e.target as HTMLInputElement).checked); - consolex.log(`updating creative mode to ${this.creativeMode}`); - this.putGameConfig(); - } - private async putGameConfig() { const response = await fetch(`/private_lobby/${this.lobbyId}`, { method: "PUT", @@ -543,9 +590,11 @@ export class HostLobbyModal extends LitElement { body: JSON.stringify({ gameMap: this.selectedMap, difficulty: this.selectedDifficulty, - disableBots: this.disableBots, disableNPCs: this.disableNPCs, - creativeMode: this.creativeMode, + bots: this.bots, + infiniteGold: this.infiniteGold, + infiniteTroops: this.infiniteTroops, + instantBuild: this.instantBuild, }), }); } diff --git a/src/client/Main.ts b/src/client/Main.ts index ca9405d8c..be15a4df4 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -150,9 +150,11 @@ class Client { clientID: generateID(), map: event.detail.map, difficulty: event.detail.difficulty, - disableBots: event.detail.disableBots, + infiniteGold: event.detail.infiniteGold, + infiniteTroops: event.detail.infiniteTroops, + instantBuild: event.detail.instantBuild, + bots: event.detail.bots, disableNPCs: event.detail.disableNPCs, - creativeMode: event.detail.creativeMode, }, () => { this.joinModal.close(); diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 7d917ca69..b89a62d54 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -12,9 +12,11 @@ export class SinglePlayerModal extends LitElement { @state() private isModalOpen = false; @state() private selectedMap: GameMapType = GameMapType.World; @state() private selectedDifficulty: Difficulty = Difficulty.Medium; - @state() private disableNPCs = false; - @state() private disableBots = false; - @state() private creativeMode = false; + @state() private disableNPCs: boolean = false; + @state() private bots: number = 400; + @state() private infiniteGold: boolean = false; + @state() private infiniteTroops: boolean = false; + @state() private instantBuild: boolean = false; static styles = css` .modal-overlay { @@ -224,6 +226,10 @@ export class SinglePlayerModal extends LitElement { color: white; } + #bots-count { + width: 80%; + } + @media screen and (max-width: 768px) { .modal-content { max-height: calc(100vh - 42px); @@ -294,18 +300,20 @@ export class SinglePlayerModal extends LitElement {
Options
-
@@ -360,15 +396,25 @@ export class SinglePlayerModal extends LitElement { private handleDifficultySelection(value: Difficulty) { this.selectedDifficulty = value; } - private handleDisableBotsChange(e: Event) { - this.disableBots = Boolean((e.target as HTMLInputElement).checked); + private handleBotsChange(e: Event) { + const value = parseInt((e.target as HTMLInputElement).value); + if (isNaN(value) || value < 0 || value > 400) { + return; + } + this.bots = value; + } + private handleInstantBuildChange(e: Event) { + this.instantBuild = Boolean((e.target as HTMLInputElement).checked); + } + private handleInfiniteGoldChange(e: Event) { + this.infiniteGold = Boolean((e.target as HTMLInputElement).checked); + } + private handleInfiniteTroopsChange(e: Event) { + this.infiniteTroops = Boolean((e.target as HTMLInputElement).checked); } private handleDisableNPCsChange(e: Event) { this.disableNPCs = Boolean((e.target as HTMLInputElement).checked); } - private handleCreativeModeChange(e: Event) { - this.creativeMode = Boolean((e.target as HTMLInputElement).checked); - } private startGame() { consolex.log( `Starting single player game with map: ${GameMapType[this.selectedMap]}`, @@ -382,9 +428,11 @@ export class SinglePlayerModal extends LitElement { }, map: this.selectedMap, difficulty: this.selectedDifficulty, - disableBots: this.disableBots, disableNPCs: this.disableNPCs, - creativeMode: this.creativeMode, + bots: this.bots, + infiniteGold: this.infiniteGold, + infiniteTroops: this.infiniteTroops, + instantBuild: this.instantBuild, }, bubbles: true, composed: true, diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 8c95b39e2..56c0e7e65 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -63,7 +63,7 @@ export class GameRunner { ) {} init() { - if (this.game.config().spawnBots()) { + if (this.game.config().bots() > 0) { this.game.addExecution( ...this.execManager.spawnBots(this.game.config().numBots()), ); diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 355754fa0..8ea615139 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -92,9 +92,11 @@ const GameConfigSchema = z.object({ gameMap: z.nativeEnum(GameMapType), difficulty: z.nativeEnum(Difficulty), gameType: z.nativeEnum(GameType), - disableBots: z.boolean(), disableNPCs: z.boolean(), - creativeMode: z.boolean(), + bots: z.number().int().min(0).max(400), + infiniteGold: z.boolean(), + infiniteTroops: z.boolean(), + instantBuild: z.boolean(), }); const SafeString = z diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index a5e2fa9b1..a43a8663a 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -77,8 +77,10 @@ export interface Config { percentageTilesOwnedToWin(): number; numBots(): number; spawnNPCs(): boolean; - spawnBots(): boolean; - creativeMode(): boolean; + bots(): number; + infiniteGold(): boolean; + infiniteTroops(): boolean; + instantBuild(): boolean; numSpawnPhaseTurns(): number; userSettings(): UserSettings; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index a5c9fe41f..9f23e200d 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -89,11 +89,17 @@ export class DefaultConfig implements Config { spawnNPCs(): boolean { return !this._gameConfig.disableNPCs; } - spawnBots(): boolean { - return !this._gameConfig.disableBots; + bots(): number { + return this._gameConfig.bots; } - creativeMode(): boolean { - return this._gameConfig.creativeMode; + instantBuild(): boolean { + return this._gameConfig.instantBuild; + } + infiniteGold(): boolean { + return this._gameConfig.infiniteGold; + } + infiniteTroops(): boolean { + return this._gameConfig.infiniteTroops; } tradeShipGold(dist: number): Gold { return 10000 + 100 * Math.pow(dist, 1.1); @@ -112,7 +118,7 @@ export class DefaultConfig implements Config { case UnitType.Warship: return { cost: (p: Player) => - p.type() == PlayerType.Human && this.creativeMode() + p.type() == PlayerType.Human && this.infiniteGold() ? 0 : (p.unitsIncludingConstruction(UnitType.Warship).length + 1) * 250_000, @@ -128,7 +134,7 @@ export class DefaultConfig implements Config { case UnitType.Port: return { cost: (p: Player) => - p.type() == PlayerType.Human && this.creativeMode() + p.type() == PlayerType.Human && this.infiniteGold() ? 0 : Math.min( 1_000_000, @@ -138,24 +144,24 @@ export class DefaultConfig implements Config { ) * 125_000, ), territoryBound: true, - constructionDuration: this.creativeMode() ? 0 : 2 * 10, + constructionDuration: this.instantBuild() ? 0 : 2 * 10, }; case UnitType.AtomBomb: return { cost: (p: Player) => - p.type() == PlayerType.Human && this.creativeMode() ? 0 : 750_000, + p.type() == PlayerType.Human && this.infiniteGold() ? 0 : 750_000, territoryBound: false, }; case UnitType.HydrogenBomb: return { cost: (p: Player) => - p.type() == PlayerType.Human && this.creativeMode() ? 0 : 5_000_000, + p.type() == PlayerType.Human && this.infiniteGold() ? 0 : 5_000_000, territoryBound: false, }; case UnitType.MIRV: return { cost: (p: Player) => - p.type() == PlayerType.Human && this.creativeMode() + p.type() == PlayerType.Human && this.infiniteGold() ? 0 : 15_000_000, territoryBound: false, @@ -173,14 +179,14 @@ export class DefaultConfig implements Config { case UnitType.MissileSilo: return { cost: (p: Player) => - p.type() == PlayerType.Human && this.creativeMode() ? 0 : 1_000_000, + p.type() == PlayerType.Human && this.infiniteGold() ? 0 : 1_000_000, territoryBound: true, - constructionDuration: this.creativeMode() ? 0 : 10 * 10, + constructionDuration: this.instantBuild() ? 0 : 10 * 10, }; case UnitType.DefensePost: return { cost: (p: Player) => - p.type() == PlayerType.Human && this.creativeMode() + p.type() == PlayerType.Human && this.infiniteGold() ? 0 : Math.min( 250_000, @@ -189,12 +195,12 @@ export class DefaultConfig implements Config { 50_000, ), territoryBound: true, - constructionDuration: this.creativeMode() ? 0 : 5 * 10, + constructionDuration: this.instantBuild() ? 0 : 5 * 10, }; case UnitType.City: return { cost: (p: Player) => - p.type() == PlayerType.Human && this.creativeMode() + p.type() == PlayerType.Human && this.infiniteGold() ? 0 : Math.min( 1_000_000, @@ -204,7 +210,7 @@ export class DefaultConfig implements Config { ) * 125_000, ), territoryBound: true, - constructionDuration: this.creativeMode() ? 0 : 2 * 10, + constructionDuration: this.instantBuild() ? 0 : 2 * 10, }; case UnitType.Construction: return { @@ -252,7 +258,7 @@ export class DefaultConfig implements Config { return this._gameConfig.gameType == GameType.Singleplayer ? 100 : 300; } numBots(): number { - return 400; + return this.bots(); } theme(): Theme { return this.userSettings().darkMode() ? pastelThemeDark : pastelTheme; @@ -395,12 +401,12 @@ export class DefaultConfig implements Config { return 50_000 * (playerInfo?.nation?.strength ?? 1); } } - return this.creativeMode() ? 1_000_000 : 25_000; + return this.infiniteTroops() ? 1_000_000 : 25_000; } maxPopulation(player: Player | PlayerView): number { let maxPop = - player.type() == PlayerType.Human && this.creativeMode() + player.type() == PlayerType.Human && this.infiniteTroops() ? 1_000_000_000 : 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) + player.units(UnitType.City).length * this.cityPopulationIncrease(); diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index dfe60e0d6..7b9f217c5 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -54,9 +54,11 @@ export class GameManager { gameMap: GameMapType.World, gameType: GameType.Private, difficulty: Difficulty.Medium, - disableBots: false, disableNPCs: false, - creativeMode: false, + infiniteGold: false, + infiniteTroops: false, + instantBuild: false, + bots: 400, }), ); return id; @@ -139,9 +141,11 @@ export class GameManager { gameMap: this.getNextMap(), gameType: GameType.Public, difficulty: Difficulty.Medium, - disableBots: false, + infiniteGold: false, + infiniteTroops: false, + instantBuild: false, disableNPCs: false, - creativeMode: false, + bots: 400, }), ); } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 140419379..570a3c90c 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -63,14 +63,20 @@ export class GameServer { if (gameConfig.difficulty != null) { this.gameConfig.difficulty = gameConfig.difficulty; } - if (gameConfig.disableBots != null) { - this.gameConfig.disableBots = gameConfig.disableBots; - } if (gameConfig.disableNPCs != null) { this.gameConfig.disableNPCs = gameConfig.disableNPCs; } - if (gameConfig.creativeMode != null) { - this.gameConfig.creativeMode = gameConfig.creativeMode; + if (gameConfig.bots != null) { + this.gameConfig.bots = gameConfig.bots; + } + if (gameConfig.infiniteGold != null) { + this.gameConfig.infiniteGold = gameConfig.infiniteGold; + } + if (gameConfig.infiniteTroops != null) { + this.gameConfig.infiniteTroops = gameConfig.infiniteTroops; + } + if (gameConfig.instantBuild != null) { + this.gameConfig.instantBuild = gameConfig.instantBuild; } } diff --git a/src/server/Server.ts b/src/server/Server.ts index b201f5117..a8a46cf14 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -225,9 +225,11 @@ app.put( gm.updateGameConfig(lobbyID, { gameMap: req.body.gameMap, difficulty: req.body.difficulty, - disableBots: req.body.disableBots, + infiniteGold: req.body.infiniteGold, + infiniteTroops: req.body.infiniteTroops, + instantBuild: req.body.instantBuild, + bots: req.body.bots, disableNPCs: req.body.disableNPCs, - creativeMode: req.body.creativeMode, }); res.status(200).json({ success: true }); }),