|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "townsquare",
|
"name": "townsquare",
|
||||||
"version": "1.6.0",
|
"version": "1.7.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "townsquare",
|
"name": "townsquare",
|
||||||
"version": "1.6.0",
|
"version": "1.7.1",
|
||||||
"description": "Blood on the Clocktower Town Square",
|
"description": "Blood on the Clocktower Town Square",
|
||||||
"author": "Steffen Baumgart",
|
"author": "Steffen Baumgart",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
10
src/App.vue
|
@ -18,6 +18,7 @@
|
||||||
<TownSquare @screenshot="takeScreenshot"></TownSquare>
|
<TownSquare @screenshot="takeScreenshot"></TownSquare>
|
||||||
<Menu ref="menu"></Menu>
|
<Menu ref="menu"></Menu>
|
||||||
<EditionModal />
|
<EditionModal />
|
||||||
|
<FabledModal />
|
||||||
<RolesModal />
|
<RolesModal />
|
||||||
<ReferenceModal />
|
<ReferenceModal />
|
||||||
<NightOrderModal />
|
<NightOrderModal />
|
||||||
|
@ -39,9 +40,11 @@ import ReferenceModal from "./components/modals/ReferenceModal";
|
||||||
import Vote from "./components/Vote";
|
import Vote from "./components/Vote";
|
||||||
import Gradients from "./components/Gradients";
|
import Gradients from "./components/Gradients";
|
||||||
import NightOrderModal from "./components/modals/NightOrderModal";
|
import NightOrderModal from "./components/modals/NightOrderModal";
|
||||||
|
import FabledModal from "@/components/modals/FabledModal";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
FabledModal,
|
||||||
NightOrderModal,
|
NightOrderModal,
|
||||||
Vote,
|
Vote,
|
||||||
ReferenceModal,
|
ReferenceModal,
|
||||||
|
@ -190,12 +193,15 @@ body {
|
||||||
|
|
||||||
// odd aspect ratio
|
// odd aspect ratio
|
||||||
@media (max-aspect-ratio: 11/7) {
|
@media (max-aspect-ratio: 11/7) {
|
||||||
.bluffs h3 {
|
.bluffs,
|
||||||
|
.fabled {
|
||||||
|
h3 {
|
||||||
max-width: 14vh;
|
max-width: 14vh;
|
||||||
}
|
}
|
||||||
.bluffs ul {
|
ul {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Firefox doesn't support screenshot mode yet
|
// Firefox doesn't support screenshot mode yet
|
||||||
|
|
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 174 KiB |
After Width: | Height: | Size: 180 KiB |
After Width: | Height: | Size: 86 KiB |
After Width: | Height: | Size: 203 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 111 KiB |
After Width: | Height: | Size: 160 KiB |
After Width: | Height: | Size: 169 KiB |
After Width: | Height: | Size: 227 KiB |
After Width: | Height: | Size: 136 KiB |
After Width: | Height: | Size: 134 KiB |
|
@ -54,6 +54,10 @@
|
||||||
]"
|
]"
|
||||||
/></em>
|
/></em>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="!session.isSpectator" @click="toggleModal('fabled')">
|
||||||
|
Add Fabled
|
||||||
|
<em><font-awesome-icon icon="dragon"/></em>
|
||||||
|
</li>
|
||||||
<li v-if="players.length">
|
<li v-if="players.length">
|
||||||
Zoom
|
Zoom
|
||||||
<em>
|
<em>
|
||||||
|
|
|
@ -558,6 +558,7 @@ li.move:not(.from) .player .overlay svg.move {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/****** Session seat glow *****/
|
||||||
@mixin glow($name, $color) {
|
@mixin glow($name, $color) {
|
||||||
@keyframes #{$name}-glow {
|
@keyframes #{$name}-glow {
|
||||||
0% {
|
0% {
|
||||||
|
@ -666,7 +667,7 @@ li.move:not(.from) .player .overlay svg.move {
|
||||||
}
|
}
|
||||||
|
|
||||||
/***** Ability text *****/
|
/***** Ability text *****/
|
||||||
#townsquare.public .ability {
|
#townsquare.public .circle .ability {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.circle .player .shroud:hover ~ .token .ability,
|
.circle .player .shroud:hover ~ .token .ability,
|
||||||
|
|
|
@ -79,8 +79,23 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
transition: border-color 250ms;
|
||||||
|
|
||||||
.icon {
|
&:hover .name .label {
|
||||||
|
stroke: black;
|
||||||
|
fill: white;
|
||||||
|
@-moz-document url-prefix() {
|
||||||
|
&.mozilla {
|
||||||
|
stroke: none;
|
||||||
|
filter: drop-shadow(0 1.5px 0 black) drop-shadow(0 -1.5px 0 black)
|
||||||
|
drop-shadow(1.5px 0 0 black) drop-shadow(-1.5px 0 0 black)
|
||||||
|
drop-shadow(0 2px 2px rgba(0, 0, 0, 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon,
|
||||||
|
&:before {
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center 30%;
|
background-position: center 30%;
|
||||||
|
@ -149,8 +164,9 @@ export default {
|
||||||
// Vue doesn't support scoped media queries, so we have to use a second css class
|
// Vue doesn't support scoped media queries, so we have to use a second css class
|
||||||
stroke: none;
|
stroke: none;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
filter: drop-shadow(0 2px 0 white) drop-shadow(0 -2px 0 white)
|
filter: drop-shadow(0 1.5px 0 white) drop-shadow(0 -1.5px 0 white)
|
||||||
drop-shadow(2px 0 0 white) drop-shadow(-2px 0 0 white);
|
drop-shadow(1.5px 0 0 white) drop-shadow(-1.5px 0 0 white)
|
||||||
|
drop-shadow(0 2px 2px rgba(0, 0, 0, 0.5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bluffs"
|
class="bluffs"
|
||||||
v-if="players.length > 6"
|
v-if="players.length"
|
||||||
ref="bluffs"
|
ref="bluffs"
|
||||||
:class="{ closed: !isBluffsOpen }"
|
:class="{ closed: !isBluffsOpen }"
|
||||||
>
|
>
|
||||||
|
@ -48,6 +48,27 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fabled"
|
||||||
|
:class="{ closed: !isFabledOpen }"
|
||||||
|
v-if="grimoire.fabled.length"
|
||||||
|
>
|
||||||
|
<h3>
|
||||||
|
<span>Fabled</span>
|
||||||
|
<font-awesome-icon icon="times-circle" @click.stop="toggleFabled" />
|
||||||
|
<font-awesome-icon icon="plus-circle" @click.stop="toggleFabled" />
|
||||||
|
</h3>
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
v-for="(fabled, index) in grimoire.fabled"
|
||||||
|
:key="index"
|
||||||
|
@click="removeFabled(index)"
|
||||||
|
>
|
||||||
|
<Token :role="fabled"></Token>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ReminderModal :player-index="selectedPlayer"></ReminderModal>
|
<ReminderModal :player-index="selectedPlayer"></ReminderModal>
|
||||||
<RoleModal :player-index="selectedPlayer"></RoleModal>
|
<RoleModal :player-index="selectedPlayer"></RoleModal>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,7 +99,8 @@ export default {
|
||||||
swap: -1,
|
swap: -1,
|
||||||
move: -1,
|
move: -1,
|
||||||
nominate: -1,
|
nominate: -1,
|
||||||
isBluffsOpen: true
|
isBluffsOpen: true,
|
||||||
|
isFabledOpen: true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -89,6 +111,13 @@ export default {
|
||||||
toggleBluffs() {
|
toggleBluffs() {
|
||||||
this.isBluffsOpen = !this.isBluffsOpen;
|
this.isBluffsOpen = !this.isBluffsOpen;
|
||||||
},
|
},
|
||||||
|
toggleFabled() {
|
||||||
|
this.isFabledOpen = !this.isFabledOpen;
|
||||||
|
},
|
||||||
|
removeFabled(index) {
|
||||||
|
if (this.session.isSpectator) return;
|
||||||
|
this.$store.commit("setFabled", { index });
|
||||||
|
},
|
||||||
handleTrigger(playerIndex, [method, params]) {
|
handleTrigger(playerIndex, [method, params]) {
|
||||||
if (typeof this[method] === "function") {
|
if (typeof this[method] === "function") {
|
||||||
this[method](playerIndex, params);
|
this[method](playerIndex, params);
|
||||||
|
@ -290,10 +319,16 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/***** Demon bluffs *******/
|
/***** Demon bluffs / Fabled *******/
|
||||||
.bluffs {
|
.bluffs,
|
||||||
|
.fabled {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
&.bluffs {
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
&.fabled {
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
left: 10px;
|
left: 10px;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
@ -305,7 +340,7 @@ export default {
|
||||||
transition: all 200ms ease-in-out;
|
transition: all 200ms ease-in-out;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
|
|
||||||
#townsquare.public & {
|
#townsquare.public &.bluffs {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.1);
|
transform: scale(0.1);
|
||||||
}
|
}
|
||||||
|
@ -378,7 +413,22 @@ export default {
|
||||||
ul li {
|
ul li {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
.token {
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fabled ul li .token:before {
|
||||||
|
content: " ";
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 250ms;
|
||||||
|
background-image: url("../assets/icons/x.png");
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#townsquare:not(.spectator) .fabled ul li:hover .token:before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -88,24 +88,24 @@ export default {
|
||||||
"https://gist.githubusercontent.com/bra1n/0337cc44c6fd2c44f7589256ed5486d2/raw/4a7a1545004620146f47583cde4b05f77dd9b6d2/penanceday.json"
|
"https://gist.githubusercontent.com/bra1n/0337cc44c6fd2c44f7589256ed5486d2/raw/4a7a1545004620146f47583cde4b05f77dd9b6d2/penanceday.json"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Catfishing 8.0 (+Sentinel)",
|
"Catfishing 8.0",
|
||||||
"https://gist.githubusercontent.com/bra1n/8a5ec41a7bbf945f6b7dfc1cef72b569/raw/a9451def4bb7b3c424426e9524ee94f3ac65dbf4/catfishing.json"
|
"https://gist.githubusercontent.com/bra1n/8a5ec41a7bbf945f6b7dfc1cef72b569/raw/86b2ce5293e7160530f8775b8a7118b2078fdd79/catfishing.json"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"On Thin Ice (Teensyville, +Sentinel)",
|
"On Thin Ice (Teensyville)",
|
||||||
"https://gist.githubusercontent.com/bra1n/8dacd9f2abc6f428331ea1213ab153f5/raw/9758aff4b59965dc7a094db549d950be5a26b571/custom-script.json"
|
"https://gist.githubusercontent.com/bra1n/8dacd9f2abc6f428331ea1213ab153f5/raw/0cacbcaf8ed9bddae0cca25a9ada97e9958d868b/on-thin-ice.json"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Race To The Bottom (Teensyville, +Sentinel, +Doomsayer)",
|
"Race To The Bottom (Teensyville)",
|
||||||
"https://gist.githubusercontent.com/bra1n/63e1354cb3dc9d4032bcd0623dc48888/raw/5be4df8386ec61e3a98c32be77f8cac3f8414379/custom-script.json"
|
"https://gist.githubusercontent.com/bra1n/63e1354cb3dc9d4032bcd0623dc48888/raw/5acb0eedcc0a67a64a99c7e0e6271de0b7b2e1b2/race-to-the-bottom.json"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Frankenstein's Mayor by Ted (Teensyville, +Sentinel)",
|
"Frankenstein's Mayor by Ted (Teensyville)",
|
||||||
"https://gist.githubusercontent.com/bra1n/32c52b422cc01b934a4291eeb81dbcee/raw/3ca5a043c41141ac40667dc15097deb327263268/Frankensteins_Mayor_by_Ted.json"
|
"https://gist.githubusercontent.com/bra1n/32c52b422cc01b934a4291eeb81dbcee/raw/5bf770693bbf7aff5e86601c82ca4af3222f4ba6/Frankensteins_Mayor_by_Ted.json"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Vigormortis High School (Teensyville, +Sentinel)",
|
"Vigormortis High School (Teensyville)",
|
||||||
"https://gist.githubusercontent.com/bra1n/1f65bd4a999524719d5dabe98c3c2d27/raw/f28d3268846c182b2078888122003c6f95c6b2cf/VigormortisHighSchool.json"
|
"https://gist.githubusercontent.com/bra1n/1f65bd4a999524719d5dabe98c3c2d27/raw/22bbec6bf56a51a7459e5ae41ed47e41971c5445/VigormortisHighSchool.json"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -150,14 +150,22 @@ export default {
|
||||||
},
|
},
|
||||||
parseRoles(roles) {
|
parseRoles(roles) {
|
||||||
if (!roles || !roles.length) return;
|
if (!roles || !roles.length) return;
|
||||||
this.$store.commit(
|
const customRoles = roles.map(role => {
|
||||||
"setCustomRoles",
|
|
||||||
roles.map(role => {
|
|
||||||
role.id = role.id.toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
|
role.id = role.id.toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
|
||||||
return role;
|
return role;
|
||||||
})
|
});
|
||||||
);
|
this.$store.commit("setCustomRoles", customRoles);
|
||||||
this.$store.commit("setEdition", "custom");
|
this.$store.commit("setEdition", "custom");
|
||||||
|
// check for fabled and set those too, if present
|
||||||
|
if (customRoles.some(({ id }) => this.$store.state.fabled.has(id))) {
|
||||||
|
const fabled = [];
|
||||||
|
customRoles.forEach(({ id }) => {
|
||||||
|
if (this.$store.state.fabled.has(id)) {
|
||||||
|
fabled.push(this.$store.state.fabled.get(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.$store.commit("setFabled", { fabled });
|
||||||
|
}
|
||||||
this.isCustom = false;
|
this.isCustom = false;
|
||||||
},
|
},
|
||||||
...mapMutations(["toggleModal", "setEdition"])
|
...mapMutations(["toggleModal", "setEdition"])
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<Modal v-show="modals.fabled && fabled.length" @close="toggleModal('fabled')">
|
||||||
|
<h3>
|
||||||
|
Choose a fabled character to add to the game
|
||||||
|
</h3>
|
||||||
|
<ul class="tokens">
|
||||||
|
<li v-for="role in fabled" v-bind:key="role.id" @click="setFabled(role)">
|
||||||
|
<Token :role="role" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapMutations, mapState } from "vuex";
|
||||||
|
import Modal from "./Modal";
|
||||||
|
import Token from "../Token";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Token, Modal },
|
||||||
|
computed: {
|
||||||
|
...mapState(["modals", "fabled", "grimoire"]),
|
||||||
|
fabled() {
|
||||||
|
const fabled = [];
|
||||||
|
this.$store.state.fabled.forEach(role => {
|
||||||
|
// don't show fabled that are already in play
|
||||||
|
if (
|
||||||
|
!this.$store.state.grimoire.fabled.some(fable => fable.id === role.id)
|
||||||
|
) {
|
||||||
|
fabled.push(role);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return fabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setFabled(role) {
|
||||||
|
this.$store.commit("setFabled", {
|
||||||
|
fabled: role
|
||||||
|
});
|
||||||
|
this.$store.commit("toggleModal", "fabled");
|
||||||
|
},
|
||||||
|
...mapMutations(["toggleModal"])
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "../../vars.scss";
|
||||||
|
|
||||||
|
ul.tokens li {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 8vw;
|
||||||
|
margin: 0.5%;
|
||||||
|
transition: transform 500ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -59,6 +59,16 @@ export default {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.$store.state.grimoire.fabled.forEach(role => {
|
||||||
|
reminders = [
|
||||||
|
...reminders,
|
||||||
|
...role.reminders.map(name => ({
|
||||||
|
role: role.id,
|
||||||
|
image: role.image,
|
||||||
|
name
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
});
|
||||||
reminders.push({ role: "good", name: "Good" });
|
reminders.push({ role: "good", name: "Good" });
|
||||||
reminders.push({ role: "evil", name: "Evil" });
|
reminders.push({ role: "evil", name: "Evil" });
|
||||||
reminders.push({ role: "custom", name: "Custom note" });
|
reminders.push({ role: "custom", name: "Custom note" });
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "doomsayer",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "",
|
||||||
|
"reminders": [],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Doomsayer",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "If 4 or more players live, each living player may publicly choose (once per game) that a player of their own alignment dies."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "angel",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "",
|
||||||
|
"reminders": ["Protect", "Punish"],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Angel",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "Something bad might happen to whoever is most responsible for the death of a new player."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "buddhist",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "",
|
||||||
|
"reminders": [],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Buddhist",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "For the first 2 minutes of each day, veteran players may not talk."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hellslibrarian",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "",
|
||||||
|
"reminders": ["Punish"],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Hell's Librarian",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "Something bad might happen to whoever talks when the Storyteller has asked for silence."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "revolutionary",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "",
|
||||||
|
"reminders": ["Used"],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Revolutionary",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "2 neighboring players are known to be the same alignment. Once per game, one of them registers falsely."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fiddler",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "",
|
||||||
|
"reminders": [],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Fiddler",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "Once per game, the Demon secretly chooses an opposing player: all players choose which of these 2 players win."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "toymaker",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "If it is a night when a Demon attack could end the game, and the Demon is marked “Final night: No Attack,” then the Demon does not act tonight. (Do not wake them.)",
|
||||||
|
"reminders": ["Final Night: No Attack"],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Toymaker",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "The Demon may choose not to attack & must do this at least once per game. Evil players get normal starting info."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fibbin",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "",
|
||||||
|
"reminders": ["Used"],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Fibbin",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "Once per game, 1 good player might get false information."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "duchess",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "Wake each player marked “Visitor” or “False Info” one at a time. Show them the Duchess token, then fingers (1, 2, 3) equaling the number of evil players marked “Visitor” or, if you are waking the player marked “False Info,” show them any number of fingers except the number of evil players marked “Visitor.”",
|
||||||
|
"reminders": ["Visitor", "False Info"],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Duchess",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "Each day, 3 players may choose to visit you. At night*, each visitor learns how many visitors are evil, but 1 gets false info."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sentinel",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "",
|
||||||
|
"reminders": [],
|
||||||
|
"setup": true,
|
||||||
|
"name": "Sentinel",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "There might be 1 extra or 1 fewer Outsider in play."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spiritofivory",
|
||||||
|
"firstNightReminder": "",
|
||||||
|
"otherNightReminder": "",
|
||||||
|
"reminders": ["No extra evil"],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Spirit of Ivory",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "There can't be more than 1 extra evil player."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "djinn",
|
||||||
|
"firstNightReminder": "Wake each evil player and show them which jinxed characters are in play, so that they know not to bluff as characters that can not be in play.",
|
||||||
|
"otherNightReminder": "",
|
||||||
|
"reminders": [],
|
||||||
|
"setup": false,
|
||||||
|
"name": "Djinn",
|
||||||
|
"team": "fabled",
|
||||||
|
"ability": "Jinxed characters are not both in-play, but evil players know which ones are. Or, use the Djinn's \"Special Rule\"."
|
||||||
|
}
|
||||||
|
]
|
|
@ -17,6 +17,7 @@ const faIcons = [
|
||||||
"Cog",
|
"Cog",
|
||||||
"Copy",
|
"Copy",
|
||||||
"Dice",
|
"Dice",
|
||||||
|
"Dragon",
|
||||||
"ExchangeAlt",
|
"ExchangeAlt",
|
||||||
"FileUpload",
|
"FileUpload",
|
||||||
"HandPointRight",
|
"HandPointRight",
|
||||||
|
|
|
@ -6,10 +6,12 @@ import players from "./modules/players";
|
||||||
import session from "./modules/session";
|
import session from "./modules/session";
|
||||||
import editionJSON from "../editions.json";
|
import editionJSON from "../editions.json";
|
||||||
import rolesJSON from "../roles.json";
|
import rolesJSON from "../roles.json";
|
||||||
|
import fabledJSON from "../fabled.json";
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
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 getRolesByEdition = (edition = "tb") => {
|
const getRolesByEdition = (edition = "tb") => {
|
||||||
const selectedEdition =
|
const selectedEdition =
|
||||||
|
@ -55,18 +57,21 @@ export default new Vuex.Store({
|
||||||
isScreenshotSuccess: false,
|
isScreenshotSuccess: false,
|
||||||
zoom: 0,
|
zoom: 0,
|
||||||
background: "",
|
background: "",
|
||||||
bluffs: []
|
bluffs: [],
|
||||||
|
fabled: []
|
||||||
},
|
},
|
||||||
modals: {
|
modals: {
|
||||||
reference: false,
|
|
||||||
edition: false,
|
edition: false,
|
||||||
roles: false,
|
fabled: false,
|
||||||
role: false,
|
nightOrder: false,
|
||||||
|
reference: false,
|
||||||
reminder: false,
|
reminder: false,
|
||||||
nightOrder: false
|
role: false,
|
||||||
|
roles: false
|
||||||
},
|
},
|
||||||
edition: "tb",
|
edition: "tb",
|
||||||
roles: getRolesByEdition()
|
roles: getRolesByEdition(),
|
||||||
|
fabled
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
/**
|
/**
|
||||||
|
@ -126,6 +131,17 @@ export default new Vuex.Store({
|
||||||
grimoire.bluffs = [];
|
grimoire.bluffs = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setFabled({ grimoire }, { index, fabled } = {}) {
|
||||||
|
if (index !== undefined) {
|
||||||
|
grimoire.fabled.splice(index, 1);
|
||||||
|
} else if (fabled) {
|
||||||
|
if (!Array.isArray(fabled)) {
|
||||||
|
grimoire.fabled.push(fabled);
|
||||||
|
} else {
|
||||||
|
grimoire.fabled = fabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
toggleModal({ modals }, name) {
|
toggleModal({ modals }, name) {
|
||||||
if (name) {
|
if (name) {
|
||||||
modals[name] = !modals[name];
|
modals[name] = !modals[name];
|
||||||
|
|
|
@ -24,6 +24,13 @@ module.exports = store => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (localStorage.fabled !== undefined) {
|
||||||
|
store.commit("setFabled", {
|
||||||
|
fabled: JSON.parse(localStorage.fabled).map(id =>
|
||||||
|
store.state.fabled.get(id)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
if (localStorage.players) {
|
if (localStorage.players) {
|
||||||
store.commit(
|
store.commit(
|
||||||
"players/set",
|
"players/set",
|
||||||
|
@ -87,6 +94,12 @@ module.exports = store => {
|
||||||
JSON.stringify(state.grimoire.bluffs.map(({ id }) => id))
|
JSON.stringify(state.grimoire.bluffs.map(({ id }) => id))
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "setFabled":
|
||||||
|
localStorage.setItem(
|
||||||
|
"fabled",
|
||||||
|
JSON.stringify(state.grimoire.fabled.map(({ id }) => id))
|
||||||
|
);
|
||||||
|
break;
|
||||||
case "players/add":
|
case "players/add":
|
||||||
case "players/update":
|
case "players/update":
|
||||||
case "players/remove":
|
case "players/remove":
|
||||||
|
|
|
@ -92,6 +92,9 @@ class LiveSession {
|
||||||
case "edition":
|
case "edition":
|
||||||
this._updateEdition(params);
|
this._updateEdition(params);
|
||||||
break;
|
break;
|
||||||
|
case "fabled":
|
||||||
|
this._updateFabled(params);
|
||||||
|
break;
|
||||||
case "gs":
|
case "gs":
|
||||||
this._updateGamestate(params);
|
this._updateGamestate(params);
|
||||||
break;
|
break;
|
||||||
|
@ -179,6 +182,7 @@ class LiveSession {
|
||||||
}));
|
}));
|
||||||
const { session } = this._store.state;
|
const { session } = this._store.state;
|
||||||
this.sendEdition();
|
this.sendEdition();
|
||||||
|
this.sendFabled();
|
||||||
this._send("gs", {
|
this._send("gs", {
|
||||||
gamestate: this._gamestate,
|
gamestate: this._gamestate,
|
||||||
nomination: session.nomination,
|
nomination: session.nomination,
|
||||||
|
@ -272,6 +276,30 @@ class LiveSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish a fabled update. ST only
|
||||||
|
*/
|
||||||
|
sendFabled() {
|
||||||
|
if (this._isSpectator) return;
|
||||||
|
const { fabled } = this._store.state.grimoire;
|
||||||
|
this._send(
|
||||||
|
"fabled",
|
||||||
|
fabled.map(({ id }) => id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update fabled roles.
|
||||||
|
* @param fabled
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_updateFabled(fabled) {
|
||||||
|
if (!this._isSpectator) return;
|
||||||
|
this._store.commit("setFabled", {
|
||||||
|
fabled: fabled.map(id => this._store.state.fabled.get(id))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publish a player update.
|
* Publish a player update.
|
||||||
* @param player
|
* @param player
|
||||||
|
@ -557,6 +585,9 @@ export default store => {
|
||||||
case "setEdition":
|
case "setEdition":
|
||||||
session.sendEdition();
|
session.sendEdition();
|
||||||
break;
|
break;
|
||||||
|
case "setFabled":
|
||||||
|
session.sendFabled();
|
||||||
|
break;
|
||||||
case "players/swap":
|
case "players/swap":
|
||||||
session.swapPlayer(payload);
|
session.swapPlayer(payload);
|
||||||
break;
|
break;
|
||||||
|
|