mirror of
				https://github.com/bra1n/townsquare.git
				synced 2025-10-21 16:55:12 +00:00 
			
		
		
		
	Merge pull request #113 from bra1n/custom-images
Support custom character icons / edition logos again with opt-in
This commit is contained in:
		
						commit
						729f3981d6
					
				
					 16 changed files with 236 additions and 172 deletions
				
			
		|  | @ -1,5 +1,11 @@ | |||
| # Release Notes | ||||
| 
 | ||||
| ### Version 2.8.0 | ||||
| - added hands-off live session support for homebrew / custom characters again! | ||||
| - added custom image opt-in that will prevent any (potentially malicious / harmful) images from loading until a player manually allows them to | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ## Version 2.7.0 | ||||
| - added support for assigning duplicate characters to more than one player (like Legion) | ||||
| - further live session bandwidth optimizations | ||||
|  |  | |||
							
								
								
									
										2
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -1,6 +1,6 @@ | |||
| { | ||||
|   "name": "townsquare", | ||||
|   "version": "2.7.0", | ||||
|   "version": "2.8.0", | ||||
|   "lockfileVersion": 1, | ||||
|   "requires": true, | ||||
|   "dependencies": { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| { | ||||
|   "name": "townsquare", | ||||
|   "version": "2.7.0", | ||||
|   "version": "2.8.0", | ||||
|   "description": "Blood on the Clocktower Town Square", | ||||
|   "author": "Steffen Baumgart", | ||||
|   "scripts": { | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								src/assets/icons/minion.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/icons/minion.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 123 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/icons/outsider.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/icons/outsider.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 139 KiB | 
|  | @ -69,11 +69,21 @@ | |||
|               /> | ||||
|             </em> | ||||
|           </li> | ||||
|           <li v-if="!edition.isOfficial" @click="imageOptIn"> | ||||
|             <small>Show Custom Images</small> | ||||
|             <em | ||||
|               ><font-awesome-icon | ||||
|                 :icon="[ | ||||
|                   'fas', | ||||
|                   grimoire.isImageOptIn ? 'check-square' : 'square' | ||||
|                 ]" | ||||
|             /></em> | ||||
|           </li> | ||||
|           <li @click="setBackground"> | ||||
|             Background image | ||||
|             <em><font-awesome-icon icon="image"/></em> | ||||
|           </li> | ||||
|           <li @click="toggleMute"> | ||||
|           <li @click="toggleMuted"> | ||||
|             Mute Sounds | ||||
|             <em | ||||
|               ><font-awesome-icon | ||||
|  | @ -83,23 +93,23 @@ | |||
|         </template> | ||||
| 
 | ||||
|         <template v-if="tab === 'session'"> | ||||
|           <!-- Session --> | ||||
|           <li class="headline" v-if="session.sessionId"> | ||||
|             {{ session.isSpectator ? "Playing" : "Hosting" }} | ||||
|           </li> | ||||
|           <li class="headline" v-else> | ||||
|             Live Session | ||||
|           </li> | ||||
|           <li @click="hostSession" v-if="!session.sessionId"> | ||||
|             Host (Storyteller)<em>[H]</em> | ||||
|           </li> | ||||
|           <li @click="joinSession" v-if="!session.sessionId"> | ||||
|             Join (Player)<em>[J]</em> | ||||
|           </li> | ||||
|           <li v-if="session.sessionId && session.ping"> | ||||
|           <template v-if="!session.sessionId"> | ||||
|             <li @click="hostSession">Host (Storyteller)<em>[H]</em></li> | ||||
|             <li @click="joinSession">Join (Player)<em>[J]</em></li> | ||||
|           </template> | ||||
|           <template v-else> | ||||
|             <li v-if="session.ping"> | ||||
|               Delay to {{ session.isSpectator ? "host" : "players" }} | ||||
|               <em>{{ session.ping }}ms</em> | ||||
|             </li> | ||||
|           <li v-if="session.sessionId" @click="copySessionUrl"> | ||||
|             <li @click="copySessionUrl"> | ||||
|               Copy player link | ||||
|               <em><font-awesome-icon icon="copy"/></em> | ||||
|             </li> | ||||
|  | @ -113,11 +123,12 @@ | |||
|             > | ||||
|               Nomination history<em>[V]</em> | ||||
|             </li> | ||||
|           <li @click="leaveSession" v-if="session.sessionId"> | ||||
|             <li @click="leaveSession"> | ||||
|               Leave Session | ||||
|               <em>{{ session.sessionId }}</em> | ||||
|             </li> | ||||
|           </template> | ||||
|         </template> | ||||
| 
 | ||||
|         <template v-if="tab === 'players' && !session.isSpectator"> | ||||
|           <!-- Users --> | ||||
|  | @ -203,7 +214,7 @@ import { mapMutations, mapState } from "vuex"; | |||
| 
 | ||||
| export default { | ||||
|   computed: { | ||||
|     ...mapState(["grimoire", "session"]), | ||||
|     ...mapState(["grimoire", "session", "edition"]), | ||||
|     ...mapState("players", ["players"]) | ||||
|   }, | ||||
|   data() { | ||||
|  | @ -218,9 +229,6 @@ export default { | |||
|         this.$store.commit("setBackground", background); | ||||
|       } | ||||
|     }, | ||||
|     toggleMute() { | ||||
|       this.$store.commit("setIsMuted", !this.grimoire.isMuted); | ||||
|     }, | ||||
|     hostSession() { | ||||
|       if (this.session.sessionId) return; | ||||
|       const sessionId = prompt( | ||||
|  | @ -253,6 +261,13 @@ export default { | |||
|         ); | ||||
|       } | ||||
|     }, | ||||
|     imageOptIn() { | ||||
|       const popup = | ||||
|         "Are you sure you want to allow custom images? A malicious script file author might track your IP address this way."; | ||||
|       if (this.grimoire.isImageOptIn || confirm(popup)) { | ||||
|         this.toggleImageOptIn(); | ||||
|       } | ||||
|     }, | ||||
|     joinSession() { | ||||
|       if (this.session.sessionId) return this.leaveSession(); | ||||
|       let sessionId = prompt( | ||||
|  | @ -302,6 +317,8 @@ export default { | |||
|     ...mapMutations([ | ||||
|       "toggleGrimoire", | ||||
|       "toggleMenu", | ||||
|       "toggleImageOptIn", | ||||
|       "toggleMuted", | ||||
|       "toggleNight", | ||||
|       "toggleNightOrder", | ||||
|       "setZoom", | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ | |||
|       > | ||||
|         <em>{{ nightOrder.get(player).first }}.</em> | ||||
|         <span v-if="player.role.firstNightReminder">{{ | ||||
|           player.role.firstNightReminder | handleEmojis | ||||
|           player.role.firstNightReminder | ||||
|         }}</span> | ||||
|       </div> | ||||
|       <div | ||||
|  | @ -32,7 +32,7 @@ | |||
|       > | ||||
|         <em>{{ nightOrder.get(player).other }}.</em> | ||||
|         <span v-if="player.role.otherNightReminder">{{ | ||||
|           player.role.otherNightReminder | handleEmojis | ||||
|           player.role.otherNightReminder | ||||
|         }}</span> | ||||
|       </div> | ||||
| 
 | ||||
|  | @ -165,8 +165,13 @@ | |||
|         <span | ||||
|           class="icon" | ||||
|           :style="{ | ||||
|             backgroundImage: `url(${reminder.image || | ||||
|               require('../assets/icons/' + reminder.role + '.png')})` | ||||
|             backgroundImage: `url(${ | ||||
|               reminder.image && grimoire.isImageOptIn | ||||
|                 ? reminder.image | ||||
|                 : require('../assets/icons/' + | ||||
|                     (reminder.imageAlt || reminder.role) + | ||||
|                     '.png') | ||||
|             })` | ||||
|           }" | ||||
|         ></span> | ||||
|         <span class="text">{{ reminder.name }}</span> | ||||
|  | @ -226,9 +231,6 @@ export default { | |||
|       isSwap: false | ||||
|     }; | ||||
|   }, | ||||
|   filters: { | ||||
|     handleEmojis: text => text.replace(/:([^: ]+?):/g, "").replace(/ •/g, "\n•") | ||||
|   }, | ||||
|   methods: { | ||||
|     toggleStatus() { | ||||
|       if (this.grimoire.isPublic) { | ||||
|  |  | |||
|  | @ -4,8 +4,11 @@ | |||
|       class="icon" | ||||
|       v-if="role.id" | ||||
|       :style="{ | ||||
|         backgroundImage: `url(${role.image || | ||||
|           require('../assets/icons/' + role.id + '.png')})` | ||||
|         backgroundImage: `url(${ | ||||
|           role.image && grimoire.isImageOptIn | ||||
|             ? role.image | ||||
|             : require('../assets/icons/' + (role.imageAlt || role.id) + '.png') | ||||
|         })` | ||||
|       }" | ||||
|     ></span> | ||||
|     <span | ||||
|  | @ -47,6 +50,8 @@ | |||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { mapState } from "vuex"; | ||||
| 
 | ||||
| export default { | ||||
|   name: "Token", | ||||
|   props: { | ||||
|  | @ -55,6 +60,9 @@ export default { | |||
|       default: () => ({}) | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(["grimoire"]) | ||||
|   }, | ||||
|   data() { | ||||
|     return {}; | ||||
|   }, | ||||
|  |  | |||
|  | @ -4,8 +4,11 @@ | |||
|       class="edition" | ||||
|       :class="['edition-' + edition.id]" | ||||
|       :style="{ | ||||
|         backgroundImage: `url(${edition.logo || | ||||
|           require('../assets/editions/' + edition.id + '.png')})` | ||||
|         backgroundImage: `url(${ | ||||
|           edition.logo && grimoire.isImageOptIn | ||||
|             ? edition.logo | ||||
|             : require('../assets/editions/' + edition.id + '.png') | ||||
|         })` | ||||
|       }" | ||||
|     ></li> | ||||
|     <li v-if="players.length - teams.traveler < 5"> | ||||
|  |  | |||
|  | @ -41,8 +41,13 @@ | |||
|             class="icon" | ||||
|             v-if="role.id" | ||||
|             :style="{ | ||||
|               backgroundImage: `url(${role.image || | ||||
|                 require('../../assets/icons/' + role.id + '.png')})` | ||||
|               backgroundImage: `url(${ | ||||
|                 role.image && grimoire.isImageOptIn | ||||
|                   ? role.image | ||||
|                   : require('../../assets/icons/' + | ||||
|                       (role.imageAlt || role.id) + | ||||
|                       '.png') | ||||
|               })` | ||||
|             }" | ||||
|           ></span> | ||||
|           <span class="reminder" v-if="role.firstNightReminder"> | ||||
|  | @ -61,8 +66,13 @@ | |||
|             class="icon" | ||||
|             v-if="role.id" | ||||
|             :style="{ | ||||
|               backgroundImage: `url(${role.image || | ||||
|                 require('../../assets/icons/' + role.id + '.png')})` | ||||
|               backgroundImage: `url(${ | ||||
|                 role.image && grimoire.isImageOptIn | ||||
|                   ? role.image | ||||
|                   : require('../../assets/icons/' + | ||||
|                       (role.imageAlt || role.id) + | ||||
|                       '.png') | ||||
|               })` | ||||
|             }" | ||||
|           ></span> | ||||
|           <span class="name"> | ||||
|  | @ -107,14 +117,21 @@ export default { | |||
|             name: "Minion info", | ||||
|             firstNight: 2, | ||||
|             team: "minion", | ||||
|             players: this.players.filter(p => p.role.team === "minion") | ||||
|             players: this.players.filter(p => p.role.team === "minion"), | ||||
|             firstNightReminder: | ||||
|               "• If more than one Minion, they all make eye contact with each other. " + | ||||
|               "• Show the “This is the Demon” card. Point to the Demon." | ||||
|           }, | ||||
|           { | ||||
|             id: "evil", | ||||
|             name: "Demon info & bluffs", | ||||
|             firstNight: 4, | ||||
|             team: "demon", | ||||
|             players: this.players.filter(p => p.role.team === "demon") | ||||
|             players: this.players.filter(p => p.role.team === "demon"), | ||||
|             firstNightReminder: | ||||
|               "• Show the “These are your minions” card. Point to each Minion. " + | ||||
|               "• Show the “These characters are not in play” card. Show 3 character tokens of good " + | ||||
|               "characters not in play." | ||||
|           } | ||||
|         ); | ||||
|       } | ||||
|  |  | |||
|  | @ -34,8 +34,13 @@ | |||
|             class="icon" | ||||
|             v-if="role.id" | ||||
|             :style="{ | ||||
|               backgroundImage: `url(${role.image || | ||||
|                 require('../../assets/icons/' + role.id + '.png')})` | ||||
|               backgroundImage: `url(${ | ||||
|                 role.image && grimoire.isImageOptIn | ||||
|                   ? role.image | ||||
|                   : require('../../assets/icons/' + | ||||
|                       (role.imageAlt || role.id) + | ||||
|                       '.png') | ||||
|               })` | ||||
|             }" | ||||
|           ></span> | ||||
|           <span class="ability">{{ role.ability }}</span> | ||||
|  | @ -80,7 +85,7 @@ export default { | |||
|       }); | ||||
|       return players; | ||||
|     }, | ||||
|     ...mapState(["roles", "modals", "edition"]), | ||||
|     ...mapState(["roles", "modals", "edition", "grimoire"]), | ||||
|     ...mapState("players", ["players"]) | ||||
|   }, | ||||
|   methods: { | ||||
|  |  | |||
|  | @ -15,8 +15,13 @@ | |||
|         <span | ||||
|           class="icon" | ||||
|           :style="{ | ||||
|             backgroundImage: `url(${reminder.image || | ||||
|               require('../../assets/icons/' + reminder.role + '.png')})` | ||||
|             backgroundImage: `url(${ | ||||
|               reminder.image && grimoire.isImageOptIn | ||||
|                 ? reminder.image | ||||
|                 : require('../../assets/icons/' + | ||||
|                     (reminder.imageAlt || reminder.role) + | ||||
|                     '.png') | ||||
|             })` | ||||
|           }" | ||||
|         ></span> | ||||
|         <span class="text">{{ reminder.name }}</span> | ||||
|  | @ -29,6 +34,18 @@ | |||
| import Modal from "./Modal"; | ||||
| import { mapMutations, mapState } from "vuex"; | ||||
| 
 | ||||
| /** | ||||
|  * Helper function that maps a reminder name with a role-based object that provides necessary visual data. | ||||
|  * @param role The role for which the reminder should be generated | ||||
|  * @return {function(*): {image: string|string[]|string|*, role: *, name: *, imageAlt: string|*}} | ||||
|  */ | ||||
| const mapReminder = ({ id, image, imageAlt }) => name => ({ | ||||
|   role: id, | ||||
|   image, | ||||
|   imageAlt, | ||||
|   name | ||||
| }); | ||||
| 
 | ||||
| export default { | ||||
|   components: { Modal }, | ||||
|   props: ["playerIndex"], | ||||
|  | @ -39,61 +56,29 @@ export default { | |||
|       this.$store.state.roles.forEach(role => { | ||||
|         // add reminders from player roles | ||||
|         if (players.some(p => p.role.id === role.id)) { | ||||
|           reminders = [ | ||||
|             ...reminders, | ||||
|             ...role.reminders.map(name => ({ | ||||
|               role: role.id, | ||||
|               image: role.image, | ||||
|               name | ||||
|             })) | ||||
|           ]; | ||||
|           reminders = [...reminders, ...role.reminders.map(mapReminder(role))]; | ||||
|         } | ||||
|         // add reminders from bluff/other roles | ||||
|         else if (bluffs.some(bluff => bluff.id === role.id)) { | ||||
|           reminders = [ | ||||
|             ...reminders, | ||||
|             ...role.reminders.map(name => ({ | ||||
|               role: role.id, | ||||
|               image: role.image, | ||||
|               name | ||||
|             })) | ||||
|           ]; | ||||
|           reminders = [...reminders, ...role.reminders.map(mapReminder(role))]; | ||||
|         } | ||||
|         // add global reminders | ||||
|         if (role.remindersGlobal && role.remindersGlobal.length) { | ||||
|           reminders = [ | ||||
|             ...reminders, | ||||
|             ...role.remindersGlobal.map(name => ({ | ||||
|               role: role.id, | ||||
|               image: role.image, | ||||
|               name | ||||
|             })) | ||||
|             ...role.remindersGlobal.map(mapReminder(role)) | ||||
|           ]; | ||||
|         } | ||||
|       }); | ||||
|       // add fabled reminders | ||||
|       this.$store.state.players.fabled.forEach(role => { | ||||
|         reminders = [ | ||||
|           ...reminders, | ||||
|           ...role.reminders.map(name => ({ | ||||
|             role: role.id, | ||||
|             image: role.image, | ||||
|             name | ||||
|           })) | ||||
|         ]; | ||||
|         reminders = [...reminders, ...role.reminders.map(mapReminder(role))]; | ||||
|       }); | ||||
| 
 | ||||
|       // add out of script traveler reminders | ||||
|       this.$store.state.otherTravelers.forEach(role => { | ||||
|         if (players.some(p => p.role.id === role.id)) { | ||||
|           reminders = [ | ||||
|             ...reminders, | ||||
|             ...role.reminders.map(name => ({ | ||||
|               role: role.id, | ||||
|               image: role.image, | ||||
|               name | ||||
|             })) | ||||
|           ]; | ||||
|           reminders = [...reminders, ...role.reminders.map(mapReminder(role))]; | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|  | @ -102,7 +87,7 @@ export default { | |||
|       reminders.push({ role: "custom", name: "Custom note" }); | ||||
|       return reminders; | ||||
|     }, | ||||
|     ...mapState(["modals"]), | ||||
|     ...mapState(["modals", "grimoire"]), | ||||
|     ...mapState("players", ["players"]) | ||||
|   }, | ||||
|   methods: { | ||||
|  |  | |||
|  | @ -10,12 +10,14 @@ import fabledJSON from "../fabled.json"; | |||
| 
 | ||||
| Vue.use(Vuex); | ||||
| 
 | ||||
| // global data maps
 | ||||
| const editionJSONbyId = new Map( | ||||
|   editionJSON.map(edition => [edition.id, edition]) | ||||
| ); | ||||
| const rolesJSONbyId = new Map(rolesJSON.map(role => [role.id, role])); | ||||
| const fabled = new Map(fabledJSON.map(role => [role.id, role])); | ||||
| 
 | ||||
| // helper functions
 | ||||
| const getRolesByEdition = (edition = editionJSON[0]) => { | ||||
|   return new Map( | ||||
|     rolesJSON | ||||
|  | @ -38,11 +40,24 @@ const getTravelersNotInEdition = (edition = editionJSON[0]) => { | |||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const set = key => ({ grimoire }, val) => { | ||||
|   grimoire[key] = val; | ||||
| }; | ||||
| 
 | ||||
| const toggle = key => ({ grimoire }, val) => { | ||||
|   if (val === true || val === false) { | ||||
|     grimoire[key] = val; | ||||
|   } else { | ||||
|     grimoire[key] = !grimoire[key]; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // base definition for custom roles
 | ||||
| const imageBase = | ||||
|   "https://raw.githubusercontent.com/bra1n/townsquare/main/src/assets/icons/"; | ||||
| const customRole = { | ||||
|   id: "", | ||||
|   name: "", | ||||
|   image: "", | ||||
|   ability: "", | ||||
|   edition: "custom", | ||||
|   firstNight: 0, | ||||
|   firstNightReminder: "", | ||||
|  | @ -67,6 +82,7 @@ export default new Vuex.Store({ | |||
|       isPublic: true, | ||||
|       isMenuOpen: false, | ||||
|       isMuted: false, | ||||
|       isImageOptIn: false, | ||||
|       zoom: 0, | ||||
|       background: "" | ||||
|     }, | ||||
|  | @ -88,27 +104,31 @@ export default new Vuex.Store({ | |||
|   }, | ||||
|   getters: { | ||||
|     /** | ||||
|      * Return all custom roles, with default values stripped. | ||||
|      * Return all custom roles, with default values and non-essential data stripped. | ||||
|      * Role object keys will be replaced with a numerical index to conserve bandwidth. | ||||
|      * @param roles | ||||
|      * @returns {[]} | ||||
|      */ | ||||
|     customRoles: ({ roles }) => { | ||||
|     customRolesStripped: ({ roles }) => { | ||||
|       const customRoles = []; | ||||
|       const customKeys = Object.keys(customRole); | ||||
|       const strippedProps = [ | ||||
|         "firstNightReminder", | ||||
|         "otherNightReminder", | ||||
|         "isCustom" | ||||
|       ]; | ||||
|       roles.forEach(role => { | ||||
|         if (!role.isCustom) { | ||||
|           customRoles.push({ id: role.id }); | ||||
|         } else { | ||||
|           const strippedRole = {}; | ||||
|           for (let prop in role) { | ||||
|             const value = role[prop]; | ||||
|             if ( | ||||
|               prop === "image" && | ||||
|               value.toLocaleLowerCase().includes(imageBase) | ||||
|             ) { | ||||
|             if (strippedProps.includes(prop)) { | ||||
|               continue; | ||||
|             } | ||||
|             if (prop !== "isCustom" && value !== customRole[prop]) { | ||||
|               strippedRole[prop] = value; | ||||
|             const value = role[prop]; | ||||
|             if (customKeys.includes(prop) && value !== customRole[prop]) { | ||||
|               strippedRole[customKeys.indexOf(prop)] = value; | ||||
|             } | ||||
|           } | ||||
|           customRoles.push(strippedRole); | ||||
|  | @ -119,38 +139,14 @@ export default new Vuex.Store({ | |||
|     rolesJSONbyId: () => rolesJSONbyId | ||||
|   }, | ||||
|   mutations: { | ||||
|     toggleMenu({ grimoire }) { | ||||
|       grimoire.isMenuOpen = !grimoire.isMenuOpen; | ||||
|     }, | ||||
|     toggleGrimoire({ grimoire }, isPublic) { | ||||
|       if (isPublic === true || isPublic === false) { | ||||
|         grimoire.isPublic = isPublic; | ||||
|       } else { | ||||
|         grimoire.isPublic = !grimoire.isPublic; | ||||
|       } | ||||
|       document.title = `Blood on the Clocktower ${ | ||||
|         grimoire.isPublic ? "Town Square" : "Grimoire" | ||||
|       }`;
 | ||||
|     }, | ||||
|     toggleNight({ grimoire }, isNight) { | ||||
|       if (isNight === true || isNight === false) { | ||||
|         grimoire.isNight = isNight; | ||||
|       } else { | ||||
|         grimoire.isNight = !grimoire.isNight; | ||||
|       } | ||||
|     }, | ||||
|     toggleNightOrder({ grimoire }) { | ||||
|       grimoire.isNightOrder = !grimoire.isNightOrder; | ||||
|     }, | ||||
|     setZoom({ grimoire }, zoom) { | ||||
|       grimoire.zoom = zoom; | ||||
|     }, | ||||
|     setBackground({ grimoire }, background) { | ||||
|       grimoire.background = background; | ||||
|     }, | ||||
|     setIsMuted({ grimoire }, isMuted) { | ||||
|       grimoire.isMuted = isMuted; | ||||
|     }, | ||||
|     setZoom: set("zoom"), | ||||
|     setBackground: set("background"), | ||||
|     toggleMuted: toggle("isMuted"), | ||||
|     toggleMenu: toggle("isMenuOpen"), | ||||
|     toggleNightOrder: toggle("isNightOrder"), | ||||
|     toggleNight: toggle("isNight"), | ||||
|     toggleGrimoire: toggle("isPublic"), | ||||
|     toggleImageOptIn: toggle("isImageOptIn"), | ||||
|     toggleModal({ modals }, name) { | ||||
|       if (name) { | ||||
|         modals[name] = !modals[name]; | ||||
|  | @ -168,6 +164,21 @@ export default new Vuex.Store({ | |||
|     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]; | ||||
|                 } | ||||
|               } | ||||
|               return mappedRole; | ||||
|             } else { | ||||
|               return role; | ||||
|             } | ||||
|           }) | ||||
|           // map existing roles to base definition or pre-populate custom roles to ensure all properties
 | ||||
|           .map( | ||||
|             role => | ||||
|  | @ -175,16 +186,16 @@ export default new Vuex.Store({ | |||
|               state.roles.get(role.id) || | ||||
|               Object.assign({}, customRole, role) | ||||
|           ) | ||||
|           // default empty icons to good / evil / traveler
 | ||||
|           // default empty icons and placeholders
 | ||||
|           .map(role => { | ||||
|             if (rolesJSONbyId.get(role.id)) return role; | ||||
|             if (role.team === "townsfolk" || role.team === "outsider") { | ||||
|               role.image = role.image || imageBase + "good.png"; | ||||
|             } else if (role.team === "demon" || role.team === "minion") { | ||||
|               role.image = role.image || imageBase + "evil.png"; | ||||
|             } else { | ||||
|               role.image = role.image || imageBase + "custom.png"; | ||||
|             } | ||||
|             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
 | ||||
|  |  | |||
|  | @ -1,8 +1,3 @@ | |||
| // helper functions
 | ||||
| const set = key => (state, val) => { | ||||
|   state[key] = val; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Handle a vote request. | ||||
|  * If the vote is from a seat that is already locked, ignore it. | ||||
|  | @ -37,6 +32,11 @@ const getters = {}; | |||
| 
 | ||||
| const actions = {}; | ||||
| 
 | ||||
| // mutations helper functions
 | ||||
| const set = key => (state, val) => { | ||||
|   state[key] = val; | ||||
| }; | ||||
| 
 | ||||
| const mutations = { | ||||
|   setPlayerId: set("playerId"), | ||||
|   setSpectator: set("isSpectator"), | ||||
|  |  | |||
|  | @ -1,16 +1,25 @@ | |||
| module.exports = store => { | ||||
|   const updatePagetitle = isPublic => | ||||
|     (document.title = `Blood on the Clocktower ${ | ||||
|       isPublic ? "Town Square" : "Grimoire" | ||||
|     }`);
 | ||||
| 
 | ||||
|   // initialize data
 | ||||
|   if (localStorage.getItem("background")) { | ||||
|     store.commit("setBackground", localStorage.background); | ||||
|   } | ||||
|   if (localStorage.getItem("muted")) { | ||||
|     store.commit("setIsMuted", true); | ||||
|     store.commit("toggleMuted", true); | ||||
|   } | ||||
|   if (localStorage.getItem("imageOptIn")) { | ||||
|     store.commit("toggleImageOptIn", true); | ||||
|   } | ||||
|   if (localStorage.getItem("zoom")) { | ||||
|     store.commit("setZoom", parseFloat(localStorage.getItem("zoom"))); | ||||
|   } | ||||
|   if (localStorage.isPublic !== undefined) { | ||||
|     store.commit("toggleGrimoire", JSON.parse(localStorage.isPublic)); | ||||
|   if (localStorage.getItem("isGrimoire")) { | ||||
|     store.commit("toggleGrimoire", false); | ||||
|     updatePagetitle(false); | ||||
|   } | ||||
|   if (localStorage.roles !== undefined) { | ||||
|     store.commit("setCustomRoles", JSON.parse(localStorage.roles)); | ||||
|  | @ -61,10 +70,12 @@ module.exports = store => { | |||
|   store.subscribe(({ type, payload }, state) => { | ||||
|     switch (type) { | ||||
|       case "toggleGrimoire": | ||||
|         localStorage.setItem( | ||||
|           "isPublic", | ||||
|           JSON.stringify(state.grimoire.isPublic) | ||||
|         ); | ||||
|         if (!state.grimoire.isPublic) { | ||||
|           localStorage.setItem("isGrimoire", 1); | ||||
|         } else { | ||||
|           localStorage.removeItem("isGrimoire"); | ||||
|         } | ||||
|         updatePagetitle(state.grimoire.isPublic); | ||||
|         break; | ||||
|       case "setBackground": | ||||
|         if (payload) { | ||||
|  | @ -73,13 +84,20 @@ module.exports = store => { | |||
|           localStorage.removeItem("background"); | ||||
|         } | ||||
|         break; | ||||
|       case "setIsMuted": | ||||
|         if (payload) { | ||||
|       case "toggleMuted": | ||||
|         if (state.grimoire.isMuted) { | ||||
|           localStorage.setItem("muted", 1); | ||||
|         } else { | ||||
|           localStorage.removeItem("muted"); | ||||
|         } | ||||
|         break; | ||||
|       case "toggleImageOptIn": | ||||
|         if (state.grimoire.isImageOptIn) { | ||||
|           localStorage.setItem("imageOptIn", 1); | ||||
|         } else { | ||||
|           localStorage.removeItem("imageOptIn"); | ||||
|         } | ||||
|         break; | ||||
|       case "setZoom": | ||||
|         if (payload !== 0) { | ||||
|           localStorage.setItem("zoom", payload); | ||||
|  | @ -97,10 +115,7 @@ module.exports = store => { | |||
|         if (!payload.length) { | ||||
|           localStorage.removeItem("roles"); | ||||
|         } else { | ||||
|           localStorage.setItem( | ||||
|             "roles", | ||||
|             JSON.stringify(store.getters.customRoles) | ||||
|           ); | ||||
|           localStorage.setItem("roles", JSON.stringify(payload)); | ||||
|         } | ||||
|         break; | ||||
|       case "players/setBluff": | ||||
|  |  | |||
|  | @ -354,12 +354,10 @@ class LiveSession { | |||
|     const { edition } = this._store.state; | ||||
|     let roles; | ||||
|     if (!edition.isOfficial) { | ||||
|       roles = Array.from(this._store.state.roles.keys()); | ||||
|       roles = this._store.getters.customRolesStripped; | ||||
|     } | ||||
|     this._sendDirect(playerId, "edition", { | ||||
|       edition: edition.isOfficial | ||||
|         ? { id: edition.id } | ||||
|         : Object.assign({}, edition, { logo: "" }), | ||||
|       edition: edition.isOfficial ? { id: edition.id } : edition, | ||||
|       ...(roles ? { roles } : {}) | ||||
|     }); | ||||
|   } | ||||
|  | @ -374,10 +372,7 @@ class LiveSession { | |||
|     if (!this._isSpectator) return; | ||||
|     this._store.commit("setEdition", edition); | ||||
|     if (roles) { | ||||
|       this._store.commit( | ||||
|         "setCustomRoles", | ||||
|         roles.map(id => ({ id })) | ||||
|       ); | ||||
|       this._store.commit("setCustomRoles", roles); | ||||
|       if (this._store.state.roles.size !== roles.length) { | ||||
|         const missing = []; | ||||
|         roles.forEach(id => { | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue