mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-27 23:36:13 +00:00
Add map picker with Featured/All tabs (#3005)
Resolves #2996 ## Description: Replace map selection UI in src/client/SinglePlayerModal.ts and src/client/HostLobbyModal.ts with the picker (Featured/All tabs + random map card). Also, since the html was getting quite long, I extracted the shared parts into a separate component. <img width="575" height="592" alt="スクリーンショット 2026-01-23 21 57 03" src="https://github.com/user-attachments/assets/fc6bfbc3-cb66-452a-b971-436940b0fb99" /> <img width="633" height="648" alt="スクリーンショット 2026-01-23 21 57 12" src="https://github.com/user-attachments/assets/1aa409a1-b801-4a60-8b26-ba20e343d66e" /> I separated Map.ts because the display logic looked reusable in other places, but I’m also open to merging it back if that makes more sense. If the review prefers it integrated, I can combine them again. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri
This commit is contained in:
@@ -280,6 +280,8 @@
|
|||||||
},
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"map": "Map",
|
"map": "Map",
|
||||||
|
"featured": "Featured",
|
||||||
|
"all": "All",
|
||||||
"world": "World",
|
"world": "World",
|
||||||
"giantworldmap": "Giant World Map",
|
"giantworldmap": "Giant World Map",
|
||||||
"europe": "Europe",
|
"europe": "Europe",
|
||||||
@@ -330,6 +332,7 @@
|
|||||||
"amazonriver": "Amazon River"
|
"amazonriver": "Amazon River"
|
||||||
},
|
},
|
||||||
"map_categories": {
|
"map_categories": {
|
||||||
|
"featured": "Featured",
|
||||||
"continental": "Continental",
|
"continental": "Continental",
|
||||||
"regional": "Regional",
|
"regional": "Regional",
|
||||||
"fantasy": "Other",
|
"fantasy": "Other",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { customElement, state } from "lit/decorators.js";
|
|||||||
import { translateText } from "../client/Utils";
|
import { translateText } from "../client/Utils";
|
||||||
import { BaseModal } from "./components/BaseModal";
|
import { BaseModal } from "./components/BaseModal";
|
||||||
import "./components/Difficulties";
|
import "./components/Difficulties";
|
||||||
import "./components/Maps";
|
|
||||||
import { modalHeader } from "./components/ui/ModalHeader";
|
import { modalHeader } from "./components/ui/ModalHeader";
|
||||||
import { TroubleshootingModal } from "./TroubleshootingModal";
|
import { TroubleshootingModal } from "./TroubleshootingModal";
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
Quads,
|
Quads,
|
||||||
Trios,
|
Trios,
|
||||||
UnitType,
|
UnitType,
|
||||||
mapCategories,
|
|
||||||
} from "../core/game/Game";
|
} from "../core/game/Game";
|
||||||
import {
|
import {
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
@@ -28,7 +27,7 @@ import "./components/CopyButton";
|
|||||||
import "./components/Difficulties";
|
import "./components/Difficulties";
|
||||||
import "./components/FluentSlider";
|
import "./components/FluentSlider";
|
||||||
import "./components/LobbyPlayerView";
|
import "./components/LobbyPlayerView";
|
||||||
import "./components/Maps";
|
import "./components/map/MapPicker";
|
||||||
import { modalHeader } from "./components/ui/ModalHeader";
|
import { modalHeader } from "./components/ui/ModalHeader";
|
||||||
import { crazyGamesSDK } from "./CrazyGamesSDK";
|
import { crazyGamesSDK } from "./CrazyGamesSDK";
|
||||||
import { JoinLobbyEvent } from "./Main";
|
import { JoinLobbyEvent } from "./Main";
|
||||||
@@ -38,7 +37,6 @@ import {
|
|||||||
renderToggleInputCardInput,
|
renderToggleInputCardInput,
|
||||||
} from "./utilities/RenderToggleInputCard";
|
} from "./utilities/RenderToggleInputCard";
|
||||||
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
|
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
|
||||||
import randomMap from "/images/RandomMap.webp?url";
|
|
||||||
@customElement("host-lobby-modal")
|
@customElement("host-lobby-modal")
|
||||||
export class HostLobbyModal extends BaseModal {
|
export class HostLobbyModal extends BaseModal {
|
||||||
@state() private selectedMap: GameMapType = GameMapType.World;
|
@state() private selectedMap: GameMapType = GameMapType.World;
|
||||||
@@ -209,80 +207,14 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
${translateText("map.map")}
|
${translateText("map.map")}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-8">
|
<map-picker
|
||||||
<!-- Use the imported mapCategories -->
|
.selectedMap=${this.selectedMap}
|
||||||
${Object.entries(mapCategories).map(
|
.useRandomMap=${this.useRandomMap}
|
||||||
([categoryKey, maps]) => html`
|
.randomMapDivider=${true}
|
||||||
<div class="w-full">
|
.onSelectMap=${(mapValue: GameMapType) =>
|
||||||
<h4
|
this.handleMapSelection(mapValue)}
|
||||||
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
.onSelectRandom=${() => this.handleSelectRandomMap()}
|
||||||
>
|
></map-picker>
|
||||||
${translateText(`map_categories.${categoryKey}`)}
|
|
||||||
</h4>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
|
|
||||||
>
|
|
||||||
${maps.map((mapValue) => {
|
|
||||||
const mapKey = Object.entries(GameMapType).find(
|
|
||||||
([, v]) => v === mapValue,
|
|
||||||
)?.[0];
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
@click=${() => this.handleMapSelection(mapValue)}
|
|
||||||
class="cursor-pointer transition-transform duration-200 active:scale-95"
|
|
||||||
>
|
|
||||||
<map-display
|
|
||||||
.mapKey=${mapKey}
|
|
||||||
.selected=${!this.useRandomMap &&
|
|
||||||
this.selectedMap === mapValue}
|
|
||||||
.translation=${translateText(
|
|
||||||
`map.${mapKey?.toLowerCase()}`,
|
|
||||||
)}
|
|
||||||
></map-display>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
<!-- Random Map Card -->
|
|
||||||
<div class="w-full pt-4 border-t border-white/5">
|
|
||||||
<h4
|
|
||||||
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
|
||||||
>
|
|
||||||
${translateText("map_categories.special")}
|
|
||||||
</h4>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="relative group rounded-xl border transition-all duration-200 overflow-hidden flex flex-col items-stretch ${this
|
|
||||||
.useRandomMap
|
|
||||||
? "bg-blue-500/20 border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.3)]"
|
|
||||||
: "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"}"
|
|
||||||
@click=${this.handleSelectRandomMap}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="aspect-[2/1] w-full relative overflow-hidden bg-black/20"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src=${randomMap}
|
|
||||||
alt=${translateText("map.random")}
|
|
||||||
class="w-full h-full object-cover opacity-60 group-hover:opacity-100 transition-opacity"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="p-3 text-center border-t border-white/5">
|
|
||||||
<div
|
|
||||||
class="text-xs font-bold text-white uppercase tracking-wider break-words hyphens-auto"
|
|
||||||
>
|
|
||||||
${translateText("map.random")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Difficulty Selection -->
|
<!-- Difficulty Selection -->
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
Quads,
|
Quads,
|
||||||
Trios,
|
Trios,
|
||||||
UnitType,
|
UnitType,
|
||||||
mapCategories,
|
|
||||||
} from "../core/game/Game";
|
} from "../core/game/Game";
|
||||||
import { UserSettings } from "../core/game/UserSettings";
|
import { UserSettings } from "../core/game/UserSettings";
|
||||||
import { TeamCountConfig } from "../core/Schemas";
|
import { TeamCountConfig } from "../core/Schemas";
|
||||||
@@ -24,7 +23,7 @@ import "./components/baseComponents/Modal";
|
|||||||
import { BaseModal } from "./components/BaseModal";
|
import { BaseModal } from "./components/BaseModal";
|
||||||
import "./components/Difficulties";
|
import "./components/Difficulties";
|
||||||
import "./components/FluentSlider";
|
import "./components/FluentSlider";
|
||||||
import "./components/Maps";
|
import "./components/map/MapPicker";
|
||||||
import { modalHeader } from "./components/ui/ModalHeader";
|
import { modalHeader } from "./components/ui/ModalHeader";
|
||||||
import { fetchCosmetics } from "./Cosmetics";
|
import { fetchCosmetics } from "./Cosmetics";
|
||||||
import { FlagInput } from "./FlagInput";
|
import { FlagInput } from "./FlagInput";
|
||||||
@@ -35,7 +34,6 @@ import {
|
|||||||
renderToggleInputCardInput,
|
renderToggleInputCardInput,
|
||||||
} from "./utilities/RenderToggleInputCard";
|
} from "./utilities/RenderToggleInputCard";
|
||||||
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
|
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
|
||||||
import randomMap from "/images/RandomMap.webp?url";
|
|
||||||
|
|
||||||
@customElement("single-player-modal")
|
@customElement("single-player-modal")
|
||||||
export class SinglePlayerModal extends BaseModal {
|
export class SinglePlayerModal extends BaseModal {
|
||||||
@@ -197,84 +195,15 @@ export class SinglePlayerModal extends BaseModal {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-8">
|
<map-picker
|
||||||
${Object.entries(mapCategories).map(
|
.selectedMap=${this.selectedMap}
|
||||||
([categoryKey, maps]) => html`
|
.useRandomMap=${this.useRandomMap}
|
||||||
<div class="w-full">
|
.showMedals=${this.showAchievements}
|
||||||
<h4
|
.mapWins=${this.mapWins}
|
||||||
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
.onSelectMap=${(mapValue: GameMapType) =>
|
||||||
>
|
this.handleMapSelection(mapValue)}
|
||||||
${translateText(`map_categories.${categoryKey}`)}
|
.onSelectRandom=${() => this.handleSelectRandomMap()}
|
||||||
</h4>
|
></map-picker>
|
||||||
<div
|
|
||||||
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
|
|
||||||
>
|
|
||||||
${maps.map((mapValue) => {
|
|
||||||
const mapKey = Object.keys(GameMapType).find(
|
|
||||||
(key) =>
|
|
||||||
GameMapType[key as keyof typeof GameMapType] ===
|
|
||||||
mapValue,
|
|
||||||
);
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
@click=${() => this.handleMapSelection(mapValue)}
|
|
||||||
class="cursor-pointer transition-transform duration-200 active:scale-95"
|
|
||||||
>
|
|
||||||
<map-display
|
|
||||||
.mapKey=${mapKey}
|
|
||||||
.selected=${!this.useRandomMap &&
|
|
||||||
this.selectedMap === mapValue}
|
|
||||||
.showMedals=${this.showAchievements}
|
|
||||||
.wins=${this.mapWins.get(mapValue) ?? new Set()}
|
|
||||||
.translation=${translateText(
|
|
||||||
`map.${mapKey?.toLowerCase()}`,
|
|
||||||
)}
|
|
||||||
></map-display>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
|
|
||||||
<!-- Random Map Card -->
|
|
||||||
<div class="w-full">
|
|
||||||
<h4
|
|
||||||
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
|
||||||
>
|
|
||||||
${translateText("map_categories.special")}
|
|
||||||
</h4>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="relative group rounded-xl border transition-all duration-200 overflow-hidden flex flex-col items-stretch ${this
|
|
||||||
.useRandomMap
|
|
||||||
? "bg-blue-500/20 border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.3)]"
|
|
||||||
: "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"}"
|
|
||||||
@click=${this.handleSelectRandomMap}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="aspect-[2/1] w-full relative overflow-hidden bg-black/20"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src=${randomMap}
|
|
||||||
alt=${translateText("map.random")}
|
|
||||||
class="w-full h-full object-cover opacity-60 group-hover:opacity-100 transition-opacity"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="p-3 text-center border-t border-white/5">
|
|
||||||
<div
|
|
||||||
class="text-xs font-bold text-white uppercase tracking-wider break-words hyphens-auto"
|
|
||||||
>
|
|
||||||
${translateText("map.random")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Difficulty Selection -->
|
<!-- Difficulty Selection -->
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { LitElement, html } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import { Difficulty, GameMapType } from "../../core/game/Game";
|
import { Difficulty, GameMapType } from "../../../core/game/Game";
|
||||||
import { terrainMapFileLoader } from "../TerrainMapFileLoader";
|
import { terrainMapFileLoader } from "../../TerrainMapFileLoader";
|
||||||
import { translateText } from "../Utils";
|
import { translateText } from "../../Utils";
|
||||||
|
|
||||||
@customElement("map-display")
|
@customElement("map-display")
|
||||||
export class MapDisplay extends LitElement {
|
export class MapDisplay extends LitElement {
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
import {
|
||||||
|
Difficulty,
|
||||||
|
GameMapType,
|
||||||
|
mapCategories,
|
||||||
|
} from "../../../core/game/Game";
|
||||||
|
import { translateText } from "../../Utils";
|
||||||
|
import "./MapDisplay";
|
||||||
|
import randomMap from "/images/RandomMap.webp?url";
|
||||||
|
|
||||||
|
const featuredMaps: GameMapType[] = [
|
||||||
|
GameMapType.World,
|
||||||
|
GameMapType.Europe,
|
||||||
|
GameMapType.NorthAmerica,
|
||||||
|
GameMapType.SouthAmerica,
|
||||||
|
GameMapType.Asia,
|
||||||
|
GameMapType.Africa,
|
||||||
|
GameMapType.Japan,
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("map-picker")
|
||||||
|
export class MapPicker extends LitElement {
|
||||||
|
@property({ type: String }) selectedMap: GameMapType = GameMapType.World;
|
||||||
|
@property({ type: Boolean }) useRandomMap = false;
|
||||||
|
@property({ type: Boolean }) showMedals = false;
|
||||||
|
@property({ type: Boolean }) randomMapDivider = false;
|
||||||
|
@property({ attribute: false }) mapWins: Map<GameMapType, Set<Difficulty>> =
|
||||||
|
new Map();
|
||||||
|
@property({ attribute: false }) onSelectMap?: (map: GameMapType) => void;
|
||||||
|
@property({ attribute: false }) onSelectRandom?: () => void;
|
||||||
|
@state() private showAllMaps = false;
|
||||||
|
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMapSelection(mapValue: GameMapType) {
|
||||||
|
this.onSelectMap?.(mapValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSelectRandomMap = () => {
|
||||||
|
this.onSelectRandom?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
private getWins(mapValue: GameMapType): Set<Difficulty> {
|
||||||
|
return this.mapWins?.get(mapValue) ?? new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderMapCard(mapValue: GameMapType) {
|
||||||
|
const mapKey = Object.entries(GameMapType).find(
|
||||||
|
([_, value]) => value === mapValue,
|
||||||
|
)?.[0];
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
@click=${() => this.handleMapSelection(mapValue)}
|
||||||
|
class="cursor-pointer transition-transform duration-200 active:scale-95"
|
||||||
|
>
|
||||||
|
<map-display
|
||||||
|
.mapKey=${mapKey}
|
||||||
|
.selected=${!this.useRandomMap && this.selectedMap === mapValue}
|
||||||
|
.showMedals=${this.showMedals}
|
||||||
|
.wins=${this.getWins(mapValue)}
|
||||||
|
.translation=${translateText(`map.${mapKey?.toLowerCase()}`)}
|
||||||
|
></map-display>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderAllMaps() {
|
||||||
|
const mapCategoryEntries = Object.entries(mapCategories);
|
||||||
|
return html`<div class="space-y-8">
|
||||||
|
${mapCategoryEntries.map(
|
||||||
|
([categoryKey, maps]) => html`
|
||||||
|
<div class="w-full">
|
||||||
|
<h4
|
||||||
|
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
||||||
|
>
|
||||||
|
${translateText(`map_categories.${categoryKey}`)}
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
${maps.map((mapValue) => this.renderMapCard(mapValue))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFeaturedMaps() {
|
||||||
|
let featuredMapList = featuredMaps;
|
||||||
|
if (!featuredMapList.includes(this.selectedMap)) {
|
||||||
|
featuredMapList = [this.selectedMap, ...featuredMaps];
|
||||||
|
}
|
||||||
|
return html`<div class="w-full">
|
||||||
|
<h4
|
||||||
|
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
||||||
|
>
|
||||||
|
${translateText("map_categories.featured")}
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
${featuredMapList.map((mapValue) => this.renderMapCard(mapValue))}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div class="w-full">
|
||||||
|
<div
|
||||||
|
role="tablist"
|
||||||
|
aria-label="${translateText("map.map")}"
|
||||||
|
class="grid grid-cols-2 gap-2 rounded-xl border border-white/10 bg-black/20 p-1"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected=${!this.showAllMaps}
|
||||||
|
class="px-3 py-2 rounded-lg text-xs font-bold uppercase tracking-wider transition-all ${this
|
||||||
|
.showAllMaps
|
||||||
|
? "text-white/60 hover:text-white"
|
||||||
|
: "bg-blue-500/20 text-blue-100 shadow-[0_0_12px_rgba(59,130,246,0.2)]"}"
|
||||||
|
@click=${() => (this.showAllMaps = false)}
|
||||||
|
>
|
||||||
|
${translateText("map.featured")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected=${this.showAllMaps}
|
||||||
|
class="px-3 py-2 rounded-lg text-xs font-bold uppercase tracking-wider transition-all ${this
|
||||||
|
.showAllMaps
|
||||||
|
? "bg-blue-500/20 text-blue-100 shadow-[0_0_12px_rgba(59,130,246,0.2)]"
|
||||||
|
: "text-white/60 hover:text-white"}"
|
||||||
|
@click=${() => (this.showAllMaps = true)}
|
||||||
|
>
|
||||||
|
${translateText("map.all")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${this.showAllMaps ? this.renderAllMaps() : this.renderFeaturedMaps()}
|
||||||
|
<div
|
||||||
|
class="w-full ${this.randomMapDivider
|
||||||
|
? "pt-4 border-t border-white/5"
|
||||||
|
: ""}"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
||||||
|
>
|
||||||
|
${translateText("map_categories.special")}
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
<button
|
||||||
|
class="relative group rounded-xl border transition-all duration-200 overflow-hidden flex flex-col items-stretch ${this
|
||||||
|
.useRandomMap
|
||||||
|
? "bg-blue-500/20 border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.3)]"
|
||||||
|
: "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"}"
|
||||||
|
@click=${this.handleSelectRandomMap}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="aspect-[2/1] w-full relative overflow-hidden bg-black/20"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src=${randomMap}
|
||||||
|
alt=${translateText("map.random")}
|
||||||
|
class="w-full h-full object-cover opacity-60 group-hover:opacity-100 transition-opacity"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 text-center border-t border-white/5">
|
||||||
|
<div
|
||||||
|
class="text-xs font-bold text-white uppercase tracking-wider break-words hyphens-auto"
|
||||||
|
>
|
||||||
|
${translateText("map.random")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user