Merge pull request #56 from bra1n/fabled

Fabled have arrived
This commit is contained in:
Steffen 2020-08-09 22:11:08 +02:00 committed by GitHub
commit ba0ae98757
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 380 additions and 40 deletions

2
package-lock.json generated
View File

@ -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": {

View File

@ -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": {

View File

@ -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

BIN
src/assets/icons/angel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
src/assets/icons/djinn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

BIN
src/assets/icons/fibbin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -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>

View File

@ -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,

View File

@ -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));
} }
} }
} }

View File

@ -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>

View File

@ -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"])

View File

@ -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>

View File

@ -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" });

122
src/fabled.json Normal file
View File

@ -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\"."
}
]

View File

@ -17,6 +17,7 @@ const faIcons = [
"Cog", "Cog",
"Copy", "Copy",
"Dice", "Dice",
"Dragon",
"ExchangeAlt", "ExchangeAlt",
"FileUpload", "FileUpload",
"HandPointRight", "HandPointRight",

View File

@ -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];

View File

@ -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":

View File

@ -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;