Add basic deck builder #12
3 changed files with 331 additions and 87 deletions
|
@ -1,5 +1,6 @@
|
||||||
import Dexie from "dexie";
|
import Dexie from "dexie";
|
||||||
import { Card, CardFilter } from "./types";
|
import { Card, CardFilter } from "./types";
|
||||||
|
import { cardFullName } from "./card";
|
||||||
|
|
||||||
class CardDatabase extends Dexie {
|
class CardDatabase extends Dexie {
|
||||||
public cards: Dexie.Table<Card, string>;
|
public cards: Dexie.Table<Card, string>;
|
||||||
|
@ -43,95 +44,91 @@ export async function getCards(filter: CardFilter) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await query
|
const results = query.filter(x => {
|
||||||
.filter(x => {
|
if (filter.Name) {
|
||||||
if (filter.Name) {
|
if (
|
||||||
if (
|
!cardFullName(x)
|
||||||
!`${x.Name}, ${x.Subname}`
|
.toLowerCase()
|
||||||
.toLowerCase()
|
.includes(filter.Name.toLowerCase())
|
||||||
.includes(filter.Name.toLowerCase())
|
) {
|
||||||
) {
|
return false;
|
||||||
return false;
|
}
|
||||||
|
}
|
||||||
|
if (filter.Rules) {
|
||||||
|
if (
|
||||||
|
!`${x.Keywords.join(" ~ ")} ~ ${x.Text}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(filter.Rules.toLowerCase())
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filter.Traits && filter.Traits.length > 0) {
|
||||||
|
let found = false;
|
||||||
|
for (const trait of x.Traits) {
|
||||||
|
if (filter.Traits.includes(trait)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filter.Rules) {
|
if (!found) {
|
||||||
if (
|
return false;
|
||||||
!`${x.Keywords.join(" ~ ")} ~ ${x.Text}`
|
}
|
||||||
.toLowerCase()
|
}
|
||||||
.includes(filter.Rules.toLowerCase())
|
if (filter.Sets && filter.Sets.length > 0) {
|
||||||
) {
|
if (!filter.Sets.includes(x.Set)) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filter.Types && filter.Types.length > 0) {
|
||||||
|
if (!filter.Types.includes(x.Type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filter.Elements && filter.Elements.length > 0) {
|
||||||
|
let found = false;
|
||||||
|
for (const element of x.Element) {
|
||||||
|
if (filter.Elements.includes(element)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filter.Traits && filter.Traits.length > 0) {
|
if (x.Requirement) {
|
||||||
let found = false;
|
for (const element in x.Requirement) {
|
||||||
for (const trait of x.Traits) {
|
|
||||||
if (filter.Traits.includes(trait)) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filter.Sets && filter.Sets.length > 0) {
|
|
||||||
if (!filter.Sets.includes(x.Set)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filter.Types && filter.Types.length > 0) {
|
|
||||||
if (!filter.Types.includes(x.Type)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filter.Elements && filter.Elements.length > 0) {
|
|
||||||
let found = false;
|
|
||||||
for (const element of x.Element) {
|
|
||||||
if (filter.Elements.includes(element)) {
|
if (filter.Elements.includes(element)) {
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (x.Requirement) {
|
}
|
||||||
for (const element in x.Requirement) {
|
if (x.ProblemRequirement) {
|
||||||
if (filter.Elements.includes(element)) {
|
for (const element in x.ProblemRequirement) {
|
||||||
found = true;
|
if (filter.Elements.includes(element)) {
|
||||||
break;
|
found = true;
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (x.ProblemRequirement) {
|
|
||||||
for (const element in x.ProblemRequirement) {
|
|
||||||
if (filter.Elements.includes(element)) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (filter.Powers && filter.Powers.length > 0) {
|
if (!found) {
|
||||||
if (
|
return false;
|
||||||
typeof x.Power === "undefined" ||
|
|
||||||
!filter.Powers.includes(x.Power)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (filter.Costs && filter.Costs.length > 0) {
|
}
|
||||||
if (typeof x.Cost === "undefined" || !filter.Costs.includes(x.Cost)) {
|
if (filter.Powers && filter.Powers.length > 0) {
|
||||||
return false;
|
if (typeof x.Power === "undefined" || !filter.Powers.includes(x.Power)) {
|
||||||
}
|
return false;
|
||||||
}
|
}
|
||||||
if (filter.Rarities && filter.Rarities.length > 0) {
|
}
|
||||||
if (!filter.Rarities.includes(x.Rarity)) {
|
if (filter.Costs && filter.Costs.length > 0) {
|
||||||
return false;
|
if (typeof x.Cost === "undefined" || !filter.Costs.includes(x.Cost)) {
|
||||||
}
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
}
|
||||||
})
|
if (filter.Rarities && filter.Rarities.length > 0) {
|
||||||
.toArray();
|
if (!filter.Rarities.includes(x.Rarity)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return await results.toArray();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { SetFile } from "./types";
|
||||||
import { Database } from "./database";
|
import { Database } from "./database";
|
||||||
|
|
||||||
const baseURL = "https://mcg.zyg.ovh/setdata/";
|
const baseURL = "https://mcg.zyg.ovh/setdata/";
|
||||||
const allSets = [
|
export const allSets = [
|
||||||
"PR",
|
"PR",
|
||||||
"CN",
|
"CN",
|
||||||
"RR",
|
"RR",
|
||||||
|
|
|
@ -1,7 +1,31 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="deckbuilder">
|
<section class="deckbuilder">
|
||||||
<section class="cardlist">
|
<section class="cardlist">
|
||||||
<section class="filters"></section>
|
<section class="filters">
|
||||||
|
<div class="row">
|
||||||
|
<b-input
|
||||||
|
@input="nameChanged"
|
||||||
|
v-model="nameFilter"
|
||||||
|
placeholder="Search name"
|
||||||
|
></b-input>
|
||||||
|
<div class="colorfilter" v-for="color in colors" :key="color">
|
||||||
|
<img
|
||||||
|
@click="toggleElementFilter(color)"
|
||||||
|
:class="elementIconClass(color)"
|
||||||
|
:src="elementIconURL(color)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="setfilter" v-for="set in sets" :key="set">
|
||||||
|
<img
|
||||||
|
@click="toggleSetFilter(set)"
|
||||||
|
:class="setIconClass(set)"
|
||||||
|
:src="setIconURL(set)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<section class="cards">
|
<section class="cards">
|
||||||
<CardPicker :columns="columns" :rows="rows" :cards="currentPage" />
|
<CardPicker :columns="columns" :rows="rows" :cards="currentPage" />
|
||||||
</section>
|
</section>
|
||||||
|
@ -24,22 +48,173 @@
|
||||||
.cardlist {
|
.cardlist {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-template-rows: 100px 1fr;
|
grid-template-rows: 110px 1fr;
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5px;
|
||||||
|
.row {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
* {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.decklist {
|
.decklist {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
section {
|
}
|
||||||
border: 1px solid red;
|
|
||||||
|
.colorfilter,
|
||||||
|
.setfilter {
|
||||||
|
cursor: pointer;
|
||||||
|
img {
|
||||||
|
opacity: 0.4;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
&.selected {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.colorfilter {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setfilter {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
import DeckList from "@/components/DeckBuilder/DeckList.vue";
|
import DeckList from "@/components/DeckBuilder/DeckList.vue";
|
||||||
import CardPicker from "@/components/DeckBuilder/CardPicker.vue";
|
import CardPicker from "@/components/DeckBuilder/CardPicker.vue";
|
||||||
import { Card, CardFilter, getCards } from "@/mlpccg";
|
import { Card, CardFilter, getCards, allSets, cardFullName } from "@/mlpccg";
|
||||||
|
|
||||||
|
const colorNames = [
|
||||||
|
"Loyalty",
|
||||||
|
"Honesty",
|
||||||
|
"Laughter",
|
||||||
|
"Magic",
|
||||||
|
"Generosity",
|
||||||
|
"Kindness",
|
||||||
|
"None"
|
||||||
|
];
|
||||||
|
|
||||||
|
const typeNames = [
|
||||||
|
"Mane Character",
|
||||||
|
"Friend",
|
||||||
|
"Event",
|
||||||
|
"Resource",
|
||||||
|
"Troublemaker",
|
||||||
|
"Problem"
|
||||||
|
];
|
||||||
|
|
||||||
|
const rarityNames = ["C", "U", "R", "SR", "UR", "RR", "F"];
|
||||||
|
|
||||||
|
// Trasform string from list to a number that can be used for comparison/sorting
|
||||||
|
function arrIndex(arr: string[]) {
|
||||||
|
return function(comp: string) {
|
||||||
|
const idx = arr.indexOf(comp);
|
||||||
|
if (idx < 0) {
|
||||||
|
return arr.length;
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const elemIndex = arrIndex(colorNames);
|
||||||
|
const typeIndex = arrIndex(typeNames);
|
||||||
|
const rarityIndex = arrIndex(rarityNames);
|
||||||
|
|
||||||
|
// Convert Element[] to number by scaling elements for fair comparisons
|
||||||
|
// Example: ["Loyalty", "Kindness"] -> [0, 5] -> [1, 6] -> 16
|
||||||
|
function multiElemStr(elems: string[]): number {
|
||||||
|
return elems
|
||||||
|
.map(elemIndex)
|
||||||
|
.reduce(
|
||||||
|
(acc, elem, idx, arr) => acc + (elem + 1) * 10 ** (arr.length - idx - 1),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort function for sorting cards
|
||||||
|
function sortByColor(a: Card, b: Card) {
|
||||||
|
const typeA = typeIndex(a.Type);
|
||||||
|
const typeB = typeIndex(b.Type);
|
||||||
|
if (typeA != typeB) {
|
||||||
|
return typeA - typeB;
|
||||||
|
}
|
||||||
|
// Same types, filter by primary element
|
||||||
|
switch (a.Type) {
|
||||||
|
case "Friend":
|
||||||
|
case "Mane Character":
|
||||||
|
const elemA = multiElemStr(a.Element);
|
||||||
|
const elemB = multiElemStr(b.Element);
|
||||||
|
if (elemA > elemB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (elemB > elemA) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Problem":
|
||||||
|
const preqA = multiElemStr(Object.keys(a.ProblemRequirement!));
|
||||||
|
const preqB = multiElemStr(Object.keys(b.ProblemRequirement!));
|
||||||
|
if (preqA > preqB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (preqB > preqA) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Event":
|
||||||
|
case "Resource":
|
||||||
|
if (a.Requirement && b.Requirement) {
|
||||||
|
const reqA = multiElemStr(Object.keys(a.Requirement));
|
||||||
|
const reqB = multiElemStr(Object.keys(b.Requirement));
|
||||||
|
if (reqA > reqB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (reqB > reqA) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Filter by power
|
||||||
|
if (a.Power && b.Power) {
|
||||||
|
if (a.Power != b.Power) {
|
||||||
|
return a.Power - b.Power;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by Rarity (not the pony)
|
||||||
|
const rarityA = rarityIndex(a.Rarity);
|
||||||
|
const rarityB = rarityIndex(b.Rarity);
|
||||||
|
if (rarityA != rarityB) {
|
||||||
|
return rarityA - rarityB;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameA = cardFullName(a);
|
||||||
|
const nameB = cardFullName(b);
|
||||||
|
if (nameA > nameB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (nameB > nameA) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -48,21 +223,38 @@ import { Card, CardFilter, getCards } from "@/mlpccg";
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export default class DeckBuilder extends Vue {
|
export default class DeckBuilder extends Vue {
|
||||||
|
// Picked/filtered cards
|
||||||
private decklist!: Card[];
|
private decklist!: Card[];
|
||||||
private filter!: CardFilter;
|
|
||||||
private filtered!: Card[];
|
private filtered!: Card[];
|
||||||
private offset!: number;
|
|
||||||
|
// Names
|
||||||
|
private colors!: string[];
|
||||||
|
private sets!: string[];
|
||||||
|
|
||||||
|
// Card picker size
|
||||||
private rows!: number;
|
private rows!: number;
|
||||||
private columns!: number;
|
private columns!: number;
|
||||||
|
|
||||||
|
// User Filters
|
||||||
|
private nameFilter!: string;
|
||||||
|
private setFilters!: string[];
|
||||||
|
private elementFilters!: string[];
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
private offset!: number;
|
||||||
|
|
||||||
private data() {
|
private data() {
|
||||||
return {
|
return {
|
||||||
decklist: [],
|
decklist: [],
|
||||||
filter: {},
|
|
||||||
filtered: [],
|
filtered: [],
|
||||||
offset: 0,
|
offset: 0,
|
||||||
rows: 2,
|
rows: 2,
|
||||||
columns: 5
|
columns: 5,
|
||||||
|
nameFilter: "",
|
||||||
|
setFilters: [],
|
||||||
|
elementFilters: [],
|
||||||
|
colors: colorNames,
|
||||||
|
sets: allSets.slice(0, -1)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +263,18 @@ export default class DeckBuilder extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async applyFilters() {
|
private async applyFilters() {
|
||||||
this.filtered = await getCards(this.filter);
|
let filters: CardFilter = {};
|
||||||
|
if (this.setFilters.length > 0) {
|
||||||
|
filters.Sets = this.setFilters;
|
||||||
|
}
|
||||||
|
if (this.elementFilters.length > 0) {
|
||||||
|
filters.Elements = this.elementFilters;
|
||||||
|
}
|
||||||
|
if (this.nameFilter.length > 0) {
|
||||||
|
filters.Name = this.nameFilter;
|
||||||
|
}
|
||||||
|
const filtered = await getCards(filters);
|
||||||
|
this.filtered = filtered.sort(sortByColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get itemsPerPage() {
|
private get itemsPerPage() {
|
||||||
|
@ -81,5 +284,49 @@ export default class DeckBuilder extends Vue {
|
||||||
private get currentPage() {
|
private get currentPage() {
|
||||||
return this.filtered.slice(this.offset, this.itemsPerPage);
|
return this.filtered.slice(this.offset, this.itemsPerPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private elementIconURL(element: string): string {
|
||||||
|
return `/images/elements/${element.toLowerCase()}.webp`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setIconURL(set: string): string {
|
||||||
|
return `/images/sets/${set.toUpperCase()}.webp`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private elementIconClass(element: string) {
|
||||||
|
return {
|
||||||
|
selected: this.elementFilters.includes(element)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private setIconClass(set: string) {
|
||||||
|
return {
|
||||||
|
selected: this.setFilters.includes(set)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleElementFilter(element: string) {
|
||||||
|
const idx = this.elementFilters.indexOf(element);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.elementFilters.splice(idx, 1);
|
||||||
|
} else {
|
||||||
|
this.elementFilters.push(element);
|
||||||
|
}
|
||||||
|
this.applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleSetFilter(set: string) {
|
||||||
|
const idx = this.setFilters.indexOf(set);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.setFilters.splice(idx, 1);
|
||||||
|
} else {
|
||||||
|
this.setFilters.push(set);
|
||||||
|
}
|
||||||
|
this.applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private nameChanged() {
|
||||||
|
this.applyFilters();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue