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>
|
||||
<div id="app">
|
||||
<section class="hero">
|
||||
<section class="hero is-primary">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">{{ title }}</h1>
|
||||
|
@ -10,38 +10,68 @@
|
|||
</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
|
||||
RoomList,
|
||||
RoomLog
|
||||
}
|
||||
})
|
||||
export default class App extends Vue {
|
||||
@State("server") private server!: ServerState;
|
||||
|
||||
private title!: string;
|
||||
private subtitle!: string;
|
||||
@State("room") private room!: RoomState;
|
||||
|
||||
private data() {
|
||||
return {
|
||||
title: "Cardgage web client",
|
||||
subtitle: ""
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
private get isConnected() {
|
||||
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>
|
||||
|
|
|
@ -1,12 +1,101 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<h1>Room list</h1>
|
||||
<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 {}
|
||||
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>
|
||||
|
|
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 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 {
|
||||
time: "string";
|
||||
type: "event" | "message";
|
||||
event?: RoomEvent;
|
||||
message?: RoomMessage;
|
||||
}
|
||||
|
||||
export interface RoomState {
|
||||
in_room: boolean;
|
||||
room: string;
|
||||
room: Room | null;
|
||||
client: RoomClient | null;
|
||||
messages: RoomServerMessage[];
|
||||
}
|
||||
|
||||
const state: RoomState = {
|
||||
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> = {
|
||||
namespaced,
|
||||
state
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
};
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
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: Object | null;
|
||||
rooms: Room[] | null;
|
||||
connectionError: string;
|
||||
joinError: string;
|
||||
}
|
||||
|
||||
const state: ServerState = {
|
||||
connecting: false,
|
||||
joining: false,
|
||||
connected: false,
|
||||
server: "",
|
||||
rooms: null,
|
||||
connectionError: ""
|
||||
connectionError: "",
|
||||
joinError: ""
|
||||
};
|
||||
|
||||
const mutations: MutationTree<ServerState> = {
|
||||
|
@ -26,9 +32,13 @@ const mutations: MutationTree<ServerState> = {
|
|||
state.connecting = true;
|
||||
},
|
||||
|
||||
beginJoin(state: ServerState) {
|
||||
state.joining = true;
|
||||
},
|
||||
|
||||
connectionDone(
|
||||
state: ServerState,
|
||||
payload: { addr: string; rooms: Object }
|
||||
payload: { addr: string; rooms: Room[] }
|
||||
) {
|
||||
state.connected = true;
|
||||
state.server = payload.addr;
|
||||
|
@ -38,6 +48,11 @@ const mutations: MutationTree<ServerState> = {
|
|||
connectionFailed(state: ServerState, err: Error) {
|
||||
state.connecting = false;
|
||||
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 {
|
||||
let req = await fetch(`${addr}/api/lobby/room/list`);
|
||||
let data = await req.json();
|
||||
commit("connectionDone", { addr, rooms: data });
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue