import { LitElement, html } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import randomMap from "../../resources/images/RandomMap.webp"; import { translateText } from "../client/Utils"; import { Difficulty, Duos, GameMapSize, GameMapType, GameMode, GameType, HumansVsNations, Quads, Trios, UnitType, mapCategories, } from "../core/game/Game"; import { UserSettings } from "../core/game/UserSettings"; import { TeamCountConfig } from "../core/Schemas"; import { generateID } from "../core/Util"; import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; import "./components/Difficulties"; import "./components/Maps"; import { fetchCosmetics } from "./Cosmetics"; import { FlagInput } from "./FlagInput"; import { JoinLobbyEvent } from "./Main"; import { UsernameInput } from "./UsernameInput"; import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; @customElement("single-player-modal") export class SinglePlayerModal extends LitElement { @query("o-modal") private modalEl!: HTMLElement & { open: () => void; close: () => void; }; @state() private selectedMap: GameMapType = GameMapType.World; @state() private selectedDifficulty: Difficulty = Difficulty.Medium; @state() private disableNations: boolean = false; @state() private bots: number = 400; @state() private infiniteGold: boolean = false; @state() private infiniteTroops: boolean = false; @state() private compactMap: boolean = false; @state() private maxTimer: boolean = false; @state() private maxTimerValue: number | undefined = undefined; @state() private instantBuild: boolean = false; @state() private randomSpawn: boolean = false; @state() private useRandomMap: boolean = false; @state() private gameMode: GameMode = GameMode.FFA; @state() private teamCount: TeamCountConfig = 2; @state() private disabledUnits: UnitType[] = []; private userSettings: UserSettings = new UserSettings(); connectedCallback() { super.connectedCallback(); window.addEventListener("keydown", this.handleKeyDown); } disconnectedCallback() { window.removeEventListener("keydown", this.handleKeyDown); super.disconnectedCallback(); } private handleKeyDown = (e: KeyboardEvent) => { if (e.code === "Escape") { e.preventDefault(); this.close(); } }; render() { return html`
${translateText("map.map")}
${Object.entries(mapCategories).map( ([categoryKey, maps]) => html`

${translateText(`map_categories.${categoryKey}`)}

${maps.map((mapValue) => { const mapKey = Object.keys(GameMapType).find( (key) => GameMapType[key as keyof typeof GameMapType] === mapValue, ); return html`
this.handleMapSelection(mapValue)} >
`; })}
`, )}
Random Map
${translateText("map.random")}
${translateText("difficulty.difficulty")}
${Object.entries(Difficulty) .filter(([key]) => isNaN(Number(key))) .map( ([key, value]) => html`
this.handleDifficultySelection(value)} >

${translateText(`difficulty.${key.toLowerCase()}`)}

`, )}
${translateText("host_modal.mode")}
this.handleGameModeSelection(GameMode.FFA)} >
${translateText("game_mode.ffa")}
this.handleGameModeSelection(GameMode.Team)} >
${translateText("game_mode.teams")}
${this.gameMode === GameMode.FFA ? "" : html`
${translateText("host_modal.team_count")}
${[ 2, 3, 4, 5, 6, 7, Quads, Trios, Duos, HumansVsNations, ].map( (o) => html`
this.handleTeamCountSelection(o)} >
${typeof o === "string" ? o === HumansVsNations ? translateText("public_lobby.teams_hvn") : translateText(`host_modal.teams_${o}`) : translateText(`public_lobby.teams`, { num: o })}
`, )}
`}
${translateText("single_modal.options_title")}
${!( this.gameMode === GameMode.Team && this.teamCount === HumansVsNations ) ? html` ` : ""}

${translateText("single_modal.enables_title")}
${renderUnitTypeOptions({ disabledUnits: this.disabledUnits, toggleUnit: this.toggleUnit.bind(this), })}
`; } createRenderRoot() { return this; // light DOM } public open() { this.modalEl?.open(); this.useRandomMap = false; } public close() { this.modalEl?.close(); } private handleRandomMapToggle() { this.useRandomMap = true; } private handleMapSelection(value: GameMapType) { this.selectedMap = value; this.useRandomMap = false; } private handleDifficultySelection(value: Difficulty) { this.selectedDifficulty = value; } 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 handleRandomSpawnChange(e: Event) { this.randomSpawn = 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 handleCompactMapChange(e: Event) { this.compactMap = Boolean((e.target as HTMLInputElement).checked); } private handleMaxTimerValueKeyDown(e: KeyboardEvent) { if (["-", "+", "e"].includes(e.key)) { e.preventDefault(); } } private handleMaxTimerValueChanges(e: Event) { (e.target as HTMLInputElement).value = ( e.target as HTMLInputElement ).value.replace(/[e+-]/gi, ""); const value = parseInt((e.target as HTMLInputElement).value); if (isNaN(value) || value < 0 || value > 120) { return; } this.maxTimerValue = value; } private handleDisableNationsChange(e: Event) { this.disableNations = Boolean((e.target as HTMLInputElement).checked); } private handleGameModeSelection(value: GameMode) { this.gameMode = value; } private handleTeamCountSelection(value: TeamCountConfig) { this.teamCount = value; } private getRandomMap(): GameMapType { const maps = Object.values(GameMapType); const randIdx = Math.floor(Math.random() * maps.length); return maps[randIdx] as GameMapType; } private toggleUnit(unit: UnitType, checked: boolean): void { console.log(`Toggling unit type: ${unit} to ${checked}`); this.disabledUnits = checked ? [...this.disabledUnits, unit] : this.disabledUnits.filter((u) => u !== unit); } private async startGame() { // If random map is selected, choose a random map now if (this.useRandomMap) { this.selectedMap = this.getRandomMap(); } 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; if (!usernameInput) { console.warn("Username input element not found"); } const flagInput = document.querySelector("flag-input") as FlagInput; if (!flagInput) { console.warn("Flag input element not found"); } const cosmetics = await fetchCosmetics(); let selectedPattern = this.userSettings.getSelectedPatternName(cosmetics); selectedPattern ??= cosmetics ? (this.userSettings.getDevOnlyPattern() ?? null) : null; const selectedColor = this.userSettings.getSelectedColor(); this.dispatchEvent( new CustomEvent("join-lobby", { detail: { clientID: clientID, gameID: gameID, gameStartInfo: { gameID: gameID, players: [ { clientID, username: usernameInput.getCurrentUsername(), cosmetics: { flag: flagInput.getCurrentFlag() === "xx" ? "" : flagInput.getCurrentFlag(), pattern: selectedPattern ?? undefined, color: selectedColor ? { color: selectedColor } : undefined, }, }, ], config: { gameMap: this.selectedMap, gameMapSize: this.compactMap ? GameMapSize.Compact : GameMapSize.Normal, gameType: GameType.Singleplayer, gameMode: this.gameMode, playerTeams: this.teamCount, difficulty: this.selectedDifficulty, maxTimerValue: this.maxTimer ? this.maxTimerValue : undefined, bots: this.bots, infiniteGold: this.infiniteGold, donateGold: true, donateTroops: true, 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), ...(this.gameMode === GameMode.Team && this.teamCount === HumansVsNations ? { disableNations: false, } : { disableNations: this.disableNations, }), }, lobbyCreatedAt: Date.now(), // ms; server should be authoritative in MP }, } satisfies JoinLobbyEvent, bubbles: true, composed: true, }), ); this.close(); } }