Compare commits
1 commit
master
...
feature/dr
Author | SHA1 | Date | |
---|---|---|---|
a6ecace783 |
32
.drone.yml
|
@ -1,4 +1,3 @@
|
||||||
---
|
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
name: default
|
name: default
|
||||||
|
|
||||||
|
@ -15,19 +14,11 @@ steps:
|
||||||
|
|
||||||
- name: dependencies
|
- name: dependencies
|
||||||
image: node
|
image: node
|
||||||
failure: ignore
|
|
||||||
commands:
|
commands:
|
||||||
- yarn
|
- yarn
|
||||||
depends_on:
|
depends_on:
|
||||||
- restore-cache
|
- restore-cache
|
||||||
|
|
||||||
- name: lint
|
|
||||||
image: node
|
|
||||||
commands:
|
|
||||||
- yarn lint
|
|
||||||
depends_on:
|
|
||||||
- dependencies
|
|
||||||
|
|
||||||
- name: build_versioned
|
- name: build_versioned
|
||||||
image: node
|
image: node
|
||||||
commands:
|
commands:
|
||||||
|
@ -37,9 +28,6 @@ steps:
|
||||||
when:
|
when:
|
||||||
event:
|
event:
|
||||||
- push
|
- push
|
||||||
branch:
|
|
||||||
exclude:
|
|
||||||
- master
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- dependencies
|
- dependencies
|
||||||
|
|
||||||
|
@ -84,9 +72,6 @@ steps:
|
||||||
when:
|
when:
|
||||||
event:
|
event:
|
||||||
- push
|
- push
|
||||||
branch:
|
|
||||||
exclude:
|
|
||||||
- master
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- build_versioned
|
- build_versioned
|
||||||
|
|
||||||
|
@ -131,16 +116,16 @@ steps:
|
||||||
- name: test
|
- name: test
|
||||||
image: node
|
image: node
|
||||||
commands:
|
commands:
|
||||||
- yarn test:unit --runInBand
|
- yarn test:unit
|
||||||
depends_on:
|
depends_on:
|
||||||
- dependencies
|
- dependencies
|
||||||
|
|
||||||
- name: coverage
|
- name: coverage
|
||||||
image: node
|
image: node
|
||||||
commands:
|
commands:
|
||||||
- yarn test:unit --coverage --runInBand
|
- yarn test:unit --coverage
|
||||||
depends_on:
|
depends_on:
|
||||||
- test # Must run after test otherwise SQLite will get mad
|
- dependencies
|
||||||
|
|
||||||
- name: upload_coverage
|
- name: upload_coverage
|
||||||
image: plugins/s3
|
image: plugins/s3
|
||||||
|
@ -180,7 +165,6 @@ steps:
|
||||||
|
|
||||||
- name: rebuild-cache
|
- name: rebuild-cache
|
||||||
image: drillster/drone-volume-cache
|
image: drillster/drone-volume-cache
|
||||||
failure: ignore
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: cache
|
- name: cache
|
||||||
path: /cache
|
path: /cache
|
||||||
|
@ -190,13 +174,3 @@ steps:
|
||||||
- ./node_modules
|
- ./node_modules
|
||||||
depends_on:
|
depends_on:
|
||||||
- dependencies
|
- dependencies
|
||||||
|
|
||||||
volumes:
|
|
||||||
- name: cache
|
|
||||||
host:
|
|
||||||
path: /opt/gitea/drone-cache/mcg/mlpcardgame
|
|
||||||
---
|
|
||||||
kind: signature
|
|
||||||
hmac: 73d743702e8545bf469f17b96c289a5d4f1d56eb3f1966c7c0a0e38d44d38aba
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
|
@ -8,10 +8,7 @@ module.exports = {
|
||||||
extends: ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"],
|
extends: ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"],
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
"no-console":
|
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? ["error", { allow: ["info", "warn", "error"] }]
|
|
||||||
: "off",
|
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"
|
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -21,7 +18,9 @@ module.exports = {
|
||||||
|
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ["**/__tests__/*.{j,t}s?(x)"],
|
files: [
|
||||||
|
'**/__tests__/*.{j,t}s?(x)'
|
||||||
|
],
|
||||||
env: {
|
env: {
|
||||||
jest: true
|
jest: true
|
||||||
}
|
}
|
||||||
|
|
1
.gitignore
vendored
|
@ -2,7 +2,6 @@
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
coverage
|
coverage
|
||||||
*.sqlite
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
|
|
52
README.md
|
@ -4,58 +4,6 @@
|
||||||
|
|
||||||
Work in progress name, work in progress game
|
Work in progress name, work in progress game
|
||||||
|
|
||||||
Try the latest build here: [mcg-builds.zyg.ovh/latest](https://mcg-builds.zyg.ovh/latest/)
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
Fetch dependencies with `yarn`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn --dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run local server for development
|
|
||||||
|
|
||||||
Run this command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn serve
|
|
||||||
```
|
|
||||||
|
|
||||||
then visit [localhost:8080](http://localhost:8080) (URL might be different if something is already listening on port 8080)
|
|
||||||
|
|
||||||
### Run tests
|
|
||||||
|
|
||||||
Run unit tests with Jest:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn test:unit
|
|
||||||
```
|
|
||||||
|
|
||||||
Generate a coverage profile with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn test:unit --coverage
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lint code
|
|
||||||
|
|
||||||
Before you submit a PR, make sure the code is formatted correctly with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn lint
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building for release
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Code is ISC, Assets "depends", some stuff is taken from FreeSound and DeviantArt so your best bet is to just praise the copyright gods.
|
Code is ISC, Assets "depends", some stuff is taken from FreeSound and DeviantArt so your best bet is to just praise the copyright gods.
|
||||||
|
|
|
@ -2,7 +2,7 @@ module.exports = {
|
||||||
moduleFileExtensions: ["js", "jsx", "json", "vue", "ts", "tsx"],
|
moduleFileExtensions: ["js", "jsx", "json", "vue", "ts", "tsx"],
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.vue$": "vue-jest",
|
"^.+\\.vue$": "vue-jest",
|
||||||
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2|webp)$":
|
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":
|
||||||
"jest-transform-stub",
|
"jest-transform-stub",
|
||||||
"^.+\\.tsx?$": "ts-jest"
|
"^.+\\.tsx?$": "ts-jest"
|
||||||
},
|
},
|
||||||
|
|
23
package.json
|
@ -9,29 +9,18 @@
|
||||||
"test:unit": "vue-cli-service test:unit"
|
"test:unit": "vue-cli-service test:unit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/jszip": "^3.1.6",
|
|
||||||
"axios": "^0.18.0",
|
|
||||||
"babel-core": "7.0.0-bridge.0",
|
|
||||||
"babel-eslint": "^10.0.1",
|
|
||||||
"buefy": "^0.8.2",
|
"buefy": "^0.8.2",
|
||||||
"bulma-prefers-dark": "^0.1.0-beta.0",
|
|
||||||
"core-js": "^2.6.5",
|
"core-js": "^2.6.5",
|
||||||
"dexie": "^2.0.4",
|
"dexie": "^2.0.4",
|
||||||
"eventemitter3": "^4.0.0",
|
"eventemitter3": "^4.0.0",
|
||||||
"jszip": "^3.2.2",
|
|
||||||
"node-sass": "^4.9.0",
|
|
||||||
"peerjs": "^1.0.4",
|
"peerjs": "^1.0.4",
|
||||||
"register-service-worker": "^1.6.2",
|
"register-service-worker": "^1.6.2",
|
||||||
"sass": "^1.18.0",
|
|
||||||
"sass-loader": "^7.1.0",
|
|
||||||
"typescript": "^3.4.3",
|
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-class-component": "^7.0.2",
|
"vue-class-component": "^7.0.2",
|
||||||
"vue-property-decorator": "^8.1.0",
|
"vue-property-decorator": "^8.1.0",
|
||||||
"vue-router": "^3.0.3",
|
"vue-router": "^3.0.3",
|
||||||
"vuex": "^3.0.1",
|
"vuex": "^3.0.1",
|
||||||
"vuex-class": "^0.3.2",
|
"vuex-class": "^0.3.2"
|
||||||
"worker-loader": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^23.1.4",
|
"@types/jest": "^23.1.4",
|
||||||
|
@ -44,16 +33,18 @@
|
||||||
"@vue/eslint-config-prettier": "^5.0.0",
|
"@vue/eslint-config-prettier": "^5.0.0",
|
||||||
"@vue/eslint-config-typescript": "^4.0.0",
|
"@vue/eslint-config-typescript": "^4.0.0",
|
||||||
"@vue/test-utils": "^1.0.0-beta.29",
|
"@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": "^5.16.0",
|
||||||
"eslint-plugin-prettier": "^3.1.0",
|
"eslint-plugin-prettier": "^3.1.0",
|
||||||
"eslint-plugin-vue": "^5.0.0",
|
"eslint-plugin-vue": "^5.0.0",
|
||||||
"git-describe": "^4.0.4",
|
"node-sass": "^4.9.0",
|
||||||
"indexeddbshim": "^4.1.0",
|
|
||||||
"prettier": "^1.18.2",
|
"prettier": "^1.18.2",
|
||||||
|
"sass": "^1.18.0",
|
||||||
|
"sass-loader": "^7.1.0",
|
||||||
"ts-jest": "^23.0.0",
|
"ts-jest": "^23.0.0",
|
||||||
"vue-cli-plugin-axios": "^0.0.4",
|
"typescript": "^3.4.3",
|
||||||
"vue-cli-plugin-buefy": "^0.3.7",
|
"vue-cli-plugin-buefy": "^0.3.7",
|
||||||
"vue-cli-plugin-git-describe": "^1.0.0",
|
|
||||||
"vue-template-compiler": "^2.6.10"
|
"vue-template-compiler": "^2.6.10"
|
||||||
},
|
},
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
|
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 |
|
@ -1,22 +1,18 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css">
|
<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css">
|
||||||
<title>MLPCARDGAME</title>
|
<title>mcgvue</title>
|
||||||
</head>
|
</head>
|
||||||
|
<body>
|
||||||
<body>
|
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>We're sorry but mcgvue doesn't work properly without JavaScript enabled. Please enable it to
|
<strong>We're sorry but mcgvue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
continue.</strong>
|
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -1,14 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "MLPCARDGAME",
|
"name": "mcgvue",
|
||||||
"short_name": "mcg",
|
"short_name": "mcgvue",
|
||||||
"description": "MLP:CCG simulator",
|
"icons": [
|
||||||
"icons": [{
|
{
|
||||||
"src": "./images/icons/android-chrome-192x192.png",
|
"src": "./img/icons/android-chrome-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./images/icons/android-chrome-512x512.png",
|
"src": "./img/icons/android-chrome-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
|
|
16
src/App.vue
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<main v-if="loaded">
|
<main v-if="loaded">
|
||||||
|
<TopBar v-if="!isFullscreen" />
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
<main class="loading-box" v-else>
|
<main class="loading-box" v-else>
|
||||||
|
@ -8,10 +9,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
main {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
main.loading-box {
|
main.loading-box {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -28,11 +25,15 @@ h1.loading-message {
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
import { Action, Getter } from "vuex-class";
|
import { Action, Getter } from "vuex-class";
|
||||||
import { AppState } from "@/store/types";
|
import TopBar from "@/components/Navigation/TopBar.vue";
|
||||||
import { refreshCardSource, loadSets, getCards } from "@/mlpccg";
|
import { loadSets } from "@/mlpccg/set";
|
||||||
|
import { AppState } from "./store/types";
|
||||||
|
import { getCards } from "./mlpccg/database";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {}
|
components: {
|
||||||
|
TopBar
|
||||||
|
}
|
||||||
})
|
})
|
||||||
export default class App extends Vue {
|
export default class App extends Vue {
|
||||||
@Action showLoading!: (msg: string) => void;
|
@Action showLoading!: (msg: string) => void;
|
||||||
|
@ -53,7 +54,6 @@ export default class App extends Vue {
|
||||||
private async loadCards() {
|
private async loadCards() {
|
||||||
this.showLoading("Downloading data for all sets");
|
this.showLoading("Downloading data for all sets");
|
||||||
await loadSets();
|
await loadSets();
|
||||||
await refreshCardSource();
|
|
||||||
this.hideLoading();
|
this.hideLoading();
|
||||||
this.setLoaded(true);
|
this.setLoaded(true);
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 46 KiB |
|
@ -1,13 +0,0 @@
|
||||||
<?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>
|
|
Before Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/images/elements.webp
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/images/races.webp
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 27 KiB |
|
@ -28,19 +28,7 @@ $red: hsl(348, 100%, 61%) !default;
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
|
|
||||||
$family-sans-serif: BlinkMacSystemFont,
|
$family-sans-serif: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif !default;
|
||||||
-apple-system,
|
|
||||||
"Segoe UI",
|
|
||||||
"Roboto",
|
|
||||||
"Oxygen",
|
|
||||||
"Ubuntu",
|
|
||||||
"Cantarell",
|
|
||||||
"Fira Sans",
|
|
||||||
"Droid Sans",
|
|
||||||
"Helvetica Neue",
|
|
||||||
"Helvetica",
|
|
||||||
"Arial",
|
|
||||||
sans-serif !default;
|
|
||||||
$family-monospace: monospace !default;
|
$family-monospace: monospace !default;
|
||||||
$render-mode: optimizeLegibility !default;
|
$render-mode: optimizeLegibility !default;
|
||||||
|
|
||||||
|
@ -65,11 +53,11 @@ $gap: 32px !default;
|
||||||
// 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16
|
// 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16
|
||||||
$tablet: 769px !default;
|
$tablet: 769px !default;
|
||||||
// 960px container + 4rem
|
// 960px container + 4rem
|
||||||
$desktop: 960px+(2 * $gap) !default;
|
$desktop: 960px + (2 * $gap) !default;
|
||||||
// 1152px container + 4rem
|
// 1152px container + 4rem
|
||||||
$widescreen: 1152px+(2 * $gap) !default;
|
$widescreen: 1152px + (2 * $gap) !default;
|
||||||
// 1344px container + 4rem;
|
// 1344px container + 4rem;
|
||||||
$fullhd: 1344px+(2 * $gap) !default;
|
$fullhd: 1344px + (2 * $gap) !default;
|
||||||
|
|
||||||
// Miscellaneous
|
// Miscellaneous
|
||||||
|
|
||||||
|
@ -84,6 +72,7 @@ $speed: 86ms !default;
|
||||||
|
|
||||||
$variable-columns: true !default;
|
$variable-columns: true !default;
|
||||||
|
|
||||||
|
|
||||||
// The default Bulma derived variables are declared below
|
// The default Bulma derived variables are declared below
|
||||||
|
|
||||||
$primary: $turquoise !default;
|
$primary: $turquoise !default;
|
||||||
|
@ -161,10 +150,3 @@ $size-small: $size-7 !default;
|
||||||
$size-normal: $size-6 !default;
|
$size-normal: $size-6 !default;
|
||||||
$size-medium: $size-5 !default;
|
$size-medium: $size-5 !default;
|
||||||
$size-large: $size-4 !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,8 +4,6 @@
|
||||||
@import "~bulma/sass/utilities/derived-variables";
|
@import "~bulma/sass/utilities/derived-variables";
|
||||||
@import "~bulma";
|
@import "~bulma";
|
||||||
@import "~buefy/src/scss/buefy";
|
@import "~buefy/src/scss/buefy";
|
||||||
@import "dark";
|
|
||||||
@import url('https://fonts.googleapis.com/css?family=Merriweather:300,400,400i,700&display=swap');
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scrollbar-color: #404245 #2f3132;
|
scrollbar-color: #404245 #2f3132;
|
||||||
|
@ -14,28 +12,5 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: $white;
|
color: white;
|
||||||
}
|
|
||||||
|
|
||||||
.modal-background {
|
|
||||||
background: rgba($black, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
border: 1px solid rgba($primary, 0.4);
|
|
||||||
box-shadow: 0 0 30px rgba($black, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-card-body {
|
|
||||||
background: rgba($black, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-card-head,
|
|
||||||
.modal-card-foot {
|
|
||||||
background: rgba($black, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
progress.progress {
|
|
||||||
filter: invert(90%);
|
|
||||||
}
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
@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"
|
|
|
@ -1,59 +0,0 @@
|
||||||
<template>
|
|
||||||
<img :src="imageURL" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
|
||||||
import { cardImageURL } from "../../mlpccg";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {}
|
|
||||||
})
|
|
||||||
export default class CardImage extends Vue {
|
|
||||||
@Prop()
|
|
||||||
private id!: string;
|
|
||||||
|
|
||||||
private loaded!: boolean;
|
|
||||||
private loadedURL!: string;
|
|
||||||
private loadedTimeout!: boolean;
|
|
||||||
|
|
||||||
private data() {
|
|
||||||
return {
|
|
||||||
loaded: false,
|
|
||||||
loadedURL: "",
|
|
||||||
loadedTimeout: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private mounted() {
|
|
||||||
this.fetchImage();
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!this.loaded) {
|
|
||||||
this.loadedTimeout = true;
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
this.$watch("id", () => {
|
|
||||||
this.loaded = false;
|
|
||||||
this.fetchImage();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchImage() {
|
|
||||||
const url = await cardImageURL(this.id);
|
|
||||||
this.loaded = true;
|
|
||||||
this.loadedURL = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get imageURL(): string {
|
|
||||||
if (this.loaded) {
|
|
||||||
return this.loadedURL;
|
|
||||||
}
|
|
||||||
if (this.loadedTimeout) {
|
|
||||||
return require("@/assets/images/cardback.webp");
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,98 +0,0 @@
|
||||||
<template>
|
|
||||||
<section class="cardpicker" :style="grid">
|
|
||||||
<article
|
|
||||||
@click="() => _picked(card)"
|
|
||||||
:class="cardClass(card)"
|
|
||||||
v-for="(card, i) in cards"
|
|
||||||
:key="i + card.data.ID"
|
|
||||||
>
|
|
||||||
<CardImage :id="card.data.ID" />
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
$padding: 10px;
|
|
||||||
|
|
||||||
.cardpicker {
|
|
||||||
max-height: 100%;
|
|
||||||
display: grid;
|
|
||||||
gap: $padding;
|
|
||||||
padding: $padding;
|
|
||||||
place-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ccgcard {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: 100ms all;
|
|
||||||
cursor: pointer;
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
transition: box-shadow 60ms;
|
|
||||||
}
|
|
||||||
&.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";
|
|
||||||
import CardImage from "@/components/Cards/CardImage.vue";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
CardImage
|
|
||||||
}
|
|
||||||
})
|
|
||||||
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>
|
|
|
@ -1,229 +0,0 @@
|
||||||
<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>
|
|
14
src/components/Navigation/TopBar.vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<nav></nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {}
|
||||||
|
})
|
||||||
|
export default class TopBar extends Vue {}
|
||||||
|
</script>
|
|
@ -1,124 +0,0 @@
|
||||||
<template>
|
|
||||||
<nav>
|
|
||||||
<section class="pages">
|
|
||||||
<router-link
|
|
||||||
:class="routeClass(route)"
|
|
||||||
v-for="route in mainRoutes"
|
|
||||||
:key="route"
|
|
||||||
:to="{ name: route }"
|
|
||||||
>{{ prettyTitle(route) }}</router-link
|
|
||||||
>
|
|
||||||
</section>
|
|
||||||
<section class="icons">
|
|
||||||
<router-link
|
|
||||||
:class="routeClass(route)"
|
|
||||||
v-for="route in iconRoutes"
|
|
||||||
:key="route"
|
|
||||||
:to="{ name: route }"
|
|
||||||
><b-icon
|
|
||||||
:icon="prettyTitle(route)"
|
|
||||||
class="route-icon"
|
|
||||||
custom-size="mdi-36px"
|
|
||||||
/></router-link>
|
|
||||||
</section>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "@/assets/scss/_variables.scss";
|
|
||||||
|
|
||||||
.route-icon {
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: flex;
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgba(200, 230, 250, 0.3),
|
|
||||||
rgba(100, 180, 255, 0.1)
|
|
||||||
);
|
|
||||||
|
|
||||||
.pages {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icons {
|
|
||||||
flex-grow: 0;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 50px;
|
|
||||||
max-width: 250px;
|
|
||||||
text-align: center;
|
|
||||||
color: $grey-lighter;
|
|
||||||
|
|
||||||
border-right: 1px solid rgba(0, 50, 100, 0.5);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgba(0, 0, 0, 0.5),
|
|
||||||
rgba(100, 100, 100, 0.1)
|
|
||||||
);
|
|
||||||
cursor: pointer;
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current,
|
|
||||||
&.current:hover {
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgba(0, 0, 0, 0.7),
|
|
||||||
rgba(100, 100, 100, 0.1)
|
|
||||||
);
|
|
||||||
color: $grey-lighter;
|
|
||||||
cursor: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
|
||||||
|
|
||||||
const mainRoutes = ["lobby", "deck-editor"];
|
|
||||||
const iconRoutes = ["settings"];
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {}
|
|
||||||
})
|
|
||||||
export default class TopNav extends Vue {
|
|
||||||
private mainRoutes!: string[];
|
|
||||||
private iconRoutes!: string[];
|
|
||||||
|
|
||||||
private data() {
|
|
||||||
return {
|
|
||||||
mainRoutes,
|
|
||||||
iconRoutes
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private prettyTitle(name: string): string {
|
|
||||||
const route = this.$router.resolve({ name });
|
|
||||||
if (!route || !route.resolved.meta || !route.resolved.meta.topnav) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
return route.resolved.meta.topnav;
|
|
||||||
}
|
|
||||||
|
|
||||||
private routeClass(name: string) {
|
|
||||||
return {
|
|
||||||
entry: true,
|
|
||||||
current: this.$route.name == name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,19 +1,15 @@
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import "./plugins/axios";
|
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
import "./registerServiceWorker";
|
import "./registerServiceWorker";
|
||||||
import Buefy from "buefy";
|
import Buefy from "buefy";
|
||||||
import "./assets/scss/app.scss";
|
import "./assets/scss/app.scss";
|
||||||
import { initDB } from "./mlpccg";
|
|
||||||
|
|
||||||
Vue.use(Buefy);
|
Vue.use(Buefy);
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
initDB();
|
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
|
|
|
@ -1,71 +1,5 @@
|
||||||
import { Card } from "./types";
|
const imgBaseURL = "https://mcg.zyg.ovh/images/cards/";
|
||||||
|
|
||||||
export function cardFullName(card: Card): string {
|
export function cardImageURL(cardid: string): string {
|
||||||
if (card.Subname != "") {
|
return `${imgBaseURL}${cardid}.webp`;
|
||||||
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,42 +1,21 @@
|
||||||
import Dexie from "dexie";
|
import Dexie from "dexie";
|
||||||
import { Card, CardFilter, StoredImage } from "./types";
|
import { Card, CardFilter } from "./types";
|
||||||
import { cardFullName } from "./card";
|
|
||||||
|
|
||||||
class CardDatabase extends Dexie {
|
class CardDatabase extends Dexie {
|
||||||
public cards: Dexie.Table<Card, string>;
|
public cards: Dexie.Table<Card, string>;
|
||||||
public images: Dexie.Table<StoredImage, string>;
|
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super("CardDatabase");
|
super("CardDatabase");
|
||||||
this.version(1).stores({
|
this.version(1).stores({
|
||||||
cards: "ID,Set,Type,Cost,Power",
|
cards: "ID,Set,Type,Cost,Power"
|
||||||
images: "id"
|
|
||||||
});
|
});
|
||||||
this.cards = this.table("cards");
|
this.cards = this.table("cards");
|
||||||
this.images = this.table("images");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export let Database: CardDatabase | null = null;
|
export let Database = new CardDatabase();
|
||||||
|
|
||||||
export async function initDB(): Promise<void> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (Database == null) {
|
|
||||||
Database = new CardDatabase();
|
|
||||||
Database.on("ready", async () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
Database.open();
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCards(filter: CardFilter) {
|
export async function getCards(filter: CardFilter) {
|
||||||
if (Database == null) {
|
|
||||||
throw new Error("Database was not initialized, init with 'initDB()'");
|
|
||||||
}
|
|
||||||
let table = Database.cards;
|
let table = Database.cards;
|
||||||
// Get best IDB index
|
// Get best IDB index
|
||||||
let query: Dexie.Collection<Card, string>;
|
let query: Dexie.Collection<Card, string>;
|
||||||
|
@ -64,10 +43,11 @@ export async function getCards(filter: CardFilter) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = query.filter(x => {
|
return await query
|
||||||
|
.filter(x => {
|
||||||
if (filter.Name) {
|
if (filter.Name) {
|
||||||
if (
|
if (
|
||||||
!cardFullName(x)
|
!`${x.Name}, ${x.Subname}`
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(filter.Name.toLowerCase())
|
.includes(filter.Name.toLowerCase())
|
||||||
) {
|
) {
|
||||||
|
@ -129,21 +109,15 @@ export async function getCards(filter: CardFilter) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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 (!found) {
|
if (!found) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filter.Powers && filter.Powers.length > 0) {
|
if (filter.Powers && filter.Powers.length > 0) {
|
||||||
if (typeof x.Power === "undefined" || !filter.Powers.includes(x.Power)) {
|
if (
|
||||||
|
typeof x.Power === "undefined" ||
|
||||||
|
!filter.Powers.includes(x.Power)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,18 +132,6 @@ export async function getCards(filter: CardFilter) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
})
|
||||||
return await results.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cardFromIDs(cardIDs: string[]): Promise<Card[]> {
|
|
||||||
if (Database == null) {
|
|
||||||
throw new Error("Database was not initialized, init with 'initDB()'");
|
|
||||||
}
|
|
||||||
let table = Database.cards;
|
|
||||||
//TODO Replace with .bulkGet when upgrading to Dexie 3.x
|
|
||||||
return await table
|
|
||||||
.where("ID")
|
|
||||||
.anyOf(cardIDs)
|
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,211 +0,0 @@
|
||||||
import { Card, getCards } from "@/mlpccg";
|
|
||||||
import { Pack, PackSchema, AlternateProvider } from "./types";
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
(This data was taken from the MLP:CCG wikia at mlpccg.fandom.com and confirmed
|
|
||||||
by people at the MLP:CCG Discord)
|
|
||||||
|
|
||||||
Distribution rates for packs is usually 8 commons, 3 uncommons and 1 rare.
|
|
||||||
|
|
||||||
No Fixed or Promo cards can be found in packs.
|
|
||||||
|
|
||||||
UR distribution depends on set:
|
|
||||||
- PR has 1/13 chance of UR replacing a common
|
|
||||||
- CN->AD has 1/11 chance of UR replacing a common
|
|
||||||
- EO->FF has 1/3 chance of SR/UR replacing a common
|
|
||||||
|
|
||||||
SR are twice as common as UR, so that's one more thing to keep in mind.
|
|
||||||
|
|
||||||
Lastly, RR can replace another common in the ratio of ~1/2 every 6 boxes, depending
|
|
||||||
on set. Specifically, this is the RR ratio for each set:
|
|
||||||
- EO->HM: 1/108
|
|
||||||
- MT->FF: 1/216
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Returns the pack schema for a specific set
|
|
||||||
async function setSchema(set: string): Promise<PackSchema> {
|
|
||||||
// Force set name to uppercase
|
|
||||||
set = set.toUpperCase();
|
|
||||||
|
|
||||||
// Return blank schemas for invalid sets
|
|
||||||
if (set == "RR" || set == "CS") {
|
|
||||||
return { slots: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get cards for set
|
|
||||||
let cards = await getCards({ Sets: [set] });
|
|
||||||
let cardMap = spanByRarity(cards);
|
|
||||||
|
|
||||||
let rr: AlternateProvider[] = [];
|
|
||||||
let srur: AlternateProvider[] = [];
|
|
||||||
|
|
||||||
// Check for RR chances
|
|
||||||
/*
|
|
||||||
switch (set) {
|
|
||||||
case "EO":
|
|
||||||
case "HM":
|
|
||||||
rr = [
|
|
||||||
{
|
|
||||||
probability: 1.0 / 108.0,
|
|
||||||
provider: randomProvider([
|
|
||||||
//TODO
|
|
||||||
])
|
|
||||||
}
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case "MT":
|
|
||||||
case "HM":
|
|
||||||
case "SB":
|
|
||||||
case "FF":
|
|
||||||
rr = [
|
|
||||||
{
|
|
||||||
probability: 1.0 / 216.0,
|
|
||||||
provider: randomProvider([
|
|
||||||
//TODO
|
|
||||||
])
|
|
||||||
}
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Check for SR/UR chances
|
|
||||||
switch (set) {
|
|
||||||
case "PR":
|
|
||||||
srur = [
|
|
||||||
{
|
|
||||||
probability: 1.0 / 13.0,
|
|
||||||
provider: randomProvider(cardMap["UR"])
|
|
||||||
}
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case "CN":
|
|
||||||
case "CG":
|
|
||||||
case "AD":
|
|
||||||
srur = [
|
|
||||||
{
|
|
||||||
probability: 1.0 / 11.0,
|
|
||||||
provider: randomProvider(cardMap["UR"])
|
|
||||||
}
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
srur = [
|
|
||||||
{
|
|
||||||
probability: (1.0 / 9.0) * 2.0,
|
|
||||||
provider: randomProvider(cardMap["SR"])
|
|
||||||
},
|
|
||||||
{
|
|
||||||
probability: 1.0 / 9.0,
|
|
||||||
provider: randomProvider(cardMap["UR"])
|
|
||||||
}
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
slots: [
|
|
||||||
{
|
|
||||||
amount: 6,
|
|
||||||
provider: randomProvider(cardMap["C"]),
|
|
||||||
alternate: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
amount: 1,
|
|
||||||
provider: randomProvider(cardMap["C"]),
|
|
||||||
alternate: rr
|
|
||||||
},
|
|
||||||
{
|
|
||||||
amount: 1,
|
|
||||||
provider: randomProvider(cardMap["C"]),
|
|
||||||
alternate: srur
|
|
||||||
},
|
|
||||||
{
|
|
||||||
amount: 1,
|
|
||||||
provider: randomProvider(cardMap["R"]),
|
|
||||||
alternate: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
amount: 3,
|
|
||||||
provider: randomProvider(cardMap["U"]),
|
|
||||||
alternate: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PackBuilder {
|
|
||||||
schema: PackSchema;
|
|
||||||
constructor(schema: PackSchema) {
|
|
||||||
this.schema = schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildPack(): Pack {
|
|
||||||
let pack = [];
|
|
||||||
for (const slot of this.schema.slots) {
|
|
||||||
let provider = slot.provider;
|
|
||||||
|
|
||||||
// Check for alternates by generating a random and checking cumulated
|
|
||||||
// probability. Ie. if one card would show 5% of the time, another would
|
|
||||||
// show up 10% of the time, the algorithm would do something like this:
|
|
||||||
//
|
|
||||||
// With Math.random() == 0.85:
|
|
||||||
// ALTERNATE NO ALTERNATE
|
|
||||||
// [0.00-0.05][0.06----0.15][0.16------------1.00]
|
|
||||||
// ^ 0.85
|
|
||||||
//
|
|
||||||
// With Math.random() == 0.03:
|
|
||||||
// ALTERNATE NO ALTERNATE
|
|
||||||
// [0.00-0.05][0.06----0.15][0.16------------1.00]
|
|
||||||
// ^ 0.03
|
|
||||||
|
|
||||||
const rnd = Math.random();
|
|
||||||
let currentProb = 0;
|
|
||||||
for (const alternate of slot.alternate) {
|
|
||||||
currentProb += alternate.probability;
|
|
||||||
// Alternate matched
|
|
||||||
if (currentProb > rnd) {
|
|
||||||
provider = alternate.provider;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = 0; i < slot.amount; i++) {
|
|
||||||
const res = provider.next();
|
|
||||||
if (res.done) {
|
|
||||||
// No more cards to get from this, exit early
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
pack.push(res.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pack;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async fromSet(set: string): Promise<PackBuilder> {
|
|
||||||
let schema = await setSchema(set);
|
|
||||||
let builder = new PackBuilder(schema);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yields random cards from a chosen pool
|
|
||||||
export function* randomProvider(pool: Card[]) {
|
|
||||||
while (true) {
|
|
||||||
const idx = Math.floor(Math.random() * pool.length);
|
|
||||||
yield pool[idx];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Divides a list of card to a map of rarities
|
|
||||||
// ie. [ff14, ff16, ff17] => { "C" : ["ff14"], "U": ["ff17"], "R": ["ff16"] }
|
|
||||||
export function spanByRarity(pool: Card[]): Record<string, Card[]> {
|
|
||||||
return pool.reduce((map, current) => {
|
|
||||||
if (!(current.Rarity in map)) {
|
|
||||||
map[current.Rarity] = [];
|
|
||||||
}
|
|
||||||
map[current.Rarity].push(current);
|
|
||||||
return map;
|
|
||||||
}, Object.create(null));
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Card } from "@/mlpccg";
|
|
||||||
import { SessionPlayer } from "./session";
|
|
||||||
|
|
||||||
export class DraftBot {
|
|
||||||
assign(player: SessionPlayer) {
|
|
||||||
player.on("available-picks", cards => {
|
|
||||||
const pick = this.pick(cards);
|
|
||||||
// setTimeout hack to avoid handlers being called before the rest of the code
|
|
||||||
setTimeout(() => player.pick(pick.ID), 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pick(picks: Card[]): Card {
|
|
||||||
// For now, pick a random card
|
|
||||||
const idx = Math.floor(Math.random() * picks.length);
|
|
||||||
return picks[idx];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
import { Card, cardFromIDs } from "@/mlpccg";
|
|
||||||
import { PackSchema } from "./types";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export class Cube {
|
|
||||||
private pool: Card[];
|
|
||||||
|
|
||||||
constructor(pool: Card[]) {
|
|
||||||
this.pool = pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
schema(): PackSchema {
|
|
||||||
return {
|
|
||||||
slots: [
|
|
||||||
{
|
|
||||||
amount: 15,
|
|
||||||
provider: this.provider(),
|
|
||||||
alternate: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
*provider() {
|
|
||||||
while (this.pool.length > 0) {
|
|
||||||
const idx = Math.floor(Math.random() * this.pool.length);
|
|
||||||
const card = this.pool.splice(idx, 1);
|
|
||||||
yield card[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async fromCardIDs(cardIDs: string[]): Promise<Cube> {
|
|
||||||
const cards = await cardFromIDs(cardIDs);
|
|
||||||
return new this(cards);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async fromList(list: string): Promise<Cube> {
|
|
||||||
const ids = list.split("\n").map(x => x.trim());
|
|
||||||
return await this.fromCardIDs(ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async fromURL(url: string) {
|
|
||||||
const res = await axios(url, {
|
|
||||||
responseType: "text"
|
|
||||||
});
|
|
||||||
return await this.fromList(res.data);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
import { Card, cardFromIDs } from "@/mlpccg";
|
|
||||||
import {
|
|
||||||
PackSchema,
|
|
||||||
I8PCubeSchema,
|
|
||||||
I8PPackSchema,
|
|
||||||
I8PFileSchema,
|
|
||||||
DraftSchema
|
|
||||||
} from "./types";
|
|
||||||
import axios from "axios";
|
|
||||||
import { PackBuilder } from "./booster";
|
|
||||||
|
|
||||||
export class I8PCube {
|
|
||||||
private pools: Record<string, Card[]>;
|
|
||||||
private packschema: I8PPackSchema[];
|
|
||||||
private problemCount: number;
|
|
||||||
|
|
||||||
constructor(cubefile: I8PCubeSchema) {
|
|
||||||
this.pools = cubefile.Cards;
|
|
||||||
this.packschema = cubefile.Schema;
|
|
||||||
this.problemCount = cubefile.ProblemPackSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
schema(): DraftSchema {
|
|
||||||
return {
|
|
||||||
boosters: {
|
|
||||||
main: 4,
|
|
||||||
problem: 1
|
|
||||||
},
|
|
||||||
factories: {
|
|
||||||
main: new PackBuilder({
|
|
||||||
slots: this.packschema.map(s => ({
|
|
||||||
amount: s.Amount,
|
|
||||||
provider: this.provider(s.Type),
|
|
||||||
alternate: []
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
problem: new PackBuilder({
|
|
||||||
slots: [
|
|
||||||
{
|
|
||||||
amount: this.problemCount,
|
|
||||||
provider: this.provider("problem"),
|
|
||||||
alternate: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
*provider(name: string | "all") {
|
|
||||||
let poolname = name;
|
|
||||||
while (true) {
|
|
||||||
if (name == "all") {
|
|
||||||
const pools = Object.keys(this.pools);
|
|
||||||
const idx = Math.floor(Math.random() * pools.length);
|
|
||||||
poolname = pools[idx];
|
|
||||||
}
|
|
||||||
const pool = this.pools[poolname];
|
|
||||||
if (pool.length <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const idx = Math.floor(Math.random() * pool.length);
|
|
||||||
const card = pool.splice(idx, 1);
|
|
||||||
yield card[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async fromURL(url: string) {
|
|
||||||
const res = await axios(url);
|
|
||||||
const cubefile = res.data as I8PFileSchema;
|
|
||||||
let cards: Record<string, Card[]> = {};
|
|
||||||
for (const pool in cubefile.Cards) {
|
|
||||||
cards[pool] = await cardFromIDs(cubefile.Cards[pool]);
|
|
||||||
}
|
|
||||||
return new this({
|
|
||||||
Cards: cards,
|
|
||||||
ProblemPackSize: cubefile.ProblemPackSize,
|
|
||||||
Schema: cubefile.Schema
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
export * from "./cube";
|
|
||||||
export * from "./booster";
|
|
||||||
export * from "./types";
|
|
||||||
export * from "./session";
|
|
||||||
export * from "./bot";
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { DraftSchema, Pack } from "./types";
|
|
||||||
import { PackBuilder } from "./booster";
|
|
||||||
|
|
||||||
export class DraftProvider {
|
|
||||||
private schema: DraftSchema;
|
|
||||||
|
|
||||||
constructor(schema: DraftSchema) {
|
|
||||||
this.schema = schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPacks(): Pack[] {
|
|
||||||
let out = [];
|
|
||||||
for (const boosterSlot in this.schema.boosters) {
|
|
||||||
const amount = this.schema.boosters[boosterSlot];
|
|
||||||
const factory = this.schema.factories[boosterSlot];
|
|
||||||
if (!factory) {
|
|
||||||
throw new Error(
|
|
||||||
`booster type ${boosterSlot} was referenced in schema but was not provided a builder`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < amount; i++) {
|
|
||||||
out.push(factory.buildPack());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
static set(factory: PackBuilder, amount: number): DraftProvider {
|
|
||||||
return new DraftProvider({
|
|
||||||
boosters: { normal: amount },
|
|
||||||
factories: { normal: factory }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,262 +0,0 @@
|
||||||
import { PackBuilder, Cube, DraftOptions } from ".";
|
|
||||||
import EventEmitter from "eventemitter3";
|
|
||||||
import { Card } from "@/mlpccg";
|
|
||||||
import { Pack, Direction } from "./types";
|
|
||||||
import { DraftProvider } from "./provider";
|
|
||||||
import { DraftBot } from "./bot";
|
|
||||||
import { I8PCube } from "./i8pcube";
|
|
||||||
|
|
||||||
export class Session extends EventEmitter {
|
|
||||||
private options: DraftOptions;
|
|
||||||
private provider: DraftProvider;
|
|
||||||
private pod: SessionPlayer[] = [];
|
|
||||||
private players: string[] = [];
|
|
||||||
private pending: number[] = [];
|
|
||||||
private assigned: boolean = false;
|
|
||||||
private direction: Direction = "cw";
|
|
||||||
|
|
||||||
constructor(options: DraftOptions, provider: DraftProvider) {
|
|
||||||
super();
|
|
||||||
this.options = options;
|
|
||||||
this.provider = provider;
|
|
||||||
this.pod = new Array(options.players).fill(0).map((x, i) => {
|
|
||||||
const player = new SessionPlayer(provider.getPacks());
|
|
||||||
player.on("pick", this.picked.bind(this, i));
|
|
||||||
return player;
|
|
||||||
});
|
|
||||||
// Populate prev/next references
|
|
||||||
this.pod.forEach((val, i) => {
|
|
||||||
if (i > 0) {
|
|
||||||
val.prev = this.pod[i - 1];
|
|
||||||
} else {
|
|
||||||
val.prev = this.pod[this.pod.length - 1];
|
|
||||||
}
|
|
||||||
if (i < this.pod.length - 1) {
|
|
||||||
val.next = this.pod[i + 1];
|
|
||||||
} else {
|
|
||||||
val.next = this.pod[0];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public assign(
|
|
||||||
players: string[],
|
|
||||||
assignFn: (name: string, instance: SessionPlayer) => void
|
|
||||||
) {
|
|
||||||
// Figure out how many players there are vs spots to be filled
|
|
||||||
this.players = players;
|
|
||||||
const spots = this.options.players;
|
|
||||||
const playerNum = players.length;
|
|
||||||
if (playerNum > spots) {
|
|
||||||
throw new Error("too many players in the pod");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playerNum < 1) {
|
|
||||||
throw new Error("not enough players");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Place players in the pod
|
|
||||||
switch (this.options.spacing) {
|
|
||||||
case "evenly": {
|
|
||||||
const playerRatio = spots / playerNum;
|
|
||||||
let i = 0;
|
|
||||||
for (const player of players) {
|
|
||||||
const pos = Math.floor(playerRatio * i);
|
|
||||||
this.pod[pos].name = player;
|
|
||||||
assignFn(player, this.pod[pos]);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "randomly":
|
|
||||||
for (const player of players) {
|
|
||||||
const free = [...Array(spots).keys()].filter(
|
|
||||||
i => this.pod[i].name == ""
|
|
||||||
);
|
|
||||||
const idx = Math.floor(Math.random() * free.length);
|
|
||||||
const chosen = free[idx];
|
|
||||||
assignFn(player, this.pod[chosen]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// All the non-assigned places go to bots!
|
|
||||||
this.pod.forEach(p => {
|
|
||||||
if (p.name == "") {
|
|
||||||
p.name = "bot";
|
|
||||||
const bot = new DraftBot();
|
|
||||||
bot.assign(p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.assigned = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public start() {
|
|
||||||
if (!this.assigned) {
|
|
||||||
throw new Error("Must assign players first (see assign())");
|
|
||||||
}
|
|
||||||
this.emit("start", this.order);
|
|
||||||
this.nextPack();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get order(): string[] {
|
|
||||||
return this.pod.map(p => p.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private picked(
|
|
||||||
playerIndex: number,
|
|
||||||
_card: string,
|
|
||||||
lastPick: boolean,
|
|
||||||
lastPack: boolean
|
|
||||||
) {
|
|
||||||
if (!this.pending.includes(playerIndex)) {
|
|
||||||
// Uh oh.
|
|
||||||
throw new Error(
|
|
||||||
`unexpected pick: player "${this.pod[playerIndex].name}" already picked their card`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const idx = this.pending.indexOf(playerIndex);
|
|
||||||
this.pending.splice(idx, 1);
|
|
||||||
|
|
||||||
this.emit("player-pick", this.pod[playerIndex].name);
|
|
||||||
|
|
||||||
// Don't continue unless everyone picked their card
|
|
||||||
if (this.pending.length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Was this the last pick for this round of packs?
|
|
||||||
if (lastPick) {
|
|
||||||
// Was the it the last pack?
|
|
||||||
if (lastPack) {
|
|
||||||
this.emit("draft-over");
|
|
||||||
this.pod.forEach(p => p.emit("your-picks", p.picks));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.nextPack();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass packs between players for next pick
|
|
||||||
this.resetPending();
|
|
||||||
this.pod.forEach(p => p.sendPack(this.direction));
|
|
||||||
this.emit("next-pick");
|
|
||||||
}
|
|
||||||
|
|
||||||
private nextPack() {
|
|
||||||
this.resetPending();
|
|
||||||
this.flipOrder();
|
|
||||||
this.pod.forEach(p => p.nextPack());
|
|
||||||
this.emit("next-pack");
|
|
||||||
}
|
|
||||||
|
|
||||||
private resetPending() {
|
|
||||||
this.pending = this.pod.map((_, i) => i);
|
|
||||||
}
|
|
||||||
|
|
||||||
private flipOrder() {
|
|
||||||
if (this.direction == "cw") {
|
|
||||||
this.direction = "ccw";
|
|
||||||
} else {
|
|
||||||
this.direction = "cw";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async create(options: DraftOptions): Promise<Session> {
|
|
||||||
switch (options.source) {
|
|
||||||
case "set": {
|
|
||||||
const factory = await PackBuilder.fromSet(options.set);
|
|
||||||
const provider = DraftProvider.set(factory, options.packs);
|
|
||||||
return new Session(options, provider);
|
|
||||||
}
|
|
||||||
case "block":
|
|
||||||
throw new Error("not implemented");
|
|
||||||
case "cube": {
|
|
||||||
const cube = await Cube.fromURL(options.url);
|
|
||||||
const factory = new PackBuilder(cube.schema());
|
|
||||||
const provider = DraftProvider.set(factory, options.packs);
|
|
||||||
return new Session(options, provider);
|
|
||||||
}
|
|
||||||
case "i8pcube": {
|
|
||||||
const cube = await I8PCube.fromURL(options.url);
|
|
||||||
const provider = new DraftProvider(cube.schema());
|
|
||||||
return new Session(options, provider);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error("Unknown draft source");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SessionPlayer extends EventEmitter {
|
|
||||||
public name: string = "";
|
|
||||||
public currentPack?: Pack;
|
|
||||||
public picks: Pack;
|
|
||||||
public packs: Pack[];
|
|
||||||
public toSend: Pack | null = null;
|
|
||||||
public next?: SessionPlayer;
|
|
||||||
public prev?: SessionPlayer;
|
|
||||||
public ready: boolean = false;
|
|
||||||
|
|
||||||
constructor(packs: Pack[]) {
|
|
||||||
super();
|
|
||||||
this.packs = packs;
|
|
||||||
this.picks = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public pick(card: string) {
|
|
||||||
if (!this.ready) {
|
|
||||||
throw new Error("not ready to pick");
|
|
||||||
}
|
|
||||||
if (!this.currentPack) {
|
|
||||||
throw new Error("no pack to pick from");
|
|
||||||
}
|
|
||||||
const idx = this.currentPack.findIndex(c => c.ID == card);
|
|
||||||
if (idx < 0) {
|
|
||||||
throw new Error("card not in available picks");
|
|
||||||
}
|
|
||||||
const pick = this.currentPack.splice(idx, 1);
|
|
||||||
this.picks.push(pick[0]);
|
|
||||||
this.toSend = this.currentPack;
|
|
||||||
this.ready = false;
|
|
||||||
|
|
||||||
this.emit("pick", card, this.currentPack.length < 1, this.packs.length < 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendPack(direction: Direction) {
|
|
||||||
if (!this.toSend) {
|
|
||||||
throw new Error("no pack to pass");
|
|
||||||
}
|
|
||||||
if (this.toSend.length < 1) {
|
|
||||||
throw new Error("empty pack");
|
|
||||||
}
|
|
||||||
if (direction == "cw") {
|
|
||||||
if (!this.next) {
|
|
||||||
throw new Error("no player to pass cards to");
|
|
||||||
}
|
|
||||||
this.next.receivePack(this.toSend);
|
|
||||||
} else {
|
|
||||||
if (!this.prev) {
|
|
||||||
throw new Error("no player to pass cards to");
|
|
||||||
}
|
|
||||||
this.prev.receivePack(this.toSend);
|
|
||||||
}
|
|
||||||
this.toSend = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public receivePack(cards: Pack) {
|
|
||||||
this.currentPack = cards;
|
|
||||||
this.ready = true;
|
|
||||||
this.emit("available-picks", cards);
|
|
||||||
}
|
|
||||||
|
|
||||||
public nextPack() {
|
|
||||||
// Open new pack
|
|
||||||
const newPack = this.packs.shift();
|
|
||||||
if (!newPack) {
|
|
||||||
throw new Error("no packs left");
|
|
||||||
}
|
|
||||||
this.receivePack(newPack);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
import { Card } from "@/mlpccg";
|
|
||||||
import { PackBuilder } from "./booster";
|
|
||||||
|
|
||||||
export type Provider = Iterator<Card>;
|
|
||||||
|
|
||||||
export type Pack = Card[];
|
|
||||||
|
|
||||||
export interface PackSchema {
|
|
||||||
slots: PackSlot[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PackSlot {
|
|
||||||
amount: number;
|
|
||||||
provider: Provider;
|
|
||||||
alternate: AlternateProvider[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AlternateProvider {
|
|
||||||
probability: number;
|
|
||||||
provider: Provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SetDraftOptions {
|
|
||||||
source: "set";
|
|
||||||
set: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlockDraftOptions {
|
|
||||||
source: "block";
|
|
||||||
block: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CubeDraftOptions {
|
|
||||||
source: "cube";
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface I8PCubeDraftOptions {
|
|
||||||
source: "i8pcube";
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LimitedBoosterDraft {
|
|
||||||
type: "booster-draft";
|
|
||||||
packs: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LimitedSealedDraft {
|
|
||||||
type: "sealed";
|
|
||||||
packs: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LimitedGameType = LimitedBoosterDraft | LimitedSealedDraft;
|
|
||||||
|
|
||||||
export type DraftType =
|
|
||||||
| SetDraftOptions
|
|
||||||
| BlockDraftOptions
|
|
||||||
| CubeDraftOptions
|
|
||||||
| I8PCubeDraftOptions;
|
|
||||||
|
|
||||||
export interface SessionOptions {
|
|
||||||
players: number;
|
|
||||||
spacing: "evenly" | "randomly";
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DraftOptions = SessionOptions & LimitedGameType & DraftType;
|
|
||||||
|
|
||||||
export interface DraftSchema {
|
|
||||||
boosters: Record<string, number>;
|
|
||||||
factories: Record<string, PackBuilder>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Direction = "cw" | "ccw";
|
|
||||||
|
|
||||||
export interface I8PCubeSchema {
|
|
||||||
Schema: I8PPackSchema[];
|
|
||||||
ProblemPackSize: number;
|
|
||||||
Cards: Record<string, Card[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface I8PFileSchema {
|
|
||||||
Schema: I8PPackSchema[];
|
|
||||||
ProblemPackSize: number;
|
|
||||||
Cards: Record<string, string[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface I8PPackSchema {
|
|
||||||
Amount: number;
|
|
||||||
Type: string;
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { Database } from "./database";
|
|
||||||
|
|
||||||
const imgBaseURL = "https://mcg.zyg.ovh/images/cards/";
|
|
||||||
let imageSource: "local" | "remote" = "remote";
|
|
||||||
|
|
||||||
export function remoteImageURL(cardid: string): string {
|
|
||||||
return `${imgBaseURL}${cardid}.webp`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cardImageURL(cardid: string): Promise<string> {
|
|
||||||
if (!Database) {
|
|
||||||
return remoteImageURL(cardid);
|
|
||||||
}
|
|
||||||
switch (cardImageSource()) {
|
|
||||||
case "local": {
|
|
||||||
const card = await Database.images.get(`${cardid}.webp`);
|
|
||||||
if (!card) {
|
|
||||||
return remoteImageURL(cardid);
|
|
||||||
}
|
|
||||||
return URL.createObjectURL(card.image);
|
|
||||||
}
|
|
||||||
//TODO
|
|
||||||
case "remote":
|
|
||||||
return remoteImageURL(cardid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cardImageSource() {
|
|
||||||
return imageSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function refreshCardSource() {
|
|
||||||
if (!Database) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const count = await Database.images.count();
|
|
||||||
imageSource = count > 1900 ? "local" : "remote";
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
export * from "./card";
|
|
||||||
export * from "./database";
|
|
||||||
export * from "./set";
|
|
||||||
export * from "./types";
|
|
||||||
export * from "./images";
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { SetFile } from "./types";
|
import { SetFile } from "./types";
|
||||||
import { Database } from "./database";
|
import { Database } from "./database";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const baseURL = "https://mcg.zyg.ovh/setdata/";
|
const baseURL = "https://mcg.zyg.ovh/setdata/";
|
||||||
export const allSets = [
|
const allSets = [
|
||||||
"PR",
|
"PR",
|
||||||
"CN",
|
"CN",
|
||||||
"RR",
|
"RR",
|
||||||
|
@ -19,27 +18,7 @@ export const allSets = [
|
||||||
"Promo"
|
"Promo"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const setNames = {
|
|
||||||
PR: "Premiere",
|
|
||||||
CN: "Canterlot Nights",
|
|
||||||
RR: "Rock and Rave",
|
|
||||||
CS: "Celestial Solstice",
|
|
||||||
CG: "Crystal Games",
|
|
||||||
AD: "Absolute Discord",
|
|
||||||
EO: "Equestrian Odysseys",
|
|
||||||
HM: "High Magic",
|
|
||||||
MT: "Marks In Time",
|
|
||||||
DE: "Defenders of Equestria",
|
|
||||||
SB: "Seaquestria and Beyond",
|
|
||||||
FF: "Friends Forever",
|
|
||||||
GF: "Promotional",
|
|
||||||
ST: "Sands of Time"
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function loadSets() {
|
export async function loadSets() {
|
||||||
if (Database == null) {
|
|
||||||
throw new Error("Database was not initialized, init with 'initDB()'");
|
|
||||||
}
|
|
||||||
const itemcount = await Database.cards.count();
|
const itemcount = await Database.cards.count();
|
||||||
if (itemcount > 100) {
|
if (itemcount > 100) {
|
||||||
// DB already filled, exit early
|
// DB already filled, exit early
|
||||||
|
@ -48,19 +27,18 @@ export async function loadSets() {
|
||||||
const sets = await Promise.all(allSets.map(set => downloadSet(set)));
|
const sets = await Promise.all(allSets.map(set => downloadSet(set)));
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
sets.map(async set => {
|
sets.map(async set => {
|
||||||
if (Database == null) {
|
console.log(`Processing cards from ${set.Name}`);
|
||||||
throw new Error("Database was not initialized, init with 'initDB()'");
|
|
||||||
}
|
|
||||||
return await Database.cards.bulkPut(set.Cards);
|
return await Database.cards.bulkPut(set.Cards);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadSet(setid: string): Promise<SetFile> {
|
async function downloadSet(setid: string): Promise<SetFile> {
|
||||||
const setdata = await axios(`${baseURL}${setid.toLowerCase()}.json`);
|
const setfile = await fetch(`${baseURL}${setid.toLowerCase()}.json`);
|
||||||
setdata.data.Cards = (setdata.data as SetFile).Cards.map(c => {
|
const setdata: SetFile = await setfile.json();
|
||||||
|
setdata.Cards = setdata.Cards.map(c => {
|
||||||
c.Set = setid;
|
c.Set = setid;
|
||||||
return c;
|
return c;
|
||||||
});
|
});
|
||||||
return setdata.data;
|
return setdata;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,6 @@ export type Rarity = "C" | "U" | "R" | "SR" | "UR" | "RR";
|
||||||
|
|
||||||
export type PowerRequirement = { [key: string]: number };
|
export type PowerRequirement = { [key: string]: number };
|
||||||
|
|
||||||
export interface StoredImage {
|
|
||||||
id: string;
|
|
||||||
image: Blob;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SetFile {
|
export interface SetFile {
|
||||||
Name: string;
|
Name: string;
|
||||||
Cards: Card[];
|
Cards: Card[];
|
||||||
|
@ -43,9 +38,3 @@ export interface CardFilter {
|
||||||
Powers?: number[];
|
Powers?: number[];
|
||||||
Rarities?: string[];
|
Rarities?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardSlot {
|
|
||||||
data: Card;
|
|
||||||
limit: number;
|
|
||||||
howmany: number;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import {
|
||||||
|
PeerMetadata,
|
||||||
|
NetworkMessage,
|
||||||
|
PasswordResponse,
|
||||||
|
RoomInfo
|
||||||
|
} from "./types";
|
||||||
import EventEmitter from "eventemitter3";
|
import EventEmitter from "eventemitter3";
|
||||||
import Vue from "vue";
|
|
||||||
|
|
||||||
import { NetworkMessage, PasswordResponse, PeerMetadata, RoomInfo } from "./types";
|
|
||||||
|
|
||||||
export abstract class Client extends EventEmitter {
|
export abstract class Client extends EventEmitter {
|
||||||
public metadata: PeerMetadata;
|
public metadata: PeerMetadata;
|
||||||
|
@ -20,19 +23,13 @@ export abstract class Client extends EventEmitter {
|
||||||
case "room-info":
|
case "room-info":
|
||||||
this.roomInfo = data.room;
|
this.roomInfo = data.room;
|
||||||
this.players = data.players;
|
this.players = data.players;
|
||||||
this.emit("handshake");
|
|
||||||
break;
|
break;
|
||||||
// Someone changed name (or was forced to)
|
// Someone changed name (or was forced to)
|
||||||
case "rename":
|
case "rename":
|
||||||
if (data.oldname == this.metadata.name) {
|
if (data.oldname == this.metadata.name) {
|
||||||
// We got a name change!
|
// We got a name change!
|
||||||
this.metadata.name = data.newname;
|
this.metadata.name = data.newname;
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// Only mutate player list if we have one
|
|
||||||
// This is because rename messages can be received during the initial
|
|
||||||
// handshake, to signal a forced name change before joining.
|
|
||||||
if (this.players) {
|
|
||||||
let idx = this.players.indexOf(data.oldname);
|
let idx = this.players.indexOf(data.oldname);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
// Weird
|
// Weird
|
||||||
|
@ -42,7 +39,7 @@ export abstract class Client extends EventEmitter {
|
||||||
this.players.push(data.newname);
|
this.players.push(data.newname);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Vue.set(this.players, idx, data.newname);
|
this.players[idx] = data.newname;
|
||||||
}
|
}
|
||||||
this.emit("rename", data.oldname, data.newname);
|
this.emit("rename", data.oldname, data.newname);
|
||||||
break;
|
break;
|
||||||
|
@ -51,7 +48,7 @@ export abstract class Client extends EventEmitter {
|
||||||
this.players.push(data.name);
|
this.players.push(data.name);
|
||||||
this.emit("player-joined", data.name);
|
this.emit("player-joined", data.name);
|
||||||
break;
|
break;
|
||||||
case "player-left": {
|
case "player-left":
|
||||||
let idx = this.players.indexOf(data.name);
|
let idx = this.players.indexOf(data.name);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
// Weird
|
// Weird
|
||||||
|
@ -60,13 +57,9 @@ export abstract class Client extends EventEmitter {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.emit("player-left", data.name);
|
|
||||||
this.players.splice(idx, 1);
|
this.players.splice(idx, 1);
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "password-req":
|
case "password-req":
|
||||||
this.emit("password-required");
|
this.emit("password-required");
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
// For most cases, we can just use the kind as event type
|
// For most cases, we can just use the kind as event type
|
||||||
this.emit(data.kind, data);
|
this.emit(data.kind, data);
|
||||||
|
|
|
@ -25,12 +25,6 @@ export class PeerClient extends Client {
|
||||||
this.connection.on("open", () => {
|
this.connection.on("open", () => {
|
||||||
this.emit("connected");
|
this.emit("connected");
|
||||||
});
|
});
|
||||||
this.connection.on("close", () => {
|
|
||||||
this.emit("disconnected");
|
|
||||||
});
|
|
||||||
this.connection.on("error", err => {
|
|
||||||
this.emit("error", err);
|
|
||||||
});
|
|
||||||
this.connection.on("data", data => {
|
this.connection.on("data", data => {
|
||||||
this._received(data);
|
this._received(data);
|
||||||
});
|
});
|
||||||
|
@ -40,10 +34,6 @@ export class PeerClient extends Client {
|
||||||
return this.metadata.name;
|
return this.metadata.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get id(): string {
|
|
||||||
return this.peer.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public send<T extends NetworkMessage>(data: T) {
|
public send<T extends NetworkMessage>(data: T) {
|
||||||
if (!this.connection) {
|
if (!this.connection) {
|
||||||
throw new Error("Client is not connected to a server");
|
throw new Error("Client is not connected to a server");
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
import EventEmitter from "eventemitter3";
|
|
||||||
import Peer, { DataConnection } from "peerjs";
|
import Peer, { DataConnection } from "peerjs";
|
||||||
|
|
||||||
import { LocalClient } from ".";
|
|
||||||
import {
|
import {
|
||||||
AckMessage,
|
RoomInfo,
|
||||||
ChatMessage,
|
|
||||||
ErrorMessage,
|
|
||||||
JoinMessage,
|
|
||||||
LeaveMessage,
|
|
||||||
NetworkMessage,
|
|
||||||
NetworkPlayer,
|
|
||||||
PasswordRequest,
|
PasswordRequest,
|
||||||
|
Room,
|
||||||
|
ErrorMessage,
|
||||||
PasswordResponse,
|
PasswordResponse,
|
||||||
PeerMetadata,
|
PeerMetadata,
|
||||||
Player,
|
JoinMessage,
|
||||||
RenameMessage,
|
|
||||||
Room,
|
|
||||||
RoomInfo,
|
|
||||||
RoomInfoMessage,
|
RoomInfoMessage,
|
||||||
|
Player,
|
||||||
|
NetworkMessage,
|
||||||
|
RenameMessage,
|
||||||
|
LeaveMessage,
|
||||||
|
NetworkPlayer,
|
||||||
|
AckMessage,
|
||||||
|
ChatMessage
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { LocalClient } from ".";
|
||||||
|
import EventEmitter from "eventemitter3";
|
||||||
|
|
||||||
// Increment name, add number at the end if not present
|
// Increment name, add number at the end if not present
|
||||||
// Examples:
|
// Examples:
|
||||||
|
@ -35,12 +34,6 @@ function nextName(name: string): string {
|
||||||
return name.substr(0, i) + (Number(name.slice(i)) + 1);
|
return name.substr(0, i) + (Number(name.slice(i)) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectionOpen(conn: DataConnection): Promise<void> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
conn.on("open", () => resolve());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PeerServer extends EventEmitter {
|
export class PeerServer extends EventEmitter {
|
||||||
protected peer: Peer;
|
protected peer: Peer;
|
||||||
private room: Room;
|
private room: Room;
|
||||||
|
@ -71,21 +64,17 @@ export class PeerServer extends EventEmitter {
|
||||||
|
|
||||||
// Setup peer
|
// Setup peer
|
||||||
this.peer = customPeer ? customPeer : new Peer();
|
this.peer = customPeer ? customPeer : new Peer();
|
||||||
this.peer.on("open", id => {
|
this.peer.on("open", function(id) {
|
||||||
console.info("Peer ID assigned: %s", id);
|
console.info("Peer ID assigned: %s", id);
|
||||||
this.emit("open", id);
|
|
||||||
});
|
});
|
||||||
this.peer.on("connection", conn => {
|
this.peer.on("connection", conn => {
|
||||||
this._connection(conn);
|
this._connection(conn);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _connection(conn: DataConnection) {
|
private _connection(conn: DataConnection) {
|
||||||
const metadata = conn.metadata as PeerMetadata;
|
const metadata = conn.metadata as PeerMetadata;
|
||||||
|
|
||||||
// Wait for connection to be open
|
|
||||||
await connectionOpen(conn);
|
|
||||||
|
|
||||||
let player: NetworkPlayer = {
|
let player: NetworkPlayer = {
|
||||||
kind: "remote",
|
kind: "remote",
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
|
@ -151,12 +140,6 @@ export class PeerServer extends EventEmitter {
|
||||||
|
|
||||||
private addPlayer(player: NetworkPlayer) {
|
private addPlayer(player: NetworkPlayer) {
|
||||||
const playerName = player.name;
|
const playerName = player.name;
|
||||||
|
|
||||||
// Hacky: Give player list before this player was added, so join
|
|
||||||
// message doesn't mess things up later
|
|
||||||
const players = Object.keys(this.room.players);
|
|
||||||
|
|
||||||
// Add player to room
|
|
||||||
this.room.players[playerName] = player;
|
this.room.players[playerName] = player;
|
||||||
|
|
||||||
// Start listening for new messages
|
// Start listening for new messages
|
||||||
|
@ -165,10 +148,6 @@ export class PeerServer extends EventEmitter {
|
||||||
this._received.bind(this, this.room.players[playerName])
|
this._received.bind(this, this.room.players[playerName])
|
||||||
);
|
);
|
||||||
|
|
||||||
player.conn.on("error", err => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send the player info about the room
|
// Send the player info about the room
|
||||||
this.send<RoomInfoMessage>(player, {
|
this.send<RoomInfoMessage>(player, {
|
||||||
kind: "room-info",
|
kind: "room-info",
|
||||||
|
@ -176,7 +155,7 @@ export class PeerServer extends EventEmitter {
|
||||||
...this.room.info,
|
...this.room.info,
|
||||||
password: ""
|
password: ""
|
||||||
},
|
},
|
||||||
players
|
players: Object.keys(this.room.players)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify other players
|
// Notify other players
|
||||||
|
@ -193,9 +172,6 @@ export class PeerServer extends EventEmitter {
|
||||||
// Close connection with player
|
// Close connection with player
|
||||||
player.conn.close();
|
player.conn.close();
|
||||||
|
|
||||||
// Remove player from player list
|
|
||||||
delete this.room.players[player.name];
|
|
||||||
|
|
||||||
// Notify other players
|
// Notify other players
|
||||||
this.broadcast<LeaveMessage>({
|
this.broadcast<LeaveMessage>({
|
||||||
kind: "player-left",
|
kind: "player-left",
|
||||||
|
@ -213,30 +189,16 @@ export class PeerServer extends EventEmitter {
|
||||||
this.broadcast(data);
|
this.broadcast(data);
|
||||||
} else {
|
} else {
|
||||||
// Player is telling someone specifically
|
// Player is telling someone specifically
|
||||||
if (!(data.to in this.players)) {
|
if (data.to in this.players) {
|
||||||
|
this.send<ChatMessage>(this.players[data.to], data);
|
||||||
|
} else {
|
||||||
this.send<ErrorMessage>(player, {
|
this.send<ErrorMessage>(player, {
|
||||||
kind: "error",
|
kind: "error",
|
||||||
error: `player not found: ${data.to}`
|
error: `player not found: ${data.to}`
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this.send<ChatMessage>(this.players[data.to], data);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// Player wants to change name
|
|
||||||
case "rename":
|
|
||||||
// Make sure new name is valid
|
|
||||||
data.oldname = player.name;
|
|
||||||
if (data.newname in this.players) {
|
|
||||||
this.send<ErrorMessage>(player, {
|
|
||||||
kind: "error",
|
|
||||||
error: "name not available"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
player.name = data.newname;
|
|
||||||
this.broadcast<RenameMessage>(data);
|
|
||||||
break;
|
|
||||||
// Player is leaving!
|
// Player is leaving!
|
||||||
case "leave-req":
|
case "leave-req":
|
||||||
// If we're leaving, end the server
|
// If we're leaving, end the server
|
||||||
|
@ -272,10 +234,6 @@ export class PeerServer extends EventEmitter {
|
||||||
this.send<T>(player, message);
|
this.send<T>(player, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get id(): string {
|
|
||||||
return this.peer.id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PeerServer;
|
export default PeerServer;
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
"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,11 +1,11 @@
|
||||||
import DeckBuilder from "@/views/DeckBuilder.vue";
|
|
||||||
import DraftView from "@/views/Draft.vue";
|
|
||||||
import GameView from "@/views/Game.vue";
|
|
||||||
import Home from "@/views/Home.vue";
|
|
||||||
import Lobby from "@/views/Lobby.vue";
|
|
||||||
import SettingsView from "@/views/Settings.vue";
|
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import Router from "vue-router";
|
import Router from "vue-router";
|
||||||
|
import Home from "@/views/Home.vue";
|
||||||
|
import DeckBuilder from "@/views/DeckBuilder.vue";
|
||||||
|
import GameView from "@/views/Game.vue";
|
||||||
|
import DraftView from "@/views/Draft.vue";
|
||||||
|
import Lobby from "@/views/Lobby.vue";
|
||||||
|
import RoomView from "@/views/Room.vue";
|
||||||
|
|
||||||
Vue.use(Router);
|
Vue.use(Router);
|
||||||
|
|
||||||
|
@ -21,10 +21,7 @@ export default new Router({
|
||||||
{
|
{
|
||||||
path: "/build",
|
path: "/build",
|
||||||
name: "deck-editor",
|
name: "deck-editor",
|
||||||
component: DeckBuilder,
|
component: DeckBuilder
|
||||||
meta: {
|
|
||||||
topnav: "Deck Builder"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/game",
|
path: "/game",
|
||||||
|
@ -39,26 +36,12 @@ export default new Router({
|
||||||
{
|
{
|
||||||
path: "/lobby",
|
path: "/lobby",
|
||||||
name: "lobby",
|
name: "lobby",
|
||||||
component: Lobby,
|
component: Lobby
|
||||||
meta: {
|
|
||||||
topnav: "Lobby"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/join/:id",
|
path: "/room",
|
||||||
name: "lobby-join",
|
name: "room",
|
||||||
component: Lobby,
|
component: RoomView
|
||||||
meta: {
|
|
||||||
topnav: "Lobby"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/settings",
|
|
||||||
name: "settings",
|
|
||||||
component: SettingsView,
|
|
||||||
meta: {
|
|
||||||
topnav: "settings"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
8
src/shims-worker.d.ts
vendored
|
@ -1,8 +0,0 @@
|
||||||
// typings/custom.d.ts
|
|
||||||
declare module "worker-loader!*" {
|
|
||||||
class WebpackWorker extends Worker {
|
|
||||||
constructor();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WebpackWorker;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { ActionTree } from "vuex";
|
|
||||||
import { AppState } from "../types";
|
|
||||||
import { DraftState } from "./types";
|
|
||||||
import { Card } from "@/mlpccg";
|
|
||||||
|
|
||||||
const actions: ActionTree<DraftState, AppState> = {
|
|
||||||
pickCard({ commit }, card: Card) {
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default actions;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { GetterTree } from "vuex";
|
|
||||||
import { AppState } from "../types";
|
|
||||||
import { DraftState } from "./types";
|
|
||||||
import { createPonyheadURL } from "@/mlpccg";
|
|
||||||
|
|
||||||
const getters: GetterTree<DraftState, AppState> = {
|
|
||||||
ponyheadURL(state): string {
|
|
||||||
return createPonyheadURL(state.picks);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getters;
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { DraftState } from "./types";
|
|
||||||
import { AppState } from "../types";
|
|
||||||
import { Module } from "vuex";
|
|
||||||
|
|
||||||
const namespaced = true;
|
|
||||||
|
|
||||||
import actions from "./actions";
|
|
||||||
import mutations from "./mutations";
|
|
||||||
import getters from "./getters";
|
|
||||||
|
|
||||||
export const state: DraftState = {
|
|
||||||
cards: [],
|
|
||||||
picks: [],
|
|
||||||
pod: [],
|
|
||||||
packCount: 0,
|
|
||||||
currentPack: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
export const draft: Module<DraftState, AppState> = {
|
|
||||||
namespaced,
|
|
||||||
state,
|
|
||||||
actions,
|
|
||||||
mutations,
|
|
||||||
getters
|
|
||||||
};
|
|
||||||
|
|
||||||
export default draft;
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { MutationTree } from "vuex";
|
|
||||||
import { DraftState, PlayerStatus } from "./types";
|
|
||||||
import { Card } from "@/mlpccg";
|
|
||||||
|
|
||||||
const mutations: MutationTree<DraftState> = {
|
|
||||||
playerPicked(state, payload: { name: string; picked: boolean }) {
|
|
||||||
const idx = state.pod.findIndex(p => p.name == payload.name);
|
|
||||||
state.pod[idx].picked = payload.picked;
|
|
||||||
},
|
|
||||||
|
|
||||||
resetPickStatus(state) {
|
|
||||||
state.pod = state.pod.map(p => ({ ...p, picked: false }));
|
|
||||||
},
|
|
||||||
|
|
||||||
setCardPool(state, pool: Card[]) {
|
|
||||||
state.cards = pool;
|
|
||||||
},
|
|
||||||
|
|
||||||
setPackInfo(state, payload: { current: number; total: number }) {
|
|
||||||
state.currentPack = payload.current;
|
|
||||||
state.packCount = payload.total;
|
|
||||||
},
|
|
||||||
|
|
||||||
addPicks(state, pick: Card) {
|
|
||||||
state.picks.push(pick);
|
|
||||||
},
|
|
||||||
|
|
||||||
setPod(state, pod: PlayerStatus[]) {
|
|
||||||
state.pod = pod;
|
|
||||||
},
|
|
||||||
|
|
||||||
resetPicks(state) {
|
|
||||||
state.picks = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default mutations;
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { Session } from "@/mlpccg/draft";
|
|
||||||
import { Card } from "@/mlpccg";
|
|
||||||
|
|
||||||
export interface DraftState {
|
|
||||||
session?: Session;
|
|
||||||
|
|
||||||
pod: PlayerStatus[];
|
|
||||||
cards: Card[];
|
|
||||||
picks: Card[];
|
|
||||||
|
|
||||||
// Multiple pack draft
|
|
||||||
packCount: number;
|
|
||||||
currentPack: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlayerStatus {
|
|
||||||
name: string;
|
|
||||||
isBot: boolean;
|
|
||||||
isMe: boolean;
|
|
||||||
picked: boolean;
|
|
||||||
}
|
|
|
@ -8,9 +8,6 @@ import actions from "./actions";
|
||||||
import mutations from "./mutations";
|
import mutations from "./mutations";
|
||||||
import getters from "./getters";
|
import getters from "./getters";
|
||||||
|
|
||||||
import network from "./network";
|
|
||||||
import draft from "./draft";
|
|
||||||
|
|
||||||
const store: StoreOptions<AppState> = {
|
const store: StoreOptions<AppState> = {
|
||||||
state: {
|
state: {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
@ -20,10 +17,7 @@ const store: StoreOptions<AppState> = {
|
||||||
actions,
|
actions,
|
||||||
mutations,
|
mutations,
|
||||||
getters,
|
getters,
|
||||||
modules: {
|
modules: {}
|
||||||
network,
|
|
||||||
draft
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default new Vuex.Store<AppState>(store);
|
export default new Vuex.Store<AppState>(store);
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
import { ChatMessage, Client, LocalClient, NetworkMessage, PeerClient, PeerServer } from "@/network";
|
|
||||||
import { ActionTree, Commit } from "vuex";
|
|
||||||
|
|
||||||
import { AppState } from "../types";
|
|
||||||
import { ConnectOptions, NetworkState, StartServerOptions } from "./types";
|
|
||||||
|
|
||||||
function bindClientEvents(commit: Commit, client: Client) {
|
|
||||||
client.on("handshake", () => {
|
|
||||||
commit("playerListChanged", client.players);
|
|
||||||
});
|
|
||||||
client.on("player-joined", () => commit("playerListChanged", client.players));
|
|
||||||
client.on("player-left", () => commit("playerListChanged", client.players));
|
|
||||||
client.on("rename", () => commit("playerListChanged", client.players));
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions: ActionTree<NetworkState, AppState> = {
|
|
||||||
startServer({ commit }, options: StartServerOptions) {
|
|
||||||
const local = new LocalClient(options.playerInfo);
|
|
||||||
const server = new PeerServer(options.roomInfo, local, options._customPeer);
|
|
||||||
server.once("open", id => {
|
|
||||||
commit("serverAssignedID", id);
|
|
||||||
});
|
|
||||||
bindClientEvents(commit, local);
|
|
||||||
commit("becomeServer", { local, server });
|
|
||||||
},
|
|
||||||
|
|
||||||
connect({ commit }, options: ConnectOptions) {
|
|
||||||
const client = new PeerClient(options.playerInfo, options._customPeer);
|
|
||||||
commit("becomeClient", { peer: client, id: options.serverID });
|
|
||||||
client.on("connected", () => {
|
|
||||||
commit("connectionStatusChanged", "connected");
|
|
||||||
});
|
|
||||||
client.on("disconnected", () => {
|
|
||||||
commit("connectionStatusChanged", "disconnected");
|
|
||||||
});
|
|
||||||
client.on("error", err => {
|
|
||||||
commit("connectionError", err);
|
|
||||||
});
|
|
||||||
bindClientEvents(commit, client);
|
|
||||||
client.connect(options.serverID);
|
|
||||||
},
|
|
||||||
|
|
||||||
sendChatMessage({ commit, dispatch, getters }, message: ChatMessage) {
|
|
||||||
if (getters.connectionType == "none") {
|
|
||||||
throw new Error("not connected");
|
|
||||||
}
|
|
||||||
dispatch("sendMessage", message);
|
|
||||||
commit("receivedChatMessage", message);
|
|
||||||
},
|
|
||||||
|
|
||||||
sendMessage({ getters }, message: NetworkMessage) {
|
|
||||||
if (getters.connectionType == "none") {
|
|
||||||
throw new Error("not connected");
|
|
||||||
}
|
|
||||||
(getters.client as Client).send(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default actions;
|
|
|
@ -1,60 +0,0 @@
|
||||||
import { Client } from "@/network";
|
|
||||||
import { GetterTree } from "vuex";
|
|
||||||
|
|
||||||
import { AppState } from "../types";
|
|
||||||
import { NetworkState } from "./types";
|
|
||||||
|
|
||||||
const getters: GetterTree<NetworkState, AppState> = {
|
|
||||||
peerID(state): string | null {
|
|
||||||
switch (state.peerType) {
|
|
||||||
case "server":
|
|
||||||
return state.server.id;
|
|
||||||
case "client":
|
|
||||||
return state.peer.id;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
sessionID(state): string | null {
|
|
||||||
return state.serverID;
|
|
||||||
},
|
|
||||||
|
|
||||||
client(state): Client | null {
|
|
||||||
switch (state.peerType) {
|
|
||||||
case "server":
|
|
||||||
return state.local;
|
|
||||||
case "client":
|
|
||||||
return state.peer;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
connectionType(state): "client" | "server" | "none" {
|
|
||||||
return state.peerType;
|
|
||||||
},
|
|
||||||
|
|
||||||
busy(state): boolean {
|
|
||||||
if (state.peerType == "client") {
|
|
||||||
if (state.connectionStatus == "connecting") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
inRoom(state): boolean {
|
|
||||||
if (state.peerType == "client") {
|
|
||||||
return state.connectionStatus == "connected";
|
|
||||||
}
|
|
||||||
if (state.peerType == "server") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
players(state): string[] {
|
|
||||||
return state.players;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getters;
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { NetworkState } from "./types";
|
|
||||||
import { AppState } from "../types";
|
|
||||||
import { Module } from "vuex";
|
|
||||||
|
|
||||||
import actions from "./actions";
|
|
||||||
import mutations from "./mutations";
|
|
||||||
import getters from "./getters";
|
|
||||||
|
|
||||||
const namespaced = true;
|
|
||||||
|
|
||||||
export const state: NetworkState = {
|
|
||||||
peerType: "none",
|
|
||||||
connectionStatus: null,
|
|
||||||
peer: null,
|
|
||||||
server: null,
|
|
||||||
local: null,
|
|
||||||
serverID: null,
|
|
||||||
players: [],
|
|
||||||
chatLog: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export const network: Module<NetworkState, AppState> = {
|
|
||||||
namespaced,
|
|
||||||
state,
|
|
||||||
actions,
|
|
||||||
mutations,
|
|
||||||
getters
|
|
||||||
};
|
|
||||||
|
|
||||||
export default network;
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { ChatMessage, LocalClient, PeerClient, PeerServer } from "@/network";
|
|
||||||
import Vue from "vue";
|
|
||||||
import { MutationTree } from "vuex";
|
|
||||||
|
|
||||||
import { ClientNetworkState, ConnectionStatus, NetworkState, ServerNetworkState } from "./types";
|
|
||||||
|
|
||||||
const mutations: MutationTree<NetworkState> = {
|
|
||||||
becomeServer(state, payload: { local: LocalClient; server: PeerServer }) {
|
|
||||||
state.peerType = "server";
|
|
||||||
state.players = [payload.local.name];
|
|
||||||
(state as ServerNetworkState).local = payload.local;
|
|
||||||
(state as ServerNetworkState).server = payload.server;
|
|
||||||
},
|
|
||||||
|
|
||||||
becomeClient(state, payload: { peer: PeerClient; id: string }) {
|
|
||||||
state.peerType = "client";
|
|
||||||
(state as ClientNetworkState).connectionStatus = "connecting";
|
|
||||||
(state as ClientNetworkState).peer = payload.peer;
|
|
||||||
(state as ClientNetworkState).serverID = payload.id;
|
|
||||||
},
|
|
||||||
|
|
||||||
connectionStatusChanged(state, status: ConnectionStatus) {
|
|
||||||
(state as ClientNetworkState).connectionStatus = status;
|
|
||||||
},
|
|
||||||
|
|
||||||
connectionError(state, error) {
|
|
||||||
(state as ClientNetworkState).connectionStatus = "error";
|
|
||||||
(state as ClientNetworkState).connectionError = error;
|
|
||||||
},
|
|
||||||
|
|
||||||
receivedChatMessage(state, message: ChatMessage) {
|
|
||||||
state.chatLog.push(message);
|
|
||||||
},
|
|
||||||
|
|
||||||
serverAssignedID(state, id: string) {
|
|
||||||
state.serverID = id;
|
|
||||||
},
|
|
||||||
|
|
||||||
playerListChanged(state, players: string[]) {
|
|
||||||
Vue.set(state, "players", players);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default mutations;
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { ChatMessage, LocalClient, PeerClient, PeerMetadata, PeerServer, RoomInfo } from "@/network";
|
|
||||||
import Peer from "peerjs";
|
|
||||||
|
|
||||||
export type ConnectionStatus =
|
|
||||||
| "connecting"
|
|
||||||
| "connected"
|
|
||||||
| "disconnected"
|
|
||||||
| "error";
|
|
||||||
|
|
||||||
export interface SharedNetworkState {
|
|
||||||
chatLog: ChatMessage[];
|
|
||||||
serverID: string | null;
|
|
||||||
players: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NoNetworkState extends SharedNetworkState {
|
|
||||||
peerType: "none";
|
|
||||||
connectionStatus: null;
|
|
||||||
connectionError?: Error;
|
|
||||||
peer: null;
|
|
||||||
server: null;
|
|
||||||
local: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientNetworkState extends SharedNetworkState {
|
|
||||||
peerType: "client";
|
|
||||||
connectionStatus: ConnectionStatus;
|
|
||||||
connectionError?: Error;
|
|
||||||
peer: PeerClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerNetworkState extends SharedNetworkState {
|
|
||||||
peerType: "server";
|
|
||||||
server: PeerServer;
|
|
||||||
local: LocalClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NetworkState =
|
|
||||||
| NoNetworkState
|
|
||||||
| ClientNetworkState
|
|
||||||
| ServerNetworkState;
|
|
||||||
|
|
||||||
export interface StartServerOptions {
|
|
||||||
roomInfo: RoomInfo;
|
|
||||||
playerInfo: PeerMetadata;
|
|
||||||
|
|
||||||
// Testing utils
|
|
||||||
_customPeer?: Peer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConnectOptions {
|
|
||||||
serverID: string;
|
|
||||||
playerInfo: PeerMetadata;
|
|
||||||
|
|
||||||
// Testing utils
|
|
||||||
_customPeer?: Peer;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import Dexie from "dexie";
|
|
||||||
|
|
||||||
let init = false;
|
|
||||||
|
|
||||||
export function setupIDBShim() {
|
|
||||||
if (!init) {
|
|
||||||
const setGlobalVars = require("indexeddbshim");
|
|
||||||
setGlobalVars(Dexie.dependencies);
|
|
||||||
init = true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,3 @@
|
||||||
export * from "./MockDataConnection";
|
export * from "./MockDataConnection";
|
||||||
export * from "./MockPeer";
|
export * from "./MockPeer";
|
||||||
export * from "./MockHelper";
|
export * from "./MockHelper";
|
||||||
export * from "./EventHook";
|
|
||||||
export * from "./IDBShim";
|
|
||||||
export * from "./sync-utils";
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
export function seconds(ms: number): Promise<void> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
setTimeout(resolve, ms * 1000);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -2,4 +2,4 @@ module.exports = {
|
||||||
env: {
|
env: {
|
||||||
jest: true
|
jest: true
|
||||||
}
|
}
|
||||||
};
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,52 +0,0 @@
|
||||||
import CardPicker from "@/components/DeckBuilder/CardPicker.vue";
|
|
||||||
import CardImage from "@/components/Cards/CardImage.vue";
|
|
||||||
import { shallowMount, mount } from "@vue/test-utils";
|
|
||||||
import { seconds } from "@/testing";
|
|
||||||
|
|
||||||
// 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 instances images for each card", () => {
|
|
||||||
const wrapper = mount(CardPicker, {
|
|
||||||
propsData: {
|
|
||||||
rows: 2,
|
|
||||||
columns: 5,
|
|
||||||
cards: testSlots
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const cards = wrapper.findAll(".ccgcard");
|
|
||||||
expect(cards.contains("img")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("CardImage correctly resolves to an URL after a while", async () => {
|
|
||||||
const wrapper = mount(CardImage, {
|
|
||||||
propsData: {
|
|
||||||
id: "sb1"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let src = wrapper.attributes("src");
|
|
||||||
expect(src).toBe(""); // Should be placeholder but it gets stubbed
|
|
||||||
await seconds(0.5);
|
|
||||||
src = wrapper.attributes("src");
|
|
||||||
expect(src).toMatch(/^https?:|^blob:/);
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,36 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { loadSets, getCards, Database, initDB, cardFullName } from "@/mlpccg";
|
|
||||||
import { setupIDBShim } from "@/testing";
|
|
||||||
|
|
||||||
setupIDBShim();
|
|
||||||
|
|
||||||
describe("mlpccg/Database", () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
jest.setTimeout(15000);
|
|
||||||
await 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,131 +0,0 @@
|
||||||
import { setupIDBShim, EventHook } from "@/testing";
|
|
||||||
import { initDB, loadSets, Database, Card } from "@/mlpccg";
|
|
||||||
import {
|
|
||||||
PackBuilder,
|
|
||||||
spanByRarity,
|
|
||||||
Cube,
|
|
||||||
Session,
|
|
||||||
DraftOptions
|
|
||||||
} from "@/mlpccg/draft";
|
|
||||||
|
|
||||||
setupIDBShim();
|
|
||||||
|
|
||||||
const testSessionOptions: DraftOptions = {
|
|
||||||
type: "booster-draft",
|
|
||||||
source: "set",
|
|
||||||
set: "FF",
|
|
||||||
packs: 2,
|
|
||||||
players: 4,
|
|
||||||
spacing: "evenly"
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("mlpccg/draft", () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
jest.setTimeout(30000);
|
|
||||||
await initDB();
|
|
||||||
await loadSets();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Set booster packs are generated correctly", async () => {
|
|
||||||
expect(Database).toBeTruthy();
|
|
||||||
const builder = await PackBuilder.fromSet("FF");
|
|
||||||
const pack = builder.buildPack();
|
|
||||||
// Check pack size
|
|
||||||
expect(pack).toHaveLength(12);
|
|
||||||
const rarities = spanByRarity(pack);
|
|
||||||
// Check pack distribution
|
|
||||||
expect(rarities["R"]).toHaveLength(1);
|
|
||||||
expect(rarities["U"]).toHaveLength(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Cube can load a newline separated card list", async () => {
|
|
||||||
expect(Database).toBeTruthy();
|
|
||||||
const cubeCards = ["ff10", "ff11", "ff12", "ff13", "ff14", "ff15"];
|
|
||||||
const cubeList = cubeCards.join("\n");
|
|
||||||
const cube = await Cube.fromList(cubeList);
|
|
||||||
const builder = new PackBuilder(cube.schema());
|
|
||||||
const pack = builder.buildPack();
|
|
||||||
// Pack size should only be 6, since there are not enough cards for a 12 cards pack
|
|
||||||
expect(pack).toHaveLength(6);
|
|
||||||
// Make sure pack has ALL the cards from the pool, no duplicates
|
|
||||||
const sortedPack = pack.map(c => c.ID).sort();
|
|
||||||
expect(sortedPack).toEqual(cubeCards);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("A session can be initialized", async () => {
|
|
||||||
expect(Database).toBeTruthy();
|
|
||||||
const session = await Session.create(testSessionOptions);
|
|
||||||
const hook = new EventHook();
|
|
||||||
hook.hookEmitter(session, "start", "session-start");
|
|
||||||
session.assign(["test1", "test2"], () => {
|
|
||||||
// Do nothing
|
|
||||||
});
|
|
||||||
session.start();
|
|
||||||
await hook.expect("session-start");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Players receive pick events and can pick cards", async () => {
|
|
||||||
expect(Database).toBeTruthy();
|
|
||||||
const session = await Session.create(testSessionOptions);
|
|
||||||
const hook = new EventHook();
|
|
||||||
hook.hookEmitter(session, "start", "session-start");
|
|
||||||
hook.hookEmitter(session, "next-pack", "session-new-pack");
|
|
||||||
hook.hookEmitter(session, "next-pick", "session-new-pick");
|
|
||||||
hook.hookEmitter(session, "player-pick", "session-picked");
|
|
||||||
hook.hookEmitter(session, "draft-over", "session-done");
|
|
||||||
session.assign(["test1", "test2"], (name, player) => {
|
|
||||||
player.on("available-picks", cards => {
|
|
||||||
// setTimeout hack to avoid handlers being called before the rest of the code
|
|
||||||
setTimeout(() => player.pick(cards[0].ID), 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
session.start();
|
|
||||||
await hook.expect("session-start");
|
|
||||||
for (let i = 0; i < testSessionOptions.packs; i++) {
|
|
||||||
await hook.expect("session-new-pack");
|
|
||||||
for (let j = 0; j < 12; j++) {
|
|
||||||
for (let p = 0; p < testSessionOptions.players; p++) {
|
|
||||||
await hook.expect("session-picked");
|
|
||||||
}
|
|
||||||
if (i < testSessionOptions.packs - 1) {
|
|
||||||
await hook.expect("session-new-pick");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await hook.expect("session-done");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Sessions can load and draft I8PCube files", async () => {
|
|
||||||
expect(Database).toBeTruthy();
|
|
||||||
const session = await Session.create({
|
|
||||||
type: "booster-draft",
|
|
||||||
source: "i8pcube",
|
|
||||||
url: "https://mcg.zyg.ovh/cubes/hamchacube.json",
|
|
||||||
packs: 4,
|
|
||||||
players: 4,
|
|
||||||
spacing: "evenly"
|
|
||||||
});
|
|
||||||
const hook = new EventHook();
|
|
||||||
hook.hookEmitter(session, "start", "session-start");
|
|
||||||
session.assign(["test1"], (_, player) => {
|
|
||||||
hook.hookEmitter(player, "available-picks", "got-cards");
|
|
||||||
});
|
|
||||||
session.start();
|
|
||||||
await hook.expect("session-start");
|
|
||||||
await hook.expect("got-cards", 1000, (cards: Card[]) => {
|
|
||||||
expect(cards).toHaveLength(12);
|
|
||||||
// Check for 2 or more multicolor cards
|
|
||||||
const multicolor = cards.filter(
|
|
||||||
c =>
|
|
||||||
c.Element.length > 1 ||
|
|
||||||
(c.Requirement && Object.keys(c.Requirement).length > 1)
|
|
||||||
);
|
|
||||||
expect(multicolor.length).toBeGreaterThanOrEqual(2);
|
|
||||||
// Check for 2 or more entry cards
|
|
||||||
const entry = cards.filter(
|
|
||||||
c => !c.Requirement || c.Requirement.length < 1
|
|
||||||
);
|
|
||||||
expect(entry.length).toBeGreaterThanOrEqual(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { MockHelper, EventHook } from "@/testing";
|
import { MockHelper } from "@/testing";
|
||||||
import { NetworkMessage, LocalClient, ChatMessage } from "@/network";
|
import { NetworkMessage, LocalClient, ChatMessage } from "@/network";
|
||||||
|
import { EventHook } from "@/testing/EventHook";
|
||||||
|
|
||||||
const sampleRoom = () => ({
|
const sampleRoom = () => ({
|
||||||
max_players: 3,
|
max_players: 3,
|
||||||
|
|
|
@ -1,479 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="deckbuilder">
|
<section class="deckbuilder"></section>
|
||||||
<TopNav class="topnav" />
|
|
||||||
<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>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped></style>
|
||||||
@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);
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topnav {
|
|
||||||
grid-column: 1/3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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%;
|
|
||||||
column-gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
import DeckList from "@/components/DeckBuilder/DeckList.vue";
|
|
||||||
import CardPicker from "@/components/DeckBuilder/CardPicker.vue";
|
|
||||||
import TopNav from "@/components/Navigation/TopNav.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({
|
@Component({
|
||||||
components: {
|
components: {}
|
||||||
TopNav,
|
|
||||||
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>
|
</script>
|
||||||
|
|
|
@ -1,118 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="draftview">
|
<section class="draftview">
|
||||||
<section class="playerlist">
|
<section class="playerlist">
|
||||||
<header>
|
<b>Players</b>
|
||||||
<h2>Players</h2>
|
|
||||||
</header>
|
|
||||||
<section class="players">
|
|
||||||
<article
|
|
||||||
v-for="(player, i) in playerStatus"
|
|
||||||
:key="player.name + i"
|
|
||||||
:class="playerClass(player)"
|
|
||||||
>
|
|
||||||
<div class="icon">
|
|
||||||
<b-icon v-if="player.isBot" icon="robot" size="is-small" />
|
|
||||||
<b-icon v-else icon="account-box" size="is-small" />
|
|
||||||
</div>
|
|
||||||
<div class="name">{{ player.name }}</div>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
<section class="pool">
|
|
||||||
<header>
|
|
||||||
<h2>
|
|
||||||
Available picks (Pack
|
|
||||||
<span class="pack-number">{{ packNumber }}</span> of
|
|
||||||
<span class="pack-count">{{ packCount }}</span
|
|
||||||
>)
|
|
||||||
</h2>
|
|
||||||
</header>
|
|
||||||
<CardPicker
|
|
||||||
@picked="cardPicked"
|
|
||||||
:columns="columns"
|
|
||||||
:rows="rows"
|
|
||||||
:cards="currentPicks"
|
|
||||||
class="picker"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<section class="cardlist">
|
|
||||||
<header>
|
|
||||||
<h2>Cards</h2>
|
|
||||||
</header>
|
|
||||||
<DeckList :cards="pickedCards" />
|
|
||||||
</section>
|
</section>
|
||||||
|
<section class="pool"><b>Card pool</b></section>
|
||||||
|
<section class="cardlist"><b>Cards</b></section>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "@/assets/scss/_variables.scss";
|
|
||||||
|
|
||||||
$player-not-picked: $red;
|
|
||||||
$player-picked: $green;
|
|
||||||
$player-me: $purple;
|
|
||||||
$border-opacity: 0.6;
|
|
||||||
|
|
||||||
.draftview {
|
.draftview {
|
||||||
background: url("../assets/images/backgrounds/draftbg.webp") center;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
gap: 10px;
|
grid-gap: 10px;
|
||||||
grid-template-columns: minmax(200px, 1fr) 3fr minmax(250px, 1fr);
|
grid-template-columns: 200px 1fr 250px;
|
||||||
& > section {
|
section {
|
||||||
padding: 10px 20px;
|
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
border: 1px solid #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
header > h2 {
|
|
||||||
font-family: $fantasy;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 2.5vh;
|
|
||||||
padding: 1vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.playerlist {
|
|
||||||
.players {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column wrap;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
.player {
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 2px;
|
|
||||||
padding: 2px;
|
|
||||||
border: 2px solid rgba($player-not-picked, $border-opacity);
|
|
||||||
display: flex;
|
|
||||||
color: $red;
|
|
||||||
align-items: center;
|
|
||||||
user-select: all;
|
|
||||||
&.picked,
|
|
||||||
&.bot {
|
|
||||||
color: $player-picked;
|
|
||||||
border-color: rgba($player-picked, $border-opacity);
|
|
||||||
}
|
|
||||||
&.me {
|
|
||||||
border-color: rgba($player-me, $border-opacity);
|
|
||||||
}
|
|
||||||
&.me::after {
|
|
||||||
content: " (me)";
|
|
||||||
font-size: 10pt;
|
|
||||||
margin-left: 5px;
|
|
||||||
color: $player-me;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pool {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.picker {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pack-number {
|
|
||||||
color: $primary;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
|
@ -149,95 +54,9 @@ $border-opacity: 0.6;
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
import { getCards, CardSlot, cardLimit, Card } from "@/mlpccg";
|
|
||||||
import CardPicker from "@/components/DeckBuilder/CardPicker.vue";
|
|
||||||
import DeckList from "@/components/DeckBuilder/DeckList.vue";
|
|
||||||
import { PlayerStatus } from "@/store/draft/types";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {}
|
||||||
CardPicker,
|
|
||||||
DeckList
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export default class DraftView extends Vue {
|
export default class DraftView extends Vue {}
|
||||||
// Card picker size
|
|
||||||
private rows!: number;
|
|
||||||
private columns!: number;
|
|
||||||
|
|
||||||
// Draft data
|
|
||||||
private packNumber!: number;
|
|
||||||
private packCount!: number;
|
|
||||||
private currentPicks!: CardSlot[];
|
|
||||||
private pickedCards!: CardSlot[];
|
|
||||||
|
|
||||||
private data() {
|
|
||||||
return {
|
|
||||||
packNumber: 1,
|
|
||||||
packCount: 4,
|
|
||||||
rows: 3,
|
|
||||||
columns: 4,
|
|
||||||
currentPicks: [],
|
|
||||||
pickedCards: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private mounted() {
|
|
||||||
this.fillcards();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fillcards() {
|
|
||||||
const cards = await getCards({ Sets: ["FF"] });
|
|
||||||
this.currentPicks = cards.slice(0, 12).map(c => ({
|
|
||||||
data: c,
|
|
||||||
howmany: 1,
|
|
||||||
limit: 0
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private cardPicked(card: Card) {
|
|
||||||
// Check if card is already in
|
|
||||||
const idx = this.pickedCards.findIndex(c => c.data.ID == card.ID);
|
|
||||||
if (idx >= 0) {
|
|
||||||
const deckitem = this.pickedCards[idx];
|
|
||||||
deckitem.howmany += 1;
|
|
||||||
Vue.set(this.pickedCards, idx, deckitem);
|
|
||||||
} else {
|
|
||||||
this.pickedCards.push({
|
|
||||||
data: card,
|
|
||||||
limit: cardLimit(card.Type),
|
|
||||||
howmany: 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const origIdx = this.currentPicks.findIndex(c => c.data.ID == card.ID);
|
|
||||||
this.currentPicks.splice(origIdx, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get playerStatus(): PlayerStatus[] {
|
|
||||||
return [
|
|
||||||
"Player1",
|
|
||||||
"bot",
|
|
||||||
"bot",
|
|
||||||
"Player2",
|
|
||||||
"bot",
|
|
||||||
"bot",
|
|
||||||
"Hamcha",
|
|
||||||
"bot"
|
|
||||||
].map(s => ({
|
|
||||||
name: s,
|
|
||||||
isBot: s == "bot",
|
|
||||||
isMe: s == "Hamcha",
|
|
||||||
picked: s == "b" || s == "Player2"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private playerClass(player: PlayerStatus) {
|
|
||||||
return {
|
|
||||||
player: true,
|
|
||||||
me: player.isMe,
|
|
||||||
bot: player.isBot,
|
|
||||||
picked: player.picked
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,140 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="home">
|
<section class="home"></section>
|
||||||
<section class="hero is-medium is-primary is-bold">
|
|
||||||
<div class="hero-body">
|
|
||||||
<div class="container has-text-centered">
|
|
||||||
<h1 class="title">
|
|
||||||
{{ projectName }}
|
|
||||||
</h1>
|
|
||||||
<h2 class="subtitle">
|
|
||||||
An unofficial, fan-made MLP:CCG simulator
|
|
||||||
</h2>
|
|
||||||
<br />
|
|
||||||
<b-button
|
|
||||||
tag="router-link"
|
|
||||||
to="/lobby"
|
|
||||||
class="is-primary spaced"
|
|
||||||
inverted
|
|
||||||
outlined
|
|
||||||
>Play with people</b-button
|
|
||||||
>
|
|
||||||
<b-button
|
|
||||||
tag="router-link"
|
|
||||||
to="/build"
|
|
||||||
class="is-primary spaced"
|
|
||||||
inverted
|
|
||||||
outlined
|
|
||||||
>Build a deck</b-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="section">
|
|
||||||
<div class="container is-widescreen landing-content">
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<section class="has-text-centered">
|
|
||||||
<div class="title is-4">
|
|
||||||
<strong>Web-based</strong>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div class="content">
|
|
||||||
The entire client runs in your browser. No downloads necessary!
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<section class="has-text-centered">
|
|
||||||
<div class="title is-4">
|
|
||||||
<strong>Constructed</strong>
|
|
||||||
<span class="behind fantasy"> & </span>
|
|
||||||
<strong>Limited</strong>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div class="content">
|
|
||||||
You can build decks or draft them with friends. You can even do
|
|
||||||
cube drafts!
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<section class="has-text-centered">
|
|
||||||
<div class="title is-4">
|
|
||||||
<strong>Peer-to-Peer</strong>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div class="content">
|
|
||||||
The entire client uses WebRTC for P2P communications. No
|
|
||||||
registration required to play!
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<footer class="footer">
|
|
||||||
<div class="container is-widescreen">
|
|
||||||
<div class="content has-text-centered">
|
|
||||||
<p>
|
|
||||||
<strong>{{ projectName }}</strong> is not affiliated with Hasbro in
|
|
||||||
any way.<br />
|
|
||||||
Come look at the
|
|
||||||
<a href="https://git.fromouter.space/mcg/mlpcardgame"
|
|
||||||
>source code</a
|
|
||||||
>
|
|
||||||
(ISC licensed)!
|
|
||||||
</p>
|
|
||||||
<p>MCG Version {{ projectVersion }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "@/assets/scss/_variables.scss";
|
|
||||||
|
|
||||||
.home {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.behind {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fantasy {
|
|
||||||
font-family: $fantasy;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-content .column {
|
|
||||||
border: 1px solid $grey;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin: 10px;
|
|
||||||
.title {
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 1px solid rgba($white, 0.2);
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 11pt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
declare const GIT_DESCRIBE: { raw: string };
|
|
||||||
let versionString: string = GIT_DESCRIBE
|
|
||||||
? GIT_DESCRIBE.raw
|
|
||||||
: require("@/../package.json").version;
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {}
|
components: {}
|
||||||
})
|
})
|
||||||
export default class Home extends Vue {
|
export default class Home extends Vue {}
|
||||||
private projectName: string = "MLPCARDGAME";
|
|
||||||
private projectVersion: string = versionString;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,374 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="lobby">
|
<section class="lobby"></section>
|
||||||
<TopNav />
|
|
||||||
<section v-if="!inRoom" class="body">
|
|
||||||
<section id="info">
|
|
||||||
<b-field label="Your name">
|
|
||||||
<b-input :disabled="busy" v-model="playerName"></b-input>
|
|
||||||
</b-field>
|
|
||||||
</section>
|
|
||||||
<section id="join">
|
|
||||||
<header>
|
|
||||||
<h1>Join someone's session</h1>
|
|
||||||
</header>
|
|
||||||
<b-field label="Session ID" class="only-full">
|
|
||||||
<b-input :disabled="busy" v-model="joinSessionID"></b-input>
|
|
||||||
</b-field>
|
|
||||||
<div class="center submit only-full">
|
|
||||||
<b-button
|
|
||||||
type="is-primary"
|
|
||||||
@click="join"
|
|
||||||
class="wide"
|
|
||||||
:disabled="busy || !canJoin"
|
|
||||||
>
|
|
||||||
Join
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
<div class="only-mobile">
|
|
||||||
<b-field class="full">
|
|
||||||
<b-input
|
|
||||||
:disabled="busy"
|
|
||||||
placeholder="Session ID"
|
|
||||||
v-model="joinSessionID"
|
|
||||||
></b-input>
|
|
||||||
<p class="control">
|
|
||||||
<b-button
|
|
||||||
type="is-primary"
|
|
||||||
@click="join"
|
|
||||||
:disabled="busy || !canJoin"
|
|
||||||
>
|
|
||||||
Join
|
|
||||||
</b-button>
|
|
||||||
</p>
|
|
||||||
</b-field>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="host">
|
|
||||||
<header>
|
|
||||||
<h1>Host a new session</h1>
|
|
||||||
</header>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<b-field label="Max players">
|
|
||||||
<b-numberinput
|
|
||||||
:disabled="busy"
|
|
||||||
controls-position="compact"
|
|
||||||
v-model="hostMaxPlayers"
|
|
||||||
min="2"
|
|
||||||
max="8"
|
|
||||||
></b-numberinput>
|
|
||||||
</b-field>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<b-field label="Password">
|
|
||||||
<b-input :disabled="busy" v-model="hostPassword"></b-input>
|
|
||||||
</b-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="center">
|
|
||||||
<b-button
|
|
||||||
:disabled="busy"
|
|
||||||
type="is-primary"
|
|
||||||
@click="create"
|
|
||||||
class="wide"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
<section class="room" v-else>
|
|
||||||
<section class="info">
|
|
||||||
Invite your friends:
|
|
||||||
<span class="selectable"
|
|
||||||
><a :href="inviteLink">{{ inviteLink }}</a></span
|
|
||||||
>
|
|
||||||
</section>
|
|
||||||
<section class="chat"></section>
|
|
||||||
<section class="players">
|
|
||||||
<header>Players</header>
|
|
||||||
<ul>
|
|
||||||
<li class="selectable" v-for="player in players" :key="player">
|
|
||||||
{{ player }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section class="player-options">
|
|
||||||
<b-field>
|
|
||||||
<b-input :disabled="busy" v-model="wantedName"></b-input>
|
|
||||||
<p class="control">
|
|
||||||
<b-button
|
|
||||||
@click="changeName"
|
|
||||||
type="is-primary"
|
|
||||||
:disabled="!nameAvailable"
|
|
||||||
>Change name</b-button
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</b-field>
|
|
||||||
<b-button type="is-danger">Leave room</b-button>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped></style>
|
||||||
@import "@/assets/scss/_variables.scss";
|
|
||||||
|
|
||||||
.lobby {
|
|
||||||
background: url("../assets/images/backgrounds/menubg.webp") center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
flex: 1;
|
|
||||||
display: grid;
|
|
||||||
padding: 10px;
|
|
||||||
padding-top: 0;
|
|
||||||
grid-template: 150px 1fr / 1fr 1fr;
|
|
||||||
|
|
||||||
#info {
|
|
||||||
grid-row: 1;
|
|
||||||
grid-column: 1 / end;
|
|
||||||
}
|
|
||||||
|
|
||||||
#join,
|
|
||||||
#host {
|
|
||||||
grid-row: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
#join {
|
|
||||||
grid-column: 2;
|
|
||||||
}
|
|
||||||
#host {
|
|
||||||
grid-column: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
margin: 10px;
|
|
||||||
border: 1px solid rgba($white, 20%);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
header {
|
|
||||||
font-family: $fantasy;
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 18pt;
|
|
||||||
}
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wide {
|
|
||||||
min-width: 30%;
|
|
||||||
margin: 10px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full {
|
|
||||||
width: 100%;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-btn {
|
|
||||||
flex: 1;
|
|
||||||
:global(.button) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.room {
|
|
||||||
padding: 10px 20px;
|
|
||||||
margin: 10px;
|
|
||||||
border: 1px solid rgba($white, 20%);
|
|
||||||
border-radius: 10px;
|
|
||||||
display: grid;
|
|
||||||
grid-template: 50px 1fr / 200px 1fr 300px;
|
|
||||||
|
|
||||||
.info {
|
|
||||||
grid-column: 1 / max;
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.players {
|
|
||||||
header {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
grid-row: 2 / max;
|
|
||||||
grid-column: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat {
|
|
||||||
grid-row: 2 / max;
|
|
||||||
grid-column: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-options {
|
|
||||||
grid-row: 2 / max;
|
|
||||||
grid-column: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.only-mobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selectable {
|
|
||||||
user-select: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
.only-full {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.only-mobile {
|
|
||||||
display: inherit;
|
|
||||||
}
|
|
||||||
.body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
section {
|
|
||||||
padding: 10px;
|
|
||||||
header h1 {
|
|
||||||
font-size: 14pt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.room {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
& > * {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
import TopNav from "@/components/Navigation/TopNav.vue";
|
|
||||||
import { StartServerOptions, ConnectOptions } from "@/store/network/types";
|
|
||||||
import { Action, Getter } from "vuex-class";
|
|
||||||
import { Client, NetworkMessage } from "@/network";
|
|
||||||
|
|
||||||
const networkNS = { namespace: "network" };
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {}
|
||||||
TopNav
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export default class Lobby extends Vue {
|
export default class Lobby extends Vue {}
|
||||||
private playerName!: string;
|
|
||||||
private hostMaxPlayers!: number;
|
|
||||||
private hostPassword!: string;
|
|
||||||
private joinSessionID!: string;
|
|
||||||
private wantedName!: string;
|
|
||||||
|
|
||||||
@Action("startServer", networkNS)
|
|
||||||
private startServer!: (options: StartServerOptions) => void;
|
|
||||||
|
|
||||||
@Action("sendMessage", networkNS)
|
|
||||||
private sendMessage!: (message: NetworkMessage) => void;
|
|
||||||
|
|
||||||
@Action("connect", networkNS)
|
|
||||||
private connect!: (options: ConnectOptions) => void;
|
|
||||||
|
|
||||||
@Getter("inRoom", networkNS)
|
|
||||||
private inRoom!: boolean;
|
|
||||||
|
|
||||||
@Getter("busy", networkNS)
|
|
||||||
private busy!: boolean;
|
|
||||||
|
|
||||||
@Getter("sessionID", networkNS)
|
|
||||||
private sessionID!: string | null;
|
|
||||||
|
|
||||||
@Getter("players", networkNS)
|
|
||||||
private players!: string[];
|
|
||||||
|
|
||||||
private data() {
|
|
||||||
const playerName =
|
|
||||||
"Guest-" +
|
|
||||||
Math.random()
|
|
||||||
.toString()
|
|
||||||
.slice(2, 8);
|
|
||||||
return {
|
|
||||||
playerName,
|
|
||||||
hostMaxPlayers: 8,
|
|
||||||
hostPassword: "",
|
|
||||||
joinSessionID: "",
|
|
||||||
wantedName: playerName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private mounted() {
|
|
||||||
if ("id" in this.$route.params) {
|
|
||||||
this.joinSessionID = this.$route.params.id;
|
|
||||||
this.join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async create() {
|
|
||||||
this.wantedName = this.playerName;
|
|
||||||
this.startServer({
|
|
||||||
playerInfo: {
|
|
||||||
name: this.playerName
|
|
||||||
},
|
|
||||||
roomInfo: {
|
|
||||||
max_players: this.hostMaxPlayers,
|
|
||||||
password: this.hostPassword
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async join() {
|
|
||||||
this.wantedName = this.playerName;
|
|
||||||
this.connect({
|
|
||||||
serverID: this.joinSessionID,
|
|
||||||
playerInfo: {
|
|
||||||
name: this.playerName
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async changeName() {
|
|
||||||
this.sendMessage({
|
|
||||||
kind: "rename",
|
|
||||||
oldname: this.playerName,
|
|
||||||
newname: this.wantedName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private get canJoin(): boolean {
|
|
||||||
return this.joinSessionID != "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private get inviteLink(): string {
|
|
||||||
let subpath = "";
|
|
||||||
const joinIndex = location.pathname.indexOf("/join");
|
|
||||||
if (joinIndex > 0) {
|
|
||||||
subpath = location.pathname.substring(0, joinIndex);
|
|
||||||
}
|
|
||||||
const lobbyIndex = location.pathname.indexOf("/lobby");
|
|
||||||
if (lobbyIndex > 0) {
|
|
||||||
subpath = location.pathname.substring(0, lobbyIndex);
|
|
||||||
}
|
|
||||||
return `${location.origin}${subpath}/join/${this.sessionID}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get nameAvailable(): boolean {
|
|
||||||
return this.wantedName != "" && !this.players.includes(this.wantedName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
14
src/views/Room.vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<section class="room"></section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {}
|
||||||
|
})
|
||||||
|
export default class RoomView extends Vue {}
|
||||||
|
</script>
|
|
@ -1,191 +0,0 @@
|
||||||
<template>
|
|
||||||
<section class="settings">
|
|
||||||
<TopNav class="top" />
|
|
||||||
<section class="settings-box download">
|
|
||||||
<header>
|
|
||||||
<h1>Storage settings</h1>
|
|
||||||
</header>
|
|
||||||
<div class="rows">
|
|
||||||
<article>
|
|
||||||
<div class="name">
|
|
||||||
Card image source
|
|
||||||
</div>
|
|
||||||
<div class="value">
|
|
||||||
{{ imageSource }}
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<b-button
|
|
||||||
class="is-primary"
|
|
||||||
@click="downloadImages"
|
|
||||||
:disabled="cardImageSource == 'local'"
|
|
||||||
>Download images</b-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<b-modal :active.sync="isDownloading" :can-cancel="false">
|
|
||||||
<div class="modal-card" style="width: auto">
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<p class="modal-card-title">
|
|
||||||
{{ downloadStatus }}
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body">
|
|
||||||
<b-progress
|
|
||||||
size="is-large"
|
|
||||||
type="is-danger"
|
|
||||||
v-if="downloadProgress"
|
|
||||||
:max="downloadProgress.total"
|
|
||||||
:value="downloadProgress.progress"
|
|
||||||
show-value
|
|
||||||
>
|
|
||||||
{{ downloadProgressString }}
|
|
||||||
</b-progress>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</b-modal>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "@/assets/scss/_variables.scss";
|
|
||||||
|
|
||||||
.settings {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top {
|
|
||||||
grid-column: 1 / end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-box {
|
|
||||||
margin: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
background: rgba($white, 0.1);
|
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
header {
|
|
||||||
h1 {
|
|
||||||
font-family: $fantasy;
|
|
||||||
font-size: 17pt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rows {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
article {
|
|
||||||
margin: 10px 0;
|
|
||||||
font-size: 12pt;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
& > div {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
border: 1px solid rgba($black, 0.4);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
|
||||||
import TopNav from "@/components/Navigation/TopNav.vue";
|
|
||||||
import { TaskRunner } from "@/workers";
|
|
||||||
import { cardImageSource, refreshCardSource } from "@/mlpccg";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: { TopNav }
|
|
||||||
})
|
|
||||||
export default class SettingsView extends Vue {
|
|
||||||
private cardImageSource!: "local" | "remote";
|
|
||||||
private downloadState!: "starting" | "download" | "extract" | null;
|
|
||||||
private downloadProgress!: { progress: number; total: number } | null;
|
|
||||||
|
|
||||||
private downloadImages() {
|
|
||||||
this.downloadState = "starting";
|
|
||||||
const worker = new TaskRunner("downloadCardImages");
|
|
||||||
worker.on("dl-progress", progress => {
|
|
||||||
if (this.downloadState != "download") {
|
|
||||||
this.downloadState = "download";
|
|
||||||
}
|
|
||||||
this.downloadProgress = progress;
|
|
||||||
});
|
|
||||||
worker.on("ex-progress", progress => {
|
|
||||||
if (this.downloadState != "extract") {
|
|
||||||
this.downloadState = "extract";
|
|
||||||
}
|
|
||||||
this.downloadProgress = progress;
|
|
||||||
});
|
|
||||||
worker.on("finish", async _ => {
|
|
||||||
this.downloadState = null;
|
|
||||||
await refreshCardSource();
|
|
||||||
this.cardImageSource = cardImageSource();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private data() {
|
|
||||||
return {
|
|
||||||
cardImageSource: cardImageSource(),
|
|
||||||
downloadState: null,
|
|
||||||
downloadProgress: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private get imageSource() {
|
|
||||||
switch (this.cardImageSource) {
|
|
||||||
case "local":
|
|
||||||
return "Local saved copy";
|
|
||||||
case "remote":
|
|
||||||
return "Remote server";
|
|
||||||
}
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
private get isDownloading(): boolean {
|
|
||||||
return this.downloadState !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get downloadStatus(): string {
|
|
||||||
switch (this.downloadState) {
|
|
||||||
case "starting":
|
|
||||||
return "Starting download...";
|
|
||||||
case "download":
|
|
||||||
return `Downloading image archive (${Math.round(
|
|
||||||
this.downloadProgress!.total / 10485.76
|
|
||||||
) / 100} MB)`;
|
|
||||||
case "extract":
|
|
||||||
return `Extracting images`;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private get downloadProgressString(): string {
|
|
||||||
if (!this.downloadProgress) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
let current = "";
|
|
||||||
let total = "";
|
|
||||||
if (this.downloadState == "extract") {
|
|
||||||
current = `${(this.downloadProgress.progress / 2) | 0}`;
|
|
||||||
total = `${(this.downloadProgress.total / 2) | 0}`;
|
|
||||||
} else {
|
|
||||||
const currentNum =
|
|
||||||
Math.round(this.downloadProgress.progress / 10485.76) / 100;
|
|
||||||
current = currentNum.toString().padEnd(currentNum < 10 ? 4 : 5, "0");
|
|
||||||
const totalNum = Math.round(this.downloadProgress.total / 10485.76) / 100;
|
|
||||||
total = totalNum.toString().padEnd(totalNum < 10 ? 4 : 5, "0") + " MB";
|
|
||||||
}
|
|
||||||
const percent = Math.round(
|
|
||||||
(this.downloadProgress.progress / this.downloadProgress.total) * 100
|
|
||||||
);
|
|
||||||
return `${percent}% (${current}/${total})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./runner";
|
|
|
@ -1,17 +0,0 @@
|
||||||
import EventEmitter from "eventemitter3";
|
|
||||||
|
|
||||||
export class TaskRunner extends EventEmitter {
|
|
||||||
public class: any;
|
|
||||||
public instance: Worker;
|
|
||||||
|
|
||||||
constructor(taskName: string) {
|
|
||||||
super();
|
|
||||||
this.class = require(`worker-loader!@/workers/tasks/${taskName}`);
|
|
||||||
this.instance = new this.class() as Worker;
|
|
||||||
this.instance.addEventListener("error", ev => this.emit("error", ev));
|
|
||||||
this.instance.addEventListener("message", ev => {
|
|
||||||
const message = JSON.parse(ev.data);
|
|
||||||
this.emit(message.type, message.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
import { Database, initDB } from "@/mlpccg";
|
|
||||||
import axios from "axios";
|
|
||||||
import JSZip from "jszip";
|
|
||||||
import { send, runAsync } from "../worker-utils";
|
|
||||||
|
|
||||||
async function downloadImages() {
|
|
||||||
if (!Database) {
|
|
||||||
await initDB();
|
|
||||||
}
|
|
||||||
|
|
||||||
let table = Database!.images;
|
|
||||||
|
|
||||||
const itemcount = await table.count();
|
|
||||||
if (itemcount > 1900) {
|
|
||||||
// DB already filled, exit early
|
|
||||||
return "already-done";
|
|
||||||
}
|
|
||||||
|
|
||||||
const zipdata = await axios({
|
|
||||||
url: "https://mcg.zyg.ovh/cards.zip",
|
|
||||||
responseType: "blob",
|
|
||||||
onDownloadProgress: (progressEvent: ProgressEvent) => {
|
|
||||||
send("dl-progress", {
|
|
||||||
progress: progressEvent.loaded,
|
|
||||||
total: progressEvent.total
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const zipfile = await JSZip.loadAsync(zipdata.data);
|
|
||||||
const cards = zipfile.folder("Cards");
|
|
||||||
|
|
||||||
let loadingState = 0;
|
|
||||||
let totalLoading = 0;
|
|
||||||
cards.forEach(async () => {
|
|
||||||
totalLoading += 2;
|
|
||||||
});
|
|
||||||
let waitgroup = new Promise(resolve => {
|
|
||||||
let timer = setInterval(() => {
|
|
||||||
if (loadingState >= totalLoading) {
|
|
||||||
clearInterval(timer);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
cards.forEach(async (filename, filedata) => {
|
|
||||||
const data = await filedata.async("blob");
|
|
||||||
loadingState += 1;
|
|
||||||
send("ex-progress", {
|
|
||||||
progress: loadingState,
|
|
||||||
total: totalLoading
|
|
||||||
});
|
|
||||||
const result = await table.put({ id: filename, image: data });
|
|
||||||
loadingState += 1;
|
|
||||||
send("ex-progress", {
|
|
||||||
progress: loadingState,
|
|
||||||
total: totalLoading
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await waitgroup;
|
|
||||||
|
|
||||||
return "downloaded";
|
|
||||||
}
|
|
||||||
|
|
||||||
runAsync(async () => {
|
|
||||||
try {
|
|
||||||
return await downloadImages();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return new Error(e.message);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
export function send(type: string, data?: any) {
|
|
||||||
const ctx: Worker = self as any;
|
|
||||||
ctx.postMessage(JSON.stringify({ type, data }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runAsync(fn: () => Promise<any>) {
|
|
||||||
const val = await fn();
|
|
||||||
send("finish", val);
|
|
||||||
}
|
|