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(); this.$refs.menu.randomizeSeatings();
break; break;
case "e": case "e":
if (this.grimoire.isSpectator) return;
this.$store.commit("toggleModal", "edition"); this.$store.commit("toggleModal", "edition");
break; break;
case "c": case "c":
if (this.grimoire.isSpectator) return;
this.$store.commit("toggleModal", "roles"); this.$store.commit("toggleModal", "roles");
break; break;
case "Escape": case "Escape":

View File

@ -43,40 +43,46 @@
<li @click="joinSession" v-if="!grimoire.sessionId"> <li @click="joinSession" v-if="!grimoire.sessionId">
Join Live Session Join Live Session
</li> </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"> <li @click="leaveSession" v-if="grimoire.sessionId">
<em>{{ grimoire.sessionId.substr(2) }}</em> <em>{{ grimoire.sessionId }}</em>
Leave Session Leave Session
</li> </li>
<!-- Users --> <template v-if="!grimoire.isSpectator">
<li class="headline"> <!-- Users -->
<font-awesome-icon icon="users" /> <li class="headline">
Players <font-awesome-icon icon="users" />
</li> Players
<li @click="addPlayer" v-if="players.length < 20"><em>[A]</em> Add</li> </li>
<li @click="randomizeSeatings" v-if="players.length > 2"> <li @click="addPlayer" v-if="players.length < 20"><em>[A]</em> Add</li>
<em>[R]</em> Randomize <li @click="randomizeSeatings" v-if="players.length > 2">
</li> <em>[R]</em> Randomize
<li @click="clearPlayers" v-if="players.length"> </li>
Remove all <li @click="clearPlayers" v-if="players.length">
</li> Remove all
</li>
<!-- Characters --> <!-- Characters -->
<li class="headline"> <li class="headline">
<font-awesome-icon icon="theater-masks" /> <font-awesome-icon icon="theater-masks" />
Characters Characters
</li> </li>
<li @click="toggleModal('edition')"> <li @click="toggleModal('edition')">
<em>[E]</em> <em>[E]</em>
Select Edition Select Edition
</li> </li>
<li @click="toggleModal('roles')" v-if="players.length > 4"> <li @click="toggleModal('roles')" v-if="players.length > 4">
<em>[C]</em> <em>[C]</em>
Choose & Assign Choose & Assign
</li> </li>
<li @click="clearRoles" v-if="players.length"> <li @click="clearRoles" v-if="players.length">
Remove all Remove all
</li> </li>
</template>
</ul> </ul>
</div> </div>
</div> </div>
@ -113,7 +119,8 @@ export default {
.substring(2, 7) .substring(2, 7)
); );
if (sessionId) { 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() { joinSession() {
@ -121,29 +128,35 @@ export default {
"Enter the code of the session you want to join" "Enter the code of the session you want to join"
); );
if (sessionId) { 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() { leaveSession() {
this.$store.commit("setSpectator", false);
this.$store.commit("setSessionId", ""); this.$store.commit("setSessionId", "");
}, },
addPlayer() { addPlayer() {
if (this.grimoire.isSpectator) return;
const name = prompt("Player name"); const name = prompt("Player name");
if (name) { if (name) {
this.$store.commit("players/add", name); this.$store.commit("players/add", name);
} }
}, },
randomizeSeatings() { randomizeSeatings() {
if (this.grimoire.isSpectator) return;
if (confirm("Are you sure you want to randomize seatings?")) { if (confirm("Are you sure you want to randomize seatings?")) {
this.$store.dispatch("players/randomize"); this.$store.dispatch("players/randomize");
} }
}, },
clearPlayers() { clearPlayers() {
if (this.grimoire.isSpectator) return;
if (confirm("Are you sure you want to remove all players?")) { if (confirm("Are you sure you want to remove all players?")) {
this.$store.commit("players/clear"); this.$store.commit("players/clear");
} }
}, },
clearRoles() { clearRoles() {
if (this.grimoire.isSpectator) return;
this.$store.commit("showGrimoire"); this.$store.commit("showGrimoire");
if (confirm("Are you sure you want to remove all player roles?")) { if (confirm("Are you sure you want to remove all player roles?")) {
this.$store.dispatch("players/clearRoles"); this.$store.dispatch("players/clearRoles");

View File

@ -4,8 +4,8 @@
ref="player" ref="player"
class="player" class="player"
:class="{ :class="{
dead: player.hasDied, dead: player.isDead,
'no-vote': player.hasVoted, 'no-vote': player.isVoteless,
traveler: player.role && player.role.team === 'traveler' traveler: player.role && player.role.team === 'traveler'
}" }"
> >
@ -36,8 +36,8 @@
<font-awesome-icon <font-awesome-icon
icon="vote-yea" icon="vote-yea"
class="vote" class="vote"
v-if="player.hasDied && !player.hasVoted" v-if="player.isDead && !player.isVoteless"
@click="updatePlayer('hasVoted', true)" @click="updatePlayer('isVoteless', true)"
title="Ghost vote" title="Ghost vote"
/> />
@ -109,20 +109,23 @@ export default {
}, },
toggleStatus() { toggleStatus() {
if (this.grimoire.isPublic) { if (this.grimoire.isPublic) {
if (!this.player.hasDied) { if (!this.player.isDead) {
this.updatePlayer("hasDied", true); this.updatePlayer("isDead", true);
} else if (this.player.hasVoted) { } else if (this.player.isVoteless) {
this.updatePlayer("hasVoted", false); this.updatePlayer("isVoteless", false);
this.updatePlayer("hasDied", false); this.updatePlayer("isDead", false);
} else { } else {
this.updatePlayer("hasVoted", true); this.updatePlayer("isVoteless", true);
} }
} else { } else {
this.updatePlayer("hasDied", !this.player.hasDied); this.updatePlayer("isDead", !this.player.isDead);
this.updatePlayer("hasVoted", false); if (this.player.isVoteless) {
this.updatePlayer("isVoteless", false);
}
} }
}, },
changeName() { changeName() {
if (this.grimoire.isSpectator) return;
const name = prompt("Player name", this.player.name) || this.player.name; const name = prompt("Player name", this.player.name) || this.player.name;
this.updatePlayer("name", name); this.updatePlayer("name", name);
}, },
@ -132,6 +135,7 @@ export default {
this.updatePlayer("reminders", reminders); this.updatePlayer("reminders", reminders);
}, },
updatePlayer(property, value) { updatePlayer(property, value) {
if (this.grimoire.isSpectator && property !== "reminders") return;
this.$store.commit("players/update", { this.$store.commit("players/update", {
player: this.player, player: this.player,
property, property,
@ -181,7 +185,7 @@ export default {
pointer-events: none; pointer-events: none;
} }
&:hover:before { #townsquare:not(.spectator) &:hover:before {
opacity: 0.5; opacity: 0.5;
top: -10px; top: -10px;
transform: scale(1); transform: scale(1);
@ -332,7 +336,7 @@ export default {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
&:hover { #townsquare:not(.spectator) &:hover {
color: red; color: red;
span { span {
display: block; display: block;

View File

@ -47,7 +47,7 @@ export default {
teams: function() { teams: function() {
const { players } = this.$store.state.players; const { players } = this.$store.state.players;
const nonTravelers = this.$store.getters["players/nonTravelers"]; 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 { return {
...gameJSON[nonTravelers - 5], ...gameJSON[nonTravelers - 5],
traveler: players.length - nonTravelers, traveler: players.length - nonTravelers,
@ -55,7 +55,7 @@ export default {
votes: votes:
alive + alive +
players.filter( players.filter(
player => player.hasDied === true && player.hasVoted !== true player => player.isDead === true && player.isVoteless !== true
).length ).length
}; };
}, },

View File

@ -2,7 +2,10 @@
<div <div
id="townsquare" id="townsquare"
class="square" class="square"
v-bind:class="{ public: grimoire.isPublic }" v-bind:class="{
public: grimoire.isPublic,
spectator: grimoire.isSpectator
}"
v-bind:style="{ zoom: grimoire.zoom }" v-bind:style="{ zoom: grimoire.zoom }"
> >
<ul class="circle" v-bind:class="['size-' + players.length]"> <ul class="circle" v-bind:class="['size-' + players.length]">
@ -70,10 +73,13 @@ export default {
this.$store.commit("toggleModal", "reminder"); this.$store.commit("toggleModal", "reminder");
}, },
openRoleModal(playerIndex) { openRoleModal(playerIndex) {
const player = this.players[playerIndex];
if (this.grimoire.isSpectator && player.role.team === "traveler") return;
this.selectedPlayer = playerIndex; this.selectedPlayer = playerIndex;
this.$store.commit("toggleModal", "role"); this.$store.commit("toggleModal", "role");
}, },
removePlayer(playerIndex) { removePlayer(playerIndex) {
if (this.grimoire.isSpectator) return;
if ( if (
confirm( confirm(
`Do you really want to remove ${this.players[playerIndex].name}?` `Do you really want to remove ${this.players[playerIndex].name}?`

View File

@ -74,8 +74,7 @@ export default {
ul.tokens li { ul.tokens li {
border-radius: 50%; border-radius: 50%;
height: 120px; width: 6vw;
width: 120px;
margin: 5px; margin: 5px;
transition: transform 500ms ease; transition: transform 500ms ease;

View File

@ -149,8 +149,7 @@ ul.tokens {
padding-left: 55px; padding-left: 55px;
li { li {
border-radius: 50%; border-radius: 50%;
height: 120px; width: 6vw;
width: 120px;
margin: 5px; margin: 5px;
opacity: 0.5; opacity: 0.5;
transition: all 250ms; transition: all 250ms;
@ -181,7 +180,6 @@ ul.tokens {
opacity: 1; opacity: 1;
position: absolute; position: absolute;
left: 0; left: 0;
top: 40px;
font-weight: bold; font-weight: bold;
line-height: 50px; line-height: 50px;
text-align: center; text-align: center;

View File

@ -18,7 +18,8 @@ import {
faCheckSquare, faCheckSquare,
faSquare, faSquare,
faRandom, faRandom,
faPeopleArrows faPeopleArrows,
faBroadcastTower
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@ -38,7 +39,8 @@ library.add(
faCheckSquare, faCheckSquare,
faSquare, faSquare,
faRandom, faRandom,
faPeopleArrows faPeopleArrows,
faBroadcastTower
); );
Vue.component("font-awesome-icon", FontAwesomeIcon); Vue.component("font-awesome-icon", FontAwesomeIcon);

View File

@ -35,7 +35,8 @@ export default new Vuex.Store({
zoom: 1, zoom: 1,
background: "", background: "",
bluffs: [], bluffs: [],
sessionId: "" sessionId: "",
isSpectator: false
}, },
modals: { modals: {
edition: false, edition: false,
@ -72,6 +73,9 @@ export default new Vuex.Store({
setSessionId({ grimoire }, sessionId) { setSessionId({ grimoire }, sessionId) {
grimoire.sessionId = sessionId; grimoire.sessionId = sessionId;
}, },
setSpectator({ grimoire }, spectator) {
grimoire.isSpectator = spectator;
},
setBluff({ grimoire }, { index, role }) { setBluff({ grimoire }, { index, role }) {
grimoire.bluffs.splice(index, 1, role); grimoire.bluffs.splice(index, 1, role);
}, },

View File

@ -1,8 +1,8 @@
const NEWPLAYER = { const NEWPLAYER = {
role: {}, role: {},
reminders: [], reminders: [],
hasVoted: false, isVoteless: false,
hasDied: false isDead: false
}; };
const state = () => ({ 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 => { module.exports = store => {
// setup // setup
const session = new LiveSession(store);
// listen to mutations // listen to mutations
store.subscribe(({ type, payload }, state) => { store.subscribe(({ type, payload }) => {
console.log(type, payload, state); 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;
}
}); });
}; };