From f888439fc7abafda9845701c72d5dc3e76241e27 Mon Sep 17 00:00:00 2001 From: zeel Date: Wed, 16 Mar 2022 15:53:04 -0400 Subject: [PATCH 01/15] WIP: Re-implementation of spellbook. See console logs when rendering sheet. --- scripts/dnd5e/ItemPrep.js | 8 ++ scripts/dnd5e/SpellBook.js | 245 +++++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 scripts/dnd5e/SpellBook.js diff --git a/scripts/dnd5e/ItemPrep.js b/scripts/dnd5e/ItemPrep.js index a6655b3..1fadbca 100644 --- a/scripts/dnd5e/ItemPrep.js +++ b/scripts/dnd5e/ItemPrep.js @@ -5,6 +5,8 @@ import ActionPreper from "./ActionPreper.js"; import ItemPreper from "./ItemPreper.js"; import InnateSpellbookPrep from "./InnateSpellbookPrep.js" +import SpellBook from "./SpellBook.js"; + /** * @typedef {import{"../../../../systems/dnd5e/module/item/sheet.js"}.Item5e} Item5e */ @@ -71,6 +73,12 @@ export default class ItemPrep { organizeSpellbooks(spells) { this.data.spellbook = this.sheet._prepareSpellbook(this.data, spells); this.data.innateSpellbook = new InnateSpellbookPrep(this.data.spellbook, this.sheet).prepare(); + + const spellbook = new SpellBook(this.sheet, spells); + console.log("SPELLBOOK:", spellbook); + console.log("PREPARED:", spellbook.getPrepared()); + console.log("INNATE", spellbook.getInnate()); + console.log("PACT", spellbook.getPact()); } /** diff --git a/scripts/dnd5e/SpellBook.js b/scripts/dnd5e/SpellBook.js new file mode 100644 index 0000000..6880b77 --- /dev/null +++ b/scripts/dnd5e/SpellBook.js @@ -0,0 +1,245 @@ +import Helpers from "./Helpers5e.js"; +import { debug, getTranslationArray } from "../utilities.js"; +import ItemPreper from "./ItemPreper.js"; +import * as Templates from "./templates.js"; + +/** + * @typedef {import{"../../../../systems/dnd5e/module/item/sheet.js"}.Item5e} Item5e + */ +/** + * @typedef {("always"|"atwill"|"innate"|"pact"|"prepared")} PrepMode + */ +/** + * @typedef {object} PageIdentity + * @property {PrepMode} mode - The mode of the page + * @property {number} level - The level of the page + * @property {number} uses - The maximum number of uses of the page + */ + +export default class SpellBook { + /** + * Create a new spellbook from the pages of an existing one + * + * @static + * @param {SpellBook} book + * @param {Object} pages + * @return {*} + * @memberof SpellBook + */ + static fromPages(book, pages) { + return new SpellBook(book.sheet, [].concat(...Object.values(pages).map(page => page.spells)), pages); + } + + static getPrepared(book) { + const entries = book.pageEntries; + const pages = entries.filter(([name, page]) => page.isPrepared); + + if (book.hasAtWillSpells && !book.hasInnateSpells) { + const will = entries.find(([name, page]) => page.isAtWill); + if (will) pages.push(will); + } + + return this.fromPages(book, Object.fromEntries(pages)); + } + static getInnate(book) { + const entries = book.pageEntries; + const pages = entries.filter(([name, page]) => page.isInnate || page.isAtWill); + return this.fromPages(book, Object.fromEntries(pages)); + } + static getPact(book) { + const entries = book.pageEntries; + const pages = entries.filter(([name, page]) => page.isPact); + + if (!book.hasPreparedSpells) { + const cantrips = entries.find(([name, page]) => page.isCantrip) + if (cantrips) pages.push(cantrips); + } + + return this.fromPages(book, Object.fromEntries(pages)); + } + + /** + * Creates an instance of SpellBook. + * @param {import("./MonsterBlock5e.js").MonsterBlock5e} sheet - The sheet instance + * @param {Item5e[]} items - All the items in the spellbook + * @param {Object} pages - Pages from another spellbook + * @memberof SpellBook + */ + constructor(sheet, items, pages) { + this.sheet = sheet; + this.items = items; + + if (!pages) this.fillPages(); + else this.pages = pages; + } + + /** @type {Object} */ + pages = {}; + + /** + * Whether or not this book has been used + * @type {boolean} + */ + shown = false; + + /** + * A spellcasting feature associated with this book + * @type {Item5e} + */ + feature = null; + + get pageEntries() { + return Object.entries(this.pages); + } + get pageValues() { + return Object.values(this.pages); + } + + fillPages() { + this.items.forEach(this.processItem.bind(this)); + } + + processItem(item) { + const mode = item.data.preparation.mode; + const level = item.data.level; + const uses = item.data.uses.max; + + const page = this.getOrCreatePage({ mode, level, uses }); + + page.add(item); + } + + getOrCreatePage(pageIdent) { + const name = SpellPage.getPageName(pageIdent); + return this.pages[name] || (this.pages[name] = new SpellPage(pageIdent)); + } + + get hasPreparedSpells() { + return this.pageValues.some(page => page.isPrepared); + } + get hasAtWillSpells() { + return this.pageValues.some(page => page.isAtWill); + } + get hasInnateSpells() { + return this.pageValues.some(page => page.isInnate); + } + get hasPactSpells() { + return this.pageValues.some(page => page.isPact); + } + + getPrepared() { + return this.constructor.getPrepared(this); + } + getInnate() { + return this.constructor.getInnate(this); + } + getPact() { + return this.constructor.getPact(this); + } +} + +export class SpellPage { + /** + * @static + * @param {PageIdentity} pageIdent + * @return {string} + * @memberof SpellPage + */ + static getPageName({ mode, level, uses }) { + if (this.isPact(mode) && this.isCantrip(level)) return "p0"; + if (this.isPrepared(mode)) return `l${level}`; + if (this.isInnate(mode)) return `u${uses}`; + if (this.isPact(mode)) return `pact`; + if (this.isAtWill(mode, uses)) return `will`; + + throw("The specified spell page is not valid"); + } + + static isPrepared(mode) { + return mode === "prepared" || mode === "always" + } + static isPact(mode) { + return mode === "pact"; + } + static isInnate(mode) { + return mode === "innate"; + } + static isAtWill(mode, uses) { + return mode === "atwill" || (this.isInnate(mode) && uses === 0); + } + static isCantrip(level) { + return level === 0; + } + + /** + * + * @param {PageIdentity} pageIdent + */ + constructor({ mode, level, uses }={}) { + /** + * The spell preperation mode for this page + * @type {PrepMode} + */ + this.mode = CONFIG.DND5E.spellPreparationModes[mode] ? mode : "innate"; + /** + * The spell level for this page + * @type {number} + */ + this.level = level || 0; + /** + * The maximum number of uses for spells in this page + * @type {number} + */ + this.uses = uses || 0; + } + + /** + * All of the spells on this page + * @type {Item5e[]} + */ + spells = []; + + /** + * Whether or not this page has been used + * @type {boolean} + */ + shown = false; + + get name() { + return this.constructor.getPageName(this); + } + + get isInnate() { + return this.constructor.isInnate(this.mode); + } + get isPrepared() { + return this.constructor.isPrepared(this.mode); + } + get isPact() { + return this.constructor.isPact(this.mode); + } + get isAtWill() { + return this.constructor.isAtWill(this.mode, this.uses); + } + get isCantrip() { + return this.constructor.isCantrip(this.mode, this.level); + } + + get any() { + return this.spells.length > 0; + } + + get spellCount() { + return this.spells.length; + } + + /** + * Add a spell to this page + * + * @param {Item5e} spells + * @memberof SpellPage + */ + add(...spells) { + this.spells.push(...spells); + } +} \ No newline at end of file From faca18605260edfc04632ed738a3df845ae11566 Mon Sep 17 00:00:00 2001 From: zeel Date: Tue, 17 May 2022 23:21:16 -0400 Subject: [PATCH 02/15] Improved debug output --- monsterblock.js | 11 +++++++++-- scripts/dnd5e/CastingPreper.js | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/monsterblock.js b/monsterblock.js index c51cc7f..06db26a 100644 --- a/monsterblock.js +++ b/monsterblock.js @@ -66,8 +66,15 @@ Hooks.once("ready", () => { // This is how the box sizing is corrected to fit the statblock // eslint-disable-next-line no-unused-vars Hooks.on("renderMonsterBlock5e", (monsterblock, html, data) => { // When the sheet is rendered - if (debug.INFO) console.log("Monster Block | Rendering sheet"); - if (debug.DEBUG) console.debug(`Monster Block |`, monsterblock, html, data); + const label = "Monster Block | Rendering sheet"; + if (debug.DEBUG) { + console.group(label); + console.log("Sheet: ", monsterblock); + console.log("HTML: ", html); + console.log("Template Data:", data); + console.groupEnd(label); + } + else if (debug.INFO) console.log(label); if (html.parent().hasClass("grid-cell-content")) return; diff --git a/scripts/dnd5e/CastingPreper.js b/scripts/dnd5e/CastingPreper.js index f902584..84fa90f 100644 --- a/scripts/dnd5e/CastingPreper.js +++ b/scripts/dnd5e/CastingPreper.js @@ -116,7 +116,7 @@ export default class CastingPreper extends ItemPreper { * @override * @memberof CastingPreper */ - prepare() { + prepare() { this.data.castingType = this.constructor.isSpellcasting(this.item) ? (this.constructor.isPactMagic(this.item) ? this.cts.pact : this.cts.standard) : this.cts.innate; From 7f71db4eea65e3aca6942105d9d77969d099ae50 Mon Sep 17 00:00:00 2001 From: zeel Date: Wed, 18 May 2022 00:01:53 -0400 Subject: [PATCH 03/15] Added some strings for spellcasting --- lang/en.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lang/en.json b/lang/en.json index 85fc0ce..e5aa4f4 100644 --- a/lang/en.json +++ b/lang/en.json @@ -177,6 +177,12 @@ "enable": "Show biography", "disable": "Hide biography" }, + + "Spellcasting": { + "Cantrip": "Cantrip", + "CantripPl": "Cantrips", + "AtWill": "at will" + }, "MonsterBlocks": "Monster Blocks", From 779096d1e6e8e6cb0da3e6a4eccd7ee8d6d0216f Mon Sep 17 00:00:00 2001 From: zeel Date: Wed, 18 May 2022 00:02:29 -0400 Subject: [PATCH 04/15] Spells are now prepared like all features before being sorted into the spellbook. --- scripts/dnd5e/ItemPrep.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/scripts/dnd5e/ItemPrep.js b/scripts/dnd5e/ItemPrep.js index 1fadbca..96720a9 100644 --- a/scripts/dnd5e/ItemPrep.js +++ b/scripts/dnd5e/ItemPrep.js @@ -6,6 +6,7 @@ import ItemPreper from "./ItemPreper.js"; import InnateSpellbookPrep from "./InnateSpellbookPrep.js" import SpellBook from "./SpellBook.js"; +import { debug } from "../utilities.js"; /** * @typedef {import{"../../../../systems/dnd5e/module/item/sheet.js"}.Item5e} Item5e @@ -40,11 +41,13 @@ export default class ItemPrep { /** @type {Object.} A set of item classifications by type */ features = { + spells: { prep: ItemPreper, filter: item => item.type === "spell", items: [], dataset: {type: "feat"} }, legResist: { prep: ItemPreper, filter: MonsterBlock5e.isLegendaryResistance, items: [], dataset: {type: "feat"} }, legendary: { prep: ActionPreper, filter: MonsterBlock5e.isLegendaryAction, items: [], dataset: {type: "feat"} }, lair: { prep: ActionPreper, filter: MonsterBlock5e.isLairAction, items: [], dataset: {type: "feat"} }, multiattack: { prep: ActionPreper, filter: MonsterBlock5e.isMultiAttack, items: [], dataset: {type: "feat"} }, - casting: { prep: CastingPreper, filter: CastingPreper.isCasting.bind(CastingPreper), items: [], dataset: {type: "feat"} }, + //casting: { prep: CastingPreper, filter: CastingPreper.isCasting.bind(CastingPreper), items: [], dataset: {type: "feat"} }, + casting: { prep: ItemPreper, filter: CastingPreper.isCasting.bind(CastingPreper), items: [], dataset: {type: "feat"} }, reaction: { prep: ActionPreper, filter: MonsterBlock5e.isReaction, items: [], dataset: {type: "feat"} }, bonusActions: { prep: ActionPreper, filter: MonsterBlock5e.isBonusAction, items: [], dataset: {type: "feat"} }, attacks: { prep: AttackPreper, filter: item => item.type === "weapon", items: [], dataset: {type: "weapon"} }, @@ -59,26 +62,32 @@ export default class ItemPrep { * @memberof ItemPrep */ prepareItems() { - const [other, spells] = this.data.items.partition(item => item.type === "spell"); - this.organizeSpellbooks(spells); - this.organizeFeatures(other); + this.organizeFeatures(this.data.items); + this.organizeSpellbooks(this.data.features.spells.items); } /** - * Prepares and organizes the regular and innate spellbooks + * Prepares and organizes the spellbooks * * @param {array} spells - All the spell items * @memberof ItemPrep */ organizeSpellbooks(spells) { - this.data.spellbook = this.sheet._prepareSpellbook(this.data, spells); - this.data.innateSpellbook = new InnateSpellbookPrep(this.data.spellbook, this.sheet).prepare(); - const spellbook = new SpellBook(this.sheet, spells); - console.log("SPELLBOOK:", spellbook); - console.log("PREPARED:", spellbook.getPrepared()); - console.log("INNATE", spellbook.getInnate()); - console.log("PACT", spellbook.getPact()); + this.data.spellbook = spellbook; + + //this.data.spellbook = this.sheet._prepareSpellbook(this.data, spells); + //this.data.innateSpellbook = new InnateSpellbookPrep(this.data.spellbook, this.sheet).prepare(); + + if (debug.DEBUG) { + const label = "Monster Blocks | Spellbook"; + console.group(label); + console.log("Full: ", spellbook); + console.log("Prepared:", spellbook.getPrepared()); + console.log("Innate ", spellbook.getInnate()); + console.log("Pact ", spellbook.getPact()); + console.groupEnd(label); + } } /** From bee9d2e8cb7e8baa7217e446cae6bd0f06fedf9f Mon Sep 17 00:00:00 2001 From: zeel Date: Wed, 18 May 2022 00:02:49 -0400 Subject: [PATCH 05/15] Added placeholder for page labels --- scripts/dnd5e/SpellBook.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/dnd5e/SpellBook.js b/scripts/dnd5e/SpellBook.js index 6880b77..80dfe6f 100644 --- a/scripts/dnd5e/SpellBook.js +++ b/scripts/dnd5e/SpellBook.js @@ -155,6 +155,22 @@ export class SpellPage { throw("The specified spell page is not valid"); } + /** + * @static + * @param {PageIdentity} pageIdent + * @return {string} + * @memberof SpellPage + */ + static getPageLabel({ mode, level, uses }) { + if (this.isPact(mode) && this.isCantrip(level)) return "MOBLOKS5E.Spellcasting.CantripPl"; + if (this.isPrepared(mode)) return `${level}th level`; + if (this.isInnate(mode)) return `${uses}/day`; + if (this.isPact(mode)) return `levels 1-${level}`; + if (this.isAtWill(mode, uses)) return "MOBLOKS5E.Spellcasting.AtWill"; + + throw("The specified spell page is not valid"); + } + static isPrepared(mode) { return mode === "prepared" || mode === "always" } @@ -208,6 +224,9 @@ export class SpellPage { get name() { return this.constructor.getPageName(this); } + get label() { + return game.i18n.localize(this.constructor.getPageLabel(this)); + } get isInnate() { return this.constructor.isInnate(this.mode); From 38eb4e22563b56824332230e81547b0e1227ea4f Mon Sep 17 00:00:00 2001 From: zeel Date: Wed, 18 May 2022 00:03:40 -0400 Subject: [PATCH 06/15] Created basic template partials for the new spellcasting system --- scripts/dnd5e/MonsterBlock5e.js | 5 +++++ templates/dnd5e/main.hbs | 3 +++ .../dnd5e/parts/spellcasting/spell-list.hbs | 15 +++++++++++++ .../parts/spellcasting/spellbook-page.hbs | 21 +++++++++++++++++++ .../dnd5e/parts/spellcasting/spellbook.hbs | 7 +++++++ .../parts/spellcasting/spellcasting-redux.hbs | 6 ++++++ 6 files changed, 57 insertions(+) create mode 100644 templates/dnd5e/parts/spellcasting/spell-list.hbs create mode 100644 templates/dnd5e/parts/spellcasting/spellbook-page.hbs create mode 100644 templates/dnd5e/parts/spellcasting/spellbook.hbs create mode 100644 templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs diff --git a/scripts/dnd5e/MonsterBlock5e.js b/scripts/dnd5e/MonsterBlock5e.js index 174283d..13a7ba9 100644 --- a/scripts/dnd5e/MonsterBlock5e.js +++ b/scripts/dnd5e/MonsterBlock5e.js @@ -1253,6 +1253,11 @@ export default class MonsterBlock5e extends ActorSheet5eNPC { "modules/monsterblock/templates/dnd5e/parts/main/legendaryActs.hbs", "modules/monsterblock/templates/dnd5e/parts/main/lairActs.hbs", + "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs", + "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellbook.hbs", + "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellbook-page.hbs", + "modules/monsterblock/templates/dnd5e/parts/spellcasting/spell-list.hbs", + "modules/monsterblock/templates/dnd5e/parts/menuItem.hbs", "modules/monsterblock/templates/dnd5e/parts/resource.hbs", "modules/monsterblock/templates/dnd5e/parts/featureBlock.hbs", diff --git a/templates/dnd5e/main.hbs b/templates/dnd5e/main.hbs index 9d6f50e..d593281 100644 --- a/templates/dnd5e/main.hbs +++ b/templates/dnd5e/main.hbs @@ -41,6 +41,9 @@ {{> "modules/monsterblock/templates/dnd5e/parts/featureBlock.hbs" item=features.multiattack.items.[0]}} {{/if}} + {{! Spellcasting Action }} + {{> "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs" spellbook=spellbook}} + {{! Attacks }} {{#each features.attacks.items as |item iid|}} {{#unless item.is.specialAction}} diff --git a/templates/dnd5e/parts/spellcasting/spell-list.hbs b/templates/dnd5e/parts/spellcasting/spell-list.hbs new file mode 100644 index 0000000..ab394c2 --- /dev/null +++ b/templates/dnd5e/parts/spellcasting/spell-list.hbs @@ -0,0 +1,15 @@ +
    +{{#each spells as |spell id|}} +
  • + {{#if @root.flags.show-delete}} + + + + {{/if}} + {{spell.name}} + {{~#if spell.hasresource~}} + {{> "modules/monsterblock/templates/dnd5e/parts/resource.hbs" resource=spell.resource}} + {{~/if~}} +
  • +{{/each}} +
\ No newline at end of file diff --git a/templates/dnd5e/parts/spellcasting/spellbook-page.hbs b/templates/dnd5e/parts/spellcasting/spellbook-page.hbs new file mode 100644 index 0000000..879cbdf --- /dev/null +++ b/templates/dnd5e/parts/spellcasting/spellbook-page.hbs @@ -0,0 +1,21 @@ +
  • + + {{page.label~}} + + {{~#if page.slotLabel}} + + ({{~#if (and @root.flags.show-resources page.dataset.level)~}} + + {{~page.uses~}} + / + {{~/if~}} + {{{page.slotLabel}}}){{~" "~}} + + {{~/if~}} + {{~localize "MOBLOKS5E.Colon"}} + {{> "modules/monsterblock/templates/dnd5e/parts/spellcasting/spell-list.hbs" spells=page.spells level=page.level}} +
  • \ No newline at end of file diff --git a/templates/dnd5e/parts/spellcasting/spellbook.hbs b/templates/dnd5e/parts/spellcasting/spellbook.hbs new file mode 100644 index 0000000..8ff1edc --- /dev/null +++ b/templates/dnd5e/parts/spellcasting/spellbook.hbs @@ -0,0 +1,7 @@ +
      +{{#each spellbook.pages as |page id|}} + {{#if page.spells}} + {{> "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellbook-page.hbs" page=page id=id}} + {{/if}} +{{/each}} +
    \ No newline at end of file diff --git a/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs b/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs new file mode 100644 index 0000000..3a81b70 --- /dev/null +++ b/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs @@ -0,0 +1,6 @@ +
    +Spellcasting. +
    + {{> "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellbook.hbs" spellbook=spellbook}} +
    +
    \ No newline at end of file From ba6f5378532d34d2b8f01fa6045937587f0aaf50 Mon Sep 17 00:00:00 2001 From: zeel Date: Thu, 28 Jul 2022 01:43:35 -0400 Subject: [PATCH 07/15] Added highlighter hinting --- scripts/dnd5e/templates.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/dnd5e/templates.js b/scripts/dnd5e/templates.js index fbd22f5..e4cd71f 100644 --- a/scripts/dnd5e/templates.js +++ b/scripts/dnd5e/templates.js @@ -22,7 +22,7 @@ export let selectField = ({ labelClass="", label, listClass="", options, enabled=true -}) => `\ +}) => /* html */`\
    @@ -53,7 +53,7 @@ export let selectField = ({ */ export let editable = ({ key, className="", dtype="Text", placeholder="", value="", enabled=true -}) => `\ +}) => /* html */`\ `\ +}) => /* html */`\ -` \ No newline at end of file +`; \ No newline at end of file From ba06b881c8aba146abe57c9ee7ad79096b2ee0f0 Mon Sep 17 00:00:00 2001 From: zeel Date: Thu, 28 Jul 2022 01:44:00 -0400 Subject: [PATCH 08/15] Added additional localization strings for casting descriptions and titles --- lang/en.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lang/en.json b/lang/en.json index e5aa4f4..cabcfff 100644 --- a/lang/en.json +++ b/lang/en.json @@ -179,9 +179,23 @@ }, "Spellcasting": { + "preparedSpellcasting": "Spellcasting", + "pactSpellcasting": "Spellcasting", + "innateSpellcasting": "Innate Spellcasting", + "psionicSpellcasting": "Spellcasting (Psionics)", "Cantrip": "Cantrip", "CantripPl": "Cantrips", - "AtWill": "at will" + "AtWill": "at will", + "preparedCasterText": + "The {{name}} is {{#if (or (eq level 8) (eq level 18))}}an{{else}}a{{/if}} {{{nth}}}-level spellcaster. Its spellcasting ability is {{{ability}}} (spell save DC {{dc}}, {{tohit}} to hit with spell attacks). The {{name}} {{#if hasAtWill}}can cast {{{spells}}} at will and {{/if}}has the following spells prepared:", + "pactCasterText": + "The {{name}} is {{#if (or (eq level 8) (eq level 18))}}an{{else}}a{{/if}} {{{nth}}}-level spellcaster. Its spellcasting ability is {{{ability}}} (spell save DC {{dc}}, {{tohit}} to hit with spell attacks). It regains its expended spell slots when it finishes a short or long rest. It knows the following warlock spells:", + "innateCasterText": + "The {{name}} spellcasting ability is {{{ability}}} (spell save DC {{dc}}). It can innately cast the following spells, requiring no material components:", + "fullCasterText": + "The {{name}} is {{#if (or (eq level 8) (eq level 18))}}an{{else}}a{{/if}} {{{nth}}}-level spellcaster. Its spellcasting ability is {{{ability}}} (spell save DC {{dc}}, {{tohit}} to hit with spell attacks). The {{name}} {{#if hasAtWill}}can cast {{{spells}}} at will and {{/if}}has the following spells prepared:", + "spellcastingActionText": + "The {{name}} casts one of the following spells, using {{{ability}}} as the spellcasting ability (spell save DC {{dc}}):" }, "MonsterBlocks": "Monster Blocks", From 0ca1d61d8356c97cec7eac04e0465a00e6f34bf5 Mon Sep 17 00:00:00 2001 From: zeel Date: Thu, 28 Jul 2022 01:44:44 -0400 Subject: [PATCH 09/15] Added organization of spells into books --- scripts/dnd5e/ItemPrep.js | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/scripts/dnd5e/ItemPrep.js b/scripts/dnd5e/ItemPrep.js index 96720a9..f43b415 100644 --- a/scripts/dnd5e/ItemPrep.js +++ b/scripts/dnd5e/ItemPrep.js @@ -63,7 +63,9 @@ export default class ItemPrep { */ prepareItems() { this.organizeFeatures(this.data.items); - this.organizeSpellbooks(this.data.features.spells.items); + + if (this.data.features.spells.items.length) + this.organizeSpellbooks(this.data.features.spells.items); } /** @@ -73,19 +75,29 @@ export default class ItemPrep { * @memberof ItemPrep */ organizeSpellbooks(spells) { - const spellbook = new SpellBook(this.sheet, spells); - this.data.spellbook = spellbook; + const spellbook = new SpellBook(this.sheet, spells, null, "full"); - //this.data.spellbook = this.sheet._prepareSpellbook(this.data, spells); - //this.data.innateSpellbook = new InnateSpellbookPrep(this.data.spellbook, this.sheet).prepare(); + const spellbooks = { + "full": spellbook, + "prepared": spellbook.getPrepared(), + "innate": spellbook.getInnate(), + "pact": spellbook.getPact(), + } + + Object.values(spellbooks).forEach(book => { + if (!book.hasPages) book.show = false; + }); + spellbooks.full.show = false; + + this.data.spellbooks = spellbooks; if (debug.DEBUG) { const label = "Monster Blocks | Spellbook"; console.group(label); - console.log("Full: ", spellbook); - console.log("Prepared:", spellbook.getPrepared()); - console.log("Innate ", spellbook.getInnate()); - console.log("Pact ", spellbook.getPact()); + console.log("Full: ", this.data.spellbooks.full); + console.log("Prepared:", this.data.spellbooks.prepared); + console.log("Innate ", this.data.spellbooks.innate); + console.log("Pact ", this.data.spellbooks.pact); console.groupEnd(label); } } From eaaa4452cc7dc53b151952af73fc7b988cf29384 Mon Sep 17 00:00:00 2001 From: zeel Date: Thu, 28 Jul 2022 01:45:13 -0400 Subject: [PATCH 10/15] Created helper class for getting various game statistics from an actor --- scripts/dnd5e/MonsterBlock5e.js | 2 + scripts/dnd5e/StatGetter.js | 73 +++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 scripts/dnd5e/StatGetter.js diff --git a/scripts/dnd5e/MonsterBlock5e.js b/scripts/dnd5e/MonsterBlock5e.js index 13a7ba9..62e5945 100644 --- a/scripts/dnd5e/MonsterBlock5e.js +++ b/scripts/dnd5e/MonsterBlock5e.js @@ -5,6 +5,7 @@ import { debug, ContentEditableAdapter, getTranslationArray } from "../utilities import { inputExpression } from "../../input-expressions/handler.js"; import ItemPrep from "./ItemPrep.js"; import Flags from "./Flags5e.js"; +import StatGetter from "./StatGetter.js"; /* global QuickInsert:readonly */ @@ -22,6 +23,7 @@ export default class MonsterBlock5e extends ActorSheet5eNPC { this.position.default = true; this.flagManager = new Flags(this); + this.statGetter = new StatGetter(this, this.actor); //this.flagManager.prep().then((p) => { this.options.classes.push(this.themes[this.currentTheme].class); diff --git a/scripts/dnd5e/StatGetter.js b/scripts/dnd5e/StatGetter.js new file mode 100644 index 0000000..86f6af6 --- /dev/null +++ b/scripts/dnd5e/StatGetter.js @@ -0,0 +1,73 @@ +/** + * A class to handle looking up statistics about the actor + * that the associated sheet is for. + * + * @export + * @class StatGetter + */ +export default class StatGetter { + constructor(sheet, actor) { + this.sheet = sheet; + this.actor = actor; + } + + /** + * Calculates the spell attack bonus of this actor + * + * @return {number} + * @memberof StatGetter + */ + get spellAttackBonus() { + const data = this.actor.data.data; + const abilityBonus = data.abilities[this.castingAbility]?.mod; + const profBonus = data.attributes?.prof; + + return abilityBonus + profBonus ?? 0; + } + + /** + * Get which ability score is the the casting ability of this actor + * If none found, use 'int' + * + * @return {string} + * @memberof StatGetter + */ + get castingAbility() { + return this.actor.data.data?.attributes?.spellcasting || "int"; + } + + /** + * Get the label for the ability that is the the casting ability of this actor + * If none found, use 'Intelligence' (localized) + * + * @return {string} + * @memberof StatGetter + */ + get castingAbilityLabel() { + const abl = this.castingAbility; + return game.i18n.localize( + // Capitalize the first letter of the ability + `DND5E.Ability${abl.charAt(0).toUpperCase() + abl.slice(1)}` + ); + } + + /** + * Get the spell level of this actor + * + * @return {number} + * @memberof StatGetter + */ + get spellLevel() { + return this.actor.data.data.details.spellLevel; + } + + /** + * Get the spell DC of this actor + * + * @return {number} + * @memberof StatGetter + */ + get spellDc() { + return this.actor.data.data?.attributes?.spelldc; + } +} \ No newline at end of file From 5619e7e9f9c2365ca4db6f890a6c15e17cd28397 Mon Sep 17 00:00:00 2001 From: zeel Date: Thu, 28 Jul 2022 01:46:39 -0400 Subject: [PATCH 11/15] Adjusting JSDoc types --- scripts/dnd5e/SpellBook.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/dnd5e/SpellBook.js b/scripts/dnd5e/SpellBook.js index 80dfe6f..e2a1f90 100644 --- a/scripts/dnd5e/SpellBook.js +++ b/scripts/dnd5e/SpellBook.js @@ -4,7 +4,10 @@ import ItemPreper from "./ItemPreper.js"; import * as Templates from "./templates.js"; /** - * @typedef {import{"../../../../systems/dnd5e/module/item/sheet.js"}.Item5e} Item5e + * @typedef {import("../../../../systems/dnd5e/module/item/sheet.js").Item5e} Item5e + */ +/** + * @typedef {import("./MonsterBlock5e.js").MonsterBlock5e} MonsterBlock5e */ /** * @typedef {("always"|"atwill"|"innate"|"pact"|"prepared")} PrepMode @@ -60,12 +63,15 @@ export default class SpellBook { /** * Creates an instance of SpellBook. - * @param {import("./MonsterBlock5e.js").MonsterBlock5e} sheet - The sheet instance + * @param {MonsterBlock5e} sheet - The sheet instance * @param {Item5e[]} items - All the items in the spellbook * @param {Object} pages - Pages from another spellbook * @memberof SpellBook */ - constructor(sheet, items, pages) { + constructor(sheet, items, pages, type) { + /** + * @type {MonsterBlock5e} The sheet this spellbook is for. + */ this.sheet = sheet; this.items = items; From 6a9ae50745ddf6e93dc9300932d5f7c79b9bd71c Mon Sep 17 00:00:00 2001 From: zeel Date: Thu, 28 Jul 2022 01:47:08 -0400 Subject: [PATCH 12/15] Improved constructing Spellbook from pages --- scripts/dnd5e/SpellBook.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/dnd5e/SpellBook.js b/scripts/dnd5e/SpellBook.js index e2a1f90..f09db4e 100644 --- a/scripts/dnd5e/SpellBook.js +++ b/scripts/dnd5e/SpellBook.js @@ -29,8 +29,8 @@ export default class SpellBook { * @return {*} * @memberof SpellBook */ - static fromPages(book, pages) { - return new SpellBook(book.sheet, [].concat(...Object.values(pages).map(page => page.spells)), pages); + static fromPages(book, pages, type) { + return new SpellBook(book.sheet, [].concat(...Object.values(pages).map(page => page.spells)), pages, type); } static getPrepared(book) { @@ -42,12 +42,12 @@ export default class SpellBook { if (will) pages.push(will); } - return this.fromPages(book, Object.fromEntries(pages)); + return this.fromPages(book, Object.fromEntries(pages), "prepared"); } static getInnate(book) { const entries = book.pageEntries; const pages = entries.filter(([name, page]) => page.isInnate || page.isAtWill); - return this.fromPages(book, Object.fromEntries(pages)); + return this.fromPages(book, Object.fromEntries(pages), "innate"); } static getPact(book) { const entries = book.pageEntries; @@ -58,7 +58,7 @@ export default class SpellBook { if (cantrips) pages.push(cantrips); } - return this.fromPages(book, Object.fromEntries(pages)); + return this.fromPages(book, Object.fromEntries(pages), "pact"); } /** From e353f311594f7c3f81b39545955495078b7e1323 Mon Sep 17 00:00:00 2001 From: zeel Date: Thu, 28 Jul 2022 01:47:39 -0400 Subject: [PATCH 13/15] Added a label check for prepared cantrips --- scripts/dnd5e/SpellBook.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/dnd5e/SpellBook.js b/scripts/dnd5e/SpellBook.js index f09db4e..1ac2f3e 100644 --- a/scripts/dnd5e/SpellBook.js +++ b/scripts/dnd5e/SpellBook.js @@ -169,6 +169,7 @@ export class SpellPage { */ static getPageLabel({ mode, level, uses }) { if (this.isPact(mode) && this.isCantrip(level)) return "MOBLOKS5E.Spellcasting.CantripPl"; + if (this.isPrepared(mode) && this.isCantrip(level)) return "MOBLOKS5E.Spellcasting.CantripPl"; if (this.isPrepared(mode)) return `${level}th level`; if (this.isInnate(mode)) return `${uses}/day`; if (this.isPact(mode)) return `levels 1-${level}`; @@ -180,7 +181,7 @@ export class SpellPage { static isPrepared(mode) { return mode === "prepared" || mode === "always" } - static isPact(mode) { + static isPact(mode) { return mode === "pact"; } static isInnate(mode) { From bc470d17175782a81dbd4f5d83727e16b9c0dda4 Mon Sep 17 00:00:00 2001 From: zeel Date: Thu, 28 Jul 2022 01:48:48 -0400 Subject: [PATCH 14/15] Added title and intro text --- scripts/dnd5e/SpellBook.js | 48 +++++++++++++++++++ .../parts/spellcasting/spellcasting-redux.hbs | 7 ++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/scripts/dnd5e/SpellBook.js b/scripts/dnd5e/SpellBook.js index 1ac2f3e..db5a543 100644 --- a/scripts/dnd5e/SpellBook.js +++ b/scripts/dnd5e/SpellBook.js @@ -74,6 +74,12 @@ export default class SpellBook { */ this.sheet = sheet; this.items = items; + this.type = type; + + this.stats = this.sheet.statGetter; + this.editing = this.sheet.flags.editing; + + this.show = true; if (!pages) this.fillPages(); else this.pages = pages; @@ -100,6 +106,9 @@ export default class SpellBook { get pageValues() { return Object.values(this.pages); } + get title() { + return game.i18n.localize(`MOBLOKS5E.Spellcasting.${this.type}Spellcasting`); + } fillPages() { this.items.forEach(this.processItem.bind(this)); @@ -132,6 +141,45 @@ export default class SpellBook { get hasPactSpells() { return this.pageValues.some(page => page.isPact); } + get hasPages() { + return Boolean(Object.keys(this.pages).length) + } + + get introText() { + const stats = this.stats; + const template = game.i18n.localize(`MOBLOKS5E.Spellcasting.${this.type}CasterText`); + const builder = Handlebars.compile(template); + console.log(stats.castingAbility, stats.castingAbilityLabel) + return builder({ + name: this.sheet.actor.name, + level: stats.spellLevel, + nth: Templates.editable({ + key: "data.details.spellLevel", + value: stats.spellLevel, + className: "caster-level", + dtype: "Number", + placeholder: "0", + enabled: this.editing + }) + Helpers.getOrdinalSuffix(stats.spellLevel), + ability: Templates.selectField({ + key: "data.attributes.spellcasting", + value: stats.castingAbility, + label: stats.castingAbilityLabel, + listClass: "actor-size", + options: Object.entries(CONFIG.DND5E.abilities) + .map(([key, value]) => ({ + value: key, + label: value + })) + .filter(opt => opt.value != stats.castingAbility), + enabled: this.editing + }), + tohit: `${stats.spellAttackBonus > -1 ? "+" : ""}${stats.spellAttackBonus}`, + dc: stats.spellDc, + hasAtWill: this.hasAtWillSpells, + spells: "" + }); + } getPrepared() { return this.constructor.getPrepared(this); diff --git a/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs b/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs index 3a81b70..a083233 100644 --- a/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs +++ b/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs @@ -1,6 +1,9 @@
    -Spellcasting.
    - {{> "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellbook.hbs" spellbook=spellbook}} + {{spellbook.title}}. + {{{spellbook.introText}}} + + {{> "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellbook.hbs" spellbook=spellbook}} +
    \ No newline at end of file From 06ccd49fe8ce97b9dfa4b9586e0cb0f0d117054d Mon Sep 17 00:00:00 2001 From: zeel Date: Thu, 28 Jul 2022 01:49:07 -0400 Subject: [PATCH 15/15] Show all visible spellbooks under features --- templates/dnd5e/main.hbs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/templates/dnd5e/main.hbs b/templates/dnd5e/main.hbs index d593281..6cdcf69 100644 --- a/templates/dnd5e/main.hbs +++ b/templates/dnd5e/main.hbs @@ -19,6 +19,12 @@ {{/if}} {{/each}} + {{#each spellbooks as |spellbook|}} + {{#if spellbook.show}} + {{> "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs" spellbook=spellbook}} + {{/if}} + {{/each}} + {{! Non-action Features }} {{#each features.features.items as |item iid|}} {{#unless item.is.specialAction}} @@ -42,7 +48,7 @@ {{/if}} {{! Spellcasting Action }} - {{> "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs" spellbook=spellbook}} + {{> "modules/monsterblock/templates/dnd5e/parts/spellcasting/spellcasting-redux.hbs" spellbook=spellbooks.full}} {{! Attacks }} {{#each features.attacks.items as |item iid|}}