mirror of https://github.com/bra1n/townsquare.git
added live sessions for townsquare sync
This commit is contained in:
parent
a028cf867f
commit
da3cd96723
|
@ -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":
|
||||
|
|
|
@ -43,11 +43,16 @@
|
|||
<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>
|
||||
|
||||
<template v-if="!grimoire.isSpectator">
|
||||
<!-- Users -->
|
||||
<li class="headline">
|
||||
<font-awesome-icon icon="users" />
|
||||
|
@ -77,6 +82,7 @@
|
|||
<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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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}?`
|
||||
|
|
|
@ -74,8 +74,7 @@ export default {
|
|||
|
||||
ul.tokens li {
|
||||
border-radius: 50%;
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
width: 6vw;
|
||||
margin: 5px;
|
||||
transition: transform 500ms ease;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const NEWPLAYER = {
|
||||
role: {},
|
||||
reminders: [],
|
||||
hasVoted: false,
|
||||
hasDied: false
|
||||
isVoteless: false,
|
||||
isDead: false
|
||||
};
|
||||
|
||||
const state = () => ({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue