Add basic deck builder #12

Merged
hamcha merged 42 commits from feature/deckbuilder into master 2019-09-12 09:11:32 +00:00
3 changed files with 331 additions and 87 deletions
Showing only changes of commit 94f4db2c68 - Show all commits

View file

@ -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();
} }

View file

@ -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",

View file

@ -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>