Add basic deck builder (#12)
All checks were successful
continuous-integration/drone/push Build is passing
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
|
@ -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
|
@ -2,6 +2,7 @@
|
|||
node_modules
|
||||
/dist
|
||||
coverage
|
||||
*.sqlite
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
|
|
16
package.json
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
main {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
main.loading-box {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
|
|
BIN
src/assets/images/cardback.webp
Normal file
After Width: | Height: | Size: 46 KiB |
13
src/assets/images/deckbuilder/navarrow.svg
Normal 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 |
Before Width: | Height: | Size: 11 KiB |
BIN
src/assets/images/elements/generosity.webp
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/images/elements/honesty.webp
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/images/elements/kindness.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/images/elements/laughter.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/images/elements/loyalty.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/images/elements/magic.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/images/elements/none.webp
Normal file
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 799 B After Width: | Height: | Size: 799 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 26 KiB |
BIN
src/assets/images/races/alicorn.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/images/races/ally.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/assets/images/races/critter.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/images/races/dragon.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/images/races/earthpony.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/races/pegasus.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/images/races/unicorn.png
Normal file
After Width: | Height: | Size: 27 KiB |
|
@ -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%);
|
|
@ -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;
|
||||
}
|
6
src/assets/scss/dark.sass
Normal 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"
|
89
src/components/DeckBuilder/CardPicker.vue
Normal 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>
|
229
src/components/DeckBuilder/DeckList.vue
Normal 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>
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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
|
@ -0,0 +1,5 @@
|
|||
export * from "./card";
|
||||
export * from "./database";
|
||||
export * from "./set";
|
||||
export * from "./types";
|
||||
export * from "./images";
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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;
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./MockDataConnection";
|
||||
export * from "./MockPeer";
|
||||
export * from "./MockHelper";
|
||||
export * from "./EventHook";
|
||||
|
|
16
src/tests/unit/cards.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
37
src/tests/unit/components/CardPicker.spec.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
36
src/tests/unit/components/DeckList.spec.ts
Normal 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
|
||||
}
|
||||
});
|
||||
});
|
39
src/tests/unit/database.spec.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -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"
|
||||
|
|