diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml
index 3d1655b..75e9d23 100644
--- a/.github/workflows/changelog-check.yml
+++ b/.github/workflows/changelog-check.yml
@@ -4,6 +4,7 @@ on:
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
branches:
- main
+ - develop
jobs:
build:
name: Check Actions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 2837f65..3b4c242 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,10 +13,10 @@ name: "CodeQL"
on:
push:
- branches: [ main ]
+ branches: [ main, develop ]
pull_request:
# The branches below must be a subset of the branches above
- branches: [ main ]
+ branches: [ main, develop ]
schedule:
- cron: '27 22 * * 1'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 634fcae..8a6f498 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
# Release Notes
- use "Exile" rather than "Banishment" for exiles
+- added global animation toggle for better performance
+- added record vote history toggle to session menu, and clear vote history button
+- add support for custom Fabled characters
+
+---
### Version 2.12.0
- tweak reference sheet to better fit screen in single column layout
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 95796e5..badc122 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,6 +19,9 @@ Before submitting your contribution, please make sure to take a moment and read
- The `main` branch is what is currently deployed to the website. All development should be done in dedicated branches.
+- The `develop` branch contains the changes that will be deployed to main next. In order to prepare a release, development
+ branches should have their Pull Request against `develop` and only releases should be merged from `develop` into `main`.
+
- Work in the `src` folder and **DO NOT** checkin `dist` in the commits.
- It's OK to have multiple small commits as you work on the PR - GitHub will automatically squash it before merging.
@@ -30,6 +33,9 @@ Before submitting your contribution, please make sure to take a moment and read
- If fixing a bug:
- If you are resolving a special issue, add `(fix #xxxx[,#xxxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `update entities encoding/decoding (fix #3899)`.
- Provide a detailed description of the bug in the PR. Live demo preferred.
+
+- You'll need to update the `CHANGELOG.md` with a description of your changes before you open a pull request and your code
+ should pass the lint check.
## Development Setup
diff --git a/README.md b/README.md
index f4587c4..e67ae5b 100644
--- a/README.md
+++ b/README.md
@@ -96,7 +96,8 @@ For base game characters, it is sufficient to only provide the ID, similar to wh
- **remindersGlobal**: global reminder tokens that will always be available, no matter if the character is assigned to a player or not
- **setup**: whether this token affects setup (orange leaf), like the Drunk or Baron
- **name**: the displayed name of this character
-- **team**: the team of the character, has to be one of `townsfolk`, `outsider`, `minion`, `demon` or `traveler`
+- **team**: the team of the character, has to be one of `townsfolk`, `outsider`, `minion`, `demon`, `traveler` or `fabled`
+ _Note_: if you create a custom Fabled character, it will be automatically added to the game when the custom script is loaded
- **ability**: the displayed ability text of the character
## [Code of Conduct](CODE_OF_CONDUCT.md)
diff --git a/package-lock.json b/package-lock.json
index f4661ef..e227db4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
"name": "townsquare",
- "version": "2.12.0",
+ "version": "2.13.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "version": "2.12.0",
+ "version": "2.13.0",
"license": "GPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32",
@@ -1628,9 +1628,9 @@
}
},
"node_modules/cacache/node_modules/ssri": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
- "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
+ "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dependencies": {
"figgy-pudding": "^3.5.1"
}
@@ -13477,9 +13477,9 @@
},
"dependencies": {
"ssri": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
- "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
+ "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"requires": {
"figgy-pudding": "^3.5.1"
}
diff --git a/package.json b/package.json
index 9e2878c..c53f899 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "townsquare",
- "version": "2.12.0",
+ "version": "2.13.0",
"description": "Blood on the Clocktower Town Square",
"author": "Steffen Baumgart",
"scripts": {
diff --git a/src/App.vue b/src/App.vue
index bb5bc65..6107e30 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -3,7 +3,10 @@
id="app"
@keyup="keyup"
tabindex="-1"
- :class="{ night: grimoire.isNight }"
+ :class="{
+ night: grimoire.isNight,
+ static: grimoire.isStatic
+ }"
:style="{
backgroundImage: grimoire.background
? `url('${grimoire.background}')`
@@ -110,7 +113,7 @@ export default {
this.$store.commit("toggleModal", "roles");
break;
case "v":
- if (this.session.voteHistory.length) {
+ if (this.session.voteHistory.length || !this.session.isSpectator) {
this.$store.commit("toggleModal", "voteHistory");
}
break;
@@ -202,6 +205,14 @@ ul {
align-items: center;
align-content: center;
justify-content: center;
+
+ // disable all animations
+ &.static *,
+ &.static *:after,
+ &.static *:before {
+ transition: none !important;
+ animation: none !important;
+ }
}
#version {
diff --git a/src/assets/icons/fabled.png b/src/assets/icons/fabled.png
new file mode 100644
index 0000000..cf11af2
Binary files /dev/null and b/src/assets/icons/fabled.png differ
diff --git a/src/components/Menu.vue b/src/components/Menu.vue
index 40abc72..e794ae3 100644
--- a/src/components/Menu.vue
+++ b/src/components/Menu.vue
@@ -60,13 +60,14 @@
Night order
-
+
+ />
+
Zoom
@@ -82,6 +83,10 @@
/>
+
+ Background image
+
+
Show Custom Images
-
- Background image
-
+
+ Disable Animations
+
Mute Sounds
@@ -131,10 +139,10 @@
- Nomination history[V]
+ Vote history[V]
Leave Session
@@ -334,6 +342,7 @@ export default {
"toggleMuted",
"toggleNight",
"toggleNightOrder",
+ "toggleStatic",
"setZoom",
"toggleModal"
])
diff --git a/src/components/modals/Modal.vue b/src/components/modals/Modal.vue
index fe08b13..d1bd5e6 100644
--- a/src/components/modals/Modal.vue
+++ b/src/components/modals/Modal.vue
@@ -115,6 +115,7 @@ export default {
.maximized {
background: rgba(0, 0, 0, 0.95);
padding: 0;
+ border-radius: 0;
height: 100%;
width: 100%;
max-width: 100%;
diff --git a/src/components/modals/VoteHistoryModal.vue b/src/components/modals/VoteHistoryModal.vue
index 95eeaa8..882da50 100644
--- a/src/components/modals/VoteHistoryModal.vue
+++ b/src/components/modals/VoteHistoryModal.vue
@@ -1,17 +1,36 @@
- Nomination history
+ Vote history
+
+
+
+
+
+ Accessible to players
+
+
+
+ Clear for everyone
+
+
+
@@ -79,8 +98,16 @@ export default {
...mapState(["session", "modals"])
},
methods: {
- ...mapMutations(["toggleModal"]),
- ...mapMutations("session", ["clearVoteHistory"])
+ clearVoteHistory() {
+ this.$store.commit("session/clearVoteHistory");
+ },
+ setRecordVoteHistory() {
+ this.$store.commit(
+ "session/setVoteHistoryAllowed",
+ !this.session.isVoteHistoryAllowed
+ );
+ },
+ ...mapMutations(["toggleModal"])
}
};
@@ -98,6 +125,24 @@ export default {
}
}
+.options {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ justify-content: center;
+ align-content: center;
+}
+
+.option {
+ color: white;
+ text-decoration: none;
+ margin: 0 15px;
+ &:hover {
+ color: red;
+ cursor: pointer;
+ }
+}
+
h3 {
margin: 0 40px 0 10px;
svg {
diff --git a/src/store/index.js b/src/store/index.js
index 06a03be..057744a 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -81,6 +81,7 @@ export default new Vuex.Store({
isNightOrder: true,
isPublic: true,
isMenuOpen: false,
+ isStatic: false,
isMuted: false,
isImageOptIn: false,
zoom: 0,
@@ -144,6 +145,7 @@ export default new Vuex.Store({
toggleMuted: toggle("isMuted"),
toggleMenu: toggle("isMenuOpen"),
toggleNightOrder: toggle("isNightOrder"),
+ toggleStatic: toggle("isStatic"),
toggleNight: toggle("isNight"),
toggleGrimoire: toggle("isPublic"),
toggleImageOptIn: toggle("isImageOptIn"),
@@ -162,54 +164,62 @@ export default new Vuex.Store({
* @param roles Array of role IDs or full role definitions
*/
setCustomRoles(state, roles) {
- state.roles = new Map(
- roles
- // replace numerical role object keys with matching key names
- .map(role => {
- if (role[0]) {
- const customKeys = Object.keys(customRole);
- const mappedRole = {};
- for (let prop in role) {
- if (customKeys[prop]) {
- mappedRole[customKeys[prop]] = role[prop];
- }
+ const processedRoles = roles
+ // replace numerical role object keys with matching key names
+ .map(role => {
+ if (role[0]) {
+ const customKeys = Object.keys(customRole);
+ const mappedRole = {};
+ for (let prop in role) {
+ if (customKeys[prop]) {
+ mappedRole[customKeys[prop]] = role[prop];
}
- return mappedRole;
- } else {
- return role;
}
- })
- // clean up role.id
- .map(role => {
- role.id = role.id.toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
+ return mappedRole;
+ } else {
return role;
- })
- // map existing roles to base definition or pre-populate custom roles to ensure all properties
- .map(
- role =>
- rolesJSONbyId.get(role.id) ||
- state.roles.get(role.id) ||
- Object.assign({}, customRole, role)
- )
- // default empty icons and placeholders
- .map(role => {
- if (rolesJSONbyId.get(role.id)) return role;
- role.imageAlt = // map team to generic icon
- {
- townsfolk: "good",
- outsider: "outsider",
- minion: "minion",
- demon: "evil"
- }[role.team] || "custom";
- return role;
- })
- // filter out roles that don't match an existing role and also don't have name/ability/team
- .filter(role => role.name && role.ability && role.team)
- // sort by team
- .sort((a, b) => b.team.localeCompare(a.team))
- // convert to Map
+ }
+ })
+ // clean up role.id
+ .map(role => {
+ role.id = role.id.toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
+ return role;
+ })
+ // map existing roles to base definition or pre-populate custom roles to ensure all properties
+ .map(
+ role =>
+ rolesJSONbyId.get(role.id) ||
+ state.roles.get(role.id) ||
+ Object.assign({}, customRole, role)
+ )
+ // default empty icons and placeholders
+ .map(role => {
+ if (rolesJSONbyId.get(role.id)) return role;
+ role.imageAlt = // map team to generic icon
+ {
+ townsfolk: "good",
+ outsider: "outsider",
+ minion: "minion",
+ demon: "evil",
+ fabled: "fabled"
+ }[role.team] || "custom";
+ return role;
+ })
+ // filter out roles that don't match an existing role and also don't have name/ability/team
+ .filter(role => role.name && role.ability && role.team)
+ // sort by team
+ .sort((a, b) => b.team.localeCompare(a.team));
+ // convert to Map without Fabled
+ state.roles = new Map(
+ processedRoles
+ .filter(role => role.team !== "fabled")
.map(role => [role.id, role])
);
+ // update Fabled to include custom Fabled from this script
+ state.fabled = new Map([
+ ...processedRoles.filter(r => r.team === "fabled").map(r => [r.id, r]),
+ ...fabledJSON.map(role => [role.id, role])
+ ]);
// update extraTravelers map to only show travelers not in this script
state.otherTravelers = new Map(
rolesJSON
diff --git a/src/store/modules/session.js b/src/store/modules/session.js
index 01f225f..c3fc909 100644
--- a/src/store/modules/session.js
+++ b/src/store/modules/session.js
@@ -25,6 +25,7 @@ const state = () => ({
votingSpeed: 3000,
isVoteInProgress: false,
voteHistory: [],
+ isVoteHistoryAllowed: true,
isRolesDistributed: false
});
@@ -45,6 +46,7 @@ const mutations = {
setPing: set("ping"),
setVotingSpeed: set("votingSpeed"),
setVoteInProgress: set("isVoteInProgress"),
+ setVoteHistoryAllowed: set("isVoteHistoryAllowed"),
claimSeat: set("claimedSeat"),
distributeRoles: set("isRolesDistributed"),
setSessionId(state, sessionId) {
@@ -70,6 +72,7 @@ const mutations = {
* @param players
*/
addHistory(state, players) {
+ if (!state.isVoteHistoryAllowed && state.isSpectator) return;
if (!state.nomination || state.lockedVote <= players.length) return;
const isExile = players[state.nomination[1]].role.team === "traveler";
state.voteHistory.push({
diff --git a/src/store/persistence.js b/src/store/persistence.js
index 55c2b78..15fc503 100644
--- a/src/store/persistence.js
+++ b/src/store/persistence.js
@@ -11,6 +11,9 @@ module.exports = store => {
if (localStorage.getItem("muted")) {
store.commit("toggleMuted", true);
}
+ if (localStorage.getItem("static")) {
+ store.commit("toggleStatic", true);
+ }
if (localStorage.getItem("imageOptIn")) {
store.commit("toggleImageOptIn", true);
}
@@ -39,8 +42,8 @@ module.exports = store => {
}
if (localStorage.fabled !== undefined) {
store.commit("players/setFabled", {
- fabled: JSON.parse(localStorage.fabled).map(id =>
- store.state.fabled.get(id)
+ fabled: JSON.parse(localStorage.fabled).map(
+ fabled => store.state.fabled.get(fabled.id) || fabled
)
});
}
@@ -91,6 +94,13 @@ module.exports = store => {
localStorage.removeItem("muted");
}
break;
+ case "toggleStatic":
+ if (state.grimoire.isStatic) {
+ localStorage.setItem("static", 1);
+ } else {
+ localStorage.removeItem("static");
+ }
+ break;
case "toggleImageOptIn":
if (state.grimoire.isImageOptIn) {
localStorage.setItem("imageOptIn", 1);
@@ -127,7 +137,11 @@ module.exports = store => {
case "players/setFabled":
localStorage.setItem(
"fabled",
- JSON.stringify(state.players.fabled.map(({ id }) => id))
+ JSON.stringify(
+ state.players.fabled.map(fabled =>
+ fabled.isCustom ? fabled : { id: fabled.id }
+ )
+ )
);
break;
case "players/add":
diff --git a/src/store/socket.js b/src/store/socket.js
index e193a08..86cd610 100644
--- a/src/store/socket.js
+++ b/src/store/socket.js
@@ -172,6 +172,11 @@ class LiveSession {
if (!this._isSpectator) return;
this._store.commit("toggleNight", params);
break;
+ case "isVoteHistoryAllowed":
+ if (!this._isSpectator) return;
+ this._store.commit("session/setVoteHistoryAllowed", params);
+ this._store.commit("session/clearVoteHistory");
+ break;
case "votingSpeed":
if (!this._isSpectator) return;
this._store.commit("session/setVotingSpeed", params);
@@ -268,11 +273,12 @@ class LiveSession {
this._sendDirect(playerId, "gs", {
gamestate: this._gamestate,
isNight: grimoire.isNight,
+ isVoteHistoryAllowed: session.isVoteHistoryAllowed,
nomination: session.nomination,
votingSpeed: session.votingSpeed,
lockedVote: session.lockedVote,
isVoteInProgress: session.isVoteInProgress,
- fabled: fabled.map(({ id }) => id),
+ fabled: fabled.map(f => (f.isCustom ? f : { id: f.id })),
...(session.nomination ? { votes: session.votes } : {})
});
}
@@ -289,6 +295,7 @@ class LiveSession {
gamestate,
isLightweight,
isNight,
+ isVoteHistoryAllowed,
nomination,
votingSpeed,
votes,
@@ -340,6 +347,7 @@ class LiveSession {
});
if (!isLightweight) {
this._store.commit("toggleNight", !!isNight);
+ this._store.commit("session/setVoteHistoryAllowed", isVoteHistoryAllowed);
this._store.commit("session/nomination", {
nomination,
votes,
@@ -348,7 +356,7 @@ class LiveSession {
isVoteInProgress
});
this._store.commit("players/setFabled", {
- fabled: fabled.map(id => this._store.state.fabled.get(id))
+ fabled: fabled.map(f => this._store.state.fabled.get(f.id) || f)
});
}
}
@@ -407,7 +415,7 @@ class LiveSession {
const { fabled } = this._store.state.players;
this._send(
"fabled",
- fabled.map(({ id }) => id)
+ fabled.map(f => (f.isCustom ? f : { id: f.id }))
);
}
@@ -419,7 +427,7 @@ class LiveSession {
_updateFabled(fabled) {
if (!this._isSpectator) return;
this._store.commit("players/setFabled", {
- fabled: fabled.map(id => this._store.state.fabled.get(id))
+ fabled: fabled.map(f => this._store.state.fabled.get(f.id) || f)
});
}
@@ -686,6 +694,17 @@ class LiveSession {
this._send("isNight", this._store.state.grimoire.isNight);
}
+ /**
+ * Send the isVoteHistoryAllowed state. ST only
+ */
+ setVoteHistoryAllowed() {
+ if (this._isSpectator) return;
+ this._send(
+ "isVoteHistoryAllowed",
+ this._store.state.session.isVoteHistoryAllowed
+ );
+ }
+
/**
* Send the voting speed. ST only
* @param votingSpeed voting speed in seconds, minimum 1
@@ -840,6 +859,9 @@ export default store => {
case "session/clearVoteHistory":
session.clearVoteHistory();
break;
+ case "session/setVoteHistoryAllowed":
+ session.setVoteHistoryAllowed();
+ break;
case "toggleNight":
session.setIsNight();
break;