2020-05-09 19:47:00 +00:00
|
|
|
class LiveSession {
|
|
|
|
constructor(store) {
|
2020-05-24 20:13:52 +00:00
|
|
|
this._wss = "wss://connect.websocket.in/v3/";
|
|
|
|
this._key = "zXzDomOphNQ94tWXrHfT8E8gkxjUMSXOQt0ypZetKoFsIUiEBegqWNAlExyd";
|
|
|
|
this._socket = null;
|
|
|
|
this._isSpectator = true;
|
|
|
|
this._gamestate = [];
|
|
|
|
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);
|
|
|
|
}
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open a new session for the passed channel.
|
|
|
|
* @param channel
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_open(channel) {
|
|
|
|
this.disconnect();
|
2020-05-24 20:13:52 +00:00
|
|
|
this._socket = new WebSocket(this._wss + channel + "?apiKey=" + this._key);
|
|
|
|
this._socket.addEventListener("message", this._handleMessage.bind(this));
|
|
|
|
this._socket.onopen = this._onOpen.bind(this);
|
|
|
|
this._socket.onclose = () => {
|
|
|
|
this._socket = null;
|
|
|
|
this._store.commit("setSessionId", "");
|
|
|
|
clearInterval(this._pingTimer);
|
|
|
|
this._pingTimer = null;
|
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() {
|
|
|
|
this._send("ping", [this._isSpectator, this._playerId]);
|
|
|
|
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;
|
|
|
|
case "gs":
|
|
|
|
this._updateGamestate(params);
|
|
|
|
break;
|
|
|
|
case "player":
|
|
|
|
this._updatePlayer(params);
|
2020-05-24 20:13:52 +00:00
|
|
|
break;
|
|
|
|
case "ping":
|
|
|
|
this._handlePing(params);
|
|
|
|
break;
|
|
|
|
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.
|
|
|
|
* @param channel
|
|
|
|
*/
|
|
|
|
connect(channel) {
|
2020-05-24 20:18:43 +00:00
|
|
|
this._store.commit("setPlayerCount", 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-05-24 20:18:43 +00:00
|
|
|
this._store.commit("setPlayerCount", 0);
|
2020-05-24 20:13:52 +00:00
|
|
|
if (this._socket) {
|
|
|
|
this._send("bye", this._playerId);
|
|
|
|
this._socket.close();
|
|
|
|
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,
|
|
|
|
isDead: player.isDead,
|
|
|
|
isVoteless: player.isVoteless,
|
|
|
|
...(player.role && player.role.team === "traveler"
|
|
|
|
? {
|
|
|
|
role: {
|
|
|
|
id: player.role.id,
|
|
|
|
team: "traveler",
|
|
|
|
name: player.role.name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
: {})
|
|
|
|
}));
|
|
|
|
this._send("gs", {
|
2020-05-24 20:13:52 +00:00
|
|
|
gamestate: this._gamestate,
|
|
|
|
edition: this._store.state.edition
|
2020-05-09 19:47:00 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the gamestate based on incoming data.
|
|
|
|
* @param gamestate
|
|
|
|
* @param edition
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_updateGamestate({ gamestate, edition }) {
|
2020-05-24 20:13:52 +00:00
|
|
|
this._store.commit("setEdition", edition);
|
|
|
|
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];
|
|
|
|
const { name, isDead, isVoteless, role } = state;
|
|
|
|
if (player.name !== name) {
|
2020-05-24 20:13:52 +00:00
|
|
|
this._store.commit("players/update", {
|
2020-05-09 19:47:00 +00:00
|
|
|
player,
|
|
|
|
property: "name",
|
|
|
|
value: name
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (player.isDead !== isDead) {
|
2020-05-24 20:13:52 +00:00
|
|
|
this._store.commit("players/update", {
|
2020-05-09 19:47:00 +00:00
|
|
|
player,
|
|
|
|
property: "isDead",
|
|
|
|
value: isDead
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (player.isVoteless !== isVoteless) {
|
2020-05-24 20:13:52 +00:00
|
|
|
this._store.commit("players/update", {
|
2020-05-09 19:47:00 +00:00
|
|
|
player,
|
|
|
|
property: "isVoteless",
|
|
|
|
value: isVoteless
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (role && player.role.id !== role.id) {
|
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
|
|
|
|
});
|
|
|
|
} else if (!role && 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: {}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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-05-24 20:13:52 +00:00
|
|
|
this._gamestate[index].role = {
|
2020-05-09 19:47:00 +00:00
|
|
|
id: player.role.id,
|
|
|
|
team: "traveler",
|
|
|
|
name: player.role.name
|
|
|
|
};
|
|
|
|
this._send("player", {
|
|
|
|
index,
|
|
|
|
property,
|
2020-05-24 20:13:52 +00:00
|
|
|
value: this._gamestate[index].role
|
2020-05-09 19:47:00 +00:00
|
|
|
});
|
2020-05-24 20:13:52 +00:00
|
|
|
} else if (this._gamestate[index].role) {
|
|
|
|
delete this._gamestate[index].role;
|
2020-05-09 19:47:00 +00:00
|
|
|
this._send("player", { index, property, value: {} });
|
|
|
|
}
|
|
|
|
} 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
|
|
|
|
if (
|
|
|
|
property === "role" &&
|
|
|
|
value.team !== "traveler" &&
|
|
|
|
player.role.team === "traveler"
|
|
|
|
) {
|
|
|
|
// reset to an unknown role
|
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: {}
|
|
|
|
});
|
|
|
|
} 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
|
|
|
|
* @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) {
|
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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
}
|
2020-05-09 19:47:00 +00:00
|
|
|
}
|
|
|
|
|
2020-05-08 17:33:29 +00:00
|
|
|
module.exports = store => {
|
|
|
|
// 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) {
|
|
|
|
case "setSessionId":
|
|
|
|
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;
|
|
|
|
case "players/set":
|
2020-05-12 20:22:36 +00:00
|
|
|
case "players/swap":
|
2020-05-19 13:21:38 +00:00
|
|
|
case "players/move":
|
2020-05-09 19:47:00 +00:00
|
|
|
case "players/clear":
|
|
|
|
case "players/remove":
|
|
|
|
case "players/add":
|
|
|
|
case "setEdition":
|
|
|
|
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") {
|
|
|
|
store.commit("setSpectator", true);
|
|
|
|
store.commit("setSessionId", param);
|
|
|
|
}
|
2020-05-08 17:33:29 +00:00
|
|
|
};
|