mirror of
https://github.com/bra1n/townsquare.git
synced 2025-04-04 22:24:36 +00:00
Add a timer
This commit is contained in:
parent
9bafcc2c61
commit
045f7112e0
9 changed files with 330 additions and 38 deletions
|
@ -1,5 +1,8 @@
|
|||
# Release Notes
|
||||
|
||||
### Version 2.15.4
|
||||
- add timer
|
||||
|
||||
### Version 2.15.3
|
||||
- add Huntsman/Damsel to list of available characters
|
||||
|
||||
|
|
|
@ -121,6 +121,10 @@ export default {
|
|||
if (this.session.isSpectator) return;
|
||||
this.$refs.menu.toggleNight();
|
||||
break;
|
||||
case "t":
|
||||
if (this.session.isSpectator) return;
|
||||
this.$refs.menu.toggleTimer();
|
||||
break;
|
||||
case "escape":
|
||||
this.$store.commit("toggleModal");
|
||||
}
|
||||
|
|
173
src/components/CountdownTimer.vue
Normal file
173
src/components/CountdownTimer.vue
Normal file
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<div class="countdown-timer">
|
||||
<div
|
||||
id="timer"
|
||||
v-bind:style="[
|
||||
remainingSeconds <= 30 ? { color: 'red' } : { color: 'white' }
|
||||
]"
|
||||
>
|
||||
{{ formattedMinutes }}:{{ formattedSeconds }}
|
||||
</div>
|
||||
<div class="timer-control" v-if="!session.isSpectator">
|
||||
<div class="timer-button" @click="startTimer" v-if="!isTicking">
|
||||
<font-awesome-icon icon="play"></font-awesome-icon>
|
||||
</div>
|
||||
<div class="timer-button" @click="pauseTimer" v-if="isTicking">
|
||||
<font-awesome-icon icon="pause"></font-awesome-icon>
|
||||
</div>
|
||||
<div class="timer-button" @click="stopTimer">
|
||||
<font-awesome-icon icon="stop"></font-awesome-icon>
|
||||
</div>
|
||||
<div class="timer-button" @click="addMinute" v-if="!isTicking">
|
||||
<font-awesome-icon icon="plus"></font-awesome-icon>
|
||||
</div>
|
||||
<div class="timer-button" @click="subtractMinute" v-if="!isTicking">
|
||||
<font-awesome-icon icon="minus"></font-awesome-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "CountdownTimer",
|
||||
computed: {
|
||||
...mapState(["session"]),
|
||||
remainingSeconds: {
|
||||
get: function() {
|
||||
return this.$store.state.countdownTimer.remainingSeconds;
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.state.countdownTimer.remainingSeconds = newValue;
|
||||
}
|
||||
},
|
||||
totalSeconds: {
|
||||
get: function() {
|
||||
return this.$store.state.countdownTimer.totalSeconds;
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.state.countdownTimer.totalSeconds = newValue;
|
||||
}
|
||||
},
|
||||
isTicking: {
|
||||
get: function() {
|
||||
return this.$store.state.countdownTimer.isTicking;
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.state.countdownTimer.isTicking = newValue;
|
||||
}
|
||||
},
|
||||
formattedMinutes() {
|
||||
let minutes = Math.floor(this.remainingSeconds / 60);
|
||||
if (minutes < 10) {
|
||||
minutes = "0" + minutes;
|
||||
}
|
||||
return minutes;
|
||||
},
|
||||
formattedSeconds() {
|
||||
let seconds = this.remainingSeconds % 60;
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds;
|
||||
}
|
||||
return seconds;
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
clearInterval(this.timerInterval);
|
||||
if (this.isTicking) {
|
||||
this.timerInterval = setInterval(this.elapseTimer, 1000);
|
||||
}
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
timerInterval: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
startTimer() {
|
||||
clearInterval(this.timerInterval);
|
||||
if (this.remainingSeconds === 0) {
|
||||
this.remainingSeconds = this.totalSeconds;
|
||||
}
|
||||
this.isTicking = true;
|
||||
this.sendTimerUpdate();
|
||||
this.timerInterval = setInterval(this.elapseTimer, 1000);
|
||||
},
|
||||
pauseTimer() {
|
||||
clearInterval(this.timerInterval);
|
||||
this.isTicking = false;
|
||||
this.sendTimerUpdate();
|
||||
},
|
||||
stopTimer() {
|
||||
clearInterval(this.timerInterval);
|
||||
this.remainingSeconds = this.totalSeconds;
|
||||
this.pauseTimer();
|
||||
},
|
||||
sendTimerUpdate() {
|
||||
if (this.session.isSpectator) return;
|
||||
let payload = {
|
||||
remainingSeconds: this.remainingSeconds,
|
||||
totalSeconds: this.totalSeconds,
|
||||
isTicking: this.isTicking
|
||||
};
|
||||
this.$store.commit("session/distributeTimerAction", payload);
|
||||
},
|
||||
elapseTimer() {
|
||||
if (this.remainingSeconds <= 0) {
|
||||
clearInterval(this.timerInterval);
|
||||
return;
|
||||
}
|
||||
this.remainingSeconds--;
|
||||
|
||||
// Update local state on ST, so if a player joins while timer is ticking, they will get updated timer state.
|
||||
if (!this.session.isSpectator) {
|
||||
let payload = {
|
||||
remainingSeconds: this.remainingSeconds,
|
||||
totalSeconds: this.totalSeconds,
|
||||
isTicking: this.isTicking
|
||||
};
|
||||
this.$store.commit("session/updateTimerState", payload);
|
||||
}
|
||||
},
|
||||
addMinute() {
|
||||
this.totalSeconds += 60;
|
||||
this.remainingSeconds += 60;
|
||||
this.sendTimerUpdate();
|
||||
},
|
||||
subtractMinute() {
|
||||
this.totalSeconds = Math.max(0, this.totalSeconds - 60);
|
||||
this.remainingSeconds = Math.max(0, this.remainingSeconds - 60);
|
||||
this.sendTimerUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.countdown-timer {
|
||||
border-color: white;
|
||||
border-style: solid;
|
||||
border-width: thick;
|
||||
border-radius: 10px;
|
||||
padding: 5px 5px 5px;
|
||||
}
|
||||
.timer-control {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.timer-button {
|
||||
margin: 0 5px 0 5px;
|
||||
filter: drop-shadow(0 2px 1px black);
|
||||
}
|
||||
#timer {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
text-shadow: 0 2px 1px black, 0 -2px 1px black, 2px 0 1px black,
|
||||
-2px 0 1px black;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -138,6 +138,10 @@
|
|||
Send Characters
|
||||
<em><font-awesome-icon icon="theater-masks"/></em>
|
||||
</li>
|
||||
<li v-if="!session.isSpectator" @click="toggleTimer">
|
||||
Toggle timer
|
||||
<em>[T]</em>
|
||||
</li>
|
||||
<li
|
||||
v-if="session.voteHistory.length || !session.isSpectator"
|
||||
@click="toggleModal('voteHistory')"
|
||||
|
@ -345,6 +349,9 @@ export default {
|
|||
this.$store.commit("session/setMarkedPlayer", -1);
|
||||
}
|
||||
},
|
||||
toggleTimer() {
|
||||
this.$store.commit("toggleTimer");
|
||||
},
|
||||
...mapMutations([
|
||||
"toggleGrimoire",
|
||||
"toggleMenu",
|
||||
|
|
|
@ -46,7 +46,12 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="fabled" :class="{ closed: !isFabledOpen }" v-if="fabled.length">
|
||||
<div id="top-left">
|
||||
<div
|
||||
class="fabled"
|
||||
:class="{ closed: !isFabledOpen }"
|
||||
v-if="fabled.length"
|
||||
>
|
||||
<h3>
|
||||
<span>Fabled</span>
|
||||
<font-awesome-icon icon="times-circle" @click.stop="toggleFabled" />
|
||||
|
@ -81,6 +86,9 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<CountdownTimer v-if="grimoire.isTimerEnabled" />
|
||||
</div>
|
||||
|
||||
<ReminderModal :player-index="selectedPlayer"></ReminderModal>
|
||||
<RoleModal :player-index="selectedPlayer"></RoleModal>
|
||||
</div>
|
||||
|
@ -90,6 +98,7 @@
|
|||
import { mapGetters, mapState } from "vuex";
|
||||
import Player from "./Player";
|
||||
import Token from "./Token";
|
||||
import CountdownTimer from "./CountdownTimer";
|
||||
import ReminderModal from "./modals/ReminderModal";
|
||||
import RoleModal from "./modals/RoleModal";
|
||||
|
||||
|
@ -97,6 +106,7 @@ export default {
|
|||
components: {
|
||||
Player,
|
||||
Token,
|
||||
CountdownTimer,
|
||||
RoleModal,
|
||||
ReminderModal
|
||||
},
|
||||
|
@ -394,16 +404,32 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
/***** Demon bluffs / Fabled *******/
|
||||
#townsquare > .bluffs,
|
||||
#townsquare > .fabled {
|
||||
#top-left {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/***** Demon bluffs / Fabled / Countdown Timer *******/
|
||||
#townsquare > .bluffs,
|
||||
#top-left > .fabled,
|
||||
#top-left > .countdown-timer {
|
||||
&.bluffs {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
}
|
||||
&.fabled {
|
||||
top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
&.countdown-timer {
|
||||
width: 100%;
|
||||
}
|
||||
margin: 5px 0 0 0;
|
||||
left: 10px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
|
|
|
@ -27,8 +27,12 @@ const faIcons = [
|
|||
"Heartbeat",
|
||||
"Image",
|
||||
"Link",
|
||||
"Minus",
|
||||
"MinusCircle",
|
||||
"Pause",
|
||||
"PeopleArrows",
|
||||
"Play",
|
||||
"Plus",
|
||||
"PlusCircle",
|
||||
"Question",
|
||||
"Random",
|
||||
|
@ -37,6 +41,7 @@ const faIcons = [
|
|||
"SearchPlus",
|
||||
"Skull",
|
||||
"Square",
|
||||
"Stop",
|
||||
"TheaterMasks",
|
||||
"Times",
|
||||
"TimesCircle",
|
||||
|
|
|
@ -105,9 +105,15 @@ export default new Vuex.Store({
|
|||
isStatic: false,
|
||||
isMuted: false,
|
||||
isImageOptIn: false,
|
||||
isTimerEnabled: false,
|
||||
zoom: 0,
|
||||
background: ""
|
||||
},
|
||||
countdownTimer: {
|
||||
totalSeconds: 300,
|
||||
remainingSeconds: 300,
|
||||
isTicking: false
|
||||
},
|
||||
modals: {
|
||||
edition: false,
|
||||
fabled: false,
|
||||
|
@ -171,6 +177,7 @@ export default new Vuex.Store({
|
|||
toggleNight: toggle("isNight"),
|
||||
toggleGrimoire: toggle("isPublic"),
|
||||
toggleImageOptIn: toggle("isImageOptIn"),
|
||||
toggleTimer: toggle("isTimerEnabled"),
|
||||
toggleModal({ modals }, name) {
|
||||
if (name) {
|
||||
modals[name] = !modals[name];
|
||||
|
@ -260,6 +267,14 @@ export default new Vuex.Store({
|
|||
state.edition = edition;
|
||||
}
|
||||
state.modals.edition = false;
|
||||
},
|
||||
/**
|
||||
* Set timer state
|
||||
* @param state
|
||||
* @param payload Object with keys: `remainingSeconds`, `totalSeconds`, and `isTicking`
|
||||
*/
|
||||
setTimerState(state, payload) {
|
||||
state.countdownTimer = payload;
|
||||
}
|
||||
},
|
||||
plugins: [persistence, socket]
|
||||
|
|
|
@ -27,7 +27,12 @@ const state = () => ({
|
|||
voteHistory: [],
|
||||
markedPlayer: -1,
|
||||
isVoteHistoryAllowed: true,
|
||||
isRolesDistributed: false
|
||||
isRolesDistributed: false,
|
||||
countdownTimer: {
|
||||
totalSeconds: 300,
|
||||
remainingSeconds: 300,
|
||||
isTicking: false
|
||||
}
|
||||
});
|
||||
|
||||
const getters = {};
|
||||
|
@ -94,6 +99,12 @@ const mutations = {
|
|||
clearVoteHistory(state) {
|
||||
state.voteHistory = [];
|
||||
},
|
||||
updateTimerState(state, payload) {
|
||||
state.countdownTimer = payload;
|
||||
},
|
||||
distributeTimerAction(state, payload) {
|
||||
state.countdownTimer = payload;
|
||||
},
|
||||
/**
|
||||
* Store a vote with and without syncing it to the live session.
|
||||
* This is necessary in order to prevent infinite voting loops.
|
||||
|
|
|
@ -205,6 +205,13 @@ class LiveSession {
|
|||
case "pronouns":
|
||||
this._updatePlayerPronouns(params);
|
||||
break;
|
||||
case "timer":
|
||||
this._handleTimerAction(params);
|
||||
break;
|
||||
case "isTimerEnabled":
|
||||
if (!this._isSpectator) return;
|
||||
this._store.commit("toggleTimer", params);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,6 +291,8 @@ class LiveSession {
|
|||
isVoteInProgress: session.isVoteInProgress,
|
||||
markedPlayer: session.markedPlayer,
|
||||
fabled: fabled.map(f => (f.isCustom ? f : { id: f.id })),
|
||||
isTimerEnabled: grimoire.isTimerEnabled,
|
||||
countdownTimer: session.countdownTimer,
|
||||
...(session.nomination ? { votes: session.votes } : {})
|
||||
});
|
||||
}
|
||||
|
@ -307,7 +316,9 @@ class LiveSession {
|
|||
lockedVote,
|
||||
isVoteInProgress,
|
||||
markedPlayer,
|
||||
fabled
|
||||
fabled,
|
||||
isTimerEnabled,
|
||||
countdownTimer
|
||||
} = data;
|
||||
const players = this._store.state.players.players;
|
||||
// adjust number of players
|
||||
|
@ -365,6 +376,8 @@ class LiveSession {
|
|||
this._store.commit("players/setFabled", {
|
||||
fabled: fabled.map(f => this._store.state.fabled.get(f.id) || f)
|
||||
});
|
||||
this._store.commit("toggleTimer", isTimerEnabled);
|
||||
this._store.commit("setTimerState", countdownTimer);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -668,6 +681,25 @@ class LiveSession {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute new timer action to each player.
|
||||
*/
|
||||
distributeTimerAction(payload) {
|
||||
if (this._isSpectator) {
|
||||
return;
|
||||
}
|
||||
this._send("timer", payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a timer action.
|
||||
* @param payload
|
||||
* @private
|
||||
*/
|
||||
_handleTimerAction(payload) {
|
||||
this._store.commit("setTimerState", payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* A player nomination. ST only
|
||||
* This also syncs the voting speed to the players.
|
||||
|
@ -703,6 +735,14 @@ class LiveSession {
|
|||
this._send("isNight", this._store.state.grimoire.isNight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the isTimerEnabled status. ST only
|
||||
*/
|
||||
setIsTimerEnabled() {
|
||||
if (this._isSpectator) return;
|
||||
this._send("isTimerEnabled", this._store.state.grimoire.isTimerEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the isVoteHistoryAllowed state. ST only
|
||||
*/
|
||||
|
@ -859,6 +899,11 @@ export default store => {
|
|||
session.distributeRoles();
|
||||
}
|
||||
break;
|
||||
case "session/distributeTimerAction":
|
||||
if (payload) {
|
||||
session.distributeTimerAction(payload);
|
||||
}
|
||||
break;
|
||||
case "session/nomination":
|
||||
case "session/setNomination":
|
||||
session.nomination(payload);
|
||||
|
@ -914,6 +959,9 @@ export default store => {
|
|||
session.sendPlayer(payload);
|
||||
}
|
||||
break;
|
||||
case "toggleTimer":
|
||||
session.setIsTimerEnabled();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue