Compare commits
9 Commits
Author | SHA1 | Date |
---|---|---|
Hamcha | 9969561af1 | |
Hamcha | 2f6e6e97ca | |
Hamcha | d093199cd9 | |
Hamcha | 83b6f8f188 | |
Hamcha | 9ce4dd67f5 | |
Hamcha | 70fe698c22 | |
Hamcha | 7156fe23e5 | |
Hamcha | a73828fc86 | |
Hamcha | 29ab978612 |
26
.drone.yml
26
.drone.yml
|
@ -1,3 +1,4 @@
|
|||
---
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
|
@ -14,11 +15,19 @@ steps:
|
|||
|
||||
- name: dependencies
|
||||
image: node
|
||||
failure: ignore
|
||||
commands:
|
||||
- yarn
|
||||
depends_on:
|
||||
- restore-cache
|
||||
|
||||
- name: lint
|
||||
image: node
|
||||
commands:
|
||||
- yarn lint
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
- name: build_versioned
|
||||
image: node
|
||||
commands:
|
||||
|
@ -28,6 +37,9 @@ steps:
|
|||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
exclude:
|
||||
- master
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
|
@ -72,6 +84,9 @@ steps:
|
|||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
exclude:
|
||||
- master
|
||||
depends_on:
|
||||
- build_versioned
|
||||
|
||||
|
@ -165,6 +180,7 @@ steps:
|
|||
|
||||
- name: rebuild-cache
|
||||
image: drillster/drone-volume-cache
|
||||
failure: ignore
|
||||
volumes:
|
||||
- name: cache
|
||||
path: /cache
|
||||
|
@ -174,3 +190,13 @@ steps:
|
|||
- ./node_modules
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
volumes:
|
||||
- name: cache
|
||||
host:
|
||||
path: /opt/gitea/drone-cache/mcg/mlpcardgame
|
||||
---
|
||||
kind: signature
|
||||
hmac: 73d743702e8545bf469f17b96c289a5d4f1d56eb3f1966c7c0a0e38d44d38aba
|
||||
|
||||
...
|
||||
|
|
|
@ -8,7 +8,10 @@ module.exports = {
|
|||
extends: ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"],
|
||||
|
||||
rules: {
|
||||
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
|
||||
"no-console":
|
||||
process.env.NODE_ENV === "production"
|
||||
? ["error", { allow: ["info", "warn", "error"] }]
|
||||
: "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"
|
||||
},
|
||||
|
||||
|
@ -18,9 +21,7 @@ module.exports = {
|
|||
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)'
|
||||
],
|
||||
files: ["**/__tests__/*.{j,t}s?(x)"],
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
|
|
52
README.md
52
README.md
|
@ -4,6 +4,58 @@
|
|||
|
||||
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
|
||||
|
||||
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"],
|
||||
transform: {
|
||||
"^.+\\.vue$": "vue-jest",
|
||||
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":
|
||||
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2|webp)$":
|
||||
"jest-transform-stub",
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"test:unit": "vue-cli-service test:unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/jszip": "^3.1.6",
|
||||
"axios": "^0.18.0",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
|
@ -17,6 +18,7 @@
|
|||
"core-js": "^2.6.5",
|
||||
"dexie": "^2.0.4",
|
||||
"eventemitter3": "^4.0.0",
|
||||
"jszip": "^3.2.2",
|
||||
"node-sass": "^4.9.0",
|
||||
"peerjs": "^1.0.4",
|
||||
"register-service-worker": "^1.6.2",
|
||||
|
@ -28,7 +30,8 @@
|
|||
"vue-property-decorator": "^8.1.0",
|
||||
"vue-router": "^3.0.3",
|
||||
"vuex": "^3.0.1",
|
||||
"vuex-class": "^0.3.2"
|
||||
"vuex-class": "^0.3.2",
|
||||
"worker-loader": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^23.1.4",
|
||||
|
@ -44,11 +47,13 @@
|
|||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-prettier": "^3.1.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"git-describe": "^4.0.4",
|
||||
"indexeddbshim": "^4.1.0",
|
||||
"prettier": "^1.18.2",
|
||||
"ts-jest": "^23.0.0",
|
||||
"vue-cli-plugin-axios": "^0.0.4",
|
||||
"vue-cli-plugin-buefy": "^0.3.7",
|
||||
"vue-cli-plugin-git-describe": "^1.0.0",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"license": "ISC"
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css">
|
||||
<title>mcgvue</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but mcgvue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css">
|
||||
<title>MLPCARDGAME</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but mcgvue doesn't work properly without JavaScript enabled. Please enable it to
|
||||
continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"name": "mcgvue",
|
||||
"short_name": "mcgvue",
|
||||
"name": "MLPCARDGAME",
|
||||
"short_name": "mcg",
|
||||
"description": "MLP:CCG simulator",
|
||||
"icons": [{
|
||||
"src": "./images/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
|
|
|
@ -28,9 +28,8 @@ h1.loading-message {
|
|||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { Action, Getter } from "vuex-class";
|
||||
import { loadSets } from "@/mlpccg/set";
|
||||
import { AppState } from "./store/types";
|
||||
import { getCards } from "./mlpccg/database";
|
||||
import { AppState } from "@/store/types";
|
||||
import { refreshCardSource, loadSets, getCards } from "@/mlpccg";
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
|
@ -54,6 +53,7 @@ export default class App extends Vue {
|
|||
private async loadCards() {
|
||||
this.showLoading("Downloading data for all sets");
|
||||
await loadSets();
|
||||
await refreshCardSource();
|
||||
this.hideLoading();
|
||||
this.setLoaded(true);
|
||||
}
|
||||
|
|
|
@ -15,4 +15,27 @@ html {
|
|||
|
||||
body {
|
||||
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%);
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<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>
|
|
@ -6,7 +6,7 @@
|
|||
v-for="(card, i) in cards"
|
||||
:key="i + card.data.ID"
|
||||
>
|
||||
<img :src="imageURL(card.data.ID)" />
|
||||
<CardImage :id="card.data.ID" />
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
@ -45,9 +45,12 @@ $padding: 10px;
|
|||
<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: {}
|
||||
components: {
|
||||
CardImage
|
||||
}
|
||||
})
|
||||
export default class CardPicker extends Vue {
|
||||
@Prop()
|
||||
|
|
|
@ -1,18 +1,36 @@
|
|||
<template>
|
||||
<nav>
|
||||
<router-link
|
||||
:class="routeClass(route)"
|
||||
v-for="route in routes"
|
||||
:key="route"
|
||||
:to="{ name: route }"
|
||||
>{{ prettyTitle(route) }}</router-link
|
||||
>
|
||||
<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;
|
||||
|
@ -22,13 +40,23 @@ nav {
|
|||
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: 300px;
|
||||
max-width: 250px;
|
||||
text-align: center;
|
||||
color: $grey-lighter;
|
||||
|
||||
|
@ -61,17 +89,20 @@ nav {
|
|||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
|
||||
const routes = ["lobby", "deck-editor"];
|
||||
const mainRoutes = ["lobby", "deck-editor"];
|
||||
const iconRoutes = ["settings"];
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
export default class TopNav extends Vue {
|
||||
private routes!: string[];
|
||||
private mainRoutes!: string[];
|
||||
private iconRoutes!: string[];
|
||||
|
||||
private data() {
|
||||
return {
|
||||
routes
|
||||
mainRoutes,
|
||||
iconRoutes
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,18 @@ class CardDatabase extends Dexie {
|
|||
|
||||
export let Database: CardDatabase | null = null;
|
||||
|
||||
export function initDB() {
|
||||
if (Database == null) {
|
||||
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) {
|
||||
|
|
|
@ -57,7 +57,7 @@ export class Session extends EventEmitter {
|
|||
|
||||
// Place players in the pod
|
||||
switch (this.options.spacing) {
|
||||
case "evenly":
|
||||
case "evenly": {
|
||||
const playerRatio = spots / playerNum;
|
||||
let i = 0;
|
||||
for (const player of players) {
|
||||
|
@ -67,16 +67,15 @@ export class Session extends EventEmitter {
|
|||
i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "randomly":
|
||||
for (const player of players) {
|
||||
while (true) {
|
||||
const idx = Math.floor(Math.random() * spots);
|
||||
if (this.pod[idx].name == "") {
|
||||
this.pod[idx].name = player;
|
||||
assignFn(name, this.pod[idx]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
@ -179,10 +178,11 @@ export class Session extends EventEmitter {
|
|||
const provider = DraftProvider.set(factory, options.packs);
|
||||
return new Session(options, provider);
|
||||
}
|
||||
case "i8pcube":
|
||||
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");
|
||||
}
|
||||
|
|
|
@ -1,35 +1,38 @@
|
|||
import axios from "axios";
|
||||
import { Database } from "./database";
|
||||
|
||||
const imgBaseURL = "https://mcg.zyg.ovh/images/cards/";
|
||||
let imageSource: "local" | "remote" = "remote";
|
||||
|
||||
export function cardImageURL(cardid: string): string {
|
||||
export function remoteImageURL(cardid: string): string {
|
||||
return `${imgBaseURL}${cardid}.webp`;
|
||||
}
|
||||
|
||||
async function getCardImageList(): Promise<string[]> {
|
||||
const req = await axios(`${imgBaseURL}list.txt`);
|
||||
return req.data;
|
||||
export async function 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 async function getImages() {
|
||||
if (Database == null) {
|
||||
throw new Error("Database was not initialized, init with 'initDB()'");
|
||||
}
|
||||
const itemcount = await Database.images.count();
|
||||
if (itemcount > 100) {
|
||||
// DB already filled, exit early
|
||||
export function cardImageSource() {
|
||||
return imageSource;
|
||||
}
|
||||
|
||||
export async function refreshCardSource() {
|
||||
if (!Database) {
|
||||
return;
|
||||
}
|
||||
const imglist = await getCardImageList();
|
||||
|
||||
let table = Database.images;
|
||||
const promises = imglist.map(async img => {
|
||||
const req = await axios({
|
||||
url: `${imgBaseURL}${img}`,
|
||||
responseType: "blob"
|
||||
});
|
||||
return table.put({ id: img, image: req.data });
|
||||
});
|
||||
return await Promise.all(promises);
|
||||
const count = await Database.images.count();
|
||||
imageSource = count > 1900 ? "local" : "remote";
|
||||
}
|
||||
|
|
|
@ -19,6 +19,23 @@ export const allSets = [
|
|||
"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() {
|
||||
if (Database == null) {
|
||||
throw new Error("Database was not initialized, init with 'initDB()'");
|
||||
|
@ -34,7 +51,6 @@ export async function loadSets() {
|
|||
if (Database == null) {
|
||||
throw new Error("Database was not initialized, init with 'initDB()'");
|
||||
}
|
||||
console.log(`Processing cards from ${set.Name}`);
|
||||
return await Database.cards.bulkPut(set.Cards);
|
||||
})
|
||||
);
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import {
|
||||
PeerMetadata,
|
||||
NetworkMessage,
|
||||
PasswordResponse,
|
||||
RoomInfo
|
||||
} from "./types";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import Vue from "vue";
|
||||
|
||||
import { NetworkMessage, PasswordResponse, PeerMetadata, RoomInfo } from "./types";
|
||||
|
||||
export abstract class Client extends EventEmitter {
|
||||
public metadata: PeerMetadata;
|
||||
|
@ -23,13 +20,19 @@ export abstract class Client extends EventEmitter {
|
|||
case "room-info":
|
||||
this.roomInfo = data.room;
|
||||
this.players = data.players;
|
||||
this.emit("handshake");
|
||||
break;
|
||||
// Someone changed name (or was forced to)
|
||||
case "rename":
|
||||
if (data.oldname == this.metadata.name) {
|
||||
// We got a name change!
|
||||
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);
|
||||
if (idx < 0) {
|
||||
// Weird
|
||||
|
@ -39,7 +42,7 @@ export abstract class Client extends EventEmitter {
|
|||
this.players.push(data.newname);
|
||||
break;
|
||||
}
|
||||
this.players[idx] = data.newname;
|
||||
Vue.set(this.players, idx, data.newname);
|
||||
}
|
||||
this.emit("rename", data.oldname, data.newname);
|
||||
break;
|
||||
|
@ -48,7 +51,7 @@ export abstract class Client extends EventEmitter {
|
|||
this.players.push(data.name);
|
||||
this.emit("player-joined", data.name);
|
||||
break;
|
||||
case "player-left":
|
||||
case "player-left": {
|
||||
let idx = this.players.indexOf(data.name);
|
||||
if (idx < 0) {
|
||||
// Weird
|
||||
|
@ -57,9 +60,13 @@ export abstract class Client extends EventEmitter {
|
|||
);
|
||||
break;
|
||||
}
|
||||
this.emit("player-left", data.name);
|
||||
this.players.splice(idx, 1);
|
||||
break;
|
||||
}
|
||||
case "password-req":
|
||||
this.emit("password-required");
|
||||
break;
|
||||
default:
|
||||
// For most cases, we can just use the kind as event type
|
||||
this.emit(data.kind, data);
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
import EventEmitter from "eventemitter3";
|
||||
import Peer, { DataConnection } from "peerjs";
|
||||
|
||||
import { LocalClient } from ".";
|
||||
import {
|
||||
RoomInfo,
|
||||
PasswordRequest,
|
||||
Room,
|
||||
AckMessage,
|
||||
ChatMessage,
|
||||
ErrorMessage,
|
||||
JoinMessage,
|
||||
LeaveMessage,
|
||||
NetworkMessage,
|
||||
NetworkPlayer,
|
||||
PasswordRequest,
|
||||
PasswordResponse,
|
||||
PeerMetadata,
|
||||
JoinMessage,
|
||||
RoomInfoMessage,
|
||||
Player,
|
||||
NetworkMessage,
|
||||
RenameMessage,
|
||||
LeaveMessage,
|
||||
NetworkPlayer,
|
||||
AckMessage,
|
||||
ChatMessage
|
||||
Room,
|
||||
RoomInfo,
|
||||
RoomInfoMessage,
|
||||
} from "./types";
|
||||
import { LocalClient } from ".";
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
// Increment name, add number at the end if not present
|
||||
// Examples:
|
||||
|
@ -34,6 +35,12 @@ function nextName(name: string): string {
|
|||
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 {
|
||||
protected peer: Peer;
|
||||
private room: Room;
|
||||
|
@ -64,17 +71,21 @@ export class PeerServer extends EventEmitter {
|
|||
|
||||
// Setup peer
|
||||
this.peer = customPeer ? customPeer : new Peer();
|
||||
this.peer.on("open", function(id) {
|
||||
this.peer.on("open", id => {
|
||||
console.info("Peer ID assigned: %s", id);
|
||||
this.emit("open", id);
|
||||
});
|
||||
this.peer.on("connection", conn => {
|
||||
this._connection(conn);
|
||||
});
|
||||
}
|
||||
|
||||
private _connection(conn: DataConnection) {
|
||||
private async _connection(conn: DataConnection) {
|
||||
const metadata = conn.metadata as PeerMetadata;
|
||||
|
||||
// Wait for connection to be open
|
||||
await connectionOpen(conn);
|
||||
|
||||
let player: NetworkPlayer = {
|
||||
kind: "remote",
|
||||
name: metadata.name,
|
||||
|
@ -140,6 +151,12 @@ export class PeerServer extends EventEmitter {
|
|||
|
||||
private addPlayer(player: NetworkPlayer) {
|
||||
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;
|
||||
|
||||
// Start listening for new messages
|
||||
|
@ -148,6 +165,10 @@ export class PeerServer extends EventEmitter {
|
|||
this._received.bind(this, this.room.players[playerName])
|
||||
);
|
||||
|
||||
player.conn.on("error", err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Send the player info about the room
|
||||
this.send<RoomInfoMessage>(player, {
|
||||
kind: "room-info",
|
||||
|
@ -155,7 +176,7 @@ export class PeerServer extends EventEmitter {
|
|||
...this.room.info,
|
||||
password: ""
|
||||
},
|
||||
players: Object.keys(this.room.players)
|
||||
players
|
||||
});
|
||||
|
||||
// Notify other players
|
||||
|
@ -172,6 +193,9 @@ export class PeerServer extends EventEmitter {
|
|||
// Close connection with player
|
||||
player.conn.close();
|
||||
|
||||
// Remove player from player list
|
||||
delete this.room.players[player.name];
|
||||
|
||||
// Notify other players
|
||||
this.broadcast<LeaveMessage>({
|
||||
kind: "player-left",
|
||||
|
@ -189,16 +213,30 @@ export class PeerServer extends EventEmitter {
|
|||
this.broadcast(data);
|
||||
} else {
|
||||
// Player is telling someone specifically
|
||||
if (data.to in this.players) {
|
||||
this.send<ChatMessage>(this.players[data.to], data);
|
||||
} else {
|
||||
if (!(data.to in this.players)) {
|
||||
this.send<ErrorMessage>(player, {
|
||||
kind: "error",
|
||||
error: `player not found: ${data.to}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.send<ChatMessage>(this.players[data.to], data);
|
||||
}
|
||||
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!
|
||||
case "leave-req":
|
||||
// If we're leaving, end the server
|
||||
|
|
|
@ -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 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);
|
||||
|
||||
|
@ -45,9 +45,20 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: "/room",
|
||||
name: "room",
|
||||
component: RoomView
|
||||
path: "/join/:id",
|
||||
name: "lobby-join",
|
||||
component: Lobby,
|
||||
meta: {
|
||||
topnav: "Lobby"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
name: "settings",
|
||||
component: SettingsView,
|
||||
meta: {
|
||||
topnav: "settings"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// typings/custom.d.ts
|
||||
declare module "worker-loader!*" {
|
||||
class WebpackWorker extends Worker {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export default WebpackWorker;
|
||||
}
|
|
@ -1,28 +1,58 @@
|
|||
import { ActionTree } from "vuex";
|
||||
import { ChatMessage, Client, LocalClient, NetworkMessage, PeerClient, PeerServer } from "@/network";
|
||||
import { ActionTree, Commit } from "vuex";
|
||||
|
||||
import { AppState } from "../types";
|
||||
import { NetworkState, StartServerOptions, ConnectOptions } from "./types";
|
||||
import { PeerServer, LocalClient, PeerClient } from "@/network";
|
||||
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 });
|
||||
commit("becomeClient", { peer: client, id: options.serverID });
|
||||
client.on("connected", () => {
|
||||
commit("connected");
|
||||
commit("connectionStatusChanged", "connected");
|
||||
});
|
||||
client.on("disconnected", () => {
|
||||
commit("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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { Client } from "@/network";
|
||||
import { GetterTree } from "vuex";
|
||||
|
||||
import { AppState } from "../types";
|
||||
import { NetworkState } from "./types";
|
||||
|
||||
|
@ -13,8 +15,45 @@ const getters: GetterTree<NetworkState, AppState> = {
|
|||
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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,12 @@ const namespaced = true;
|
|||
|
||||
export const state: NetworkState = {
|
||||
peerType: "none",
|
||||
connectionStatus: null,
|
||||
peer: null,
|
||||
server: null,
|
||||
local: null,
|
||||
serverID: null,
|
||||
players: [],
|
||||
chatLog: []
|
||||
};
|
||||
|
||||
|
|
|
@ -1,37 +1,43 @@
|
|||
import { ChatMessage, LocalClient, PeerClient, PeerServer } from "@/network";
|
||||
import Vue from "vue";
|
||||
import { MutationTree } from "vuex";
|
||||
import { NetworkState, ServerNetworkState, ClientNetworkState } from "./types";
|
||||
import { LocalClient, PeerServer, PeerClient } from "@/network";
|
||||
|
||||
import { ClientNetworkState, ConnectionStatus, NetworkState, ServerNetworkState } from "./types";
|
||||
|
||||
const mutations: MutationTree<NetworkState> = {
|
||||
becomeServer(state, payload: { local: LocalClient; server: PeerServer }) {
|
||||
state = {
|
||||
...state,
|
||||
peerType: "server",
|
||||
local: payload.local,
|
||||
server: payload.server
|
||||
};
|
||||
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 }) {
|
||||
state = {
|
||||
...state,
|
||||
connectionStatus: "connecting",
|
||||
peerType: "client",
|
||||
peer: payload.peer
|
||||
};
|
||||
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;
|
||||
},
|
||||
|
||||
connected(state) {
|
||||
(state as ClientNetworkState).connectionStatus = "connected";
|
||||
},
|
||||
|
||||
disconnected(state) {
|
||||
(state as ClientNetworkState).connectionStatus = "disconnected";
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,29 +1,30 @@
|
|||
import {
|
||||
PeerClient,
|
||||
PeerServer,
|
||||
LocalClient,
|
||||
RoomInfo,
|
||||
PeerMetadata
|
||||
} from "@/network";
|
||||
import { ChatMessage, LocalClient, PeerClient, PeerMetadata, PeerServer, RoomInfo } from "@/network";
|
||||
import Peer from "peerjs";
|
||||
|
||||
export interface ChatMessage {
|
||||
who: string;
|
||||
to: string;
|
||||
message: string;
|
||||
}
|
||||
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: "connecting" | "connected" | "disconnected" | "error";
|
||||
connectionStatus: ConnectionStatus;
|
||||
connectionError?: Error;
|
||||
peer: PeerClient;
|
||||
}
|
||||
|
|
|
@ -3,3 +3,4 @@ export * from "./MockPeer";
|
|||
export * from "./MockHelper";
|
||||
export * from "./EventHook";
|
||||
export * from "./IDBShim";
|
||||
export * from "./sync-utils";
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export function seconds(ms: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms * 1000);
|
||||
});
|
||||
}
|
|
@ -2,4 +2,4 @@ module.exports = {
|
|||
env: {
|
||||
jest: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import CardPicker from "@/components/DeckBuilder/CardPicker.vue";
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
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)
|
||||
|
@ -8,8 +10,8 @@ const testCards = new Array(10)
|
|||
const testSlots = testCards.map(c => ({ data: c, limit: 3, howmany: 1 }));
|
||||
|
||||
describe("components/DeckBuilder/CardPicker", () => {
|
||||
test("CardPicker correctly creates images for each card", () => {
|
||||
const wrapper = shallowMount(CardPicker, {
|
||||
test("CardPicker correctly instances images for each card", () => {
|
||||
const wrapper = mount(CardPicker, {
|
||||
propsData: {
|
||||
rows: 2,
|
||||
columns: 5,
|
||||
|
@ -20,6 +22,19 @@ describe("components/DeckBuilder/CardPicker", () => {
|
|||
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: {
|
||||
|
|
|
@ -6,7 +6,7 @@ setupIDBShim();
|
|||
describe("mlpccg/Database", () => {
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(15000);
|
||||
initDB();
|
||||
await initDB();
|
||||
await loadSets();
|
||||
});
|
||||
|
||||
|
|
|
@ -21,8 +21,8 @@ const testSessionOptions: DraftOptions = {
|
|||
|
||||
describe("mlpccg/draft", () => {
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(15000);
|
||||
initDB();
|
||||
jest.setTimeout(30000);
|
||||
await initDB();
|
||||
await loadSets();
|
||||
});
|
||||
|
||||
|
|
|
@ -13,11 +13,18 @@
|
|||
<b-button
|
||||
tag="router-link"
|
||||
to="/lobby"
|
||||
@click="gotoLobby"
|
||||
class="is-primary"
|
||||
class="is-primary spaced"
|
||||
inverted
|
||||
outlined
|
||||
>Play now!</b-button
|
||||
>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>
|
||||
|
@ -74,6 +81,7 @@
|
|||
>
|
||||
(ISC licensed)!
|
||||
</p>
|
||||
<p>MCG Version {{ projectVersion }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -108,15 +116,25 @@
|
|||
font-size: 11pt;
|
||||
}
|
||||
}
|
||||
|
||||
.spaced {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
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({
|
||||
components: {}
|
||||
})
|
||||
export default class Home extends Vue {
|
||||
private projectName: string = "MLPCARDGAME";
|
||||
private projectVersion: string = versionString;
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,26 +1,374 @@
|
|||
<template>
|
||||
<section class="lobby">
|
||||
<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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@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">
|
||||
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({
|
||||
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>
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<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>
|
|
@ -0,0 +1,191 @@
|
|||
<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>
|
|
@ -0,0 +1 @@
|
|||
export * from "./runner";
|
|
@ -0,0 +1,17 @@
|
|||
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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
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);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
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);
|
||||
}
|
|
@ -11,21 +11,11 @@
|
|||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env",
|
||||
"jest"
|
||||
],
|
||||
"types": ["webpack-env", "jest"],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
@ -34,7 +24,5 @@
|
|||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
module.exports = {
|
||||
publicPath: process.env.SUBPATH ? process.env.SUBPATH : ""
|
||||
configureWebpack: {
|
||||
devServer: {
|
||||
disableHostCheck: true,
|
||||
host: "0.0.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
publicPath: process.env.SUBPATH ? process.env.SUBPATH : "/",
|
||||
|
||||
pluginOptions: {
|
||||
gitDescribe: {
|
||||
variableName: "GIT_DESCRIBE"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
68
yarn.lock
68
yarn.lock
|
@ -770,6 +770,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
|
||||
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
|
||||
|
||||
"@types/jszip@^3.1.6":
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/jszip/-/jszip-3.1.6.tgz#0512574a2b35f3194b41769c56e4b35065e67000"
|
||||
integrity sha512-m8uFcI+O2EupCfbEVQWsBM/4nhbegjOHL7cQgBpM95FeF98kdFJXzy9/8yhx4b3lCRl/gMBhcvyh30Qt3X+XPQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
|
@ -4649,6 +4656,15 @@ getpass@^0.1.1:
|
|||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
git-describe@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/git-describe/-/git-describe-4.0.4.tgz#f3d55bce309becf6dc27fed535d380a621967e8c"
|
||||
integrity sha512-L1X9OO1e4MusB4PzG9LXeXCQifRvyuoHTpuuZ521Qyxn/B0kWHWEOtsT4LsSfSNacZz0h4ZdYDsDG7f+SrA3hg==
|
||||
dependencies:
|
||||
lodash "^4.17.11"
|
||||
optionalDependencies:
|
||||
semver "^5.6.0"
|
||||
|
||||
glob-base@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
|
||||
|
@ -5138,6 +5154,11 @@ immediate@^3.2.2:
|
|||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c"
|
||||
integrity sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=
|
||||
|
||||
immediate@~3.0.5:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
|
||||
|
||||
import-cwd@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
|
||||
|
@ -6353,6 +6374,16 @@ jsprim@^1.2.2:
|
|||
json-schema "0.2.3"
|
||||
verror "1.10.0"
|
||||
|
||||
jszip@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d"
|
||||
integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA==
|
||||
dependencies:
|
||||
lie "~3.3.0"
|
||||
pako "~1.0.2"
|
||||
readable-stream "~2.3.6"
|
||||
set-immediate-shim "~1.0.1"
|
||||
|
||||
killable@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
|
||||
|
@ -6434,6 +6465,13 @@ levn@^0.3.0, levn@~0.3.0:
|
|||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
lie@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
|
||||
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
|
||||
dependencies:
|
||||
immediate "~3.0.5"
|
||||
|
||||
lines-and-columns@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
||||
|
@ -6473,7 +6511,7 @@ loader-utils@^0.2.16:
|
|||
json5 "^0.5.0"
|
||||
object-assign "^4.0.1"
|
||||
|
||||
loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
|
||||
loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
|
||||
integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
|
||||
|
@ -7676,7 +7714,7 @@ p-try@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||
|
||||
pako@~1.0.5:
|
||||
pako@~1.0.2, pako@~1.0.5:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732"
|
||||
integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==
|
||||
|
@ -9089,6 +9127,14 @@ sax@^1.2.4, sax@~1.2.4:
|
|||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
|
||||
schema-utils@^0.4.0:
|
||||
version "0.4.7"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187"
|
||||
integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==
|
||||
dependencies:
|
||||
ajv "^6.1.0"
|
||||
ajv-keywords "^3.1.0"
|
||||
|
||||
schema-utils@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
|
||||
|
@ -9195,6 +9241,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
||||
|
||||
set-immediate-shim@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
|
||||
integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=
|
||||
|
||||
set-value@^2.0.0, set-value@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
|
||||
|
@ -10453,6 +10504,11 @@ vue-cli-plugin-buefy@^0.3.7:
|
|||
resolved "https://registry.yarnpkg.com/vue-cli-plugin-buefy/-/vue-cli-plugin-buefy-0.3.7.tgz#31e5637529482a5a4564676f539db16278b0895c"
|
||||
integrity sha512-w+1Wnj1VmoLJmv0yHoLUErnFkLSzz4yTGhCz7GudDKX7vB5NpVK+/xWmiFOytuDkleWeYrJgHN8CIOY7xe26jQ==
|
||||
|
||||
vue-cli-plugin-git-describe@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-cli-plugin-git-describe/-/vue-cli-plugin-git-describe-1.0.0.tgz#6980e7e7c9b30a8da5e56363c43a8224d3b178e7"
|
||||
integrity sha512-vrln5ryf1VucQ3SD1NfMTnzM8Uyw0CDEjL9KYzyrGXGbTJuFXhPfm2fJsSvOClS5sotjOV9+4MnIWlqvZO5JuQ==
|
||||
|
||||
vue-eslint-parser@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1"
|
||||
|
@ -10965,6 +11021,14 @@ worker-farm@^1.7.0:
|
|||
dependencies:
|
||||
errno "~0.1.7"
|
||||
|
||||
worker-loader@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac"
|
||||
integrity sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==
|
||||
dependencies:
|
||||
loader-utils "^1.0.0"
|
||||
schema-utils "^0.4.0"
|
||||
|
||||
wrap-ansi@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
|
||||
|
|
Loading…
Reference in New Issue