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