Add basic deck builder (#12)
continuous-integration/drone/push Build is passing Details

Includes:

- Basic layout
- Card list
- Filter cards by set/color
- Filter cards by type
- Filter cards by rule text
- Add/remove cards to decklist
- Export deck to ponyhead URL
This commit is contained in:
Hamcha 2019-09-12 09:11:32 +00:00 committed by Gitea
parent e14f6679fb
commit eade73f9f5
58 changed files with 1446 additions and 141 deletions

View File

@ -125,7 +125,7 @@ steps:
commands:
- yarn test:unit --coverage
depends_on:
- dependencies
- test # Must run after test otherwise SQLite will get mad
- name: upload_coverage
image: plugins/s3

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
node_modules
/dist
coverage
*.sqlite
# local env files
.env.local

View File

@ -9,12 +9,20 @@
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"axios": "^0.18.0",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"buefy": "^0.8.2",
"bulma-prefers-dark": "^0.1.0-beta.0",
"core-js": "^2.6.5",
"dexie": "^2.0.4",
"eventemitter3": "^4.0.0",
"node-sass": "^4.9.0",
"peerjs": "^1.0.4",
"register-service-worker": "^1.6.2",
"sass": "^1.18.0",
"sass-loader": "^7.1.0",
"typescript": "^3.4.3",
"vue": "^2.6.10",
"vue-class-component": "^7.0.2",
"vue-property-decorator": "^8.1.0",
@ -33,17 +41,13 @@
"@vue/eslint-config-prettier": "^5.0.0",
"@vue/eslint-config-typescript": "^4.0.0",
"@vue/test-utils": "^1.0.0-beta.29",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-vue": "^5.0.0",
"node-sass": "^4.9.0",
"indexeddbshim": "^4.1.0",
"prettier": "^1.18.2",
"sass": "^1.18.0",
"sass-loader": "^7.1.0",
"ts-jest": "^23.0.0",
"typescript": "^3.4.3",
"vue-cli-plugin-axios": "^0.0.4",
"vue-cli-plugin-buefy": "^0.3.7",
"vue-template-compiler": "^2.6.10"
},

View File

@ -1,14 +1,13 @@
{
"name": "mcgvue",
"short_name": "mcgvue",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"icons": [{
"src": "./images/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-512x512.png",
"src": "./images/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
@ -17,4 +16,4 @@
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}
}

View File

@ -9,6 +9,10 @@
</template>
<style lang="scss" scoped>
main {
user-select: none;
}
main.loading-box {
height: 100vh;
display: flex;

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 50 82" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Artboard1" transform="matrix(1,0,0,1,0,15.7771)">
<rect x="0" y="-15.777" width="50" height="81.873" style="fill:none;"/>
<g transform="matrix(0.457009,0,0,1.2614,2.95268,-32.3672)">
<path d="M48.243,45.605L90.773,73.875L48.243,73.875L5.712,45.605L48.243,17.336L90.773,17.336L48.243,45.605L48.243,45.605Z" style="fill:url(#_Linear1);"/>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(137.101,65.9713,-182.089,49.6719,-34.1545,12.0871)"><stop offset="0" style="stop-color:rgb(235,235,235);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(163,163,163);stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 799 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -3,32 +3,44 @@
// Colors
$black: hsl(0, 0%, 4%) !default;
$black-bis: hsl(0, 0%, 7%) !default;
$black-ter: hsl(0, 0%, 14%) !default;
$black: hsl(0, 0%, 4%) !default;
$black-bis: hsl(0, 0%, 7%) !default;
$black-ter: hsl(0, 0%, 14%) !default;
$grey-darker: hsl(0, 0%, 21%) !default;
$grey-dark: hsl(0, 0%, 29%) !default;
$grey: hsl(0, 0%, 48%) !default;
$grey-light: hsl(0, 0%, 71%) !default;
$grey-darker: hsl(0, 0%, 21%) !default;
$grey-dark: hsl(0, 0%, 29%) !default;
$grey: hsl(0, 0%, 48%) !default;
$grey-light: hsl(0, 0%, 71%) !default;
$grey-lighter: hsl(0, 0%, 86%) !default;
$white-ter: hsl(0, 0%, 96%) !default;
$white-bis: hsl(0, 0%, 98%) !default;
$white: hsl(0, 0%, 100%) !default;
$white-ter: hsl(0, 0%, 96%) !default;
$white-bis: hsl(0, 0%, 98%) !default;
$white: hsl(0, 0%, 100%) !default;
$orange: hsl(14, 100%, 53%) !default;
$yellow: hsl(48, 100%, 67%) !default;
$green: hsl(141, 71%, 48%) !default;
$turquoise: hsl(171, 100%, 41%) !default;
$cyan: hsl(204, 86%, 53%) !default;
$blue: hsl(217, 71%, 53%) !default;
$purple: hsl(271, 100%, 71%) !default;
$red: hsl(348, 100%, 61%) !default;
$orange: hsl(14, 100%, 53%) !default;
$yellow: hsl(48, 100%, 67%) !default;
$green: hsl(141, 71%, 48%) !default;
$turquoise: hsl(171, 100%, 41%) !default;
$cyan: hsl(204, 86%, 53%) !default;
$blue: hsl(217, 71%, 53%) !default;
$purple: hsl(271, 100%, 71%) !default;
$red: hsl(348, 100%, 61%) !default;
// Typography
$family-sans-serif: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif !default;
$family-sans-serif: BlinkMacSystemFont,
-apple-system,
"Segoe UI",
"Roboto",
"Oxygen",
"Ubuntu",
"Cantarell",
"Fira Sans",
"Droid Sans",
"Helvetica Neue",
"Helvetica",
"Arial",
sans-serif !default;
$family-monospace: monospace !default;
$render-mode: optimizeLegibility !default;
@ -53,11 +65,11 @@ $gap: 32px !default;
// 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16
$tablet: 769px !default;
// 960px container + 4rem
$desktop: 960px + (2 * $gap) !default;
$desktop: 960px+(2 * $gap) !default;
// 1152px container + 4rem
$widescreen: 1152px + (2 * $gap) !default;
$widescreen: 1152px+(2 * $gap) !default;
// 1344px container + 4rem;
$fullhd: 1344px + (2 * $gap) !default;
$fullhd: 1344px+(2 * $gap) !default;
// Miscellaneous
@ -72,7 +84,6 @@ $speed: 86ms !default;
$variable-columns: true !default;
// The default Bulma derived variables are declared below
$primary: $turquoise !default;
@ -150,3 +161,10 @@ $size-small: $size-7 !default;
$size-normal: $size-6 !default;
$size-medium: $size-5 !default;
$size-large: $size-4 !default;
// Input box styling
$input-focus-border-color: $turquoise;
$input-hover-border-color: scale-color($turquoise, $lightness: -30%);
$fantasy: 'Merriweather';
$primary-text: scale-color($primary, $saturation: -50%, $lightness: 20%);

View File

@ -4,6 +4,8 @@
@import "~bulma/sass/utilities/derived-variables";
@import "~bulma";
@import "~buefy/src/scss/buefy";
@import "dark";
@import url('https://fonts.googleapis.com/css?family=Merriweather:300,400,400i,700&display=swap');
html {
scrollbar-color: #404245 #2f3132;
@ -12,5 +14,5 @@ html {
}
body {
color: white;
color: $white;
}

View File

@ -0,0 +1,6 @@
@charset "utf-8"
@import "~bulma-prefers-dark/sass/utilities/_all"
@import "~bulma-prefers-dark/sass/base/_all"
@import "~bulma-prefers-dark/sass/elements/_all"
@import "~bulma-prefers-dark/sass/components/_all"
@import "~bulma-prefers-dark/sass/layout/_all"

View File

@ -0,0 +1,89 @@
<template>
<section class="cardpicker" :style="grid">
<article
@click="() => _picked(card)"
:class="cardClass(card)"
v-for="(card, i) in cards"
:key="i + card.data.ID"
>
<img :src="imageURL(card.data.ID)" />
</article>
</section>
</template>
<style lang="scss" scoped>
$padding: 10px;
.cardpicker {
height: 100%;
display: grid;
gap: $padding;
padding: ($padding * 4) $padding;
row-gap: $padding * 4;
}
.ccgcard {
display: flex;
align-items: center;
transition: 100ms all;
&.available:hover img {
box-shadow: 0 0 15px 5px rgba(200, 210, 255, 0.5);
}
&.disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
</style>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import { Card, CardSlot, cardImageURL } from "@/mlpccg";
@Component({
components: {}
})
export default class CardPicker extends Vue {
@Prop()
public cards!: CardSlot[];
@Prop({ default: 2 })
public rows!: number;
@Prop({ default: 5 })
public columns!: number;
@Prop({ default: false })
public ignoreLimit!: boolean;
private get grid() {
return {
gridTemplateRows: "1fr ".repeat(this.rows).trim(),
gridTemplateColumns: "1fr ".repeat(this.columns).trim()
};
}
private imageURL(id: string) {
return cardImageURL(id);
}
private _picked(card: CardSlot) {
if (this.isAvailable(card)) {
this.$emit("picked", card.data);
}
}
private cardClass(card: CardSlot) {
const available = this.isAvailable(card);
return {
ccgcard: true,
available,
disabled: !available
};
}
private isAvailable(card: CardSlot) {
return card.limit == 0 || card.howmany < card.limit || this.ignoreLimit;
}
}
</script>

View File

@ -0,0 +1,229 @@
<template>
<section class="decklist">
<section class="card-section" v-for="section in sections" :key="section">
<header>
<h1>{{ section }}</h1>
</header>
<article
class="ccgcard"
@click="() => _drop(card)"
v-for="(card, i) in getCards(section, true)"
:key="i"
>
<img :src="imageURL(card.data.ID)" class="cardbg" />
<div class="amt">{{ card.howmany }}</div>
<div class="fullname">
<div class="name">{{ card.data.Name }}</div>
<div class="subname">
{{ card.data.Subname ? card.data.Subname : "" }}
</div>
</div>
</article>
</section>
</section>
</template>
<style lang="scss" scoped>
@import "@/assets/scss/_variables.scss";
.decklist {
display: flex;
flex-direction: column;
}
.card-section {
header {
h1 {
padding: 5px 10px;
font-family: $fantasy;
font-size: 10pt;
}
}
}
.ccgcard {
display: flex;
align-content: space-between;
align-items: center;
border: 1px solid $grey;
border-radius: 10px;
padding: 5px 3px;
margin: 2px 0;
cursor: pointer;
position: relative;
overflow: hidden;
div {
z-index: 2;
}
}
.cardbg {
position: absolute;
margin-top: 30%;
right: -20px;
left: -20px;
max-width: none;
width: 120%;
filter: brightness(20%);
z-index: 1;
}
.fullname {
display: flex;
flex-direction: column;
}
.amt {
margin: 0 6pt 0 10pt;
font-weight: bold;
font-size: 13pt;
&:after {
content: " ×";
font-weight: 300;
}
}
.name {
font-family: $fantasy;
font-size: 10.5pt;
line-height: 1rem;
}
.subname {
color: $grey-light;
font-size: 10pt;
line-height: 1rem;
}
</style>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import {
cardFullName,
CardSlot,
Card,
cardImageURL,
multiElemStr,
typeNames
} from "@/mlpccg";
function sortCards(a: CardSlot, b: CardSlot): number {
// Sort by element
// (Cards are guaranteed to be the same type)
switch (a.data.Type) {
case "Friend":
{
// Sort by requirement
if (a.data.Requirement && b.data.Requirement) {
const reqA = multiElemStr(Object.keys(a.data.Requirement));
const reqB = multiElemStr(Object.keys(b.data.Requirement));
if (reqA > reqB) {
return 1;
}
if (reqB > reqA) {
return -1;
}
}
// Sort by cost
if (a.data.Cost && b.data.Cost) {
if (a.data.Cost > b.data.Cost) {
return 1;
}
if (a.data.Cost < b.data.Cost) {
return -1;
}
}
// Sort by element
const elemA = multiElemStr(a.data.Element);
const elemB = multiElemStr(b.data.Element);
if (elemA > elemB) {
return 1;
}
if (elemB > elemA) {
return -1;
}
}
break;
case "Problem":
if (a.data.ProblemRequirement && b.data.ProblemRequirement) {
const preqA = multiElemStr(Object.keys(a.data.ProblemRequirement));
const preqB = multiElemStr(Object.keys(b.data.ProblemRequirement));
if (preqA > preqB) {
return 1;
}
if (preqB > preqA) {
return -1;
}
}
break;
case "Event":
case "Resource":
if (a.data.Requirement && b.data.Requirement) {
const reqA = multiElemStr(Object.keys(a.data.Requirement));
const reqB = multiElemStr(Object.keys(b.data.Requirement));
if (reqA > reqB) {
return 1;
}
if (reqB > reqA) {
return -1;
}
}
break;
}
// Sort by power
if (a.data.Power && b.data.Power) {
if (a.data.Power > b.data.Power) {
return 1;
}
if (a.data.Power < b.data.Power) {
return -1;
}
}
// If all else fail, sort by name
const nameA = cardFullName(a.data);
const nameB = cardFullName(b.data);
if (nameA > nameB) {
return 1;
}
if (nameA < nameB) {
return -1;
}
return 0;
}
@Component({
components: {}
})
export default class DeckList extends Vue {
@Prop()
public cards!: CardSlot[];
private fullName(card: Card): string {
return cardFullName(card);
}
private _drop(slot: CardSlot) {
this.$emit("removed", slot);
}
private imageURL(id: string) {
return cardImageURL(id);
}
private getCards(section: string, sort: boolean): CardSlot[] {
let cards = this.cards.filter(c => c.data.Type == section);
if (!sort) {
return cards;
}
return cards.sort(sortCards);
}
private get sections(): string[] {
return typeNames.filter(s => this.getCards(s, false).length > 0);
}
}
</script>

View File

@ -1,15 +1,19 @@
import Vue from "vue";
import "./plugins/axios";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "./registerServiceWorker";
import Buefy from "buefy";
import "./assets/scss/app.scss";
import { initDB } from "./mlpccg";
Vue.use(Buefy);
Vue.config.productionTip = false;
initDB();
new Vue({
router,
store,

View File

@ -1,5 +1,71 @@
const imgBaseURL = "https://mcg.zyg.ovh/images/cards/";
import { Card } from "./types";
export function cardImageURL(cardid: string): string {
return `${imgBaseURL}${cardid}.webp`;
export function cardFullName(card: Card): string {
if (card.Subname != "") {
return `${card.Name}, ${card.Subname}`;
}
return card.Name;
}
export function createPonyheadURL(cards: Card[]): string {
const cardlist = cards.map(c => `${c.ID}x1`);
return "https://ponyhead.com/deckbuilder?v1code=" + cardlist.join("-");
}
export const colorNames = [
"Loyalty",
"Honesty",
"Laughter",
"Magic",
"Generosity",
"Kindness",
"None"
];
export const typeNames = [
"Mane Character",
"Friend",
"Event",
"Resource",
"Troublemaker",
"Problem"
];
export 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;
};
}
export const elemIndex = arrIndex(colorNames);
export const typeIndex = arrIndex(typeNames);
export const rarityIndex = arrIndex(rarityNames);
// Convert Element[] to number by scaling elements for fair comparisons
// Example: ["Loyalty", "Kindness"] -> [0, 5] -> [1, 6] -> 16
export function multiElemStr(elems: string[]): number {
return elems
.map(elemIndex)
.reduce(
(acc, elem, idx, arr) => acc + (elem + 1) * 10 ** (arr.length - idx - 1),
0
);
}
export function cardLimit(type: string) {
switch (type) {
case "Mane Character":
return 1;
case "Problem":
return 2;
default:
return 3;
}
}

View File

@ -1,21 +1,32 @@
import Dexie from "dexie";
import { Card, CardFilter } from "./types";
import { Card, CardFilter, StoredImages } from "./types";
import { cardFullName } from "./card";
class CardDatabase extends Dexie {
public cards: Dexie.Table<Card, string>;
public images: Dexie.Table<StoredImages, string>;
public constructor() {
super("CardDatabase");
this.version(1).stores({
cards: "ID,Set,Type,Cost,Power"
cards: "ID,Set,Type,Cost,Power",
images: "id"
});
this.cards = this.table("cards");
this.images = this.table("images");
}
}
export let Database = new CardDatabase();
export let Database: CardDatabase | null = null;
export function initDB() {
Database = new CardDatabase();
}
export async function getCards(filter: CardFilter) {
if (Database == null) {
throw new Error("Database was not initialized, init with 'initDB()'");
}
let table = Database.cards;
// Get best IDB index
let query: Dexie.Collection<Card, string>;
@ -43,95 +54,100 @@ export async function getCards(filter: CardFilter) {
}
}
return await query
.filter(x => {
if (filter.Name) {
if (
!`${x.Name}, ${x.Subname}`
.toLowerCase()
.includes(filter.Name.toLowerCase())
) {
return false;
const results = query.filter(x => {
if (filter.Name) {
if (
!cardFullName(x)
.toLowerCase()
.includes(filter.Name.toLowerCase())
) {
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 (
!`${x.Keywords.join(" ~ ")} ~ ${x.Text}`
.toLowerCase()
.includes(filter.Rules.toLowerCase())
) {
return false;
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)) {
found = true;
break;
}
}
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 (!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 (x.Requirement) {
for (const element in x.Requirement) {
if (filter.Elements.includes(element)) {
found = true;
break;
}
}
if (x.Requirement) {
for (const element in x.Requirement) {
if (filter.Elements.includes(element)) {
found = true;
break;
}
}
if (x.ProblemRequirement) {
for (const element in x.ProblemRequirement) {
if (filter.Elements.includes(element)) {
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 (
typeof x.Power === "undefined" ||
!filter.Powers.includes(x.Power)
) {
return false;
}
// For "None" element searches, "nothing" is actually ok
if (
filter.Elements.includes("None") &&
x.Element.length == 0 &&
(!x.Requirement || x.Requirement.length == 0) &&
(!x.ProblemRequirement || x.ProblemRequirement.length == 0)
) {
found = true;
}
if (filter.Costs && filter.Costs.length > 0) {
if (typeof x.Cost === "undefined" || !filter.Costs.includes(x.Cost)) {
return false;
}
if (!found) {
return false;
}
if (filter.Rarities && filter.Rarities.length > 0) {
if (!filter.Rarities.includes(x.Rarity)) {
return false;
}
}
if (filter.Powers && filter.Powers.length > 0) {
if (typeof x.Power === "undefined" || !filter.Powers.includes(x.Power)) {
return false;
}
return true;
})
.toArray();
}
if (filter.Costs && filter.Costs.length > 0) {
if (typeof x.Cost === "undefined" || !filter.Costs.includes(x.Cost)) {
return false;
}
}
if (filter.Rarities && filter.Rarities.length > 0) {
if (!filter.Rarities.includes(x.Rarity)) {
return false;
}
}
return true;
});
return await results.toArray();
}

35
src/mlpccg/images.ts Normal file
View File

@ -0,0 +1,35 @@
import axios from "axios";
import { Database } from "./database";
const imgBaseURL = "https://mcg.zyg.ovh/images/cards/";
export function cardImageURL(cardid: string): string {
return `${imgBaseURL}${cardid}.webp`;
}
async function getCardImageList(): Promise<string[]> {
const req = await axios(`${imgBaseURL}list.txt`);
return req.data;
}
export async function getImages() {
if (Database == null) {
throw new Error("Database was not initialized, init with 'initDB()'");
}
const itemcount = await Database.images.count();
if (itemcount > 100) {
// DB already filled, exit early
return;
}
const imglist = await getCardImageList();
let table = Database.images;
const promises = imglist.map(async img => {
const req = await axios({
url: `${imgBaseURL}${img}`,
responseType: "blob"
});
return table.put({ id: img, image: req.data });
});
return await Promise.all(promises);
}

5
src/mlpccg/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from "./card";
export * from "./database";
export * from "./set";
export * from "./types";
export * from "./images";

View File

@ -1,8 +1,9 @@
import { SetFile } from "./types";
import { Database } from "./database";
import axios from "axios";
const baseURL = "https://mcg.zyg.ovh/setdata/";
const allSets = [
export const allSets = [
"PR",
"CN",
"RR",
@ -19,6 +20,9 @@ const allSets = [
];
export async function loadSets() {
if (Database == null) {
throw new Error("Database was not initialized, init with 'initDB()'");
}
const itemcount = await Database.cards.count();
if (itemcount > 100) {
// DB already filled, exit early
@ -27,6 +31,9 @@ export async function loadSets() {
const sets = await Promise.all(allSets.map(set => downloadSet(set)));
await Promise.all(
sets.map(async set => {
if (Database == null) {
throw new Error("Database was not initialized, init with 'initDB()'");
}
console.log(`Processing cards from ${set.Name}`);
return await Database.cards.bulkPut(set.Cards);
})
@ -34,11 +41,10 @@ export async function loadSets() {
}
async function downloadSet(setid: string): Promise<SetFile> {
const setfile = await fetch(`${baseURL}${setid.toLowerCase()}.json`);
const setdata: SetFile = await setfile.json();
setdata.Cards = setdata.Cards.map(c => {
const setdata = await axios(`${baseURL}${setid.toLowerCase()}.json`);
setdata.data.Cards = (setdata.data as SetFile).Cards.map(c => {
c.Set = setid;
return c;
});
return setdata;
return setdata.data;
}

View File

@ -2,6 +2,11 @@ export type Rarity = "C" | "U" | "R" | "SR" | "UR" | "RR";
export type PowerRequirement = { [key: string]: number };
export interface StoredImages {
id: string;
image: Blob;
}
export interface SetFile {
Name: string;
Cards: Card[];
@ -38,3 +43,9 @@ export interface CardFilter {
Powers?: number[];
Rarities?: string[];
}
export interface CardSlot {
data: Card;
limit: number;
howmany: number;
}

61
src/plugins/axios.js Normal file
View File

@ -0,0 +1,61 @@
"use strict";
import Vue from "vue";
import axios from "axios";
// Full config: https://github.com/axios/axios#request-config
// axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || '';
// axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
let config = {
// baseURL: process.env.baseURL || process.env.apiUrl || ""
// timeout: 60 * 1000, // Timeout
// withCredentials: true, // Check cross-site Access-Control
};
const _axios = axios.create(config);
_axios.interceptors.request.use(
function(config) {
// Do something before request is sent
return config;
},
function(error) {
// Do something with request error
return Promise.reject(error);
}
);
// Add a response interceptor
_axios.interceptors.response.use(
function(response) {
// Do something with response data
return response;
},
function(error) {
// Do something with response error
return Promise.reject(error);
}
);
Plugin.install = function(Vue, options) {
Vue.axios = _axios;
window.axios = _axios;
Object.defineProperties(Vue.prototype, {
axios: {
get() {
return _axios;
}
},
$axios: {
get() {
return _axios;
}
}
});
};
Vue.use(Plugin);
export default Plugin;

View File

@ -1,3 +1,4 @@
export * from "./MockDataConnection";
export * from "./MockPeer";
export * from "./MockHelper";
export * from "./EventHook";

View File

@ -0,0 +1,16 @@
import { Card, createPonyheadURL, cardFullName } from "@/mlpccg";
describe("mlpccg/cards", () => {
test("Card full names are correctly generated in all cases", () => {
const card1 = { Name: "Name", Subname: "" };
const card2 = { Name: "Name1", Subname: "the Name2" };
expect(cardFullName(card1 as Card)).toEqual("Name");
expect(cardFullName(card2 as Card)).toEqual("Name1, the Name2");
});
test("Ponyhead URL is generated correctly", () => {
const cards: any[] = [{ ID: "pr10" }, { ID: "pr12" }, { ID: "pr13" }];
const url = "https://ponyhead.com/deckbuilder?v1code=pr10x1-pr12x1-pr13x1";
expect(createPonyheadURL(cards!)).toEqual(url);
});
});

View File

@ -0,0 +1,37 @@
import CardPicker from "@/components/DeckBuilder/CardPicker.vue";
import { shallowMount } from "@vue/test-utils";
// Generate 10 test cards
const testCards = new Array(10)
.fill("test")
.map((t, i) => ({ ID: `${t}${i}` }));
const testSlots = testCards.map(c => ({ data: c, limit: 3, howmany: 1 }));
describe("components/DeckBuilder/CardPicker", () => {
test("CardPicker correctly creates images for each card", () => {
const wrapper = shallowMount(CardPicker, {
propsData: {
rows: 2,
columns: 5,
cards: testSlots
}
});
const cards = wrapper.findAll(".ccgcard");
expect(cards.contains("img")).toBe(true);
});
test("CardPicker correctly aligns items in a grid", () => {
const wrapper = shallowMount(CardPicker, {
propsData: {
rows: 3,
columns: 5,
cards: testSlots
}
});
const section = wrapper.find(".cardpicker");
const style = section.attributes("style");
expect(style).toMatch(
/grid-template-rows: \S+ \S+ \S+; grid-template-columns: \S+ \S+ \S+ \S+ \S+;/i
);
});
});

View File

@ -0,0 +1,36 @@
import DeckList from "@/components/DeckBuilder/DeckList.vue";
import { shallowMount } from "@vue/test-utils";
import { colorNames } from "@/mlpccg";
// Generate 10 test cards
const testCards = new Array(3).fill("test").map((t, i) => ({
ID: `test${i}`,
Name: `Test name ${i}`,
Subname: `Subname ${i}`,
Type: "Friend",
Element: [colorNames[i]],
Power: i,
Cost: i,
Requirement: { Generosity: i }
}));
const testSlots = testCards.map((c, i) => ({ data: c, limit: 3, howmany: i }));
describe("components/DeckBuilder/DeckList", () => {
test("DeckList correctly detects card info", () => {
const wrapper = shallowMount(DeckList, {
propsData: {
cards: testSlots
}
});
const cards = wrapper.findAll(".ccgcard");
expect(cards.contains(".fullname")).toBe(true);
for (let index = 0; index < testSlots.length; index++) {
const item = cards.at(index);
const card = testSlots[index];
expect(item.find(".amt").text()).toEqual(`${card.howmany}`);
expect(item.find(".fullname .name").text()).toEqual(card.data.Name);
expect(item.find(".fullname .subname").text()).toEqual(card.data.Subname);
//TODO Add more fields check as they are added
}
});
});

View File

@ -0,0 +1,39 @@
import { loadSets, getCards, Database, initDB, cardFullName } from "@/mlpccg";
import Dexie from "dexie";
const setGlobalVars = require("indexeddbshim");
setGlobalVars(Dexie.dependencies);
describe("mlpccg/Database", () => {
beforeAll(async () => {
jest.setTimeout(15000);
initDB();
await loadSets();
});
test("getCards without a filter returns all the cards", async () => {
expect(Database).toBeTruthy();
const allCards = await Database!.cards.count();
const filtered = await getCards({});
expect(filtered).toHaveLength(allCards);
});
test("getCards with a primary filter filters card correctly", async () => {
expect(Database).toBeTruthy();
const filtered = await getCards({
Types: ["Troublemaker"]
});
for (const card of filtered) {
expect(card.Type).toBe("Troublemaker");
}
});
test("getCards with a secondary filter filters card correctly", async () => {
expect(Database).toBeTruthy();
const filtered = await getCards({
Name: "Rainbow Dash"
});
for (const card of filtered) {
expect(cardFullName(card).indexOf("Rainbow Dash") >= 0).toBeTruthy();
}
});
});

View File

@ -1,6 +1,5 @@
import { MockHelper } from "@/testing";
import { MockHelper, EventHook } from "@/testing";
import { NetworkMessage, LocalClient, ChatMessage } from "@/network";
import { EventHook } from "@/testing/EventHook";
const sampleRoom = () => ({
max_players: 3,

View File

@ -1,14 +1,470 @@
<template>
<section class="deckbuilder"></section>
<section class="deckbuilder">
<section class="cardlist">
<section class="filters">
<div class="row">
<b-input
@input="textChanged"
v-model="nameFilter"
placeholder="Search name"
></b-input>
<div class="colorfilter" v-for="color in colors" :key="color">
<img
@click="toggleFilter(elementFilters, color)"
:class="filterIconClass(elementFilters, color)"
:src="elementIconURL(color)"
/>
</div>
<div class="divider" />
<div class="typefilter" v-for="type in types" :key="type">
<img
@click="toggleFilter(typeFilters, type)"
:class="filterIconClass(typeFilters, type)"
:src="typeIconURL(type)"
/>
</div>
</div>
<div class="row">
<b-input
@input="textChanged"
v-model="ruleFilter"
placeholder="Search rule text"
></b-input>
<div class="setfilter" v-for="set in sets" :key="set">
<img
@click="toggleFilter(setFilters, set)"
:class="filterIconClass(setFilters, set)"
:src="setIconURL(set)"
/>
</div>
</div>
</section>
<section class="cards">
<div @click="prevPage" :class="canGoPrev ? 'prev' : 'prev unavailable'">
<img src="../assets/images/deckbuilder/navarrow.svg" />
</div>
<CardPicker
@picked="cardPicked"
:columns="columns"
:rows="rows"
:cards="currentPage"
/>
<div @click="nextPage" :class="canGoNext ? 'next' : 'next unavailable'">
<img src="../assets/images/deckbuilder/navarrow.svg" />
</div>
</section>
</section>
<section class="decklist">
<header>
<h1>{{ deckname }}</h1>
<nav class="buttons">
<b-button @click="exportToPonyhead" size="is-small deck-btn"
>Ponyhead</b-button
>
</nav>
</header>
<DeckList :cards="decklist" @removed="cardRemoved" />
</section>
</section>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
@import "@/assets/scss/_variables.scss";
.deckbuilder {
background: url("../assets/images/backgrounds/deckbuilderbg.webp") center;
background-repeat: no-repeat;
background-size: cover;
height: 100vh;
display: grid;
grid-template-columns: 3fr minmax(250px, 1fr);
}
.cardlist {
display: grid;
grid-column: 1;
grid-template-rows: 110px 1fr;
}
.filters {
display: flex;
flex-direction: column;
padding: 5px;
.row {
flex: 1;
display: flex;
align-items: center;
* {
margin: 5px;
}
}
}
.cards {
display: grid;
grid-template-columns: 5% 1fr 5%;
}
.decklist {
padding: 10px;
padding-top: 15px;
grid-column: 2;
header {
padding-left: 1rem;
color: $primary-text;
font-family: $fantasy;
h1 {
font-size: 20pt;
font-weight: 700;
margin-bottom: 5px;
}
}
}
.colorfilter,
.setfilter,
.typefilter {
cursor: pointer;
img {
opacity: 0.4;
&:hover {
opacity: 0.7;
}
&.selected {
opacity: 1;
}
}
}
.colorfilter {
width: 42px;
height: 42px;
}
.setfilter {
width: 32px;
height: 32px;
}
.typefilter {
width: 42px;
height: 42px;
}
.prev,
.next {
opacity: 0.5;
display: flex;
cursor: pointer;
&:hover {
opacity: 0.9;
}
&.unavailable {
opacity: 0.1;
cursor: not-allowed;
}
}
.next img {
transform: scaleX(-1);
}
.divider {
width: 10px;
height: 10px;
}
.deck-btn {
padding: 0 10px;
padding-bottom: 2px;
}
</style>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import DeckList from "@/components/DeckBuilder/DeckList.vue";
import CardPicker from "@/components/DeckBuilder/CardPicker.vue";
import {
Card,
CardFilter,
CardSlot,
getCards,
allSets,
cardFullName,
createPonyheadURL,
multiElemStr,
typeIndex,
rarityIndex,
colorNames,
typeNames,
cardLimit
} from "@/mlpccg";
// 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":
if (a.ProblemRequirement && b.ProblemRequirement) {
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({
components: {}
components: {
DeckList,
CardPicker
}
})
export default class DeckBuilder extends Vue {}
export default class DeckBuilder extends Vue {
// Picked/filtered cards
private decklist!: CardSlot[];
private filtered!: Card[];
// Names
private colors!: string[];
private sets!: string[];
private types!: string[];
// Card picker size
private rows!: number;
private columns!: number;
// User Filters
private nameFilter!: string;
private ruleFilter!: string;
private setFilters!: string[];
private elementFilters!: string[];
private typeFilters!: string[];
// Decklist options
private deckname!: string;
// Navigation
private offset!: number;
private data() {
return {
decklist: [],
filtered: [],
offset: 0,
rows: 2,
columns: 5,
nameFilter: "",
ruleFilter: "",
setFilters: [],
elementFilters: [],
typeFilters: [],
colors: colorNames,
sets: allSets.slice(0, -1),
types: typeNames,
deckname: "Unnamed deck"
};
}
private mounted() {
this.applyFilters();
}
private async applyFilters() {
let filters: CardFilter = {};
if (this.setFilters.length > 0) {
filters.Sets = this.setFilters;
}
if (this.elementFilters.length > 0) {
filters.Elements = this.elementFilters;
}
if (this.typeFilters.length > 0) {
filters.Types = this.typeFilters;
}
if (this.nameFilter.length > 0) {
filters.Name = this.nameFilter;
}
if (this.ruleFilter.length > 0) {
filters.Rules = this.ruleFilter;
}
const filtered = await getCards(filters);
this.filtered = filtered.sort(sortByColor);
this.offset = 0;
}
private get itemsPerPage() {
return this.columns * this.rows;
}
private get currentPage(): CardSlot[] {
return this.filtered
.slice(this.offset, this.offset + this.itemsPerPage)
.map(card => {
const res = this.decklist.find(c => c.data.ID == card.ID);
if (res) {
return res;
}
return {
data: card,
limit: cardLimit(card.Type),
howmany: 0
};
});
}
private elementIconURL(element: string): string {
return require(`../assets/images/elements/${element.toLowerCase()}.webp`);
}
private setIconURL(set: string): string {
return require(`../assets/images/sets/${set.toUpperCase()}.webp`);
}
private typeIconURL(type: string): string {
let urltype = type.toLowerCase();
if (urltype == "mane character") {
urltype = "mane-char";
}
return require(`../assets/images/cardtypes/${urltype}.webp`);
}
private filterIconClass(filter: string[], key: string) {
return {
selected: filter.includes(key)
};
}
private toggleFilter(filter: string[], key: string) {
const idx = filter.indexOf(key);
if (idx >= 0) {
filter.splice(idx, 1);
} else {
filter.push(key);
}
this.applyFilters();
}
private textChanged() {
this.applyFilters();
}
private get canGoPrev(): boolean {
return this.offset > 0;
}
private get canGoNext(): boolean {
return this.offset + this.itemsPerPage < this.filtered.length;
}
private prevPage() {
if (!this.canGoPrev) {
return;
}
this.offset = Math.max(0, this.offset - this.itemsPerPage);
}
private nextPage() {
if (!this.canGoNext) {
return;
}
this.offset = Math.min(
this.filtered.length - this.itemsPerPage,
this.offset + this.itemsPerPage
);
}
private cardPicked(card: Card) {
// Check if card is already in
const idx = this.decklist.findIndex(c => c.data.ID == card.ID);
if (idx >= 0) {
const deckitem = this.decklist[idx];
deckitem.howmany += 1;
Vue.set(this.decklist, idx, deckitem);
} else {
this.decklist.push({
data: card,
limit: cardLimit(card.Type),
howmany: 1
});
}
}
private exportToPonyhead() {
const url = createPonyheadURL(this.decklist.map(c => c.data));
window.open(url, "_blank");
}
private cardRemoved(card: CardSlot) {
const idx = this.decklist.findIndex(c => c.data.ID == card.data.ID);
if (idx < 0) {
throw new Error("Removing card that isn't in the deck?");
}
const deckitem = this.decklist[idx];
deckitem.howmany -= 1;
if (deckitem.howmany <= 0) {
this.decklist.splice(idx, 1);
} else {
Vue.set(this.decklist, idx, deckitem);
}
}
}
</script>

View File

@ -3,8 +3,12 @@
<section class="playerlist">
<b>Players</b>
</section>
<section class="pool"><b>Card pool</b></section>
<section class="cardlist"><b>Cards</b></section>
<section class="pool">
<b>Card pool</b>
</section>
<section class="cardlist">
<b>Cards</b>
</section>
</section>
</template>
@ -12,8 +16,8 @@
.draftview {
display: grid;
height: 100vh;
grid-gap: 10px;
grid-template-columns: 200px 1fr 250px;
gap: 10px;
grid-template-columns: minmax(200px, 1fr) 3fr minmax(250px, 1fr);
section {
grid-row: 1;
border: 1px solid #555;

164
yarn.lock
View File

@ -573,6 +573,14 @@
"@babel/helper-regex" "^7.4.4"
regexpu-core "^4.5.4"
"@babel/polyfill@^7.0.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc"
integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw==
dependencies:
core-js "^2.6.5"
regenerator-runtime "^0.13.2"
"@babel/preset-env@^7.0.0 < 7.4.0":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1"
@ -1488,6 +1496,11 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
argsarray@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb"
integrity sha1-bnIHtOzbObCviDA/pa4ivajfYcs=
arr-diff@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
@ -1656,6 +1669,14 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
axios@^0.18.0:
version "0.18.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3"
integrity sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==
dependencies:
follow-redirects "1.5.10"
is-buffer "^2.0.2"
babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@ -1909,6 +1930,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base64-arraybuffer-es6@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.4.2.tgz#b567d364065843113589b6c1436bd9492701c7fe"
integrity sha512-HaJx92u12By863ZXVHZs4Bp1nkKaLpbs3Ec9SI1OKzq60Hz+Ks6z7UvdD8pIx61Ck3e8F9MH/IPEu5T0xKSbkQ==
base64-js@^1.0.2:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
@ -2199,6 +2225,11 @@ builtin-status-codes@^3.0.0:
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
bulma-prefers-dark@^0.1.0-beta.0:
version "0.1.0-beta.0"
resolved "https://registry.yarnpkg.com/bulma-prefers-dark/-/bulma-prefers-dark-0.1.0-beta.0.tgz#646350738ed00ac66d0f84ec6821a677aa1a66c5"
integrity sha512-EeDW8pQrkYEOXo2l3WykfghbUzi8jlQWGI+Cu2HwmXwQHMcoGF6yiKYCNShttN+8z3atq8fLWh3B7pqXUV4fBA==
bulma@0.7.5:
version "0.7.5"
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.7.5.tgz#35066c37f82c088b68f94450be758fc00a967208"
@ -3169,6 +3200,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^3.0.0, debug@^3.1.0, debug@^3.2.5, debug@^3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@ -3906,6 +3944,13 @@ eventsource@^1.0.7:
dependencies:
original "^1.0.0"
eventtargeter@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/eventtargeter/-/eventtargeter-0.5.0.tgz#ab5e05cc7d96bef6a05a0a4a7053bf8fb7621ba7"
integrity sha512-FQbP+ToTYLKEF3VpyaciNbaexbvIOrXW1V1Hg7kKCT+AiX6sq8rUn1NIQiYEpA04eWzHpopH/QRHqm3K2KnLtQ==
dependencies:
"@babel/polyfill" "^7.0.0"
evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
@ -4377,6 +4422,13 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
follow-redirects@1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
dependencies:
debug "=3.1.0"
follow-redirects@^1.0.0:
version "1.8.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.8.1.tgz#24804f9eaab67160b0e840c085885d606371a35b"
@ -5081,6 +5133,11 @@ ignore@^4.0.3, ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
immediate@^3.2.2:
version "3.2.3"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c"
integrity sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@ -5144,6 +5201,18 @@ indent-string@^2.1.0:
dependencies:
repeating "^2.0.0"
indexeddbshim@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/indexeddbshim/-/indexeddbshim-4.1.0.tgz#6fffc99a2e302445c9df7b3366ef072c9925225a"
integrity sha512-gnhy0Fz1fWU9pnIo16uKC9dGimsv/vKlXzZ9zasN2EUkx/KxtHkCIfR8I1XRqujsuTelQmn1/34RpDFycNVxtw==
dependencies:
"@babel/polyfill" "^7.0.0"
eventtargeter "0.5.0"
sync-promise "git+https://github.com/brettz9/sync-promise.git#full-sync-missing-promise-features"
typeson "5.11.0"
typeson-registry "1.0.0-alpha.26"
websql "git+https://github.com/brettz9/node-websql.git#configurable-secure2"
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@ -5343,6 +5412,11 @@ is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-buffer@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"
integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==
is-callable@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
@ -7092,6 +7166,22 @@ node-notifier@^5.2.1:
shellwords "^0.1.1"
which "^1.3.0"
node-pre-gyp@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054"
integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==
dependencies:
detect-libc "^1.0.2"
mkdirp "^0.5.1"
needle "^2.2.1"
nopt "^4.0.1"
npm-packlist "^1.1.6"
npmlog "^4.0.2"
rc "^1.2.7"
rimraf "^2.6.1"
semver "^5.3.0"
tar "^4"
node-pre-gyp@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149"
@ -7138,6 +7228,11 @@ node-sass@^4.9.0:
stdout-stream "^1.4.0"
"true-case-path" "^1.0.2"
noop-fn@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/noop-fn/-/noop-fn-1.0.0.tgz#5f33d47f13d2150df93e0cb036699e982f78ffbf"
integrity sha1-XzPUfxPSFQ35PgywNmmemC94/78=
"nopt@2 || 3":
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
@ -9381,6 +9476,15 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
sqlite3@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.1.0.tgz#e051fb9c133be15726322a69e2e37ec560368380"
integrity sha512-RvqoKxq+8pDHsJo7aXxsFR18i+dU2Wp5o12qAJOV5LNcDt+fgJsc2QKKg3sIRfXrN9ZjzY1T7SNe/DFVqAXjaw==
dependencies:
nan "^2.12.1"
node-pre-gyp "^0.11.0"
request "^2.87.0"
sshpk@^1.7.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
@ -9694,6 +9798,10 @@ symbol-tree@^3.2.2:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
"sync-promise@git+https://github.com/brettz9/sync-promise.git#full-sync-missing-promise-features":
version "1.0.1"
resolved "git+https://github.com/brettz9/sync-promise.git#25845a49a00aa2d2c985a5149b97c86a1fcdc75a"
table@4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
@ -9841,6 +9949,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-queue@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046"
integrity sha1-JaZ/LG4lOyypQZd7XvdELvl6YEY=
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@ -10083,6 +10196,21 @@ typescript@^3.4.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.2.tgz#105b0f1934119dde543ac8eb71af3a91009efe54"
integrity sha512-lmQ4L+J6mnu3xweP8+rOrUwzmN+MRAj7TgtJtDaXE5PMyX2kCrklhg3rvOsOIfNeAWMQWO2F1GPc1kMD2vLAfw==
typeson-registry@1.0.0-alpha.26:
version "1.0.0-alpha.26"
resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.26.tgz#d1f337584196c5d5d112ad981e0dbbd2ced30c30"
integrity sha512-R0wwXIYSiJMh+1XfvyUsCnEGVERoJcNrMl9e/ka30dJ+gQyh4/0NU9WHaqUm8oHtZzZYCz+A5fDRCiXYIq7H1Q==
dependencies:
base64-arraybuffer-es6 "0.4.2"
typeson "5.11.0"
uuid "3.3.2"
whatwg-url "7.0.0"
typeson@5.11.0:
version "5.11.0"
resolved "https://registry.yarnpkg.com/typeson/-/typeson-5.11.0.tgz#a8273f00050be9eeef974aaa04a0c95a394f821a"
integrity sha512-S5KtLzcU4dr4BXh8VuJDYugsRGsDQYlumCbrmwuAX1a1GNpbVYK4p9wluCIfTVPFvVyV6wRfExXX6Q1+YDItEQ==
uglify-js@3.4.x:
version "3.4.10"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
@ -10268,6 +10396,11 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@^3.0.1, uuid@^3.3.2:
version "3.3.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
@ -10310,6 +10443,11 @@ vue-class-component@^7.0.1, vue-class-component@^7.0.2:
resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.1.0.tgz#b33efcb10e17236d684f70b1e96f1946ec793e87"
integrity sha512-G9152NzUkz0i0xTfhk0Afc8vzdXxDR1pfN4dTwE72cskkgJtdXfrKBkMfGvDuxUh35U500g5Ve4xL8PEGdWeHg==
vue-cli-plugin-axios@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/vue-cli-plugin-axios/-/vue-cli-plugin-axios-0.0.4.tgz#29d4eb48275c7fe15b92e1fd5d95fbe2a966436f"
integrity sha512-p2b/fvPJuPBnvU8027PAAuU5DiOzUn2lku8XLG/f6c8FU0N+/MXWZAlOuHhqd9e7+KIZitwe/c8qlmv7TglbTg==
vue-cli-plugin-buefy@^0.3.7:
version "0.3.7"
resolved "https://registry.yarnpkg.com/vue-cli-plugin-buefy/-/vue-cli-plugin-buefy-0.3.7.tgz#31e5637529482a5a4564676f539db16278b0895c"
@ -10619,6 +10757,16 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
"websql@git+https://github.com/brettz9/node-websql.git#configurable-secure2":
version "1.0.0"
resolved "git+https://github.com/brettz9/node-websql.git#5149bc0763376ca757fc32dc74345ada0467bfbb"
dependencies:
argsarray "^0.0.1"
immediate "^3.2.2"
noop-fn "^1.0.0"
sqlite3 "^4.0.0"
tiny-queue "^0.2.1"
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
version "1.0.5"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
@ -10631,19 +10779,19 @@ whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-url@^6.4.1:
version "6.5.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
whatwg-url@7.0.0, whatwg-url@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==
dependencies:
lodash.sortby "^4.7.0"
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
whatwg-url@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==
whatwg-url@^6.4.1:
version "6.5.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
dependencies:
lodash.sortby "^4.7.0"
tr46 "^1.0.1"