mirror of
https://github.com/bra1n/townsquare.git
synced 2025-04-04 22:24:36 +00:00
443 lines
11 KiB
Vue
443 lines
11 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 />
|
|
<em class="blue">
|
|
{{ voters.length }} vote{{ voters.length !== 1 ? "s" : "" }}
|
|
</em>
|
|
in favor
|
|
<em v-if="nominee.role.team !== 'traveler'">
|
|
(majority is {{ Math.ceil(alive / 2) }})
|
|
</em>
|
|
<em v-else>(majority is {{ Math.ceil(players.length / 2) }})</em>
|
|
|
|
<template v-if="!session.isSpectator">
|
|
<div
|
|
v-if="
|
|
session.isVoteWatchingAllowed &&
|
|
!session.isVoteInProgress &&
|
|
session.lockedVote < 1
|
|
"
|
|
>
|
|
Time per player:
|
|
<span @mousedown.prevent="setVotingSpeed(-500)">
|
|
<font-awesome-icon icon="minus-circle"
|
|
/></span>
|
|
|
|
{{ session.votingSpeed / 1000 }}s
|
|
<span @mousedown.prevent="setVotingSpeed(500)">
|
|
<font-awesome-icon icon="plus-circle" />
|
|
</span>
|
|
</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">
|
|
{{ session.lockedVote ? "Restart" : "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>
|
|
<div class="button-group mark" v-if="nominee.role.team !== 'traveler'">
|
|
<div
|
|
class="button"
|
|
:class="{
|
|
disabled: session.nomination[1] === session.markedPlayer,
|
|
}"
|
|
@click="setMarked"
|
|
>
|
|
Mark for execution
|
|
</div>
|
|
<div class="button" @click="removeMarked">Clear mark</div>
|
|
</div>
|
|
</template>
|
|
<template v-else-if="canVote">
|
|
<div v-if="session.isVoteWatchingAllowed && !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>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapGetters, mapState } from "vuex";
|
|
import bongs from "../assets/sounds/countdown.mp3";
|
|
|
|
export default {
|
|
computed: {
|
|
...mapState("players", ["players"]),
|
|
...mapState(["session", "grimoire"]),
|
|
...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 - 100 + "ms",
|
|
};
|
|
},
|
|
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 = Array(this.players.length)
|
|
.fill("")
|
|
.map((x, index) =>
|
|
this.session.votes[index] ? this.players[index].name : ""
|
|
);
|
|
const reorder = [
|
|
...voters.slice(nomination + 1),
|
|
...voters.slice(0, nomination + 1),
|
|
];
|
|
return (
|
|
this.session.lockedVote
|
|
? reorder.slice(0, this.session.lockedVote - 1)
|
|
: reorder
|
|
).filter((n) => !!n);
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
voteTimer: null,
|
|
};
|
|
},
|
|
methods: {
|
|
countdown() {
|
|
this.$store.commit("session/lockVote", 0);
|
|
this.$store.commit("session/setVoteInProgress", true);
|
|
if (!this.grimoire.isMuted) {
|
|
var sound = new Audio(bongs);
|
|
sound.play();
|
|
}
|
|
this.voteTimer = setInterval(() => {
|
|
this.start();
|
|
}, 4000);
|
|
},
|
|
start() {
|
|
this.$store.commit("session/lockVote", 1);
|
|
this.$store.commit("session/setVoteInProgress", true);
|
|
clearInterval(this.voteTimer);
|
|
|
|
if (this.session.isVoteWatchingAllowed) {
|
|
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);
|
|
} else {
|
|
this.$store.commit("session/lockVote", this.players, length + 1);
|
|
}
|
|
},
|
|
pause() {
|
|
if (this.voteTimer) {
|
|
clearInterval(this.voteTimer);
|
|
this.voteTimer = null;
|
|
} else {
|
|
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);
|
|
}
|
|
},
|
|
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);
|
|
}
|
|
},
|
|
setMarked() {
|
|
this.$store.commit("session/setMarkedPlayer", this.session.nomination[1]);
|
|
},
|
|
removeMarked() {
|
|
this.$store.commit("session/setMarkedPlayer", -1);
|
|
},
|
|
},
|
|
};
|
|
</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;
|
|
|
|
.mark .button {
|
|
font-size: 75%;
|
|
margin: 0;
|
|
}
|
|
|
|
&: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>
|