added live sessions for townsquare sync

This commit is contained in:
Steffen 2020-05-09 21:47:00 +02:00
parent a028cf867f
commit da3cd96723
No known key found for this signature in database
GPG Key ID: 764D74E98267DFC6
11 changed files with 342 additions and 60 deletions

View File

@ -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":

View File

@ -43,40 +43,46 @@
<li @click="joinSession" v-if="!grimoire.sessionId">
Join Live Session
</li>
<li class="headline" v-if="grimoire.sessionId">
<font-awesome-icon icon="broadcast-tower" />
{{ grimoire.isSpectator ? "Spectating" : "Hosting" }}
</li>
<li @click="leaveSession" v-if="grimoire.sessionId">
<em>{{ grimoire.sessionId.substr(2) }}</em>
<em>{{ grimoire.sessionId }}</em>
Leave Session
</li>
<!-- 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>
<template v-if="!grimoire.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>
<!-- 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>
@ -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");

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'
}"
>
@ -36,8 +36,8 @@
<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"
/>
@ -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;

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: grimoire.isSpectator
}"
v-bind:style="{ zoom: grimoire.zoom }"
>
<ul class="circle" v-bind:class="['size-' + players.length]">
@ -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}?`

View File

@ -74,8 +74,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

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

View File

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

View File

@ -1,8 +1,8 @@
const NEWPLAYER = {
role: {},
reminders: [],
hasVoted: false,
hasDied: false
isVoteless: false,
isDead: false
};
const state = () => ({

View File

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