townsquare/src/components/Vote.vue

401 lines
9.8 KiB
Vue

<template>
<div id="vote">
<div class="arrows">
<span class="nominee" :style="nomineeStyle"></span>
<span class="nominator" :style="nominatorStyle"></span>
</div>
<div class="overlay">
<em class="blue">{{ nominator.name }}</em> nominated
<em>{{ nominee.name }}</em
>!
<br />
<template v-if="nominee.role.team !== 'traveler'">
<em class="blue">
{{ voters.length }} vote{{ voters.length !== 1 ? "s" : "" }}
</em>
in favor
<em>(majority is {{ Math.ceil(alive / 2) }})</em>
</template>
<template v-else>
<em>{{ Math.ceil(players.length / 2) }} votes</em> required for a
<em>majority</em>.
</template>
<div v-if="session.isVoteInProgress">
<em class="blue" v-if="voters.length">{{ voters.join(", ") }} </em>
<span v-else>nobody</span>
had their hand <em>UP</em>
</div>
<template v-if="!session.isSpectator">
<div v-if="!session.isVoteInProgress">
Time per player:
<font-awesome-icon
@mousedown.prevent="setVotingSpeed(-500)"
icon="minus-circle"
/>
{{ session.votingSpeed / 1000 }}s
<font-awesome-icon
@mousedown.prevent="setVotingSpeed(500)"
icon="plus-circle"
/>
</div>
<div class="button-group">
<div
class="button townsfolk"
v-if="!session.isVoteInProgress"
@click="countdown"
>
Countdown
</div>
<div class="button" v-if="!session.isVoteInProgress" @click="start">
Start
</div>
<template v-else>
<div
class="button townsfolk"
:class="{ disabled: !session.lockedVote }"
@click="pause"
>
{{ voteTimer ? "Pause" : "Resume" }}
</div>
<div class="button" @click="stop">Reset</div>
</template>
<div class="button demon" @click="finish">Close</div>
</div>
</template>
<template v-else-if="canVote">
<div v-if="!session.isVoteInProgress">
{{ session.votingSpeed / 1000 }} seconds between votes
</div>
<div class="button-group">
<div
class="button townsfolk"
@click="vote(false)"
:class="{ disabled: !currentVote }"
>
Hand DOWN
</div>
<div
class="button demon"
@click="vote(true)"
:class="{ disabled: currentVote }"
>
Hand UP
</div>
</div>
</template>
<div v-else-if="!player">
Please claim a seat to vote.
</div>
</div>
<transition name="blur">
<div
class="countdown"
v-if="session.isVoteInProgress && !session.lockedVote"
>
<span>3</span>
<span>2</span>
<span>1</span>
<span>GO</span>
<audio autoplay src="../assets/sounds/countdown.mp3"></audio>
</div>
</transition>
</div>
</template>
<script>
import { mapGetters, mapState } from "vuex";
export default {
computed: {
...mapState("players", ["players"]),
...mapState(["session"]),
...mapGetters({ alive: "players/alive" }),
nominator: function() {
return this.players[this.session.nomination[0]];
},
nominatorStyle: function() {
const players = this.players.length;
const nomination = this.session.nomination[0];
return {
transform: `rotate(${Math.round((nomination / players) * 360)}deg)`,
transitionDuration: this.session.votingSpeed - 0.1 + "s"
};
},
nominee: function() {
return this.players[this.session.nomination[1]];
},
nomineeStyle: function() {
const players = this.players.length;
const nomination = this.session.nomination[1];
const lock = this.session.lockedVote;
const rotation = (360 * (nomination + Math.min(lock, players))) / players;
return {
transform: `rotate(${Math.round(rotation)}deg)`,
transitionDuration: this.session.votingSpeed - 100 + "ms"
};
},
player: function() {
return this.players.find(p => p.id === this.session.playerId);
},
currentVote: function() {
const index = this.players.findIndex(p => p.id === this.session.playerId);
return index >= 0 ? !!this.session.votes[index] : undefined;
},
canVote: function() {
if (!this.player) return false;
if (this.player.isVoteless && this.nominee.role.team !== "traveler")
return false;
const session = this.session;
const players = this.players.length;
const index = this.players.indexOf(this.player);
const indexAdjusted =
(index - 1 + players - session.nomination[1]) % players;
return indexAdjusted >= session.lockedVote - 1;
},
voters: function() {
const nomination = this.session.nomination[1];
const voters = this.session.votes.map((vote, index) =>
vote ? this.players[index].name : ""
);
const reorder = [
...voters.slice(nomination + 1, this.players.length),
...voters.slice(0, nomination + 1)
];
return reorder.slice(0, this.session.lockedVote - 1).filter(n => !!n);
}
},
data() {
return {
voteTimer: null
};
},
methods: {
countdown() {
this.$store.commit("session/setVoteInProgress", true);
this.voteTimer = setInterval(() => {
this.start();
}, 4000);
},
start() {
this.$store.commit("session/setVoteInProgress", true);
this.$store.commit("session/lockVote");
clearInterval(this.voteTimer);
this.voteTimer = setInterval(() => {
this.$store.commit("session/lockVote");
if (this.session.lockedVote > this.players.length) {
clearInterval(this.voteTimer);
this.$store.commit("session/setVoteInProgress", false);
}
}, this.session.votingSpeed);
},
pause() {
if (this.voteTimer) {
clearInterval(this.voteTimer);
this.voteTimer = null;
} else {
this.start();
}
},
stop() {
clearInterval(this.voteTimer);
this.voteTimer = null;
this.$store.commit("session/setVoteInProgress", false);
this.$store.commit("session/lockVote", 0);
},
finish() {
clearInterval(this.voteTimer);
this.$store.commit("session/addHistory", this.players);
this.$store.commit("session/nomination");
},
vote(vote) {
if (!this.canVote) return false;
const index = this.players.findIndex(p => p.id === this.session.playerId);
if (index >= 0 && !!this.session.votes[index] !== vote) {
this.$store.commit("session/voteSync", [index, vote]);
}
},
setVotingSpeed(diff) {
const speed = Math.round(this.session.votingSpeed + diff);
if (speed > 0) {
this.$store.commit("session/setVotingSpeed", speed);
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../vars.scss";
#vote {
position: absolute;
width: 20%;
z-index: 20;
display: flex;
align-items: center;
align-content: center;
justify-content: center;
background: url("../assets/demon-head.png") center center no-repeat;
background-size: auto 75%;
text-align: center;
text-shadow: 0 1px 2px #000000, 0 -1px 2px #000000, 1px 0 2px #000000,
-1px 0 2px #000000;
&:after {
content: " ";
padding-bottom: 100%;
display: block;
}
em {
color: $demon;
font-style: normal;
font-weight: bold;
&.blue {
color: $townsfolk;
}
}
svg {
cursor: pointer;
&:hover path {
fill: url(#demon);
stroke-width: 30px;
stroke: white;
}
}
}
@keyframes arrow-cw {
0% {
opacity: 0;
transform: rotate(-180deg);
}
100% {
opacity: 1;
transform: rotate(0deg);
}
}
@keyframes arrow-ccw {
0% {
opacity: 0;
transform: rotate(180deg);
}
100% {
opacity: 1;
transform: rotate(0deg);
}
}
.arrows {
position: absolute;
display: flex;
height: 150%;
width: 25%;
pointer-events: none;
span {
position: absolute;
width: 100%;
height: 100%;
transition: transform 2.9s ease-in-out;
}
span:before {
content: " ";
width: 100%;
height: 100%;
display: block;
background-size: auto 100%;
background-repeat: no-repeat;
background-position: center center;
position: absolute;
filter: drop-shadow(0px 0px 3px #000);
}
.nominator:before {
background-image: url("../assets/clock-small.png");
animation: arrow-ccw 1s ease-out;
}
.nominee:before {
background-image: url("../assets/clock-big.png");
animation: arrow-cw 1s ease-out;
}
}
@keyframes countdown {
0% {
transform: scale(1.5);
opacity: 0;
filter: blur(20px);
}
10% {
opacity: 1;
}
50% {
transform: scale(1);
filter: blur(0);
}
90% {
color: $townsfolk;
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes countdown-go {
0% {
transform: scale(1.5);
opacity: 0;
filter: blur(20px);
}
10% {
opacity: 1;
}
50% {
transform: scale(1);
filter: blur(0);
}
90% {
color: $demon;
opacity: 1;
}
100% {
opacity: 0;
}
}
.countdown {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
pointer-events: none;
audio {
height: 0;
width: 0;
visibility: hidden;
}
span {
position: absolute;
font-size: 8em;
font-weight: bold;
opacity: 0;
}
span:nth-child(1) {
animation: countdown 1100ms normal forwards;
}
span:nth-child(2) {
animation: countdown 1100ms normal forwards 1000ms;
}
span:nth-child(3) {
animation: countdown 1100ms normal forwards 2000ms;
}
span:nth-child(4) {
animation: countdown-go 1100ms normal forwards 3000ms;
}
}
</style>