Merge pull request #10 from bra1n/voting

Added live play session, player swapping and more
This commit is contained in:
Steffen 2020-05-12 22:27:08 +02:00 committed by GitHub
commit 78a32d3581
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 667 additions and 117 deletions

View File

@ -37,10 +37,10 @@ export default {
EditionModal,
RolesModal
},
computed: mapState({
grimoire: state => state.grimoire,
players: state => state.players.players
}),
computed: {
...mapState(["grimoire", "session"]),
...mapState("players", ["players"])
},
methods: {
takeScreenshot(dimensions) {
this.$refs.menu.takeScreenshot(dimensions);
@ -57,9 +57,11 @@ export default {
this.$refs.menu.randomizeSeatings();
break;
case "e":
if (this.session.isSpectator) return;
this.$store.commit("toggleModal", "edition");
break;
case "c":
if (this.session.isSpectator) return;
this.$store.commit("toggleModal", "roles");
break;
case "Escape":

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -1,9 +1,17 @@
<template>
<div id="controls">
<Screenshot ref="screenshot"></Screenshot>
<font-awesome-icon
@click="leaveSession"
icon="broadcast-tower"
v-if="session.sessionId"
v-bind:class="{ spectator: session.isSpectator }"
title="You're currently in a live game!"
/>
<font-awesome-icon
icon="camera"
@click="takeScreenshot()"
title="Take a screenshot"
v-bind:class="{ success: grimoire.isScreenshotSuccess }"
/>
<div class="menu" v-bind:class="{ open: grimoire.isMenuOpen }">
@ -37,36 +45,58 @@
<li @click="setBackground">
Background image
</li>
<!-- Users -->
<li class="headline">
<font-awesome-icon icon="users" />
Players
<li @click="hostSession" v-if="!session.sessionId">
Host Live Session
</li>
<li @click="addPlayer" v-if="players.length < 20"><em>[A]</em> Add</li>
<li @click="randomizeSeatings" v-if="players.length > 2">
<em>[R]</em> Randomize
<li @click="joinSession" v-if="!session.sessionId">
Join Live Session
</li>
<li @click="clearPlayers" v-if="players.length">
Remove all
<li class="headline" v-if="session.sessionId">
<font-awesome-icon icon="broadcast-tower" />
{{ session.isSpectator ? "Playing" : "Hosting" }}
</li>
<li v-if="session.sessionId" @click="copySessionUrl">
<em><font-awesome-icon icon="copy"/></em>
Copy player link
</li>
<li @click="leaveSession" v-if="session.sessionId">
<em>{{ session.sessionId }}</em>
Leave Session
</li>
<!-- Characters -->
<li class="headline">
<font-awesome-icon icon="theater-masks" />
Characters
</li>
<li @click="toggleModal('edition')">
<em>[E]</em>
Select Edition
</li>
<li @click="toggleModal('roles')" v-if="players.length > 4">
<em>[C]</em>
Choose & Assign
</li>
<li @click="clearRoles" v-if="players.length">
Remove all
</li>
<template v-if="!session.isSpectator">
<!-- Users -->
<li class="headline">
<font-awesome-icon icon="users" />
Players
</li>
<li @click="addPlayer" v-if="players.length < 20">
<em>[A]</em> Add
</li>
<li @click="randomizeSeatings" v-if="players.length > 2">
<em>[R]</em> Randomize
</li>
<li @click="clearPlayers" v-if="players.length">
Remove all
</li>
<!-- Characters -->
<li class="headline">
<font-awesome-icon icon="theater-masks" />
Characters
</li>
<li @click="toggleModal('edition')">
<em>[E]</em>
Select Edition
</li>
<li @click="toggleModal('roles')" v-if="players.length > 4">
<em>[C]</em>
Choose & Assign
</li>
<li @click="clearRoles" v-if="players.length">
Remove all
</li>
</template>
</ul>
</div>
</div>
@ -80,10 +110,10 @@ export default {
components: {
Screenshot
},
computed: mapState({
grimoire: state => state.grimoire,
players: state => state.players.players
}),
computed: {
...mapState(["grimoire", "session"]),
...mapState("players", ["players"])
},
methods: {
takeScreenshot(dimensions = {}) {
this.$store.commit("updateScreenshot");
@ -95,26 +125,75 @@ export default {
prompt("Enter custom background URL")
);
},
hostSession() {
const sessionId = prompt(
"Enter a channel number for your session",
Math.round(Math.random() * 10000)
);
if (sessionId) {
this.$store.commit("setSpectator", false);
this.$store.commit(
"setSessionId",
sessionId.replace(/[^0-9]/g, "").substr(0, 5)
);
this.copySessionUrl();
}
},
copySessionUrl() {
// check for clipboard permissions
navigator.permissions
.query({ name: "clipboard-write" })
.then(({ state }) => {
if (state === "granted" || state === "prompt") {
const url = window.location.href.split("#")[0];
const link = url + "#play/" + this.session.sessionId;
navigator.clipboard.writeText(link);
}
});
},
joinSession() {
const sessionId = prompt(
"Enter the channel number of the session you want to join"
);
if (sessionId) {
this.$store.commit("setSpectator", true);
this.$store.commit(
"setSessionId",
sessionId.replace(/[^0-9]/g, "").substr(0, 5)
);
}
},
leaveSession() {
if (confirm("Are you sure you want to leave the active live game?")) {
this.$store.commit("setSpectator", false);
this.$store.commit("setSessionId", "");
}
},
addPlayer() {
if (this.session.isSpectator) return;
const name = prompt("Player name");
if (name) {
this.$store.commit("players/add", name);
}
},
randomizeSeatings() {
if (this.session.isSpectator) return;
if (confirm("Are you sure you want to randomize seatings?")) {
this.$store.dispatch("players/randomize");
}
},
clearPlayers() {
if (this.session.isSpectator) return;
if (confirm("Are you sure you want to remove all players?")) {
this.$store.commit("players/clear");
this.$store.commit("setBluff");
}
},
clearRoles() {
this.$store.commit("showGrimoire");
if (this.session.isSpectator) return;
if (confirm("Are you sure you want to remove all player roles?")) {
this.$store.dispatch("players/clearRoles");
this.$store.commit("setBluff");
}
},
...mapMutations([
@ -148,13 +227,13 @@ export default {
right: 3px;
top: 3px;
text-align: right;
padding-right: 50px;
#app.screenshot & {
display: none;
}
svg {
cursor: pointer;
filter: drop-shadow(0 0 5px rgba(0, 0, 0, 1));
&.success {
animation: greenToWhite 1s normal forwards;
@ -162,11 +241,18 @@ export default {
}
}
.fa-camera {
position: absolute;
right: 50px;
top: 10px;
> svg {
cursor: pointer;
z-index: 5;
margin-top: 10px;
margin-left: 10px;
}
> .fa-broadcast-tower {
color: $demon;
&.spectator {
color: $townsfolk;
}
}
}
@ -175,12 +261,16 @@ export default {
transform-origin: 190px 22px;
transition: transform 500ms cubic-bezier(0.68, -0.55, 0.27, 1.55);
transform: rotate(-90deg);
position: absolute;
right: 0;
top: 0;
&.open {
transform: rotate(0deg);
}
> svg {
cursor: pointer;
background: rgba(0, 0, 0, 0.5);
border: 3px solid black;
width: 40px;

View File

@ -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'
}"
>
@ -33,26 +33,61 @@
<Token :role="player.role" @set-role="$emit('set-role')" />
<font-awesome-icon
icon="times-circle"
class="cancel"
title="Cancel"
@click="doSwap(true)"
/>
<font-awesome-icon
icon="exchange-alt"
class="swap"
@click="doSwap()"
title="Swap seats with this player"
/>
<font-awesome-icon
icon="vote-yea"
class="vote"
v-if="player.hasDied && !player.hasVoted"
@click="updatePlayer('hasVoted', true)"
v-if="player.isDead && !player.isVoteless"
@click="updatePlayer('isVoteless', true)"
title="Ghost vote"
/>
<div class="name" @click="changeName">
<span class="screenshot" @click.stop="takeScreenshot">
<font-awesome-icon icon="camera" />
</span>
<span class="name">
{{ player.name }}
</span>
<span class="remove" @click.stop="$emit('remove-player')">
<font-awesome-icon icon="times-circle" />
</span>
<div
class="name"
@click="isMenuOpen = !isMenuOpen"
v-bind:class="{ active: isMenuOpen }"
>
{{ player.name }}
</div>
<transition name="fold">
<ul class="menu" v-if="isMenuOpen && !session.isSpectator">
<li @click="changeName">
<font-awesome-icon icon="user-edit" />
Rename
</li>
<!--<li @click="nomination">
<font-awesome-icon icon="hand-point-right" />
Nomination
</li>-->
<li @click="initSwap">
<font-awesome-icon icon="exchange-alt" />
Swap seats
</li>
<li @click="takeScreenshot">
<font-awesome-icon icon="camera" />
Screenshot
</li>
<li @click="$emit('remove-player')">
<font-awesome-icon icon="times-circle" />
Remove
</li>
</ul>
</transition>
</div>
<template v-if="player.reminders">
<div
class="reminder"
@ -93,11 +128,14 @@ export default {
}
},
computed: {
...mapState(["grimoire"]),
...mapState(["grimoire", "session"]),
...mapGetters({ nightOrder: "players/nightOrder" })
},
data() {
return {};
return {
isMenuOpen: false,
isSwap: false
};
},
filters: {
handleEmojis: text => text.replace(/:([^: ]+?):/g, "").replace(/ •/g, "\n•")
@ -106,25 +144,30 @@ export default {
takeScreenshot() {
const { width, height, x, y } = this.$refs.player.getBoundingClientRect();
this.$emit("screenshot", { width, height, x, y });
this.isMenuOpen = false;
},
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.session.isSpectator) return;
const name = prompt("Player name", this.player.name) || this.player.name;
this.updatePlayer("name", name);
this.isMenuOpen = false;
},
removeReminder(reminder) {
const reminders = [...this.player.reminders];
@ -132,11 +175,19 @@ export default {
this.updatePlayer("reminders", reminders);
},
updatePlayer(property, value) {
if (this.session.isSpectator && property !== "reminders") return;
this.$store.commit("players/update", {
player: this.player,
property,
value
});
},
initSwap() {
this.isMenuOpen = false;
this.$emit("swap-seats");
},
doSwap(cancel) {
this.$emit("swap-seats", cancel ? false : this.player);
}
}
};
@ -145,6 +196,17 @@ export default {
<style lang="scss">
@import "../vars.scss";
.fold-enter-active,
.fold-leave-active {
transition: transform 250ms ease-in-out;
transform-origin: left center;
transform: perspective(200px);
}
.fold-enter,
.fold-leave-to {
transform: perspective(200px) rotateY(90deg);
}
/***** Player token *****/
.circle .player {
margin-bottom: 10px;
@ -181,7 +243,7 @@ export default {
pointer-events: none;
}
&:hover:before {
#townsquare:not(.spectator) &:hover:before {
opacity: 0.5;
top: -10px;
transform: scale(1);
@ -280,6 +342,40 @@ export default {
transform: perspective(400px) rotateY(-180deg);
}
/****** Player choice icons *******/
.player > svg {
position: absolute;
filter: drop-shadow(0 0 3px black);
z-index: 2;
cursor: pointer;
&.swap,
&.cancel {
top: 9%;
left: 20%;
width: 60%;
height: 60%;
opacity: 0;
pointer-events: none;
transition: all 250ms;
transform: scale(0.2);
&:hover {
color: red;
}
}
}
li.swap-from .player > svg.cancel {
opacity: 1;
transform: scale(1);
pointer-events: all;
}
li.swap:not(.swap-from) .player > svg.swap {
opacity: 1;
transform: scale(1);
pointer-events: all;
}
/****** Vote icon ********/
.player .vote {
position: absolute;
@ -310,37 +406,12 @@ export default {
border-radius: 10px;
top: 5px;
box-shadow: 0 0 5px black;
text-overflow: ellipsis;
overflow: hidden;
span.screenshot,
span.remove {
display: none;
position: absolute;
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5));
#app.screenshot & {
display: none;
}
}
span.screenshot {
right: 100%;
}
span.remove {
left: 100%;
}
span.name {
flex-shrink: 1;
text-overflow: ellipsis;
overflow: hidden;
}
&:hover {
#townsquare:not(.spectator) &:hover,
&.active {
color: red;
span {
display: block;
color: white;
&:hover {
color: red;
}
}
}
}
@ -348,11 +419,47 @@ export default {
opacity: 0.5;
}
/***** Player menu *****/
.player > .menu {
position: absolute;
left: 100%;
bottom: 0;
text-align: left;
white-space: nowrap;
background: rgba(0, 0, 0, 0.5);
padding: 0 5px;
border-radius: 5px;
border: 3px solid black;
margin-left: 15px;
cursor: pointer;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
&:before {
content: " ";
width: 0;
height: 0;
position: absolute;
border: 10px solid transparent;
border-right-color: black;
right: 100%;
bottom: 7px;
margin-right: 2px;
}
li:hover {
color: red;
}
svg {
margin-right: 2px;
}
}
/***** Ability text *****/
#townsquare.public .ability {
display: none;
}
.circle .player:hover .ability {
.circle .player .shroud:hover ~ .token .ability,
.circle .player .token:hover .ability {
opacity: 1;
}
@ -366,7 +473,7 @@ export default {
opacity: 1;
transition: opacity 200ms;
display: flex;
top: -16px;
top: -20px;
align-items: center;
pointer-events: none;

View File

@ -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
};
},

View File

@ -2,7 +2,10 @@
<div
id="townsquare"
class="square"
v-bind:class="{ public: grimoire.isPublic }"
v-bind:class="{
public: grimoire.isPublic,
spectator: session.isSpectator
}"
v-bind:style="{ zoom: grimoire.zoom }"
>
<ul class="circle" v-bind:class="['size-' + players.length]">
@ -13,7 +16,12 @@
@add-reminder="openReminderModal(index)"
@set-role="openRoleModal(index)"
@remove-player="removePlayer(index)"
@swap-seats="swapSeats(index, $event)"
@screenshot="$emit('screenshot', $event)"
v-bind:class="{
'swap-from': swapFrom === index,
swap: swapFrom > -1
}"
></Player>
</ul>
@ -51,13 +59,14 @@ export default {
ReminderModal
},
computed: {
...mapState(["grimoire", "roles"]),
...mapState(["grimoire", "roles", "session"]),
...mapState("players", ["players"])
},
data() {
return {
selectedPlayer: 0,
bluffs: 3
bluffs: 3,
swapFrom: -1
};
},
methods: {
@ -70,10 +79,13 @@ export default {
this.$store.commit("toggleModal", "reminder");
},
openRoleModal(playerIndex) {
const player = this.players[playerIndex];
if (this.session.isSpectator && player.role.team === "traveler") return;
this.selectedPlayer = playerIndex;
this.$store.commit("toggleModal", "role");
},
removePlayer(playerIndex) {
if (this.session.isSpectator) return;
if (
confirm(
`Do you really want to remove ${this.players[playerIndex].name}?`
@ -81,6 +93,19 @@ export default {
) {
this.$store.commit("players/remove", playerIndex);
}
},
swapSeats(from, to) {
if (to === undefined) {
this.swapFrom = from;
} else if (to === false) {
this.swapFrom = -1;
} else {
this.$store.commit("players/swap", [
this.swapFrom,
this.players.indexOf(to)
]);
this.swapFrom = -1;
}
}
}
};
@ -94,7 +119,7 @@ export default {
list-style: none;
margin: 0;
li {
> li {
position: absolute;
top: 0;
left: 50%;
@ -163,7 +188,7 @@ export default {
}
@for $i from 1 through 20 {
.circle.size-#{$i} li {
.circle.size-#{$i} > li {
@include on-circle($item-count: $i);
}
}

View File

@ -3,7 +3,14 @@
v-show="modals.role && availableRoles.length"
@close="toggleModal('role')"
>
<h3>Choose a new character for {{ playerIndex >= 0 ? players[playerIndex].name : "bluffing" }}</h3>
<h3>
Choose a new character for
{{
playerIndex >= 0 && players.length
? players[playerIndex].name
: "bluffing"
}}
</h3>
<ul class="tokens">
<li
v-for="role in availableRoles"
@ -74,8 +81,7 @@ export default {
ul.tokens li {
border-radius: 50%;
height: 120px;
width: 120px;
width: 6vw;
margin: 5px;
transition: transform 500ms ease;

View File

@ -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;

View File

@ -12,13 +12,18 @@ import {
faTheaterMasks,
faTimesCircle,
faUser,
faUserEdit,
faUserFriends,
faUsers,
faVoteYea,
faCheckSquare,
faSquare,
faRandom,
faPeopleArrows
faPeopleArrows,
faBroadcastTower,
faCopy,
faExchangeAlt,
faHandPointRight
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@ -32,13 +37,18 @@ library.add(
faTheaterMasks,
faTimesCircle,
faUser,
faUserEdit,
faUserFriends,
faUsers,
faVoteYea,
faCheckSquare,
faSquare,
faRandom,
faPeopleArrows
faPeopleArrows,
faBroadcastTower,
faCopy,
faExchangeAlt,
faHandPointRight
);
Vue.component("font-awesome-icon", FontAwesomeIcon);

View File

@ -1,6 +1,7 @@
import Vue from "vue";
import Vuex from "vuex";
import persistence from "./persistence";
import session from "./session";
import players from "./modules/players";
import editionJSON from "../editions.json";
import rolesJSON from "../roles.json";
@ -35,6 +36,10 @@ export default new Vuex.Store({
background: "",
bluffs: []
},
session: {
sessionId: "",
isSpectator: false
},
modals: {
edition: false,
roles: false,
@ -67,8 +72,18 @@ export default new Vuex.Store({
setBackground({ grimoire }, background) {
grimoire.background = background;
},
setBluff({ grimoire }, { index, role }) {
grimoire.bluffs.splice(index, 1, role);
setSessionId({ session }, sessionId) {
session.sessionId = sessionId;
},
setSpectator({ session }, spectator) {
session.isSpectator = spectator;
},
setBluff({ grimoire }, { index, role } = {}) {
if (index !== undefined) {
grimoire.bluffs.splice(index, 1, role);
} else {
grimoire.bluffs = [];
}
},
toggleModal({ modals }, name) {
modals[name] = !modals[name];
@ -88,5 +103,5 @@ export default new Vuex.Store({
state.roles = getRolesByEdition(edition);
}
},
plugins: [persistence]
plugins: [persistence, session]
});

View File

@ -1,8 +1,8 @@
const NEWPLAYER = {
role: {},
reminders: [],
hasVoted: false,
hasDied: false
isVoteless: false,
isDead: false
};
const state = () => ({
@ -78,6 +78,12 @@ const mutations = {
},
remove(state, index) {
state.players.splice(index, 1);
},
swap(state, [from, to]) {
[state.players[from], state.players[to]] = [
state.players[to],
state.players[from]
];
}
};

View File

@ -27,12 +27,16 @@ module.exports = store => {
}))
);
}
if (localStorage.getItem("session")) {
const [spectator, sessionId] = JSON.parse(localStorage.getItem("session"));
store.commit("setSpectator", spectator);
store.commit("setSessionId", sessionId);
}
// listen to mutations
store.subscribe(({ type, payload }, state) => {
switch (type) {
case "toggleGrimoire":
case "showGrimoire":
localStorage.setItem(
"isPublic",
JSON.stringify(state.grimoire.isPublic)
@ -54,11 +58,22 @@ module.exports = store => {
JSON.stringify(state.grimoire.bluffs.map(({ id }) => id))
);
break;
case "setSessionId":
if (payload) {
localStorage.setItem(
"session",
JSON.stringify([state.session.isSpectator, payload])
);
} else {
localStorage.removeItem("session");
}
break;
case "players/add":
case "players/update":
case "players/remove":
case "players/clear":
case "players/set":
case "players/swap":
if (state.players.players.length) {
localStorage.setItem(
"players",

276
src/store/session.js Normal file
View File

@ -0,0 +1,276 @@
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 }) {
let command, params;
try {
[command, params] = JSON.parse(data);
} catch (err) {
console.log("unsupported socket message", 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.session.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 }) => {
switch (type) {
case "setSessionId":
if (payload) {
session.connect(payload);
} else {
window.location.hash = "";
session.disconnect();
}
break;
case "players/set":
case "players/swap":
case "players/clear":
case "players/remove":
case "players/add":
case "setEdition":
session.sendGamestate();
break;
case "players/update":
session.sendPlayer(payload);
break;
}
});
// 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);
}
};