diff --git a/package-lock.json b/package-lock.json index a77b378..b31d715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "townsquare", - "version": "1.6.0", + "version": "1.7.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f46c367..bb85786 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "townsquare", - "version": "1.6.0", + "version": "1.7.1", "description": "Blood on the Clocktower Town Square", "author": "Steffen Baumgart", "scripts": { diff --git a/src/App.vue b/src/App.vue index c52f6ce..f261468 100644 --- a/src/App.vue +++ b/src/App.vue @@ -18,6 +18,7 @@ + @@ -39,9 +40,11 @@ import ReferenceModal from "./components/modals/ReferenceModal"; import Vote from "./components/Vote"; import Gradients from "./components/Gradients"; import NightOrderModal from "./components/modals/NightOrderModal"; +import FabledModal from "@/components/modals/FabledModal"; export default { components: { + FabledModal, NightOrderModal, Vote, ReferenceModal, @@ -190,11 +193,14 @@ body { // odd aspect ratio @media (max-aspect-ratio: 11/7) { - .bluffs h3 { - max-width: 14vh; - } - .bluffs ul { - flex-direction: column; + .bluffs, + .fabled { + h3 { + max-width: 14vh; + } + ul { + flex-direction: column; + } } } diff --git a/src/assets/icons/angel.png b/src/assets/icons/angel.png new file mode 100644 index 0000000..d4b4379 Binary files /dev/null and b/src/assets/icons/angel.png differ diff --git a/src/assets/icons/buddhist.png b/src/assets/icons/buddhist.png new file mode 100644 index 0000000..a137f8c Binary files /dev/null and b/src/assets/icons/buddhist.png differ diff --git a/src/assets/icons/djinn.png b/src/assets/icons/djinn.png new file mode 100644 index 0000000..8dbae4d Binary files /dev/null and b/src/assets/icons/djinn.png differ diff --git a/src/assets/icons/doomsayer.png b/src/assets/icons/doomsayer.png new file mode 100644 index 0000000..f6ff8e0 Binary files /dev/null and b/src/assets/icons/doomsayer.png differ diff --git a/src/assets/icons/duchess.png b/src/assets/icons/duchess.png new file mode 100644 index 0000000..37710dd Binary files /dev/null and b/src/assets/icons/duchess.png differ diff --git a/src/assets/icons/fibbin.png b/src/assets/icons/fibbin.png new file mode 100644 index 0000000..fef52d5 Binary files /dev/null and b/src/assets/icons/fibbin.png differ diff --git a/src/assets/icons/fiddler.png b/src/assets/icons/fiddler.png new file mode 100644 index 0000000..5e3a28a Binary files /dev/null and b/src/assets/icons/fiddler.png differ diff --git a/src/assets/icons/hellslibrarian.png b/src/assets/icons/hellslibrarian.png new file mode 100644 index 0000000..c8996e0 Binary files /dev/null and b/src/assets/icons/hellslibrarian.png differ diff --git a/src/assets/icons/revolutionary.png b/src/assets/icons/revolutionary.png new file mode 100644 index 0000000..071d899 Binary files /dev/null and b/src/assets/icons/revolutionary.png differ diff --git a/src/assets/icons/sentinel.png b/src/assets/icons/sentinel.png new file mode 100644 index 0000000..9f45019 Binary files /dev/null and b/src/assets/icons/sentinel.png differ diff --git a/src/assets/icons/spiritofivory.png b/src/assets/icons/spiritofivory.png new file mode 100644 index 0000000..613e6b2 Binary files /dev/null and b/src/assets/icons/spiritofivory.png differ diff --git a/src/assets/icons/toymaker.png b/src/assets/icons/toymaker.png new file mode 100644 index 0000000..488f241 Binary files /dev/null and b/src/assets/icons/toymaker.png differ diff --git a/src/components/Menu.vue b/src/components/Menu.vue index 481154e..f4cafb4 100644 --- a/src/components/Menu.vue +++ b/src/components/Menu.vue @@ -54,6 +54,10 @@ ]" /> +
  • + Add Fabled + +
  • Zoom diff --git a/src/components/Player.vue b/src/components/Player.vue index 29ae30e..66f0084 100644 --- a/src/components/Player.vue +++ b/src/components/Player.vue @@ -558,6 +558,7 @@ li.move:not(.from) .player .overlay svg.move { } } +/****** Session seat glow *****/ @mixin glow($name, $color) { @keyframes #{$name}-glow { 0% { @@ -666,7 +667,7 @@ li.move:not(.from) .player .overlay svg.move { } /***** Ability text *****/ -#townsquare.public .ability { +#townsquare.public .circle .ability { display: none; } .circle .player .shroud:hover ~ .token .ability, diff --git a/src/components/Token.vue b/src/components/Token.vue index 9315694..e7310e0 100644 --- a/src/components/Token.vue +++ b/src/components/Token.vue @@ -79,8 +79,23 @@ export default { display: flex; align-items: center; justify-content: center; + transition: border-color 250ms; - .icon { + &:hover .name .label { + stroke: black; + fill: white; + @-moz-document url-prefix() { + &.mozilla { + stroke: none; + filter: drop-shadow(0 1.5px 0 black) drop-shadow(0 -1.5px 0 black) + drop-shadow(1.5px 0 0 black) drop-shadow(-1.5px 0 0 black) + drop-shadow(0 2px 2px rgba(0, 0, 0, 0.5)); + } + } + } + + .icon, + &:before { background-size: 100%; background-repeat: no-repeat; background-position: center 30%; @@ -149,8 +164,9 @@ export default { // Vue doesn't support scoped media queries, so we have to use a second css class stroke: none; text-shadow: none; - filter: drop-shadow(0 2px 0 white) drop-shadow(0 -2px 0 white) - drop-shadow(2px 0 0 white) drop-shadow(-2px 0 0 white); + filter: drop-shadow(0 1.5px 0 white) drop-shadow(0 -1.5px 0 white) + drop-shadow(1.5px 0 0 white) drop-shadow(-1.5px 0 0 white) + drop-shadow(0 2px 2px rgba(0, 0, 0, 0.5)); } } } diff --git a/src/components/TownSquare.vue b/src/components/TownSquare.vue index 5debc09..8aacb5a 100644 --- a/src/components/TownSquare.vue +++ b/src/components/TownSquare.vue @@ -26,7 +26,7 @@
    @@ -48,6 +48,27 @@
    +
    +

    + Fabled + + +

    +
      +
    • + +
    • +
    +
    + @@ -78,7 +99,8 @@ export default { swap: -1, move: -1, nominate: -1, - isBluffsOpen: true + isBluffsOpen: true, + isFabledOpen: true }; }, methods: { @@ -89,6 +111,13 @@ export default { toggleBluffs() { this.isBluffsOpen = !this.isBluffsOpen; }, + toggleFabled() { + this.isFabledOpen = !this.isFabledOpen; + }, + removeFabled(index) { + if (this.session.isSpectator) return; + this.$store.commit("setFabled", { index }); + }, handleTrigger(playerIndex, [method, params]) { if (typeof this[method] === "function") { this[method](playerIndex, params); @@ -290,10 +319,16 @@ export default { } } -/***** Demon bluffs *******/ -.bluffs { +/***** Demon bluffs / Fabled *******/ +.bluffs, +.fabled { position: absolute; - bottom: 10px; + &.bluffs { + bottom: 10px; + } + &.fabled { + top: 10px; + } left: 10px; background: rgba(0, 0, 0, 0.5); border-radius: 10px; @@ -305,7 +340,7 @@ export default { transition: all 200ms ease-in-out; z-index: 50; - #townsquare.public & { + #townsquare.public &.bluffs { opacity: 0; transform: scale(0.1); } @@ -378,7 +413,22 @@ export default { ul li { width: 0; height: 0; + .token { + border-width: 0; + } } } } + +.fabled ul li .token:before { + content: " "; + opacity: 0; + transition: opacity 250ms; + background-image: url("../assets/icons/x.png"); + z-index: 2; +} + +#townsquare:not(.spectator) .fabled ul li:hover .token:before { + opacity: 1; +} diff --git a/src/components/modals/EditionModal.vue b/src/components/modals/EditionModal.vue index 779b7ae..d16e050 100644 --- a/src/components/modals/EditionModal.vue +++ b/src/components/modals/EditionModal.vue @@ -88,24 +88,24 @@ export default { "https://gist.githubusercontent.com/bra1n/0337cc44c6fd2c44f7589256ed5486d2/raw/4a7a1545004620146f47583cde4b05f77dd9b6d2/penanceday.json" ], [ - "Catfishing 8.0 (+Sentinel)", - "https://gist.githubusercontent.com/bra1n/8a5ec41a7bbf945f6b7dfc1cef72b569/raw/a9451def4bb7b3c424426e9524ee94f3ac65dbf4/catfishing.json" + "Catfishing 8.0", + "https://gist.githubusercontent.com/bra1n/8a5ec41a7bbf945f6b7dfc1cef72b569/raw/86b2ce5293e7160530f8775b8a7118b2078fdd79/catfishing.json" ], [ - "On Thin Ice (Teensyville, +Sentinel)", - "https://gist.githubusercontent.com/bra1n/8dacd9f2abc6f428331ea1213ab153f5/raw/9758aff4b59965dc7a094db549d950be5a26b571/custom-script.json" + "On Thin Ice (Teensyville)", + "https://gist.githubusercontent.com/bra1n/8dacd9f2abc6f428331ea1213ab153f5/raw/0cacbcaf8ed9bddae0cca25a9ada97e9958d868b/on-thin-ice.json" ], [ - "Race To The Bottom (Teensyville, +Sentinel, +Doomsayer)", - "https://gist.githubusercontent.com/bra1n/63e1354cb3dc9d4032bcd0623dc48888/raw/5be4df8386ec61e3a98c32be77f8cac3f8414379/custom-script.json" + "Race To The Bottom (Teensyville)", + "https://gist.githubusercontent.com/bra1n/63e1354cb3dc9d4032bcd0623dc48888/raw/5acb0eedcc0a67a64a99c7e0e6271de0b7b2e1b2/race-to-the-bottom.json" ], [ - "Frankenstein's Mayor by Ted (Teensyville, +Sentinel)", - "https://gist.githubusercontent.com/bra1n/32c52b422cc01b934a4291eeb81dbcee/raw/3ca5a043c41141ac40667dc15097deb327263268/Frankensteins_Mayor_by_Ted.json" + "Frankenstein's Mayor by Ted (Teensyville)", + "https://gist.githubusercontent.com/bra1n/32c52b422cc01b934a4291eeb81dbcee/raw/5bf770693bbf7aff5e86601c82ca4af3222f4ba6/Frankensteins_Mayor_by_Ted.json" ], [ - "Vigormortis High School (Teensyville, +Sentinel)", - "https://gist.githubusercontent.com/bra1n/1f65bd4a999524719d5dabe98c3c2d27/raw/f28d3268846c182b2078888122003c6f95c6b2cf/VigormortisHighSchool.json" + "Vigormortis High School (Teensyville)", + "https://gist.githubusercontent.com/bra1n/1f65bd4a999524719d5dabe98c3c2d27/raw/22bbec6bf56a51a7459e5ae41ed47e41971c5445/VigormortisHighSchool.json" ] ] }; @@ -150,14 +150,22 @@ export default { }, parseRoles(roles) { if (!roles || !roles.length) return; - this.$store.commit( - "setCustomRoles", - roles.map(role => { - role.id = role.id.toLocaleLowerCase().replace(/[^a-z0-9]/g, ""); - return role; - }) - ); + const customRoles = roles.map(role => { + role.id = role.id.toLocaleLowerCase().replace(/[^a-z0-9]/g, ""); + return role; + }); + this.$store.commit("setCustomRoles", customRoles); this.$store.commit("setEdition", "custom"); + // check for fabled and set those too, if present + if (customRoles.some(({ id }) => this.$store.state.fabled.has(id))) { + const fabled = []; + customRoles.forEach(({ id }) => { + if (this.$store.state.fabled.has(id)) { + fabled.push(this.$store.state.fabled.get(id)); + } + }); + this.$store.commit("setFabled", { fabled }); + } this.isCustom = false; }, ...mapMutations(["toggleModal", "setEdition"]) diff --git a/src/components/modals/FabledModal.vue b/src/components/modals/FabledModal.vue new file mode 100644 index 0000000..bb84209 --- /dev/null +++ b/src/components/modals/FabledModal.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/components/modals/ReminderModal.vue b/src/components/modals/ReminderModal.vue index 036258c..fd4c123 100644 --- a/src/components/modals/ReminderModal.vue +++ b/src/components/modals/ReminderModal.vue @@ -59,6 +59,16 @@ export default { ]; } }); + this.$store.state.grimoire.fabled.forEach(role => { + reminders = [ + ...reminders, + ...role.reminders.map(name => ({ + role: role.id, + image: role.image, + name + })) + ]; + }); reminders.push({ role: "good", name: "Good" }); reminders.push({ role: "evil", name: "Evil" }); reminders.push({ role: "custom", name: "Custom note" }); diff --git a/src/fabled.json b/src/fabled.json new file mode 100644 index 0000000..3a51fdd --- /dev/null +++ b/src/fabled.json @@ -0,0 +1,122 @@ +[ + { + "id": "doomsayer", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Doomsayer", + "team": "fabled", + "ability": "If 4 or more players live, each living player may publicly choose (once per game) that a player of their own alignment dies." + }, + { + "id": "angel", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": ["Protect", "Punish"], + "setup": false, + "name": "Angel", + "team": "fabled", + "ability": "Something bad might happen to whoever is most responsible for the death of a new player." + }, + { + "id": "buddhist", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Buddhist", + "team": "fabled", + "ability": "For the first 2 minutes of each day, veteran players may not talk." + }, + { + "id": "hellslibrarian", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": ["Punish"], + "setup": false, + "name": "Hell's Librarian", + "team": "fabled", + "ability": "Something bad might happen to whoever talks when the Storyteller has asked for silence." + }, + { + "id": "revolutionary", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": ["Used"], + "setup": false, + "name": "Revolutionary", + "team": "fabled", + "ability": "2 neighboring players are known to be the same alignment. Once per game, one of them registers falsely." + }, + { + "id": "fiddler", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Fiddler", + "team": "fabled", + "ability": "Once per game, the Demon secretly chooses an opposing player: all players choose which of these 2 players win." + }, + { + "id": "toymaker", + "firstNightReminder": "", + "otherNightReminder": "If it is a night when a Demon attack could end the game, and the Demon is marked “Final night: No Attack,” then the Demon does not act tonight. (Do not wake them.)", + "reminders": ["Final Night: No Attack"], + "setup": false, + "name": "Toymaker", + "team": "fabled", + "ability": "The Demon may choose not to attack & must do this at least once per game. Evil players get normal starting info." + }, + { + "id": "fibbin", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": ["Used"], + "setup": false, + "name": "Fibbin", + "team": "fabled", + "ability": "Once per game, 1 good player might get false information." + }, + { + "id": "duchess", + "firstNightReminder": "", + "otherNightReminder": "Wake each player marked “Visitor” or “False Info” one at a time. Show them the Duchess token, then fingers (1, 2, 3) equaling the number of evil players marked “Visitor” or, if you are waking the player marked “False Info,” show them any number of fingers except the number of evil players marked “Visitor.”", + "reminders": ["Visitor", "False Info"], + "setup": false, + "name": "Duchess", + "team": "fabled", + "ability": "Each day, 3 players may choose to visit you. At night*, each visitor learns how many visitors are evil, but 1 gets false info." + }, + { + "id": "sentinel", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": true, + "name": "Sentinel", + "team": "fabled", + "ability": "There might be 1 extra or 1 fewer Outsider in play." + }, + { + "id": "spiritofivory", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": ["No extra evil"], + "setup": false, + "name": "Spirit of Ivory", + "team": "fabled", + "ability": "There can't be more than 1 extra evil player." + }, + { + "id": "djinn", + "firstNightReminder": "Wake each evil player and show them which jinxed characters are in play, so that they know not to bluff as characters that can not be in play.", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Djinn", + "team": "fabled", + "ability": "Jinxed characters are not both in-play, but evil players know which ones are. Or, use the Djinn's \"Special Rule\"." + } +] diff --git a/src/main.js b/src/main.js index 8549216..4c2bac5 100644 --- a/src/main.js +++ b/src/main.js @@ -17,6 +17,7 @@ const faIcons = [ "Cog", "Copy", "Dice", + "Dragon", "ExchangeAlt", "FileUpload", "HandPointRight", diff --git a/src/store/index.js b/src/store/index.js index 6a3ce76..7c7fb5b 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -6,10 +6,12 @@ import players from "./modules/players"; import session from "./modules/session"; import editionJSON from "../editions.json"; import rolesJSON from "../roles.json"; +import fabledJSON from "../fabled.json"; Vue.use(Vuex); const rolesJSONbyId = new Map(rolesJSON.map(role => [role.id, role])); +const fabled = new Map(fabledJSON.map(role => [role.id, role])); const getRolesByEdition = (edition = "tb") => { const selectedEdition = @@ -55,18 +57,21 @@ export default new Vuex.Store({ isScreenshotSuccess: false, zoom: 0, background: "", - bluffs: [] + bluffs: [], + fabled: [] }, modals: { - reference: false, edition: false, - roles: false, - role: false, + fabled: false, + nightOrder: false, + reference: false, reminder: false, - nightOrder: false + role: false, + roles: false }, edition: "tb", - roles: getRolesByEdition() + roles: getRolesByEdition(), + fabled }, getters: { /** @@ -126,6 +131,17 @@ export default new Vuex.Store({ grimoire.bluffs = []; } }, + setFabled({ grimoire }, { index, fabled } = {}) { + if (index !== undefined) { + grimoire.fabled.splice(index, 1); + } else if (fabled) { + if (!Array.isArray(fabled)) { + grimoire.fabled.push(fabled); + } else { + grimoire.fabled = fabled; + } + } + }, toggleModal({ modals }, name) { if (name) { modals[name] = !modals[name]; diff --git a/src/store/persistence.js b/src/store/persistence.js index 24b37cc..88e8001 100644 --- a/src/store/persistence.js +++ b/src/store/persistence.js @@ -24,6 +24,13 @@ module.exports = store => { }); }); } + if (localStorage.fabled !== undefined) { + store.commit("setFabled", { + fabled: JSON.parse(localStorage.fabled).map(id => + store.state.fabled.get(id) + ) + }); + } if (localStorage.players) { store.commit( "players/set", @@ -87,6 +94,12 @@ module.exports = store => { JSON.stringify(state.grimoire.bluffs.map(({ id }) => id)) ); break; + case "setFabled": + localStorage.setItem( + "fabled", + JSON.stringify(state.grimoire.fabled.map(({ id }) => id)) + ); + break; case "players/add": case "players/update": case "players/remove": diff --git a/src/store/socket.js b/src/store/socket.js index 1cea03b..0ef97e8 100644 --- a/src/store/socket.js +++ b/src/store/socket.js @@ -92,6 +92,9 @@ class LiveSession { case "edition": this._updateEdition(params); break; + case "fabled": + this._updateFabled(params); + break; case "gs": this._updateGamestate(params); break; @@ -179,6 +182,7 @@ class LiveSession { })); const { session } = this._store.state; this.sendEdition(); + this.sendFabled(); this._send("gs", { gamestate: this._gamestate, nomination: session.nomination, @@ -272,6 +276,30 @@ class LiveSession { } } + /** + * Publish a fabled update. ST only + */ + sendFabled() { + if (this._isSpectator) return; + const { fabled } = this._store.state.grimoire; + this._send( + "fabled", + fabled.map(({ id }) => id) + ); + } + + /** + * Update fabled roles. + * @param fabled + * @private + */ + _updateFabled(fabled) { + if (!this._isSpectator) return; + this._store.commit("setFabled", { + fabled: fabled.map(id => this._store.state.fabled.get(id)) + }); + } + /** * Publish a player update. * @param player @@ -557,6 +585,9 @@ export default store => { case "setEdition": session.sendEdition(); break; + case "setFabled": + session.sendFabled(); + break; case "players/swap": session.swapPlayer(payload); break;