diff --git a/hench.mjs b/hench.mjs index bd35beb..026016e 100644 --- a/hench.mjs +++ b/hench.mjs @@ -1,4 +1,4 @@ -import { HenchDataModel } from "./module/data-models.mjs"; +import { BossDataModel, HenchDataModel } from "./module/data-models.mjs"; import { HenchActorSheet } from "./module/sheets/hench-actor-sheet.mjs"; @@ -8,6 +8,15 @@ Handlebars.registerHelper('int2checkbox', (size, threshold, options) => { ).reduce((prev, next) => (prev + next), ""); }); +Handlebars.registerHelper('partialint2checkbox', (size, threshold, start, end, options) => { + const indexBase = start + 1; + const arrSize = Math.max(end - start, 0); + + return Array(arrSize).fill(0).map( + (e, i) => options.fn({ index: i + indexBase, marked: (i + start) < threshold }) + ).reduce((prev, next) => (prev + next), ""); +}); + Handlebars.registerHelper('partialList', (list, start, end, options) => { return list.slice(start, end).map( (e, i) => options.fn({ item: e, index: (start + i)}) @@ -28,6 +37,7 @@ Handlebars.registerHelper('decrement', (value) => (value - 1)); Hooks.once("init", () => { CONFIG.Actor.dataModels = { hench: HenchDataModel, + boss: BossDataModel, }; Actors.unregisterSheet('core', ActorSheet); diff --git a/module/boss.mjs b/module/boss.mjs new file mode 100644 index 0000000..4849721 --- /dev/null +++ b/module/boss.mjs @@ -0,0 +1,142 @@ +export const nullStorylineKey = "FREEFORM"; + +export const storylineKeys = [ + nullStorylineKey, + "BOOTSTRAPPER", + "VENGEANCE", + "DOWNWARD SPIRAL", + "DOMINION", + "CLEANUP CREW" +]; + +export function getBossMutation() { + return bossData; +} + +const bossData = { + details: [ + { + question: "What fuels your quest for villainy?", + answer: "", + }, + { + question: "How do you dress your henches?", + answer: "", + }, + { + question: "How is your lair designed? ", + answer: "", + }, + { + question: "Where do you draw the line to what you will do? ", + answer: "", + }, + ], + moves: [ + { + marked: true, + name: "Force of Will", + description: `Whenever you act, do not draw. Simply declare what you do, and what happens after.`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Extra Armorment", + description: `Write in an additional gear type on each of your henches' sheets. They can take it on any future mission.`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Rotating Armorment I", + description: `Erase the write-in gear from any number of your henches' sheets, and write in a new equipment.`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Rotating Armorment II", + description: `Erase the write-in gear from any number of your henches' sheets, and write in a new equipment.`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Rotating Armorment III", + description: `Erase the write-in gear from any number of your henches' sheets, and write in a new equipment.`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Specialized Training", + description: `Write a new inclination below. It applies for all of your henches.`, + hasWriteIn: true, + writeIn: "", + }, + { + marked: false, + name: "General Training", + description: `When you take this ability, fill the experience track of each of your henches immediately.`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Exceptional Planning", + description: `At the start of each mission, remove one card from the deck. Any time one of your henches draws, you may replace the card they play with the removed card.`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Limelight Lovers", + description: `If your heat is in Tier III, your henches can take 1 stress to take +1 card to any draw (possibly drawing 4).`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Plot Armor", + description: `Once per mission, one of your henches can ignore any single instance of harm they would take.`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Play it Off", + description: `Your henches can resist consequences by taking 1 harm "Laughing Stock", instead of marking stress. They can do this even if all of their stress is marked.`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Guild Bigwig", + description: `During fallout, you may draw a second time and use that card instead. Explain who intervenes on your behalf, and how.`, + hasWriteIn: false, + writeIn: null, + }, + { + marked: false, + name: "Exit Plan", + description: `When you take this move, pick a hench to pass the mantle on to. You retire to peace, and they take your place. Is there any ceremony to the transfer? Who knows, and who doesn't?`, + hasWriteIn: false, + writeIn: null, + }, + ], + experienceTriggers: [ + { + marked: false, + description: "Your henches followed even your most superfluous orders.", + }, + { + marked: false, + description: "You achieved your evil ambitions.", + }, + { + marked: false, + description: "News of your deeds spreads far and wide.", + }, + ] +}; \ No newline at end of file diff --git a/module/data-models.mjs b/module/data-models.mjs index 86a5e21..20f6f22 100644 --- a/module/data-models.mjs +++ b/module/data-models.mjs @@ -1,5 +1,6 @@ const { HTMLField, SchemaField, NumberField, StringField, BooleanField, FilePathField, ArrayField } = foundry.data.fields; +import { getBossMutation, nullStorylineKey, storylineKeys } from './boss.mjs'; import { nullPlaybookKey, playbookKeys, lookupPlaybook, getPlaybookMutation } from './playbooks.mjs'; const textField = () => new StringField({ required: true, blank: true }); @@ -110,4 +111,42 @@ export class HenchDataModel extends foundry.abstract.TypeDataModel { this.customGear, ]; } +} + +export class BossDataModel extends foundry.abstract.TypeDataModel { + static defineSchema() { + return { + look: textField(), + details: cappedArrayField(promptField(), 4), + storyline: new StringField({ required: true, blank: false, initial: nullStorylineKey, options: storylineKeys}), + + heat: new NumberField({ required: true, integer: true, min: 0, initial: 0, max: 18}), + + experienceTriggers: cappedArrayField(markableField(), 4), + experience: new NumberField({ required: true, integer: true, min: 0, initial: 0, max: 5 }), + + moves: cappedArrayField(moveField(), 13), + }; + } + + static migrateData(source) { + // no migrations yet + + return super.migrateData(source); + } + + /** @override */ + async _preCreate(data, options, user) { + await super._preCreate(data, options, user); + + const initMutation = getBossMutation(); + + return this.updateSource(initMutation); + } + + get tier() { + const divisor = 18 / 3; + + return Math.ceil(this.heat / divisor); + } } \ No newline at end of file diff --git a/module/sheets/hench-actor-sheet.mjs b/module/sheets/hench-actor-sheet.mjs index 80ef600..3b20d0e 100644 --- a/module/sheets/hench-actor-sheet.mjs +++ b/module/sheets/hench-actor-sheet.mjs @@ -1,5 +1,6 @@ import { playbookKeys, validatePlaybookKey, getPlaybookMutation } from "../playbooks.mjs"; import { updateField } from "../helpers/mutation-helper.mjs"; +import { storylineKeys } from "../boss.mjs"; export class HenchActorSheet extends ActorSheet { /** @override */ @@ -13,10 +14,12 @@ export class HenchActorSheet extends ActorSheet { const context = super.getData(); context.playbookKeys = playbookKeys.map((k) => ({ key: k, selected: k === this.actor.system.playbook})); + context.storylineKeys = storylineKeys.map((k) => ({ key: k, selected: k === this.actor.system.storyline})); // TODO define system constants for these context.maxStress = 12; context.maxExp = 5; + context.maxHeat = 18; context.minGear = 3; context.maxGear = 5; @@ -54,6 +57,14 @@ export class HenchActorSheet extends ActorSheet { updateField(this.actor, path, value); }); + // normal dropdowns + html.find('.hench-hench-sheet-dropdown').on('change', (event) => { + const value = event.target.value; + const path = event.currentTarget.dataset.fieldPath; + + updateField(this.actor, path, value); + }); + // text fields html.find('.hench-text-input').on('change', async (event) => { const element = event.currentTarget; diff --git a/styles/hench.css b/styles/hench.css index 45458ae..c49a5e5 100644 --- a/styles/hench.css +++ b/styles/hench.css @@ -48,6 +48,11 @@ font-size: 1.5em; } +.hench-huge { + font-size: 3em; + font-weight: 700; +} + /* Flexbox */ .hench-row { display: flex; diff --git a/templates/actors/boss.hbs b/templates/actors/boss.hbs new file mode 100644 index 0000000..27fa5ee --- /dev/null +++ b/templates/actors/boss.hbs @@ -0,0 +1,169 @@ +
+
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ THE BOSS +
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+ Details +
+
+ {{#each actor.system.details}} +
+ +
+
+ +
+ {{/each}} +
+
+ +
+
+
+
+ Heat +
+
+
+
+ Tier I +
+
+
+
+ (Nuisance, low-stakes, local cops) +
+
+
+ {{#partialint2checkbox maxHeat actor.system.heat 0 6}} + + {{/partialint2checkbox}} +
+
+
+ Tier II +
+
+
+
+ (Full-time, classic stakes, local super) +
+
+
+ {{#partialint2checkbox maxHeat actor.system.heat 6 12}} + + {{/partialint2checkbox}} +
+
+
+ Tier III +
+
+
+
+ (Top-class, high-stakes, world's finest heroes) +
+
+
+ {{#partialint2checkbox maxHeat actor.system.heat 12 18}} + + {{/partialint2checkbox}} +
+ +
+
+ +
+
+
+
+ Experience +
+
+
+ {{#int2checkbox maxExp actor.system.experience}} + + {{/int2checkbox}} +
+ {{#each actor.system.experienceTriggers}} +
+
+ +
+
+ {{this.description}} +
+
+ {{/each}} +
+
+
+ +
+
+
+ Abilities +
+
+ {{#each actor.system.moves}} +
+
+ +
+
+
+ +
+
+ {{{this.description}}} +
+ {{#if this.hasWriteIn}} +
+ +
+ {{/if}} +
+
+ {{/each}} +
+
+
+
\ No newline at end of file