storyteller tools

This commit is contained in:
Pingumask 2022-11-06 17:11:58 +01:00
parent 77c70c5538
commit d59cef24d3
8 changed files with 259 additions and 30 deletions

View file

@ -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>

View file

@ -63,20 +63,27 @@
:icon="teams.traveler > 1 ? 'user-friends' : 'user'" :icon="teams.traveler > 1 ? 'user-friends' : 'user'"
/> />
</span> </span>
<span v-if="grimoire.isNight"> </li>
Night phase <li v-if="grimoire.isNight">
<font-awesome-icon :icon="['fas', 'cloud-moon']" /> <font-awesome-icon :icon="['fas', 'cloud-moon']" />
</span> {{ locale.towninfo.nightPhase }}
<span v-if="grimoire.isRinging"> </li>
<audio <li v-if="grimoire.isRinging">
:autoplay="!grimoire.isMuted" <audio
src="../assets/sounds/countdown.mp3" :autoplay="!grimoire.isMuted"
:muted="grimoire.isMuted" src="../assets/sounds/countdown.mp3"
></audio> :muted="grimoire.isMuted"
<font-awesome-icon :icon="['fas', 'music']" /> ></audio>
<font-awesome-icon :icon="['fas', 'bell']" /> <font-awesome-icon :icon="['fas', 'music']" />
<font-awesome-icon :icon="['fas', 'music']" /> <font-awesome-icon :icon="['fas', 'bell']" />
</span> <font-awesome-icon :icon="['fas', 'music']" />
</li>
<li>
<Countdown
v-if="grimoire.timer.duration"
:timerName="grimoire.timer.name"
:timerDuration="grimoire.timer.duration"
/>
</li> </li>
</ul> </ul>
</template> </template>
@ -84,8 +91,12 @@
<script> <script>
import gameJSON from "./../game"; import gameJSON from "./../game";
import { mapState } from "vuex"; import { mapState } from "vuex";
import Countdown from "./Countdown";
export default { export default {
components: {
Countdown
},
computed: { computed: {
teams: function() { teams: function() {
const { players } = this.$store.state.players; const { players } = this.$store.state.players;
@ -102,7 +113,10 @@ export default {
).length ).length
}; };
}, },
...mapState(["edition", "grimoire"]), countdownStyle: function() {
return `--timer: ${this.$store.state.grimoire.timer.duration}`;
},
...mapState(["edition", "grimoire", "locale"]),
...mapState("players", ["players"]) ...mapState("players", ["players"])
} }
}; };
@ -184,7 +198,7 @@ export default {
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 100% auto; background-size: 100% auto;
position: absolute; position: absolute;
top: -25%; top: -50%;
} }
} }
</style> </style>

View file

@ -46,6 +46,52 @@
</ul> </ul>
</div> </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"> <div class="fabled" :class="{ closed: !isFabledOpen }" v-if="fabled.length">
<h3> <h3>
<span>{{ locale.townsquare.fabled }}</span> <span>{{ locale.townsquare.fabled }}</span>
@ -113,7 +159,12 @@ export default {
move: -1, move: -1,
nominate: -1, nominate: -1,
isBluffsOpen: true, isBluffsOpen: true,
isFabledOpen: true isFabledOpen: true,
isTimeControlsOpen: false,
timerName: "Timer",
timerDuration: 1,
timerOn: false,
timerEnder: false
}; };
}, },
methods: { methods: {
@ -123,10 +174,23 @@ export default {
toggleFabled() { toggleFabled() {
this.isFabledOpen = !this.isFabledOpen; this.isFabledOpen = !this.isFabledOpen;
}, },
toggleTimeControls() {
this.isTimeControlsOpen = !this.isTimeControlsOpen;
},
removeFabled(index) { removeFabled(index) {
if (this.session.isSpectator) return; if (this.session.isSpectator) return;
this.$store.commit("players/setFabled", { index }); 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]) { handleTrigger(playerIndex, [method, params]) {
if (typeof this[method] === "function") { if (typeof this[method] === "function") {
this[method](playerIndex, params); this[method](playerIndex, params);
@ -251,6 +315,33 @@ export default {
this.move = -1; this.move = -1;
this.swap = -1; this.swap = -1;
this.nominate = -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 *******/ /***** Demon bluffs / Fabled *******/
#townsquare > .bluffs, #townsquare > .bluffs,
#townsquare > .fabled { #townsquare > .fabled,
#townsquare > .storytelling {
position: absolute; position: absolute;
left: 10px;
&.bluffs { &.bluffs {
bottom: 10px; bottom: 10px;
} }
&.fabled { &.fabled {
top: 10px; top: 10px;
} }
left: 10px; &.storytelling{
bottom: 20vmin;
left: auto;
right: 10px;
width: min-content;
}
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border-radius: 10px; border-radius: 10px;
border: 3px solid black; border: 3px solid black;
@ -412,7 +510,7 @@ export default {
transform-origin: bottom left; transform-origin: bottom left;
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
transition: all 200ms ease-in-out; transition: all 250ms ease-in-out;
z-index: 50; z-index: 50;
> svg { > svg {
@ -465,6 +563,15 @@ export default {
transition: all 250ms; transition: all 250ms;
} }
} }
.button-group {
transition: all 250ms;
input {
background: none;
border: none;
color: white;
font-size: 1.1em;
}
}
&.closed { &.closed {
svg.fa-times-circle { svg.fa-times-circle {
display: none; display: none;
@ -473,6 +580,7 @@ export default {
display: block; display: block;
} }
ul li { ul li {
scale: 0;
width: 0; width: 0;
height: 0; height: 0;
.night-order { .night-order {
@ -482,6 +590,12 @@ export default {
border-width: 0; border-width: 0;
} }
} }
.button-group,
.button-group * {
width: 0px;
height: 0px;
scale: 0;
}
} }
} }

View file

@ -17,7 +17,9 @@
<em v-if="nominee.role.team !== 'traveler'"> <em v-if="nominee.role.team !== 'traveler'">
({{ locale.vote.majorityIs }} {{ Math.ceil(alive / 2) }}) ({{ locale.vote.majorityIs }} {{ Math.ceil(alive / 2) }})
</em> </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"> <template v-if="!session.isSpectator">
<div v-if="!session.isVoteInProgress && session.lockedVote < 1"> <div v-if="!session.isVoteInProgress && session.lockedVote < 1">
@ -94,6 +96,11 @@
<div v-else-if="!player"> <div v-else-if="!player">
{{ locale.vote.seatToVote }} {{ locale.vote.seatToVote }}
</div> </div>
<Countdown
v-if="grimoire.timer.duration"
:timerName="grimoire.timer.name"
:timerDuration="grimoire.timer.duration"
/>
</div> </div>
<transition name="blur"> <transition name="blur">
<div <div
@ -116,8 +123,12 @@
<script> <script>
import { mapGetters, mapState } from "vuex"; import { mapGetters, mapState } from "vuex";
import Countdown from "./Countdown";
export default { export default {
components: {
Countdown
},
computed: { computed: {
...mapState("players", ["players"]), ...mapState("players", ["players"]),
...mapState(["session", "grimoire", "locale"]), ...mapState(["session", "grimoire", "locale"]),

View file

@ -111,7 +111,11 @@ export default new Vuex.Store({
isMuted: false, isMuted: false,
isImageOptIn: false, isImageOptIn: false,
zoom: 0, zoom: 0,
background: "" background: "",
timer: {
name: "",
duration: 0
}
}, },
modals: { modals: {
edition: false, edition: false,
@ -179,6 +183,9 @@ export default new Vuex.Store({
toggleRinging: toggle("isRinging"), toggleRinging: toggle("isRinging"),
toggleGrimoire: toggle("isPublic"), toggleGrimoire: toggle("isPublic"),
toggleImageOptIn: toggle("isImageOptIn"), toggleImageOptIn: toggle("isImageOptIn"),
setTimer(state, timer) {
state.grimoire.timer = timer;
},
toggleModal({ modals }, name) { toggleModal({ modals }, name) {
if (name) { if (name) {
modals[name] = !modals[name]; modals[name] = !modals[name];

View file

@ -91,10 +91,12 @@
"townsquare":{ "townsquare":{
"others": "Other characters", "others": "Other characters",
"bluffs": "Demon bluffs", "bluffs": "Demon bluffs",
"fabled": "Fabled" "fabled": "Fabled",
"storytellerTools": "Storytelling"
}, },
"towninfo":{ "towninfo":{
"addPlayers":"Please add more players!" "addPlayers":"Please add more players!",
"nightPhase":"Night Phase"
}, },
"player":{ "player":{
"handUp": "Hand UP", "handUp": "Hand UP",

View file

@ -91,10 +91,12 @@
"townsquare":{ "townsquare":{
"others": "Autres Rôles", "others": "Autres Rôles",
"bluffs": "Bluffs de Démon", "bluffs": "Bluffs de Démon",
"fabled": "Fabuleux" "fabled": "Fabuleux",
"storytellerTools": "Narration"
}, },
"towninfo":{ "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":{ "player":{
"handUp": "Main levée", "handUp": "Main levée",
@ -118,7 +120,7 @@
"intro":{ "intro":{
"header": "Bienvenue sur le Centre-ville Virtuel (non-officiel) pour Blood on the Clocktower! Veuillez ajouter des Joueurs via le", "header": "Bienvenue sur le Centre-ville Virtuel (non-officiel) pour Blood on the Clocktower! Veuillez ajouter des Joueurs via le",
"menu": "Menu", "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", "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." "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."
}, },

View file

@ -179,9 +179,10 @@ class LiveSession {
case "isRinging": case "isRinging":
if (!this._isSpectator) return; if (!this._isSpectator) return;
this._store.commit("toggleRinging", params); this._store.commit("toggleRinging", params);
// if (params){ break;
// setTimeout(this._store.commit, 4000, "toggleRinging", false); case "setTimer":
// } if (!this._isSpectator) return;
this._store.commit("setTimer", params);
break; break;
case "isVoteHistoryAllowed": case "isVoteHistoryAllowed":
if (!this._isSpectator) return; if (!this._isSpectator) return;
@ -285,6 +286,7 @@ class LiveSession {
gamestate: this._gamestate, gamestate: this._gamestate,
isNight: grimoire.isNight, isNight: grimoire.isNight,
isRinging: grimoire.isRinging, isRinging: grimoire.isRinging,
timer: grimoire.timer,
isVoteHistoryAllowed: session.isVoteHistoryAllowed, isVoteHistoryAllowed: session.isVoteHistoryAllowed,
nomination: session.nomination, nomination: session.nomination,
votingSpeed: session.votingSpeed, votingSpeed: session.votingSpeed,
@ -310,6 +312,7 @@ class LiveSession {
isNight, isNight,
isVoteHistoryAllowed, isVoteHistoryAllowed,
isRinging, isRinging,
timer,
nomination, nomination,
votingSpeed, votingSpeed,
votes, votes,
@ -361,6 +364,7 @@ class LiveSession {
} }
}); });
if (!isLightweight) { if (!isLightweight) {
this._store.commit("timer", timer);
this._store.commit("toggleRinging", !!isRinging); this._store.commit("toggleRinging", !!isRinging);
this._store.commit("toggleNight", !!isNight); this._store.commit("toggleNight", !!isNight);
this._store.commit("session/setVoteHistoryAllowed", isVoteHistoryAllowed); this._store.commit("session/setVoteHistoryAllowed", isVoteHistoryAllowed);
@ -721,6 +725,14 @@ class LiveSession {
this._send("isRinging", this._store.state.grimoire.isRinging); 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 * Send the isVoteHistoryAllowed state. ST only
*/ */
@ -905,6 +917,9 @@ export default store => {
case "toggleRinging": case "toggleRinging":
session.setIsRinging(); session.setIsRinging();
break; break;
case "setTimer":
session.setTimer();
break;
case "setEdition": case "setEdition":
session.sendEdition(); session.sendEdition();
break; break;