diff --git a/module/deltagreen.js b/module/deltagreen.js index 5f7aad5..3ff6720 100644 --- a/module/deltagreen.js +++ b/module/deltagreen.js @@ -3,7 +3,7 @@ import DG from "./config.js"; import DeltaGreenActor from "./actor/actor.js"; import DGAgentSheet from "./sheets/agent-sheet.js"; import DeltaGreenItem from "./item/item.js"; -import DeltaGreenItemSheet from "./item/item-sheet.js"; +import DGItemSheet from "./sheets/base-item-sheet.js"; import * as DGRolls from "./roll/roll.js"; import registerSystemSettings from "./settings.js"; import preloadHandlebarsTemplates from "./templates.js"; @@ -103,16 +103,15 @@ Hooks.once("init", async () => { }); }); - // Don't register new sheet yet. - // Actors.registerSheet(DG.ID, DGAgentSheetV2, { - // makeDefault: false, - // themes: null, - // label: `Agent Sheet V2`, - // types: ["agent"], - // }); + Actors.registerSheet(DG.ID, DGAgentSheetV2, { + makeDefault: false, + themes: null, + label: "DG.Sheet.class.agentV2", + types: ["agent"], + }); // Register item sheet. - Items.registerSheet(DG.ID, DeltaGreenItemSheet, { + Items.registerSheet(DG.ID, DGItemSheet, { makeDefault: true, label: "DG.Sheet.class.item", themes: null, @@ -164,32 +163,25 @@ Hooks.on("preCreateItem", (item) => { }); // Hook into the render call for the Actors Directory to add an extra button -Hooks.on("renderActorDirectory", (app, html, data) => { - if (!game.user.isGM) { - return; - } +Hooks.on("renderActorDirectory", (app, element) => { + if (!game.user.isGM) return; - if (html.querySelector("#statParserButton")) { - return; - } + const footer = element?.querySelector?.(".directory-footer"); + if (!footer || footer.querySelector("#statParserButton")) return; const button = document.createElement("button"); button.id = "statParserButton"; + button.type = "button"; const icon = document.createElement("i"); - icon.classList.add("fas", "fa-file-import"); - button.appendChild(icon); button.append("Delta Green Stat Block Parser"); - html.querySelector(".directory-footer").appendChild(button); - - if (button) { - button.addEventListener("click", function (event) { - ParseDeltaGreenStatBlock(); - }); - } + footer.appendChild(button); + button.addEventListener("click", () => { + ParseDeltaGreenStatBlock(); + }); }); // Note - this event is fired on ALL connected clients... diff --git a/module/item/item-sheet.js b/module/item/item-sheet.js index dd2d011..9ef2bf7 100644 --- a/module/item/item-sheet.js +++ b/module/item/item-sheet.js @@ -1,196 +1 @@ -import { - DGPercentileRoll, - DGLethalityRoll, - DGDamageRoll, - DGSanityDamageRoll, -} from "../roll/roll.js"; - -/** - * Extend the basic ItemSheet with some very simple modifications - * @extends {ItemSheet} - */ -export default class DeltaGreenItemSheet extends foundry.appv1.sheets - .ItemSheet { - /** @override */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["deltagreen", "sheet", "item"], - width: 520, - height: 600, - tabs: [ - { - navSelector: ".sheet-tabs", - contentSelector: ".sheet-body", - initial: "attributes", - }, - ], - }); - } - - /** @override */ - get template() { - const path = "systems/deltagreen/templates/item"; - - // unique item sheet by type, like `weapon-sheet.html`. - return `${path}/item-${this.item.type}-sheet.html`; - } - - /* -------------------------------------------- */ - - /** @override */ - async getData() { - const data = super.getData(); - - data.enrichedDescription = - await foundry.applications.ux.TextEditor.implementation.enrichHTML( - this.object.system.description, - { async: true }, - ); - - if (data.item.type === "tome" || data.item.type === "ritual") { - data.enrichedHandlerNotes = - await foundry.applications.ux.TextEditor.implementation.enrichHTML( - this.object.system.handlerNotes, - { async: true }, - ); - } - - return data; - } - - /* -------------------------------------------- */ - - /** @override */ - setPosition(options = {}) { - const position = super.setPosition(options); - const sheetBody = this.element.find(".sheet-body"); - const bodyHeight = position.height - 192; - sheetBody.css("height", bodyHeight); - return position; - } - - /* -------------------------------------------- */ - - /** @override */ - activateListeners(html) { - super.activateListeners(html); - - // Everything below here is only needed if the sheet is editable - // eslint-disable-next-line no-useless-return - if (!this.options.editable) return; - - // Rollable abilities - bind to everything with the 'Rollable' class - html.find(".rollable").click(this._onRoll.bind(this)); - html.find(".rollable").contextmenu(this._onRoll.bind(this)); // this is for right-click, which triggers the roll modifier dialogue for most rolls - - // toggle whether information is shown to a player for a tome or not - html.find(".tome-show-hide-info").click((event) => { - event.preventDefault(); - - this.item.system.revealed = !this.item.system.revealed; - - this.item.update({ "system.revealed": this.item.system.revealed }); - }); - } - - /** - * Handle clickable rolls. - * - * @param {Event} event The originating click event - * @async - * @private - */ - async _onRoll(event) { - event.preventDefault(); - const element = event.currentTarget; - const { dataset } = element; - - if (event && event.which === 2) { - // probably don't want rolls to trigger from a middle mouse click so just kill it here - return; - } - - // const item = this.actor.items.get(dataset.iid); - const rollOptions = { - rollType: dataset.rolltype, - key: dataset.key, - // actor: this, - specialTrainingName: dataset?.name || null, // Only applies to Special Training Rolls - item: this.item, - }; - - // Create a default 1d100 roll just in case. - let roll = new Roll("1d100", {}); - switch (dataset.rolltype) { - case "stat": - case "skill": - case "sanity": - case "special-training": - case "weapon": - roll = new DGPercentileRoll("1D100", {}, rollOptions); - break; - case "lethality": - roll = new DGLethalityRoll("1D100", {}, rollOptions); - break; - case "damage": { - let diceFormula = this.item.system.damage; - const { skill } = this.item.system; - if ( - (this.actor.type === "agent" || this.actor.type === "npc") && - (skill === "unarmed_combat" || skill === "melee_weapons") - ) { - diceFormula += - this.actor.system.statistics.str.meleeDamageBonusFormula; - } - roll = new DGDamageRoll(diceFormula, {}, rollOptions); - break; - } - case "sanity-damage": { - // Applies only to rituals, as far as Jalen can tell. Tomes have "study damage", but it just uses `sanity`. - const isLearntDamage = element.hasAttribute("data-san-on-learn"); - - const { successLoss, failedLoss } = isLearntDamage - ? this.item.system.learnedSanity - : this.item.system.sanity; - const combinedFormula = `{${successLoss}, ${failedLoss}}`; - - roll = new DGSanityDamageRoll(combinedFormula, {}, rollOptions); - break; - } - default: - break; - } - this.processRoll(event, roll, rollOptions); - } - - /** - * Show a dialog for the roll and then send to chat. - * Broke this logic out from `_onRoll()` so that other files can call it, - * namely the macro logic. - * - * TODO: Move this logic to the roll.js. - * - * @param {Event} event The originating click event - * @param {Event} roll The roll to show a dialog for and then send to chat. - * @async - */ - async processRoll(event, roll) { - // Open dialog if user requests it (no dialog for Sanity Damage rolls) - if ( - (event.shiftKey || event.which === 3) && - !(roll instanceof DGSanityDamageRoll) - ) { - const dialogData = await roll.showDialog(); - if (!dialogData) return; - if (dialogData.newFormula) { - roll = new DGDamageRoll(dialogData.newFormula, {}, roll.options); - } - roll.modifier += dialogData.targetModifier; - roll.options.rollMode = dialogData.rollMode; - } - // Evaluate the roll. - await roll.evaluate(); - // Send the roll to chat. - roll.toChat(); - } -} +export { default } from "../sheets/base-item-sheet.js"; diff --git a/module/other/register-helpers.js b/module/other/register-helpers.js index 1356dde..3938d26 100644 --- a/module/other/register-helpers.js +++ b/module/other/register-helpers.js @@ -83,9 +83,16 @@ export default function registerHandlebarsHelpers() { }, ); + /** Roll visibility modes for dialog selects (v14+ CONFIG.ChatMessage.modes keys). */ + const ROLL_MESSAGE_MODE_KEYS = ["public", "gm", "blind", "self"]; + Handlebars.registerHelper("getAvailableRollModes", () => { try { - return CONFIG.Dice.rollModes; + return Object.fromEntries( + ROLL_MESSAGE_MODE_KEYS.filter((key) => key in CONFIG.ChatMessage.modes).map( + (key) => [key, CONFIG.ChatMessage.modes[key]], + ), + ); } catch (error) { return console.log(error); } @@ -93,7 +100,7 @@ export default function registerHandlebarsHelpers() { Handlebars.registerHelper("getDefaultRollMode", () => { try { - return game.settings.get("core", "rollMode"); + return game.settings.get("core", "messageMode"); } catch (error) { return console.log(error); } diff --git a/module/roll/roll.js b/module/roll/roll.js index db002e4..578d182 100644 --- a/module/roll/roll.js +++ b/module/roll/roll.js @@ -32,25 +32,30 @@ export class DGRoll extends Roll { /** * Simple function that actually creates the message and sends it to chat. * We override this to have a little more control over certain aspects of the message, - * right now, its `speaker` and `rollMode`. + * right now, its `speaker` and `messageMode`. * * @override * The following `@param` descriptions comes from the Foundry VTT code. * @param {object} messageData The data object to use when creating the message * @param {options} [options] Additional options which modify the created message. - * @param {string} [options.rollMode] The template roll mode to use for the message from CONFIG.Dice.rollModes + * @param {string} [options.messageMode] A key of CONFIG.ChatMessage.modes * @param {boolean} [options.create=true] Whether to automatically create the chat message, or only return the * prepared chatData object. * @returns {Promise} A promise which resolves to the created ChatMessage document if create is * true, or the Object of prepared chatData otherwise. */ - async toMessage(messageData = {}, { rollMode, create = true } = {}) { + async toMessage(messageData = {}, { messageMode, create = true } = {}) { // eslint-disable-next-line no-param-reassign messageData.speaker = ChatMessage.getSpeaker({ actor: this.actor }); - return super.toMessage(messageData, { - rollMode: this.options.rollMode || rollMode, - create, - }); + const mode = + messageMode ?? + this.options.messageMode ?? + this.options.rollMode; + const opts = { create }; + if (mode) { + opts.messageMode = foundry.dice.Roll._mapLegacyRollMode(mode); + } + return super.toMessage(messageData, opts); } } @@ -165,12 +170,12 @@ export class DGPercentileRoll extends DGRoll { const sign = mod > 0 ? "Positive" : "Negative"; return { action: `roll${Math.abs(mod)}${sign}`, - label: String(mod), + label: mod > 0 ? `+${mod}` : String(mod), callback: (event, button, dialog) => { try { - const rollMode = + const messageMode = dialog.element.querySelector("[name='rollMode']")?.value; - resolve({ targetModifier: mod, rollMode }); + resolve({ targetModifier: mod, messageMode }); } catch (ex) { reject(console.log(ex)); } @@ -197,7 +202,7 @@ export class DGPercentileRoll extends DGRoll { "[name='targetModifier']", )?.value; // this is text as a heads up - const rollMode = + const messageMode = dialog.element.querySelector("[name='rollMode']")?.value; const plusMinus = dialog.element.querySelector( @@ -222,7 +227,7 @@ export class DGPercentileRoll extends DGRoll { }); } } - resolve({ targetModifier, rollMode }); + resolve({ targetModifier, messageMode }); } catch (ex) { reject(console.log(ex)); } @@ -253,7 +258,7 @@ export class DGPercentileRoll extends DGRoll { (this.type === "sanity" || this.key === "ritual") && !game.user.isGM ) { - this.options.rollMode = "blindroll"; + this.options.messageMode = "blind"; } const diceSoNice = @@ -743,7 +748,7 @@ export class DGDamageRoll extends DGRoll { const modifiedBaseRoll = dialog.element.querySelector( "[name='originalFormula']", )?.value; // this is text as a heads up - const rollMode = dialog.element.querySelector( + const messageMode = dialog.element.querySelector( "[name='targetRollMode']", )?.value; @@ -758,7 +763,7 @@ export class DGDamageRoll extends DGRoll { newFormula += modifiedBaseRoll + innerModifier.trim(); } - resolve({ newFormula, rollMode }); + resolve({ newFormula, messageMode }); } catch (ex) { reject(console.log(ex)); } @@ -845,3 +850,103 @@ export class DGSanityDamageRoll extends DGRoll { return [lowResult?.result, highResult?.result]; } } + +/** + * Build a Delta Green roll from a click target's dataset. + * + * @param {DOMStringMap} dataset + * @param {object} params + * @param {Actor|null} [params.actor] + * @param {Item|null} [params.item] + * @param {HTMLElement} [params.element] + * @param {"actor"|"item"} [params.sanityDamageSource] + * @returns {Roll} + */ +export function createDGRollFromDataset( + dataset, + { actor = null, item = null, element = null, sanityDamageSource = "actor" } = {}, +) { + const rollOptions = { + rollType: dataset.rolltype, + key: dataset.key, + actor, + specialTrainingName: dataset?.name || null, + item, + }; + + let roll = new Roll("1d100", {}); + switch (dataset.rolltype) { + case "stat": + case "skill": + case "sanity": + case "special-training": + case "weapon": + roll = new DGPercentileRoll("1D100", {}, rollOptions); + break; + case "lethality": + roll = new DGLethalityRoll("1D100", {}, rollOptions); + break; + case "damage": { + let diceFormula = item.system.damage; + const { skill } = item.system; + if ( + actor && + (actor.type === "agent" || actor.type === "npc") && + (skill === "unarmed_combat" || skill === "melee_weapons") + ) { + diceFormula += actor.system.statistics.str.meleeDamageBonusFormula; + } + roll = new DGDamageRoll(diceFormula, {}, rollOptions); + break; + } + case "sanity-damage": { + let successLoss; + let failedLoss; + if (sanityDamageSource === "item" && item) { + const isLearntDamage = element?.hasAttribute("data-san-on-learn"); + const sanityData = isLearntDamage + ? item.system.learnedSanity + : item.system.sanity; + ({ successLoss, failedLoss } = sanityData); + } else if (actor) { + ({ successLoss, failedLoss } = actor.system.sanity); + } else { + break; + } + const combinedFormula = `{${successLoss}, ${failedLoss}}`; + roll = new DGSanityDamageRoll(combinedFormula, {}, rollOptions); + break; + } + default: + break; + } + return roll; +} + +/** + * Show an optional modifier dialog, evaluate, and send a roll to chat. + * + * @param {Event|object} event + * @param {Roll} roll + * @returns {Promise} + */ +export async function processDGRoll(event, roll) { + const shiftKey = event?.shiftKey ?? false; + const which = event?.which ?? 0; + + if (shiftKey || which === 3) { + if (!(roll instanceof DGSanityDamageRoll)) { + const dialogData = await roll.showDialog(); + if (!dialogData) return; + if (dialogData.newFormula) { + roll = new DGDamageRoll(dialogData.newFormula, {}, roll.options); + } + roll.modifier += dialogData.targetModifier; + if (dialogData.messageMode) { + roll.options.messageMode = dialogData.messageMode; + } + } + } + await roll.evaluate(); + roll.toChat(); +} diff --git a/module/settings.js b/module/settings.js index 2e3ad25..91c4a2e 100644 --- a/module/settings.js +++ b/module/settings.js @@ -1,6 +1,7 @@ /* eslint-disable max-classes-per-file */ import DG, { BASE_TEMPLATE_PATH } from "./config.js"; import DGActorSheet from "./sheets/base-actor-sheet.js"; +import DGItemSheet from "./sheets/base-item-sheet.js"; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; @@ -329,7 +330,7 @@ export default function registerSystemSettings() { default: "hover", onChange: () => { foundry.applications.instances.forEach((app) => { - if (app instanceof DGActorSheet) { + if (app instanceof DGActorSheet || app instanceof DGItemSheet) { app.render(); } }); diff --git a/module/sheets/agent-sheet.js b/module/sheets/agent-sheet.js index cd406dd..d98b059 100644 --- a/module/sheets/agent-sheet.js +++ b/module/sheets/agent-sheet.js @@ -308,7 +308,7 @@ export default class DGAgentSheet extends DGActorSheet { }), content, flavor, - rollMode: game.settings.get("core", "rollMode"), + messageMode: game.settings.get("core", "messageMode"), }; return ChatMessage.create(chatData); diff --git a/module/sheets/base-actor-sheet.js b/module/sheets/base-actor-sheet.js index 5b2b651..073bec2 100644 --- a/module/sheets/base-actor-sheet.js +++ b/module/sheets/base-actor-sheet.js @@ -1,9 +1,8 @@ import DG from "../config.js"; import { - DGDamageRoll, - DGLethalityRoll, DGPercentileRoll, - DGSanityDamageRoll, + createDGRollFromDataset, + processDGRoll, } from "../roll/roll.js"; import DGSheetMixin from "./base-sheet.js"; @@ -1221,52 +1220,14 @@ export default class DGActorSheet extends DGSheetMixin(ActorSheetV2) { static async _onRoll(event, target) { if (target.classList.contains("not-rollable") || event.which === 2) return; - const { dataset } = target; - const item = this.actor.items.get(dataset.iid); - const rollOptions = { - rollType: dataset.rolltype, - key: dataset.key, + const item = this.actor.items.get(target.dataset.iid); + const roll = createDGRollFromDataset(target.dataset, { actor: this.actor, - specialTrainingName: dataset?.name || null, // Only applies to Special Training Rolls item, - }; - - // Create a default 1d100 roll just in case. - let roll = new Roll("1d100", {}); - switch (dataset.rolltype) { - case "stat": - case "skill": - case "sanity": - case "special-training": - case "weapon": - roll = new DGPercentileRoll("1D100", {}, rollOptions); - break; - case "lethality": - roll = new DGLethalityRoll("1D100", {}, rollOptions); - break; - case "damage": { - let diceFormula = item.system.damage; - const { skill } = item.system; - if ( - (this.actor.type === "agent" || this.actor.type === "npc") && - (skill === "unarmed_combat" || skill === "melee_weapons") - ) { - diceFormula += - this.actor.system.statistics.str.meleeDamageBonusFormula; - } - roll = new DGDamageRoll(diceFormula, {}, rollOptions); - break; - } - case "sanity-damage": { - const { successLoss, failedLoss } = this.actor.system.sanity; - const combinedFormula = `{${successLoss}, ${failedLoss}}`; - roll = new DGSanityDamageRoll(combinedFormula, {}, rollOptions); - break; - } - default: - break; - } - this.processRoll(event, roll, rollOptions); + element: target, + sanityDamageSource: "actor", + }); + await this.processRoll(event, roll); } // This only exists to give a chance to activate the modifier dialogue if desired @@ -1288,7 +1249,9 @@ export default class DGActorSheet extends DGSheetMixin(ActorSheetV2) { const dialogData = await roll.showDialog(); if (!dialogData) return; roll.modifier += dialogData.targetModifier; - roll.options.rollMode = dialogData.rollMode; + if (dialogData.messageMode) { + roll.options.messageMode = dialogData.messageMode; + } } // Evaluate the roll. await roll.evaluate(); @@ -1308,23 +1271,7 @@ export default class DGActorSheet extends DGSheetMixin(ActorSheetV2) { * @async */ async processRoll(event, roll) { - // Open dialog if user requests it (no dialog for Sanity Damage rolls) - if ( - (event.shiftKey || event.which === 3) && - !(roll instanceof DGSanityDamageRoll) - ) { - const dialogData = await roll.showDialog(); - if (!dialogData) return; - if (dialogData.newFormula) { - roll = new DGDamageRoll(dialogData.newFormula, {}, roll.options); - } - roll.modifier += dialogData.targetModifier; - roll.options.rollMode = dialogData.rollMode; - } - // Evaluate the roll. - await roll.evaluate(); - // Send the roll to chat. - roll.toChat(); + return processDGRoll(event, roll); } /** Handler for generic toggle events */ diff --git a/module/sheets/base-item-sheet.js b/module/sheets/base-item-sheet.js new file mode 100644 index 0000000..25f6c6c --- /dev/null +++ b/module/sheets/base-item-sheet.js @@ -0,0 +1,245 @@ +import { BASE_TEMPLATE_PATH } from "../config.js"; +import DGSheetMixin from "./base-sheet.js"; +import { createDGRollFromDataset, processDGRoll } from "../roll/roll.js"; + +const { ItemSheetV2 } = foundry.applications.sheets; +const ITEM_PARTS_PATH = `${BASE_TEMPLATE_PATH}/item/parts`; + +/** Tab and part layout per item type. */ +const ITEM_SHEET_LAYOUT = /** @type {const} */ ({ + weapon: { + parts: { + header: { template: `${ITEM_PARTS_PATH}/weapon-header.html` }, + description: { + template: `${ITEM_PARTS_PATH}/weapon-description.html`, + scrollable: [""], + }, + }, + }, + gear: { + parts: { + header: { template: `${ITEM_PARTS_PATH}/gear-header.html` }, + description: { + template: `${ITEM_PARTS_PATH}/gear-description.html`, + scrollable: [""], + }, + }, + }, + armor: { + parts: { + header: { template: `${ITEM_PARTS_PATH}/armor-header.html` }, + description: { + template: `${ITEM_PARTS_PATH}/armor-description.html`, + scrollable: [""], + }, + }, + }, + bond: { + parts: { + header: { template: `${ITEM_PARTS_PATH}/bond-header.html` }, + description: { + template: `${ITEM_PARTS_PATH}/bond-description.html`, + scrollable: [""], + }, + }, + }, + motivation: { + tabs: { + initial: "attributes", + tabs: [ + { + id: "attributes", + label: "DG.ItemWindow.Motivations.Attributes", + }, + { + id: "description", + label: "DG.ItemWindow.Motivations.Description", + }, + ], + }, + parts: { + tabs: { template: "templates/generic/tab-navigation.hbs" }, + attributes: { template: `${ITEM_PARTS_PATH}/motivation-attributes.html` }, + description: { + template: `${ITEM_PARTS_PATH}/motivation-description.html`, + scrollable: [""], + }, + }, + }, + tome: { + tabs: { + initial: "description", + tabs: [ + { id: "description", label: "DG.ItemWindow.Tome.Description" }, + { + id: "handler", + label: "DG.ItemWindow.Tome.HandlerOnlyTabHeader", + gmOnly: true, + }, + ], + }, + parts: { + header: { template: `${ITEM_PARTS_PATH}/tome-header.html` }, + tabs: { template: "templates/generic/tab-navigation.hbs" }, + description: { + template: `${ITEM_PARTS_PATH}/tome-description.html`, + scrollable: [""], + }, + handler: { + template: `${ITEM_PARTS_PATH}/tome-handler.html`, + scrollable: [""], + }, + }, + }, + ritual: { + tabs: { + initial: "description", + tabs: [ + { id: "description", label: "DG.ItemWindow.Tome.Description" }, + { + id: "handler", + label: "DG.ItemWindow.Tome.HandlerOnlyTabHeader", + gmOnly: true, + }, + ], + }, + parts: { + header: { template: `${ITEM_PARTS_PATH}/ritual-header.html` }, + tabs: { template: "templates/generic/tab-navigation.hbs" }, + description: { + template: `${ITEM_PARTS_PATH}/ritual-description.html`, + scrollable: [""], + }, + handler: { + template: `${ITEM_PARTS_PATH}/ritual-handler.html`, + scrollable: [""], + }, + }, + }, +}); + +/** + * @extends {DGSheetMixin(ItemSheetV2)} + */ +export default class DGItemSheet extends DGSheetMixin(ItemSheetV2) { + /** @override */ + static DEFAULT_OPTIONS = /** @type {const} */ ({ + classes: ["item"], + position: { width: 520, height: 600 }, + actions: { + roll: DGItemSheet._onRoll, + toggleTomeRevealed: DGItemSheet._toggleTomeRevealed, + }, + }); + + /** @inheritdoc */ + get item() { + return this.document; + } + + /** @inheritdoc */ + _configureRenderParts(options) { + const layout = ITEM_SHEET_LAYOUT[this.item.type]; + if (!layout) return super._configureRenderParts(options); + + const parts = foundry.utils.deepClone(layout.parts); + if (layout.tabs) { + const tabIds = layout.tabs.tabs + .filter((tab) => !tab.gmOnly || game.user.isGM) + .map((tab) => tab.id); + for (const partId of Object.keys(parts)) { + if (partId === "tabs" || partId === "header") continue; + if (!tabIds.includes(partId)) delete parts[partId]; + } + } + Object.values(parts).forEach((p) => { + p.templates ??= []; + }); + return parts; + } + + /** @inheritdoc */ + _getTabsConfig(group) { + const layout = ITEM_SHEET_LAYOUT[this.item.type]; + if (!layout?.tabs) return super._getTabsConfig(group); + + const tabs = layout.tabs.tabs.filter((tab) => !tab.gmOnly || game.user.isGM); + return { ...layout.tabs, tabs }; + } + + /** @inheritdoc */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + + context.owner = this.document.isOwner; + context.enrichedDescription = + await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.item.system.description, + { async: true }, + ); + + if (this.item.type === "tome" || this.item.type === "ritual") { + context.enrichedHandlerNotes = + await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.item.system.handlerNotes, + { async: true }, + ); + } + + return context; + } + + /** @inheritdoc */ + async _onFirstRender(context, options) { + await super._onFirstRender(context, options); + this._setRightClickListeners(); + } + + /** + * Right-click on roll controls opens the modifier dialog. + * @returns {void} + */ + _setRightClickListeners() { + this.element.addEventListener("contextmenu", (event) => { + const target = event.target.closest("[data-action='roll']"); + if (!target) return; + event.preventDefault(); + DGItemSheet._onRoll.call(this, event, target); + }); + } + + /** + * @param {PointerEvent} event + * @param {HTMLElement} target + */ + static async _onRoll(event, target) { + if (event.which === 2) return; + + const roll = createDGRollFromDataset(target.dataset, { + actor: this.actor, + item: this.item, + element: target, + sanityDamageSource: "item", + }); + await processDGRoll(event, roll); + } + + /** + * Toggle whether tome/ritual details are visible to players. + * @param {PointerEvent} event + */ + static _toggleTomeRevealed(event) { + event.preventDefault(); + const revealed = !this.item.system.revealed; + this.item.update({ "system.revealed": revealed }); + } + + /** + * @param {Event|object} event + * @param {Roll} roll + * @returns {Promise} + */ + async processRoll(event, roll) { + return processDGRoll(event, roll); + } +} diff --git a/templates/actor/parts/gear-tab.html b/templates/actor/parts/gear-tab.html index 39e290a..be8482d 100644 --- a/templates/actor/parts/gear-tab.html +++ b/templates/actor/parts/gear-tab.html @@ -47,14 +47,15 @@ height="24" /> {{item.name}} - - - + @@ -80,10 +81,12 @@ {{/if}} {{#unless item.system.isLethal}} - + {{/unless}} {{#if item.system.isLethal }} - + {{/if}} @@ -370,13 +374,14 @@
{{localize 'DG.Gear.Rituals'}} - - - +
diff --git a/templates/actor/parts/header.html b/templates/actor/parts/header.html index d20d777..8606def 100644 --- a/templates/actor/parts/header.html +++ b/templates/actor/parts/header.html @@ -6,7 +6,6 @@ src="{{actor.img}}" alt="{{actor.name}}" data-action="editImage" - data-edit="img" height="150" width="100" />
diff --git a/templates/item/item-armor-sheet.html b/templates/item/item-armor-sheet.html deleted file mode 100644 index b7a7f33..0000000 --- a/templates/item/item-armor-sheet.html +++ /dev/null @@ -1,50 +0,0 @@ -
-
-
- - - -
- - {{localize 'DG.Bonds.Name'}}: - - - {{localize 'DG.ItemWindow.Armor.Expense'}}: - - - {{localize 'DG.ItemWindow.Armor.Equipped'}}: - - -
-
- -
- {{localize 'DG.ItemWindow.Armor.ArmorRating'}}: - -
- -
- - {{!-- Sheet Tab Navigation --}} - - - {{!-- Sheet Body --}} -
- - {{!-- Description Tab --}} -
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
-
-
-
diff --git a/templates/item/item-bond-sheet.html b/templates/item/item-bond-sheet.html deleted file mode 100644 index 23b59f7..0000000 --- a/templates/item/item-bond-sheet.html +++ /dev/null @@ -1,35 +0,0 @@ -
-
-
- -
- - {{localize 'DG.Bonds.Name'}}: - - - {{localize 'DG.Bonds.Score'}}: - - - {{localize 'DG.Bonds.Relationship'}}: - - -
-
-
- - {{!-- Sheet Tab Navigation --}} - - - {{!-- Sheet Body --}} -
- {{!-- Description Tab --}} -
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
-
-
-
diff --git a/templates/item/item-gear-sheet.html b/templates/item/item-gear-sheet.html deleted file mode 100644 index f656bf9..0000000 --- a/templates/item/item-gear-sheet.html +++ /dev/null @@ -1,47 +0,0 @@ -
-
-
- - - -
- - {{localize 'DG.Bonds.Name'}}: - - - - {{localize 'DG.ItemWindow.Gear.Expense'}}: - - - {{localize 'DG.ItemWindow.Gear.Equipped'}}: - - -
-
- -
- - {{!-- Sheet Tab Navigation --}} - - - {{!-- Sheet Body --}} -
- - {{!-- Description Tab --}} -
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
-
-
-
diff --git a/templates/item/item-motivation-sheet.html b/templates/item/item-motivation-sheet.html deleted file mode 100644 index 6bcbc20..0000000 --- a/templates/item/item-motivation-sheet.html +++ /dev/null @@ -1,66 +0,0 @@ -
- - {{!-- Sheet Tab Navigation --}} - - - {{!-- Sheet Body motivation-sheet-horizontal-grid-2col --}} -
- -
- -
- {{localize 'DG.ItemWindow.Motivations.IsDisorder'}}: - - {{localize 'DG.ItemWindow.Motivations.Motivation'}}: - - {{!-- Only Show the Disorders Section if Motivation Lost Checked --}} - {{#if item.system.crossedOut }} - - {{localize 'DG.ItemWindow.Motivations.Disorder'}}: - - {{localize 'DG.ItemWindow.Motivations.DisorderHasBeenCured'}}: - - {{/if}} - -
-
- -
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
-
- -
- -
diff --git a/templates/item/item-ritual-sheet.html b/templates/item/item-ritual-sheet.html deleted file mode 100644 index 50f49cc..0000000 --- a/templates/item/item-ritual-sheet.html +++ /dev/null @@ -1,113 +0,0 @@ -
-
-
- - -
- {{#if item.system.revealed}} - - {{#if (playerHasGamemasterPrivileges)}}{{/if}} - {{localize 'DG.Tome.Name'}}: - - - - {{localize 'DG.ItemWindow.Ritual.StudyTime'}}: - - - {{localize 'DG.ItemWindow.Ritual.ActivationTime'}}: - - - {{localize 'DG.ItemWindow.Ritual.ActivationCosts'}}: - - {{else}} - {{#if (playerHasGamemasterPrivileges)}} - - {{#if (playerHasGamemasterPrivileges)}}{{/if}} - {{localize 'DG.Tome.Name'}}: - - - - {{localize 'DG.ItemWindow.Ritual.StudyTime'}}: - - - {{localize 'DG.ItemWindow.Ritual.ActivationTime'}}: - - - {{localize 'DG.ItemWindow.Ritual.ActivationCosts'}}: - - {{else}} - - {{#if (playerHasGamemasterPrivileges)}}{{/if}} - {{localize 'DG.Tome.Name'}}: - - ??? - - {{localize 'DG.ItemWindow.Ritual.StudyTime'}}: - ??? - - {{localize 'DG.ItemWindow.Ritual.ActivationTime'}}: - ??? - - {{localize 'DG.ItemWindow.Ritual.ActivationCosts'}}: - ??? - {{/if}} - {{/if}} -
- -
- -
- - {{!-- Sheet Tab Navigation --}} - - - {{!-- Sheet Body --}} -
- - {{!-- Description Tab --}} -
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
-
- - {{!-- Handler Notes Tab --}} - {{#if (playerHasGamemasterPrivileges)}} -
-
- {{localize 'DG.ItemWindow.Tome.UnnaturalIncrease'}}: - - - {{localize 'DG.ItemWindow.Ritual.LearnedSanityDamage'}}: - - - / - - - - - {{localize 'DG.ItemWindow.Ritual.ActivationSanityDamage'}}: - - - / - - - -
- - {{localize 'DG.ItemWindow.Tome.HandlerNotes'}}: - -
-
- {{editor enrichedHandlerNotes target="system.handlerNotes" engine="prosemirror" button=true owner=owner editable=editable}} -
-
-
- {{/if}} -
-
diff --git a/templates/item/item-tome-sheet.html b/templates/item/item-tome-sheet.html deleted file mode 100644 index b85bccd..0000000 --- a/templates/item/item-tome-sheet.html +++ /dev/null @@ -1,99 +0,0 @@ -
-
-
- - -
- {{#if item.system.revealed}} - - {{#if (playerHasGamemasterPrivileges)}}{{/if}} - {{localize 'DG.Tome.Name'}}: - - - - {{localize 'DG.Tome.Language'}}: - - - {{localize 'DG.Tome.StudyTime'}}: - - {{else}} - {{#if (playerHasGamemasterPrivileges)}} - - {{#if (playerHasGamemasterPrivileges)}}{{/if}} - {{localize 'DG.Tome.Name'}}: - - - - {{localize 'DG.Tome.Language'}}: - - - {{localize 'DG.Tome.StudyTime'}}: - - {{else}} - - {{#if (playerHasGamemasterPrivileges)}}{{/if}} - {{localize 'DG.Tome.Name'}}: - - ??? - - {{localize 'DG.Tome.Language'}}: - ??? - - {{localize 'DG.Tome.StudyTime'}}: - ??? - {{/if}} - {{/if}} -
- -
- -
- - {{!-- Sheet Tab Navigation --}} - - - {{!-- Sheet Body --}} -
- - {{!-- Description Tab --}} -
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
-
- - {{!-- Handler Notes Tab --}} - {{#if (playerHasGamemasterPrivileges)}} -
-
- {{localize 'DG.ItemWindow.Tome.UnnaturalIncrease'}}: - - - {{localize 'DG.ItemWindow.Tome.OccultIncrease'}}: - - - {{localize 'DG.ItemWindow.Tome.StudySanityDamage'}}: - - - / - - - -
- - {{localize 'DG.ItemWindow.Tome.HandlerNotes'}}: - -
-
- {{editor enrichedHandlerNotes target="system.handlerNotes" engine="prosemirror" button=true owner=owner editable=editable}} -
-
-
- {{/if}} -
-
diff --git a/templates/item/item-weapon-sheet.html b/templates/item/item-weapon-sheet.html deleted file mode 100644 index cc03669..0000000 --- a/templates/item/item-weapon-sheet.html +++ /dev/null @@ -1,127 +0,0 @@ -
-
-
- - - -
- - {{localize 'DG.Bonds.Name'}}: - - - {{localize 'DG.ItemWindow.Weapons.Expense'}}: - - - {{localize 'DG.ItemWindow.Gear.Equipped'}}: - - -
-
- -
- -
- - {{localize 'DG.ItemWindow.Weapons.Skill'}}: - -
- -
- {{#if_eq item.system.skill 'custom'}} - {{localize 'DG.ItemWindow.Weapons.CustomSkillTarget'}}: - - {{/if_eq}} -
- - -
- - {{localize 'DG.ItemWindow.Weapons.ArmorPiercing'}}: - - - {{localize 'DG.ItemWindow.Weapons.SkillModifier'}}: - - - {{localize 'DG.ItemWindow.Weapons.IsLethality'}}: - - -
- - -
- - {{localize 'DG.ItemWindow.Weapons.Range'}}: - - - {{localize 'DG.ItemWindow.Weapons.KillRadius'}}: - - - {{!-- If a weapon is 'Lethal', need to show the d100% Lethality input, otherwise only show the regular Damage formula (e.g. 1D6, 1D4+2, etc...) --}} - - {{#unless item.system.isLethal}} - - {{localize 'DG.ItemWindow.Weapons.Damage'}}: - - - {{/unless}} - - {{#if item.system.isLethal}} - - {{localize 'DG.ItemWindow.Weapons.Lethality'}} %: - - - {{/if}} - -
- -
- -
- {{localize 'DG.ItemWindow.Weapons.Ammo'}}: - -
-
-
- - {{!-- Sheet Tab Navigation --}} - - - {{!-- Sheet Body --}} -
- - {{!-- Description Tab --}} -
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
-
-
-
diff --git a/templates/item/parts/armor-description.html b/templates/item/parts/armor-description.html new file mode 100644 index 0000000..6950bf9 --- /dev/null +++ b/templates/item/parts/armor-description.html @@ -0,0 +1,7 @@ +
+
+
+ {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} +
+
+
diff --git a/templates/item/parts/armor-header.html b/templates/item/parts/armor-header.html new file mode 100644 index 0000000..9e6cdfc --- /dev/null +++ b/templates/item/parts/armor-header.html @@ -0,0 +1,32 @@ +
+
+ + + +
+ + {{localize 'DG.Bonds.Name'}}: + + + {{localize 'DG.ItemWindow.Armor.Expense'}}: + + + {{localize 'DG.ItemWindow.Armor.Equipped'}}: + + +
+
+ +
+ {{localize 'DG.ItemWindow.Armor.ArmorRating'}}: + +
+ +
diff --git a/templates/item/parts/bond-description.html b/templates/item/parts/bond-description.html new file mode 100644 index 0000000..6950bf9 --- /dev/null +++ b/templates/item/parts/bond-description.html @@ -0,0 +1,7 @@ +
+
+
+ {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} +
+
+
diff --git a/templates/item/parts/bond-header.html b/templates/item/parts/bond-header.html new file mode 100644 index 0000000..fdbe351 --- /dev/null +++ b/templates/item/parts/bond-header.html @@ -0,0 +1,18 @@ +
+
+ +
+ + {{localize 'DG.Bonds.Name'}}: + + + {{localize 'DG.Bonds.Score'}}: + + + {{localize 'DG.Bonds.Relationship'}}: + + +
+
+
diff --git a/templates/item/parts/gear-description.html b/templates/item/parts/gear-description.html new file mode 100644 index 0000000..6950bf9 --- /dev/null +++ b/templates/item/parts/gear-description.html @@ -0,0 +1,7 @@ +
+
+
+ {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} +
+
+
diff --git a/templates/item/parts/gear-header.html b/templates/item/parts/gear-header.html new file mode 100644 index 0000000..244fdfa --- /dev/null +++ b/templates/item/parts/gear-header.html @@ -0,0 +1,27 @@ +
+
+ + + +
+ + {{localize 'DG.Bonds.Name'}}: + + + {{localize 'DG.ItemWindow.Gear.Expense'}}: + + + {{localize 'DG.ItemWindow.Gear.Equipped'}}: + + +
+
+ +
diff --git a/templates/item/parts/motivation-attributes.html b/templates/item/parts/motivation-attributes.html new file mode 100644 index 0000000..2b3b3af --- /dev/null +++ b/templates/item/parts/motivation-attributes.html @@ -0,0 +1,42 @@ +
+
+ +
+ {{localize 'DG.ItemWindow.Motivations.IsDisorder'}}: + + {{localize 'DG.ItemWindow.Motivations.Motivation'}}: + + {{#if item.system.crossedOut }} + + {{localize 'DG.ItemWindow.Motivations.Disorder'}}: + + {{localize 'DG.ItemWindow.Motivations.DisorderHasBeenCured'}}: + + {{/if}} + +
+
diff --git a/templates/item/parts/motivation-description.html b/templates/item/parts/motivation-description.html new file mode 100644 index 0000000..e54c16e --- /dev/null +++ b/templates/item/parts/motivation-description.html @@ -0,0 +1,8 @@ +
+
+ {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} +
+
+
diff --git a/templates/item/parts/ritual-description.html b/templates/item/parts/ritual-description.html new file mode 100644 index 0000000..99c658f --- /dev/null +++ b/templates/item/parts/ritual-description.html @@ -0,0 +1,9 @@ +
+
+
+ {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} +
+
+
diff --git a/templates/item/parts/ritual-handler.html b/templates/item/parts/ritual-handler.html new file mode 100644 index 0000000..1a04cd2 --- /dev/null +++ b/templates/item/parts/ritual-handler.html @@ -0,0 +1,32 @@ +
+
+ {{localize 'DG.ItemWindow.Tome.UnnaturalIncrease'}}: + + + {{localize 'DG.ItemWindow.Ritual.LearnedSanityDamage'}}: + + + / + + + + + {{localize 'DG.ItemWindow.Ritual.ActivationSanityDamage'}}: + + + / + + + +
+ + {{localize 'DG.ItemWindow.Tome.HandlerNotes'}}: + +
+
+ {{editor enrichedHandlerNotes target="system.handlerNotes" engine="prosemirror" button=true owner=owner editable=editable}} +
+
+
diff --git a/templates/item/parts/ritual-header.html b/templates/item/parts/ritual-header.html new file mode 100644 index 0000000..77f0af5 --- /dev/null +++ b/templates/item/parts/ritual-header.html @@ -0,0 +1,55 @@ +
+
+ + +
+ {{#if item.system.revealed}} + + {{#if (playerHasGamemasterPrivileges)}}{{/if}} + {{localize 'DG.Tome.Name'}}: + + + + {{localize 'DG.ItemWindow.Ritual.StudyTime'}}: + + + {{localize 'DG.ItemWindow.Ritual.ActivationTime'}}: + + + {{localize 'DG.ItemWindow.Ritual.ActivationCosts'}}: + + {{else}} + {{#if (playerHasGamemasterPrivileges)}} + + + {{localize 'DG.Tome.Name'}}: + + + + {{localize 'DG.ItemWindow.Ritual.StudyTime'}}: + + + {{localize 'DG.ItemWindow.Ritual.ActivationTime'}}: + + + {{localize 'DG.ItemWindow.Ritual.ActivationCosts'}}: + + {{else}} + {{localize 'DG.Tome.Name'}}: + ??? + + {{localize 'DG.ItemWindow.Ritual.StudyTime'}}: + ??? + + {{localize 'DG.ItemWindow.Ritual.ActivationTime'}}: + ??? + + {{localize 'DG.ItemWindow.Ritual.ActivationCosts'}}: + ??? + {{/if}} + {{/if}} +
+ +
+ +
diff --git a/templates/item/parts/tome-description.html b/templates/item/parts/tome-description.html new file mode 100644 index 0000000..99c658f --- /dev/null +++ b/templates/item/parts/tome-description.html @@ -0,0 +1,9 @@ +
+
+
+ {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} +
+
+
diff --git a/templates/item/parts/tome-handler.html b/templates/item/parts/tome-handler.html new file mode 100644 index 0000000..1b02cfd --- /dev/null +++ b/templates/item/parts/tome-handler.html @@ -0,0 +1,27 @@ +
+
+ {{localize 'DG.ItemWindow.Tome.UnnaturalIncrease'}}: + + + {{localize 'DG.ItemWindow.Tome.OccultIncrease'}}: + + + {{localize 'DG.ItemWindow.Tome.StudySanityDamage'}}: + + + / + + + +
+ + {{localize 'DG.ItemWindow.Tome.HandlerNotes'}}: + +
+
+ {{editor enrichedHandlerNotes target="system.handlerNotes" engine="prosemirror" button=true owner=owner editable=editable}} +
+
+
diff --git a/templates/item/parts/tome-header.html b/templates/item/parts/tome-header.html new file mode 100644 index 0000000..6224c45 --- /dev/null +++ b/templates/item/parts/tome-header.html @@ -0,0 +1,46 @@ +
+
+ + +
+ {{#if item.system.revealed}} + + {{#if (playerHasGamemasterPrivileges)}}{{/if}} + {{localize 'DG.Tome.Name'}}: + + + + {{localize 'DG.Tome.Language'}}: + + + {{localize 'DG.Tome.StudyTime'}}: + + {{else}} + {{#if (playerHasGamemasterPrivileges)}} + + + {{localize 'DG.Tome.Name'}}: + + + + {{localize 'DG.Tome.Language'}}: + + + {{localize 'DG.Tome.StudyTime'}}: + + {{else}} + {{localize 'DG.Tome.Name'}}: + ??? + + {{localize 'DG.Tome.Language'}}: + ??? + + {{localize 'DG.Tome.StudyTime'}}: + ??? + {{/if}} + {{/if}} +
+ +
+ +
diff --git a/templates/item/parts/weapon-description.html b/templates/item/parts/weapon-description.html new file mode 100644 index 0000000..cf1b564 --- /dev/null +++ b/templates/item/parts/weapon-description.html @@ -0,0 +1,7 @@ +
+
+
+ {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} +
+
+
diff --git a/templates/item/parts/weapon-header.html b/templates/item/parts/weapon-header.html new file mode 100644 index 0000000..2c66421 --- /dev/null +++ b/templates/item/parts/weapon-header.html @@ -0,0 +1,101 @@ +
+
+ + + +
+ + {{localize 'DG.Bonds.Name'}}: + + + {{localize 'DG.ItemWindow.Weapons.Expense'}}: + + + {{localize 'DG.ItemWindow.Gear.Equipped'}}: + + +
+
+ +
+ +
+ + {{localize 'DG.ItemWindow.Weapons.Skill'}}: + +
+ +
+ {{#if_eq item.system.skill 'custom'}} + {{localize 'DG.ItemWindow.Weapons.CustomSkillTarget'}}: + + {{/if_eq}} +
+ +
+ + {{localize 'DG.ItemWindow.Weapons.ArmorPiercing'}}: + + + {{localize 'DG.ItemWindow.Weapons.SkillModifier'}}: + + + {{localize 'DG.ItemWindow.Weapons.IsLethality'}}: + + +
+ +
+ + {{localize 'DG.ItemWindow.Weapons.Range'}}: + + + {{localize 'DG.ItemWindow.Weapons.KillRadius'}}: + + + {{#unless item.system.isLethal}} + {{localize 'DG.ItemWindow.Weapons.Damage'}}: + + {{/unless}} + + {{#if item.system.isLethal}} + {{localize 'DG.ItemWindow.Weapons.Lethality'}} %: + + {{/if}} + +
+ +
+ +
+ {{localize 'DG.ItemWindow.Weapons.Ammo'}}: + +
+
+