mirror of https://github.com/bra1n/townsquare.git
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
|
@ -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
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 123 KiB |
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…
Reference in New Issue