Move webclient to cardgage

This commit is contained in:
Hamcha 2019-06-21 10:19:35 +02:00
parent 9feb703f01
commit 14ac80df84
Signed by: hamcha
GPG Key ID: A40413D21021EAEE
27 changed files with 2 additions and 9550 deletions

View File

@ -2,6 +2,8 @@
Tools and services needed for the MCG backend to work.
## Inside this repository
### draftbot
Bot for drafting MLP:CCG sets and cubes on MCG
@ -9,7 +11,3 @@ Bot for drafting MLP:CCG sets and cubes on MCG
### buildmap / convertsets / genpics
Tools for converting data/image packs from OCTGN to MCG/Cardgage
### webclient
Client for connecting to a Cardgage server and creating/joining test rooms

View File

@ -1,2 +0,0 @@
> 1%
last 2 versions

View File

@ -1,14 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"],
rules: {
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"
},
parserOptions: {
parser: "@typescript-eslint/parser"
}
};

21
webclient/.gitignore vendored
View File

@ -1,21 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,3 +0,0 @@
# webclient
Web-client for browsing, creating and joining rooms on Cardgage servers

View File

@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/app"]
};

View File

@ -1,35 +0,0 @@
{
"name": "webclient",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"buefy": "^0.7.4",
"core-js": "^3.1.2",
"vue": "^2.6.10",
"vue-class-component": "^7.0.2",
"vue-property-decorator": "^8.1.0",
"vuex": "^3.0.1",
"vuex-class": "^0.3.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.0.0-alpha.1",
"@vue/cli-plugin-eslint": "^4.0.0-alpha.1",
"@vue/cli-plugin-typescript": "^4.0.0-alpha.1",
"@vue/cli-service": "^4.0.0-alpha.1",
"@vue/eslint-config-prettier": "^4.0.1",
"@vue/eslint-config-typescript": "^4.0.0",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"node-sass": "^4.9.0",
"sass": "^1.19.0",
"sass-loader": "^7.1.0",
"typescript": "^3.4.5",
"vue-cli-plugin-buefy": "^0.3.6",
"vue-template-compiler": "^2.6.10"
}
}

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {}
}
};

View File

@ -1,5 +0,0 @@
// prettier.config.js or .prettierrc.js
module.exports = {
tabWidth: 4,
useTabs: true
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,24 +0,0 @@
<!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>Cardgage Web Client</title>
</head>
<body>
<noscript>
<strong
>We're sorry but webclient 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>

View File

@ -1,77 +0,0 @@
<template>
<div id="app">
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
<h1 class="title">{{ title }}</h1>
<h2 class="subtitle">{{ subtitle }}</h2>
</div>
</div>
</section>
<div class="container">
<ConnectForm v-if="!isConnected" />
<RoomList v-if="inLobby" />
<RoomLog v-if="inRoom" />
</div>
</div>
</template>
<style lang="scss" scoped>
.hero {
margin-bottom: 2rem;
}
</style>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { State } from "vuex-class";
import ConnectForm from "@/components/ConnectForm.vue";
import RoomList from "@/components/RoomList.vue";
import RoomLog from "@/components/RoomLog.vue";
import { ServerState } from "./store/server";
import { RoomState } from "./store/room";
@Component({
components: {
ConnectForm,
RoomList,
RoomLog
}
})
export default class App extends Vue {
@State("server") private server!: ServerState;
@State("room") private room!: RoomState;
private data() {
return {};
}
private get isConnected(): boolean {
return this.server.connected;
}
private get inLobby(): boolean {
return this.isConnected && !this.room.in_room;
}
private get inRoom(): boolean {
return this.isConnected && this.room.in_room;
}
private get title(): string {
return "Cardgage test client";
}
private get subtitle(): string {
if (this.inRoom && this.room.room) {
return `Inside room "${this.room.room.name}" (${
this.room.room.id
})`;
}
if (this.isConnected) {
return "Browsing " + this.server.server;
}
return "";
}
}
</script>

View File

@ -1,153 +0,0 @@
// Included below are all the defined variables from Bulma
// Modify as needed, removing the !default attribute.
// Colors
$black: hsl(0, 0%, 4%) !default;
$black-bis: hsl(0, 0%, 7%) !default;
$black-ter: hsl(0, 0%, 14%) !default;
$grey-darker: hsl(0, 0%, 21%) !default;
$grey-dark: hsl(0, 0%, 29%) !default;
$grey: hsl(0, 0%, 48%) !default;
$grey-light: hsl(0, 0%, 71%) !default;
$grey-lighter: hsl(0, 0%, 86%) !default;
$white-ter: hsl(0, 0%, 96%) !default;
$white-bis: hsl(0, 0%, 98%) !default;
$white: hsl(0, 0%, 100%) !default;
$orange: hsl(14, 100%, 53%) !default;
$yellow: hsl(48, 100%, 67%) !default;
$green: hsl(141, 71%, 48%) !default;
$turquoise: hsl(171, 100%, 41%) !default;
$cyan: hsl(204, 86%, 53%) !default;
$blue: hsl(217, 71%, 53%) !default;
$purple: hsl(271, 100%, 71%) !default;
$red: hsl(348, 100%, 61%) !default;
// Typography
$family-sans-serif: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
"Helvetica", "Arial", sans-serif !default;
$family-monospace: monospace !default;
$render-mode: optimizeLegibility !default;
$size-1: 3rem !default;
$size-2: 2.5rem !default;
$size-3: 2rem !default;
$size-4: 1.5rem !default;
$size-5: 1.25rem !default;
$size-6: 1rem !default;
$size-7: 0.75rem !default;
$weight-light: 300 !default;
$weight-normal: 400 !default;
$weight-medium: 500 !default;
$weight-semibold: 600 !default;
$weight-bold: 700 !default;
// Responsiveness
// The container horizontal gap, which acts as the offset for breakpoints
$gap: 32px !default;
// 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16
$tablet: 769px !default;
// 960px container + 4rem
$desktop: 960px + (2 * $gap) !default;
// 1152px container + 4rem
$widescreen: 1152px + (2 * $gap) !default;
// 1344px container + 4rem;
$fullhd: 1344px + (2 * $gap) !default;
// Miscellaneous
$easing: ease-out !default;
$radius-small: 2px !default;
$radius: 3px !default;
$radius-large: 5px !default;
$radius-rounded: 290486px !default;
$speed: 86ms !default;
// Flags
$variable-columns: true !default;
// The default Bulma derived variables are declared below
$primary: $turquoise !default;
$info: $cyan !default;
$success: $green !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $white-ter !default;
$dark: $grey-darker !default;
// Invert colors
$orange-invert: findColorInvert($orange) !default;
$yellow-invert: findColorInvert($yellow) !default;
$green-invert: findColorInvert($green) !default;
$turquoise-invert: findColorInvert($turquoise) !default;
$cyan-invert: findColorInvert($cyan) !default;
$blue-invert: findColorInvert($blue) !default;
$purple-invert: findColorInvert($purple) !default;
$red-invert: findColorInvert($red) !default;
$primary-invert: $turquoise-invert !default;
$info-invert: $cyan-invert !default;
$success-invert: $green-invert !default;
$warning-invert: $yellow-invert !default;
$danger-invert: $red-invert !default;
$light-invert: $dark !default;
$dark-invert: $light !default;
// General colors
$background: $white-ter !default;
$border: $grey-lighter !default;
$border-hover: $grey-light !default;
// Text colors
$text: $grey-dark !default;
$text-invert: findColorInvert($text) !default;
$text-light: $grey !default;
$text-strong: $grey-darker !default;
// Code colors
$code: $red !default;
$code-background: $background !default;
$pre: $text !default;
$pre-background: $background !default;
// Link colors
$link: $blue !default;
$link-invert: $blue-invert !default;
$link-visited: $purple !default;
$link-hover: $grey-darker !default;
$link-hover-border: $grey-light !default;
$link-focus: $grey-darker !default;
$link-focus-border: $blue !default;
$link-active: $grey-darker !default;
$link-active-border: $grey-dark !default;
// Typography
$family-primary: $family-sans-serif !default;
$family-code: $family-monospace !default;
$size-small: $size-7 !default;
$size-normal: $size-6 !default;
$size-medium: $size-5 !default;
$size-large: $size-4 !default;

View File

@ -1,10 +0,0 @@
@import "~bulma/sass/utilities/initial-variables";
@import "~bulma/sass/utilities/functions";
@import "variables";
@import "~bulma/sass/utilities/derived-variables";
@import "~bulma";
@import "~buefy/src/scss/buefy";
@import "overrides";

View File

@ -1,48 +0,0 @@
<template>
<section>
<form @submit.prevent="tryConnect">
<b-notification
type="is-danger"
aria-close-label="Close notification"
role="alert"
v-if="server.connectionError"
>
{{ server.connectionError }}
</b-notification>
<b-field label="Server address">
<b-input size="is-large" v-model="addr"></b-input>
</b-field>
<b-field>
<button type="submit" class="button is-primary">Connect</button>
</b-field>
<b-loading
is-full-page
:active.sync="server.connecting"
></b-loading>
</form>
</section>
</template>
<script lang="ts">
import { Action, State } from "vuex-class";
import { Component, Vue } from "vue-property-decorator";
import { ServerState } from "@/store/server";
@Component({})
export default class ConnectForm extends Vue {
@State("server") private server!: ServerState;
@Action("connect", { namespace: "server" }) private connect: any;
private addr!: string;
private loading!: boolean;
private data() {
return {
addr: "http://192.168.22.22"
};
}
private async tryConnect() {
this.connect(this.addr);
}
}
</script>

View File

@ -1,107 +0,0 @@
<template>
<div class="container">
<b-button @click="create" icon-left="library-plus" type="is-primary"
>Create</b-button
>
<b-table
striped
hoverable
:data="rooms"
:columns="columns"
@click="rowClicked"
></b-table>
</div>
</template>
<style lang="scss" scoped>
.b-table {
cursor: pointer;
}
</style>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { State, Action } from "vuex-class";
import { ServerState } from "@/store/server";
import { Room } from "@/store/room";
const columns = [
{
field: "game_id",
label: "Game"
},
{
field: "name",
label: "Name"
},
{
field: "creator",
label: "Creator"
},
{
field: "can_spectate",
label: "Spectators allowed"
},
{
field: "players",
label: "Players",
numeric: true
},
{
field: "spectators",
label: "Spectators",
numeric: true
}
];
interface RoomRow {
id: string;
game_id: string;
name: string;
creator: string;
players: number;
spectators: number;
can_spectate: string;
}
@Component({})
export default class RoomList extends Vue {
@State("server") private server!: ServerState;
@Action("joinRoom", { namespace: "server" }) private join: any;
@Action("createRoom", { namespace: "server" }) private create: any;
private data() {
return {
columns
};
}
private get rooms(): RoomRow[] {
if (this.server.rooms == null) {
return [];
}
let rooms: RoomRow[] = [];
for (const room of this.server.rooms) {
rooms.push({
id: room.id,
game_id: room.game_id,
name: room.name,
creator: room.creator,
players: room.current_players ? room.current_players : 0,
spectators: room.current_spectators
? room.current_spectators
: 0,
can_spectate: room.can_spectate ? "Yes" : "No"
});
}
return rooms;
}
private rowClicked(row: RoomRow) {
this.join({
roomid: row.id,
as_spectator: row.can_spectate == "Yes"
});
}
}
</script>

View File

@ -1,88 +0,0 @@
<template>
<div class="container">
<b-table striped hoverable :data="rows" :columns="columns"></b-table>
<br />
<b-field grouped>
<b-select v-model="target" placeholder="Target">
<option value="@channel">
Channel
</option>
</b-select>
<b-input v-model="text" placeholder="Message" expanded></b-input>
<p class="control">
<button @click="sendTxt" class="button is-primary">Send</button>
</p>
</b-field>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { State, Getter } from "vuex-class";
import { RoomState } from "@/store/room";
const columns = [
{
field: "time",
label: "Time",
width: 300
},
{
field: "type",
label: "Type",
width: 100
},
{
field: "message",
label: "Message"
}
];
interface LogRow {
time: string;
type: string;
message: string;
}
@Component({})
export default class RoomLog extends Vue {
@State("room") private room!: RoomState;
private target!: "@channel" | string;
private text!: string;
private data() {
return {
columns,
target: "@channel",
text: ""
};
}
private get rows(): LogRow[] {
return this.room.messages.map(v => {
let fullType = v.type;
let msg = "* no message *";
if (v.type == "message" && v.message) {
fullType += "/" + v.message.type;
if (v.message.message && v.message.message != "") {
msg = v.message.message;
}
}
if (v.type == "event" && v.event) {
fullType += "/" + v.event.type;
if (v.event.message && v.event.message != "") {
msg = v.event.message;
}
}
return {
time: v.time,
type: fullType,
message: msg
};
});
}
private sendTxt() {}
}
</script>

View File

@ -1,14 +0,0 @@
import Vue from "vue";
import App from "./App.vue";
import store from "./store/index";
import Buefy from "buefy";
import "./assets/scss/app.scss";
Vue.use(Buefy);
Vue.config.productionTip = false;
new Vue({
store,
render: h => h(App)
}).$mount("#app");

View File

@ -1,75 +0,0 @@
import { Room, RoomServerMessage, RoomMessage } from "./store/room";
export interface RoomConnectionData {
ws_url: string;
auth_token: string;
}
export type MessageHandler = (msg: RoomServerMessage) => void;
export default class RoomClient {
private ws: WebSocket;
public info: Room;
public backlog: RoomServerMessage[];
private onMessage?: MessageHandler;
private buffer: RoomServerMessage[];
constructor(_ws: WebSocket, _info: Room) {
this.info = _info;
this.buffer = [];
this.backlog = [];
this.ws = _ws;
this.ws.addEventListener("message", this._received.bind(this));
}
private _received(ev: MessageEvent) {
let data = JSON.parse(ev.data);
if (this.onMessage) {
return this.onMessage(data);
}
// Save messages in a buffer if no handler is set
this.buffer.push(data);
}
public setMessageHandler(handler: MessageHandler) {
// Set as handler for all future messages
this.onMessage = handler;
// If we have messages in our buffer, send them over
for (const msg of this.buffer) {
handler(msg);
}
// Empty buffer
this.buffer = [];
}
public static connect(wsdata: RoomConnectionData): Promise<RoomClient> {
return new Promise((resolve, reject) => {
let ws = new WebSocket(wsdata.ws_url);
const onMessage = (ev: MessageEvent) => {
// Unregister handler
ws.removeEventListener("message", onMessage);
// Parse message
let data = JSON.parse(ev.data) as RoomServerMessage;
if ("error" in data) {
reject(data.error);
}
if (data.room) {
let client = new RoomClient(ws, data.room);
// Check for backlog
if (data.backlog) {
client.backlog = data.backlog;
}
resolve(client);
} else {
reject("missing room info");
}
};
ws.addEventListener("message", onMessage);
ws.addEventListener("open", ev => {
// Send authentication
ws.send(JSON.stringify({ token: wsdata.auth_token }));
});
});
}
}

View File

@ -1,13 +0,0 @@
import Vue, { VNode } from "vue";
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

View File

@ -1,4 +0,0 @@
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}

View File

@ -1,27 +0,0 @@
import Vue from "vue";
import Vuex, { StoreOptions } from "vuex";
import { server } from "./server";
import { room } from "./room";
Vue.use(Vuex);
export interface AppState {
// Client info
playerName: string;
}
const store: StoreOptions<AppState> = {
state: {
playerName:
"webclient-" +
Math.random()
.toString(32)
.slice(2)
},
modules: {
server,
room
}
};
export default new Vuex.Store<AppState>(store);

View File

@ -1,98 +0,0 @@
import { Module, MutationTree, ActionTree, GetterTree } from "vuex";
import { AppState } from "@/store/index";
import RoomClient from "@/roomclient";
const namespaced: boolean = true;
export interface Room {
id: string;
game_id: string;
name: string;
creator: string;
players: string[];
spectators: string[];
current_players: number;
current_spectators: number;
can_spectate: boolean;
tags: string[];
}
export interface RoomEvent {
type: string;
player?: string;
role?: string;
room?: Room;
message?: string;
}
export interface RoomMessage {
from?: string;
to?: string;
channel: string;
type: string;
data: Object;
message?: string;
}
export interface RoomServerMessage {
roomid: string;
time: string;
type: "event" | "message" | "welcome";
event?: RoomEvent;
message?: RoomMessage;
error?: string;
backlog?: RoomServerMessage[];
room?: Room;
}
export interface RoomState {
in_room: boolean;
room: Room | null;
client: RoomClient | null;
messages: RoomServerMessage[];
}
const state: RoomState = {
in_room: false,
room: null,
client: null,
messages: []
};
const mutations: MutationTree<RoomState> = {
joinedRoom(state: RoomState, ws: RoomClient) {
state.client = ws;
state.in_room = true;
state.room = ws.info;
state.messages = ws.backlog;
},
messageReceived(state: RoomState, msg: RoomServerMessage) {
state.messages.push(msg);
}
};
const actions: ActionTree<RoomState, AppState> = {
setClient({ commit }, ws: RoomClient) {
ws.setMessageHandler(msg => {
commit("messageReceived", msg);
});
commit("joinedRoom", ws);
}
};
const getters: GetterTree<RoomState, AppState> = {
users(state: RoomState): string[] {
if (!state.room) {
return [];
}
return [...state.room.players, ...state.room.spectators];
}
};
export const room: Module<RoomState, AppState> = {
namespaced,
state,
mutations,
actions,
getters
};

View File

@ -1,137 +0,0 @@
import { Module, ActionTree, MutationTree, GetterTree } from "vuex";
import { AppState } from "@/store/index";
import { Room } from "./room";
import RoomClient from "@/roomclient";
const namespaced: boolean = true;
export interface ServerState {
server: string;
joining: boolean;
connecting: boolean;
connected: boolean;
rooms: Room[] | null;
connectionError: string;
joinError: string;
}
const state: ServerState = {
connecting: false,
joining: false,
connected: false,
server: "",
rooms: null,
connectionError: "",
joinError: ""
};
const mutations: MutationTree<ServerState> = {
beginConnection(state: ServerState) {
state.connected = false;
state.connectionError = "";
state.connecting = true;
},
beginJoin(state: ServerState) {
state.joining = true;
},
connectionDone(
state: ServerState,
payload: { addr: string; rooms: Room[] }
) {
state.connected = true;
state.server = payload.addr;
state.rooms = payload.rooms;
},
connectionFailed(state: ServerState, err: Error) {
state.connecting = false;
state.connectionError = err.message;
},
joinFailed(state: ServerState, err: Error) {
state.joining = false;
state.joinError = err.message;
}
};
const actions: ActionTree<ServerState, AppState> = {
async connect({ commit }, addr: string) {
commit("beginConnection");
// Get room list
try {
let req = await fetch(`${addr}/api/lobby/room/list`);
let data = await req.json();
commit("connectionDone", { addr, rooms: data.rooms });
} catch (err) {
commit("connectionFailed", err);
}
},
async joinRoom(
{ state, commit, dispatch, rootState },
{ roomid, as_spectator }
) {
commit("beginJoin");
// Try joining room
try {
// Ask lobby server for permission
let req = await fetch(`${state.server}/api/lobby/room/join`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
room_id: roomid,
player_name: rootState.playerName,
as_player: !as_spectator
})
});
let data = await req.json();
// We haz permission, let's contact the room server
let ws = await RoomClient.connect(data);
dispatch("room/setClient", ws, { root: true });
} catch (err) {
commit("joinFailed", err);
}
},
async createRoom({ state, commit, dispatch, rootState }) {
commit("beginJoin");
// Try creating room
try {
// Ask lobby server for permission
let req = await fetch(`${state.server}/api/lobby/room/create`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
game_id: "webclient",
player_name: rootState.playerName,
name: `${rootState.playerName}'s room`,
allow_spectators: true,
max_players: 32,
max_spectators: 32,
tags: ["test"]
})
});
let data = await req.json();
// We haz permission, let's contact the room server
let ws = await RoomClient.connect(data);
dispatch("room/setClient", ws, { root: true });
} catch (err) {
commit("joinFailed", err);
}
}
};
export const server: Module<ServerState, AppState> = {
namespaced,
state,
actions,
mutations
};

View File

@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": ["webpack-env"],
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load Diff