Web client works!
This commit is contained in:
parent
0b31cc86a2
commit
6abce0dc72
6 changed files with 382 additions and 20 deletions
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<section class="hero">
|
<section class="hero is-primary">
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="title">{{ title }}</h1>
|
<h1 class="title">{{ title }}</h1>
|
||||||
|
@ -10,38 +10,68 @@
|
||||||
</section>
|
</section>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ConnectForm v-if="!isConnected" />
|
<ConnectForm v-if="!isConnected" />
|
||||||
|
<RoomList v-if="inLobby" />
|
||||||
|
<RoomLog v-if="inRoom" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hero {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
import { State } from "vuex-class";
|
import { State } from "vuex-class";
|
||||||
import ConnectForm from "@/components/ConnectForm.vue";
|
import ConnectForm from "@/components/ConnectForm.vue";
|
||||||
import RoomList from "@/components/RoomList.vue";
|
import RoomList from "@/components/RoomList.vue";
|
||||||
|
import RoomLog from "@/components/RoomLog.vue";
|
||||||
import { ServerState } from "./store/server";
|
import { ServerState } from "./store/server";
|
||||||
|
import { RoomState } from "./store/room";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
ConnectForm,
|
ConnectForm,
|
||||||
RoomList
|
RoomList,
|
||||||
|
RoomLog
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export default class App extends Vue {
|
export default class App extends Vue {
|
||||||
@State("server") private server!: ServerState;
|
@State("server") private server!: ServerState;
|
||||||
|
@State("room") private room!: RoomState;
|
||||||
private title!: string;
|
|
||||||
private subtitle!: string;
|
|
||||||
|
|
||||||
private data() {
|
private data() {
|
||||||
return {
|
return {};
|
||||||
title: "Cardgage web client",
|
|
||||||
subtitle: ""
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get isConnected() {
|
private get isConnected(): boolean {
|
||||||
return this.server.connected;
|
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>
|
</script>
|
||||||
|
|
|
@ -1,12 +1,101 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Room list</h1>
|
<b-table
|
||||||
|
striped
|
||||||
|
hoverable
|
||||||
|
:data="rooms"
|
||||||
|
:columns="columns"
|
||||||
|
@click="rowClicked"
|
||||||
|
></b-table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.b-table {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
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({})
|
@Component({})
|
||||||
export default class RoomList extends Vue {}
|
export default class RoomList extends Vue {
|
||||||
|
@State("server") private server!: ServerState;
|
||||||
|
@Action("joinRoom", { namespace: "server" }) private join: 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,
|
||||||
|
spectators: room.current_spectators,
|
||||||
|
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>
|
</script>
|
||||||
|
|
70
webclient/src/components/RoomLog.vue
Normal file
70
webclient/src/components/RoomLog.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<b-table striped hoverable :data="rows" :columns="columns"></b-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
import { State } 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 data() {
|
||||||
|
return {
|
||||||
|
columns
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
65
webclient/src/roomclient.ts
Normal file
65
webclient/src/roomclient.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
private onMessage?: MessageHandler;
|
||||||
|
private buffer: RoomServerMessage[];
|
||||||
|
|
||||||
|
constructor(_ws: WebSocket, _info: Room) {
|
||||||
|
this.info = _info;
|
||||||
|
this.buffer = [];
|
||||||
|
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);
|
||||||
|
if ("error" in data) {
|
||||||
|
reject(data.error);
|
||||||
|
}
|
||||||
|
let client = new RoomClient(ws, data.event.room);
|
||||||
|
resolve(client);
|
||||||
|
};
|
||||||
|
ws.addEventListener("message", onMessage);
|
||||||
|
ws.addEventListener("open", ev => {
|
||||||
|
// Send authentication
|
||||||
|
ws.send(JSON.stringify({ token: wsdata.auth_token }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,83 @@
|
||||||
import { Module } from "vuex";
|
import { Module, MutationTree, ActionTree } from "vuex";
|
||||||
import { AppState } from "@/store/index";
|
import { AppState } from "@/store/index";
|
||||||
|
import RoomClient from "@/roomclient";
|
||||||
|
|
||||||
const namespaced: boolean = true;
|
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 {
|
||||||
|
time: "string";
|
||||||
|
type: "event" | "message";
|
||||||
|
event?: RoomEvent;
|
||||||
|
message?: RoomMessage;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RoomState {
|
export interface RoomState {
|
||||||
in_room: boolean;
|
in_room: boolean;
|
||||||
room: string;
|
room: Room | null;
|
||||||
|
client: RoomClient | null;
|
||||||
|
messages: RoomServerMessage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: RoomState = {
|
const state: RoomState = {
|
||||||
in_room: false,
|
in_room: false,
|
||||||
room: ""
|
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;
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const room: Module<RoomState, AppState> = {
|
export const room: Module<RoomState, AppState> = {
|
||||||
namespaced,
|
namespaced,
|
||||||
state
|
state,
|
||||||
|
mutations,
|
||||||
|
actions
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
import { Module, ActionTree, MutationTree, GetterTree } from "vuex";
|
import { Module, ActionTree, MutationTree, GetterTree } from "vuex";
|
||||||
import { AppState } from "@/store/index";
|
import { AppState } from "@/store/index";
|
||||||
|
import { Room } from "./room";
|
||||||
|
import RoomClient from "@/roomclient";
|
||||||
|
|
||||||
const namespaced: boolean = true;
|
const namespaced: boolean = true;
|
||||||
|
|
||||||
export interface ServerState {
|
export interface ServerState {
|
||||||
server: string;
|
server: string;
|
||||||
|
joining: boolean;
|
||||||
connecting: boolean;
|
connecting: boolean;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
rooms: Object | null;
|
rooms: Room[] | null;
|
||||||
connectionError: string;
|
connectionError: string;
|
||||||
|
joinError: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: ServerState = {
|
const state: ServerState = {
|
||||||
connecting: false,
|
connecting: false,
|
||||||
|
joining: false,
|
||||||
connected: false,
|
connected: false,
|
||||||
server: "",
|
server: "",
|
||||||
rooms: null,
|
rooms: null,
|
||||||
connectionError: ""
|
connectionError: "",
|
||||||
|
joinError: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutations: MutationTree<ServerState> = {
|
const mutations: MutationTree<ServerState> = {
|
||||||
|
@ -26,9 +32,13 @@ const mutations: MutationTree<ServerState> = {
|
||||||
state.connecting = true;
|
state.connecting = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
beginJoin(state: ServerState) {
|
||||||
|
state.joining = true;
|
||||||
|
},
|
||||||
|
|
||||||
connectionDone(
|
connectionDone(
|
||||||
state: ServerState,
|
state: ServerState,
|
||||||
payload: { addr: string; rooms: Object }
|
payload: { addr: string; rooms: Room[] }
|
||||||
) {
|
) {
|
||||||
state.connected = true;
|
state.connected = true;
|
||||||
state.server = payload.addr;
|
state.server = payload.addr;
|
||||||
|
@ -38,6 +48,11 @@ const mutations: MutationTree<ServerState> = {
|
||||||
connectionFailed(state: ServerState, err: Error) {
|
connectionFailed(state: ServerState, err: Error) {
|
||||||
state.connecting = false;
|
state.connecting = false;
|
||||||
state.connectionError = err.message;
|
state.connectionError = err.message;
|
||||||
|
},
|
||||||
|
|
||||||
|
joinFailed(state: ServerState, err: Error) {
|
||||||
|
state.joining = false;
|
||||||
|
state.joinError = err.message;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,10 +63,39 @@ const actions: ActionTree<ServerState, AppState> = {
|
||||||
try {
|
try {
|
||||||
let req = await fetch(`${addr}/api/lobby/room/list`);
|
let req = await fetch(`${addr}/api/lobby/room/list`);
|
||||||
let data = await req.json();
|
let data = await req.json();
|
||||||
commit("connectionDone", { addr, rooms: data });
|
commit("connectionDone", { addr, rooms: data.rooms });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
commit("connectionFailed", 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue