diff --git a/src/components/Menu.vue b/src/components/Menu.vue
index f4cafb4..5f5a915 100644
--- a/src/components/Menu.vue
+++ b/src/components/Menu.vue
@@ -7,7 +7,7 @@
v-if="session.sessionId"
@click="leaveSession"
:title="
- `You're currently in a live game with ${session.playerCount} other players!`
+ `${session.playerCount} other players in this session (${session.ping}ms latency)`
"
>
@@ -91,6 +91,10 @@
Join (Player)[J]
+
+ Latency to {{ session.isSpectator ? "storyteller" : "players" }}
+ {{ session.ping }}ms
+
Copy player link
diff --git a/src/components/Vote.vue b/src/components/Vote.vue
index 2f04ce5..50e3d0f 100644
--- a/src/components/Vote.vue
+++ b/src/components/Vote.vue
@@ -10,8 +10,9 @@
>!
- {{ Math.ceil(alive / 2) }} votes required for a
- majority.
+ {{ voters.length }} vote{{ voters.length != 1 ? 's':'' }}
+ in favor
+ (majority is {{ Math.ceil(alive / 2) }})
{{ Math.ceil(players.length / 2) }} votes required for a
diff --git a/src/store/modules/session.js b/src/store/modules/session.js
index d36545a..05d190a 100644
--- a/src/store/modules/session.js
+++ b/src/store/modules/session.js
@@ -3,6 +3,14 @@ const set = key => (state, val) => {
state[key] = val;
};
+/**
+ * Handle a vote request.
+ * If the vote is from a seat that is already locked, ignore it.
+ * @param state session state
+ * @param index seat of the player in the circle
+ * @param vote true or false
+ * @param indexAdjusted seat of the player counted from the nominated player
+ */
const handleVote = (state, [index, vote]) => {
if (!state.nomination) return;
state.votes = [...state.votes];
@@ -13,6 +21,7 @@ const state = () => ({
sessionId: "",
isSpectator: false,
playerCount: 0,
+ ping: 0,
playerId: "",
claimedSeat: -1,
nomination: false,
@@ -30,6 +39,7 @@ const mutations = {
setPlayerId: set("playerId"),
setSpectator: set("isSpectator"),
setPlayerCount: set("playerCount"),
+ setPing: set("ping"),
setVotingSpeed: set("votingSpeed"),
claimSeat: set("claimedSeat"),
nomination(state, { nomination, votes, votingSpeed, lockedVote } = {}) {
diff --git a/src/store/socket.js b/src/store/socket.js
index 0ef97e8..e785c49 100644
--- a/src/store/socket.js
+++ b/src/store/socket.js
@@ -11,6 +11,7 @@ class LiveSession {
this._pingInterval = 30 * 1000; // 30 seconds between pings
this._pingTimer = null;
this._players = {}; // map of players connected to a session
+ this._pings = {}; // map of player IDs to ping
// reconnect to previous session
if (this._store.state.session.sessionId) {
this.connect(this._store.state.session.sessionId);
@@ -65,7 +66,11 @@ class LiveSession {
* @private
*/
_ping() {
- this._send("ping", [this._isSpectator, this._store.state.session.playerId]);
+ this._send("ping", [
+ this._isSpectator,
+ this._store.state.session.playerId,
+ new Date().getTime()
+ ]);
this._handlePing();
clearTimeout(this._pingTimer);
this._pingTimer = setTimeout(this._ping.bind(this), this._pingInterval);
@@ -149,7 +154,9 @@ class LiveSession {
.substr(2)
);
}
+ this._pings = {};
this._store.commit("session/setPlayerCount", 0);
+ this._store.commit("session/setPing", 0);
this._isSpectator = this._store.state.session.isSpectator;
this._open(channel);
}
@@ -158,7 +165,9 @@ class LiveSession {
* Close the current session, if any.
*/
disconnect() {
+ this._pings = {};
this._store.commit("session/setPlayerCount", 0);
+ this._store.commit("session/setPing", 0);
if (this._socket) {
this._send("bye", this._store.state.session.playerId);
this._socket.close();
@@ -366,20 +375,16 @@ class LiveSession {
* Handle a ping message by another player / storyteller
* @param isSpectator
* @param playerId
+ * @param timestamp
* @private
*/
- _handlePing([isSpectator, playerId] = []) {
+ _handlePing([isSpectator, playerId, timestamp] = []) {
const now = new Date().getTime();
- if (playerId) {
- this._players[playerId] = now;
- if (!this._isSpectator && !isSpectator) {
- alert("Another storyteller joined the session!");
- }
- }
// remove players that haven't sent a ping in twice the timespan
for (let player in this._players) {
if (now - this._players[player] > this._pingInterval * 2) {
delete this._players[player];
+ delete this._pings[player];
}
}
// remove claimed seats from players that are no longer connected
@@ -392,6 +397,24 @@ class LiveSession {
});
}
});
+ // store new player data
+ if (playerId) {
+ this._players[playerId] = now;
+ if (!this._isSpectator && !isSpectator) {
+ alert("Another storyteller joined the session!");
+ } else if (this._isSpectator && !isSpectator) {
+ // ping to ST
+ this._store.commit("session/setPing", now - timestamp);
+ } else {
+ // ping to Players
+ this._pings[playerId] = now - timestamp;
+ const pings = Object.values(this._pings);
+ this._store.commit(
+ "session/setPing",
+ Math.round(pings.reduce((a, b) => a + b, 0) / pings.length)
+ );
+ }
+ }
this._store.commit(
"session/setPlayerCount",
Object.keys(this._players).length
@@ -491,17 +514,28 @@ class LiveSession {
!this._isSpectator
) {
// send vote only if it is your own vote or you are the storyteller
- this._send("vote", [index, this._store.state.session.votes[index]]);
+ this._send("vote", [
+ index,
+ this._store.state.session.votes[index],
+ !this._isSpectator
+ ]);
}
}
/**
- * Handle an incoming vote, but not if it is for your own seat.
+ * Handle an incoming vote, but only if it is from ST or unlocked.
* @param index
* @param vote
+ * @param fromST
*/
- _handleVote([index, vote]) {
- this._store.commit("session/vote", [index, vote]);
+ _handleVote([index, vote, fromST]) {
+ const { session, players } = this._store.state;
+ const playerCount = players.players.length;
+ const indexAdjusted =
+ (index - 1 + playerCount - session.nomination[1]) % playerCount;
+ if (fromST || indexAdjusted >= session.lockedVote - 1) {
+ this._store.commit("session/vote", [index, vote]);
+ }
}
/**