diff --git a/src/components/Countdown.vue b/src/components/Countdown.vue new file mode 100644 index 0000000..ad9afba --- /dev/null +++ b/src/components/Countdown.vue @@ -0,0 +1,64 @@ +<template> + <div :data-text="timerName" :style="style" class="countdown"></div> +</template> + +<script> +export default { + props: { + timerName: String, + timerDuration: Number + }, + computed: { + style() { + return `--timer: ${this.timerDuration}`; + } + } +}; +</script> + +<style lang="scss" scoped> +div { + width: 100%; + height: 1.6em; + border: 2px solid black; + background: rgba(0, 0, 0, 0.4); + position: relative; + z-index: 0; + margin-top: 0.3em; +} + +div::before { + content: ""; + position: absolute; + inset: 0; + background: rgba(255, 0, 0, 0.6); + z-index: 1; + animation: forwards countdown calc(var(--timer) * 1s) linear; +} + +div::after{ + position:absolute; + inset: 0; + text-align:center; + content: attr(data-text); + z-index: 2; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0) 5%, + rgba(255, 255, 255, 0.5) 15%, + rgba(255, 255, 255, 0) 35%, + rgba(0, 0, 0, 0) 60%, + rgba(0, 0, 0, 0.7) 100% + ); +} + +@keyframes countdown { + 0% { + width: 100%; + } + 100% { + width:0%; + display:none; + } +} +</style> diff --git a/src/components/TownInfo.vue b/src/components/TownInfo.vue index c99a2a3..6a63148 100644 --- a/src/components/TownInfo.vue +++ b/src/components/TownInfo.vue @@ -63,20 +63,27 @@ :icon="teams.traveler > 1 ? 'user-friends' : 'user'" /> </span> - <span v-if="grimoire.isNight"> - Night phase - <font-awesome-icon :icon="['fas', 'cloud-moon']" /> - </span> - <span v-if="grimoire.isRinging"> - <audio - :autoplay="!grimoire.isMuted" - src="../assets/sounds/countdown.mp3" - :muted="grimoire.isMuted" - ></audio> - <font-awesome-icon :icon="['fas', 'music']" /> - <font-awesome-icon :icon="['fas', 'bell']" /> - <font-awesome-icon :icon="['fas', 'music']" /> - </span> + </li> + <li v-if="grimoire.isNight"> + <font-awesome-icon :icon="['fas', 'cloud-moon']" /> + {{ locale.towninfo.nightPhase }} + </li> + <li v-if="grimoire.isRinging"> + <audio + :autoplay="!grimoire.isMuted" + src="../assets/sounds/countdown.mp3" + :muted="grimoire.isMuted" + ></audio> + <font-awesome-icon :icon="['fas', 'music']" /> + <font-awesome-icon :icon="['fas', 'bell']" /> + <font-awesome-icon :icon="['fas', 'music']" /> + </li> + <li> + <Countdown + v-if="grimoire.timer.duration" + :timerName="grimoire.timer.name" + :timerDuration="grimoire.timer.duration" + /> </li> </ul> </template> @@ -84,8 +91,12 @@ <script> import gameJSON from "./../game"; import { mapState } from "vuex"; +import Countdown from "./Countdown"; export default { + components: { + Countdown + }, computed: { teams: function() { const { players } = this.$store.state.players; @@ -102,7 +113,10 @@ export default { ).length }; }, - ...mapState(["edition", "grimoire"]), + countdownStyle: function() { + return `--timer: ${this.$store.state.grimoire.timer.duration}`; + }, + ...mapState(["edition", "grimoire", "locale"]), ...mapState("players", ["players"]) } }; @@ -184,7 +198,7 @@ export default { background-repeat: no-repeat; background-size: 100% auto; position: absolute; - top: -25%; + top: -50%; } } </style> diff --git a/src/components/TownSquare.vue b/src/components/TownSquare.vue index 1da049f..54cd2e9 100644 --- a/src/components/TownSquare.vue +++ b/src/components/TownSquare.vue @@ -46,6 +46,52 @@ </ul> </div> + <div + class="storytelling" + v-if="!session.isSpectator" + ref="storytelling" + :class="{ closed: !isTimeControlsOpen }" + > + <h3> + <span>{{ locale.townsquare.storytellerTools }}</span> + <font-awesome-icon + icon="times-circle" + @click.stop="toggleTimeControls" + /> + <font-awesome-icon + icon="plus-circle" + @click.stop="toggleTimeControls" + /> + </h3> + <div class="button-group"> + <div @click="setTimer()" class="button">đ {{ timerDuration }} min</div> + <div @click="renameTimer()" class="button">đ {{ timerName }}</div> + <div + class="button demon" + @click="stopTimer()" + :class="{ disabled: !timerOn }" + > + â + </div> + <div + class="button townfolk" + @click="startTimer()" + :class="{ disabled: timerOn }" + > + â” + </div> + </div> + <div class="button-group"> + <div @click="toggleNight()" class="button" :class="{disabled: grimoire.isNight}">â</div> + <div @click="toggleNight()" class="button" :class="{disabled: !grimoire.isNight}">âœ</div> + </div> + <div class="button-group"> + <div @click="toggleRinging()" class="button"> + <font-awesome-icon :icon="['fas', 'bell']" /> + </div> + </div> + </div> + <div class="fabled" :class="{ closed: !isFabledOpen }" v-if="fabled.length"> <h3> <span>{{ locale.townsquare.fabled }}</span> @@ -113,7 +159,12 @@ export default { move: -1, nominate: -1, isBluffsOpen: true, - isFabledOpen: true + isFabledOpen: true, + isTimeControlsOpen: false, + timerName: "Timer", + timerDuration: 1, + timerOn: false, + timerEnder: false }; }, methods: { @@ -123,10 +174,23 @@ export default { toggleFabled() { this.isFabledOpen = !this.isFabledOpen; }, + toggleTimeControls() { + this.isTimeControlsOpen = !this.isTimeControlsOpen; + }, removeFabled(index) { if (this.session.isSpectator) return; this.$store.commit("players/setFabled", { index }); }, + toggleNight() { + this.$store.commit("toggleNight"); + if (this.grimoire.isNight) { + this.$store.commit("session/setMarkedPlayer", -1); + } + }, + toggleRinging() { + this.$store.commit("toggleRinging", true); + setTimeout(this.$store.commit, 4000, "toggleRinging", false); + }, handleTrigger(playerIndex, [method, params]) { if (typeof this[method] === "function") { this[method](playerIndex, params); @@ -251,6 +315,33 @@ export default { this.move = -1; this.swap = -1; this.nominate = -1; + }, + renameTimer() { + let newName = prompt("Timer Name", ""); + if (newName === "") { + return; + } + this.timerName = newName.trim(); + }, + setTimer() { + let newDuration = prompt("Timer Duration in minutes"); + if (isNaN(newDuration)) { + return alert("Incorrect number"); + } + if (newDuration > 0) { + this.timerDuration = newDuration; + } + }, + startTimer() { + let timer = { name: this.timerName, duration: this.timerDuration * 60 }; + this.$store.commit("setTimer", timer); + this.timerOn = true; + this.timerEnder = setTimeout(this.stopTimer, timer.duration * 1000); + }, + stopTimer() { + this.$store.commit("setTimer", {}); + this.timerOn = false; + clearTimeout(this.timerEnder); } } }; @@ -396,15 +487,22 @@ export default { /***** Demon bluffs / Fabled *******/ #townsquare > .bluffs, -#townsquare > .fabled { +#townsquare > .fabled, +#townsquare > .storytelling { position: absolute; + left: 10px; &.bluffs { bottom: 10px; } &.fabled { top: 10px; } - left: 10px; + &.storytelling{ + bottom: 20vmin; + left: auto; + right: 10px; + width: min-content; + } background: rgba(0, 0, 0, 0.5); border-radius: 10px; border: 3px solid black; @@ -412,7 +510,7 @@ export default { transform-origin: bottom left; transform: scale(1); opacity: 1; - transition: all 200ms ease-in-out; + transition: all 250ms ease-in-out; z-index: 50; > svg { @@ -465,6 +563,15 @@ export default { transition: all 250ms; } } + .button-group { + transition: all 250ms; + input { + background: none; + border: none; + color: white; + font-size: 1.1em; + } + } &.closed { svg.fa-times-circle { display: none; @@ -473,6 +580,7 @@ export default { display: block; } ul li { + scale: 0; width: 0; height: 0; .night-order { @@ -482,6 +590,12 @@ export default { border-width: 0; } } + .button-group, + .button-group * { + width: 0px; + height: 0px; + scale: 0; + } } } diff --git a/src/components/Vote.vue b/src/components/Vote.vue index 48f9e85..36f0b3c 100644 --- a/src/components/Vote.vue +++ b/src/components/Vote.vue @@ -17,7 +17,9 @@ <em v-if="nominee.role.team !== 'traveler'"> ({{ locale.vote.majorityIs }} {{ Math.ceil(alive / 2) }}) </em> - <em v-else>({{ locale.vote.majorityIs }} {{ Math.ceil(players.length / 2) }})</em> + <em v-else> + ({{ locale.vote.majorityIs }} {{ Math.ceil(players.length / 2) }}) + </em> <template v-if="!session.isSpectator"> <div v-if="!session.isVoteInProgress && session.lockedVote < 1"> @@ -94,6 +96,11 @@ <div v-else-if="!player"> {{ locale.vote.seatToVote }} </div> + <Countdown + v-if="grimoire.timer.duration" + :timerName="grimoire.timer.name" + :timerDuration="grimoire.timer.duration" + /> </div> <transition name="blur"> <div @@ -116,8 +123,12 @@ <script> import { mapGetters, mapState } from "vuex"; +import Countdown from "./Countdown"; export default { + components: { + Countdown + }, computed: { ...mapState("players", ["players"]), ...mapState(["session", "grimoire", "locale"]), diff --git a/src/store/index.js b/src/store/index.js index 5694658..87ace2c 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -111,7 +111,11 @@ export default new Vuex.Store({ isMuted: false, isImageOptIn: false, zoom: 0, - background: "" + background: "", + timer: { + name: "", + duration: 0 + } }, modals: { edition: false, @@ -179,6 +183,9 @@ export default new Vuex.Store({ toggleRinging: toggle("isRinging"), toggleGrimoire: toggle("isPublic"), toggleImageOptIn: toggle("isImageOptIn"), + setTimer(state, timer) { + state.grimoire.timer = timer; + }, toggleModal({ modals }, name) { if (name) { modals[name] = !modals[name]; diff --git a/src/store/locale/en/ui.json b/src/store/locale/en/ui.json index d8b3fc1..12034e4 100644 --- a/src/store/locale/en/ui.json +++ b/src/store/locale/en/ui.json @@ -91,10 +91,12 @@ "townsquare":{ "others": "Other characters", "bluffs": "Demon bluffs", - "fabled": "Fabled" + "fabled": "Fabled", + "storytellerTools": "Storytelling" }, "towninfo":{ - "addPlayers":"Please add more players!" + "addPlayers":"Please add more players!", + "nightPhase":"Night Phase" }, "player":{ "handUp": "Hand UP", diff --git a/src/store/locale/fr/ui.json b/src/store/locale/fr/ui.json index cbab18a..a50f900 100644 --- a/src/store/locale/fr/ui.json +++ b/src/store/locale/fr/ui.json @@ -91,10 +91,12 @@ "townsquare":{ "others": "Autres RĂŽles", "bluffs": "Bluffs de DĂ©mon", - "fabled": "Fabuleux" + "fabled": "Fabuleux", + "storytellerTools": "Narration" }, "towninfo":{ - "addPlayers":"Appuyez sur [A] pour ajouter plus de joueurs !" + "addPlayers": "Appuyez sur [A] pour ajouter plus de joueurs !", + "nightPhase": "C'est la nuit" }, "player":{ "handUp": "Main levĂ©e", @@ -118,7 +120,7 @@ "intro":{ "header": "Bienvenue sur le Centre-ville Virtuel (non-officiel) pour Blood on the Clocktower! Veuillez ajouter des Joueurs via le", "menu": "Menu", - "body": "en haut Ă droite ou en appuyant sur [A] pour commencer. Vous pouvez aussi rejoindre une ssession en appuyant sur [J].", + "body": "en haut Ă droite ou en appuyant sur [A] pour commencer. Vous pouvez aussi rejoindre une session en appuyant sur [J].", "footerStart": "Ce programme est libre et ses sources peuvent ĂȘtre trouvĂ©es sur", "footerEnd": ". Ce site n'est pas affiliĂ© Ă The Pandemonium Institute. \"Blood on the Clocktower\" est une marque dĂ©posĂ©e de Steven Medway & The Pandemonium Institute." }, diff --git a/src/store/socket.js b/src/store/socket.js index 5da2c65..36b3e9d 100644 --- a/src/store/socket.js +++ b/src/store/socket.js @@ -179,9 +179,10 @@ class LiveSession { case "isRinging": if (!this._isSpectator) return; this._store.commit("toggleRinging", params); - // if (params){ - // setTimeout(this._store.commit, 4000, "toggleRinging", false); - // } + break; + case "setTimer": + if (!this._isSpectator) return; + this._store.commit("setTimer", params); break; case "isVoteHistoryAllowed": if (!this._isSpectator) return; @@ -285,6 +286,7 @@ class LiveSession { gamestate: this._gamestate, isNight: grimoire.isNight, isRinging: grimoire.isRinging, + timer: grimoire.timer, isVoteHistoryAllowed: session.isVoteHistoryAllowed, nomination: session.nomination, votingSpeed: session.votingSpeed, @@ -310,6 +312,7 @@ class LiveSession { isNight, isVoteHistoryAllowed, isRinging, + timer, nomination, votingSpeed, votes, @@ -361,6 +364,7 @@ class LiveSession { } }); if (!isLightweight) { + this._store.commit("timer", timer); this._store.commit("toggleRinging", !!isRinging); this._store.commit("toggleNight", !!isNight); this._store.commit("session/setVoteHistoryAllowed", isVoteHistoryAllowed); @@ -721,6 +725,14 @@ class LiveSession { this._send("isRinging", this._store.state.grimoire.isRinging); } + /** + * Start or stop a timer + */ + setTimer() { + if (this._isSpectator) return; + this._send("setTimer", this._store.state.grimoire.timer); + } + /** * Send the isVoteHistoryAllowed state. ST only */ @@ -905,6 +917,9 @@ export default store => { case "toggleRinging": session.setIsRinging(); break; + case "setTimer": + session.setTimer(); + break; case "setEdition": session.sendEdition(); break;