added session pings and connected player count

This commit is contained in:
Steffen 2020-05-24 22:13:52 +02:00
parent 28a99ae914
commit 06a11d7208
No known key found for this signature in database
GPG Key ID: 764D74E98267DFC6
3 changed files with 136 additions and 60 deletions

View File

@ -1,19 +1,26 @@
<template> <template>
<div id="controls"> <div id="controls">
<Screenshot ref="screenshot"></Screenshot> <Screenshot ref="screenshot"></Screenshot>
<font-awesome-icon <span class="session">
@click="leaveSession" <font-awesome-icon
icon="broadcast-tower" @click="leaveSession"
v-if="session.sessionId" icon="broadcast-tower"
v-bind:class="{ spectator: session.isSpectator }" v-if="session.sessionId"
title="You're currently in a live game!" :class="{ spectator: session.isSpectator }"
/> :title="
<font-awesome-icon `You're currently in a live game with ${session.playerCount} other players!`
icon="camera" "
@click="takeScreenshot()" />
title="Take a screenshot" {{ session.playerCount }}
v-bind:class="{ success: grimoire.isScreenshotSuccess }" </span>
/> <span class="camera">
<font-awesome-icon
icon="camera"
@click="takeScreenshot()"
title="Take a screenshot"
:class="{ success: grimoire.isScreenshotSuccess }"
/>
</span>
<div class="menu" v-bind:class="{ open: grimoire.isMenuOpen }"> <div class="menu" v-bind:class="{ open: grimoire.isMenuOpen }">
<font-awesome-icon icon="cog" @click="toggleMenu" /> <font-awesome-icon icon="cog" @click="toggleMenu" />
<ul> <ul>
@ -241,17 +248,19 @@ export default {
} }
} }
> svg { > span > svg {
cursor: pointer; cursor: pointer;
z-index: 5; z-index: 5;
margin-top: 10px; margin-top: 10px;
margin-left: 10px; margin-left: 10px;
} }
> .fa-broadcast-tower { .session {
color: $demon; .fa-broadcast-tower {
&.spectator { color: $demon;
color: $townsfolk; &.spectator {
color: $townsfolk;
}
} }
} }
} }

View File

@ -38,7 +38,8 @@ export default new Vuex.Store({
}, },
session: { session: {
sessionId: "", sessionId: "",
isSpectator: false isSpectator: false,
playerCount: 0
}, },
modals: { modals: {
edition: false, edition: false,
@ -78,6 +79,9 @@ export default new Vuex.Store({
setSpectator({ session }, spectator) { setSpectator({ session }, spectator) {
session.isSpectator = spectator; session.isSpectator = spectator;
}, },
setPlayerCount({ session }, playerCount) {
session.playerCount = playerCount;
},
setBluff({ grimoire }, { index, role } = {}) { setBluff({ grimoire }, { index, role } = {}) {
if (index !== undefined) { if (index !== undefined) {
grimoire.bluffs.splice(index, 1, role); grimoire.bluffs.splice(index, 1, role);

View File

@ -1,11 +1,22 @@
class LiveSession { class LiveSession {
constructor(store) { constructor(store) {
this.wss = "wss://connect.websocket.in/v3/"; this._wss = "wss://connect.websocket.in/v3/";
this.key = "zXzDomOphNQ94tWXrHfT8E8gkxjUMSXOQt0ypZetKoFsIUiEBegqWNAlExyd"; this._key = "zXzDomOphNQ94tWXrHfT8E8gkxjUMSXOQt0ypZetKoFsIUiEBegqWNAlExyd";
this.socket = null; this._socket = null;
this.isSpectator = true; this._isSpectator = true;
this.gamestate = []; this._gamestate = [];
this.store = store; this._store = store;
this._pingInterval = 30 * 1000; // 30 seconds between pings
this._pingTimer = null;
this._players = {}; // map of players connected to a session
this._playerId = Math.random()
.toString(36)
.substr(2);
// reconnect to previous session
if (this._store.state.session.sessionId) {
this.connect(this._store.state.session.sessionId);
}
} }
/** /**
@ -15,12 +26,14 @@ class LiveSession {
*/ */
_open(channel) { _open(channel) {
this.disconnect(); this.disconnect();
this.socket = new WebSocket(this.wss + channel + "?apiKey=" + this.key); this._socket = new WebSocket(this._wss + channel + "?apiKey=" + this._key);
this.socket.addEventListener("message", this._handleMessage.bind(this)); this._socket.addEventListener("message", this._handleMessage.bind(this));
this.socket.onopen = this._onOpen.bind(this); this._socket.onopen = this._onOpen.bind(this);
this.socket.onclose = () => { this._socket.onclose = () => {
this.socket = null; this._socket = null;
this.store.commit("setSessionId", ""); this._store.commit("setSessionId", "");
clearInterval(this._pingTimer);
this._pingTimer = null;
}; };
} }
@ -31,8 +44,8 @@ class LiveSession {
* @private * @private
*/ */
_send(command, params) { _send(command, params) {
if (this.socket) { if (this._socket && this._socket.readyState === 1) {
this.socket.send(JSON.stringify([command, params])); this._socket.send(JSON.stringify([command, params]));
} }
} }
@ -41,11 +54,22 @@ class LiveSession {
* @private * @private
*/ */
_onOpen() { _onOpen() {
if (this.isSpectator) { if (this._isSpectator) {
this._send("req", "gs"); this._send("req", "gs");
} else { } else {
this.sendGamestate(); this.sendGamestate();
} }
this._ping();
}
/**
* Send a ping message with player ID and ST flag.
* @private
*/
_ping() {
this._send("ping", [this._isSpectator, this._playerId]);
clearTimeout(this._pingTimer);
this._pingTimer = setTimeout(this._ping.bind(this), this._pingInterval);
} }
/** /**
@ -71,6 +95,13 @@ class LiveSession {
break; break;
case "player": case "player":
this._updatePlayer(params); this._updatePlayer(params);
break;
case "ping":
this._handlePing(params);
break;
case "bye":
this._handleBye(params);
break;
} }
} }
@ -79,7 +110,7 @@ class LiveSession {
* @param channel * @param channel
*/ */
connect(channel) { connect(channel) {
this.isSpectator = this.store.state.session.isSpectator; this._isSpectator = this._store.state.session.isSpectator;
this._open(channel); this._open(channel);
} }
@ -87,9 +118,10 @@ class LiveSession {
* Close the current session, if any. * Close the current session, if any.
*/ */
disconnect() { disconnect() {
if (this.socket) { if (this._socket) {
this.socket.close(); this._send("bye", this._playerId);
this.socket = null; this._socket.close();
this._socket = null;
} }
} }
@ -97,8 +129,8 @@ class LiveSession {
* Publish the current gamestate. * Publish the current gamestate.
*/ */
sendGamestate() { sendGamestate() {
if (this.isSpectator) return; if (this._isSpectator) return;
this.gamestate = this.store.state.players.players.map(player => ({ this._gamestate = this._store.state.players.players.map(player => ({
name: player.name, name: player.name,
isDead: player.isDead, isDead: player.isDead,
isVoteless: player.isVoteless, isVoteless: player.isVoteless,
@ -113,8 +145,8 @@ class LiveSession {
: {}) : {})
})); }));
this._send("gs", { this._send("gs", {
gamestate: this.gamestate, gamestate: this._gamestate,
edition: this.store.state.edition edition: this._store.state.edition
}); });
} }
@ -125,16 +157,16 @@ class LiveSession {
* @private * @private
*/ */
_updateGamestate({ gamestate, edition }) { _updateGamestate({ gamestate, edition }) {
this.store.commit("setEdition", edition); this._store.commit("setEdition", edition);
const players = this.store.state.players.players; const players = this._store.state.players.players;
// adjust number of players // adjust number of players
if (players.length < gamestate.length) { if (players.length < gamestate.length) {
for (let x = players.length; x < gamestate.length; x++) { for (let x = players.length; x < gamestate.length; x++) {
this.store.commit("players/add", gamestate[x].name); this._store.commit("players/add", gamestate[x].name);
} }
} else if (players.length > gamestate.length) { } else if (players.length > gamestate.length) {
for (let x = players.length; x > gamestate.length; x--) { for (let x = players.length; x > gamestate.length; x--) {
this.store.commit("players/remove", x - 1); this._store.commit("players/remove", x - 1);
} }
} }
// update status for each player // update status for each player
@ -142,34 +174,34 @@ class LiveSession {
const player = players[x]; const player = players[x];
const { name, isDead, isVoteless, role } = state; const { name, isDead, isVoteless, role } = state;
if (player.name !== name) { if (player.name !== name) {
this.store.commit("players/update", { this._store.commit("players/update", {
player, player,
property: "name", property: "name",
value: name value: name
}); });
} }
if (player.isDead !== isDead) { if (player.isDead !== isDead) {
this.store.commit("players/update", { this._store.commit("players/update", {
player, player,
property: "isDead", property: "isDead",
value: isDead value: isDead
}); });
} }
if (player.isVoteless !== isVoteless) { if (player.isVoteless !== isVoteless) {
this.store.commit("players/update", { this._store.commit("players/update", {
player, player,
property: "isVoteless", property: "isVoteless",
value: isVoteless value: isVoteless
}); });
} }
if (role && player.role.id !== role.id) { if (role && player.role.id !== role.id) {
this.store.commit("players/update", { this._store.commit("players/update", {
player, player,
property: "role", property: "role",
value: role value: role
}); });
} else if (!role && player.role.team === "traveler") { } else if (!role && player.role.team === "traveler") {
this.store.commit("players/update", { this._store.commit("players/update", {
player, player,
property: "role", property: "role",
value: {} value: {}
@ -185,12 +217,12 @@ class LiveSession {
* @param value * @param value
*/ */
sendPlayer({ player, property, value }) { sendPlayer({ player, property, value }) {
if (this.isSpectator || property === "reminders") return; if (this._isSpectator || property === "reminders") return;
const index = this.store.state.players.players.indexOf(player); const index = this._store.state.players.players.indexOf(player);
if (property === "role") { if (property === "role") {
if (value.team && value.team === "traveler") { if (value.team && value.team === "traveler") {
// update local gamestate to remember this player as a traveler // update local gamestate to remember this player as a traveler
this.gamestate[index].role = { this._gamestate[index].role = {
id: player.role.id, id: player.role.id,
team: "traveler", team: "traveler",
name: player.role.name name: player.role.name
@ -198,10 +230,10 @@ class LiveSession {
this._send("player", { this._send("player", {
index, index,
property, property,
value: this.gamestate[index].role value: this._gamestate[index].role
}); });
} else if (this.gamestate[index].role) { } else if (this._gamestate[index].role) {
delete this.gamestate[index].role; delete this._gamestate[index].role;
this._send("player", { index, property, value: {} }); this._send("player", { index, property, value: {} });
} }
} else { } else {
@ -217,7 +249,7 @@ class LiveSession {
* @private * @private
*/ */
_updatePlayer({ index, property, value }) { _updatePlayer({ index, property, value }) {
const player = this.store.state.players.players[index]; const player = this._store.state.players.players[index];
if (!player) return; if (!player) return;
// special case where a player stops being a traveler // special case where a player stops being a traveler
if ( if (
@ -226,16 +258,47 @@ class LiveSession {
player.role.team === "traveler" player.role.team === "traveler"
) { ) {
// reset to an unknown role // reset to an unknown role
this.store.commit("players/update", { this._store.commit("players/update", {
player, player,
property: "role", property: "role",
value: {} value: {}
}); });
} else { } else {
// just update the player otherwise // just update the player otherwise
this.store.commit("players/update", { player, property, value }); this._store.commit("players/update", { player, property, value });
} }
} }
/**
* Handle a ping message by another player / storyteller
* @param isSpectator
* @param playerId
* @private
*/
_handlePing([isSpectator, playerId]) {
const now = new Date().getTime();
this._players[playerId] = now;
// remove players that haven't sent a ping in twice the timespan
for (let player in this._players) {
if (now - this._players[player] > this._pingTimer * 2) {
delete this._players[player];
}
}
this._store.commit("setPlayerCount", Object.keys(this._players).length);
if (!this._isSpectator && !isSpectator) {
alert("Another storyteller joined the session!");
}
}
/**
* Handle a player leaving the sessions
* @param playerId
* @private
*/
_handleBye(playerId) {
delete this._players[playerId];
this._store.commit("setPlayerCount", Object.keys(this._players).length);
}
} }
module.exports = store => { module.exports = store => {