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 | # 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 | ## Version 2.7.0 | ||||||
| - added support for assigning duplicate characters to more than one player (like Legion) | - added support for assigning duplicate characters to more than one player (like Legion) | ||||||
| - further live session bandwidth optimizations | - further live session bandwidth optimizations | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|   "name": "townsquare", |   "name": "townsquare", | ||||||
|   "version": "2.7.0", |   "version": "2.8.0", | ||||||
|   "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|   "name": "townsquare", |   "name": "townsquare", | ||||||
|   "version": "2.7.0", |   "version": "2.8.0", | ||||||
|   "description": "Blood on the Clocktower Town Square", |   "description": "Blood on the Clocktower Town Square", | ||||||
|   "author": "Steffen Baumgart", |   "author": "Steffen Baumgart", | ||||||
|   "scripts": { |   "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> |             </em> | ||||||
|           </li> |           </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"> |           <li @click="setBackground"> | ||||||
|             Background image |             Background image | ||||||
|             <em><font-awesome-icon icon="image"/></em> |             <em><font-awesome-icon icon="image"/></em> | ||||||
|           </li> |           </li> | ||||||
|           <li @click="toggleMute"> |           <li @click="toggleMuted"> | ||||||
|             Mute Sounds |             Mute Sounds | ||||||
|             <em |             <em | ||||||
|               ><font-awesome-icon |               ><font-awesome-icon | ||||||
|  | @ -83,40 +93,41 @@ | ||||||
|         </template> |         </template> | ||||||
| 
 | 
 | ||||||
|         <template v-if="tab === 'session'"> |         <template v-if="tab === 'session'"> | ||||||
|  |           <!-- Session --> | ||||||
|           <li class="headline" v-if="session.sessionId"> |           <li class="headline" v-if="session.sessionId"> | ||||||
|             {{ session.isSpectator ? "Playing" : "Hosting" }} |             {{ session.isSpectator ? "Playing" : "Hosting" }} | ||||||
|           </li> |           </li> | ||||||
|           <li class="headline" v-else> |           <li class="headline" v-else> | ||||||
|             Live Session |             Live Session | ||||||
|           </li> |           </li> | ||||||
|           <li @click="hostSession" v-if="!session.sessionId"> |           <template v-if="!session.sessionId"> | ||||||
|             Host (Storyteller)<em>[H]</em> |             <li @click="hostSession">Host (Storyteller)<em>[H]</em></li> | ||||||
|           </li> |             <li @click="joinSession">Join (Player)<em>[J]</em></li> | ||||||
|           <li @click="joinSession" v-if="!session.sessionId"> |           </template> | ||||||
|             Join (Player)<em>[J]</em> |           <template v-else> | ||||||
|           </li> |             <li v-if="session.ping"> | ||||||
|           <li v-if="session.sessionId && session.ping"> |               Delay to {{ session.isSpectator ? "host" : "players" }} | ||||||
|             Delay to {{ session.isSpectator ? "host" : "players" }} |               <em>{{ session.ping }}ms</em> | ||||||
|             <em>{{ session.ping }}ms</em> |             </li> | ||||||
|           </li> |             <li @click="copySessionUrl"> | ||||||
|           <li v-if="session.sessionId" @click="copySessionUrl"> |               Copy player link | ||||||
|             Copy player link |               <em><font-awesome-icon icon="copy"/></em> | ||||||
|             <em><font-awesome-icon icon="copy"/></em> |             </li> | ||||||
|           </li> |             <li v-if="!session.isSpectator" @click="distributeRoles"> | ||||||
|           <li v-if="!session.isSpectator" @click="distributeRoles"> |               Send Characters | ||||||
|             Send Characters |               <em><font-awesome-icon icon="theater-masks"/></em> | ||||||
|             <em><font-awesome-icon icon="theater-masks"/></em> |             </li> | ||||||
|           </li> |             <li | ||||||
|           <li |               v-if="session.voteHistory.length" | ||||||
|             v-if="session.voteHistory.length" |               @click="toggleModal('voteHistory')" | ||||||
|             @click="toggleModal('voteHistory')" |             > | ||||||
|           > |               Nomination history<em>[V]</em> | ||||||
|             Nomination history<em>[V]</em> |             </li> | ||||||
|           </li> |             <li @click="leaveSession"> | ||||||
|           <li @click="leaveSession" v-if="session.sessionId"> |               Leave Session | ||||||
|             Leave Session |               <em>{{ session.sessionId }}</em> | ||||||
|             <em>{{ session.sessionId }}</em> |             </li> | ||||||
|           </li> |           </template> | ||||||
|         </template> |         </template> | ||||||
| 
 | 
 | ||||||
|         <template v-if="tab === 'players' && !session.isSpectator"> |         <template v-if="tab === 'players' && !session.isSpectator"> | ||||||
|  | @ -203,7 +214,7 @@ import { mapMutations, mapState } from "vuex"; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapState(["grimoire", "session"]), |     ...mapState(["grimoire", "session", "edition"]), | ||||||
|     ...mapState("players", ["players"]) |     ...mapState("players", ["players"]) | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|  | @ -218,9 +229,6 @@ export default { | ||||||
|         this.$store.commit("setBackground", background); |         this.$store.commit("setBackground", background); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     toggleMute() { |  | ||||||
|       this.$store.commit("setIsMuted", !this.grimoire.isMuted); |  | ||||||
|     }, |  | ||||||
|     hostSession() { |     hostSession() { | ||||||
|       if (this.session.sessionId) return; |       if (this.session.sessionId) return; | ||||||
|       const sessionId = prompt( |       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() { |     joinSession() { | ||||||
|       if (this.session.sessionId) return this.leaveSession(); |       if (this.session.sessionId) return this.leaveSession(); | ||||||
|       let sessionId = prompt( |       let sessionId = prompt( | ||||||
|  | @ -302,6 +317,8 @@ export default { | ||||||
|     ...mapMutations([ |     ...mapMutations([ | ||||||
|       "toggleGrimoire", |       "toggleGrimoire", | ||||||
|       "toggleMenu", |       "toggleMenu", | ||||||
|  |       "toggleImageOptIn", | ||||||
|  |       "toggleMuted", | ||||||
|       "toggleNight", |       "toggleNight", | ||||||
|       "toggleNightOrder", |       "toggleNightOrder", | ||||||
|       "setZoom", |       "setZoom", | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ | ||||||
|       > |       > | ||||||
|         <em>{{ nightOrder.get(player).first }}.</em> |         <em>{{ nightOrder.get(player).first }}.</em> | ||||||
|         <span v-if="player.role.firstNightReminder">{{ |         <span v-if="player.role.firstNightReminder">{{ | ||||||
|           player.role.firstNightReminder | handleEmojis |           player.role.firstNightReminder | ||||||
|         }}</span> |         }}</span> | ||||||
|       </div> |       </div> | ||||||
|       <div |       <div | ||||||
|  | @ -32,7 +32,7 @@ | ||||||
|       > |       > | ||||||
|         <em>{{ nightOrder.get(player).other }}.</em> |         <em>{{ nightOrder.get(player).other }}.</em> | ||||||
|         <span v-if="player.role.otherNightReminder">{{ |         <span v-if="player.role.otherNightReminder">{{ | ||||||
|           player.role.otherNightReminder | handleEmojis |           player.role.otherNightReminder | ||||||
|         }}</span> |         }}</span> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|  | @ -165,8 +165,13 @@ | ||||||
|         <span |         <span | ||||||
|           class="icon" |           class="icon" | ||||||
|           :style="{ |           :style="{ | ||||||
|             backgroundImage: `url(${reminder.image || |             backgroundImage: `url(${ | ||||||
|               require('../assets/icons/' + reminder.role + '.png')})` |               reminder.image && grimoire.isImageOptIn | ||||||
|  |                 ? reminder.image | ||||||
|  |                 : require('../assets/icons/' + | ||||||
|  |                     (reminder.imageAlt || reminder.role) + | ||||||
|  |                     '.png') | ||||||
|  |             })` | ||||||
|           }" |           }" | ||||||
|         ></span> |         ></span> | ||||||
|         <span class="text">{{ reminder.name }}</span> |         <span class="text">{{ reminder.name }}</span> | ||||||
|  | @ -226,9 +231,6 @@ export default { | ||||||
|       isSwap: false |       isSwap: false | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   filters: { |  | ||||||
|     handleEmojis: text => text.replace(/:([^: ]+?):/g, "").replace(/ •/g, "\n•") |  | ||||||
|   }, |  | ||||||
|   methods: { |   methods: { | ||||||
|     toggleStatus() { |     toggleStatus() { | ||||||
|       if (this.grimoire.isPublic) { |       if (this.grimoire.isPublic) { | ||||||
|  |  | ||||||
|  | @ -4,8 +4,11 @@ | ||||||
|       class="icon" |       class="icon" | ||||||
|       v-if="role.id" |       v-if="role.id" | ||||||
|       :style="{ |       :style="{ | ||||||
|         backgroundImage: `url(${role.image || |         backgroundImage: `url(${ | ||||||
|           require('../assets/icons/' + role.id + '.png')})` |           role.image && grimoire.isImageOptIn | ||||||
|  |             ? role.image | ||||||
|  |             : require('../assets/icons/' + (role.imageAlt || role.id) + '.png') | ||||||
|  |         })` | ||||||
|       }" |       }" | ||||||
|     ></span> |     ></span> | ||||||
|     <span |     <span | ||||||
|  | @ -47,6 +50,8 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
|  | import { mapState } from "vuex"; | ||||||
|  | 
 | ||||||
| export default { | export default { | ||||||
|   name: "Token", |   name: "Token", | ||||||
|   props: { |   props: { | ||||||
|  | @ -55,6 +60,9 @@ export default { | ||||||
|       default: () => ({}) |       default: () => ({}) | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapState(["grimoire"]) | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return {}; |     return {}; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  | @ -4,8 +4,11 @@ | ||||||
|       class="edition" |       class="edition" | ||||||
|       :class="['edition-' + edition.id]" |       :class="['edition-' + edition.id]" | ||||||
|       :style="{ |       :style="{ | ||||||
|         backgroundImage: `url(${edition.logo || |         backgroundImage: `url(${ | ||||||
|           require('../assets/editions/' + edition.id + '.png')})` |           edition.logo && grimoire.isImageOptIn | ||||||
|  |             ? edition.logo | ||||||
|  |             : require('../assets/editions/' + edition.id + '.png') | ||||||
|  |         })` | ||||||
|       }" |       }" | ||||||
|     ></li> |     ></li> | ||||||
|     <li v-if="players.length - teams.traveler < 5"> |     <li v-if="players.length - teams.traveler < 5"> | ||||||
|  |  | ||||||
|  | @ -41,8 +41,13 @@ | ||||||
|             class="icon" |             class="icon" | ||||||
|             v-if="role.id" |             v-if="role.id" | ||||||
|             :style="{ |             :style="{ | ||||||
|               backgroundImage: `url(${role.image || |               backgroundImage: `url(${ | ||||||
|                 require('../../assets/icons/' + role.id + '.png')})` |                 role.image && grimoire.isImageOptIn | ||||||
|  |                   ? role.image | ||||||
|  |                   : require('../../assets/icons/' + | ||||||
|  |                       (role.imageAlt || role.id) + | ||||||
|  |                       '.png') | ||||||
|  |               })` | ||||||
|             }" |             }" | ||||||
|           ></span> |           ></span> | ||||||
|           <span class="reminder" v-if="role.firstNightReminder"> |           <span class="reminder" v-if="role.firstNightReminder"> | ||||||
|  | @ -61,8 +66,13 @@ | ||||||
|             class="icon" |             class="icon" | ||||||
|             v-if="role.id" |             v-if="role.id" | ||||||
|             :style="{ |             :style="{ | ||||||
|               backgroundImage: `url(${role.image || |               backgroundImage: `url(${ | ||||||
|                 require('../../assets/icons/' + role.id + '.png')})` |                 role.image && grimoire.isImageOptIn | ||||||
|  |                   ? role.image | ||||||
|  |                   : require('../../assets/icons/' + | ||||||
|  |                       (role.imageAlt || role.id) + | ||||||
|  |                       '.png') | ||||||
|  |               })` | ||||||
|             }" |             }" | ||||||
|           ></span> |           ></span> | ||||||
|           <span class="name"> |           <span class="name"> | ||||||
|  | @ -107,14 +117,21 @@ export default { | ||||||
|             name: "Minion info", |             name: "Minion info", | ||||||
|             firstNight: 2, |             firstNight: 2, | ||||||
|             team: "minion", |             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", |             id: "evil", | ||||||
|             name: "Demon info & bluffs", |             name: "Demon info & bluffs", | ||||||
|             firstNight: 4, |             firstNight: 4, | ||||||
|             team: "demon", |             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" |             class="icon" | ||||||
|             v-if="role.id" |             v-if="role.id" | ||||||
|             :style="{ |             :style="{ | ||||||
|               backgroundImage: `url(${role.image || |               backgroundImage: `url(${ | ||||||
|                 require('../../assets/icons/' + role.id + '.png')})` |                 role.image && grimoire.isImageOptIn | ||||||
|  |                   ? role.image | ||||||
|  |                   : require('../../assets/icons/' + | ||||||
|  |                       (role.imageAlt || role.id) + | ||||||
|  |                       '.png') | ||||||
|  |               })` | ||||||
|             }" |             }" | ||||||
|           ></span> |           ></span> | ||||||
|           <span class="ability">{{ role.ability }}</span> |           <span class="ability">{{ role.ability }}</span> | ||||||
|  | @ -80,7 +85,7 @@ export default { | ||||||
|       }); |       }); | ||||||
|       return players; |       return players; | ||||||
|     }, |     }, | ||||||
|     ...mapState(["roles", "modals", "edition"]), |     ...mapState(["roles", "modals", "edition", "grimoire"]), | ||||||
|     ...mapState("players", ["players"]) |     ...mapState("players", ["players"]) | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |  | ||||||
|  | @ -15,8 +15,13 @@ | ||||||
|         <span |         <span | ||||||
|           class="icon" |           class="icon" | ||||||
|           :style="{ |           :style="{ | ||||||
|             backgroundImage: `url(${reminder.image || |             backgroundImage: `url(${ | ||||||
|               require('../../assets/icons/' + reminder.role + '.png')})` |               reminder.image && grimoire.isImageOptIn | ||||||
|  |                 ? reminder.image | ||||||
|  |                 : require('../../assets/icons/' + | ||||||
|  |                     (reminder.imageAlt || reminder.role) + | ||||||
|  |                     '.png') | ||||||
|  |             })` | ||||||
|           }" |           }" | ||||||
|         ></span> |         ></span> | ||||||
|         <span class="text">{{ reminder.name }}</span> |         <span class="text">{{ reminder.name }}</span> | ||||||
|  | @ -29,6 +34,18 @@ | ||||||
| import Modal from "./Modal"; | import Modal from "./Modal"; | ||||||
| import { mapMutations, mapState } from "vuex"; | 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 { | export default { | ||||||
|   components: { Modal }, |   components: { Modal }, | ||||||
|   props: ["playerIndex"], |   props: ["playerIndex"], | ||||||
|  | @ -39,61 +56,29 @@ export default { | ||||||
|       this.$store.state.roles.forEach(role => { |       this.$store.state.roles.forEach(role => { | ||||||
|         // add reminders from player roles |         // add reminders from player roles | ||||||
|         if (players.some(p => p.role.id === role.id)) { |         if (players.some(p => p.role.id === role.id)) { | ||||||
|           reminders = [ |           reminders = [...reminders, ...role.reminders.map(mapReminder(role))]; | ||||||
|             ...reminders, |  | ||||||
|             ...role.reminders.map(name => ({ |  | ||||||
|               role: role.id, |  | ||||||
|               image: role.image, |  | ||||||
|               name |  | ||||||
|             })) |  | ||||||
|           ]; |  | ||||||
|         } |         } | ||||||
|         // add reminders from bluff/other roles |         // add reminders from bluff/other roles | ||||||
|         else if (bluffs.some(bluff => bluff.id === role.id)) { |         else if (bluffs.some(bluff => bluff.id === role.id)) { | ||||||
|           reminders = [ |           reminders = [...reminders, ...role.reminders.map(mapReminder(role))]; | ||||||
|             ...reminders, |  | ||||||
|             ...role.reminders.map(name => ({ |  | ||||||
|               role: role.id, |  | ||||||
|               image: role.image, |  | ||||||
|               name |  | ||||||
|             })) |  | ||||||
|           ]; |  | ||||||
|         } |         } | ||||||
|         // add global reminders |         // add global reminders | ||||||
|         if (role.remindersGlobal && role.remindersGlobal.length) { |         if (role.remindersGlobal && role.remindersGlobal.length) { | ||||||
|           reminders = [ |           reminders = [ | ||||||
|             ...reminders, |             ...reminders, | ||||||
|             ...role.remindersGlobal.map(name => ({ |             ...role.remindersGlobal.map(mapReminder(role)) | ||||||
|               role: role.id, |  | ||||||
|               image: role.image, |  | ||||||
|               name |  | ||||||
|             })) |  | ||||||
|           ]; |           ]; | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|       // add fabled reminders |       // add fabled reminders | ||||||
|       this.$store.state.players.fabled.forEach(role => { |       this.$store.state.players.fabled.forEach(role => { | ||||||
|         reminders = [ |         reminders = [...reminders, ...role.reminders.map(mapReminder(role))]; | ||||||
|           ...reminders, |  | ||||||
|           ...role.reminders.map(name => ({ |  | ||||||
|             role: role.id, |  | ||||||
|             image: role.image, |  | ||||||
|             name |  | ||||||
|           })) |  | ||||||
|         ]; |  | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       // add out of script traveler reminders |       // add out of script traveler reminders | ||||||
|       this.$store.state.otherTravelers.forEach(role => { |       this.$store.state.otherTravelers.forEach(role => { | ||||||
|         if (players.some(p => p.role.id === role.id)) { |         if (players.some(p => p.role.id === role.id)) { | ||||||
|           reminders = [ |           reminders = [...reminders, ...role.reminders.map(mapReminder(role))]; | ||||||
|             ...reminders, |  | ||||||
|             ...role.reminders.map(name => ({ |  | ||||||
|               role: role.id, |  | ||||||
|               image: role.image, |  | ||||||
|               name |  | ||||||
|             })) |  | ||||||
|           ]; |  | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|  | @ -102,7 +87,7 @@ export default { | ||||||
|       reminders.push({ role: "custom", name: "Custom note" }); |       reminders.push({ role: "custom", name: "Custom note" }); | ||||||
|       return reminders; |       return reminders; | ||||||
|     }, |     }, | ||||||
|     ...mapState(["modals"]), |     ...mapState(["modals", "grimoire"]), | ||||||
|     ...mapState("players", ["players"]) |     ...mapState("players", ["players"]) | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |  | ||||||
|  | @ -10,12 +10,14 @@ import fabledJSON from "../fabled.json"; | ||||||
| 
 | 
 | ||||||
| Vue.use(Vuex); | Vue.use(Vuex); | ||||||
| 
 | 
 | ||||||
|  | // global data maps
 | ||||||
| const editionJSONbyId = new Map( | const editionJSONbyId = new Map( | ||||||
|   editionJSON.map(edition => [edition.id, edition]) |   editionJSON.map(edition => [edition.id, edition]) | ||||||
| ); | ); | ||||||
| const rolesJSONbyId = new Map(rolesJSON.map(role => [role.id, role])); | const rolesJSONbyId = new Map(rolesJSON.map(role => [role.id, role])); | ||||||
| const fabled = new Map(fabledJSON.map(role => [role.id, role])); | const fabled = new Map(fabledJSON.map(role => [role.id, role])); | ||||||
| 
 | 
 | ||||||
|  | // helper functions
 | ||||||
| const getRolesByEdition = (edition = editionJSON[0]) => { | const getRolesByEdition = (edition = editionJSON[0]) => { | ||||||
|   return new Map( |   return new Map( | ||||||
|     rolesJSON |     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
 | // base definition for custom roles
 | ||||||
| const imageBase = |  | ||||||
|   "https://raw.githubusercontent.com/bra1n/townsquare/main/src/assets/icons/"; |  | ||||||
| const customRole = { | const customRole = { | ||||||
|  |   id: "", | ||||||
|  |   name: "", | ||||||
|   image: "", |   image: "", | ||||||
|  |   ability: "", | ||||||
|   edition: "custom", |   edition: "custom", | ||||||
|   firstNight: 0, |   firstNight: 0, | ||||||
|   firstNightReminder: "", |   firstNightReminder: "", | ||||||
|  | @ -67,6 +82,7 @@ export default new Vuex.Store({ | ||||||
|       isPublic: true, |       isPublic: true, | ||||||
|       isMenuOpen: false, |       isMenuOpen: false, | ||||||
|       isMuted: false, |       isMuted: false, | ||||||
|  |       isImageOptIn: false, | ||||||
|       zoom: 0, |       zoom: 0, | ||||||
|       background: "" |       background: "" | ||||||
|     }, |     }, | ||||||
|  | @ -88,27 +104,31 @@ export default new Vuex.Store({ | ||||||
|   }, |   }, | ||||||
|   getters: { |   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 |      * @param roles | ||||||
|      * @returns {[]} |      * @returns {[]} | ||||||
|      */ |      */ | ||||||
|     customRoles: ({ roles }) => { |     customRolesStripped: ({ roles }) => { | ||||||
|       const customRoles = []; |       const customRoles = []; | ||||||
|  |       const customKeys = Object.keys(customRole); | ||||||
|  |       const strippedProps = [ | ||||||
|  |         "firstNightReminder", | ||||||
|  |         "otherNightReminder", | ||||||
|  |         "isCustom" | ||||||
|  |       ]; | ||||||
|       roles.forEach(role => { |       roles.forEach(role => { | ||||||
|         if (!role.isCustom) { |         if (!role.isCustom) { | ||||||
|           customRoles.push({ id: role.id }); |           customRoles.push({ id: role.id }); | ||||||
|         } else { |         } else { | ||||||
|           const strippedRole = {}; |           const strippedRole = {}; | ||||||
|           for (let prop in role) { |           for (let prop in role) { | ||||||
|             const value = role[prop]; |             if (strippedProps.includes(prop)) { | ||||||
|             if ( |  | ||||||
|               prop === "image" && |  | ||||||
|               value.toLocaleLowerCase().includes(imageBase) |  | ||||||
|             ) { |  | ||||||
|               continue; |               continue; | ||||||
|             } |             } | ||||||
|             if (prop !== "isCustom" && value !== customRole[prop]) { |             const value = role[prop]; | ||||||
|               strippedRole[prop] = value; |             if (customKeys.includes(prop) && value !== customRole[prop]) { | ||||||
|  |               strippedRole[customKeys.indexOf(prop)] = value; | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|           customRoles.push(strippedRole); |           customRoles.push(strippedRole); | ||||||
|  | @ -119,38 +139,14 @@ export default new Vuex.Store({ | ||||||
|     rolesJSONbyId: () => rolesJSONbyId |     rolesJSONbyId: () => rolesJSONbyId | ||||||
|   }, |   }, | ||||||
|   mutations: { |   mutations: { | ||||||
|     toggleMenu({ grimoire }) { |     setZoom: set("zoom"), | ||||||
|       grimoire.isMenuOpen = !grimoire.isMenuOpen; |     setBackground: set("background"), | ||||||
|     }, |     toggleMuted: toggle("isMuted"), | ||||||
|     toggleGrimoire({ grimoire }, isPublic) { |     toggleMenu: toggle("isMenuOpen"), | ||||||
|       if (isPublic === true || isPublic === false) { |     toggleNightOrder: toggle("isNightOrder"), | ||||||
|         grimoire.isPublic = isPublic; |     toggleNight: toggle("isNight"), | ||||||
|       } else { |     toggleGrimoire: toggle("isPublic"), | ||||||
|         grimoire.isPublic = !grimoire.isPublic; |     toggleImageOptIn: toggle("isImageOptIn"), | ||||||
|       } |  | ||||||
|       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; |  | ||||||
|     }, |  | ||||||
|     toggleModal({ modals }, name) { |     toggleModal({ modals }, name) { | ||||||
|       if (name) { |       if (name) { | ||||||
|         modals[name] = !modals[name]; |         modals[name] = !modals[name]; | ||||||
|  | @ -168,6 +164,21 @@ export default new Vuex.Store({ | ||||||
|     setCustomRoles(state, roles) { |     setCustomRoles(state, roles) { | ||||||
|       state.roles = new Map( |       state.roles = new Map( | ||||||
|         roles |         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 existing roles to base definition or pre-populate custom roles to ensure all properties
 | ||||||
|           .map( |           .map( | ||||||
|             role => |             role => | ||||||
|  | @ -175,16 +186,16 @@ export default new Vuex.Store({ | ||||||
|               state.roles.get(role.id) || |               state.roles.get(role.id) || | ||||||
|               Object.assign({}, customRole, role) |               Object.assign({}, customRole, role) | ||||||
|           ) |           ) | ||||||
|           // default empty icons to good / evil / traveler
 |           // default empty icons and placeholders
 | ||||||
|           .map(role => { |           .map(role => { | ||||||
|             if (rolesJSONbyId.get(role.id)) return role; |             if (rolesJSONbyId.get(role.id)) return role; | ||||||
|             if (role.team === "townsfolk" || role.team === "outsider") { |             role.imageAlt = // map team to generic icon
 | ||||||
|               role.image = role.image || imageBase + "good.png"; |               { | ||||||
|             } else if (role.team === "demon" || role.team === "minion") { |                 townsfolk: "good", | ||||||
|               role.image = role.image || imageBase + "evil.png"; |                 outsider: "outsider", | ||||||
|             } else { |                 minion: "minion", | ||||||
|               role.image = role.image || imageBase + "custom.png"; |                 demon: "evil" | ||||||
|             } |               }[role.team] || "custom"; | ||||||
|             return role; |             return role; | ||||||
|           }) |           }) | ||||||
|           // filter out roles that don't match an existing role and also don't have name/ability/team
 |           // 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. |  * Handle a vote request. | ||||||
|  * If the vote is from a seat that is already locked, ignore it. |  * If the vote is from a seat that is already locked, ignore it. | ||||||
|  | @ -37,6 +32,11 @@ const getters = {}; | ||||||
| 
 | 
 | ||||||
| const actions = {}; | const actions = {}; | ||||||
| 
 | 
 | ||||||
|  | // mutations helper functions
 | ||||||
|  | const set = key => (state, val) => { | ||||||
|  |   state[key] = val; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| const mutations = { | const mutations = { | ||||||
|   setPlayerId: set("playerId"), |   setPlayerId: set("playerId"), | ||||||
|   setSpectator: set("isSpectator"), |   setSpectator: set("isSpectator"), | ||||||
|  |  | ||||||
|  | @ -1,16 +1,25 @@ | ||||||
| module.exports = store => { | module.exports = store => { | ||||||
|  |   const updatePagetitle = isPublic => | ||||||
|  |     (document.title = `Blood on the Clocktower ${ | ||||||
|  |       isPublic ? "Town Square" : "Grimoire" | ||||||
|  |     }`);
 | ||||||
|  | 
 | ||||||
|   // initialize data
 |   // initialize data
 | ||||||
|   if (localStorage.getItem("background")) { |   if (localStorage.getItem("background")) { | ||||||
|     store.commit("setBackground", localStorage.background); |     store.commit("setBackground", localStorage.background); | ||||||
|   } |   } | ||||||
|   if (localStorage.getItem("muted")) { |   if (localStorage.getItem("muted")) { | ||||||
|     store.commit("setIsMuted", true); |     store.commit("toggleMuted", true); | ||||||
|  |   } | ||||||
|  |   if (localStorage.getItem("imageOptIn")) { | ||||||
|  |     store.commit("toggleImageOptIn", true); | ||||||
|   } |   } | ||||||
|   if (localStorage.getItem("zoom")) { |   if (localStorage.getItem("zoom")) { | ||||||
|     store.commit("setZoom", parseFloat(localStorage.getItem("zoom"))); |     store.commit("setZoom", parseFloat(localStorage.getItem("zoom"))); | ||||||
|   } |   } | ||||||
|   if (localStorage.isPublic !== undefined) { |   if (localStorage.getItem("isGrimoire")) { | ||||||
|     store.commit("toggleGrimoire", JSON.parse(localStorage.isPublic)); |     store.commit("toggleGrimoire", false); | ||||||
|  |     updatePagetitle(false); | ||||||
|   } |   } | ||||||
|   if (localStorage.roles !== undefined) { |   if (localStorage.roles !== undefined) { | ||||||
|     store.commit("setCustomRoles", JSON.parse(localStorage.roles)); |     store.commit("setCustomRoles", JSON.parse(localStorage.roles)); | ||||||
|  | @ -61,10 +70,12 @@ module.exports = store => { | ||||||
|   store.subscribe(({ type, payload }, state) => { |   store.subscribe(({ type, payload }, state) => { | ||||||
|     switch (type) { |     switch (type) { | ||||||
|       case "toggleGrimoire": |       case "toggleGrimoire": | ||||||
|         localStorage.setItem( |         if (!state.grimoire.isPublic) { | ||||||
|           "isPublic", |           localStorage.setItem("isGrimoire", 1); | ||||||
|           JSON.stringify(state.grimoire.isPublic) |         } else { | ||||||
|         ); |           localStorage.removeItem("isGrimoire"); | ||||||
|  |         } | ||||||
|  |         updatePagetitle(state.grimoire.isPublic); | ||||||
|         break; |         break; | ||||||
|       case "setBackground": |       case "setBackground": | ||||||
|         if (payload) { |         if (payload) { | ||||||
|  | @ -73,13 +84,20 @@ module.exports = store => { | ||||||
|           localStorage.removeItem("background"); |           localStorage.removeItem("background"); | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       case "setIsMuted": |       case "toggleMuted": | ||||||
|         if (payload) { |         if (state.grimoire.isMuted) { | ||||||
|           localStorage.setItem("muted", 1); |           localStorage.setItem("muted", 1); | ||||||
|         } else { |         } else { | ||||||
|           localStorage.removeItem("muted"); |           localStorage.removeItem("muted"); | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|  |       case "toggleImageOptIn": | ||||||
|  |         if (state.grimoire.isImageOptIn) { | ||||||
|  |           localStorage.setItem("imageOptIn", 1); | ||||||
|  |         } else { | ||||||
|  |           localStorage.removeItem("imageOptIn"); | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|       case "setZoom": |       case "setZoom": | ||||||
|         if (payload !== 0) { |         if (payload !== 0) { | ||||||
|           localStorage.setItem("zoom", payload); |           localStorage.setItem("zoom", payload); | ||||||
|  | @ -97,10 +115,7 @@ module.exports = store => { | ||||||
|         if (!payload.length) { |         if (!payload.length) { | ||||||
|           localStorage.removeItem("roles"); |           localStorage.removeItem("roles"); | ||||||
|         } else { |         } else { | ||||||
|           localStorage.setItem( |           localStorage.setItem("roles", JSON.stringify(payload)); | ||||||
|             "roles", |  | ||||||
|             JSON.stringify(store.getters.customRoles) |  | ||||||
|           ); |  | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       case "players/setBluff": |       case "players/setBluff": | ||||||
|  |  | ||||||
|  | @ -354,12 +354,10 @@ class LiveSession { | ||||||
|     const { edition } = this._store.state; |     const { edition } = this._store.state; | ||||||
|     let roles; |     let roles; | ||||||
|     if (!edition.isOfficial) { |     if (!edition.isOfficial) { | ||||||
|       roles = Array.from(this._store.state.roles.keys()); |       roles = this._store.getters.customRolesStripped; | ||||||
|     } |     } | ||||||
|     this._sendDirect(playerId, "edition", { |     this._sendDirect(playerId, "edition", { | ||||||
|       edition: edition.isOfficial |       edition: edition.isOfficial ? { id: edition.id } : edition, | ||||||
|         ? { id: edition.id } |  | ||||||
|         : Object.assign({}, edition, { logo: "" }), |  | ||||||
|       ...(roles ? { roles } : {}) |       ...(roles ? { roles } : {}) | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | @ -374,10 +372,7 @@ class LiveSession { | ||||||
|     if (!this._isSpectator) return; |     if (!this._isSpectator) return; | ||||||
|     this._store.commit("setEdition", edition); |     this._store.commit("setEdition", edition); | ||||||
|     if (roles) { |     if (roles) { | ||||||
|       this._store.commit( |       this._store.commit("setCustomRoles", roles); | ||||||
|         "setCustomRoles", |  | ||||||
|         roles.map(id => ({ id })) |  | ||||||
|       ); |  | ||||||
|       if (this._store.state.roles.size !== roles.length) { |       if (this._store.state.roles.size !== roles.length) { | ||||||
|         const missing = []; |         const missing = []; | ||||||
|         roles.forEach(id => { |         roles.forEach(id => { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue