this.kickPlayer(clientID)}
>
`;
}
protected onOpen(): void {
this.startLobbyUpdates();
this.lobbyId = generateID();
// Note: clientID will be assigned by server when we join the lobby
// lobbyCreatorClientID stays empty until then
// Copy immediately so the host can share the link without waiting for the
// server. If lobby creation fails, clear the clipboard to avoid a dead link.
void this.constructUrl().then(async (url) => {
this.updateLobbyHistory(url);
await this.updateComplete;
void (this.querySelector("copy-button") as CopyButton)?.handleCopy();
});
// Pass auth token for creator identification (server extracts persistentID from it)
createLobby(this.lobbyId)
.then(async (lobby) => {
this.lobbyId = lobby.gameID;
if (!isValidGameID(this.lobbyId)) {
throw new Error(`Invalid lobby ID format: ${this.lobbyId}`);
}
crazyGamesSDK.showInviteButton(this.lobbyId);
})
.then(() => {
this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: {
gameID: this.lobbyId,
source: "host",
} as JoinLobbyEvent,
bubbles: true,
composed: true,
}),
);
})
.catch(() => {
// Clear clipboard so the host doesn't accidentally share a dead link
void navigator.clipboard.writeText("").catch(() => {});
});
if (this.modalEl) {
this.modalEl.onClose = () => {
this.close();
};
}
this.loadNationCount();
}
private leaveLobby() {
if (!this.lobbyId) {
return;
}
this.dispatchEvent(
new CustomEvent("leave-lobby", {
detail: { lobby: this.lobbyId },
bubbles: true,
composed: true,
}),
);
}
public confirmBeforeClose(): boolean {
return confirm(translateText("host_modal.leave_confirmation"));
}
protected onClose(): void {
console.log("Closing host lobby modal");
this.stopLobbyUpdates();
if (this.leaveLobbyOnClose) {
this.leaveLobby();
this.updateHistory("/"); // Reset URL to base
}
crazyGamesSDK.hideInviteButton();
// Clean up timers and resources
if (this.botsUpdateTimer !== null) {
clearTimeout(this.botsUpdateTimer);
this.botsUpdateTimer = null;
}
if (this.nationsUpdateTimer !== null) {
clearTimeout(this.nationsUpdateTimer);
this.nationsUpdateTimer = null;
}
// Reset all transient form state to ensure clean slate
this.selectedMap = GameMapType.World;
this.selectedDifficulty = Difficulty.Easy;
this.nations = 0;
this.defaultNationCount = 0;
this.gameMode = GameMode.FFA;
this.teamCount = 2;
this.bots = 400;
this.spawnImmunity = false;
this.spawnImmunityDurationMinutes = undefined;
this.infiniteGold = false;
this.donateGold = false;
this.infiniteTroops = false;
this.donateTroops = false;
this.maxTimer = false;
this.maxTimerValue = undefined;
this.startDelayValue = 3;
this.instantBuild = false;
this.randomSpawn = false;
this.compactMap = false;
this.useRandomMap = false;
this.disabledUnits = [];
this.lobbyId = "";
this.clients = [];
this.lobbyCreatorClientID = "";
this.goldMultiplier = false;
this.goldMultiplierValue = undefined;
this.startingGold = false;
this.startingGoldValue = undefined;
this.disableAlliances = false;
this.waterNukes = false;
this.hostCheatsEnabled = false;
this.hostCheatInfiniteGold = false;
this.hostCheatInfiniteTroops = false;
this.hostCheatGoldMultiplier = false;
this.hostCheatGoldMultiplierValue = undefined;
this.hostCheatStartingGold = false;
this.hostCheatStartingGoldValue = undefined;
this.leaveLobbyOnClose = true;
}
private async handleSelectRandomMap() {
this.useRandomMap = true;
this.selectedMap = getRandomMapType();
await this.loadNationCount();
this.putGameConfig();
}
private handleConfigRandomMapSelected = () => {
void this.handleSelectRandomMap();
};
private async handleMapSelection(value: GameMapType) {
this.selectedMap = value;
this.useRandomMap = false;
await this.loadNationCount();
this.putGameConfig();
}
private handleConfigMapSelected = (e: Event) => {
const customEvent = e as CustomEvent<{ map: GameMapType }>;
void this.handleMapSelection(customEvent.detail.map);
};
private async handleDifficultySelection(value: Difficulty) {
this.selectedDifficulty = value;
this.putGameConfig();
}
private handleConfigDifficultySelected = (e: Event) => {
const customEvent = e as CustomEvent<{ difficulty: Difficulty }>;
void this.handleDifficultySelection(customEvent.detail.difficulty);
};
private handleConfigGameModeSelected = (e: Event) => {
const customEvent = e as CustomEvent<{ mode: GameMode }>;
void this.handleGameModeSelection(customEvent.detail.mode);
};
private handleConfigTeamCountSelected = (e: Event) => {
const customEvent = e as CustomEvent<{ count: TeamCountConfig }>;
void this.handleTeamCountSelection(customEvent.detail.count);
};
private handleConfigOptionToggleChanged = (e: Event) => {
const customEvent = e as CustomEvent<{
labelKey: string;
checked: boolean;
}>;
const { labelKey, checked } = customEvent.detail;
switch (labelKey) {
case "host_modal.instant_build":
this.handleInstantBuildChange(checked);
break;
case "host_modal.random_spawn":
this.handleRandomSpawnChange(checked);
break;
case "host_modal.donate_gold":
this.handleDonateGoldChange(checked);
break;
case "host_modal.donate_troops":
this.handleDonateTroopsChange(checked);
break;
case "host_modal.infinite_gold":
this.handleInfiniteGoldChange(checked);
break;
case "host_modal.infinite_troops":
this.handleInfiniteTroopsChange(checked);
break;
case "host_modal.compact_map":
this.handleCompactMapChange(checked);
break;
case "host_modal.disable_alliances":
this.disableAlliances = checked;
this.putGameConfig();
break;
case "host_modal.water_nukes":
this.waterNukes = checked;
this.putGameConfig();
break;
case "host_modal.host_cheats":
this.hostCheatsEnabled = checked;
this.putGameConfig();
break;
default:
break;
}
};
private handleConfigHostCheatToggleChanged = (e: Event) => {
const customEvent = e as CustomEvent<{
labelKey: string;
checked: boolean;
}>;
const { labelKey, checked } = customEvent.detail;
switch (labelKey) {
case "host_modal.infinite_gold":
this.hostCheatInfiniteGold = checked;
this.putGameConfig();
break;
case "host_modal.infinite_troops":
this.hostCheatInfiniteTroops = checked;
this.putGameConfig();
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,
);
this.putGameConfig();
};
// Modified to include debouncing
private handleBotsChange = (e: Event) => {
const customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) {
return;
}
// Update the display value immediately
this.bots = value;
// Clear any existing timer
if (this.botsUpdateTimer !== null) {
clearTimeout(this.botsUpdateTimer);
}
// Set a new timer to call putGameConfig after 300ms of inactivity
this.botsUpdateTimer = window.setTimeout(() => {
this.putGameConfig();
this.botsUpdateTimer = null;
}, 300);
};
private handleInstantBuildChange = (val: boolean) => {
this.instantBuild = val;
this.putGameConfig();
};
private handleMaxTimerToggle = (
checked: boolean,
value: number | string | undefined,
) => {
this.maxTimer = checked;
this.maxTimerValue = toOptionalNumber(value);
this.putGameConfig();
};
private handleSpawnImmunityToggle = (
checked: boolean,
value: number | string | undefined,
) => {
this.spawnImmunity = checked;
this.spawnImmunityDurationMinutes = toOptionalNumber(value);
this.putGameConfig();
};
private handleGoldMultiplierToggle = (
checked: boolean,
value: number | string | undefined,
) => {
this.goldMultiplier = checked;
this.goldMultiplierValue = toOptionalNumber(value);
this.putGameConfig();
};
private handleStartingGoldToggle = (
checked: boolean,
value: number | string | undefined,
) => {
this.startingGold = checked;
this.startingGoldValue = toOptionalNumber(value);
this.putGameConfig();
};
private handleSpawnImmunityDurationKeyDown = (e: KeyboardEvent) => {
preventDisallowedKeys(e, ["-", "+", "e", "E"]);
};
private handleSpawnImmunityDurationInput = (e: Event) => {
const input = e.target as HTMLInputElement;
const value = parseBoundedIntegerFromInput(input, { min: 0, max: 120 });
if (value === undefined) {
return;
}
this.spawnImmunityDurationMinutes = value;
this.putGameConfig();
};
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;
}
this.putGameConfig();
};
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;
}
this.putGameConfig();
};
private handleHostCheatGoldMultiplierToggle = (
checked: boolean,
value: number | string | undefined,
) => {
this.hostCheatGoldMultiplier = checked;
this.hostCheatGoldMultiplierValue = toOptionalNumber(value);
this.putGameConfig();
};
private handleHostCheatGoldMultiplierValueKeyDown = (e: KeyboardEvent) => {
preventDisallowedKeys(e, ["+", "-", "e", "E"]);
};
private handleHostCheatGoldMultiplierValueChanges = (e: Event) => {
const input = e.target as HTMLInputElement;
const value = parseBoundedFloatFromInput(input, { min: 0.1, max: 1000 });
if (value === undefined) {
this.hostCheatGoldMultiplierValue = undefined;
input.value = "";
} else {
this.hostCheatGoldMultiplierValue = value;
}
this.putGameConfig();
};
private handleHostCheatStartingGoldToggle = (
checked: boolean,
value: number | string | undefined,
) => {
this.hostCheatStartingGold = checked;
this.hostCheatStartingGoldValue = toOptionalNumber(value);
this.putGameConfig();
};
private handleHostCheatStartingGoldValueKeyDown = (e: KeyboardEvent) => {
preventDisallowedKeys(e, ["-", "+", "e", "E"]);
};
private handleHostCheatStartingGoldValueChanges = (e: Event) => {
const input = e.target as HTMLInputElement;
const value = parseBoundedFloatFromInput(input, {
min: 0.1,
max: 1000,
});
if (value === undefined) {
this.hostCheatStartingGoldValue = undefined;
input.value = "";
} else {
this.hostCheatStartingGoldValue = value;
}
this.putGameConfig();
};
private handleRandomSpawnChange = (val: boolean) => {
this.randomSpawn = val;
this.putGameConfig();
};
private handleInfiniteGoldChange = (val: boolean) => {
this.infiniteGold = val;
this.putGameConfig();
};
private handleDonateGoldChange = (val: boolean) => {
this.donateGold = val;
this.putGameConfig();
};
private handleInfiniteTroopsChange = (val: boolean) => {
this.infiniteTroops = val;
this.putGameConfig();
};
private handleCompactMapChange = (val: boolean) => {
this.compactMap = val;
this.bots = getBotsForCompactMap(this.bots, val);
this.nations = getNationsForCompactMap(
this.nations,
this.defaultNationCount,
val,
);
this.putGameConfig();
};
private handleDonateTroopsChange = (val: boolean) => {
this.donateTroops = val;
this.putGameConfig();
};
private handleMaxTimerValueKeyDown = (e: KeyboardEvent) => {
preventDisallowedKeys(e, ["-", "+", "e"]);
};
private handleMaxTimerValueChanges = (e: Event) => {
const input = e.target as HTMLInputElement;
const value = parseBoundedIntegerFromInput(input, {
min: 1,
max: 120,
stripPattern: /[e+-]/gi,
});
if (value === undefined) {
return;
}
this.maxTimerValue = value;
this.putGameConfig();
};
private handleStartDelayValueKeyDown = (e: KeyboardEvent) => {
preventDisallowedKeys(e, ["-", "+", "e", "E", "."]);
};
private handleStartDelayValueChanges = (e: Event) => {
const input = e.target as HTMLInputElement;
const value = parseBoundedIntegerFromInput(input, {
min: 0,
max: 600,
});
if (value === undefined) {
this.startDelayValue = undefined;
input.value = "";
} else {
this.startDelayValue = value;
}
this.putGameConfig();
};
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;
if (this.nationsUpdateTimer !== null) {
clearTimeout(this.nationsUpdateTimer);
}
this.nationsUpdateTimer = window.setTimeout(() => {
this.putGameConfig();
this.nationsUpdateTimer = null;
}, 300);
};
private async handleGameModeSelection(value: GameMode) {
this.gameMode = value;
if (this.gameMode === GameMode.Team) {
this.donateGold = true;
this.donateTroops = true;
} else {
this.donateGold = false;
this.donateTroops = false;
}
this.putGameConfig();
}
private async handleTeamCountSelection(value: TeamCountConfig) {
this.teamCount = value;
this.putGameConfig();
}
private async putGameConfig() {
const spawnImmunityTicks = this.spawnImmunityDurationMinutes
? this.spawnImmunityDurationMinutes * 60 * 10
: 0;
const url = await this.constructUrl();
this.updateLobbyHistory(url);
this.dispatchEvent(
new CustomEvent("update-game-config", {
detail: {
config: {
gameMap: this.selectedMap,
gameMapSize: this.compactMap
? GameMapSize.Compact
: GameMapSize.Normal,
difficulty: this.selectedDifficulty,
bots: this.bots,
infiniteGold: this.infiniteGold,
donateGold: this.donateGold,
infiniteTroops: this.infiniteTroops,
donateTroops: this.donateTroops,
instantBuild: this.instantBuild,
randomSpawn: this.randomSpawn,
gameMode: this.gameMode,
disabledUnits: this.disabledUnits,
spawnImmunityDuration: this.spawnImmunity
? spawnImmunityTicks
: null,
playerTeams: this.teamCount,
nations: sliderToNationsConfig(
this.nations,
this.defaultNationCount,
),
maxTimerValue: this.maxTimer === true ? this.maxTimerValue : null,
startDelay: this.startDelayValue,
goldMultiplier:
this.goldMultiplier === true ? this.goldMultiplierValue : null,
startingGold:
this.startingGold === true && this.startingGoldValue !== undefined
? Math.round(this.startingGoldValue * 1_000_000)
: null,
disableAlliances: this.disableAlliances || null,
waterNukes: this.waterNukes ? true : null,
hostCheats: this.hostCheatsEnabled
? {
infiniteGold: this.hostCheatInfiniteGold || undefined,
infiniteTroops: this.hostCheatInfiniteTroops || undefined,
goldMultiplier:
this.hostCheatGoldMultiplier === true
? this.hostCheatGoldMultiplierValue
: null,
startingGold:
this.hostCheatStartingGold === true &&
this.hostCheatStartingGoldValue !== undefined
? Math.round(this.hostCheatStartingGoldValue * 1_000_000)
: null,
}
: undefined,
} satisfies Partial