Merge branch 'main' into develop

# Conflicts:
#	CHANGELOG.md
#	src/store/index.js
This commit is contained in:
Steffen 2021-05-25 19:40:20 +02:00
commit 1f4871b3b0
No known key found for this signature in database
GPG Key ID: 764D74E98267DFC6
6 changed files with 271 additions and 12 deletions

View File

@ -1,11 +1,15 @@
# Release Notes # Release Notes
---
### Version 2.13.0
- fix players being moved or removed during nomination - fix players being moved or removed during nomination
- add vue linter - add vue linter
- use "Exile" rather than "Banishment" for exiles - use "Exile" rather than "Banishment" for exiles
- added global animation toggle for better performance - added global animation toggle for better performance
- added record vote history toggle to session menu, and clear vote history button - added record vote history toggle to session menu, and clear vote history button
- add support for custom Fabled characters - add support for custom Fabled characters
- show Jinxed interactions on character reference list
- add 'marked for execution' indicator - add 'marked for execution' indicator
--- ---

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "townsquare", "name": "townsquare",
"version": "2.12.0", "version": "2.13.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@ -1,6 +1,6 @@
{ {
"name": "townsquare", "name": "townsquare",
"version": "2.12.0", "version": "2.13.0",
"description": "Blood on the Clocktower Town Square", "description": "Blood on the Clocktower Town Square",
"author": "Steffen Baumgart", "author": "Steffen Baumgart",
"scripts": { "scripts": {

View File

@ -50,6 +50,40 @@
<li :class="[team]"></li> <li :class="[team]"></li>
</ul> </ul>
</div> </div>
<div class="team jinxed" v-if="jinxed.length">
<aside>
<h4>Jinxed</h4>
</aside>
<ul>
<li v-for="(jinx, index) in jinxed" :key="index">
<span
class="icon"
:style="{
backgroundImage: `url(${require('../../assets/icons/' +
jinx.first.id +
'.png')})`
}"
></span>
<span
class="icon"
:style="{
backgroundImage: `url(${require('../../assets/icons/' +
jinx.second.id +
'.png')})`
}"
></span>
<div class="role">
<span class="name"
>{{ jinx.first.name }} & {{ jinx.second.name }}</span
>
<span class="ability">{{ jinx.reason }}</span>
</div>
</li>
<li></li>
<li></li>
</ul>
</div>
</Modal> </Modal>
</template> </template>
@ -62,6 +96,27 @@ export default {
Modal Modal
}, },
computed: { computed: {
/**
* Return a list of jinxes in the form of role IDs and a reason
* @returns {*[]} [{first, second, reason}]
*/
jinxed: function() {
const jinxed = [];
this.roles.forEach(role => {
if (this.jinxes.get(role.id)) {
this.jinxes.get(role.id).forEach((reason, second) => {
if (this.roles.get(second)) {
jinxed.push({
first: role,
second: this.roles.get(second),
reason
});
}
});
}
});
return jinxed;
},
rolesGrouped: function() { rolesGrouped: function() {
const rolesGrouped = {}; const rolesGrouped = {};
this.roles.forEach(role => { this.roles.forEach(role => {
@ -85,7 +140,7 @@ export default {
}); });
return players; return players;
}, },
...mapState(["roles", "modals", "edition", "grimoire"]), ...mapState(["roles", "modals", "edition", "grimoire", "jinxes"]),
...mapState("players", ["players"]) ...mapState("players", ["players"])
}, },
methods: { methods: {
@ -147,6 +202,15 @@ h3 {
} }
} }
.jinxed {
.name {
color: $fabled;
}
aside {
background: linear-gradient(-90deg, $fabled, transparent);
}
}
.team { .team {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
@ -180,6 +244,12 @@ h3 {
transform-origin: center; transform-origin: center;
font-size: 80%; font-size: 80%;
} }
&.jinxed {
.icon {
margin: 0 -5px;
}
}
} }
ul { ul {

163
src/hatred.json Normal file
View File

@ -0,0 +1,163 @@
[{
"id": "Chambermaid",
"hatred": [{
"id": "Mathematician",
"reason": "The Chambermaid learns if the Mathematician wakes tonight or not, even though the Chambermaid wakes first."
}]
},
{
"id": "Butler",
"hatred": [{
"id": "Cannibal",
"reason": "If the Cannibal gains the Butler ability, the Cannibal learns this."
}]
},
{
"id": "Mutant",
"hatred": [{
"id": "Undertaker",
"reason": "If the Mutant causes a second execution, the Undertaker learns either one or both executed characters (Storyteller's choice)."
}]
},
{
"id": "Lunatic",
"hatred": [{
"id": "Mathematician",
"reason": "The Mathematician learns if the Lunatic attacks a different player(s) than the real Demon attacked."
}]
},
{
"id": "Pit-Hag",
"hatred": [{
"id": "Politician",
"reason": "A Pit-Hag can not create an evil Politician."
},
{
"id": "Heretic",
"reason": "A Pit-Hag can not create a Heretic. "
}
]
},
{
"id": "Cerenovus",
"hatred": [{
"id": "Undertaker",
"reason": "If the Cerenovus causes a second execution, the Undertaker learns either one or both executed characters (Storyteller's choice)."
},
{
"id": "Goblin",
"reason": "The Cerenovus may choose to make a player mad that they are the Goblin."
}
]
},
{
"id": "Leviathan",
"hatred": [{
"id": "Soldier",
"reason": "If Leviathan nominates and executes the Soldier, the Soldier does not die."
},
{
"id": "Monk",
"reason": "If Leviathan nominates and executes the player the Monk chose, that player does not die."
},
{
"id": "Innkeeper",
"reason": "If Leviathan nominates and executes a player the Innkeeper chose, that player does not die."
},
{
"id": "Ravenkeeper",
"reason": "If Leviathan is in play & the Ravenkeeper dies by execution, they wake that night to use their ability."
},
{
"id": "Sage",
"reason": "If Leviathan is in play & the Sage dies by execution, they wake that night to use their ability."
},
{
"id": "Mayor",
"reason": "If Leviathan is in play & no execution occurs on day 5, good wins."
}
]
},
{
"id": "Lil' Monsta",
"hatred": [{
"id": "Scarlet Woman",
"reason": "If there are 5 or more players alive and the player holding the Lil' Monsta token dies, the Scarlet Woman is given the Lil' Monsta token tonight."
},
{
"id": "Poppy Grower",
"reason": "If the Poppy Grower is in play, Minions don't wake together. They are woken one by one, until one of them chooses to take the Lil' Monsta token."
}
]
},
{
"id": "Lycanthrope",
"hatred": [{
"id": "Gambler",
"reason": "If the Lycanthrope is alive and the Gambler kills themself at night, no other players can die tonight."
}]
},
{
"id": "Legion",
"hatred": [{
"id": "Preacher",
"reason": "Only 1 jinxed character can be in play. Evil players start knowing which player and character it is."
}]
},
{
"id": "Fang Gu",
"hatred": [{
"id": "Scarlet Woman",
"reason": "If the Fang Gu chooses an Outsider and dies, the Scarlet Woman does not become the Fang Gu."
}]
},
{
"id": "Spy",
"hatred": [{
"id": "Poppy Grower",
"reason": "If the Poppy Grower is in play, the Spy does not see the Grimoire until the Poppy Grower dies."
},
{
"id": "Heretic",
"reason": "Only 1 jinxed character can be in play."
}
]
},
{
"id": "Widow",
"hatred": [{
"id": "Poppy Grower",
"reason": "If the Poppy Grower is in play, the Widow does not see the Grimoire until the Poppy Grower dies."
},
{
"id": "Heretic",
"reason": "Only 1 jinxed character can be in play."
}
]
},
{
"id": "Godfather",
"hatred": [{
"id": "Heretic",
"reason": "Only 1 jinxed character can be in play."
}]
},
{
"id": "Marionette",
"hatred": [
{
"id": "Lil' Monsta",
"reason": "The Marionette neighbors a Minion, not the Demon. The Marionette is not woken to choose who takes the Lil' Monsta token."
},
{
"id": "Poppy Grower",
"reason": "When the Poppy Grower dies, the Demon learns the Marionette but the Marionette learns nothing."
},
{
"id": "Balloonist",
"reason": "If the Marionette thinks that they are the Balloonist, +1 Outsider was added."
}
]
}
]

View File

@ -7,16 +7,10 @@ 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"; import fabledJSON from "../fabled.json";
import jinxesJSON from "../hatred.json";
Vue.use(Vuex); Vue.use(Vuex);
// global data maps
const editionJSONbyId = new Map(
editionJSON.map(edition => [edition.id, edition])
);
const rolesJSONbyId = new Map(rolesJSON.map(role => [role.id, role]));
const fabled = new Map(fabledJSON.map(role => [role.id, role]));
// helper functions // helper functions
const getRolesByEdition = (edition = editionJSON[0]) => { const getRolesByEdition = (edition = editionJSON[0]) => {
return new Map( return new Map(
@ -52,6 +46,33 @@ const toggle = key => ({ grimoire }, val) => {
} }
}; };
const clean = id => id.toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
// global data maps
const editionJSONbyId = new Map(
editionJSON.map(edition => [edition.id, edition])
);
const rolesJSONbyId = new Map(rolesJSON.map(role => [role.id, role]));
const fabled = new Map(fabledJSON.map(role => [role.id, role]));
// jinxes
let jinxes = {};
try {
// Note: can't fetch live list due to lack of CORS headers
// fetch("https://bloodontheclocktower.com/script/data/hatred.json")
// .then(res => res.json())
// .then(jinxesJSON => {
jinxes = new Map(
jinxesJSON.map(({ id, hatred }) => [
clean(id),
new Map(hatred.map(({ id, reason }) => [clean(id), reason]))
])
);
// });
} catch (e) {
console.error("couldn't load jinxes", e);
}
// base definition for custom roles // base definition for custom roles
const customRole = { const customRole = {
id: "", id: "",
@ -101,7 +122,8 @@ export default new Vuex.Store({
edition: editionJSONbyId.get("tb"), edition: editionJSONbyId.get("tb"),
roles: getRolesByEdition(), roles: getRolesByEdition(),
otherTravelers: getTravelersNotInEdition(), otherTravelers: getTravelersNotInEdition(),
fabled fabled,
jinxes
}, },
getters: { getters: {
/** /**
@ -182,7 +204,7 @@ export default new Vuex.Store({
}) })
// clean up role.id // clean up role.id
.map(role => { .map(role => {
role.id = role.id.toLocaleLowerCase().replace(/[^a-z0-9]/g, ""); role.id = clean(role.id);
return role; 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