diff --git a/CHANGELOG.md b/CHANGELOG.md
index d87c76e..49f2341 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
- added global animation toggle for better performance
- added record vote history toggle to session menu, and clear vote history button
- add support for custom Fabled characters
+- show Jinxed interactions on character reference list
- add 'marked for execution' indicator
---
diff --git a/src/components/modals/ReferenceModal.vue b/src/components/modals/ReferenceModal.vue
index 45936ac..1dd7af1 100644
--- a/src/components/modals/ReferenceModal.vue
+++ b/src/components/modals/ReferenceModal.vue
@@ -50,6 +50,40 @@
+
+
@@ -62,6 +96,27 @@ export default {
Modal
},
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() {
const rolesGrouped = {};
this.roles.forEach(role => {
@@ -85,7 +140,7 @@ export default {
});
return players;
},
- ...mapState(["roles", "modals", "edition", "grimoire"]),
+ ...mapState(["roles", "modals", "edition", "grimoire", "jinxes"]),
...mapState("players", ["players"])
},
methods: {
@@ -147,6 +202,15 @@ h3 {
}
}
+.jinxed {
+ .name {
+ color: $fabled;
+ }
+ aside {
+ background: linear-gradient(-90deg, $fabled, transparent);
+ }
+}
+
.team {
display: flex;
align-items: stretch;
@@ -180,6 +244,12 @@ h3 {
transform-origin: center;
font-size: 80%;
}
+
+ &.jinxed {
+ .icon {
+ margin: 0 -5px;
+ }
+ }
}
ul {
diff --git a/src/hatred.json b/src/hatred.json
new file mode 100644
index 0000000..89e81fa
--- /dev/null
+++ b/src/hatred.json
@@ -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."
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/src/store/index.js b/src/store/index.js
index 057744a..4ab2cdd 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -7,16 +7,10 @@ import session from "./modules/session";
import editionJSON from "../editions.json";
import rolesJSON from "../roles.json";
import fabledJSON from "../fabled.json";
+import jinxesJSON from "../hatred.json";
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
const getRolesByEdition = (edition = editionJSON[0]) => {
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
const customRole = {
id: "",
@@ -101,7 +122,8 @@ export default new Vuex.Store({
edition: editionJSONbyId.get("tb"),
roles: getRolesByEdition(),
otherTravelers: getTravelersNotInEdition(),
- fabled
+ fabled,
+ jinxes
},
getters: {
/**
@@ -182,7 +204,7 @@ export default new Vuex.Store({
})
// clean up role.id
.map(role => {
- role.id = role.id.toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
+ role.id = clean(role.id);
return role;
})
// map existing roles to base definition or pre-populate custom roles to ensure all properties