Customizable creative mode

This commit is contained in:
NewHappyRabbit
2025-02-24 23:06:50 +02:00
parent f995fc4ec6
commit 02d5060352
11 changed files with 224 additions and 99 deletions
+8 -4
View File
@@ -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,
};
}
+81 -32
View File
@@ -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 {
<div class="options-section">
<div class="option-title">Options</div>
<div class="option-cards">
<label
for="disable-bots"
class="option-card ${this.disableBots ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<label for="bots-count" class="option-card">
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
.checked=${this.disableBots}
type="range"
id="bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${this.bots}"
/>
<div class="option-card-title">Disable Bots</div>
<div class="option-card-title">
Bots: ${this.bots == 0 ? "Disabled" : this.bots}
</div>
</label>
<label
@@ -422,21 +430,49 @@ export class HostLobbyModal extends LitElement {
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
/>
<div class="option-card-title">Disable NPCs</div>
<div class="option-card-title">Disable Nations</div>
</label>
<label
for="creative-mode"
class="option-card ${this.creativeMode ? "selected" : ""}"
for="instant-build"
class="option-card ${this.instantBuild ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
.checked=${this.creativeMode}
id="instant-build"
@change=${this.handleInstantBuildChange}
.checked=${this.instantBuild}
/>
<div class="option-card-title">Creative Mode</div>
<div class="option-card-title">Instant build</div>
</label>
<label
for="infinite-gold"
class="option-card ${this.infiniteGold ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="infinite-gold"
@change=${this.handleInfiniteGoldChange}
.checked=${this.infiniteGold}
/>
<div class="option-card-title">Infinite gold</div>
</label>
<label
for="infinite-troops"
class="option-card ${this.infiniteTroops ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="infinite-troops"
@change=${this.handleInfiniteTroopsChange}
.checked=${this.infiniteTroops}
/>
<div class="option-card-title">Infinite troops</div>
</label>
</div>
</div>
@@ -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,
}),
});
}
+4 -2
View File
@@ -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();
+74 -26
View File
@@ -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 {
<div class="options-section">
<div class="option-title">Options</div>
<div class="option-cards">
<label
for="disable-bots"
class="option-card ${this.disableBots ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<label for="bots-count" class="option-card">
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
.checked=${this.disableBots}
type="range"
id="bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${this.bots}"
/>
<div class="option-card-title">Disable Bots</div>
<div class="option-card-title">
Bots: ${this.bots == 0 ? "Disabled" : this.bots}
</div>
</label>
<label
@@ -323,17 +331,45 @@ export class SinglePlayerModal extends LitElement {
</label>
<label
for="creative-mode"
class="option-card ${this.creativeMode ? "selected" : ""}"
for="instant-build"
class="option-card ${this.instantBuild ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
.checked=${this.creativeMode}
id="instant-build"
@change=${this.handleInstantBuildChange}
.checked=${this.instantBuild}
/>
<div class="option-card-title">Creative Mode</div>
<div class="option-card-title">Instant build</div>
</label>
<label
for="infinite-gold"
class="option-card ${this.infiniteGold ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="infinite-gold"
@change=${this.handleInfiniteGoldChange}
.checked=${this.infiniteGold}
/>
<div class="option-card-title">Infinite gold</div>
</label>
<label
for="infinite-troops"
class="option-card ${this.infiniteTroops ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="infinite-troops"
@change=${this.handleInfiniteTroopsChange}
.checked=${this.infiniteTroops}
/>
<div class="option-card-title">Infinite troops</div>
</label>
</div>
</div>
@@ -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,
+1 -1
View File
@@ -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()),
);
+4 -2
View File
@@ -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
+4 -2
View File
@@ -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;
+25 -19
View File
@@ -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();
+8 -4
View File
@@ -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,
}),
);
}
+11 -5
View File
@@ -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;
}
}
+4 -2
View File
@@ -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 });
}),