Skip to content

Commit

Permalink
Map filters (#281)
Browse files Browse the repository at this point in the history
* Add map filters for max players, terrain, and game type

* fix linting
  • Loading branch information
CollinsSpencer authored Nov 21, 2024
1 parent 95a6fee commit 0fc9b4a
Show file tree
Hide file tree
Showing 42 changed files with 616 additions and 62 deletions.
8 changes: 7 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ export default [
rules: {
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-unused-expressions": "warn",
"@typescript-eslint/no-unused-expressions": [
"warn",
{
allowShortCircuit: true,
allowTernary: true,
},
],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-non-null-asserted-optional-chain": "warn",
"@typescript-eslint/no-unsafe-function-type": "warn",
Expand Down
51 changes: 49 additions & 2 deletions src/main/content/maps/map-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export interface MapMetadata {
positions: Record<string, { x: number; y: number }>;
team?: Team[];
};
tags: string[];
terrain: string[];
tags: GameType[];
terrain: Terrain[];
tidalStrength?: number;
windMax: number;
windMin: number;
Expand All @@ -42,3 +42,50 @@ export interface Team {
}[];
teamCount: number;
}

export type Terrain =
| "acidic"
| "alien"
| "ice"
| "lava"
| "space"
| "asteroid"
| "desert"
| "forests"
| "grassy"
| "industrial"
| "jungle"
| "metal"
| "ruins"
| "swamp"
| "tropical"
| "wasteland"
| "shallows"
| "sea"
| "island"
| "water"
| "chokepoints"
| "asymmetrical"
| "flat"
| "hills";

export type GameType =
| "1v1"
| "1v1v1"
| "2v2"
| "2v2v2"
| "2v2v2v2"
| "3v3"
| "3v3v3v3"
| "4v4"
| "4v4v4"
| "4v4v4v4"
| "5v5"
| "6v6"
| "6v6v6v6"
| "7v7"
| "7v7v7v7"
| "8v8"
| "8v8v8v8"
| "ffa"
| "pve";
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 18 additions & 12 deletions src/renderer/components/battle/MapListModal.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<template>
<Modal :title="title" style="height: 80vh; width: 1000px">
<MapListComponent @map-selected="mapSelected" />
<Modal :title="title" style="height: 80vh; width: 80vw; max-width: 1440px">
<div class="layout">
<div class="map-filters">
<MapFiltersComponent />
</div>
<MapListComponent @map-selected="mapSelected" />
</div>
</Modal>
</template>

<script lang="ts" setup>
import { MapData } from "@main/content/maps/map-data";
import Modal from "@renderer/components/common/Modal.vue";
import MapFiltersComponent from "@renderer/components/maps/MapFiltersComponent.vue";
import MapListComponent from "@renderer/components/maps/MapListComponent.vue";
defineProps<{
Expand All @@ -21,16 +27,16 @@ function mapSelected(map: MapData) {
</script>

<style lang="scss" scoped>
.map-list-modal {
width: 1440px;
height: 80vh;
.layout {
display: flex;
flex-direction: row;
gap: 20px;
height: 100%;
}
.container {
padding: 15px;
}
.map-list {
width: 1000px;
.map-filters {
width: 300px;
display: flex;
flex-direction: column;
gap: 20px;
}
</style>
2 changes: 1 addition & 1 deletion src/renderer/components/controls/Checkbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function onClick() {
align-items: center;
justify-content: center;
min-height: 33px;
max-width: 33px;
max-height: 33px;
max-width: 33px;
min-width: 33px;
}
Expand Down
71 changes: 49 additions & 22 deletions src/renderer/components/controls/Range.vue
Original file line number Diff line number Diff line change
@@ -1,46 +1,65 @@
<template>
<Control class="range">
<Slider ref="slider" v-bind="$attrs" :modelValue="modelValue" @update:model-value="onSlide" />
<InputNumber v-if="typeof modelValue === 'number'" v-bind="$attrs" :modelValue="modelValue" @update:model-value="onInput" />
<InputNumber
v-if="range"
v-bind="$attrs"
:modelValue="low"
@update:modelValue="(input: number) => onInput([input, high])"
class="min"
/>
<Slider v-bind="$props" :modelValue="modelValue" @update:modelValue="onSlide" />
<InputNumber
v-bind="$attrs"
:modelValue="typeof modelValue === 'number' ? modelValue : high"
@update:modelValue="typeof modelValue === 'number' ? onInput : (input: number) => onInput([low, input])"
class="max"
/>
<!-- <InputNumber
v-if="!range && typeof modelValue === 'number'"
v-bind="$attrs"
:modelValue="typeof modelValue === 'number' ? modelValue : high"
@update:modelValue="onInput"
class="max"
/>
<InputNumber
v-if="range"
v-bind="$attrs"
:modelValue="high"
@update:modelValue="(input: number) => onInput([low, input])"
class="max"
/> -->
</Control>
</template>

<script lang="ts" setup>
// https://primefaces.org/primevue/slider
import { computed } from "vue";
// https://v3.primevue.org/slider/
import InputNumber from "primevue/inputnumber";
import Slider, { SliderProps } from "primevue/slider";
import { computed, onMounted, Ref, ref } from "vue";
import Slider, { type SliderProps } from "primevue/slider";
import Control from "@renderer/components/controls/Control.vue";
export interface Props extends SliderProps {
modelValue: number | number[] | undefined;
}
export type Props = SliderProps;
defineProps<Props>();
const props = defineProps<Props>();
const emits = defineEmits<{
(event: "update:modelValue", value: number | number[]): void;
}>();
const slider: Ref<null | Props> = ref(null);
const max = ref(100);
const maxInputWidth = computed(() => `${max.value.toString().length + 1}ch`);
const low = computed(() => (props.modelValue instanceof Array ? props.modelValue[0] : null));
const high = computed(() => (props.modelValue instanceof Array ? props.modelValue[1] : null));
onMounted(() => {
if (slider.value?.max) {
max.value = slider.value?.max;
}
});
const min = computed<number>(() => props?.min ?? 0);
const minInputWidth = computed(() => `${min.value.toString().length + 1}ch`);
const max = computed<number>(() => props?.max ?? 100);
const maxInputWidth = computed(() => `${max.value.toString().length + 1}ch`);
function onSlide(input: number | number[]) {
emits("update:modelValue", input);
}
function onInput(input: number | number[]) {
if (typeof input === "number" && !Array.isArray(input)) {
emits("update:modelValue", input);
}
emits("update:modelValue", input);
}
</script>

Expand Down Expand Up @@ -85,7 +104,11 @@ function onInput(input: number | number[]) {
background-color: #fff;
}
}
:deep(.p-inputtext) {
.min :deep(.p-inputtext) {
width: v-bind(minInputWidth);
text-align: center;
}
.max :deep(.p-inputtext) {
width: v-bind(maxInputWidth);
text-align: center;
}
Expand All @@ -102,5 +125,9 @@ function onInput(input: number | number[]) {
top: 0;
background: rgba(255, 255, 255, 0.1);
}
&.min:before {
left: unset;
right: 0;
}
}
</style>
45 changes: 45 additions & 0 deletions src/renderer/components/maps/MapFiltersComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<div class="fullheight scroll-container">
<div class="flex-col gap-xl filters">
<div>
<h6>Max Players</h6>
<MaxPlayersFilter />
</div>
<div>
<h6>Terrain</h6>
<TerrainFilter />
</div>
<div>
<h6>Game Type</h6>
<GameTypeFilter />
</div>
</div>
</div>
</template>

<script lang="ts" setup>
/**
* Sort by:
* - name
* - size
* - ideal max player count
* - downloaded
* - date created or updated
*
* Filter by:
* - name (text search)
* - map size (min and max, as slider)
* - max players (min and max, as slider)
* - game type
* - terrain (general, water type, & layout)
*/
import GameTypeFilter from "@renderer/components/maps/filters/GameTypeFilter.vue";
import MaxPlayersFilter from "@renderer/components/maps/filters/MaxPlayersFilter.vue";
import TerrainFilter from "@renderer/components/maps/filters/TerrainFilter.vue";
</script>

<style lang="scss" scoped>
.filters {
padding-right: 10px;
}
</style>
32 changes: 25 additions & 7 deletions src/renderer/components/maps/MapListComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
<TransitionGroup name="maps-list">
<MapOverviewCard v-for="map in maps" :key="map.springName" :map="map" @click="mapSelected(map)" />
</TransitionGroup>
<div v-if="maps?.length <= 0">
<h4>No maps found!</h4>
<span>Please try different keywords / filters</span>
</div>
</div>
</div>
</div>
Expand All @@ -25,18 +29,21 @@
* - Easy one click install button
* - Demo map button that launches a simple offline game on the map
*/
import { Ref, ref } from "vue";
import SearchBox from "@renderer/components/controls/SearchBox.vue";
import Select from "@renderer/components/controls/Select.vue";
import MapOverviewCard from "@renderer/components/maps/MapOverviewCard.vue";
import { MapData } from "@main/content/maps/map-data";
import { type MapData } from "@main/content/maps/map-data";
import type { GameType, Terrain } from "@main/content/maps/map-metadata";
import { db } from "@renderer/store/db";
import { useDexieLiveQueryWithDeps } from "@renderer/composables/useDexieLiveQuery";
import { mapsStore } from "@renderer/store/maps.store";
import { useInfiniteScroll } from "@vueuse/core";
const { filters } = mapsStore;
type SortMethod = { label: string; dbKey: string };
const sortMethods: SortMethod[] = [
Expand All @@ -58,12 +65,23 @@ useInfiniteScroll(
},
{ distance: 300, interval: 550 }
);
const maps = useDexieLiveQueryWithDeps([searchVal, sortMethod, limit], () =>
db.maps
.filter((map) => map.displayName.toLocaleLowerCase().includes(searchVal.value.toLocaleLowerCase()))
const maps = useDexieLiveQueryWithDeps([searchVal, sortMethod, limit, filters], () => {
const { terrain, gameType } = filters;
const terrainFilters = new Set([...(<Terrain[]>Object.keys(terrain)).filter((key) => !!terrain[key]).map((k) => k)]);
const gameTypeFilters = new Set([...(<GameType[]>Object.keys(gameType)).filter((key) => gameType[key]).map((k) => k)]);
return db.maps
.filter(
(map) =>
map.displayName.toLocaleLowerCase().includes(searchVal.value.toLocaleLowerCase()) &&
filters.minPlayers < map.playerCountMax &&
filters.maxPlayers > map.playerCountMax &&
(terrainFilters.size === 0 || terrainFilters.isSubsetOf(new Set([...map.terrain]))) &&
(gameTypeFilters.size === 0 || !gameTypeFilters.isDisjointFrom(new Set([...map.tags])))
)
.limit(limit.value)
.sortBy(sortMethod.value.dbKey)
);
.sortBy(sortMethod.value.dbKey);
});
function mapSelected(map: MapData) {
emit("map-selected", map);
Expand Down
Loading

0 comments on commit 0fc9b4a

Please sign in to comment.