${
hasLinkedAccount(this.userMeResponse) && this.hasOptionsChanged()
? html`
${translateText(
"single_modal.options_changed_no_achievements",
)}
`
: null
}
`;
}
// Check if any options other than map and difficulty have been changed from defaults
private hasOptionsChanged(): boolean {
return (
this.nations !== this.defaultNationCount ||
this.bots !== DEFAULT_OPTIONS.bots ||
this.infiniteGold !== DEFAULT_OPTIONS.infiniteGold ||
this.infiniteTroops !== DEFAULT_OPTIONS.infiniteTroops ||
this.compactMap !== DEFAULT_OPTIONS.compactMap ||
this.maxTimer !== DEFAULT_OPTIONS.maxTimer ||
this.instantBuild !== DEFAULT_OPTIONS.instantBuild ||
this.randomSpawn !== DEFAULT_OPTIONS.randomSpawn ||
this.gameMode !== DEFAULT_OPTIONS.gameMode ||
this.goldMultiplier !== DEFAULT_OPTIONS.goldMultiplier ||
this.startingGold !== DEFAULT_OPTIONS.startingGold ||
this.disableAlliances !== DEFAULT_OPTIONS.disableAlliances ||
this.waterNukes !== DEFAULT_OPTIONS.waterNukes ||
this.disabledUnits.length > 0
);
}
protected onClose(): void {
// Reset all transient form state to ensure clean slate
this.selectedMap = DEFAULT_OPTIONS.selectedMap;
this.selectedDifficulty = DEFAULT_OPTIONS.selectedDifficulty;
this.gameMode = DEFAULT_OPTIONS.gameMode;
this.useRandomMap = DEFAULT_OPTIONS.useRandomMap;
this.bots = DEFAULT_OPTIONS.bots;
this.nations = 0;
this.defaultNationCount = 0;
this.infiniteGold = DEFAULT_OPTIONS.infiniteGold;
this.infiniteTroops = DEFAULT_OPTIONS.infiniteTroops;
this.compactMap = DEFAULT_OPTIONS.compactMap;
this.maxTimer = DEFAULT_OPTIONS.maxTimer;
this.maxTimerValue = DEFAULT_OPTIONS.maxTimerValue;
this.instantBuild = DEFAULT_OPTIONS.instantBuild;
this.randomSpawn = DEFAULT_OPTIONS.randomSpawn;
this.teamCount = DEFAULT_OPTIONS.teamCount;
this.disabledUnits = [...DEFAULT_OPTIONS.disabledUnits];
this.goldMultiplier = DEFAULT_OPTIONS.goldMultiplier;
this.goldMultiplierValue = DEFAULT_OPTIONS.goldMultiplierValue;
this.startingGold = DEFAULT_OPTIONS.startingGold;
this.startingGoldValue = DEFAULT_OPTIONS.startingGoldValue;
this.disableAlliances = DEFAULT_OPTIONS.disableAlliances;
this.waterNukes = DEFAULT_OPTIONS.waterNukes;
}
protected onOpen(): void {
void this.loadNationCount();
}
private handleSelectRandomMap() {
this.useRandomMap = true;
this.selectedMap = getRandomMapType();
void this.loadNationCount();
}
private handleConfigRandomMapSelected = () => {
this.handleSelectRandomMap();
};
private handleMapSelection(value: GameMapType) {
this.selectedMap = value;
this.useRandomMap = false;
void this.loadNationCount();
}
private handleConfigMapSelected = (e: Event) => {
const customEvent = e as CustomEvent<{ map: GameMapType }>;
this.handleMapSelection(customEvent.detail.map);
};
private handleDifficultySelection(value: Difficulty) {
this.selectedDifficulty = value;
}
private handleConfigDifficultySelected = (e: Event) => {
const customEvent = e as CustomEvent<{ difficulty: Difficulty }>;
this.handleDifficultySelection(customEvent.detail.difficulty);
};
private handleConfigGameModeSelected = (e: Event) => {
const customEvent = e as CustomEvent<{ mode: GameMode }>;
this.handleGameModeSelection(customEvent.detail.mode);
};
private handleConfigTeamCountSelected = (e: Event) => {
const customEvent = e as CustomEvent<{ count: TeamCountConfig }>;
this.handleTeamCountSelection(customEvent.detail.count);
};
private handleCompactMapChange(val: boolean) {
this.compactMap = val;
this.bots = getBotsForCompactMap(this.bots, val);
this.nations = getNationsForCompactMap(
this.nations,
this.defaultNationCount,
val,
);
}
private handleConfigOptionToggleChanged = (e: Event) => {
const customEvent = e as CustomEvent<{
labelKey: string;
checked: boolean;
}>;
const { labelKey, checked } = customEvent.detail;
switch (labelKey) {
case "single_modal.instant_build":
this.instantBuild = checked;
break;
case "single_modal.random_spawn":
this.randomSpawn = checked;
break;
case "single_modal.infinite_gold":
this.infiniteGold = checked;
break;
case "single_modal.infinite_troops":
this.infiniteTroops = checked;
break;
case "single_modal.compact_map":
this.handleCompactMapChange(checked);
break;
case "single_modal.disable_alliances":
this.disableAlliances = checked;
break;
case "single_modal.water_nukes":
this.waterNukes = checked;
break;
default:
break;
}
};
private handleConfigUnitToggleChanged = (e: Event) => {
const customEvent = e as CustomEvent<{ unit: UnitType; checked: boolean }>;
const { unit, checked } = customEvent.detail;
this.disabledUnits = getUpdatedDisabledUnits(
this.disabledUnits,
unit,
checked,
);
};
private handleBotsChange = (e: Event) => {
const customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) {
return;
}
this.bots = value;
};
private handleNationsChange = (e: Event) => {
const customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) {
return;
}
this.nations = value;
};
private handleMaxTimerToggle = (
checked: boolean,
value: number | string | undefined,
) => {
this.maxTimer = checked;
this.maxTimerValue = toOptionalNumber(value);
};
private handleGoldMultiplierToggle = (
checked: boolean,
value: number | string | undefined,
) => {
this.goldMultiplier = checked;
this.goldMultiplierValue = toOptionalNumber(value);
};
private handleStartingGoldToggle = (
checked: boolean,
value: number | string | undefined,
) => {
this.startingGold = checked;
this.startingGoldValue = toOptionalNumber(value);
};
private handleMaxTimerValueKeyDown = (e: KeyboardEvent) => {
preventDisallowedKeys(e, ["-", "+", "e"]);
};
private getEndTimerInput(): HTMLInputElement | null {
return (
(this.renderRoot.querySelector(
"#end-timer-value",
) as HTMLInputElement | null) ??
(this.querySelector("#end-timer-value") as HTMLInputElement | null)
);
}
private handleMaxTimerValueChanges = (e: Event) => {
const input = e.target as HTMLInputElement;
const value = parseBoundedIntegerFromInput(input, {
min: 1,
max: 120,
stripPattern: /[e+-]/gi,
});
this.maxTimerValue = value;
};
private handleGoldMultiplierValueKeyDown = (e: KeyboardEvent) => {
preventDisallowedKeys(e, ["+", "-", "e", "E"]);
};
private handleGoldMultiplierValueChanges = (e: Event) => {
const input = e.target as HTMLInputElement;
const value = parseBoundedFloatFromInput(input, { min: 0.1, max: 1000 });
if (value === undefined) {
this.goldMultiplierValue = undefined;
input.value = "";
} else {
this.goldMultiplierValue = value;
}
};
private handleStartingGoldValueKeyDown = (e: KeyboardEvent) => {
preventDisallowedKeys(e, ["-", "+", "e", "E"]);
};
private handleStartingGoldValueChanges = (e: Event) => {
const input = e.target as HTMLInputElement;
const value = parseBoundedFloatFromInput(input, {
min: 0.1,
max: 1000,
});
if (value === undefined) {
this.startingGoldValue = undefined;
input.value = "";
} else {
this.startingGoldValue = value;
}
};
private handleGameModeSelection(value: GameMode) {
this.gameMode = value;
}
private handleTeamCountSelection(value: TeamCountConfig) {
this.teamCount = value;
}
private async startGame() {
// Validate and clamp maxTimer setting before starting
let finalMaxTimerValue: number | undefined = undefined;
if (this.maxTimer) {
if (!this.maxTimerValue || this.maxTimerValue <= 0) {
console.error("Max timer is enabled but no valid value is set");
alert(
translateText("single_modal.max_timer_invalid") ||
"Please enter a valid max timer value (1-120 minutes)",
);
// Focus the input
const input = this.getEndTimerInput();
if (input) {
input.focus();
input.select();
}
return;
}
// Clamp value to valid range
finalMaxTimerValue = Math.max(1, Math.min(120, this.maxTimerValue));
}
console.log(
`Starting single player game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]}${this.useRandomMap ? " (Randomly selected)" : ""}`,
);
const clientID = generateID();
const gameID = generateID();
const usernameInput = document.querySelector(
"username-input",
) as UsernameInput;
await crazyGamesSDK.requestMidgameAd();
this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: {
gameID: gameID,
gameStartInfo: {
gameID: gameID,
players: [
{
clientID,
username: usernameInput.getUsername(),
clanTag: usernameInput.getClanTag() ?? null,
cosmetics: await getPlayerCosmetics(),
},
],
config: {
gameMap: this.selectedMap,
gameMapSize: this.compactMap
? GameMapSize.Compact
: GameMapSize.Normal,
gameType: GameType.Singleplayer,
gameMode: this.gameMode,
playerTeams: this.teamCount,
difficulty: this.selectedDifficulty,
maxTimerValue: finalMaxTimerValue,
bots: this.bots,
infiniteGold: this.infiniteGold,
donateGold: this.gameMode === GameMode.Team,
donateTroops: this.gameMode === GameMode.Team,
infiniteTroops: this.infiniteTroops,
instantBuild: this.instantBuild,
randomSpawn: this.randomSpawn,
disabledUnits: this.disabledUnits
.map((u) => Object.values(UnitType).find((ut) => ut === u))
.filter((ut): ut is UnitType => ut !== undefined),
nations: sliderToNationsConfig(
this.nations,
this.defaultNationCount,
),
...(this.goldMultiplier && this.goldMultiplierValue
? { goldMultiplier: this.goldMultiplierValue }
: {}),
...(this.startingGold && this.startingGoldValue !== undefined
? {
startingGold: Math.round(
this.startingGoldValue * 1_000_000,
),
}
: {}),
...(this.disableAlliances ? { disableAlliances: true } : {}),
...(this.waterNukes ? { waterNukes: true } : {}),
},
lobbyCreatedAt: Date.now(), // ms; server should be authoritative in MP
},
source: "singleplayer",
} satisfies JoinLobbyEvent,
bubbles: true,
composed: true,
}),
);
this.close();
}
private async loadNationCount() {
const currentMap = this.selectedMap;
try {
const mapData = this.mapLoader.getMapData(currentMap);
const manifest = await mapData.manifest();
// Only update if the map hasn't changed
if (this.selectedMap === currentMap) {
this.defaultNationCount = manifest.nations.length;
this.nations = this.compactMap
? Math.max(0, Math.floor(manifest.nations.length * 0.25))
: manifest.nations.length;
}
} catch (error) {
console.warn("Failed to load nation count", error);
// Leave existing values unchanged so the UI stays consistent
}
}
}