mirror of https://github.com/bra1n/townsquare.git
Merge pull request #10 from bra1n/voting
Added live play session, player swapping and more
This commit is contained in:
commit
78a32d3581
10
src/App.vue
10
src/App.vue
|
@ -37,10 +37,10 @@ export default {
|
||||||
EditionModal,
|
EditionModal,
|
||||||
RolesModal
|
RolesModal
|
||||||
},
|
},
|
||||||
computed: mapState({
|
computed: {
|
||||||
grimoire: state => state.grimoire,
|
...mapState(["grimoire", "session"]),
|
||||||
players: state => state.players.players
|
...mapState("players", ["players"])
|
||||||
}),
|
},
|
||||||
methods: {
|
methods: {
|
||||||
takeScreenshot(dimensions) {
|
takeScreenshot(dimensions) {
|
||||||
this.$refs.menu.takeScreenshot(dimensions);
|
this.$refs.menu.takeScreenshot(dimensions);
|
||||||
|
@ -57,9 +57,11 @@ export default {
|
||||||
this.$refs.menu.randomizeSeatings();
|
this.$refs.menu.randomizeSeatings();
|
||||||
break;
|
break;
|
||||||
case "e":
|
case "e":
|
||||||
|
if (this.session.isSpectator) return;
|
||||||
this.$store.commit("toggleModal", "edition");
|
this.$store.commit("toggleModal", "edition");
|
||||||
break;
|
break;
|
||||||
case "c":
|
case "c":
|
||||||
|
if (this.session.isSpectator) return;
|
||||||
this.$store.commit("toggleModal", "roles");
|
this.$store.commit("toggleModal", "roles");
|
||||||
break;
|
break;
|
||||||
case "Escape":
|
case "Escape":
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
|
@ -1,9 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="controls">
|
<div id="controls">
|
||||||
<Screenshot ref="screenshot"></Screenshot>
|
<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
|
<font-awesome-icon
|
||||||
icon="camera"
|
icon="camera"
|
||||||
@click="takeScreenshot()"
|
@click="takeScreenshot()"
|
||||||
|
title="Take a screenshot"
|
||||||
v-bind:class="{ success: grimoire.isScreenshotSuccess }"
|
v-bind:class="{ success: grimoire.isScreenshotSuccess }"
|
||||||
/>
|
/>
|
||||||
<div class="menu" v-bind:class="{ open: grimoire.isMenuOpen }">
|
<div class="menu" v-bind:class="{ open: grimoire.isMenuOpen }">
|
||||||
|
@ -37,36 +45,58 @@
|
||||||
<li @click="setBackground">
|
<li @click="setBackground">
|
||||||
Background image
|
Background image
|
||||||
</li>
|
</li>
|
||||||
|
<li @click="hostSession" v-if="!session.sessionId">
|
||||||
<!-- Users -->
|
Host Live Session
|
||||||
<li class="headline">
|
|
||||||
<font-awesome-icon icon="users" />
|
|
||||||
Players
|
|
||||||
</li>
|
</li>
|
||||||
<li @click="addPlayer" v-if="players.length < 20"><em>[A]</em> Add</li>
|
<li @click="joinSession" v-if="!session.sessionId">
|
||||||
<li @click="randomizeSeatings" v-if="players.length > 2">
|
Join Live Session
|
||||||
<em>[R]</em> Randomize
|
|
||||||
</li>
|
</li>
|
||||||
<li @click="clearPlayers" v-if="players.length">
|
<li class="headline" v-if="session.sessionId">
|
||||||
Remove all
|
<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>
|
</li>
|
||||||
|
|
||||||
<!-- Characters -->
|
<template v-if="!session.isSpectator">
|
||||||
<li class="headline">
|
<!-- Users -->
|
||||||
<font-awesome-icon icon="theater-masks" />
|
<li class="headline">
|
||||||
Characters
|
<font-awesome-icon icon="users" />
|
||||||
</li>
|
Players
|
||||||
<li @click="toggleModal('edition')">
|
</li>
|
||||||
<em>[E]</em>
|
<li @click="addPlayer" v-if="players.length < 20">
|
||||||
Select Edition
|
<em>[A]</em> Add
|
||||||
</li>
|
</li>
|
||||||
<li @click="toggleModal('roles')" v-if="players.length > 4">
|
<li @click="randomizeSeatings" v-if="players.length > 2">
|
||||||
<em>[C]</em>
|
<em>[R]</em> Randomize
|
||||||
Choose & Assign
|
</li>
|
||||||
</li>
|
<li @click="clearPlayers" v-if="players.length">
|
||||||
<li @click="clearRoles" v-if="players.length">
|
Remove all
|
||||||
Remove all
|
</li>
|
||||||
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,10 +110,10 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
Screenshot
|
Screenshot
|
||||||
},
|
},
|
||||||
computed: mapState({
|
computed: {
|
||||||
grimoire: state => state.grimoire,
|
...mapState(["grimoire", "session"]),
|
||||||
players: state => state.players.players
|
...mapState("players", ["players"])
|
||||||
}),
|
},
|
||||||
methods: {
|
methods: {
|
||||||
takeScreenshot(dimensions = {}) {
|
takeScreenshot(dimensions = {}) {
|
||||||
this.$store.commit("updateScreenshot");
|
this.$store.commit("updateScreenshot");
|
||||||
|
@ -95,26 +125,75 @@ export default {
|
||||||
prompt("Enter custom background URL")
|
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() {
|
addPlayer() {
|
||||||
|
if (this.session.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.session.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.session.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");
|
||||||
|
this.$store.commit("setBluff");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearRoles() {
|
clearRoles() {
|
||||||
this.$store.commit("showGrimoire");
|
if (this.session.isSpectator) return;
|
||||||
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");
|
||||||
|
this.$store.commit("setBluff");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...mapMutations([
|
...mapMutations([
|
||||||
|
@ -148,13 +227,13 @@ export default {
|
||||||
right: 3px;
|
right: 3px;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
padding-right: 50px;
|
||||||
|
|
||||||
#app.screenshot & {
|
#app.screenshot & {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
cursor: pointer;
|
|
||||||
filter: drop-shadow(0 0 5px rgba(0, 0, 0, 1));
|
filter: drop-shadow(0 0 5px rgba(0, 0, 0, 1));
|
||||||
&.success {
|
&.success {
|
||||||
animation: greenToWhite 1s normal forwards;
|
animation: greenToWhite 1s normal forwards;
|
||||||
|
@ -162,11 +241,18 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-camera {
|
> svg {
|
||||||
position: absolute;
|
cursor: pointer;
|
||||||
right: 50px;
|
|
||||||
top: 10px;
|
|
||||||
z-index: 5;
|
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;
|
transform-origin: 190px 22px;
|
||||||
transition: transform 500ms cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
transition: transform 500ms cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
> svg {
|
> svg {
|
||||||
|
cursor: pointer;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
border: 3px solid black;
|
border: 3px solid black;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
|
|
|
@ -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'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
@ -33,26 +33,61 @@
|
||||||
|
|
||||||
<Token :role="player.role" @set-role="$emit('set-role')" />
|
<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
|
<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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="name" @click="changeName">
|
<div
|
||||||
<span class="screenshot" @click.stop="takeScreenshot">
|
class="name"
|
||||||
<font-awesome-icon icon="camera" />
|
@click="isMenuOpen = !isMenuOpen"
|
||||||
</span>
|
v-bind:class="{ active: isMenuOpen }"
|
||||||
<span class="name">
|
>
|
||||||
{{ player.name }}
|
{{ player.name }}
|
||||||
</span>
|
|
||||||
<span class="remove" @click.stop="$emit('remove-player')">
|
|
||||||
<font-awesome-icon icon="times-circle" />
|
|
||||||
</span>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<template v-if="player.reminders">
|
<template v-if="player.reminders">
|
||||||
<div
|
<div
|
||||||
class="reminder"
|
class="reminder"
|
||||||
|
@ -93,11 +128,14 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["grimoire"]),
|
...mapState(["grimoire", "session"]),
|
||||||
...mapGetters({ nightOrder: "players/nightOrder" })
|
...mapGetters({ nightOrder: "players/nightOrder" })
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {};
|
return {
|
||||||
|
isMenuOpen: false,
|
||||||
|
isSwap: false
|
||||||
|
};
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
handleEmojis: text => text.replace(/:([^: ]+?):/g, "").replace(/ •/g, "\n•")
|
handleEmojis: text => text.replace(/:([^: ]+?):/g, "").replace(/ •/g, "\n•")
|
||||||
|
@ -106,25 +144,30 @@ export default {
|
||||||
takeScreenshot() {
|
takeScreenshot() {
|
||||||
const { width, height, x, y } = this.$refs.player.getBoundingClientRect();
|
const { width, height, x, y } = this.$refs.player.getBoundingClientRect();
|
||||||
this.$emit("screenshot", { width, height, x, y });
|
this.$emit("screenshot", { width, height, x, y });
|
||||||
|
this.isMenuOpen = false;
|
||||||
},
|
},
|
||||||
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.session.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);
|
||||||
|
this.isMenuOpen = false;
|
||||||
},
|
},
|
||||||
removeReminder(reminder) {
|
removeReminder(reminder) {
|
||||||
const reminders = [...this.player.reminders];
|
const reminders = [...this.player.reminders];
|
||||||
|
@ -132,11 +175,19 @@ export default {
|
||||||
this.updatePlayer("reminders", reminders);
|
this.updatePlayer("reminders", reminders);
|
||||||
},
|
},
|
||||||
updatePlayer(property, value) {
|
updatePlayer(property, value) {
|
||||||
|
if (this.session.isSpectator && property !== "reminders") return;
|
||||||
this.$store.commit("players/update", {
|
this.$store.commit("players/update", {
|
||||||
player: this.player,
|
player: this.player,
|
||||||
property,
|
property,
|
||||||
value
|
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">
|
<style lang="scss">
|
||||||
@import "../vars.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 *****/
|
/***** Player token *****/
|
||||||
.circle .player {
|
.circle .player {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
@ -181,7 +243,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);
|
||||||
|
@ -280,6 +342,40 @@ export default {
|
||||||
transform: perspective(400px) rotateY(-180deg);
|
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 ********/
|
/****** Vote icon ********/
|
||||||
.player .vote {
|
.player .vote {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -310,37 +406,12 @@ export default {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
box-shadow: 0 0 5px black;
|
box-shadow: 0 0 5px black;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
span.screenshot,
|
#townsquare:not(.spectator) &:hover,
|
||||||
span.remove {
|
&.active {
|
||||||
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 {
|
|
||||||
color: red;
|
color: red;
|
||||||
span {
|
|
||||||
display: block;
|
|
||||||
color: white;
|
|
||||||
&:hover {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,11 +419,47 @@ export default {
|
||||||
opacity: 0.5;
|
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 *****/
|
/***** Ability text *****/
|
||||||
#townsquare.public .ability {
|
#townsquare.public .ability {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.circle .player:hover .ability {
|
.circle .player .shroud:hover ~ .token .ability,
|
||||||
|
.circle .player .token:hover .ability {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,7 +473,7 @@ export default {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 200ms;
|
transition: opacity 200ms;
|
||||||
display: flex;
|
display: flex;
|
||||||
top: -16px;
|
top: -20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -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: session.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]">
|
||||||
|
@ -13,7 +16,12 @@
|
||||||
@add-reminder="openReminderModal(index)"
|
@add-reminder="openReminderModal(index)"
|
||||||
@set-role="openRoleModal(index)"
|
@set-role="openRoleModal(index)"
|
||||||
@remove-player="removePlayer(index)"
|
@remove-player="removePlayer(index)"
|
||||||
|
@swap-seats="swapSeats(index, $event)"
|
||||||
@screenshot="$emit('screenshot', $event)"
|
@screenshot="$emit('screenshot', $event)"
|
||||||
|
v-bind:class="{
|
||||||
|
'swap-from': swapFrom === index,
|
||||||
|
swap: swapFrom > -1
|
||||||
|
}"
|
||||||
></Player>
|
></Player>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -51,13 +59,14 @@ export default {
|
||||||
ReminderModal
|
ReminderModal
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["grimoire", "roles"]),
|
...mapState(["grimoire", "roles", "session"]),
|
||||||
...mapState("players", ["players"])
|
...mapState("players", ["players"])
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedPlayer: 0,
|
selectedPlayer: 0,
|
||||||
bluffs: 3
|
bluffs: 3,
|
||||||
|
swapFrom: -1
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -70,10 +79,13 @@ export default {
|
||||||
this.$store.commit("toggleModal", "reminder");
|
this.$store.commit("toggleModal", "reminder");
|
||||||
},
|
},
|
||||||
openRoleModal(playerIndex) {
|
openRoleModal(playerIndex) {
|
||||||
|
const player = this.players[playerIndex];
|
||||||
|
if (this.session.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.session.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}?`
|
||||||
|
@ -81,6 +93,19 @@ export default {
|
||||||
) {
|
) {
|
||||||
this.$store.commit("players/remove", playerIndex);
|
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;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
li {
|
> li {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -163,7 +188,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
@for $i from 1 through 20 {
|
@for $i from 1 through 20 {
|
||||||
.circle.size-#{$i} li {
|
.circle.size-#{$i} > li {
|
||||||
@include on-circle($item-count: $i);
|
@include on-circle($item-count: $i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,14 @@
|
||||||
v-show="modals.role && availableRoles.length"
|
v-show="modals.role && availableRoles.length"
|
||||||
@close="toggleModal('role')"
|
@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">
|
<ul class="tokens">
|
||||||
<li
|
<li
|
||||||
v-for="role in availableRoles"
|
v-for="role in availableRoles"
|
||||||
|
@ -74,8 +81,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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
14
src/main.js
14
src/main.js
|
@ -12,13 +12,18 @@ import {
|
||||||
faTheaterMasks,
|
faTheaterMasks,
|
||||||
faTimesCircle,
|
faTimesCircle,
|
||||||
faUser,
|
faUser,
|
||||||
|
faUserEdit,
|
||||||
faUserFriends,
|
faUserFriends,
|
||||||
faUsers,
|
faUsers,
|
||||||
faVoteYea,
|
faVoteYea,
|
||||||
faCheckSquare,
|
faCheckSquare,
|
||||||
faSquare,
|
faSquare,
|
||||||
faRandom,
|
faRandom,
|
||||||
faPeopleArrows
|
faPeopleArrows,
|
||||||
|
faBroadcastTower,
|
||||||
|
faCopy,
|
||||||
|
faExchangeAlt,
|
||||||
|
faHandPointRight
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
|
|
||||||
|
@ -32,13 +37,18 @@ library.add(
|
||||||
faTheaterMasks,
|
faTheaterMasks,
|
||||||
faTimesCircle,
|
faTimesCircle,
|
||||||
faUser,
|
faUser,
|
||||||
|
faUserEdit,
|
||||||
faUserFriends,
|
faUserFriends,
|
||||||
faUsers,
|
faUsers,
|
||||||
faVoteYea,
|
faVoteYea,
|
||||||
faCheckSquare,
|
faCheckSquare,
|
||||||
faSquare,
|
faSquare,
|
||||||
faRandom,
|
faRandom,
|
||||||
faPeopleArrows
|
faPeopleArrows,
|
||||||
|
faBroadcastTower,
|
||||||
|
faCopy,
|
||||||
|
faExchangeAlt,
|
||||||
|
faHandPointRight
|
||||||
);
|
);
|
||||||
|
|
||||||
Vue.component("font-awesome-icon", FontAwesomeIcon);
|
Vue.component("font-awesome-icon", FontAwesomeIcon);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import Vuex from "vuex";
|
import Vuex from "vuex";
|
||||||
import persistence from "./persistence";
|
import persistence from "./persistence";
|
||||||
|
import session from "./session";
|
||||||
import players from "./modules/players";
|
import players from "./modules/players";
|
||||||
import editionJSON from "../editions.json";
|
import editionJSON from "../editions.json";
|
||||||
import rolesJSON from "../roles.json";
|
import rolesJSON from "../roles.json";
|
||||||
|
@ -35,6 +36,10 @@ export default new Vuex.Store({
|
||||||
background: "",
|
background: "",
|
||||||
bluffs: []
|
bluffs: []
|
||||||
},
|
},
|
||||||
|
session: {
|
||||||
|
sessionId: "",
|
||||||
|
isSpectator: false
|
||||||
|
},
|
||||||
modals: {
|
modals: {
|
||||||
edition: false,
|
edition: false,
|
||||||
roles: false,
|
roles: false,
|
||||||
|
@ -67,8 +72,18 @@ export default new Vuex.Store({
|
||||||
setBackground({ grimoire }, background) {
|
setBackground({ grimoire }, background) {
|
||||||
grimoire.background = background;
|
grimoire.background = background;
|
||||||
},
|
},
|
||||||
setBluff({ grimoire }, { index, role }) {
|
setSessionId({ session }, sessionId) {
|
||||||
grimoire.bluffs.splice(index, 1, role);
|
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) {
|
toggleModal({ modals }, name) {
|
||||||
modals[name] = !modals[name];
|
modals[name] = !modals[name];
|
||||||
|
@ -88,5 +103,5 @@ export default new Vuex.Store({
|
||||||
state.roles = getRolesByEdition(edition);
|
state.roles = getRolesByEdition(edition);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [persistence]
|
plugins: [persistence, session]
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const NEWPLAYER = {
|
const NEWPLAYER = {
|
||||||
role: {},
|
role: {},
|
||||||
reminders: [],
|
reminders: [],
|
||||||
hasVoted: false,
|
isVoteless: false,
|
||||||
hasDied: false
|
isDead: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = () => ({
|
const state = () => ({
|
||||||
|
@ -78,6 +78,12 @@ const mutations = {
|
||||||
},
|
},
|
||||||
remove(state, index) {
|
remove(state, index) {
|
||||||
state.players.splice(index, 1);
|
state.players.splice(index, 1);
|
||||||
|
},
|
||||||
|
swap(state, [from, to]) {
|
||||||
|
[state.players[from], state.players[to]] = [
|
||||||
|
state.players[to],
|
||||||
|
state.players[from]
|
||||||
|
];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
// listen to mutations
|
||||||
store.subscribe(({ type, payload }, state) => {
|
store.subscribe(({ type, payload }, state) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "toggleGrimoire":
|
case "toggleGrimoire":
|
||||||
case "showGrimoire":
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"isPublic",
|
"isPublic",
|
||||||
JSON.stringify(state.grimoire.isPublic)
|
JSON.stringify(state.grimoire.isPublic)
|
||||||
|
@ -54,11 +58,22 @@ module.exports = store => {
|
||||||
JSON.stringify(state.grimoire.bluffs.map(({ id }) => id))
|
JSON.stringify(state.grimoire.bluffs.map(({ id }) => id))
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "setSessionId":
|
||||||
|
if (payload) {
|
||||||
|
localStorage.setItem(
|
||||||
|
"session",
|
||||||
|
JSON.stringify([state.session.isSpectator, payload])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("session");
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "players/add":
|
case "players/add":
|
||||||
case "players/update":
|
case "players/update":
|
||||||
case "players/remove":
|
case "players/remove":
|
||||||
case "players/clear":
|
case "players/clear":
|
||||||
case "players/set":
|
case "players/set":
|
||||||
|
case "players/swap":
|
||||||
if (state.players.players.length) {
|
if (state.players.players.length) {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"players",
|
"players",
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue