diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99577c0..aca7e78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
# Release Notes
+### Version 2.15.4
+- add timer
+
### Version 2.15.3
- add Huntsman/Damsel to list of available characters
diff --git a/src/App.vue b/src/App.vue
index 866d599..4337a69 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -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");
}
diff --git a/src/components/CountdownTimer.vue b/src/components/CountdownTimer.vue
new file mode 100644
index 0000000..9904e33
--- /dev/null
+++ b/src/components/CountdownTimer.vue
@@ -0,0 +1,173 @@
+
+
+
+ {{ formattedMinutes }}:{{ formattedSeconds }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Menu.vue b/src/components/Menu.vue
index 46133ca..d144e3d 100644
--- a/src/components/Menu.vue
+++ b/src/components/Menu.vue
@@ -138,6 +138,10 @@
Send Characters
+
+ Toggle timer
+ [T]
+
-
-
- Fabled
-
-
-
-
- -
-
+
+
+ Fabled
+
+
+
+
+ -
- {{ nightOrder.get(role).first }}.
- {{
- role.firstNightReminder
- }}
-
-
- {{ nightOrder.get(role).other }}.
- {{
- role.otherNightReminder
- }}
-
-
-
-
+
+ {{ nightOrder.get(role).first }}.
+ {{
+ role.firstNightReminder
+ }}
+
+
+ {{ nightOrder.get(role).other }}.
+ {{
+ role.otherNightReminder
+ }}
+
+
+
+
+
+
+
@@ -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;
diff --git a/src/main.js b/src/main.js
index 9d7af31..b7f1a8c 100644
--- a/src/main.js
+++ b/src/main.js
@@ -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",
diff --git a/src/store/index.js b/src/store/index.js
index feb528d..568d5bc 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -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]
diff --git a/src/store/modules/session.js b/src/store/modules/session.js
index 884117a..62f0179 100644
--- a/src/store/modules/session.js
+++ b/src/store/modules/session.js
@@ -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.
diff --git a/src/store/socket.js b/src/store/socket.js
index eea5821..4abee67 100644
--- a/src/store/socket.js
+++ b/src/store/socket.js
@@ -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;
}
});