diff --git a/webclient/src/App.vue b/webclient/src/App.vue index be3088f..79bee4a 100644 --- a/webclient/src/App.vue +++ b/webclient/src/App.vue @@ -1,6 +1,6 @@ + + diff --git a/webclient/src/components/RoomList.vue b/webclient/src/components/RoomList.vue index fa779ef..a793f42 100644 --- a/webclient/src/components/RoomList.vue +++ b/webclient/src/components/RoomList.vue @@ -1,12 +1,101 @@ + + diff --git a/webclient/src/components/RoomLog.vue b/webclient/src/components/RoomLog.vue new file mode 100644 index 0000000..edf2f52 --- /dev/null +++ b/webclient/src/components/RoomLog.vue @@ -0,0 +1,70 @@ + + + + diff --git a/webclient/src/roomclient.ts b/webclient/src/roomclient.ts new file mode 100644 index 0000000..60cdc88 --- /dev/null +++ b/webclient/src/roomclient.ts @@ -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 { + 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 })); + }); + }); + } +} diff --git a/webclient/src/store/room.ts b/webclient/src/store/room.ts index 47fd81c..e255189 100644 --- a/webclient/src/store/room.ts +++ b/webclient/src/store/room.ts @@ -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 = { + 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 = { + setClient({ commit }, ws: RoomClient) { + ws.setMessageHandler(msg => { + commit("messageReceived", msg); + }); + commit("joinedRoom", ws); + } }; export const room: Module = { namespaced, - state + state, + mutations, + actions }; diff --git a/webclient/src/store/server.ts b/webclient/src/store/server.ts index a7a1186..8309977 100644 --- a/webclient/src/store/server.ts +++ b/webclient/src/store/server.ts @@ -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 = { @@ -26,9 +32,13 @@ const mutations: MutationTree = { 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 = { 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 = { 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); + } } };