From da3cd967230de0900e2dca06ddcf5cf809671a02 Mon Sep 17 00:00:00 2001 From: Steffen Date: Sat, 9 May 2020 21:47:00 +0200 Subject: [PATCH] added live sessions for townsquare sync --- src/App.vue | 2 + src/components/Menu.vue | 75 ++++---- src/components/Player.vue | 32 ++-- src/components/TownInfo.vue | 4 +- src/components/TownSquare.vue | 8 +- src/components/modals/RoleModal.vue | 3 +- src/components/modals/RolesModal.vue | 4 +- src/main.js | 6 +- src/store/index.js | 6 +- src/store/modules/players.js | 4 +- src/store/session.js | 258 ++++++++++++++++++++++++++- 11 files changed, 342 insertions(+), 60 deletions(-) diff --git a/src/App.vue b/src/App.vue index 41baadf..00f57d7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -57,9 +57,11 @@ export default { this.$refs.menu.randomizeSeatings(); break; case "e": + if (this.grimoire.isSpectator) return; this.$store.commit("toggleModal", "edition"); break; case "c": + if (this.grimoire.isSpectator) return; this.$store.commit("toggleModal", "roles"); break; case "Escape": diff --git a/src/components/Menu.vue b/src/components/Menu.vue index 24c4794..af22e0c 100644 --- a/src/components/Menu.vue +++ b/src/components/Menu.vue @@ -43,40 +43,46 @@
  • Join Live Session
  • +
  • + + {{ grimoire.isSpectator ? "Spectating" : "Hosting" }} +
  • - {{ grimoire.sessionId.substr(2) }} + {{ grimoire.sessionId }} Leave Session
  • - -
  • - - Players -
  • -
  • [A] Add
  • -
  • - [R] Randomize -
  • -
  • - Remove all -
  • + @@ -113,7 +119,8 @@ export default { .substring(2, 7) ); if (sessionId) { - this.$store.commit("setSessionId", "h:" + sessionId.substr(0, 5)); + this.$store.commit("setSpectator", false); + this.$store.commit("setSessionId", sessionId.substr(0, 5)); } }, joinSession() { @@ -121,29 +128,35 @@ export default { "Enter the code of the session you want to join" ); if (sessionId) { - this.$store.commit("setSessionId", "j:" + sessionId.substr(0, 5)); + this.$store.commit("setSpectator", true); + this.$store.commit("setSessionId", sessionId.substr(0, 5)); } }, leaveSession() { + this.$store.commit("setSpectator", false); this.$store.commit("setSessionId", ""); }, addPlayer() { + if (this.grimoire.isSpectator) return; const name = prompt("Player name"); if (name) { this.$store.commit("players/add", name); } }, randomizeSeatings() { + if (this.grimoire.isSpectator) return; if (confirm("Are you sure you want to randomize seatings?")) { this.$store.dispatch("players/randomize"); } }, clearPlayers() { + if (this.grimoire.isSpectator) return; if (confirm("Are you sure you want to remove all players?")) { this.$store.commit("players/clear"); } }, clearRoles() { + if (this.grimoire.isSpectator) return; this.$store.commit("showGrimoire"); if (confirm("Are you sure you want to remove all player roles?")) { this.$store.dispatch("players/clearRoles"); diff --git a/src/components/Player.vue b/src/components/Player.vue index fc2b60d..31c2e0f 100644 --- a/src/components/Player.vue +++ b/src/components/Player.vue @@ -4,8 +4,8 @@ ref="player" class="player" :class="{ - dead: player.hasDied, - 'no-vote': player.hasVoted, + dead: player.isDead, + 'no-vote': player.isVoteless, traveler: player.role && player.role.team === 'traveler' }" > @@ -36,8 +36,8 @@ @@ -109,20 +109,23 @@ export default { }, toggleStatus() { if (this.grimoire.isPublic) { - if (!this.player.hasDied) { - this.updatePlayer("hasDied", true); - } else if (this.player.hasVoted) { - this.updatePlayer("hasVoted", false); - this.updatePlayer("hasDied", false); + if (!this.player.isDead) { + this.updatePlayer("isDead", true); + } else if (this.player.isVoteless) { + this.updatePlayer("isVoteless", false); + this.updatePlayer("isDead", false); } else { - this.updatePlayer("hasVoted", true); + this.updatePlayer("isVoteless", true); } } else { - this.updatePlayer("hasDied", !this.player.hasDied); - this.updatePlayer("hasVoted", false); + this.updatePlayer("isDead", !this.player.isDead); + if (this.player.isVoteless) { + this.updatePlayer("isVoteless", false); + } } }, changeName() { + if (this.grimoire.isSpectator) return; const name = prompt("Player name", this.player.name) || this.player.name; this.updatePlayer("name", name); }, @@ -132,6 +135,7 @@ export default { this.updatePlayer("reminders", reminders); }, updatePlayer(property, value) { + if (this.grimoire.isSpectator && property !== "reminders") return; this.$store.commit("players/update", { player: this.player, property, @@ -181,7 +185,7 @@ export default { pointer-events: none; } - &:hover:before { + #townsquare:not(.spectator) &:hover:before { opacity: 0.5; top: -10px; transform: scale(1); @@ -332,7 +336,7 @@ export default { text-overflow: ellipsis; overflow: hidden; } - &:hover { + #townsquare:not(.spectator) &:hover { color: red; span { display: block; diff --git a/src/components/TownInfo.vue b/src/components/TownInfo.vue index 7eb9229..c2bf7ae 100644 --- a/src/components/TownInfo.vue +++ b/src/components/TownInfo.vue @@ -47,7 +47,7 @@ export default { teams: function() { const { players } = this.$store.state.players; const nonTravelers = this.$store.getters["players/nonTravelers"]; - const alive = players.filter(player => player.hasDied !== true).length; + const alive = players.filter(player => player.isDead !== true).length; return { ...gameJSON[nonTravelers - 5], traveler: players.length - nonTravelers, @@ -55,7 +55,7 @@ export default { votes: alive + players.filter( - player => player.hasDied === true && player.hasVoted !== true + player => player.isDead === true && player.isVoteless !== true ).length }; }, diff --git a/src/components/TownSquare.vue b/src/components/TownSquare.vue index 1ec2bcf..f596c91 100644 --- a/src/components/TownSquare.vue +++ b/src/components/TownSquare.vue @@ -2,7 +2,10 @@
      @@ -70,10 +73,13 @@ export default { this.$store.commit("toggleModal", "reminder"); }, openRoleModal(playerIndex) { + const player = this.players[playerIndex]; + if (this.grimoire.isSpectator && player.role.team === "traveler") return; this.selectedPlayer = playerIndex; this.$store.commit("toggleModal", "role"); }, removePlayer(playerIndex) { + if (this.grimoire.isSpectator) return; if ( confirm( `Do you really want to remove ${this.players[playerIndex].name}?` diff --git a/src/components/modals/RoleModal.vue b/src/components/modals/RoleModal.vue index 1ecffda..8a5bedc 100644 --- a/src/components/modals/RoleModal.vue +++ b/src/components/modals/RoleModal.vue @@ -74,8 +74,7 @@ export default { ul.tokens li { border-radius: 50%; - height: 120px; - width: 120px; + width: 6vw; margin: 5px; transition: transform 500ms ease; diff --git a/src/components/modals/RolesModal.vue b/src/components/modals/RolesModal.vue index 92ce720..be36b5f 100644 --- a/src/components/modals/RolesModal.vue +++ b/src/components/modals/RolesModal.vue @@ -149,8 +149,7 @@ ul.tokens { padding-left: 55px; li { border-radius: 50%; - height: 120px; - width: 120px; + width: 6vw; margin: 5px; opacity: 0.5; transition: all 250ms; @@ -181,7 +180,6 @@ ul.tokens { opacity: 1; position: absolute; left: 0; - top: 40px; font-weight: bold; line-height: 50px; text-align: center; diff --git a/src/main.js b/src/main.js index dba9a27..4ca497b 100644 --- a/src/main.js +++ b/src/main.js @@ -18,7 +18,8 @@ import { faCheckSquare, faSquare, faRandom, - faPeopleArrows + faPeopleArrows, + faBroadcastTower } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; @@ -38,7 +39,8 @@ library.add( faCheckSquare, faSquare, faRandom, - faPeopleArrows + faPeopleArrows, + faBroadcastTower ); Vue.component("font-awesome-icon", FontAwesomeIcon); diff --git a/src/store/index.js b/src/store/index.js index 7a6b23e..36891ab 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -35,7 +35,8 @@ export default new Vuex.Store({ zoom: 1, background: "", bluffs: [], - sessionId: "" + sessionId: "", + isSpectator: false }, modals: { edition: false, @@ -72,6 +73,9 @@ export default new Vuex.Store({ setSessionId({ grimoire }, sessionId) { grimoire.sessionId = sessionId; }, + setSpectator({ grimoire }, spectator) { + grimoire.isSpectator = spectator; + }, setBluff({ grimoire }, { index, role }) { grimoire.bluffs.splice(index, 1, role); }, diff --git a/src/store/modules/players.js b/src/store/modules/players.js index 8b28fb6..59c326d 100644 --- a/src/store/modules/players.js +++ b/src/store/modules/players.js @@ -1,8 +1,8 @@ const NEWPLAYER = { role: {}, reminders: [], - hasVoted: false, - hasDied: false + isVoteless: false, + isDead: false }; const state = () => ({ diff --git a/src/store/session.js b/src/store/session.js index 0077564..d0021e2 100644 --- a/src/store/session.js +++ b/src/store/session.js @@ -1,8 +1,262 @@ +class LiveSession { + constructor(store) { + this.wss = "wss://connect.websocket.in/v3/"; + this.key = "zXzDomOphNQ94tWXrHfT8E8gkxjUMSXOQt0ypZetKoFsIUiEBegqWNAlExyd"; + this.socket = null; + this.isSpectator = true; + this.gamestate = []; + this.store = store; + } + + /** + * Open a new session for the passed channel. + * @param channel + * @private + */ + _open(channel) { + this.disconnect(); + 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", ""); + }; + } + + /** + * Send a message through the socket. + * @param command + * @param params + * @private + */ + _send(command, params) { + if (this.socket) { + this.socket.send(JSON.stringify([command, params])); + } + } + + /** + * Open event handler for socket. + * @private + */ + _onOpen() { + if (this.isSpectator) { + this._send("req", "gs"); + } else { + this.sendGamestate(); + } + } + + /** + * Handle an incoming socket message. + * @param data + * @private + */ + _handleMessage({ data }) { + const [command, params] = JSON.parse(data); + switch (command) { + case "req": + if (params === "gs") { + this.sendGamestate(); + } + break; + case "gs": + this._updateGamestate(params); + break; + case "player": + this._updatePlayer(params); + } + } + + /** + * Connect to a new live session, either as host or spectator. + * @param channel + */ + connect(channel) { + this.isSpectator = this.store.state.grimoire.isSpectator; + this._open(channel); + } + + /** + * Close the current session, if any. + */ + disconnect() { + if (this.socket) { + this.socket.close(); + this.socket = null; + } + } + + /** + * Publish the current gamestate. + */ + sendGamestate() { + if (this.isSpectator) return; + this.gamestate = this.store.state.players.players.map(player => ({ + 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", { + gamestate: this.gamestate, + edition: this.store.state.edition + }); + } + + /** + * Update the gamestate based on incoming data. + * @param gamestate + * @param edition + * @private + */ + _updateGamestate({ gamestate, edition }) { + this.store.commit("setEdition", edition); + const players = this.store.state.players.players; + // adjust number of players + if (players.length < gamestate.length) { + for (let x = players.length; x < gamestate.length; x++) { + this.store.commit("players/add", gamestate[x].name); + } + } else if (players.length > gamestate.length) { + for (let x = players.length; x > gamestate.length; x--) { + this.store.commit("players/remove", x - 1); + } + } + // update status for each player + gamestate.forEach((state, x) => { + const player = players[x]; + const { name, isDead, isVoteless, role } = state; + if (player.name !== name) { + this.store.commit("players/update", { + player, + property: "name", + value: name + }); + } + if (player.isDead !== isDead) { + this.store.commit("players/update", { + player, + property: "isDead", + value: isDead + }); + } + if (player.isVoteless !== isVoteless) { + this.store.commit("players/update", { + player, + property: "isVoteless", + value: isVoteless + }); + } + if (role && player.role.id !== role.id) { + this.store.commit("players/update", { + player, + property: "role", + value: role + }); + } else if (!role && player.role.team === "traveler") { + this.store.commit("players/update", { + player, + property: "role", + value: {} + }); + } + }); + } + + /** + * Publish a player update. + * @param player + * @param property + * @param value + */ + sendPlayer({ player, property, value }) { + if (this.isSpectator || property === "reminders") return; + const index = this.store.state.players.players.indexOf(player); + if (property === "role") { + if (value.team && value.team === "traveler") { + // update local gamestate to remember this player as a traveler + this.gamestate[index].role = { + id: player.role.id, + team: "traveler", + name: player.role.name + }; + this._send("player", { + index, + property, + value: this.gamestate[index].role + }); + } else if (this.gamestate[index].role) { + delete this.gamestate[index].role; + 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 }) { + const player = this.store.state.players.players[index]; + 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 + this.store.commit("players/update", { + player, + property: "role", + value: {} + }); + } else { + // just update the player otherwise + this.store.commit("players/update", { player, property, value }); + } + } +} + module.exports = store => { // setup + const session = new LiveSession(store); // listen to mutations - store.subscribe(({ type, payload }, state) => { - console.log(type, payload, state); + store.subscribe(({ type, payload }) => { + switch (type) { + case "setSessionId": + if (payload) { + session.connect(payload); + } else { + session.disconnect(); + } + break; + case "players/set": + case "players/clear": + case "players/remove": + case "players/add": + case "setEdition": + session.sendGamestate(); + break; + case "players/update": + session.sendPlayer(payload); + break; + } }); };