2020-06-18 08:15:37 +00:00
|
|
|
import rolesJSON from "../roles.json";
|
|
|
|
|
2020-05-09 19:47:00 +00:00
|
|
|
class LiveSession {
|
|
|
|
constructor(store) {
|
2020-09-21 09:19:46 +00:00
|
|
|
//this._wss = "ws://localhost:8081/";
|
2020-11-29 21:17:02 +00:00
|
|
|
this._wss = "wss://live.clocktower.online:8080/";
|
|
|
|
this._wss = "wss://baumgart.biz:8080/"; //todo: delete this
|
2020-05-24 20:13:52 +00:00
|
|
|
this._socket = null;
|
|
|
|
this._isSpectator = true;
|
|
|
|
this._gamestate = [];
|
|
|
|
this._store = store;
|
|
|
|
this._pingInterval = 30 * 1000; // 30 seconds between pings
|
|
|
|
this._pingTimer = null;
|
2020-09-21 09:19:46 +00:00
|
|
|
this._reconnectTimer = null;
|
2020-05-24 20:13:52 +00:00
|
|
|
this._players = {}; // map of players connected to a session
|
2020-09-16 09:39:36 +00:00
|
|
|
this._pings = {}; // map of player IDs to ping
|
2020-05-24 20:13:52 +00:00
|
|
|
// reconnect to previous session
|
|
|
|
if (this._store.state.session.sessionId) {
|
|
|
|
this.connect(this._store.state.session.sessionId);
|
|
|
|
}
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open a new session for the passed channel.
|
|
|
|
* @param channel
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_open(channel) {
|
|
|
|
this.disconnect();
|
2020-09-21 09:19:46 +00:00
|
|
|
this._socket = new WebSocket(
|
|
|
|
this._wss + channel + (this._isSpectator ? "" : "-host")
|
|
|
|
);
|
2020-05-24 20:13:52 +00:00
|
|
|
this._socket.addEventListener("message", this._handleMessage.bind(this));
|
|
|
|
this._socket.onopen = this._onOpen.bind(this);
|
2020-09-21 09:19:46 +00:00
|
|
|
this._socket.onclose = err => {
|
2020-05-24 20:13:52 +00:00
|
|
|
this._socket = null;
|
|
|
|
clearInterval(this._pingTimer);
|
|
|
|
this._pingTimer = null;
|
2020-09-21 09:19:46 +00:00
|
|
|
if (err.code !== 1000) {
|
|
|
|
// connection interrupted, reconnect after 3 seconds
|
|
|
|
this._store.commit("session/setReconnecting", true);
|
|
|
|
this._reconnectTimer = setTimeout(
|
|
|
|
() => this.connect(channel),
|
|
|
|
3 * 1000
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this._store.commit("session/setSessionId", "");
|
|
|
|
if (err.reason) alert(err.reason);
|
|
|
|
}
|
2020-05-09 19:47:00 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send a message through the socket.
|
|
|
|
* @param command
|
|
|
|
* @param params
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_send(command, params) {
|
2020-05-24 20:13:52 +00:00
|
|
|
if (this._socket && this._socket.readyState === 1) {
|
|
|
|
this._socket.send(JSON.stringify([command, params]));
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open event handler for socket.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_onOpen() {
|
2020-05-24 20:13:52 +00:00
|
|
|
if (this._isSpectator) {
|
2020-05-09 19:47:00 +00:00
|
|
|
this._send("req", "gs");
|
|
|
|
} else {
|
|
|
|
this.sendGamestate();
|
|
|
|
}
|
2020-05-24 20:13:52 +00:00
|
|
|
this._ping();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send a ping message with player ID and ST flag.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_ping() {
|
2020-09-16 09:39:36 +00:00
|
|
|
this._send("ping", [
|
|
|
|
this._isSpectator,
|
|
|
|
this._store.state.session.playerId,
|
2020-09-21 09:19:46 +00:00
|
|
|
"latency"
|
2020-09-16 09:39:36 +00:00
|
|
|
]);
|
2020-05-27 20:55:33 +00:00
|
|
|
this._handlePing();
|
2020-05-24 20:13:52 +00:00
|
|
|
clearTimeout(this._pingTimer);
|
|
|
|
this._pingTimer = setTimeout(this._ping.bind(this), this._pingInterval);
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle an incoming socket message.
|
|
|
|
* @param data
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_handleMessage({ data }) {
|
2020-05-09 19:56:51 +00:00
|
|
|
let command, params;
|
|
|
|
try {
|
|
|
|
[command, params] = JSON.parse(data);
|
|
|
|
} catch (err) {
|
|
|
|
console.log("unsupported socket message", data);
|
|
|
|
}
|
2020-05-09 19:47:00 +00:00
|
|
|
switch (command) {
|
|
|
|
case "req":
|
|
|
|
if (params === "gs") {
|
|
|
|
this.sendGamestate();
|
|
|
|
}
|
|
|
|
break;
|
2020-06-18 08:52:07 +00:00
|
|
|
case "edition":
|
|
|
|
this._updateEdition(params);
|
|
|
|
break;
|
2020-08-09 20:02:02 +00:00
|
|
|
case "fabled":
|
|
|
|
this._updateFabled(params);
|
|
|
|
break;
|
2020-05-09 19:47:00 +00:00
|
|
|
case "gs":
|
|
|
|
this._updateGamestate(params);
|
|
|
|
break;
|
|
|
|
case "player":
|
|
|
|
this._updatePlayer(params);
|
2020-05-24 20:13:52 +00:00
|
|
|
break;
|
2020-05-30 22:18:31 +00:00
|
|
|
case "claim":
|
|
|
|
this._updateSeat(params);
|
|
|
|
break;
|
2020-05-24 20:13:52 +00:00
|
|
|
case "ping":
|
|
|
|
this._handlePing(params);
|
|
|
|
break;
|
2020-06-03 20:25:23 +00:00
|
|
|
case "nomination":
|
2020-07-19 19:11:23 +00:00
|
|
|
if (!this._isSpectator) return;
|
2020-06-30 08:16:56 +00:00
|
|
|
this._store.commit("session/nomination", { nomination: params });
|
2020-06-03 20:25:23 +00:00
|
|
|
break;
|
2020-07-19 19:11:23 +00:00
|
|
|
case "swap":
|
|
|
|
if (!this._isSpectator) return;
|
|
|
|
this._store.commit("players/swap", params);
|
|
|
|
break;
|
|
|
|
case "move":
|
|
|
|
if (!this._isSpectator) return;
|
|
|
|
this._store.commit("players/move", params);
|
|
|
|
break;
|
2020-06-18 08:00:32 +00:00
|
|
|
case "votingSpeed":
|
2020-07-19 19:11:23 +00:00
|
|
|
if (!this._isSpectator) return;
|
2020-06-18 08:00:32 +00:00
|
|
|
this._store.commit("session/setVotingSpeed", params);
|
|
|
|
break;
|
2020-06-03 20:25:23 +00:00
|
|
|
case "vote":
|
2020-06-05 20:12:51 +00:00
|
|
|
this._handleVote(params);
|
2020-06-03 20:25:23 +00:00
|
|
|
break;
|
2020-06-03 21:04:50 +00:00
|
|
|
case "lock":
|
2020-06-07 20:22:50 +00:00
|
|
|
this._handleLock(params);
|
2020-06-03 21:04:50 +00:00
|
|
|
break;
|
2020-05-24 20:13:52 +00:00
|
|
|
case "bye":
|
|
|
|
this._handleBye(params);
|
|
|
|
break;
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Connect to a new live session, either as host or spectator.
|
2020-05-27 20:33:51 +00:00
|
|
|
* Set a unique playerId if there isn't one yet.
|
2020-05-09 19:47:00 +00:00
|
|
|
* @param channel
|
|
|
|
*/
|
|
|
|
connect(channel) {
|
2020-05-27 20:33:51 +00:00
|
|
|
if (!this._store.state.session.playerId) {
|
|
|
|
this._store.commit(
|
2020-05-30 20:47:42 +00:00
|
|
|
"session/setPlayerId",
|
2020-05-27 20:33:51 +00:00
|
|
|
Math.random()
|
|
|
|
.toString(36)
|
|
|
|
.substr(2)
|
|
|
|
);
|
|
|
|
}
|
2020-09-16 09:39:36 +00:00
|
|
|
this._pings = {};
|
2020-05-30 20:47:42 +00:00
|
|
|
this._store.commit("session/setPlayerCount", 0);
|
2020-09-16 09:39:36 +00:00
|
|
|
this._store.commit("session/setPing", 0);
|
2020-05-24 20:13:52 +00:00
|
|
|
this._isSpectator = this._store.state.session.isSpectator;
|
2020-05-09 19:47:00 +00:00
|
|
|
this._open(channel);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Close the current session, if any.
|
|
|
|
*/
|
|
|
|
disconnect() {
|
2020-09-16 09:39:36 +00:00
|
|
|
this._pings = {};
|
2020-05-30 20:47:42 +00:00
|
|
|
this._store.commit("session/setPlayerCount", 0);
|
2020-09-16 09:39:36 +00:00
|
|
|
this._store.commit("session/setPing", 0);
|
2020-09-21 09:19:46 +00:00
|
|
|
this._store.commit("session/setReconnecting", false);
|
|
|
|
clearTimeout(this._reconnectTimer);
|
2020-05-24 20:13:52 +00:00
|
|
|
if (this._socket) {
|
2020-05-27 20:33:51 +00:00
|
|
|
this._send("bye", this._store.state.session.playerId);
|
2020-09-21 09:19:46 +00:00
|
|
|
this._socket.close(1000);
|
2020-05-24 20:13:52 +00:00
|
|
|
this._socket = null;
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Publish the current gamestate.
|
|
|
|
*/
|
|
|
|
sendGamestate() {
|
2020-05-24 20:13:52 +00:00
|
|
|
if (this._isSpectator) return;
|
|
|
|
this._gamestate = this._store.state.players.players.map(player => ({
|
2020-05-09 19:47:00 +00:00
|
|
|
name: player.name,
|
2020-05-30 20:47:42 +00:00
|
|
|
id: player.id,
|
2020-05-09 19:47:00 +00:00
|
|
|
isDead: player.isDead,
|
|
|
|
isVoteless: player.isVoteless,
|
|
|
|
...(player.role && player.role.team === "traveler"
|
2020-06-18 08:15:37 +00:00
|
|
|
? { roleId: player.role.id }
|
2020-05-09 19:47:00 +00:00
|
|
|
: {})
|
|
|
|
}));
|
2020-06-30 08:16:56 +00:00
|
|
|
const { session } = this._store.state;
|
2020-12-01 21:42:21 +00:00
|
|
|
const { fabled } = this._store.state.players;
|
2020-06-18 08:52:07 +00:00
|
|
|
this.sendEdition();
|
2020-05-09 19:47:00 +00:00
|
|
|
this._send("gs", {
|
2020-05-24 20:13:52 +00:00
|
|
|
gamestate: this._gamestate,
|
2020-06-30 08:16:56 +00:00
|
|
|
nomination: session.nomination,
|
|
|
|
votingSpeed: session.votingSpeed,
|
2020-07-23 08:39:48 +00:00
|
|
|
lockedVote: session.lockedVote,
|
2020-12-01 21:42:21 +00:00
|
|
|
fabled: fabled.map(({ id }) => id),
|
2020-06-30 08:16:56 +00:00
|
|
|
...(session.nomination ? { votes: session.votes } : {})
|
2020-05-09 19:47:00 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the gamestate based on incoming data.
|
2020-06-18 08:00:32 +00:00
|
|
|
* @param data
|
2020-05-09 19:47:00 +00:00
|
|
|
* @private
|
|
|
|
*/
|
2020-06-18 08:00:32 +00:00
|
|
|
_updateGamestate(data) {
|
2020-05-27 20:33:51 +00:00
|
|
|
if (!this._isSpectator) return;
|
2020-12-01 21:42:21 +00:00
|
|
|
const {
|
|
|
|
gamestate,
|
|
|
|
nomination,
|
|
|
|
votingSpeed,
|
|
|
|
votes,
|
|
|
|
lockedVote,
|
|
|
|
fabled
|
|
|
|
} = data;
|
2020-06-30 08:16:56 +00:00
|
|
|
this._store.commit("session/nomination", {
|
|
|
|
nomination,
|
|
|
|
votes,
|
2020-07-23 08:39:48 +00:00
|
|
|
votingSpeed,
|
|
|
|
lockedVote
|
2020-06-30 08:16:56 +00:00
|
|
|
});
|
2020-12-01 21:42:21 +00:00
|
|
|
this._store.commit("players/setFabled", {
|
|
|
|
fabled: fabled.map(id => this._store.state.fabled.get(id))
|
|
|
|
});
|
2020-05-24 20:13:52 +00:00
|
|
|
const players = this._store.state.players.players;
|
2020-05-09 19:47:00 +00:00
|
|
|
// adjust number of players
|
|
|
|
if (players.length < gamestate.length) {
|
|
|
|
for (let x = players.length; x < gamestate.length; x++) {
|
2020-05-24 20:13:52 +00:00
|
|
|
this._store.commit("players/add", gamestate[x].name);
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
} else if (players.length > gamestate.length) {
|
|
|
|
for (let x = players.length; x > gamestate.length; x--) {
|
2020-05-24 20:13:52 +00:00
|
|
|
this._store.commit("players/remove", x - 1);
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// update status for each player
|
|
|
|
gamestate.forEach((state, x) => {
|
|
|
|
const player = players[x];
|
2020-06-18 08:15:37 +00:00
|
|
|
const { roleId } = state;
|
2020-05-30 20:47:42 +00:00
|
|
|
// update relevant properties
|
|
|
|
["name", "id", "isDead", "isVoteless"].forEach(property => {
|
|
|
|
const value = state[property];
|
|
|
|
if (player[property] !== value) {
|
|
|
|
this._store.commit("players/update", { player, property, value });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
// roles are special, because of travelers
|
2020-06-18 08:15:37 +00:00
|
|
|
if (roleId && player.role.id !== roleId) {
|
|
|
|
const role = rolesJSON.find(r => r.id === roleId);
|
2020-05-24 20:13:52 +00:00
|
|
|
this._store.commit("players/update", {
|
2020-05-09 19:47:00 +00:00
|
|
|
player,
|
|
|
|
property: "role",
|
|
|
|
value: role
|
|
|
|
});
|
2020-06-18 08:15:37 +00:00
|
|
|
} else if (!roleId && player.role.team === "traveler") {
|
2020-05-24 20:13:52 +00:00
|
|
|
this._store.commit("players/update", {
|
2020-05-09 19:47:00 +00:00
|
|
|
player,
|
|
|
|
property: "role",
|
|
|
|
value: {}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-06-18 08:52:07 +00:00
|
|
|
/**
|
|
|
|
* Publish an edition update. ST only
|
|
|
|
*/
|
|
|
|
sendEdition() {
|
|
|
|
if (this._isSpectator) return;
|
|
|
|
const { edition } = this._store.state;
|
|
|
|
let roles;
|
|
|
|
if (edition === "custom") {
|
2020-07-23 10:25:51 +00:00
|
|
|
roles = this._store.getters.customRoles;
|
2020-06-18 08:52:07 +00:00
|
|
|
}
|
|
|
|
this._send("edition", {
|
|
|
|
edition,
|
|
|
|
...(roles ? { roles } : {})
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update edition and roles for custom editions.
|
|
|
|
* @param edition
|
|
|
|
* @param roles
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_updateEdition({ edition, roles }) {
|
|
|
|
if (!this._isSpectator) return;
|
|
|
|
this._store.commit("setEdition", edition);
|
|
|
|
if (roles) {
|
2020-07-23 10:25:51 +00:00
|
|
|
this._store.commit("setCustomRoles", roles);
|
2020-06-18 08:52:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-09 20:02:02 +00:00
|
|
|
/**
|
|
|
|
* Publish a fabled update. ST only
|
|
|
|
*/
|
|
|
|
sendFabled() {
|
|
|
|
if (this._isSpectator) return;
|
2020-11-29 21:17:02 +00:00
|
|
|
const { fabled } = this._store.state.players;
|
2020-08-09 20:02:02 +00:00
|
|
|
this._send(
|
|
|
|
"fabled",
|
|
|
|
fabled.map(({ id }) => id)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update fabled roles.
|
|
|
|
* @param fabled
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_updateFabled(fabled) {
|
|
|
|
if (!this._isSpectator) return;
|
2020-11-29 21:17:02 +00:00
|
|
|
this._store.commit("players/setFabled", {
|
2020-08-09 20:02:02 +00:00
|
|
|
fabled: fabled.map(id => this._store.state.fabled.get(id))
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-05-09 19:47:00 +00:00
|
|
|
/**
|
|
|
|
* Publish a player update.
|
|
|
|
* @param player
|
|
|
|
* @param property
|
|
|
|
* @param value
|
|
|
|
*/
|
|
|
|
sendPlayer({ player, property, value }) {
|
2020-05-24 20:13:52 +00:00
|
|
|
if (this._isSpectator || property === "reminders") return;
|
|
|
|
const index = this._store.state.players.players.indexOf(player);
|
2020-05-09 19:47:00 +00:00
|
|
|
if (property === "role") {
|
|
|
|
if (value.team && value.team === "traveler") {
|
|
|
|
// update local gamestate to remember this player as a traveler
|
2020-06-18 08:15:37 +00:00
|
|
|
this._gamestate[index].roleId = value.id;
|
2020-05-09 19:47:00 +00:00
|
|
|
this._send("player", {
|
|
|
|
index,
|
|
|
|
property,
|
2020-06-18 08:15:37 +00:00
|
|
|
value: value.id
|
2020-05-09 19:47:00 +00:00
|
|
|
});
|
2020-06-18 08:15:37 +00:00
|
|
|
} else if (this._gamestate[index].roleId) {
|
|
|
|
// player was previously a traveler
|
|
|
|
delete this._gamestate[index].roleId;
|
|
|
|
this._send("player", { index, property, value: "" });
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this._send("player", { index, property, value });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update a player based on incoming data.
|
|
|
|
* @param index
|
|
|
|
* @param property
|
|
|
|
* @param value
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_updatePlayer({ index, property, value }) {
|
2020-05-24 20:13:52 +00:00
|
|
|
const player = this._store.state.players.players[index];
|
2020-05-09 19:47:00 +00:00
|
|
|
if (!player) return;
|
|
|
|
// special case where a player stops being a traveler
|
2020-06-18 08:15:37 +00:00
|
|
|
if (property === "role") {
|
|
|
|
if (!value && player.role.team === "traveler") {
|
|
|
|
// reset to an unknown role
|
|
|
|
this._store.commit("players/update", {
|
|
|
|
player,
|
|
|
|
property: "role",
|
|
|
|
value: {}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// load traveler role
|
|
|
|
const role = rolesJSON.find(r => r.id === value);
|
|
|
|
this._store.commit("players/update", {
|
|
|
|
player,
|
|
|
|
property: "role",
|
|
|
|
value: role
|
|
|
|
});
|
|
|
|
}
|
2020-05-09 19:47:00 +00:00
|
|
|
} else {
|
|
|
|
// just update the player otherwise
|
2020-05-24 20:13:52 +00:00
|
|
|
this._store.commit("players/update", { player, property, value });
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-24 20:13:52 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle a ping message by another player / storyteller
|
|
|
|
* @param isSpectator
|
|
|
|
* @param playerId
|
2020-09-16 09:39:36 +00:00
|
|
|
* @param timestamp
|
2020-05-24 20:13:52 +00:00
|
|
|
* @private
|
|
|
|
*/
|
2020-09-21 09:19:46 +00:00
|
|
|
_handlePing([isSpectator, playerId, latency] = []) {
|
2020-05-24 20:13:52 +00:00
|
|
|
const now = new Date().getTime();
|
|
|
|
// remove players that haven't sent a ping in twice the timespan
|
|
|
|
for (let player in this._players) {
|
2020-05-25 17:32:23 +00:00
|
|
|
if (now - this._players[player] > this._pingInterval * 2) {
|
2020-05-24 20:13:52 +00:00
|
|
|
delete this._players[player];
|
2020-09-16 09:39:36 +00:00
|
|
|
delete this._pings[player];
|
2020-05-24 20:13:52 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-31 21:42:08 +00:00
|
|
|
// remove claimed seats from players that are no longer connected
|
|
|
|
this._store.state.players.players.forEach(player => {
|
2020-06-03 20:25:23 +00:00
|
|
|
if (!this._isSpectator && player.id && !this._players[player.id]) {
|
2020-05-31 21:42:08 +00:00
|
|
|
this._store.commit("players/update", {
|
|
|
|
player,
|
|
|
|
property: "id",
|
|
|
|
value: ""
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2020-09-16 09:39:36 +00:00
|
|
|
// store new player data
|
|
|
|
if (playerId) {
|
|
|
|
this._players[playerId] = now;
|
2020-09-22 07:10:35 +00:00
|
|
|
const ping = parseInt(latency, 10);
|
|
|
|
if (ping && ping > 0 && ping < 30 * 1000) {
|
|
|
|
if (this._isSpectator && !isSpectator) {
|
|
|
|
// ping to ST
|
|
|
|
this._store.commit("session/setPing", ping);
|
|
|
|
} else if (!this._isSpectator) {
|
|
|
|
// ping to Players
|
|
|
|
this._pings[playerId] = ping;
|
|
|
|
const pings = Object.values(this._pings);
|
|
|
|
this._store.commit(
|
|
|
|
"session/setPing",
|
|
|
|
Math.round(pings.reduce((a, b) => a + b, 0) / pings.length)
|
|
|
|
);
|
2020-09-21 09:19:46 +00:00
|
|
|
}
|
2020-09-16 09:39:36 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-30 20:47:42 +00:00
|
|
|
this._store.commit(
|
|
|
|
"session/setPlayerCount",
|
|
|
|
Object.keys(this._players).length
|
|
|
|
);
|
2020-05-24 20:13:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle a player leaving the sessions
|
|
|
|
* @param playerId
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_handleBye(playerId) {
|
|
|
|
delete this._players[playerId];
|
2020-05-30 20:47:42 +00:00
|
|
|
this._store.commit(
|
|
|
|
"session/setPlayerCount",
|
|
|
|
Object.keys(this._players).length
|
|
|
|
);
|
2020-05-24 20:13:52 +00:00
|
|
|
}
|
2020-05-30 22:18:31 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Claim a seat, needs to be confirmed by the Storyteller.
|
|
|
|
* @param seat either -1 or the index of the seat claimed
|
|
|
|
*/
|
|
|
|
claimSeat(seat) {
|
|
|
|
if (!this._isSpectator) return;
|
|
|
|
if (this._store.state.players.players.length > seat) {
|
|
|
|
this._send("claim", [seat, this._store.state.session.playerId]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update a player id associated with that seat.
|
|
|
|
* @param index seat index or -1
|
|
|
|
* @param value playerId to add / remove
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_updateSeat([index, value]) {
|
2020-06-03 20:25:23 +00:00
|
|
|
if (this._isSpectator) return;
|
2020-05-30 22:18:31 +00:00
|
|
|
const property = "id";
|
2020-06-03 20:25:23 +00:00
|
|
|
const players = this._store.state.players.players;
|
2020-05-30 22:18:31 +00:00
|
|
|
// remove previous seat
|
2020-06-03 20:25:23 +00:00
|
|
|
const oldIndex = players.findIndex(({ id }) => id === value);
|
|
|
|
if (oldIndex >= 0 && oldIndex !== index) {
|
|
|
|
this._store.commit("players/update", {
|
|
|
|
player: players[oldIndex],
|
|
|
|
property,
|
|
|
|
value: ""
|
|
|
|
});
|
2020-05-30 22:18:31 +00:00
|
|
|
}
|
|
|
|
// add playerId to new seat
|
|
|
|
if (index >= 0) {
|
2020-06-03 20:25:23 +00:00
|
|
|
const player = players[index];
|
2020-05-30 22:18:31 +00:00
|
|
|
if (!player) return;
|
|
|
|
this._store.commit("players/update", { player, property, value });
|
|
|
|
}
|
2020-06-03 20:25:23 +00:00
|
|
|
// update player session list as if this was a ping
|
|
|
|
this._handlePing([true, value]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-03 21:04:50 +00:00
|
|
|
* A player nomination. ST only
|
2020-06-18 08:00:32 +00:00
|
|
|
* This also syncs the voting speed to the players.
|
2020-06-03 20:25:23 +00:00
|
|
|
* @param nomination [nominator, nominee]
|
|
|
|
*/
|
2020-06-30 08:16:56 +00:00
|
|
|
nomination({ nomination } = {}) {
|
2020-06-03 20:25:23 +00:00
|
|
|
if (this._isSpectator) return;
|
|
|
|
const players = this._store.state.players.players;
|
|
|
|
if (
|
|
|
|
!nomination ||
|
|
|
|
(players.length > nomination[0] && players.length > nomination[1])
|
|
|
|
) {
|
2020-06-18 08:00:32 +00:00
|
|
|
this.setVotingSpeed(this._store.state.session.votingSpeed);
|
2020-06-03 20:25:23 +00:00
|
|
|
this._send("nomination", nomination);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-18 08:00:32 +00:00
|
|
|
/**
|
|
|
|
* Send the voting speed. ST only
|
|
|
|
* @param votingSpeed voting speed in seconds, minimum 1
|
|
|
|
*/
|
|
|
|
setVotingSpeed(votingSpeed) {
|
|
|
|
if (this._isSpectator) return;
|
|
|
|
if (votingSpeed) {
|
|
|
|
this._send("votingSpeed", votingSpeed);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-03 20:25:23 +00:00
|
|
|
/**
|
2020-06-16 08:58:36 +00:00
|
|
|
* Send a vote. Player or ST
|
2020-06-05 20:12:51 +00:00
|
|
|
* @param index Seat of the player
|
2020-06-16 08:58:36 +00:00
|
|
|
* @param sync Flag whether to sync this vote with others or not
|
2020-06-03 20:25:23 +00:00
|
|
|
*/
|
|
|
|
vote([index]) {
|
2020-06-05 20:12:51 +00:00
|
|
|
const player = this._store.state.players.players[index];
|
2020-06-16 08:58:36 +00:00
|
|
|
if (
|
|
|
|
this._store.state.session.playerId === player.id ||
|
|
|
|
!this._isSpectator
|
|
|
|
) {
|
|
|
|
// send vote only if it is your own vote or you are the storyteller
|
2020-09-16 09:39:36 +00:00
|
|
|
this._send("vote", [
|
|
|
|
index,
|
|
|
|
this._store.state.session.votes[index],
|
|
|
|
!this._isSpectator
|
|
|
|
]);
|
2020-06-05 20:12:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-16 09:39:36 +00:00
|
|
|
* Handle an incoming vote, but only if it is from ST or unlocked.
|
2020-06-16 08:58:36 +00:00
|
|
|
* @param index
|
2020-06-05 20:12:51 +00:00
|
|
|
* @param vote
|
2020-09-16 09:39:36 +00:00
|
|
|
* @param fromST
|
2020-06-05 20:12:51 +00:00
|
|
|
*/
|
2020-09-16 09:39:36 +00:00
|
|
|
_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]);
|
|
|
|
}
|
2020-05-30 22:18:31 +00:00
|
|
|
}
|
2020-06-03 21:04:50 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Lock a vote. ST only
|
|
|
|
*/
|
|
|
|
lockVote() {
|
|
|
|
if (this._isSpectator) return;
|
2020-06-07 20:22:50 +00:00
|
|
|
const { lockedVote, votes, nomination } = this._store.state.session;
|
|
|
|
const { players } = this._store.state.players;
|
|
|
|
const index = (nomination[1] + lockedVote - 1) % players.length;
|
|
|
|
this._send("lock", [this._store.state.session.lockedVote, votes[index]]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update vote lock and the locked vote, if it differs.
|
|
|
|
* @param lock
|
|
|
|
* @param vote
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_handleLock([lock, vote]) {
|
|
|
|
this._store.commit("session/lockVote", lock);
|
|
|
|
if (lock > 1) {
|
|
|
|
const { lockedVote, nomination } = this._store.state.session;
|
|
|
|
const { players } = this._store.state.players;
|
|
|
|
const index = (nomination[1] + lockedVote - 1) % players.length;
|
|
|
|
if (this._store.state.session.votes[index] !== vote) {
|
|
|
|
this._store.commit("session/vote", [index, vote]);
|
|
|
|
}
|
|
|
|
}
|
2020-06-03 21:04:50 +00:00
|
|
|
}
|
2020-07-19 19:11:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Swap two player seats. ST only
|
|
|
|
* @param payload
|
|
|
|
*/
|
|
|
|
swapPlayer(payload) {
|
|
|
|
if (this._isSpectator) return;
|
|
|
|
this._send("swap", payload);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move a player to another seat. ST only
|
|
|
|
* @param payload
|
|
|
|
*/
|
|
|
|
movePlayer(payload) {
|
|
|
|
if (this._isSpectator) return;
|
|
|
|
this._send("move", payload);
|
|
|
|
}
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
|
2020-06-18 08:15:37 +00:00
|
|
|
export default store => {
|
2020-05-08 17:33:29 +00:00
|
|
|
// setup
|
2020-05-09 19:47:00 +00:00
|
|
|
const session = new LiveSession(store);
|
2020-05-08 17:33:29 +00:00
|
|
|
|
|
|
|
// listen to mutations
|
2020-05-09 19:47:00 +00:00
|
|
|
store.subscribe(({ type, payload }) => {
|
|
|
|
switch (type) {
|
2020-05-30 20:47:42 +00:00
|
|
|
case "session/setSessionId":
|
2020-05-09 19:47:00 +00:00
|
|
|
if (payload) {
|
|
|
|
session.connect(payload);
|
|
|
|
} else {
|
2020-05-12 18:48:00 +00:00
|
|
|
window.location.hash = "";
|
2020-05-09 19:47:00 +00:00
|
|
|
session.disconnect();
|
|
|
|
}
|
|
|
|
break;
|
2020-05-30 22:18:31 +00:00
|
|
|
case "session/claimSeat":
|
|
|
|
session.claimSeat(payload);
|
|
|
|
break;
|
2020-06-03 20:25:23 +00:00
|
|
|
case "session/nomination":
|
|
|
|
session.nomination(payload);
|
|
|
|
break;
|
2020-06-16 08:58:36 +00:00
|
|
|
case "session/voteSync":
|
2020-06-03 20:25:23 +00:00
|
|
|
session.vote(payload);
|
|
|
|
break;
|
2020-06-03 21:04:50 +00:00
|
|
|
case "session/lockVote":
|
|
|
|
session.lockVote();
|
|
|
|
break;
|
2020-06-18 08:00:32 +00:00
|
|
|
case "session/setVotingSpeed":
|
|
|
|
session.setVotingSpeed(payload);
|
|
|
|
break;
|
2020-06-18 08:52:07 +00:00
|
|
|
case "setEdition":
|
|
|
|
session.sendEdition();
|
|
|
|
break;
|
2020-08-09 20:02:02 +00:00
|
|
|
case "setFabled":
|
|
|
|
session.sendFabled();
|
|
|
|
break;
|
2020-05-12 20:22:36 +00:00
|
|
|
case "players/swap":
|
2020-07-19 19:11:23 +00:00
|
|
|
session.swapPlayer(payload);
|
|
|
|
break;
|
2020-05-19 13:21:38 +00:00
|
|
|
case "players/move":
|
2020-07-19 19:11:23 +00:00
|
|
|
session.movePlayer(payload);
|
|
|
|
break;
|
|
|
|
case "players/set":
|
2020-05-09 19:47:00 +00:00
|
|
|
case "players/clear":
|
|
|
|
case "players/remove":
|
|
|
|
case "players/add":
|
|
|
|
session.sendGamestate();
|
|
|
|
break;
|
|
|
|
case "players/update":
|
|
|
|
session.sendPlayer(payload);
|
|
|
|
break;
|
|
|
|
}
|
2020-05-08 17:33:29 +00:00
|
|
|
});
|
2020-05-12 18:48:00 +00:00
|
|
|
|
|
|
|
// check for session Id in hash
|
|
|
|
const [command, param] = window.location.hash.substr(1).split("/");
|
|
|
|
if (command === "play") {
|
2020-05-30 20:47:42 +00:00
|
|
|
store.commit("session/setSpectator", true);
|
|
|
|
store.commit("session/setSessionId", param);
|
2020-05-12 18:48:00 +00:00
|
|
|
}
|
2020-05-08 17:33:29 +00:00
|
|
|
};
|