diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 3a8f5c48f..71e661842 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -1,17 +1,14 @@ -import { TemplateResult, html } from "lit"; +import { html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { translateText } from "../client/Utils"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { EventBus } from "../core/EventBus"; import { Difficulty, - Duos, GameMapSize, GameMapType, GameMode, HumansVsNations, - Quads, - Trios, UnitType, } from "../core/game/Game"; import { @@ -27,19 +24,23 @@ import { getPlayToken } from "./Auth"; import "./components/baseComponents/Modal"; import { BaseModal } from "./components/BaseModal"; import "./components/CopyButton"; -import "./components/Difficulties"; -import "./components/FluentSlider"; +import "./components/GameConfigSettings"; import "./components/LobbyPlayerView"; -import "./components/map/MapPicker"; +import "./components/ToggleInputCard"; import { modalHeader } from "./components/ui/ModalHeader"; import { crazyGamesSDK } from "./CrazyGamesSDK"; import { JoinLobbyEvent } from "./Main"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { - renderToggleInputCard, - renderToggleInputCardInput, -} from "./utilities/RenderToggleInputCard"; -import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; + getBotsForCompactMap, + getRandomMapType, + getUpdatedDisabledUnits, + parseBoundedFloatFromInput, + parseBoundedIntegerFromInput, + preventDisallowedKeys, + toOptionalNumber, +} from "./utilities/GameConfigHelpers"; + @customElement("host-lobby-modal") export class HostLobbyModal extends BaseModal { @state() private selectedMap: GameMapType = GameMapType.World; @@ -93,32 +94,6 @@ export class HostLobbyModal extends BaseModal { } }; - private renderOptionToggle( - labelKey: string, - checked: boolean, - onChange: (val: boolean) => void, - hidden: boolean = false, - ): TemplateResult { - if (hidden) return html``; - - return html` - - `; - } - private getRandomString(): string { const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; return Array.from( @@ -165,34 +140,73 @@ export class HostLobbyModal extends BaseModal { } render() { - const maxTimerHandlers = this.createToggleHandlers( - () => this.maxTimer, - (val) => (this.maxTimer = val), - () => this.maxTimerValue, - (val) => (this.maxTimerValue = val), - 30, - ); - const spawnImmunityHandlers = this.createToggleHandlers( - () => this.spawnImmunity, - (val) => (this.spawnImmunity = val), - () => this.spawnImmunityDurationMinutes, - (val) => (this.spawnImmunityDurationMinutes = val), - 5, - ); - const goldMultiplierHandlers = this.createToggleHandlers( - () => this.goldMultiplier, - (val) => (this.goldMultiplier = val), - () => this.goldMultiplierValue, - (val) => (this.goldMultiplierValue = val), - 2, - ); - const startingGoldHandlers = this.createToggleHandlers( - () => this.startingGold, - (val) => (this.startingGold = val), - () => this.startingGoldValue, - (val) => (this.startingGoldValue = val), - 5000000, - ); + const inputCards = [ + html``, + html``, + html``, + html``, + ]; const content = html`
-
-
- -
-
-
- - - -
-

- ${translateText("map.map")} -

-
- - this.handleMapSelection(mapValue)} - .onSelectRandom=${() => this.handleSelectRandomMap()} - > -
+
+ - -
-
-
- - - -
-

- ${translateText("difficulty.difficulty")} -

-
- -
- ${Object.entries(Difficulty) - .filter(([key]) => isNaN(Number(key))) - .map(([key, value]) => { - const isSelected = this.selectedDifficulty === value; - const isDisabled = this.disableNations; - return html` - - `; - })} -
-
- - -
-
-
- - - -
-

- ${translateText("host_modal.mode")} -

-
-
- ${[GameMode.FFA, GameMode.Team].map((mode) => { - const isSelected = this.gameMode === mode; - return html` - - `; - })} -
-
- - ${this.gameMode === GameMode.FFA - ? "" - : html` - -
-
- ${translateText("host_modal.team_count")} -
-
- ${[ - 2, - 3, - 4, - 5, - 6, - 7, - Quads, - Trios, - Duos, - HumansVsNations, - ].map((o) => { - const isSelected = this.teamCount === o; - return html` - - `; - })} -
-
- `} - - -
-
-
- - - -
-

- ${translateText("host_modal.options_title")} -

-
-
- -
- -
- - ${!( - this.gameMode === GameMode.Team && - this.teamCount === HumansVsNations - ) - ? this.renderOptionToggle( - "host_modal.disable_nations", - this.disableNations, - this.handleDisableNationsChange, - ) - : ""} - ${this.renderOptionToggle( - "host_modal.instant_build", - this.instantBuild, - this.handleInstantBuildChange, - )} - ${this.renderOptionToggle( - "host_modal.random_spawn", - this.randomSpawn, - this.handleRandomSpawnChange, - )} - ${this.renderOptionToggle( - "host_modal.donate_gold", - this.donateGold, - this.handleDonateGoldChange, - )} - ${this.renderOptionToggle( - "host_modal.donate_troops", - this.donateTroops, - this.handleDonateTroopsChange, - )} - ${this.renderOptionToggle( - "host_modal.infinite_gold", - this.infiniteGold, - this.handleInfiniteGoldChange, - )} - ${this.renderOptionToggle( - "host_modal.infinite_troops", - this.infiniteTroops, - this.handleInfiniteTroopsChange, - )} - ${this.renderOptionToggle( - "host_modal.compact_map", - this.compactMap, - this.handleCompactMapChange, - )} - - - ${renderToggleInputCard({ - labelKey: "host_modal.max_timer", - checked: this.maxTimer, - onClick: maxTimerHandlers.click, - input: renderToggleInputCardInput({ - min: 0, - max: 120, - value: this.maxTimerValue ?? 0, - ariaLabel: translateText("host_modal.max_timer"), - placeholder: translateText("host_modal.mins_placeholder"), - onInput: this.handleMaxTimerValueChanges, - onKeyDown: this.handleMaxTimerValueKeyDown, - }), - })} - - - ${renderToggleInputCard({ - labelKey: "host_modal.player_immunity_duration", - checked: this.spawnImmunity, - onClick: spawnImmunityHandlers.click, - input: renderToggleInputCardInput({ - min: 0, - max: 120, - step: 1, - value: this.spawnImmunityDurationMinutes ?? 0, - ariaLabel: translateText( - "host_modal.player_immunity_duration", - ), - placeholder: translateText("host_modal.mins_placeholder"), - onInput: this.handleSpawnImmunityDurationInput, - onKeyDown: this.handleSpawnImmunityDurationKeyDown, - }), - })} - - - ${renderToggleInputCard({ - labelKey: "single_modal.gold_multiplier", - checked: this.goldMultiplier, - onClick: goldMultiplierHandlers.click, - input: renderToggleInputCardInput({ - id: "gold-multiplier-value", - min: 0.1, - max: 1000, - step: "any", - value: this.goldMultiplierValue ?? "", - ariaLabel: translateText("single_modal.gold_multiplier"), - placeholder: translateText( - "single_modal.gold_multiplier_placeholder", - ), - onChange: this.handleGoldMultiplierValueChanges, - onKeyDown: this.handleGoldMultiplierValueKeyDown, - }), - })} - - - ${renderToggleInputCard({ - labelKey: "single_modal.starting_gold", - checked: this.startingGold, - onClick: startingGoldHandlers.click, - input: renderToggleInputCardInput({ - id: "starting-gold-value", - min: 0, - max: 1000000000, - step: 100000, - value: this.startingGoldValue ?? "", - ariaLabel: translateText("single_modal.starting_gold"), - placeholder: translateText( - "single_modal.starting_gold_placeholder", - ), - onInput: this.handleStartingGoldValueChanges, - onKeyDown: this.handleStartingGoldValueKeyDown, - }), - })} -
-
- - -
-
-
- - - -
-

- ${translateText("host_modal.enables_title")} -

-
-
- ${renderUnitTypeOptions({ - disabledUnits: this.disabledUnits, - toggleUnit: this.toggleUnit.bind(this), - })} -
-
- - - this.kickPlayer(clientID)} - > -
+ this.kickPlayer(clientID)} + >
@@ -700,40 +395,6 @@ export class HostLobbyModal extends BaseModal { this.loadNationCount(); } - private createToggleHandlers( - toggleStateGetter: () => boolean, - toggleStateSetter: (val: boolean) => void, - valueGetter: () => number | undefined, - valueSetter: (val: number | undefined) => void, - defaultValue: number = 0, - ) { - const toggleLogic = () => { - const newState = !toggleStateGetter(); - toggleStateSetter(newState); - if (newState) { - valueSetter(valueGetter() ?? defaultValue); - } else { - valueSetter(undefined); - } - this.putGameConfig(); - this.requestUpdate(); - }; - - return { - click: (e: Event) => { - if ((e.target as HTMLElement).tagName.toLowerCase() === "input") return; - toggleLogic(); - }, - keydown: (e: KeyboardEvent) => { - if ((e.target as HTMLElement).tagName.toLowerCase() === "input") return; - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - toggleLogic(); - } - }, - }; - } - private leaveLobby() { if (!this.lobbyId) { return; @@ -800,11 +461,15 @@ export class HostLobbyModal extends BaseModal { private async handleSelectRandomMap() { this.useRandomMap = true; - this.selectedMap = this.getRandomMap(); + this.selectedMap = getRandomMapType(); await this.loadNationCount(); this.putGameConfig(); } + private handleConfigRandomMapSelected = () => { + void this.handleSelectRandomMap(); + }; + private async handleMapSelection(value: GameMapType) { this.selectedMap = value; this.useRandomMap = false; @@ -812,13 +477,81 @@ export class HostLobbyModal extends BaseModal { 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.disable_nations": + void this.handleDisableNationsChange(checked); + break; + 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; + 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) { + private handleBotsChange = (e: Event) => { const customEvent = e as CustomEvent<{ value: number }>; const value = customEvent.detail.value; if (isNaN(value) || value < 0 || value > 400) { @@ -838,67 +571,94 @@ export class HostLobbyModal extends BaseModal { this.putGameConfig(); this.botsUpdateTimer = null; }, 300); - } + }; private handleInstantBuildChange = (val: boolean) => { this.instantBuild = val; this.putGameConfig(); }; - private handleSpawnImmunityDurationKeyDown(e: KeyboardEvent) { - if (["-", "+", "e", "E"].includes(e.key)) { - e.preventDefault(); - } - } + private handleMaxTimerToggle = ( + checked: boolean, + value: number | string | undefined, + ) => { + this.maxTimer = checked; + this.maxTimerValue = toOptionalNumber(value); + this.putGameConfig(); + }; - private handleSpawnImmunityDurationInput(e: Event) { + 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; - input.value = input.value.replace(/[eE+-]/g, ""); - const value = parseInt(input.value, 10); - if (Number.isNaN(value) || value < 0 || value > 120) { + const value = parseBoundedIntegerFromInput(input, { min: 0, max: 120 }); + if (value === undefined) { return; } this.spawnImmunityDurationMinutes = value; this.putGameConfig(); - } + }; - private handleGoldMultiplierValueKeyDown(e: KeyboardEvent) { - if (["+", "-", "e", "E"].includes(e.key)) { - e.preventDefault(); - } - } + private handleGoldMultiplierValueKeyDown = (e: KeyboardEvent) => { + preventDisallowedKeys(e, ["+", "-", "e", "E"]); + }; - private handleGoldMultiplierValueChanges(e: Event) { + private handleGoldMultiplierValueChanges = (e: Event) => { const input = e.target as HTMLInputElement; - const value = parseFloat(input.value); + const value = parseBoundedFloatFromInput(input, { min: 0.1, max: 1000 }); - if (isNaN(value) || value < 0.1 || value > 1000) { + if (value === undefined) { this.goldMultiplierValue = undefined; input.value = ""; } else { this.goldMultiplierValue = value; } this.putGameConfig(); - } + }; - private handleStartingGoldValueKeyDown(e: KeyboardEvent) { - if (["-", "+", "e", "E"].includes(e.key)) { - e.preventDefault(); - } - } + private handleStartingGoldValueKeyDown = (e: KeyboardEvent) => { + preventDisallowedKeys(e, ["-", "+", "e", "E"]); + }; - private handleStartingGoldValueChanges(e: Event) { + private handleStartingGoldValueChanges = (e: Event) => { const input = e.target as HTMLInputElement; - input.value = input.value.replace(/[eE+-]/g, ""); - const value = parseInt(input.value); + const value = parseBoundedIntegerFromInput(input, { + min: 0, + max: 1000000000, + }); - if (isNaN(value) || value < 0 || value > 1000000000) { - this.startingGoldValue = undefined; - } else { - this.startingGoldValue = value; - } + this.startingGoldValue = value; this.putGameConfig(); - } + }; private handleRandomSpawnChange = (val: boolean) => { this.randomSpawn = val; @@ -922,11 +682,7 @@ export class HostLobbyModal extends BaseModal { private handleCompactMapChange = (val: boolean) => { this.compactMap = val; - if (val && this.bots === 400) { - this.bots = 100; - } else if (!val && this.bots === 100) { - this.bots = 400; - } + this.bots = getBotsForCompactMap(this.bots, val); this.putGameConfig(); }; @@ -935,24 +691,24 @@ export class HostLobbyModal extends BaseModal { this.putGameConfig(); }; - private handleMaxTimerValueKeyDown(e: KeyboardEvent) { - if (["-", "+", "e"].includes(e.key)) { - e.preventDefault(); - } - } + private handleMaxTimerValueKeyDown = (e: KeyboardEvent) => { + preventDisallowedKeys(e, ["-", "+", "e"]); + }; - 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); + private handleMaxTimerValueChanges = (e: Event) => { + const input = e.target as HTMLInputElement; + const value = parseBoundedIntegerFromInput(input, { + min: 1, + max: 120, + stripPattern: /[e+-]/gi, + }); - if (isNaN(value) || value < 0 || value > 120) { + if (value === undefined) { return; } this.maxTimerValue = value; this.putGameConfig(); - } + }; private handleDisableNationsChange = async (val: boolean) => { this.disableNations = val; @@ -1029,20 +785,6 @@ export class HostLobbyModal extends BaseModal { ); } - private toggleUnit(unit: UnitType, checked: boolean): void { - this.disabledUnits = checked - ? [...this.disabledUnits, unit] - : this.disabledUnits.filter((u) => u !== unit); - - this.putGameConfig(); - } - - private getRandomMap(): GameMapType { - const maps = Object.values(GameMapType); - const randIdx = Math.floor(Math.random() * maps.length); - return maps[randIdx] as GameMapType; - } - private async startGame() { await this.putGameConfig(); console.log( diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 4f7bf279c..6a72726bc 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -4,14 +4,11 @@ import { translateText } from "../client/Utils"; import { UserMeResponse } from "../core/ApiSchemas"; import { Difficulty, - Duos, GameMapSize, GameMapType, GameMode, GameType, HumansVsNations, - Quads, - Trios, UnitType, } from "../core/game/Game"; import { UserSettings } from "../core/game/UserSettings"; @@ -21,9 +18,8 @@ import { hasLinkedAccount } from "./Api"; import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; import { BaseModal } from "./components/BaseModal"; -import "./components/Difficulties"; -import "./components/FluentSlider"; -import "./components/map/MapPicker"; +import "./components/GameConfigSettings"; +import "./components/ToggleInputCard"; import { modalHeader } from "./components/ui/ModalHeader"; import { fetchCosmetics } from "./Cosmetics"; import { crazyGamesSDK } from "./CrazyGamesSDK"; @@ -31,10 +27,14 @@ import { FlagInput } from "./FlagInput"; import { JoinLobbyEvent } from "./Main"; import { UsernameInput } from "./UsernameInput"; import { - renderToggleInputCard, - renderToggleInputCardInput, -} from "./utilities/RenderToggleInputCard"; -import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; + getBotsForCompactMap, + getRandomMapType, + getUpdatedDisabledUnits, + parseBoundedFloatFromInput, + parseBoundedIntegerFromInput, + preventDisallowedKeys, + toOptionalNumber, +} from "./utilities/GameConfigHelpers"; const DEFAULT_OPTIONS = { selectedMap: GameMapType.World, @@ -166,6 +166,60 @@ export class SinglePlayerModal extends BaseModal { } render() { + const inputCards = [ + html``, + html``, + html``, + ]; + const content = html`
-
-
- -
-
-
- - - -
-

- ${translateText("map.map")} -

-
- - - this.handleMapSelection(mapValue)} - .onSelectRandom=${() => this.handleSelectRandomMap()} - > -
- - -
-
-
- - - -
-

- ${translateText("difficulty.difficulty")} -

-
- -
- ${Object.entries(Difficulty) - .filter(([key]) => isNaN(Number(key))) - .map( - ([key, value]) => html` - - `, - )} -
-
- - -
-
-
- - - -
-

- ${translateText("host_modal.mode")} -

-
- -
- ${[GameMode.FFA, GameMode.Team].map((mode) => { - const isSelected = this.gameMode === mode; - const label = - mode === GameMode.FFA - ? translateText("game_mode.ffa") - : translateText("game_mode.teams"); - - return html` - - `; - })} -
-
- - ${this.gameMode === GameMode.FFA - ? "" - : html` - -
-
- ${translateText("host_modal.team_count")} -
-
- ${[ - 2, - 3, - 4, - 5, - 6, - 7, - Quads, - Trios, - Duos, - HumansVsNations, - ].map( - (o) => html` - - `, - )} -
-
- `} - - -
-
-
- - - -
-

- ${translateText("single_modal.options_title")} -

-
- -
- -
- -
- - ${this.renderOptionToggle( - "single_modal.disable_nations", - this.disableNations, - (val) => (this.disableNations = val), - this.gameMode === GameMode.Team && - this.teamCount === HumansVsNations, - )} - ${this.renderOptionToggle( - "single_modal.instant_build", - this.instantBuild, - (val) => (this.instantBuild = val), - )} - ${this.renderOptionToggle( - "single_modal.random_spawn", - this.randomSpawn, - (val) => (this.randomSpawn = val), - )} - ${this.renderOptionToggle( - "single_modal.infinite_gold", - this.infiniteGold, - (val) => (this.infiniteGold = val), - )} - ${this.renderOptionToggle( - "single_modal.infinite_troops", - this.infiniteTroops, - (val) => (this.infiniteTroops = val), - )} - ${this.renderOptionToggle( - "single_modal.compact_map", - this.compactMap, - (val) => { - this.compactMap = val; - if (val && this.bots === 400) { - this.bots = 100; - } else if (!val && this.bots === 100) { - this.bots = 400; - } +
+ { - this.maxTimer = !this.maxTimer; - if (!this.maxTimer) { - this.maxTimerValue = undefined; - } else { - // Set default value when enabling if not already set or invalid - if (!this.maxTimerValue || this.maxTimerValue <= 0) { - this.maxTimerValue = 30; - } - // Focus the input after render - setTimeout(() => { - const input = this.getEndTimerInput(); - if (input) { - input.focus(); - input.select(); - } - }, 0); - } + { + labelKey: "single_modal.instant_build", + checked: this.instantBuild, }, - input: renderToggleInputCardInput({ - id: "end-timer-value", - min: 1, - max: 120, - value: this.maxTimerValue ?? "", - ariaLabel: translateText("single_modal.max_timer"), - placeholder: translateText( - "single_modal.max_timer_placeholder", - ), - onInput: this.handleMaxTimerValueChanges, - onKeyDown: this.handleMaxTimerValueKeyDown, - }), - })} - - - ${renderToggleInputCard({ - labelKey: "single_modal.gold_multiplier", - checked: this.goldMultiplier, - onClick: () => { - this.goldMultiplier = !this.goldMultiplier; - if (!this.goldMultiplier) { - this.goldMultiplierValue = undefined; - } else { - if ( - !this.goldMultiplierValue || - this.goldMultiplierValue <= 0 - ) { - this.goldMultiplierValue = 2; - } - setTimeout(() => { - const input = this.renderRoot.querySelector( - "#gold-multiplier-value", - ) as HTMLInputElement; - if (input) { - input.focus(); - input.select(); - } - }, 0); - } + { + labelKey: "single_modal.random_spawn", + checked: this.randomSpawn, }, - input: renderToggleInputCardInput({ - id: "gold-multiplier-value", - min: 0.1, - max: 1000, - step: "any", - value: this.goldMultiplierValue ?? "", - ariaLabel: translateText("single_modal.gold_multiplier"), - placeholder: translateText( - "single_modal.gold_multiplier_placeholder", - ), - onChange: this.handleGoldMultiplierValueChanges, - onKeyDown: this.handleGoldMultiplierValueKeyDown, - }), - })} - - - ${renderToggleInputCard({ - labelKey: "single_modal.starting_gold", - checked: this.startingGold, - onClick: () => { - this.startingGold = !this.startingGold; - if (!this.startingGold) { - this.startingGoldValue = undefined; - } else { - if ( - !this.startingGoldValue || - this.startingGoldValue < 0 - ) { - this.startingGoldValue = 5000000; - } - setTimeout(() => { - const input = this.renderRoot.querySelector( - "#starting-gold-value", - ) as HTMLInputElement; - if (input) { - input.focus(); - input.select(); - } - }, 0); - } + { + labelKey: "single_modal.infinite_gold", + checked: this.infiniteGold, }, - input: renderToggleInputCardInput({ - id: "starting-gold-value", - min: 0, - max: 1000000000, - step: 100000, - value: this.startingGoldValue ?? "", - ariaLabel: translateText("single_modal.starting_gold"), - placeholder: translateText( - "single_modal.starting_gold_placeholder", - ), - onInput: this.handleStartingGoldValueChanges, - onKeyDown: this.handleStartingGoldValueKeyDown, - }), - })} -
-
- - -
-
-
- - - -
-

- ${translateText("single_modal.enables_title")} -

-
-
- ${renderUnitTypeOptions({ - disabledUnits: this.disabledUnits, - toggleUnit: this.toggleUnit.bind(this), - })} -
-
-
+ { + labelKey: "single_modal.infinite_troops", + checked: this.infiniteTroops, + }, + { + labelKey: "single_modal.compact_map", + checked: this.compactMap, + }, + ], + inputCards, + }, + unitTypes: { + titleKey: "single_modal.enables_title", + disabledUnits: this.disabledUnits, + }, + }} + @map-selected=${this.handleConfigMapSelected} + @random-map-selected=${this.handleConfigRandomMapSelected} + @difficulty-selected=${this.handleConfigDifficultySelected} + @game-mode-selected=${this.handleConfigGameModeSelected} + @team-count-selected=${this.handleConfigTeamCountSelected} + @bots-changed=${this.handleBotsChange} + @option-toggle-changed=${this.handleConfigOptionToggleChanged} + @unit-toggle-changed=${this.handleConfigUnitToggleChanged} + >
@@ -698,33 +383,6 @@ export class SinglePlayerModal extends BaseModal { ); } - // Helper for consistent option buttons - private renderOptionToggle( - labelKey: string, - checked: boolean, - onChange: (val: boolean) => void, - hidden: boolean = false, - ): TemplateResult { - if (hidden) return html``; - - return html` - - `; - } - protected onClose(): void { // Reset all transient form state to ensure clean slate this.selectedMap = DEFAULT_OPTIONS.selectedMap; @@ -752,29 +410,121 @@ export class SinglePlayerModal extends BaseModal { this.useRandomMap = true; } + private handleConfigRandomMapSelected = () => { + this.handleSelectRandomMap(); + }; + private handleMapSelection(value: GameMapType) { this.selectedMap = value; this.useRandomMap = false; } + private handleConfigMapSelected = (e: Event) => { + const customEvent = e as CustomEvent<{ map: GameMapType }>; + this.handleMapSelection(customEvent.detail.map); + }; + private handleDifficultySelection(value: Difficulty) { this.selectedDifficulty = value; } - private handleBotsChange(e: Event) { + 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); + } + + private handleConfigOptionToggleChanged = (e: Event) => { + const customEvent = e as CustomEvent<{ + labelKey: string; + checked: boolean; + }>; + const { labelKey, checked } = customEvent.detail; + + switch (labelKey) { + case "single_modal.disable_nations": + this.disableNations = checked; + break; + 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; + 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 handleMaxTimerValueKeyDown(e: KeyboardEvent) { - if (["-", "+", "e"].includes(e.key)) { - e.preventDefault(); - } - } + 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 ( @@ -785,55 +535,46 @@ export class SinglePlayerModal extends BaseModal { ); } - private handleMaxTimerValueChanges(e: Event) { + private handleMaxTimerValueChanges = (e: Event) => { const input = e.target as HTMLInputElement; - input.value = input.value.replace(/[e+-]/gi, ""); - const value = parseInt(input.value); + const value = parseBoundedIntegerFromInput(input, { + min: 1, + max: 120, + stripPattern: /[e+-]/gi, + }); - // Always update state to keep UI and internal state in sync - if (isNaN(value) || value < 1 || value > 120) { - // Set to undefined for invalid/empty/out-of-range values - this.maxTimerValue = undefined; - } else { - this.maxTimerValue = value; - } - } + this.maxTimerValue = value; + }; - private handleGoldMultiplierValueKeyDown(e: KeyboardEvent) { - if (["+", "-", "e", "E"].includes(e.key)) { - e.preventDefault(); - } - } + private handleGoldMultiplierValueKeyDown = (e: KeyboardEvent) => { + preventDisallowedKeys(e, ["+", "-", "e", "E"]); + }; - private handleGoldMultiplierValueChanges(e: Event) { + private handleGoldMultiplierValueChanges = (e: Event) => { const input = e.target as HTMLInputElement; - const value = parseFloat(input.value); + const value = parseBoundedFloatFromInput(input, { min: 0.1, max: 1000 }); - if (isNaN(value) || value < 0.1 || value > 1000) { + if (value === undefined) { this.goldMultiplierValue = undefined; input.value = ""; } else { this.goldMultiplierValue = value; } - } + }; - private handleStartingGoldValueKeyDown(e: KeyboardEvent) { - if (["-", "+", "e", "E"].includes(e.key)) { - e.preventDefault(); - } - } + private handleStartingGoldValueKeyDown = (e: KeyboardEvent) => { + preventDisallowedKeys(e, ["-", "+", "e", "E"]); + }; - private handleStartingGoldValueChanges(e: Event) { + private handleStartingGoldValueChanges = (e: Event) => { const input = e.target as HTMLInputElement; - input.value = input.value.replace(/[eE+-]/g, ""); - const value = parseInt(input.value); + const value = parseBoundedIntegerFromInput(input, { + min: 0, + max: 1000000000, + }); - if (isNaN(value) || value < 0 || value > 1000000000) { - this.startingGoldValue = undefined; - } else { - this.startingGoldValue = value; - } - } + this.startingGoldValue = value; + }; private handleGameModeSelection(value: GameMode) { this.gameMode = value; @@ -843,18 +584,6 @@ export class SinglePlayerModal extends BaseModal { 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 { - this.disabledUnits = checked - ? [...this.disabledUnits, unit] - : this.disabledUnits.filter((u) => u !== unit); - } - private async startGame() { // Validate and clamp maxTimer setting before starting let finalMaxTimerValue: number | undefined = undefined; @@ -879,7 +608,7 @@ export class SinglePlayerModal extends BaseModal { // If random map is selected, choose a random map now if (this.useRandomMap) { - this.selectedMap = this.getRandomMap(); + this.selectedMap = getRandomMapType(); } console.log( diff --git a/src/client/components/GameConfigSettings.ts b/src/client/components/GameConfigSettings.ts new file mode 100644 index 000000000..b981a89fe --- /dev/null +++ b/src/client/components/GameConfigSettings.ts @@ -0,0 +1,447 @@ +import { + LitElement, + SVGTemplateResult, + TemplateResult, + html, + nothing, + svg, +} from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { + Difficulty, + Duos, + GameMapType, + GameMode, + HumansVsNations, + Quads, + Trios, + UnitType, +} from "../../core/game/Game"; +import { TeamCountConfig } from "../../core/Schemas"; +import { translateText } from "../Utils"; +import "./Difficulties"; +import "./FluentSlider"; +import "./map/MapPicker"; + +const ACTIVE_CARD = + "bg-blue-500/20 border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.2)]"; +const INACTIVE_CARD = + "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"; + +const DISABLED_CARD = + "w-full rounded-xl border transition-all duration-200 opacity-30 grayscale cursor-not-allowed bg-white/5 border-white/5"; + +function cardClass(active: boolean, extra = ""): string { + return `w-full rounded-xl border cursor-pointer transition-all duration-200 active:scale-95 ${extra} ${active ? ACTIVE_CARD : INACTIVE_CARD}`; +} + +const CARD_LABEL_CLASS = + "text-xs uppercase font-bold tracking-wider leading-tight break-words hyphens-auto"; + +const DIFFICULTY_OPTIONS = Object.entries(Difficulty).filter(([key]) => + isNaN(Number(key)), +) as Array<[string, Difficulty]>; +const TEAM_COUNT_OPTIONS: TeamCountConfig[] = [ + 2, + 3, + 4, + 5, + 6, + 7, + Quads, + Trios, + Duos, + HumansVsNations, +]; + +function stateTextClass(active: boolean): string { + return active ? "text-white" : "text-white/60"; +} + +function renderTextCardButton( + label: string, + active: boolean, + onClick: () => void, + cardExtraClass: string, +): TemplateResult { + return html` + + `; +} + +function renderSection( + iconSvg: SVGTemplateResult, + colorClass: string, + bgClass: string, + titleKey: string, + content: TemplateResult | TemplateResult[], + sectionClass = "space-y-6", +): TemplateResult { + return html` +
+ ${renderSectionHeader(iconSvg, colorClass, bgClass, titleKey)} ${content} +
+ `; +} + +const unitOptions: { type: UnitType; translationKey: string }[] = [ + { type: UnitType.City, translationKey: "unit_type.city" }, + { type: UnitType.DefensePost, translationKey: "unit_type.defense_post" }, + { type: UnitType.Port, translationKey: "unit_type.port" }, + { type: UnitType.Warship, translationKey: "unit_type.warship" }, + { type: UnitType.MissileSilo, translationKey: "unit_type.missile_silo" }, + { type: UnitType.SAMLauncher, translationKey: "unit_type.sam_launcher" }, + { type: UnitType.AtomBomb, translationKey: "unit_type.atom_bomb" }, + { type: UnitType.HydrogenBomb, translationKey: "unit_type.hydrogen_bomb" }, + { type: UnitType.MIRV, translationKey: "unit_type.mirv" }, + { type: UnitType.Factory, translationKey: "unit_type.factory" }, +]; + +const MAP_ICON = svg``; + +const DIFFICULTY_ICON = svg``; + +const MODE_ICON = svg``; + +const OPTIONS_ICON = svg``; + +const ENABLES_ICON = svg``; + +function renderSectionHeader( + iconSvg: SVGTemplateResult, + colorClass: string, + bgClass: string, + titleKey: string, +): TemplateResult { + return html` +
+
+ + ${iconSvg} + +
+

+ ${translateText(titleKey)} +

+
+ `; +} + +export interface ToggleOptionConfig { + labelKey: string; + checked: boolean; + hidden?: boolean; +} + +export interface GameConfigSettingsData { + map: { + selected: GameMapType; + useRandom: boolean; + randomMapDivider?: boolean; + showMedals?: boolean; + mapWins?: Map>; + }; + difficulty: { + selected: Difficulty; + disabled: boolean; + }; + gameMode: { + selected: GameMode; + }; + teamCount: { + selected: TeamCountConfig; + }; + options: { + titleKey: string; + bots: { + value: number; + labelKey: string; + disabledKey: string; + }; + toggles: ToggleOptionConfig[]; + inputCards: TemplateResult[]; + }; + unitTypes: { + titleKey: string; + disabledUnits: UnitType[]; + }; +} + +@customElement("game-config-settings") +export class GameConfigSettings extends LitElement { + @property({ attribute: false }) settings?: GameConfigSettingsData; + @property({ attribute: false }) sectionGapClass = "space-y-6"; + + createRenderRoot() { + return this; + } + + private emit(name: string, detail: T) { + this.dispatchEvent( + new CustomEvent(name, { + detail, + bubbles: true, + composed: true, + }), + ); + } + + private handleSelectMap = (map: GameMapType) => { + this.emit("map-selected", { map }); + }; + + private handleSelectRandom = () => { + this.emit("random-map-selected", {}); + }; + + private handleDifficultySelect = (difficulty: Difficulty) => { + this.emit("difficulty-selected", { difficulty }); + }; + + private handleGameModeSelect = (mode: GameMode) => { + this.emit("game-mode-selected", { mode }); + }; + + private handleTeamCountSelect = (count: TeamCountConfig) => { + this.emit("team-count-selected", { count }); + }; + + private handleOptionToggle = (toggle: ToggleOptionConfig) => { + this.emit("option-toggle-changed", { + labelKey: toggle.labelKey, + checked: !toggle.checked, + }); + }; + + private handleBotsChanged = (event: Event) => { + const customEvent = event as CustomEvent<{ value: number }>; + this.emit("bots-changed", customEvent.detail); + }; + + private handleUnitToggle = (unit: UnitType, checked: boolean) => { + this.emit("unit-toggle-changed", { unit, checked }); + }; + + private renderOptionToggle(toggle: ToggleOptionConfig): TemplateResult { + if (toggle.hidden) return html``; + + return renderTextCardButton( + translateText(toggle.labelKey), + toggle.checked, + () => this.handleOptionToggle(toggle), + "p-4 text-center", + ); + } + + private renderUnitTypeOptions(disabledUnits: UnitType[]): TemplateResult[] { + return unitOptions.map(({ type, translationKey }) => { + const isEnabled = !disabledUnits.includes(type); + return html` + + `; + }); + } + + render() { + if (!this.settings) return nothing; + const settings = this.settings; + + return html` +
+ ${renderSection( + MAP_ICON, + "text-blue-400", + "bg-blue-500/20", + "map.map", + html``, + )} + ${renderSection( + DIFFICULTY_ICON, + "text-green-400", + "bg-green-500/20", + "difficulty.difficulty", + html` +
+ ${DIFFICULTY_OPTIONS.map(([key, value]) => { + const isSelected = settings.difficulty.selected === value; + const isDisabled = settings.difficulty.disabled; + return html` + + `; + })} +
+ `, + )} + ${renderSection( + MODE_ICON, + "text-purple-400", + "bg-purple-500/20", + "host_modal.mode", + html` +
+ ${[GameMode.FFA, GameMode.Team].map((mode) => { + const isSelected = settings.gameMode.selected === mode; + return html` + + `; + })} +
+ `, + )} + ${settings.gameMode.selected === GameMode.FFA + ? nothing + : html` +
+
+ ${translateText("host_modal.team_count")} +
+
+ ${TEAM_COUNT_OPTIONS.map((o) => { + const isSelected = settings.teamCount.selected === o; + return html` + + `; + })} +
+
+ `} + ${renderSection( + OPTIONS_ICON, + "text-orange-400", + "bg-orange-500/20", + settings.options.titleKey, + html` +
+
+ +
+ + ${settings.options.toggles.map((toggle) => + this.renderOptionToggle(toggle), + )} + ${settings.options.inputCards} +
+ `, + )} + ${renderSection( + ENABLES_ICON, + "text-teal-400", + "bg-teal-500/20", + settings.unitTypes.titleKey, + html` +
+ ${this.renderUnitTypeOptions(settings.unitTypes.disabledUnits)} +
+ `, + "space-y-6 pb-6", + )} +
+ `; + } +} diff --git a/src/client/components/ToggleInputCard.ts b/src/client/components/ToggleInputCard.ts new file mode 100644 index 000000000..279366aff --- /dev/null +++ b/src/client/components/ToggleInputCard.ts @@ -0,0 +1,173 @@ +import { LitElement, PropertyValues, html, nothing } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { translateText } from "../Utils"; + +const ACTIVE_CARD = + "bg-blue-500/20 border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.2)]"; +const INACTIVE_CARD = + "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"; +const INPUT_CLASS = + "w-full text-center rounded bg-black/60 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-blue-500 p-1 my-1"; +const CARD_LABEL_CLASS = + "text-xs uppercase font-bold tracking-wider leading-tight break-words hyphens-auto"; + +function cardClass(active: boolean, extra = ""): string { + return `w-full rounded-xl border cursor-pointer transition-all duration-200 active:scale-95 ${extra} ${active ? ACTIVE_CARD : INACTIVE_CARD}`; +} + +@customElement("toggle-input-card") +export class ToggleInputCard extends LitElement { + @property({ attribute: false }) labelKey = ""; + @property({ type: Boolean, attribute: false }) checked = false; + @property({ attribute: false }) inputId?: string; + @property({ attribute: false }) inputType = "number"; + @property({ attribute: false }) inputMin?: number | string; + @property({ attribute: false }) inputMax?: number | string; + @property({ attribute: false }) inputStep?: number | string; + @property({ attribute: false }) inputValue?: number | string; + @property({ attribute: false }) inputAriaLabel?: string; + @property({ attribute: false }) inputPlaceholder?: string; + @property({ attribute: false }) defaultInputValue?: number | string; + @property({ attribute: false }) minValidOnEnable?: number; + @property({ attribute: false }) onToggle?: ( + checked: boolean, + value: number | string | undefined, + ) => void; + @property({ attribute: false }) onInput?: (e: Event) => void; + @property({ attribute: false }) onChange?: (e: Event) => void; + @property({ attribute: false }) onKeyDown?: (e: KeyboardEvent) => void; + + createRenderRoot() { + return this; + } + + protected updated(changedProperties: PropertyValues) { + if (!changedProperties.has("checked")) return; + const previousChecked = changedProperties.get("checked"); + if (previousChecked === false && this.checked) { + const input = this.querySelector("input"); + if (input) { + input.focus(); + input.select(); + } + } + } + + private toOptionalNumber( + value: number | string | undefined, + ): number | undefined { + if (typeof value === "number") { + return Number.isFinite(value) ? value : undefined; + } + if (typeof value === "string") { + const trimmed = value.trim(); + if (!trimmed) return undefined; + const numeric = Number(trimmed); + return Number.isFinite(numeric) ? numeric : undefined; + } + return undefined; + } + + private resolveValueOnEnable(): number | string | undefined { + const currentValue = this.inputValue; + + if ( + currentValue === undefined || + currentValue === null || + currentValue === "" + ) { + return this.defaultInputValue; + } + + if (this.minValidOnEnable === undefined) { + return currentValue; + } + + const numericValue = this.toOptionalNumber(currentValue); + if (numericValue === undefined || numericValue < this.minValidOnEnable) { + return this.defaultInputValue; + } + + return numericValue; + } + + private emitToggle() { + const nextChecked = !this.checked; + const nextValue = nextChecked ? this.resolveValueOnEnable() : undefined; + this.onToggle?.(nextChecked, nextValue); + } + + private handleCardClick = () => { + this.emitToggle(); + }; + + render() { + return html` +
+ + + ${this.checked + ? html` +
+ +
+ ` + : nothing} +
+ `; + } +} diff --git a/src/client/components/map/MapDisplay.ts b/src/client/components/map/MapDisplay.ts index d843a9999..61241eff2 100644 --- a/src/client/components/map/MapDisplay.ts +++ b/src/client/components/map/MapDisplay.ts @@ -53,6 +53,10 @@ export class MapDisplay extends LitElement { } } + private preventImageDrag(event: DragEvent) { + event.preventDefault(); + } + render() { return html`
${this.isLoading ? html`
{ return this.mapWins?.get(mapValue) ?? new Set(); } @@ -54,7 +58,7 @@ export class MapPicker extends LitElement { return html`
this.handleMapSelection(mapValue)} - class="cursor-pointer transition-transform duration-200 active:scale-95" + class="cursor-pointer" >
diff --git a/src/client/utilities/GameConfigHelpers.ts b/src/client/utilities/GameConfigHelpers.ts new file mode 100644 index 000000000..a149dee88 --- /dev/null +++ b/src/client/utilities/GameConfigHelpers.ts @@ -0,0 +1,93 @@ +import { GameMapType, UnitType } from "../../core/game/Game"; + +export function toOptionalNumber( + value: number | string | undefined, +): number | undefined { + if (typeof value === "number") { + return Number.isFinite(value) ? value : undefined; + } + if (typeof value === "string") { + const trimmed = value.trim(); + if (!trimmed) return undefined; + const numeric = Number(trimmed); + return Number.isFinite(numeric) ? numeric : undefined; + } + return undefined; +} + +export function preventDisallowedKeys( + e: KeyboardEvent, + disallowedKeys: string[], +): void { + if (disallowedKeys.includes(e.key)) { + e.preventDefault(); + } +} + +export function parseBoundedIntegerFromInput( + input: HTMLInputElement, + { + min, + max, + stripPattern = /[eE+-]/g, + radix = 10, + }: { + min: number; + max: number; + stripPattern?: RegExp; + radix?: number; + }, +): number | undefined { + input.value = input.value.replace(stripPattern, ""); + const value = parseInt(input.value, radix); + + if (isNaN(value) || value < min || value > max) { + return undefined; + } + + return value; +} + +export function parseBoundedFloatFromInput( + input: HTMLInputElement, + { min, max }: { min: number; max: number }, +): number | undefined { + const value = parseFloat(input.value); + + if (isNaN(value) || value < min || value > max) { + return undefined; + } + + return value; +} + +export function getBotsForCompactMap( + bots: number, + compactMapEnabled: boolean, +): number { + if (compactMapEnabled && bots === 400) { + return 100; + } + + if (!compactMapEnabled && bots === 100) { + return 400; + } + + return bots; +} + +export function getRandomMapType(): GameMapType { + const maps = Object.values(GameMapType); + const randIdx = Math.floor(Math.random() * maps.length); + return maps[randIdx] as GameMapType; +} + +export function getUpdatedDisabledUnits( + disabledUnits: UnitType[], + unit: UnitType, + checked: boolean, +): UnitType[] { + return checked + ? [...disabledUnits, unit] + : disabledUnits.filter((u) => u !== unit); +} diff --git a/src/client/utilities/RenderToggleInputCard.ts b/src/client/utilities/RenderToggleInputCard.ts deleted file mode 100644 index 44fbd437e..000000000 --- a/src/client/utilities/RenderToggleInputCard.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { TemplateResult, html, nothing } from "lit"; -import { translateText } from "../Utils"; - -export const TOGGLE_INPUT_CARD_CLASSES = { - containerActive: - "bg-blue-500/20 border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.2)]", - containerInactive: - "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20 opacity-80", - labelBase: - "text-[10px] uppercase font-bold tracking-wider text-center w-full leading-tight break-words hyphens-auto", - labelActive: "text-white", - labelInactive: "text-white/60", - input: - "w-full text-center rounded bg-black/60 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-blue-500 p-1 my-1", -}; - -export interface ToggleInputCardInputOptions { - id?: string; - type?: string; - min?: number | string; - max?: number | string; - step?: number | string; - value?: number | string; - ariaLabel?: string; - placeholder?: string; - onInput?: (e: Event) => void; - onChange?: (e: Event) => void; - onKeyDown?: (e: KeyboardEvent) => void; - onClick?: (e: Event) => void; - className?: string; -} - -export function renderToggleInputCardInput({ - id, - type = "number", - min, - max, - step, - value, - ariaLabel, - placeholder, - onInput, - onChange, - onKeyDown, - onClick, - className = TOGGLE_INPUT_CARD_CLASSES.input, -}: ToggleInputCardInputOptions): TemplateResult { - const resolvedValue = value ?? ""; - const handleClick = onClick ?? ((e: Event) => e.stopPropagation()); - - return html` - - `; -} - -export interface ToggleInputCardRenderContext { - labelKey: string; - checked: boolean; - input?: TemplateResult; - onClick?: (e: Event) => void; - onKeyDown?: (e: KeyboardEvent) => void; - activeClassName?: string; - inactiveClassName?: string; - labelBaseClassName?: string; - labelActiveClassName?: string; - labelInactiveClassName?: string; - role?: string; - tabIndex?: number; -} - -export function renderToggleInputCard({ - labelKey, - checked, - input, - onClick, - onKeyDown, - activeClassName = TOGGLE_INPUT_CARD_CLASSES.containerActive, - inactiveClassName = TOGGLE_INPUT_CARD_CLASSES.containerInactive, - labelBaseClassName = TOGGLE_INPUT_CARD_CLASSES.labelBase, - labelActiveClassName = TOGGLE_INPUT_CARD_CLASSES.labelActive, - labelInactiveClassName = TOGGLE_INPUT_CARD_CLASSES.labelInactive, - role, - tabIndex, -}: ToggleInputCardRenderContext): TemplateResult { - const shouldBehaveLikeButton = Boolean(onClick ?? onKeyDown); - const resolvedRole = role ?? (shouldBehaveLikeButton ? "button" : undefined); - const resolvedTabIndex = tabIndex ?? (shouldBehaveLikeButton ? 0 : undefined); - const resolvedOnKeyDown = - onKeyDown ?? - (onClick - ? (e: KeyboardEvent) => { - if ((e.target as HTMLElement).tagName.toLowerCase() === "input") { - return; - } - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onClick(e); - } - } - : undefined); - - return html` -
-
-
- ${checked - ? html` - - ` - : ""} -
-
- - ${checked - ? (input ?? html``) - : html`
`} - -
- ${translateText(labelKey)} -
-
- `; -} diff --git a/src/client/utilities/RenderUnitTypeOptions.ts b/src/client/utilities/RenderUnitTypeOptions.ts deleted file mode 100644 index c04dea7f4..000000000 --- a/src/client/utilities/RenderUnitTypeOptions.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { html, TemplateResult } from "lit"; -import { UnitType } from "../../core/game/Game"; -import { translateText } from "../Utils"; - -export interface UnitTypeRenderContext { - disabledUnits: UnitType[]; - toggleUnit: (unit: UnitType, checked: boolean) => void; -} - -const unitOptions: { type: UnitType; translationKey: string }[] = [ - { type: UnitType.City, translationKey: "unit_type.city" }, - { type: UnitType.DefensePost, translationKey: "unit_type.defense_post" }, - { type: UnitType.Port, translationKey: "unit_type.port" }, - { type: UnitType.Warship, translationKey: "unit_type.warship" }, - { type: UnitType.MissileSilo, translationKey: "unit_type.missile_silo" }, - { type: UnitType.SAMLauncher, translationKey: "unit_type.sam_launcher" }, - { type: UnitType.AtomBomb, translationKey: "unit_type.atom_bomb" }, - { type: UnitType.HydrogenBomb, translationKey: "unit_type.hydrogen_bomb" }, - { type: UnitType.MIRV, translationKey: "unit_type.mirv" }, - { type: UnitType.Factory, translationKey: "unit_type.factory" }, -]; - -export function renderUnitTypeOptions({ - disabledUnits, - toggleUnit, -}: UnitTypeRenderContext): TemplateResult[] { - return unitOptions.map(({ type, translationKey }) => { - const isEnabled = !disabledUnits.includes(type); - return html` - - `; - }); -}