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,
|
||||
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 |
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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: 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
14
src/main.js
14
src/main.js
|
@ -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);
|
||||
|
|
|
@ -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]
|
||||
});
|
||||
|
|
|
@ -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]
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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