From 3a0a4673ad72dc9cdba09c89fff9a58bc20fb5d5 Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:03:50 +0200 Subject: [PATCH 1/2] Feature/112 items use action datamodel (#127) * Create new actions classes * actions types - attack roll * fixes before merge * First PR * Add daggerheart.css to gitignore * Update ToDo * Remove console log * Fixed chat /dr roll * Remove jQuery * Fixed so the different chat themes work again * Fixed duality roll buttons * Fix to advantage/disadvantage shortcut * Extand action to other item types * Roll fixes * Fixes to adversary rolls * resources * Fixed adversary dice --------- Co-authored-by: WBHarry --- .gitignore | 3 +- daggerheart.mjs | 115 +++--- lang/en.json | 45 ++- module/applications/chatMessage.mjs | 4 + module/applications/config/Action.mjs | 135 +++++-- .../applications/npcRollSelectionDialog.mjs | 2 +- module/applications/rollSelectionDialog.mjs | 21 +- module/applications/settings.mjs | 4 +- .../settings/appearanceSettings.mjs | 14 + module/applications/sheets/adversary.mjs | 94 ++--- module/applications/sheets/character.mjs | 89 ++-- module/applications/sheets/item.mjs | 129 ++++++ module/applications/sheets/items/armor.mjs | 50 +-- .../applications/sheets/items/consumable.mjs | 50 +-- .../applications/sheets/items/domainCard.mjs | 91 +---- module/applications/sheets/items/feature.mjs | 71 +--- .../sheets/items/miscellaneous.mjs | 50 +-- module/applications/sheets/items/weapon.mjs | 52 +-- module/config/actionConfig.mjs | 38 +- module/config/generalConfig.mjs | 25 ++ module/data/_module.mjs | 1 + module/data/action.mjs | 34 -- module/data/action/_module.mjs | 13 + module/data/action/action.mjs | 380 ++++++++++++++++++ module/data/action/actionDice.mjs | 49 +++ module/data/chat-message/adversaryRoll.mjs | 48 +-- module/data/chat-message/damageRoll.mjs | 23 +- module/data/chat-message/dualityRoll.mjs | 24 +- module/data/fields/actionField.mjs | 41 ++ module/data/item/_module.mjs | 2 +- module/data/item/domainCard.mjs | 6 +- module/data/item/feature.mjs | 4 +- module/data/item/weapon.mjs | 14 +- module/documents/actor.mjs | 275 +++++++------ module/documents/item.mjs | 48 +++ module/enrichers/DualityRollEnricher.mjs | 17 +- module/helpers/utils.mjs | 23 +- styles/application.less | 31 ++ styles/daggerheart.css | 59 +++ styles/daggerheart.less | 35 ++ templates/chat/adversary-attack-roll.hbs | 32 +- templates/chat/adversary-roll.hbs | 25 +- templates/chat/attack-roll.hbs | 52 +-- templates/chat/damage-roll.hbs | 14 +- templates/chat/duality-roll.hbs | 48 +-- .../sheets/actors/adversary/information.hbs | 12 +- templates/sheets/actors/adversary/main.hbs | 1 + .../sheets/character/sections/inventory.hbs | 2 +- templates/views/action.hbs | 57 +-- templates/views/actionSelect.hbs | 13 + templates/views/actionType.hbs | 13 + templates/views/actionTypes/cost.hbs | 21 + templates/views/actionTypes/damage.hbs | 36 ++ templates/views/actionTypes/effect.hbs | 19 + templates/views/actionTypes/healing.hbs | 21 + templates/views/actionTypes/range-target.hbs | 13 + templates/views/actionTypes/resource.hbs | 14 + templates/views/actionTypes/roll.hbs | 10 + templates/views/actionTypes/target.hbs | 8 + templates/views/actionTypes/uses.hbs | 12 + templates/views/actionTypes/uuid.hbs | 9 + templates/views/rollSelection.hbs | 4 +- 62 files changed, 1712 insertions(+), 933 deletions(-) create mode 100644 module/applications/sheets/item.mjs delete mode 100755 module/data/action.mjs create mode 100644 module/data/action/_module.mjs create mode 100644 module/data/action/action.mjs create mode 100644 module/data/action/actionDice.mjs create mode 100644 module/data/fields/actionField.mjs create mode 100644 templates/views/actionSelect.hbs create mode 100644 templates/views/actionType.hbs create mode 100644 templates/views/actionTypes/cost.hbs create mode 100644 templates/views/actionTypes/damage.hbs create mode 100644 templates/views/actionTypes/effect.hbs create mode 100644 templates/views/actionTypes/healing.hbs create mode 100644 templates/views/actionTypes/range-target.hbs create mode 100644 templates/views/actionTypes/resource.hbs create mode 100644 templates/views/actionTypes/roll.hbs create mode 100644 templates/views/actionTypes/target.hbs create mode 100644 templates/views/actionTypes/uses.hbs create mode 100644 templates/views/actionTypes/uuid.hbs diff --git a/.gitignore b/.gitignore index 48fb3ad3..a5c96aa8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules /packs Build /build -foundry \ No newline at end of file +foundry +styles/daggerheart.css \ No newline at end of file diff --git a/daggerheart.mjs b/daggerheart.mjs index fc32e152..f2fef669 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -134,43 +134,24 @@ Hooks.on(socketEvent.GMUpdate, async (action, uuid, update) => { }); const renderDualityButton = async event => { - const button = event.currentTarget; - const attributeValue = button.dataset.attribute?.toLowerCase(); - - const target = getCommandTarget(); + const button = event.currentTarget, + traitValue = button.dataset.trait?.toLowerCase(), + target = getCommandTarget(); if (!target) return; - const rollModifier = attributeValue ? target.system.attributes[attributeValue].data.value : null; - const { roll, hope, fear, advantage, disadvantage, modifiers } = await target.diceRoll({ - title: button.dataset.label, - value: rollModifier - }); - - const systemData = new DHDualityRoll({ - title: button.dataset.label, - origin: target.id, - roll: roll._formula, - modifiers: modifiers, - hope: hope, - fear: fear, - advantage: advantage, - disadvantage: disadvantage - }); - - const cls = getDocumentClass('ChatMessage'); - const msgData = { - type: 'dualityRoll', - sound: CONFIG.sounds.dice, - system: systemData, - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/duality-roll.hbs', - systemData - ), - rolls: [roll] + const config = { + event: event, + title: button.dataset.title, + roll: { + modifier: traitValue ? target.system.traits[traitValue].value : null, + label: button.dataset.label, + type: button.dataset.actionType ?? null // Need check + }, + chatMessage: { + template: 'systems/daggerheart/templates/chat/duality-roll.hbs' + } }; - - await cls.create(msgData); + await target.diceRoll(config); }; Hooks.on('renderChatMessageHTML', (_, element) => { @@ -199,64 +180,54 @@ Hooks.on('chatMessage', (_, message) => { return false; } - const attributeValue = rollCommand.attribute?.toLowerCase(); + const traitValue = rollCommand.trait?.toLowerCase(); + const advantageState = rollCommand.advantage ? true : rollCommand.disadvantage ? false : null; // Target not required if an attribute is not used. - const target = attributeValue ? getCommandTarget() : undefined; - if (target || !attributeValue) { + const target = traitValue ? getCommandTarget() : undefined; + if (target || !traitValue) { new Promise(async (resolve, reject) => { - const attribute = target ? target.system.attributes[attributeValue] : undefined; - if (attributeValue && !attribute) { + const trait = target ? target.system.traits[traitValue] : undefined; + if (traitValue && !trait) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.AttributeFaulty')); reject(); return; } - const title = attributeValue + const title = traitValue ? game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { - ability: game.i18n.localize(abilities[attributeValue].label) + ability: game.i18n.localize(abilities[traitValue].label) }) : game.i18n.localize('DAGGERHEART.General.Duality'); const hopeAndFearRoll = `1${rollCommand.hope ?? 'd12'}+1${rollCommand.fear ?? 'd12'}`; - const advantageRoll = `${rollCommand.advantage && !rollCommand.disadvantage ? '+d6' : rollCommand.disadvantage && !rollCommand.advantage ? '-d6' : ''}`; - const attributeRoll = `${attribute?.data?.value ? `${attribute.data.value > 0 ? `+${attribute.data.value}` : `${attribute.data.value}`}` : ''}`; - const roll = new Roll(`${hopeAndFearRoll}${advantageRoll}${attributeRoll}`); - await roll.evaluate(); + const advantageRoll = `${advantageState === true ? '+d6' : advantageState === false ? '-d6' : ''}`; + const attributeRoll = `${trait?.value ? `${trait.value > 0 ? `+${trait.value}` : `${trait.value}`}` : ''}`; + const roll = await Roll.create(`${hopeAndFearRoll}${advantageRoll}${attributeRoll}`).evaluate(); - setDiceSoNiceForDualityRoll( - roll, - rollCommand.advantage && !rollCommand.disadvantage, - rollCommand.disadvantage && !rollCommand.advantage - ); + setDiceSoNiceForDualityRoll(roll, advantageState); resolve({ roll, - attribute: attribute + trait: trait ? { - value: attribute.data.value, - label: `${game.i18n.localize(abilities[attributeValue].label)} ${attribute.data.value >= 0 ? `+` : ``}${attribute.data.value}` + value: trait.value, + label: `${game.i18n.localize(abilities[traitValue].label)} ${trait.value >= 0 ? `+` : ``}${trait.value}` } : undefined, title }); - }).then(async ({ roll, attribute, title }) => { + }).then(async ({ roll, trait, title }) => { const cls = getDocumentClass('ChatMessage'); const systemData = new DHDualityRoll({ title: title, origin: target?.id, - roll: roll._formula, - modifiers: attribute ? [attribute] : [], + roll: roll, + modifiers: trait ? [trait] : [], hope: { dice: rollCommand.hope ?? 'd12', value: roll.dice[0].total }, fear: { dice: rollCommand.fear ?? 'd12', value: roll.dice[1].total }, - advantage: - rollCommand.advantage && !rollCommand.disadvantage - ? { dice: 'd6', value: roll.dice[2].total } - : undefined, - disadvantage: - rollCommand.disadvantage && !rollCommand.advantage - ? { dice: 'd6', value: roll.dice[2].total } - : undefined + advantage: advantageState !== null ? { dice: 'd6', value: roll.dice[2].total } : undefined, + advantageState }); const msgData = { @@ -264,10 +235,7 @@ Hooks.on('chatMessage', (_, message) => { sound: CONFIG.sounds.dice, system: systemData, user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/duality-roll.hbs', - systemData - ), + content: 'systems/daggerheart/templates/chat/duality-roll.hbs', rolls: [roll] }; @@ -301,6 +269,15 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/components/card-preview.hbs', 'systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs', 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', - 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' + 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs', + 'systems/daggerheart/templates/views/actionTypes/damage.hbs', + 'systems/daggerheart/templates/views/actionTypes/healing.hbs', + 'systems/daggerheart/templates/views/actionTypes/resource.hbs', + 'systems/daggerheart/templates/views/actionTypes/uuid.hbs', + 'systems/daggerheart/templates/views/actionTypes/uses.hbs', + 'systems/daggerheart/templates/views/actionTypes/roll.hbs', + 'systems/daggerheart/templates/views/actionTypes/cost.hbs', + 'systems/daggerheart/templates/views/actionTypes/range-target.hbs', + 'systems/daggerheart/templates/views/actionTypes/effect.hbs' ]); }; diff --git a/lang/en.json b/lang/en.json index 363a4e63..00064585 100755 --- a/lang/en.json +++ b/lang/en.json @@ -146,6 +146,7 @@ } }, "General": { + "Name": "Name", "Hope": "Hope", "Fear": "Fear", "Duality": "Duality", @@ -478,6 +479,10 @@ "twoHanded": "Two-Handed" }, "Range": { + "self": { + "name": "Self", + "description": "means yourself." + }, "melee": { "name": "Melee", "description": "means a character is within touching distance of the target. PCs can generally touch targets up to a few feet away from them, but melee range may be greater for especially large NPCs." @@ -1284,10 +1289,48 @@ } } }, - "Tooltip": { "openItemWorld": "Open Item World", "delete": "Delete" + }, + "Actions": { + "Types": { + "Attack": { + "Name": "Attack" + }, + "Spellcast": { + "Name": "Spellcast" + }, + "Resource": { + "Name": "Resource" + }, + "Damage": { + "Name": "Damage" + }, + "Healing": { + "Name": "Healing" + }, + "Summon": { + "Name": "Summon" + }, + "Effect": { + "Name": "Effect" + }, + "Macro": { + "Name": "Macro" + } + } + }, + "RollTypes": { + "ability": { + "name": "Ability" + }, + "weapon": { + "name": "Weapon" + }, + "spellcast": { + "name": "SpellCast" + } } } } diff --git a/module/applications/chatMessage.mjs b/module/applications/chatMessage.mjs index aef05bae..a7f61cd8 100644 --- a/module/applications/chatMessage.mjs +++ b/module/applications/chatMessage.mjs @@ -3,6 +3,10 @@ import DHDualityRoll from '../data/chat-message/dualityRoll.mjs'; export default class DhpChatMessage extends foundry.documents.ChatMessage { async renderHTML() { + if (this.type === 'dualityRoll' || this.type === 'adversaryRoll') { + this.content = await foundry.applications.handlebars.renderTemplate(this.content, this.system); + } + /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML(); diff --git a/module/applications/config/Action.mjs b/module/applications/config/Action.mjs index 52a30918..52dc7754 100644 --- a/module/applications/config/Action.mjs +++ b/module/applications/config/Action.mjs @@ -1,29 +1,33 @@ import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs'; const { ApplicationV2 } = foundry.applications.api; -export default class DaggerheartActionConfig extends DaggerheartSheet(ApplicationV2) { +export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { constructor(action) { super({}); - + this.action = action; this.openSection = null; } - // get title(){ - // return `Action - ${this.action.name}`; - // } - static DEFAULT_OPTIONS = { tag: 'form', id: 'daggerheart-action', classes: ['daggerheart', 'views', 'action'], position: { width: 600, height: 'auto' }, actions: { - toggleSection: this.toggleSection + toggleSection: this.toggleSection, + addEffect: this.addEffect, + removeEffect: this.removeEffect, + addElement: this.addElement, + removeElement: this.removeElement, + editEffect: this.editEffect, + addDamage: this.addDamage, + removeDamage: this.removeDamage }, form: { handler: this.updateForm, - closeOnSubmit: true + submitOnChange: true, + closeOnSubmit: false } }; @@ -36,16 +40,9 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio _getTabs() { const tabs = { - effects: { active: true, cssClass: '', group: 'primary', id: 'effects', icon: null, label: 'Effects' }, - useage: { active: false, cssClass: '', group: 'primary', id: 'useage', icon: null, label: 'Useage' }, - conditions: { - active: false, - cssClass: '', - group: 'primary', - id: 'conditions', - icon: null, - label: 'Conditions' - } + base: { active: true, cssClass: '', group: 'primary', id: 'base', icon: null, label: 'Base' }, + config: { active: false, cssClass: '', group: 'primary', id: 'config', icon: null, label: 'Configuration' }, + effect: { active: false, cssClass: '', group: 'primary', id: 'effect', icon: null, label: 'Effect' } }; for (const v of Object.values(tabs)) { @@ -58,9 +55,13 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio async _prepareContext(_options) { const context = await super._prepareContext(_options, 'action'); + context.source = this.action.toObject(false); context.openSection = this.openSection; context.tabs = this._getTabs(); - + context.config = SYSTEM; + if(!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id)); + if(this.action.damage?.hasOwnProperty('includeBase')) context.hasBaseDamage = !!this.action.parent.damage; + context.getRealIndex = this.getRealIndex.bind(this); return context; } @@ -69,15 +70,93 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio this.render(true); } - static async updateForm(event, _, formData) { - const data = foundry.utils.expandObject( - foundry.utils.mergeObject(this.action.toObject(), foundry.utils.expandObject(formData.object)) - ); - const newActions = this.action.parent.actions.map(x => x.toObject()); - if (!newActions.findSplice(x => x.id === data.id, data)) { - newActions.push(data); - } + getRealIndex(index) { + const data = this.action.toObject(false); + return data.damage.parts.find(d => d.base) ? index - 1 : index; + } + + _prepareSubmitData(event, formData) { + const submitData = foundry.utils.expandObject(formData.object); + // this.element.querySelectorAll("fieldset[disabled] :is(input, select)").forEach(input => { + // foundry.utils.setProperty(submitData, input.name, input.value); + // }); + return submitData; + } + + static async updateForm(event, _, formData) { + const submitData = this._prepareSubmitData(event, formData), + data = foundry.utils.expandObject(foundry.utils.mergeObject(this.action.toObject(), submitData)), + newActions = this.action.parent.actions.map(x => x.toObject()); // Find better way + if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data); + const updates = await this.action.parent.parent.update({ 'system.actions': newActions }); + if(!updates) return; + this.action = updates.system.actions[this.action.index]; + this.render(); + } + + static addElement(event) { + const data = this.action.toObject(), + key = event.target.closest('.action-category-data').dataset.key; + if ( !this.action[key] ) return; + data[key].push({}); + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + + static removeElement(event) { + const data = this.action.toObject(), + key = event.target.closest('.action-category-data').dataset.key, + index = event.target.dataset.index; + data[key].splice(index, 1); + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + + static addDamage(event) { + if ( !this.action.damage.parts ) return; + const data = this.action.toObject(); + data.damage.parts.push({}); + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + + static removeDamage(event) { + if ( !this.action.damage.parts ) return; + const data = this.action.toObject(), + index = event.target.dataset.index; + data.damage.parts.splice(index, 1); + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + + static async addEffect(event) { + if ( !this.action.effects ) return; + const effectData = this._addEffectData.bind(this)(), + [created] = await this.action.item.createEmbeddedDocuments("ActiveEffect", [effectData], { render: false }), + data = this.action.toObject(); + data.effects.push( { '_id': created._id } ) + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + + /** + * The data for a newly created applied effect. + * @returns {object} + * @protected + */ + _addEffectData() { + return { + name: this.action.item.name, + img: this.action.item.img, + origin: this.action.item.uuid, + transfer: false + }; + } + + static removeEffect(event) { + if ( !this.action.effects ) return; + const index = event.target.dataset.index, + effectId = this.action.effects[index]._id; + this.constructor.removeElement.bind(this)(event); + this.action.item.deleteEmbeddedDocuments("ActiveEffect", [effectId]); + } + + static editEffect(event) { - await this.action.parent.parent.update({ 'system.actions': newActions }); } } diff --git a/module/applications/npcRollSelectionDialog.mjs b/module/applications/npcRollSelectionDialog.mjs index 3660edaf..7c8290fb 100644 --- a/module/applications/npcRollSelectionDialog.mjs +++ b/module/applications/npcRollSelectionDialog.mjs @@ -14,7 +14,7 @@ export default class NpcRollSelectionDialog extends HandlebarsApplicationMixin(A } get title() { - return game.i18n.localize('DAGGERHEART.Application.Settings.Title'); + return game.i18n.localize('DAGGERHEART.Application.RollSelection.Title'); } static DEFAULT_OPTIONS = { diff --git a/module/applications/rollSelectionDialog.mjs b/module/applications/rollSelectionDialog.mjs index e97d7cf1..eca8c361 100644 --- a/module/applications/rollSelectionDialog.mjs +++ b/module/applications/rollSelectionDialog.mjs @@ -16,7 +16,6 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl hope: ['d12'], fear: ['d12'], advantage: null, - disadvantage: null, hopeResource: hopeResource }; } @@ -30,9 +29,8 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl height: 'auto' }, actions: { + updateIsAdvantage: this.updateIsAdvantage, selectExperience: this.selectExperience, - setAdvantage: this.setAdvantage, - setDisadvantage: this.setDisadvantage, finish: this.finish }, form: { @@ -61,7 +59,6 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl context.hope = this.data.hope; context.fear = this.data.fear; context.advantage = this.data.advantage; - context.disadvantage = this.data.disadvantage; context.experiences = Object.keys(this.experiences).map(id => ({ id, ...this.experiences[id] })); context.hopeResource = this.data.hopeResource + 1; @@ -85,18 +82,10 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl this.render(); } - static setAdvantage() { - this.data.advantage = this.data.advantage ? null : 'd6'; - this.data.disadvantage = null; - - this.render(true); - } - - static setDisadvantage() { - this.data.advantage = null; - this.data.disadvantage = this.data.disadvantage ? null : 'd6'; - - this.render(true); + static updateIsAdvantage(_, button) { + const advantage = Boolean(button.dataset.advantage); + this.data.advantage = this.data.advantage === advantage ? null : advantage; + this.render(); } static async finish() { diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs index a7eed1b9..8f5b3446 100644 --- a/module/applications/settings.mjs +++ b/module/applications/settings.mjs @@ -166,8 +166,6 @@ class DhpRangeSettings extends FormApplication { } export const registerDHSettings = () => { - // const debouncedReload = foundry.utils.debounce(() => window.location.reload(), 100); - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray, { name: game.i18n.localize('DAGGERHEART.Settings.General.AbilityArray.Name'), hint: game.i18n.localize('DAGGERHEART.Settings.General.AbilityArray.Hint'), @@ -274,7 +272,7 @@ export const registerDHSettings = () => { name: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Name'), hint: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Hint'), scope: 'world', - config: true, + config: false, type: Number, choices: Object.values(DualityRollColor), default: DualityRollColor.colorful.value diff --git a/module/applications/settings/appearanceSettings.mjs b/module/applications/settings/appearanceSettings.mjs index e906012b..fb395d6e 100644 --- a/module/applications/settings/appearanceSettings.mjs +++ b/module/applications/settings/appearanceSettings.mjs @@ -54,6 +54,20 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App static async save() { await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, this.settings.toObject()); + const reload = await foundry.applications.api.DialogV2.confirm({ + id: 'reload-world-confirm', + modal: true, + rejectClose: false, + window: { title: 'SETTINGS.ReloadPromptTitle' }, + position: { width: 400 }, + content: `

${game.i18n.localize('SETTINGS.ReloadPromptBody')}

` + }); + + if (reload) { + await game.socket.emit('reload'); + foundry.utils.debouncedReload(); + } + this.close(); } } diff --git a/module/applications/sheets/adversary.mjs b/module/applications/sheets/adversary.mjs index 0196a8c8..234d49b9 100644 --- a/module/applications/sheets/adversary.mjs +++ b/module/applications/sheets/adversary.mjs @@ -61,70 +61,42 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { } static async reactionRoll(event) { - const { roll, diceResults, modifiers } = await this.actor.diceRoll( - { title: `${this.actor.name} - Reaction Roll`, value: 0 }, - event.shiftKey - ); - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - roll: roll._formula, - total: roll._total, - modifiers: modifiers, - diceResults: diceResults + const config = { + event: event, + title: `${this.actor.name} - Reaction Roll`, + roll: { + modifier: null, + type: 'reaction' + }, + chatMessage: { + type: 'adversaryRoll', + template: 'systems/daggerheart/templates/chat/adversary-roll.hbs', + mute: true + } }; - const msg = new cls({ - type: 'adversaryRoll', - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/adversary-roll.hbs', - systemData - ), - rolls: [roll] - }); - - cls.create(msg.toObject()); + this.actor.diceRoll(config); } - static async attackRoll() { - const { modifier, damage, name: attackName } = this.actor.system.attack; - const { roll, dice, advantageState, modifiers } = await this.actor.diceRoll( - { title: `${this.actor.name} - Attack Roll`, value: modifier }, - event.shiftKey - ); - - const targets = Array.from(game.user.targets).map(x => ({ - id: x.id, - name: x.actor.name, - img: x.actor.img, - difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion - })); - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: attackName, - origin: this.document.id, - roll: roll._formula, - advantageState, - total: roll._total, - modifiers: modifiers, - dice: dice, - targets: targets, - damage: { value: damage.value, type: damage.type } - }; - const msg = new cls({ - type: 'adversaryRoll', - sound: CONFIG.sounds.dice, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/adversary-attack-roll.hbs', - systemData - ), - rolls: [roll] - }); - - cls.create(msg.toObject()); + static async attackRoll(event) { + const { modifier, damage, name: attackName } = this.actor.system.attack, + config = { + event: event, + title: attackName, + roll: { + modifier: modifier, + type: 'action' + }, + chatMessage: { + type: 'adversaryRoll', + template: 'systems/daggerheart/templates/chat/adversary-attack-roll.hbs' + }, + damage: { + value: damage.value, + type: damage.type + }, + checkTarget: true + }; + this.actor.diceRoll(config); } static async addExperience() { diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs index 8eaf67aa..bea918e6 100644 --- a/module/applications/sheets/character.mjs +++ b/module/applications/sheets/character.mjs @@ -33,6 +33,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { selectAncestry: this.selectAncestry, selectCommunity: this.selectCommunity, viewObject: this.viewObject, + useItem: this.useItem, useFeature: this.useFeature, takeShortRest: this.takeShortRest, takeLongRest: this.takeLongRest, @@ -150,6 +151,10 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { super._attachPartListeners(partId, htmlElement, options); htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this)); + // To Remove when ContextMenu Handler is made + htmlElement + .querySelectorAll('[data-item-id]') + .forEach(element => element.addEventListener('contextmenu', this.editItem.bind(this))); } async _prepareContext(_options) { @@ -280,7 +285,24 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { } static async rollAttribute(event, button) { - const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll( + const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label); + const config = { + event: event, + title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { + ability: abilityLabel + }), + roll: { + label: abilityLabel, + modifier: button.dataset.value + }, + chatMessage: { + template: 'systems/daggerheart/templates/chat/duality-roll.hbs' + } + }; + this.document.diceRoll(config); + + // Delete when new roll logic test done + /* const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll( { title: game.i18n.localize(abilities[button.dataset.attribute].label), value: button.dataset.value }, event.shiftKey ); @@ -310,7 +332,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { systemContent ), rolls: [roll] - }); + }); */ } static async toggleMarks(_, button) { @@ -348,51 +370,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { static async attackRoll(event, button) { const weapon = await fromUuid(button.dataset.weapon); - const damage = { - value: `${this.document.system.proficiency}${weapon.system.damage.value}`, - type: weapon.system.damage.type - }; - const modifier = this.document.system.traits[weapon.system.trait].value; - - const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll( - { title: game.i18n.localize(abilities[weapon.system.trait].label), value: modifier }, - event.shiftKey - ); - - const targets = Array.from(game.user.targets).map(x => ({ - id: x.id, - name: x.actor.name, - img: x.actor.img, - difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion - })); - - const systemData = new DHDualityRoll({ - title: weapon.name, - origin: this.document.id, - roll: roll._formula, - modifiers: modifiers, - hope: hope, - fear: fear, - advantage: advantage, - disadvantage: disadvantage, - damage: damage, - targets: targets - }); - - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - type: 'dualityRoll', - sound: CONFIG.sounds.dice, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/attack-roll.hbs', - systemData - ), - rolls: [roll] - }); - - await cls.create(msg.toObject()); + if (!weapon) return; + weapon.use(event); } static openLevelUp() { @@ -470,6 +449,12 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { (await game.packs.get('daggerheart.communities'))?.render(true); } + static useItem(event) { + const uuid = event.target.closest('[data-item-id]').dataset.itemId, + item = this.document.items.find(i => i.uuid === uuid); + item.use(event); + } + static async viewObject(_, button) { const object = await fromUuid(button.dataset.value); if (!object) return; @@ -482,6 +467,16 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { object.sheet.render(true); } + editItem(event) { + const uuid = event.target.closest('[data-item-id]').dataset.itemId, + item = this.document.items.find(i => i.uuid === uuid); + if (!item) return; + + if (item.sheet.editMode) item.sheet.editMode = false; + + item.sheet.render(true); + } + static async takeShortRest() { await new DhpDowntime(this.document, true).render(true); await this.minimize(); diff --git a/module/applications/sheets/item.mjs b/module/applications/sheets/item.mjs new file mode 100644 index 00000000..7a50dc94 --- /dev/null +++ b/module/applications/sheets/item.mjs @@ -0,0 +1,129 @@ +import DhpApplicationMixin from './daggerheart-sheet.mjs'; +import DHActionConfig from '../config/Action.mjs'; +import { actionsTypes } from '../../data/_module.mjs'; + +export default function DHItemMixin(Base) { + return class DHItemSheetV2 extends DhpApplicationMixin(Base) { + constructor(options = {}) { + super(options); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'sheet', 'item', 'dh-style'], + position: { width: 600 }, + form: { + handler: this.updateForm, + submitOnChange: true, + closeOnSubmit: false + }, + actions: { + addAction: this.addAction, + editAction: this.editAction, + removeAction: this.removeAction + } + } + + static TABS = { + description: { + active: true, + cssClass: '', + group: 'primary', + id: 'description', + icon: null, + label: 'DAGGERHEART.Sheets.Feature.Tabs.Description' + }, + actions: { + active: false, + cssClass: '', + group: 'primary', + id: 'actions', + icon: null, + label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions' + }, + settings: { + active: false, + cssClass: '', + group: 'primary', + id: 'settings', + icon: null, + label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings' + } + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.document = this.document; + context.config = CONFIG.daggerheart; + context.tabs = super._getTabs(this.constructor.TABS); + + return context; + } + + static async updateForm(event, _, formData) { + await this.document.update(formData.object); + this.render(); + } + + static async selectActionType() { + const content = await foundry.applications.handlebars.renderTemplate( + "systems/daggerheart/templates/views/actionType.hbs", + {types: SYSTEM.ACTIONS.actionTypes} + ), + title = 'Select Action Type', + type = 'form', + data = {}; + return Dialog.prompt({ + title, + label: title, + content, type, + callback: html => { + const form = html[0].querySelector("form"), + fd = new foundry.applications.ux.FormDataExtended(form); + foundry.utils.mergeObject(data, fd.object, { inplace: true }); + // if (!data.name?.trim()) data.name = game.i18n.localize(SYSTEM.ACTIONS.actionTypes[data.type].name); + return data; + }, + rejectClose: false + }) + } + + static async addAction() { + const actionType = await DHItemSheetV2.selectActionType(), + actionIndexes = this.document.system.actions.map(x => x._id.split('-')[2]).sort((a, b) => a - b) + try { + const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack, + action = new cls( + { + // id: `${this.document.id}-Action-${actionIndexes.length > 0 ? actionIndexes[0] + 1 : 1}` + _id: foundry.utils.randomID(), + type: actionType.type, + name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name), + ...cls.getSourceConfig(this.document) + }, + { + parent: this.document + } + ); + await this.document.update({ 'system.actions': [...this.document.system.actions, action] }); + await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render(true); + } catch (error) { + console.log(error) + } + } + + static async editAction(_, button) { + const action = this.document.system.actions[button.dataset.index]; + await new DHActionConfig(action).render(true); + } + + static async removeAction(event, button) { + event.stopPropagation(); + await this.document.update({ + 'system.actions': this.document.system.actions.filter( + (_, index) => index !== Number.parseInt(button.dataset.index) + ) + }); + } + } +} \ No newline at end of file diff --git a/module/applications/sheets/items/armor.mjs b/module/applications/sheets/items/armor.mjs index b9759917..43e6dce9 100644 --- a/module/applications/sheets/items/armor.mjs +++ b/module/applications/sheets/items/armor.mjs @@ -1,16 +1,9 @@ -import DaggerheartSheet from '../daggerheart-sheet.mjs'; +import DHItemSheetV2 from '../item.mjs'; const { ItemSheetV2 } = foundry.applications.sheets; -export default class ArmorSheet extends DaggerheartSheet(ItemSheetV2) { +export default class ArmorSheet extends DHItemSheetV2(ItemSheetV2) { static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'armor'], - position: { width: 600 }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - }, + classes: ['armor'], dragDrop: [{ dragSelector: null, dropSelector: null }] }; @@ -18,42 +11,13 @@ export default class ArmorSheet extends DaggerheartSheet(ItemSheetV2) { header: { template: 'systems/daggerheart/templates/sheets/items/armor/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, + actions: { + template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', + scrollable: ['.actions'] + }, settings: { template: 'systems/daggerheart/templates/sheets/items/armor/settings.hbs', scrollable: ['.settings'] } }; - - static TABS = { - description: { - active: true, - cssClass: '', - group: 'primary', - id: 'description', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Description' - }, - settings: { - active: false, - cssClass: '', - group: 'primary', - id: 'settings', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings' - } - }; - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.document = this.document; - context.config = CONFIG.daggerheart; - context.tabs = super._getTabs(this.constructor.TABS); - - return context; - } - - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); - } } diff --git a/module/applications/sheets/items/consumable.mjs b/module/applications/sheets/items/consumable.mjs index cc36bf97..fbab47f8 100644 --- a/module/applications/sheets/items/consumable.mjs +++ b/module/applications/sheets/items/consumable.mjs @@ -1,57 +1,23 @@ -import DaggerheartSheet from '../daggerheart-sheet.mjs'; +import DHItemSheetV2 from '../item.mjs' const { ItemSheetV2 } = foundry.applications.sheets; -export default class ConsumableSheet extends DaggerheartSheet(ItemSheetV2) { +export default class ConsumableSheet extends DHItemSheetV2(ItemSheetV2) { static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'consumable'], - position: { width: 550 }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - } + classes: ['consumable'], + position: { width: 550 } }; static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/consumable/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, + actions: { + template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', + scrollable: ['.actions'] + }, settings: { template: 'systems/daggerheart/templates/sheets/items/consumable/settings.hbs', scrollable: ['.settings'] } }; - - static TABS = { - description: { - active: true, - cssClass: '', - group: 'primary', - id: 'description', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Description' - }, - settings: { - active: false, - cssClass: '', - group: 'primary', - id: 'settings', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings' - } - }; - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.document = this.document; - context.tabs = super._getTabs(this.constructor.TABS); - - return context; - } - - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); - } } diff --git a/module/applications/sheets/items/domainCard.mjs b/module/applications/sheets/items/domainCard.mjs index bd278311..c1c32382 100644 --- a/module/applications/sheets/items/domainCard.mjs +++ b/module/applications/sheets/items/domainCard.mjs @@ -1,23 +1,10 @@ -import DaggerheartAction from '../../../data/action.mjs'; -import DaggerheartActionConfig from '../../config/Action.mjs'; -import DaggerheartSheet from '../daggerheart-sheet.mjs'; +import DHItemSheetV2 from '../item.mjs' const { ItemSheetV2 } = foundry.applications.sheets; -export default class DomainCardSheet extends DaggerheartSheet(ItemSheetV2) { +export default class DomainCardSheet extends DHItemSheetV2(ItemSheetV2) { static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'domain-card'], - position: { width: 450, height: 700 }, - actions: { - addAction: this.addAction, - editAction: this.editAction, - removeAction: this.removeAction - }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - } + classes: ['domain-card'], + position: { width: 450, height: 700 } }; static PARTS = { @@ -33,74 +20,4 @@ export default class DomainCardSheet extends DaggerheartSheet(ItemSheetV2) { scrollable: ['.settings'] } }; - - static TABS = { - description: { - active: true, - cssClass: '', - group: 'primary', - id: 'description', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Description' - }, - actions: { - active: false, - cssClass: '', - group: 'primary', - id: 'actions', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions' - }, - settings: { - active: false, - cssClass: '', - group: 'primary', - id: 'settings', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings' - } - }; - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.config = CONFIG.daggerheart; - context.tabs = super._getTabs(this.constructor.TABS); - - return context; - } - - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); - } - - static async addAction() { - const actionIndexes = this.document.system.actions.map(x => x.id.split('-')[2]).sort((a, b) => a - b); - const action = await new DaggerheartAction( - { - id: `${this.document.id}-Action-${actionIndexes.length > 0 ? actionIndexes[0] + 1 : 1}` - }, - { - parent: this.document - } - ); - await this.document.update({ 'system.actions': [...this.document.system.actions, action] }); - await new DaggerheartActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render( - true - ); - } - - static async editAction(_, button) { - const action = this.document.system.actions[button.dataset.index]; - await new DaggerheartActionConfig(action).render(true); - } - - static async removeAction(event, button) { - event.stopPropagation(); - await this.document.update({ - 'system.actions': this.document.system.actions.filter( - (_, index) => index !== Number.parseInt(button.dataset.index) - ) - }); - } } diff --git a/module/applications/sheets/items/feature.mjs b/module/applications/sheets/items/feature.mjs index 25d37822..c76127da 100644 --- a/module/applications/sheets/items/feature.mjs +++ b/module/applications/sheets/items/feature.mjs @@ -1,9 +1,7 @@ -import DaggerheartAction from '../../../data/action.mjs'; -import DaggerheartActionConfig from '../../config/Action.mjs'; -import DaggerheartSheet from '../daggerheart-sheet.mjs'; +import DHItemSheetV2 from '../item.mjs' const { ItemSheetV2 } = foundry.applications.sheets; -export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) { +export default class FeatureSheet extends DHItemSheetV2(ItemSheetV2) { constructor(options = {}) { super(options); @@ -11,22 +9,13 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) { } static DEFAULT_OPTIONS = { - tag: 'form', id: 'daggerheart-feature', - classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'feature'], + classes: ['feature'], position: { width: 600, height: 600 }, window: { resizable: true }, actions: { addEffect: this.addEffect, - removeEffect: this.removeEffect, - addAction: this.addAction, - editAction: this.editAction, - removeAction: this.removeAction - }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false + removeEffect: this.removeEffect } }; @@ -49,30 +38,7 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) { }; static TABS = { - description: { - active: true, - cssClass: '', - group: 'primary', - id: 'description', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Description' - }, - actions: { - active: false, - cssClass: '', - group: 'primary', - id: 'actions', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions' - }, - settings: { - active: false, - cssClass: '', - group: 'primary', - id: 'settings', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings' - }, + ...super.TABS, effects: { active: false, cssClass: '', @@ -102,11 +68,6 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) { return context; } - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); - } - effectSelect(event) { this.selectedEffectType = event.currentTarget.value; this.render(true); @@ -130,26 +91,4 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) { const path = `system.effects.-=${button.dataset.effect}`; await this.item.update({ [path]: null }); } - - static async addAction() { - const action = await new DaggerheartAction({ img: this.document.img }, { parent: this.document }); - await this.document.update({ 'system.actions': [...this.document.system.actions, action] }); - await new DaggerheartActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render( - true - ); - } - - static async editAction(_, button) { - const action = this.document.system.actions[button.dataset.index]; - await new DaggerheartActionConfig(action).render(true); - } - - static async removeAction(event, button) { - event.stopPropagation(); - await this.document.update({ - 'system.actions': this.document.system.actions.filter( - (_, index) => index !== Number.parseInt(button.dataset.index) - ) - }); - } } diff --git a/module/applications/sheets/items/miscellaneous.mjs b/module/applications/sheets/items/miscellaneous.mjs index af43fa51..a286af41 100644 --- a/module/applications/sheets/items/miscellaneous.mjs +++ b/module/applications/sheets/items/miscellaneous.mjs @@ -1,57 +1,23 @@ -import DaggerheartSheet from '../daggerheart-sheet.mjs'; +import DHItemSheetV2 from '../item.mjs' const { ItemSheetV2 } = foundry.applications.sheets; -export default class MiscellaneousSheet extends DaggerheartSheet(ItemSheetV2) { +export default class MiscellaneousSheet extends DHItemSheetV2(ItemSheetV2) { static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'miscellaneous'], - position: { width: 550 }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - } + classes: ['miscellaneous'], + position: { width: 550 } }; static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/miscellaneous/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, + actions: { + template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', + scrollable: ['.actions'] + }, settings: { template: 'systems/daggerheart/templates/sheets/items/miscellaneous/settings.hbs', scrollable: ['.settings'] } }; - - static TABS = { - description: { - active: true, - cssClass: '', - group: 'primary', - id: 'description', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Description' - }, - settings: { - active: false, - cssClass: '', - group: 'primary', - id: 'settings', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings' - } - }; - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.document = this.document; - context.tabs = super._getTabs(this.constructor.TABS); - - return context; - } - - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); - } } diff --git a/module/applications/sheets/items/weapon.mjs b/module/applications/sheets/items/weapon.mjs index 90cde394..fdb3973f 100644 --- a/module/applications/sheets/items/weapon.mjs +++ b/module/applications/sheets/items/weapon.mjs @@ -1,58 +1,22 @@ -import DaggerheartSheet from '../daggerheart-sheet.mjs'; +import DHItemSheetV2 from '../item.mjs' const { ItemSheetV2 } = foundry.applications.sheets; -export default class WeaponSheet extends DaggerheartSheet(ItemSheetV2) { +export default class WeaponSheet extends DHItemSheetV2(ItemSheetV2) { static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'weapon'], - position: { width: 600 }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - } - }; + classes: ['weapon'] + } static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/weapon/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, + actions: { + template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', + scrollable: ['.actions'] + }, settings: { template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs', scrollable: ['.settings'] } - }; - - static TABS = { - description: { - active: true, - cssClass: '', - group: 'primary', - id: 'description', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Description' - }, - settings: { - active: false, - cssClass: '', - group: 'primary', - id: 'settings', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings' - } - }; - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.document = this.document; - context.config = CONFIG.daggerheart; - context.tabs = super._getTabs(this.constructor.TABS); - - return context; - } - - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); } } diff --git a/module/config/actionConfig.mjs b/module/config/actionConfig.mjs index 792df85d..dcc3146d 100644 --- a/module/config/actionConfig.mjs +++ b/module/config/actionConfig.mjs @@ -1,7 +1,43 @@ export const actionTypes = { + attack: { + id: 'attack', + name: 'DAGGERHEART.Actions.Types.Attack.Name', + icon: "fa-swords" + }, + spellcast: { + id: 'spellcast', + name: 'DAGGERHEART.Actions.Types.Spellcast.Name', + icon: "fa-book-sparkles" + }, + healing: { + id: 'healing', + name: 'DAGGERHEART.Actions.Types.Healing.Name', + icon: "fa-kit-medical" + }, + resource: { + id: 'resource', + name: 'DAGGERHEART.Actions.Types.Resource.Name', + icon: "fa-honey-pot" + }, damage: { id: 'damage', - name: 'DAGGERHEART.Effects.Types.Health.Name' + name: 'DAGGERHEART.Actions.Types.Damage.Name', + icon: "fa-bone-break" + }, + summon: { + id: 'summon', + name: 'DAGGERHEART.Actions.Types.Summon.Name', + icon: "fa-ghost" + }, + effect: { + id: 'effect', + name: 'DAGGERHEART.Actions.Types.Effect.Name', + icon: "fa-person-rays" + }, + macro: { + id: 'macro', + name: 'DAGGERHEART.Actions.Types.Macro.Name', + icon: "fa-scroll" } }; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 6526392f..f52b144f 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -1,4 +1,9 @@ export const range = { + self: { + label: 'DAGGERHEART.Range.self.name', + description: 'DAGGERHEART.Range.self.description', + distance: 0 + }, melee: { id: 'melee', label: 'DAGGERHEART.Range.melee.name', @@ -248,6 +253,11 @@ export const diceTypes = { d20: 'd20' }; +export const multiplierTypes = { + proficiency: 'Proficiency', + spellcast: 'Spellcast' +}; + export const getDiceSoNicePresets = () => { const { diceSoNice } = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance); @@ -312,3 +322,18 @@ export const abilityCosts = { label: 'Stress' } }; + +export const rollTypes = { + weapon: { + id: 'weapon', + label: 'DAGGERHEART.RollTypes.weapon.name' + }, + spellcast: { + id: 'spellcast', + label: 'DAGGERHEART.RollTypes.spellcast.name' + }, + ability: { + id: 'ability', + label: 'DAGGERHEART.RollTypes.ability.name' + } +} diff --git a/module/data/_module.mjs b/module/data/_module.mjs index e7712547..130c5653 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -5,4 +5,5 @@ export { default as DhCombatant } from './combatant.mjs'; export * as actors from './actor/_module.mjs'; export * as items from './item/_module.mjs'; +export { actionsTypes } from './action/_module.mjs'; export * as messages from './chat-message/_modules.mjs'; diff --git a/module/data/action.mjs b/module/data/action.mjs deleted file mode 100755 index 2c110202..00000000 --- a/module/data/action.mjs +++ /dev/null @@ -1,34 +0,0 @@ -export default class DaggerheartAction extends foundry.abstract.DataModel { - static defineSchema() { - const fields = foundry.data.fields; - return { - id: new fields.DocumentIdField(), - name: new fields.StringField({ initial: 'New Action' }), - damage: new fields.SchemaField({ - type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, nullable: true, initial: null }), - value: new fields.StringField({}) - }), - healing: new fields.SchemaField({ - type: new fields.StringField({ choices: SYSTEM.GENERAL.healingTypes, nullable: true, initial: null }), - value: new fields.StringField() - }), - conditions: new fields.ArrayField( - new fields.SchemaField({ - name: new fields.StringField(), - icon: new fields.StringField(), - description: new fields.StringField() - }) - ), - cost: new fields.SchemaField({ - type: new fields.StringField({ choices: SYSTEM.GENERAL.abilityCosts, nullable: true, initial: null }), - value: new fields.NumberField({ nullable: true, initial: null }) - }), - target: new fields.SchemaField({ - type: new fields.StringField({ - choices: SYSTEM.ACTIONS.targetTypes, - initial: SYSTEM.ACTIONS.targetTypes.other.id - }) - }) - }; - } -} diff --git a/module/data/action/_module.mjs b/module/data/action/_module.mjs new file mode 100644 index 00000000..ccde347a --- /dev/null +++ b/module/data/action/_module.mjs @@ -0,0 +1,13 @@ +import { DHAttackAction, DHBaseAction, DHDamageAction, DHEffectAction, DHHealingAction, DHMacroAction, DHResourceAction, DHSpellCastAction, DHSummonAction } from "./action.mjs"; + +export const actionsTypes = { + base: DHBaseAction, + attack: DHAttackAction, + spellcast: DHSpellCastAction, + resource: DHResourceAction, + damage: DHDamageAction, + healing: DHHealingAction, + summon: DHSummonAction, + effect: DHEffectAction, + macro: DHMacroAction +} \ No newline at end of file diff --git a/module/data/action/action.mjs b/module/data/action/action.mjs new file mode 100644 index 00000000..220ccde6 --- /dev/null +++ b/module/data/action/action.mjs @@ -0,0 +1,380 @@ +import { abilities } from '../../config/actorConfig.mjs'; +import { DHActionDiceData, DHDamageData, DHDamageField } from './actionDice.mjs'; + +export default class DHAction extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + return { + id: new fields.DocumentIdField(), + name: new fields.StringField({ initial: 'New Action' }), + damage: new fields.SchemaField({ + type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, nullable: true, initial: null }), + value: new fields.StringField({}) + }), + healing: new fields.SchemaField({ + type: new fields.StringField({ choices: SYSTEM.GENERAL.healingTypes, nullable: true, initial: null }), + value: new fields.StringField() + }), + conditions: new fields.ArrayField( + new fields.SchemaField({ + name: new fields.StringField(), + icon: new fields.StringField(), + description: new fields.StringField() + }) + ), + cost: new fields.SchemaField({ + type: new fields.StringField({ choices: SYSTEM.GENERAL.abilityCosts, nullable: true, initial: null }), + value: new fields.NumberField({ nullable: true, initial: null }) + }), + target: new fields.SchemaField({ + type: new fields.StringField({ + choices: SYSTEM.ACTIONS.targetTypes, + initial: SYSTEM.ACTIONS.targetTypes.other.id + }) + }) + }; + } +} + +const fields = foundry.data.fields; + +/* + ToDo + - Apply ActiveEffect => Add to Chat message like Damage Button ? + - Add Drag & Drop for documentUUID field (Macro & Summon) + - Add optionnal Role for Healing ? + - Handle Roll result as part of formula if needed + - Target Check + - Cost Check + - Range Check + - Area of effect and measurement placement + - Auto use costs and action +*/ + +export class DHBaseAction extends foundry.abstract.DataModel { + static defineSchema() { + return { + _id: new fields.DocumentIdField(), + type: new fields.StringField({ initial: undefined, readonly: true, required: true }), + name: new fields.StringField({ initial: undefined }), + img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }), + actionType: new fields.StringField({ choices: SYSTEM.ITEM.actionTypes, initial: 'action', nullable: true }), + cost: new fields.ArrayField( + new fields.SchemaField({ + type: new fields.StringField({ + choices: SYSTEM.GENERAL.abilityCosts, + nullable: false, + required: true, + initial: 'hope' + }), + value: new fields.NumberField({ nullable: true, initial: 1 }), + scalable: new fields.BooleanField({ initial: false }), + step: new fields.NumberField({ nullable: true, initial: null }) + }) + ), + uses: new fields.SchemaField({ + value: new fields.NumberField({ nullable: true, initial: null }), + max: new fields.NumberField({ nullable: true, initial: null }), + recovery: new fields.StringField({ + choices: SYSTEM.GENERAL.refreshTypes, + initial: null, + nullable: true + }) + }), + range: new fields.StringField({ + choices: SYSTEM.GENERAL.range, + required: true, + blank: false, + initial: 'self' + }) + }; + } + + prepareData() {} + + get index() { + return this.parent.actions.indexOf(this); + } + + get item() { + return this.parent.parent; + } + + get actor() { + return this.item?.actor; + } + + get chatTemplate() { + return 'systems/daggerheart/templates/chat/attack-roll.hbs'; + } + + static getRollType() { + return 'ability'; + } + + static getSourceConfig(parent) { + const updateSource = {}; + updateSource.img ??= parent?.img ?? parent?.system?.img; + if (parent?.system?.trait) { + updateSource['roll'] = { + type: this.getRollType(), + trait: parent.system.trait + }; + } + if (parent?.system?.range) { + updateSource['range'] = parent?.system?.range; + } + return updateSource; + } + + async use(event) { + if (this.roll.type && this.roll.trait) { + const modifierValue = this.actor.system.traits[this.roll.trait].value; + const config = { + event: event, + title: this.item.name, + roll: { + modifier: modifierValue, + label: game.i18n.localize(abilities[this.roll.trait].label), + type: this.actionType, + difficulty: this.roll?.difficulty + }, + chatMessage: { + template: this.chatTemplate + } + }; + if (this.target?.type) config.checkTarget = true; + if (this.damage.parts.length) { + config.damage = { + value: this.damage.parts.map(p => p.getFormula(this.actor)).join(' + '), + type: this.damage.parts[0].type + }; + } + if (this.effects.length) { + // Apply Active Effects. In Chat Message ? + } + return this.actor.diceRoll(config); + } + } +} + +const extraDefineSchema = (field, option) => { + return { + [field]: { + // damage: new fields.SchemaField({ + // parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)) + // }), + damage: new DHDamageField(option), + roll: new fields.SchemaField({ + type: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.GENERAL.rollTypes }), + trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }), + difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }) + }), + target: new fields.SchemaField({ + type: new fields.StringField({ + choices: SYSTEM.ACTIONS.targetTypes, + initial: SYSTEM.ACTIONS.targetTypes.other.id + }) + }), + effects: new fields.ArrayField( // ActiveEffect + new fields.SchemaField({ + _id: new fields.DocumentIdField() + }) + ) + }[field] + }; +}; + +export class DHAttackAction extends DHBaseAction { + static defineSchema() { + return { + ...super.defineSchema(), + ...extraDefineSchema('damage', true), + ...extraDefineSchema('roll'), + ...extraDefineSchema('target'), + ...extraDefineSchema('effects') + }; + } + + static getRollType() { + return 'weapon'; + } + + prepareData() { + super.prepareData(); + if (this.damage.includeBase && !!this.item?.system?.damage) { + const baseDamage = this.getParentDamage(); + this.damage.parts.unshift(new DHDamageData(baseDamage)); + } + } + + getParentDamage() { + return { + multiplier: 'proficiency', + dice: this.item?.system?.damage.value, + bonus: this.item?.system?.damage.bonus ?? 0, + type: this.item?.system?.damage.type, + base: true + }; + } + + // Temporary until full formula parser + // getDamageFormula() { + // return this.damage.parts.map(p => p.formula).join(' + '); + // } +} + +export class DHSpellCastAction extends DHBaseAction { + static defineSchema() { + return { + ...super.defineSchema(), + ...extraDefineSchema('damage'), + ...extraDefineSchema('roll'), + ...extraDefineSchema('target'), + ...extraDefineSchema('effects') + }; + } + + static getRollType() { + return 'spellcast'; + } +} + +export class DHDamageAction extends DHBaseAction { + static defineSchema() { + return { + ...super.defineSchema(), + ...extraDefineSchema('damage', false), + ...extraDefineSchema('target'), + ...extraDefineSchema('effects') + }; + } + + async use(event) { + const formula = this.damage.parts.map(p => p.getFormula(this.actor)).join(' + '); + if (!formula || formula == '') return; + + let roll = { formula: formula, total: formula }; + if (isNaN(formula)) { + roll = await new Roll(formula).evaluate(); + } + + const cls = getDocumentClass('ChatMessage'); + const msg = new cls({ + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/damage-roll.hbs', + { + roll: roll.formula, + total: roll.total, + type: this.damage.parts.map(p => p.type) + } + ) + }); + + cls.create(msg.toObject()); + } +} + +export class DHHealingAction extends DHBaseAction { + static defineSchema() { + return { + ...super.defineSchema(), + healing: new fields.SchemaField({ + type: new fields.StringField({ + choices: SYSTEM.GENERAL.healingTypes, + required: true, + blank: false, + initial: SYSTEM.GENERAL.healingTypes.health.id, + label: 'Healing' + }), + value: new fields.EmbeddedDataField(DHActionDiceData) + }), + ...extraDefineSchema('target'), + ...extraDefineSchema('effects') + }; + } + + async use(event) { + const formula = this.healing.value.getFormula(this.actor); + if (!formula || formula == '') return; + + // const roll = await super.use(event); + let roll = { formula: formula, total: formula }; + if (isNaN(formula)) { + roll = await new Roll(formula).evaluate(); + } + + const cls = getDocumentClass('ChatMessage'); + const msg = new cls({ + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/healing-roll.hbs', + { + roll: roll.formula, + total: roll.total, + type: this.healing.type + } + ) + }); + + cls.create(msg.toObject()); + } + + get chatTemplate() { + return 'systems/daggerheart/templates/chat/healing-roll.hbs'; + } +} + +export class DHResourceAction extends DHBaseAction { + static defineSchema() { + return { + ...super.defineSchema(), + // ...extraDefineSchema('roll'), + ...extraDefineSchema('target'), + ...extraDefineSchema('effects'), + resource: new fields.SchemaField({ + type: new fields.StringField({ choices: [], blank: true, required: false, initial: "", label: "Resource" }), + value: new fields.NumberField({ initial: 0, label: "Value" }) + }) + }; + } +} + +export class DHSummonAction extends DHBaseAction { + static defineSchema() { + return { + ...super.defineSchema(), + documentUUID: new fields.StringField({ blank: true, initial: '', placeholder: 'Enter a Creature UUID' }) + }; + } +} + +export class DHEffectAction extends DHBaseAction { + static defineSchema() { + return { + ...super.defineSchema(), + ...extraDefineSchema('effects') + }; + } +} + +export class DHMacroAction extends DHBaseAction { + static defineSchema() { + return { + ...super.defineSchema(), + documentUUID: new fields.StringField({ blank: true, initial: '', placeholder: 'Enter a macro UUID' }) + }; + } + + async use(event) { + const fixUUID = !this.documentUUID.includes('Macro.') ? `Macro.${this.documentUUID}` : this.documentUUID, + macro = await fromUuid(fixUUID); + try { + if (!macro) throw new Error(`No macro found for the UUID: ${this.documentUUID}.`); + macro.execute(); + } catch (error) { + ui.notifications.error(error); + } + } +} diff --git a/module/data/action/actionDice.mjs b/module/data/action/actionDice.mjs new file mode 100644 index 00000000..072310ed --- /dev/null +++ b/module/data/action/actionDice.mjs @@ -0,0 +1,49 @@ +import FormulaField from "../fields/formulaField.mjs"; + +const fields = foundry.data.fields; + +export class DHActionDiceData extends foundry.abstract.DataModel { + /** @override */ + static defineSchema() { + return { + multiplier: new fields.StringField({ choices: SYSTEM.GENERAL.multiplierTypes, initial: 'proficiency', label: 'Multiplier' }), + dice: new fields.StringField({ choices: SYSTEM.GENERAL.diceTypes, initial: 'd6', label: 'Formula' }), + bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }), + custom: new fields.SchemaField({ + enabled: new fields.BooleanField({ label: 'Custom Formula' }), + formula: new FormulaField( { label: 'Formula' } ) + }) + } + } + + getFormula(actor) { + return this.custom.enabled ? this.custom.formula : `${(actor.system[this.multiplier] ?? 1)}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; + } +} + +export class DHDamageField extends fields.SchemaField { + constructor(hasBase, options, context={}) { + const damageFields = { + parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)) + } + if(hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true }) + super(damageFields, options, context); + } +} + +export class DHDamageData extends DHActionDiceData { + /** @override */ + static defineSchema() { + return { + ...super.defineSchema(), + base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }), + type: new fields.StringField({ + choices: SYSTEM.GENERAL.damageTypes, + initial: 'physical', + label: 'Type', + nullable: false, + required: true + }) + } + } +} \ No newline at end of file diff --git a/module/data/chat-message/adversaryRoll.mjs b/module/data/chat-message/adversaryRoll.mjs index d279d643..0a02677f 100644 --- a/module/data/chat-message/adversaryRoll.mjs +++ b/module/data/chat-message/adversaryRoll.mjs @@ -5,17 +5,19 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel { return { title: new fields.StringField(), origin: new fields.StringField({ required: true }), - roll: new fields.StringField({}), - total: new fields.NumberField({ integer: true }), + dice: new fields.DataField(), + roll: new fields.DataField(), modifiers: new fields.ArrayField( new fields.SchemaField({ value: new fields.NumberField({ integer: true }), - label: new fields.StringField({}), - title: new fields.StringField({}) + label: new fields.StringField({}) }) ), - advantageState: new fields.NumberField({ required: true, choices: [0, 1, 2], initial: 0 }), - dice: new fields.EmbeddedDataField(DhpAdversaryRollDice), + advantageState: new fields.BooleanField({ nullable: true, initial: null }), + advantage: new fields.SchemaField({ + dice: new fields.StringField({}), + value: new fields.NumberField({ integer: true }) + }), targets: new fields.ArrayField( new fields.SchemaField({ id: new fields.StringField({}), @@ -37,42 +39,8 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel { } prepareDerivedData() { - const diceKeys = Object.keys(this.dice.rolls); - const highestDiceIndex = - diceKeys.length < 2 - ? null - : this.dice.rolls[diceKeys[0]].value > this.dice.rolls[diceKeys[1]].value - ? 0 - : 1; - if (highestDiceIndex !== null) { - this.dice.rolls = this.dice.rolls.map((roll, index) => ({ - ...roll, - discarded: this.advantageState === 1 ? index !== highestDiceIndex : index === highestDiceIndex - })); - } - this.targets.forEach(target => { target.hit = target.difficulty ? this.total >= target.difficulty : this.total >= target.evasion; }); } } - -class DhpAdversaryRollDice extends foundry.abstract.DataModel { - static defineSchema() { - const fields = foundry.data.fields; - - return { - type: new fields.StringField({ required: true }), - rolls: new fields.ArrayField( - new fields.SchemaField({ - value: new fields.NumberField({ required: true, integer: true }), - discarded: new fields.BooleanField({ initial: false }) - }) - ) - }; - } - - get rollTotal() { - return this.rolls.reduce((acc, roll) => acc + (!roll.discarded ? roll.value : 0), 0); - } -} diff --git a/module/data/chat-message/damageRoll.mjs b/module/data/chat-message/damageRoll.mjs index bacd4074..07118d6d 100644 --- a/module/data/chat-message/damageRoll.mjs +++ b/module/data/chat-message/damageRoll.mjs @@ -9,7 +9,13 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel { total: new fields.NumberField({ required: true, integer: true }), type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }) }), - dice: new fields.ArrayField(new fields.EmbeddedDataField(DhpDamageDice)), + dice: new fields.ArrayField( + new fields.SchemaField({ + type: new fields.StringField({ required: true }), + rolls: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })), + total: new fields.NumberField({ integer: true }) + }) + ), modifiers: new fields.ArrayField( new fields.SchemaField({ value: new fields.NumberField({ required: true, integer: true }), @@ -26,18 +32,3 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel { }; } } - -class DhpDamageDice extends foundry.abstract.DataModel { - static defineSchema() { - const fields = foundry.data.fields; - - return { - type: new fields.StringField({ required: true }), - rolls: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })) - }; - } - - get rollTotal() { - return this.rolls.reduce((acc, roll) => acc + roll, 0); - } -} diff --git a/module/data/chat-message/dualityRoll.mjs b/module/data/chat-message/dualityRoll.mjs index 965eff01..60283b7d 100644 --- a/module/data/chat-message/dualityRoll.mjs +++ b/module/data/chat-message/dualityRoll.mjs @@ -1,4 +1,4 @@ -import { DualityRollColor } from "../settings/Appearance.mjs"; +import { DualityRollColor } from '../settings/Appearance.mjs'; const fields = foundry.data.fields; const diceField = () => @@ -18,18 +18,17 @@ export default class DHDualityRoll extends foundry.abstract.TypeDataModel { return { title: new fields.StringField(), origin: new fields.StringField({ required: true }), - roll: new fields.StringField({}), + roll: new fields.DataField({}), modifiers: new fields.ArrayField( new fields.SchemaField({ value: new fields.NumberField({ integer: true }), - label: new fields.StringField({}), - title: new fields.StringField({}) + label: new fields.StringField({}) }) ), hope: diceField(), fear: diceField(), + advantageState: new fields.BooleanField({ nullable: true, initial: null }), advantage: diceField(), - disadvantage: diceField(), targets: new fields.ArrayField( new fields.SchemaField({ id: new fields.StringField({}), @@ -64,15 +63,6 @@ export default class DHDualityRoll extends foundry.abstract.TypeDataModel { }; } - get total() { - const advantage = this.advantage.value - ? this.advantage.value - : this.disadvantage.value - ? -this.disadvantage.value - : 0; - return this.diceTotal + advantage + this.modifierTotal.value; - } - get diceTotal() { return this.hope.value + this.fear.value; } @@ -112,13 +102,7 @@ export default class DHDualityRoll extends foundry.abstract.TypeDataModel { } prepareDerivedData() { - const total = this.total; - this.hope.discarded = this.hope.value < this.fear.value; this.fear.discarded = this.fear.value < this.hope.value; - - this.targets.forEach(target => { - target.hit = target.difficulty ? total >= target.difficulty : total >= target.evasion; - }); } } diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs new file mode 100644 index 00000000..2dea1153 --- /dev/null +++ b/module/data/fields/actionField.mjs @@ -0,0 +1,41 @@ +import { actionsTypes } from "../action/_module.mjs"; + +// Temporary Solution +export default class ActionField extends foundry.data.fields.ObjectField { + + getModel(value) { + return actionsTypes[value.type] ?? actionsTypes.attack; + } + + /* -------------------------------------------- */ + + /** @override */ + _cleanType(value, options) { + if ( !(typeof value === "object") ) value = {}; + + const cls = this.getModel(value); + if ( cls ) return cls.cleanData(value, options); + return value; + } + + /* -------------------------------------------- */ + + /** @override */ + initialize(value, model, options = {}) { + const cls = this.getModel(value); + if ( cls ) return new cls(value, { parent: model, ...options }); + return foundry.utils.deepClone(value); + } + + /* -------------------------------------------- */ + + /** + * Migrate this field's candidate source data. + * @param {object} sourceData Candidate source data of the root model. + * @param {any} fieldData The value of this field within the source data. + */ + migrateSource(sourceData, fieldData) { + const cls = this.getModel(fieldData); + if ( cls ) cls.migrateDataSafe(fieldData); + } +} \ No newline at end of file diff --git a/module/data/item/_module.mjs b/module/data/item/_module.mjs index 30db2468..e29898cb 100644 --- a/module/data/item/_module.mjs +++ b/module/data/item/_module.mjs @@ -19,7 +19,7 @@ export { DHFeature, DHMiscellaneous, DHSubclass, - DHWeapon, + DHWeapon } export const config = { diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index b5880aad..9a097a23 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -1,5 +1,5 @@ -import DaggerheartAction from '../action.mjs'; -import BaseDataItem from './base.mjs'; +import DHAction from "../action/action.mjs"; +import BaseDataItem from "./base.mjs"; export default class DHDomainCard extends BaseDataItem { /** @inheritDoc */ @@ -22,7 +22,7 @@ export default class DHDomainCard extends BaseDataItem { type: new fields.StringField({ choices: SYSTEM.DOMAIN.cardTypes, required: true, blank: true }), foundation: new fields.BooleanField({ initial: false }), inVault: new fields.BooleanField({ initial: false }), - actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction)) + actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAction)) }; } diff --git a/module/data/item/feature.mjs b/module/data/item/feature.mjs index b14c98da..56ae2489 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -1,5 +1,5 @@ import { getTier } from '../../helpers/utils.mjs'; -import DaggerheartAction from '../action.mjs'; +import DHAction from '../action/action.mjs'; import BaseDataItem from './base.mjs'; export default class DHFeature extends BaseDataItem { @@ -92,7 +92,7 @@ export default class DHFeature extends BaseDataItem { }) }) ), - actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction)) + actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAction)) }; } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index b3c82e52..928d523d 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -1,14 +1,15 @@ -import BaseDataItem from "./base.mjs"; -import FormulaField from "../fields/formulaField.mjs"; +import BaseDataItem from './base.mjs'; +import FormulaField from '../fields/formulaField.mjs'; +import ActionField from '../fields/actionField.mjs'; export default class DHWeapon extends BaseDataItem { /** @inheritDoc */ static get metadata() { return foundry.utils.mergeObject(super.metadata, { - label: "TYPES.Item.weapon", - type: "weapon", + label: 'TYPES.Item.weapon', + type: 'weapon', hasDescription: true, - isQuantifiable: true, + isQuantifiable: true }); } @@ -33,8 +34,9 @@ export default class DHWeapon extends BaseDataItem { initial: 'physical' }) }), - feature: new fields.StringField({ choices: SYSTEM.ITEM.weaponFeatures, blank: true }), + // actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAttackAction)) + actions: new fields.ArrayField(new ActionField()) }; } } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 55b00634..2f24a272 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -3,6 +3,7 @@ import NpcRollSelectionDialog from '../applications/npcRollSelectionDialog.mjs'; import RollSelectionDialog from '../applications/rollSelectionDialog.mjs'; import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; +import DHDualityRoll from '../data/chat-message/dualityRoll.mjs'; export default class DhpActor extends Actor { async _preCreate(data, options, user) { @@ -123,87 +124,52 @@ export default class DhpActor extends Actor { }); } - async diceRoll(modifier, shiftKey) { - if (this.type === 'character') { - return await this.dualityRoll(modifier, shiftKey); - } else { - return await this.npcRoll(modifier, shiftKey); - } - } - - async npcRoll(modifier, shiftKey) { - let advantage = null; - - const modifiers = [ - { - value: Number.parseInt(modifier.value), - label: modifier.value >= 0 ? `+${modifier.value}` : `-${modifier.value}`, - title: modifier.title - } - ]; - if (!shiftKey) { - const dialogClosed = new Promise((resolve, _) => { - new NpcRollSelectionDialog(this.system.experiences, resolve).render(true); - }); - const result = await dialogClosed; - - advantage = result.advantage; - result.experiences.forEach(x => - modifiers.push({ - value: x.value, - label: x.value >= 0 ? `+${x.value}` : `-${x.value}`, - title: x.description - }) - ); - } - - const roll = Roll.create( - `${advantage === true || advantage === false ? 2 : 1}d20${advantage === true ? 'kh' : advantage === false ? 'kl' : ''} ${modifiers.map(x => `+ ${x.value}`).join(' ')}` - ); - let rollResult = await roll.evaluate(); - const dice = []; - for (var i = 0; i < rollResult.terms.length; i++) { - const term = rollResult.terms[i]; - if (term.faces) { - dice.push({ type: `d${term.faces}`, rolls: term.results.map(x => ({ value: x.result })) }); - } - } - - // There is Only ever one dice term here - return { roll, dice: dice[0], modifiers, advantageState: advantage === true ? 1 : advantage === false ? 2 : 0 }; - } - - async dualityRoll(modifier, shiftKey) { + /** + * @param {object} config + * @param {Event} config.event + * @param {string} config.title + * @param {object} config.roll + * @param {number} config.roll.modifier + * @param {boolean} [config.roll.simple=false] + * @param {string} [config.roll.type] + * @param {number} [config.roll.difficulty] + * @param {any} [config.damage] + * @param {object} [config.chatMessage] + * @param {string} config.chatMessage.template + * @param {boolean} [config.chatMessage.mute] + * @param {boolean} [config.checkTarget] + */ + async diceRoll(config) { let hopeDice = 'd12', fearDice = 'd12', - advantageDice = null, - disadvantageDice = null; + advantageDice = 'd6', + disadvantageDice = 'd6', + advantage = config.event.altKey ? true : config.event.ctrlKey ? false : null, + targets, + damage = config.damage, + modifiers = this.formatRollModifier(config.roll), + rollConfig, + formula, + hope, + fear; - const modifiers = - modifier.value !== null - ? [ - { - value: modifier.value ? Number.parseInt(modifier.value) : 0, - label: - modifier.value >= 0 - ? `${modifier.title} +${modifier.value}` - : `${modifier.title} ${modifier.value}`, - title: modifier.title - } - ] - : []; - if (!shiftKey) { + if (!config.event.shiftKey && !config.event.altKey && !config.event.ctrlKey) { const dialogClosed = new Promise((resolve, _) => { - new RollSelectionDialog(this.system.experiences, this.system.resources.hope.value, resolve).render( - true - ); + this.type === 'character' + ? new RollSelectionDialog( + this.system.experiences, + this.system.resources.hope.value, + resolve + ).render(true) + : new NpcRollSelectionDialog(this.system.experiences, resolve).render(true); }); - const result = await dialogClosed; - (hopeDice = result.hope), - (fearDice = result.fear), - (advantageDice = result.advantage), - (disadvantageDice = result.disadvantage); - result.experiences.forEach(x => + rollConfig = await dialogClosed; + + advantage = rollConfig.advantage; + hopeDice = rollConfig.hope; + fearDice = rollConfig.fear; + + rollConfig.experiences.forEach(x => modifiers.push({ value: x.value, label: x.value >= 0 ? `+${x.value}` : `-${x.value}`, @@ -211,60 +177,123 @@ export default class DhpActor extends Actor { }) ); - const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope); + if (this.type === 'character') { + const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope); - if (automateHope && result.hopeUsed) { - await this.update({ - 'system.resources.hope.value': this.system.resources.hope.value - result.hopeUsed - }); + if (automateHope && result.hopeUsed) { + await this.update({ + 'system.resources.hope.value': this.system.resources.hope.value - result.hopeUsed + }); + } } } - const roll = new Roll( - `1${hopeDice} + 1${fearDice}${advantageDice ? ` + 1${advantageDice}` : disadvantageDice ? ` - 1${disadvantageDice}` : ''} ${modifiers.map(x => `+ ${x.value}`).join(' ')}` - ); - let rollResult = await roll.evaluate(); - setDiceSoNiceForDualityRoll(rollResult, advantageDice, disadvantageDice); - const hope = rollResult.dice[0].results[0].result; - const fear = rollResult.dice[1].results[0].result; - const advantage = advantageDice ? rollResult.dice[2].results[0].result : null; - const disadvantage = disadvantageDice ? rollResult.dice[2].results[0].result : null; - - if (disadvantage) { - rollResult = { ...rollResult, total: rollResult.total - Math.max(hope, disadvantage) }; - } - if (advantage) { - rollResult = { ...rollResult, total: 'Select Hope Die' }; + if (this.type === 'character') { + formula = `1${hopeDice} + 1${fearDice}${advantage === true ? ` + 1d6` : advantage === false ? ` - 1d6` : ''}`; + } else { + formula = `${advantage === true || advantage === false ? 2 : 1}d20${advantage === true ? 'kh' : advantage === false ? 'kl' : ''}`; } + formula += ` ${modifiers.map(x => `+ ${x.value}`).join(' ')}`; + const roll = await Roll.create(formula).evaluate(); + const dice = roll.dice.flatMap(dice => ({ + denomination: dice.denomination, + number: dice.number, + total: dice.total, + results: dice.results.map(result => ({ result: result.result, discarded: !result.active })) + })); - const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope); - if (automateHope && hope > fear) { - await this.update({ - 'system.resources.hope.value': Math.min( - this.system.resources.hope.value + 1, - this.system.resources.hope.max - ) - }); - } - - if (automateHope && hope === fear) { - await this.update({ - 'system.resources': { - 'hope.value': Math.min(this.system.resources.hope.value + 1, this.system.resources.hope.max), - 'stress.value': Math.max(this.system.resources.stress.value - 1, 0) + if (this.type === 'character') { + setDiceSoNiceForDualityRoll(roll, advantage); + hope = roll.dice[0].results[0].result; + fear = roll.dice[1].results[0].result; + if ( + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope) && + config.roll.type === 'action' + ) { + if (hope > fear) { + await this.update({ + 'system.resources.hope.value': Math.min( + this.system.resources.hope.value + 1, + this.system.resources.hope.max + ) + }); + } else if (hope === fear) { + await this.update({ + 'system.resources': { + 'hope.value': Math.min( + this.system.resources.hope.value + 1, + this.system.resources.hope.max + ), + 'stress.value': Math.max(this.system.resources.stress.value - 1, 0) + } + }); } + } + } + + if (config.checkTarget) { + targets = Array.from(game.user.targets).map(x => { + const target = { + id: x.id, + name: x.actor.name, + img: x.actor.img, + difficulty: x.actor.system.difficulty, + evasion: x.actor.system.evasion?.value + }; + + target.hit = target.difficulty ? roll.total >= target.difficulty : roll.total >= target.evasion; + + return target; }); } - return { - roll, - rollResult, - hope: { dice: hopeDice, value: hope }, - fear: { dice: fearDice, value: fear }, - advantage: { dice: advantageDice, value: advantage }, - disadvantage: { dice: disadvantageDice, value: disadvantage }, - modifiers: modifiers - }; + if (config.chatMessage) { + const configRoll = { + title: config.title, + origin: this.id, + dice, + roll, + modifiers: modifiers.filter(x => x.label), + advantageState: advantage + }; + if (this.type === 'character') { + configRoll.hope = { dice: hopeDice, value: hope }; + configRoll.fear = { dice: fearDice, value: fear }; + configRoll.advantage = { dice: advantageDice, value: roll.dice[2]?.results[0].result ?? null }; + } + if (damage) configRoll.damage = damage; + if (targets) configRoll.targets = targets; + const systemData = + this.type === 'character' && !config.roll.simple ? new DHDualityRoll(configRoll) : configRoll, + cls = getDocumentClass('ChatMessage'), + msg = new cls({ + type: config.chatMessage.type ?? 'dualityRoll', + sound: config.chatMessage.mute ? null : CONFIG.sounds.dice, + system: systemData, + content: config.chatMessage.template, + rolls: [roll] + }); + + await cls.create(msg.toObject()); + } + return roll; + } + + formatRollModifier(roll) { + const modifier = roll.modifier !== null ? Number.parseInt(roll.modifier) : null; + return modifier !== null + ? [ + { + value: modifier, + label: roll.label + ? modifier >= 0 + ? `${roll.label} +${modifier}` + : `${roll.label} ${modifier}` + : null, + title: roll.label + } + ] + : []; } async damageRoll(title, damage, targets, shiftKey) { @@ -294,7 +323,11 @@ export default class DhpActor extends Actor { for (var i = 0; i < rollResult.terms.length; i++) { const term = rollResult.terms[i]; if (term.faces) { - dice.push({ type: `d${term.faces}`, rolls: term.results.map(x => x.result) }); + dice.push({ + type: `d${term.faces}`, + rolls: term.results.map(x => x.result), + total: term.results.reduce((acc, x) => acc + x.result, 0) + }); } else if (term.operator) { } else if (term.number) { const operator = i === 0 ? '' : rollResult.terms[i - 1].operator; diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 65dafc51..dc8beea7 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -2,6 +2,12 @@ export default class DhpItem extends Item { prepareData() { super.prepareData(); } + + /** @inheritDoc */ + prepareEmbeddedDocuments() { + super.prepareEmbeddedDocuments(); + for ( const action of this.system.actions ?? [] ) action.prepareData(); + } /** * @inheritdoc @@ -90,4 +96,46 @@ export default class DhpItem extends Item { options }); } + + async selectActionDialog() { + const content = await foundry.applications.handlebars.renderTemplate( + "systems/daggerheart/templates/views/actionSelect.hbs", + {actions: this.system.actions} + ), + title = 'Select Action', + type = 'div', + data = {}; + return Dialog.prompt({ + title, + // label: title, + content, type, + callback: html => { + const form = html[0].querySelector("form"), + fd = new foundry.applications.ux.FormDataExtended(form); + return this.system.actions.find(a => a._id === fd.object.actionId); + }, + rejectClose: false + }) + } + + async use(event) { + const actions = this.system.actions + let response; + if(actions?.length) { + let action = actions[0]; + if(actions.length > 1 && !event?.shiftKey) { + // Actions Choice Dialog + action = await this.selectActionDialog(); + } + if(action) response = action.use(event); + // Check Target + // If action.roll => Roll Dialog + // Else If action.cost => Cost Dialog + // Then + // Apply Cost + // Apply Effect + } + // Display Item Card in chat + return response; + } } diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 93c3a999..01fbe1af 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -9,21 +9,26 @@ export function dualityRollEnricher(match, _options) { } export function getDualityMessage(roll) { - const attributeLabel = - roll.attribute && abilities[roll.attribute] + const traitLabel = + roll.trait && abilities[roll.trait] ? game.i18n.format('DAGGERHEART.General.Check', { - check: game.i18n.localize(abilities[roll.attribute].label) + check: game.i18n.localize(abilities[roll.trait].label) }) : null; - const label = attributeLabel ?? game.i18n.localize('DAGGERHEART.General.Duality'); + + const label = traitLabel ?? game.i18n.localize('DAGGERHEART.General.Duality'); + const dataLabel = traitLabel + ? game.i18n.localize(abilities[roll.trait].label) + : game.i18n.localize('DAGGERHEART.General.Duality'); const dualityElement = document.createElement('span'); dualityElement.innerHTML = ` + \ No newline at end of file diff --git a/templates/chat/adversary-roll.hbs b/templates/chat/adversary-roll.hbs index 88eb7248..b29f60b5 100644 --- a/templates/chat/adversary-roll.hbs +++ b/templates/chat/adversary-roll.hbs @@ -1,22 +1,33 @@ -
+
-
{{roll}}
+
{{roll.formula}}
    - {{#each diceResults}} -
  1. {{this.value}}
  2. - {{/each}} + {{#each dice}} +
    + {{number}}{{denomination}} + {{total}} +
    +
    +
      + {{#each results}} +
    1. {{result}}
    2. + {{/each}} +
    +
    {{#if ../advantageState}}{{localize "DAGGERHEART.General.Advantage.Full"}}{{/if}}{{#if (eq ../advantageState false)}}{{localize "DAGGERHEART.General.Disadvantage.Full"}}{{/if}}
    +
    + {{/each}}
    {{#each modifiers}} -
  3. {{this.label}}
  4. +
  5. {{label}}
  6. {{/each}}
-
{{total}}
+
{{roll.total}}
\ No newline at end of file diff --git a/templates/chat/attack-roll.hbs b/templates/chat/attack-roll.hbs index 4deaf45b..4fd1438f 100644 --- a/templates/chat/attack-roll.hbs +++ b/templates/chat/attack-roll.hbs @@ -1,27 +1,27 @@ -{{#if this.colorful}} +{{#if colorful}}
-
{{localize "DAGGERHEART.Chat.AttackRoll.Title" attack=this.title}}
+
{{localize "DAGGERHEART.Chat.AttackRoll.Title" attack=title}}
- {{#each this.modifiers}} + {{#each modifiers}}
- {{this.label}} + {{label}}
{{/each}} - {{#if this.advantage.value}} + {{#if advantageState}}
{{localize "DAGGERHEART.General.Advantage.Full"}}
{{/if}} - {{#if this.disadvantage.value}} + {{#if (eq advantageState false)}}
{{localize "DAGGERHEART.General.Disadvantage.Full"}}
{{/if}}
-
+
{{localize "DAGGERHEART.General.Hope"}}
@@ -41,27 +41,27 @@
{{fear.value}}
- {{#if this.advantage.value}} + {{#if advantageState}}
-
{{this.advantage.value}}
+
{{advantage.value}}
{{/if}} - {{#if this.disadvantage.value}} + {{#if (eq advantageState false)}}
-
{{this.disadvantage.value}}
+
{{advantage.value}}
{{/if}} - {{#if this.modifierTotal.value}}
{{this.modifierTotal.label}}
{{/if}} + {{#if modifierTotal.value}}
{{modifierTotal.label}}
{{/if}}
- {{#if (not this.damage.value)}} + {{#if (not damage.value)}}
-
{{this.total}} {{#if (eq dualityResult 1)}}With Hope{{else}}{{#if (eq dualityResult 2)}}With Fear{{else}}Critical Success{{/if}}{{/if}}
+
{{roll.total}} {{#if (eq dualityResult 1)}}With Hope{{else}}{{#if (eq dualityResult 2)}}With Fear{{else}}Critical Success{{/if}}{{/if}}
{{/if}}
@@ -78,18 +78,18 @@ {{/each}}
{{/if}} - {{#if this.damage.value}} + {{#if damage.value}}
- +
-
{{this.total}} {{#if (eq dualityResult 1)}}With Hope{{else}}{{#if (eq dualityResult 2)}}With Fear{{else}}Critical Success{{/if}}{{/if}}
+
{{roll.total}} {{#if (eq dualityResult 1)}}With Hope{{else}}{{#if (eq dualityResult 2)}}With Fear{{else}}Critical Success{{/if}}{{/if}}
{{/if}}
{{else}}
-
{{localize "DAGGERHEART.Chat.AttackRoll.Title" attack=this.title}}
+
{{localize "DAGGERHEART.Chat.AttackRoll.Title" attack=title}}
{{roll}}
@@ -103,7 +103,7 @@ | 1{{fear.dice}} - {{this.diceTotal}} + {{diceTotal}}
    @@ -112,7 +112,7 @@
- {{#if advantage.value}} + {{#if (eq advantageState 1)}}
@@ -127,17 +127,17 @@
{{/if}} - {{#if disadvantage.value}} + {{#if (eq advantageState 2)}}
- 1{{disadvantage.dice}} + 1{{advantage.dice}} - {{disadvantage.value}} + {{advantage.value}}
    -
  1. {{disadvantage.value}}
  2. +
  3. {{advantage.value}}
@@ -148,7 +148,7 @@
{{totalLabel}}
- {{this.total}} + {{roll.total}}
{{#if (gt targets.length 0)}} @@ -164,7 +164,7 @@
{{/if}}
- +
diff --git a/templates/chat/damage-roll.hbs b/templates/chat/damage-roll.hbs index 7cee09ed..b57a8f4e 100644 --- a/templates/chat/damage-roll.hbs +++ b/templates/chat/damage-roll.hbs @@ -1,7 +1,7 @@
-
{{this.title}}
+
{{title}}
-
{{this.roll}}
+
{{roll}}
@@ -9,12 +9,12 @@ {{#each dice}}
- {{this.rolls.length}}{{this.type}} + {{rolls.length}}{{type}} - {{this.rollTotal}} + {{this.total}}
    - {{#each this.rolls}} + {{#each rolls}}
  1. {{this}}
  2. {{/each}}
@@ -23,9 +23,9 @@
-
{{this.damage.total}}
+
{{damage.total}}
- +
diff --git a/templates/chat/duality-roll.hbs b/templates/chat/duality-roll.hbs index 047e88e7..1ae5c720 100644 --- a/templates/chat/duality-roll.hbs +++ b/templates/chat/duality-roll.hbs @@ -1,27 +1,27 @@ -{{#if this.colorful}} +{{#if colorful}}
-
{{this.title}}
+
{{title}}
- {{#each this.modifiers}} + {{#each modifiers}}
- {{this.label}} + {{label}}
{{/each}} - {{#if this.advantage.value}} + {{#if advantageState}}
{{localize "DAGGERHEART.General.Advantage.Full"}}
{{/if}} - {{#if this.disadvantage.value}} + {{#if (eq advantageState false)}}
{{localize "DAGGERHEART.General.Disadvantage.Full"}}
{{/if}}
-
+
{{localize "DAGGERHEART.General.Hope"}}
@@ -41,32 +41,32 @@
{{fear.value}}
- {{#if this.advantage.value}} + {{#if advantageState}}
-
{{this.advantage.value}}
+
{{advantage.value}}
{{/if}} - {{#if this.disadvantage.value}} + {{#if (eq advantageState false)}}
-
{{this.disadvantage.value}}
+
{{advantage.value}}
{{/if}} - {{#if this.modifierTotal.value}}
{{this.modifierTotal.label}}
{{/if}} + {{#if modifierTotal.value}}
{{modifierTotal.label}}
{{/if}}
- {{#if (not this.damage.value)}} + {{#if (not damage.value)}}
-
{{this.total}} {{#if (eq dualityResult 1)}}With Hope{{else}}{{#if (eq dualityResult 2)}}With Fear{{else}}Critical Success{{/if}}{{/if}}
+
{{roll.total}} {{#if (eq dualityResult 1)}}With Hope{{else}}{{#if (eq dualityResult 2)}}With Fear{{else}}Critical Success{{/if}}{{/if}}
{{/if}}
- {{#if this.damage.value}} + {{#if damage.value}}
@@ -77,9 +77,9 @@
{{else}}
-
{{this.title}}
+
{{title}}
-
{{roll}}
+
{{roll.formula}}
@@ -90,7 +90,7 @@ | 1{{fear.dice}} - {{this.diceTotal}} + {{diceTotal}}
    @@ -99,7 +99,7 @@
- {{#if advantage.value}} + {{#if (eq advantageState 1)}}
@@ -114,17 +114,17 @@
{{/if}} - {{#if disadvantage.value}} + {{#if (eq advantageState 2)}}
- 1{{disadvantage.dice}} + 1{{advantage.dice}} - {{disadvantage.value}} + {{advantage.value}}
    -
  1. {{disadvantage.value}}
  2. +
  3. {{advantage.value}}
@@ -135,7 +135,7 @@
{{totalLabel}}
- {{total}} + {{roll.total}}
diff --git a/templates/sheets/actors/adversary/information.hbs b/templates/sheets/actors/adversary/information.hbs index 6d6efb21..085f200c 100644 --- a/templates/sheets/actors/adversary/information.hbs +++ b/templates/sheets/actors/adversary/information.hbs @@ -3,15 +3,15 @@ data-tab='{{tabs.information.id}}' data-group='{{tabs.information.group}}' > -
- {{localize "DAGGERHEART.Sheets.Adversary.Description" }} + {{!--
+ {{localize "DAGGERHEART.Sheets.Adversary.FIELDS.description.label" }} - {{formGroup systemFields.description value=source.system.description}} -
+ {{formInput systemFields.description value=source.system.description}} +
--}}
- {{localize "DAGGERHEART.Sheets.Adversary.MotivesAndTactics" }} + {{localize "DAGGERHEART.Sheets.Adversary.FIELDS.motivesAndTactics.label" }} - {{formGroup systemFields.motivesAndTactics value=source.system.motivesAndTactics}} + {{formInput systemFields.motivesAndTactics value=source.system.motivesAndTactics}}
diff --git a/templates/sheets/actors/adversary/main.hbs b/templates/sheets/actors/adversary/main.hbs index 788af5ea..66718b66 100644 --- a/templates/sheets/actors/adversary/main.hbs +++ b/templates/sheets/actors/adversary/main.hbs @@ -4,6 +4,7 @@ data-group='{{tabs.main.group}}' >
+
{{localize "DAGGERHEART.Sheets.Adversary.General"}} diff --git a/templates/sheets/character/sections/inventory.hbs b/templates/sheets/character/sections/inventory.hbs index 6f4aeb49..f56138dc 100644 --- a/templates/sheets/character/sections/inventory.hbs +++ b/templates/sheets/character/sections/inventory.hbs @@ -13,7 +13,7 @@
  • -
    +
    {{item.name}}
    diff --git a/templates/views/action.hbs b/templates/views/action.hbs index 0b034ea4..6de3bda8 100644 --- a/templates/views/action.hbs +++ b/templates/views/action.hbs @@ -1,6 +1,6 @@
    - {{formField fields.name value=source.name label="Name" name="name" rootId=partId}} + {{!-- {{formField fields.name value=source.name label="Name" name="name" rootId=partId}} --}}
    -
    +
    - -
    Damage
    + +
    Identity
    -
    - {{formField fields.damage.fields.type value=source.damage.type label="Damage Type" name="damage.type" rootId=partId localize=true}} - {{formField fields.damage.fields.value value=source.damage.value label="Damage" name="damage.value" rootId=partId localize=true}} -
    -
    -
    - -
    Healing
    -
    - -
    - {{formField fields.healing.fields.type value=source.healing.type label="Healing Type" name="healing.type" rootId=partId localize=true}} - {{formField fields.healing.fields.value value=source.healing.value label="Healing" name="healing.value" rootId=partId localize=true}} + {{formField fields.name value=source.name label="Name" name="name"}} + {{formField fields.img value=source.img label="Icon" name="img"}} + {{formField fields.actionType value=source.actionType label="Type" name="actionType" localize=true}}
    + {{#if fields.roll}}{{> 'systems/daggerheart/templates/views/actionTypes/roll.hbs' fields=fields.roll.fields source=source.roll}}{{/if}}
    -
    -
    - -
    Cost
    -
    - -
    - {{formField fields.cost.fields.type value=source.cost.type label="Cost Type" name="cost.type" rootId=partId}} - {{formField fields.cost.fields.value value=source.cost.value label="Value" name="cost.value" rootId=partId}} -
    -
    - - {{formField fields.target.fields.type value=source.target.type label="Target Type" name="target.type" rootId=partId}} +
    + {{> 'systems/daggerheart/templates/views/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}} + {{> 'systems/daggerheart/templates/views/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost}} + {{#if fields.target}}{{> 'systems/daggerheart/templates/views/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}}{{/if}}
    -
    - {{!--

    - {{localize "Conditions"}} - - -

    --}} +
    + {{#if fields.damage}}{{> 'systems/daggerheart/templates/views/actionTypes/damage.hbs' fields=fields.damage.fields.parts.element.fields source=source.damage}}{{/if}} + {{#if fields.healing}}{{> 'systems/daggerheart/templates/views/actionTypes/healing.hbs' fields=fields.healing.fields source=source.healing}}{{/if}} + {{#if fields.resource}}{{> 'systems/daggerheart/templates/views/actionTypes/resource.hbs' fields=fields.resource.fields source=source.resource}}{{/if}} + {{#if fields.documentUUID}}{{> 'systems/daggerheart/templates/views/actionTypes/uuid.hbs' fields=fields.documentUUID source=source.documentUUID}}{{/if}} + {{#if fields.effects}}{{> 'systems/daggerheart/templates/views/actionTypes/effect.hbs'}}{{/if}}
    -
    \ No newline at end of file diff --git a/templates/views/actionSelect.hbs b/templates/views/actionSelect.hbs new file mode 100644 index 00000000..2ea76cae --- /dev/null +++ b/templates/views/actionSelect.hbs @@ -0,0 +1,13 @@ +
    +
      + {{#each actions}} +
    • + +
    • + {{/each}} +
    +
    \ No newline at end of file diff --git a/templates/views/actionType.hbs b/templates/views/actionType.hbs new file mode 100644 index 00000000..75dceeb7 --- /dev/null +++ b/templates/views/actionType.hbs @@ -0,0 +1,13 @@ +
    +
      + {{#each types}} +
    • + +
    • + {{/each}} +
    +
    \ No newline at end of file diff --git a/templates/views/actionTypes/cost.hbs b/templates/views/actionTypes/cost.hbs new file mode 100644 index 00000000..7c9166e4 --- /dev/null +++ b/templates/views/actionTypes/cost.hbs @@ -0,0 +1,21 @@ +
    + +
    Cost
    +
    +
    +
    + {{#each source as |cost index|}} +
    +
    + {{formField ../fields.type label="Resource" value=cost.type name=(concat "cost." index ".type") localize=true}} + {{formField ../fields.value label="Value" value=cost.value name=(concat "cost." index ".value")}} +
    +
    + {{formField ../fields.scalable label="Scalable" value=cost.scalable name=(concat "cost." index ".scalable")}} + {{formField ../fields.step label="Step" value=cost.step name=(concat "cost." index ".step")}} +
    +
    +
    + {{/each}} +
    +
    \ No newline at end of file diff --git a/templates/views/actionTypes/damage.hbs b/templates/views/actionTypes/damage.hbs new file mode 100644 index 00000000..13e2fffe --- /dev/null +++ b/templates/views/actionTypes/damage.hbs @@ -0,0 +1,36 @@ + +
    + +
    Damage
    +
    +
    +
    + {{#if @root.hasBaseDamage}} +
    + {{!-- --}} + {{formField @root.fields.damage.fields.includeBase value=@root.source.damage.includeBase label="Include Item Damage" name="damage.includeBase" }} +
    + {{/if}} + {{#each source.parts as |dmg index|}} + {{#with (@root.getRealIndex index) as | realIndex |}} + + {{#unless dmg.base}} + {{formField ../../fields.custom.fields.enabled value=dmg.custom.enabled name=(concat "damage.parts." realIndex ".custom.enabled")}} + {{/unless}} + {{#if dmg.custom.enabled}} + {{formField ../../fields.custom.fields.formula value=dmg.custom.formula name=(concat "damage.parts." realIndex ".custom.formula") localize=true}} + {{else}} +
    + {{formField ../../fields.multiplier value=dmg.multiplier name=(concat "damage.parts." realIndex ".multiplier") localize=true}} + {{formField ../../fields.dice value=dmg.dice name=(concat "damage.parts." realIndex ".dice")}} + {{formField ../../fields.bonus value=dmg.bonus name=(concat "damage.parts." realIndex ".bonus") localize=true}} +
    + {{/if}} + {{formField ../../fields.type value=dmg.type name=(concat "damage.parts." realIndex ".type") localize=true}} + + {{#unless dmg.base}}
    {{/unless}} +
    + {{/with}} + {{/each}} +
    +
  • \ No newline at end of file diff --git a/templates/views/actionTypes/effect.hbs b/templates/views/actionTypes/effect.hbs new file mode 100644 index 00000000..0f99327e --- /dev/null +++ b/templates/views/actionTypes/effect.hbs @@ -0,0 +1,19 @@ +
    + +
    Effects
    +
    +
    +
    + {{#each @root.effects as | effect index | }} +
    + {{!--
    --}} +
    + + +
    +
    + {{!--
    --}} +
    + {{/each}} +
    +
    \ No newline at end of file diff --git a/templates/views/actionTypes/healing.hbs b/templates/views/actionTypes/healing.hbs new file mode 100644 index 00000000..52fe23dc --- /dev/null +++ b/templates/views/actionTypes/healing.hbs @@ -0,0 +1,21 @@ + +
    + +
    Healing
    +
    +
    +
    + {{formField fields.type value=source.type name="healing.type" localize=true}} +
    + {{formField fields.value.fields.custom.fields.enabled value=source.value.custom.enabled name="healing.value.custom.enabled"}} + {{#if source.value.custom.enabled}} + {{formField fields.value.fields.custom.fields.formula value=source.value.custom.formula name="healing.value.custom.formula" localize=true}} + {{else}} + {{formField fields.value.fields.multiplier value=source.value.multiplier name="healing.value.multiplier" localize=true}} + {{formField fields.value.fields.dice value=source.value.dice name="healing.value.dice"}} + {{formField fields.value.fields.bonus value=source.value.bonus name="healing.value.bonus" localize=true}} + {{/if}} +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/views/actionTypes/range-target.hbs b/templates/views/actionTypes/range-target.hbs new file mode 100644 index 00000000..a408ebf6 --- /dev/null +++ b/templates/views/actionTypes/range-target.hbs @@ -0,0 +1,13 @@ +
    + +
    Range{{#if fields.target}} & Target{{/if}}
    +
    +
    + {{formField fields.range value=source.range label="Range" name="range" localize=true}} +
    + {{#if fields.target}} +
    + {{formField fields.target.type value=source.target.type label="Target" name="target.type" localize=true}} +
    + {{/if}} +
    \ No newline at end of file diff --git a/templates/views/actionTypes/resource.hbs b/templates/views/actionTypes/resource.hbs new file mode 100644 index 00000000..4b1daa1e --- /dev/null +++ b/templates/views/actionTypes/resource.hbs @@ -0,0 +1,14 @@ + +
    + +
    Resource
    +
    +
    +
    +
    + {{formField fields.type value=source.type name="resource.type" localize=true}} + {{formField fields.value value=source.value name="resource.value"}} +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/views/actionTypes/roll.hbs b/templates/views/actionTypes/roll.hbs new file mode 100644 index 00000000..a3b1a31a --- /dev/null +++ b/templates/views/actionTypes/roll.hbs @@ -0,0 +1,10 @@ +
    + +
    Roll
    +
    +
    + {{formField fields.type label="Type" name="roll.type" value=source.type localize=true}} + {{formField fields.trait label="Trait" name="roll.trait" value=source.trait localize=true}} + {{formField fields.difficulty label="Difficulty" name="roll.difficulty" value=source.difficulty}} +
    +
    \ No newline at end of file diff --git a/templates/views/actionTypes/target.hbs b/templates/views/actionTypes/target.hbs new file mode 100644 index 00000000..ec7ebfb2 --- /dev/null +++ b/templates/views/actionTypes/target.hbs @@ -0,0 +1,8 @@ +
    + +
    Target
    +
    +
    + {{formField targetField.type label="Target" name="target" rootId=partId localize=true}} +
    +
    \ No newline at end of file diff --git a/templates/views/actionTypes/uses.hbs b/templates/views/actionTypes/uses.hbs new file mode 100644 index 00000000..9015987e --- /dev/null +++ b/templates/views/actionTypes/uses.hbs @@ -0,0 +1,12 @@ +
    + +
    Uses
    +
    +
    +
    + {{formField fields.value label="Value" value=source.value name="uses.value" rootId=partId}} + {{formField fields.max label="Max" value=source.max name="uses.max" rootId=partId}} +
    + {{formField fields.recovery label="Recovery" value=source.recovery name="uses.recovery" rootId=partId localize=true}} +
    +
    \ No newline at end of file diff --git a/templates/views/actionTypes/uuid.hbs b/templates/views/actionTypes/uuid.hbs new file mode 100644 index 00000000..b7ccd578 --- /dev/null +++ b/templates/views/actionTypes/uuid.hbs @@ -0,0 +1,9 @@ + +
    + +
    Macro
    +
    +
    + {{formInput fields value=source name="documentUUID" placeholder=fields.options.placeholder}} +
    +
    \ No newline at end of file diff --git a/templates/views/rollSelection.hbs b/templates/views/rollSelection.hbs index a103686a..55c19795 100644 --- a/templates/views/rollSelection.hbs +++ b/templates/views/rollSelection.hbs @@ -12,8 +12,8 @@ {{/each}}
    - - + +
    {{#if (not this.isNpc)}}
    From f840dc255312438a2d7a170e42c4e9ace8de3575 Mon Sep 17 00:00:00 2001 From: joaquinpereyra98 <24190917+joaquinpereyra98@users.noreply.github.com> Date: Fri, 13 Jun 2025 08:51:33 -0300 Subject: [PATCH 2/2] Feature/116-implementation-of-pseudo-documents (#125) * FEAT: add baseDataModel logic * FEAT: new PseudoDocumentsField FIX: BasePseudoDocument 's getEmbeddedDocument * FEAT: PseudoDocument class * FEAT: add TypedPseudoDocument REFACTOR: PreudoDocument FIX: Typos Bug * FIX: CONFIG types * FEAT: basic PseudoDocumentSheet * FIX: remove schema ADD: input of example --------- Co-authored-by: Joaquin Pereyra Co-authored-by: WBHarry --- daggerheart.d.ts | 1 - daggerheart.mjs | 1 + module/_types.d.ts | 0 module/applications/_module.mjs | 2 + module/applications/config/Action.mjs | 34 ++- module/applications/sheets/item.mjs | 31 +-- .../applications/sheets/items/consumable.mjs | 2 +- .../applications/sheets/items/domainCard.mjs | 2 +- module/applications/sheets/items/feature.mjs | 2 +- .../sheets/items/miscellaneous.mjs | 2 +- module/applications/sheets/items/weapon.mjs | 6 +- .../sheets/pseudo-documents/_module.mjs | 1 + .../pseudo-documents-sheet.mjs | 66 ++++++ module/config/actionConfig.mjs | 16 +- module/config/generalConfig.mjs | 2 +- module/config/pseudoConfig.mjs | 17 ++ module/config/system.mjs | 4 +- module/data/_module.mjs | 2 + module/data/action/_module.mjs | 14 +- module/data/action/action.mjs | 10 +- module/data/action/actionDice.mjs | 26 ++- module/data/fields/_module.mjs | 5 +- module/data/fields/actionField.mjs | 13 +- module/data/fields/formulaField.mjs | 124 +++++----- module/data/fields/pseudoDocumentsField.mjs | 56 +++++ module/data/item/_module.mjs | 44 ++-- module/data/item/base.mjs | 4 +- module/data/item/domainCard.mjs | 4 +- module/data/item/feature.mjs | 17 +- module/data/item/weapon.mjs | 13 +- module/data/pseudo-documents/_module.mjs | 2 + module/data/pseudo-documents/base/base.mjs | 213 ++++++++++++++++++ .../pseudo-documents/base/pseudoDocument.mjs | 59 +++++ .../base/sheetManagementMixin.mjs | 158 +++++++++++++ .../data/pseudo-documents/feature/_module.mjs | 2 + .../feature/baseFeatureData.mjs | 24 ++ .../feature/weaponFeature.mjs | 6 + module/documents/item.mjs | 37 +-- styles/application.less | 6 +- styles/daggerheart.less | 3 +- templates/sheets/pseudo-documents/header.hbs | 3 + 41 files changed, 844 insertions(+), 190 deletions(-) delete mode 100644 module/_types.d.ts create mode 100644 module/applications/sheets/pseudo-documents/_module.mjs create mode 100644 module/applications/sheets/pseudo-documents/pseudo-documents-sheet.mjs create mode 100644 module/config/pseudoConfig.mjs create mode 100644 module/data/fields/pseudoDocumentsField.mjs create mode 100644 module/data/pseudo-documents/_module.mjs create mode 100644 module/data/pseudo-documents/base/base.mjs create mode 100644 module/data/pseudo-documents/base/pseudoDocument.mjs create mode 100644 module/data/pseudo-documents/base/sheetManagementMixin.mjs create mode 100644 module/data/pseudo-documents/feature/_module.mjs create mode 100644 module/data/pseudo-documents/feature/baseFeatureData.mjs create mode 100644 module/data/pseudo-documents/feature/weaponFeature.mjs create mode 100644 templates/sheets/pseudo-documents/header.hbs diff --git a/daggerheart.d.ts b/daggerheart.d.ts index 3b753baf..ab754b17 100644 --- a/daggerheart.d.ts +++ b/daggerheart.d.ts @@ -1,4 +1,3 @@ -import './module/_types'; import '@client/global.mjs'; import Canvas from '@client/canvas/board.mjs'; diff --git a/daggerheart.mjs b/daggerheart.mjs index f2fef669..adbe5e43 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -19,6 +19,7 @@ globalThis.SYSTEM = SYSTEM; Hooks.once('init', () => { CONFIG.daggerheart = SYSTEM; + game.system.api = { applications, models, diff --git a/module/_types.d.ts b/module/_types.d.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index b1a1d59e..5ee13189 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -13,3 +13,5 @@ export { default as DhpArmor } from './sheets/items/armor.mjs'; export { default as DhpChatMessage } from './chatMessage.mjs'; export { default as DhpEnvironment } from './sheets/environment.mjs'; export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs'; + +export * as pseudoDocumentSheet from './sheets/pseudo-documents/_module.mjs'; diff --git a/module/applications/config/Action.mjs b/module/applications/config/Action.mjs index 52dc7754..6453f896 100644 --- a/module/applications/config/Action.mjs +++ b/module/applications/config/Action.mjs @@ -4,7 +4,7 @@ const { ApplicationV2 } = foundry.applications.api; export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { constructor(action) { super({}); - + this.action = action; this.openSection = null; } @@ -59,8 +59,8 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { context.openSection = this.openSection; context.tabs = this._getTabs(); context.config = SYSTEM; - if(!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id)); - if(this.action.damage?.hasOwnProperty('includeBase')) context.hasBaseDamage = !!this.action.parent.damage; + if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id)); + if (this.action.damage?.hasOwnProperty('includeBase')) context.hasBaseDamage = !!this.action.parent.damage; context.getRealIndex = this.getRealIndex.bind(this); return context; } @@ -86,10 +86,10 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { static async updateForm(event, _, formData) { const submitData = this._prepareSubmitData(event, formData), data = foundry.utils.expandObject(foundry.utils.mergeObject(this.action.toObject(), submitData)), - newActions = this.action.parent.actions.map(x => x.toObject()); // Find better way + newActions = this.action.parent.actions.map(x => x.toObject()); // Find better way if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data); const updates = await this.action.parent.parent.update({ 'system.actions': newActions }); - if(!updates) return; + if (!updates) return; this.action = updates.system.actions[this.action.index]; this.render(); } @@ -97,7 +97,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { static addElement(event) { const data = this.action.toObject(), key = event.target.closest('.action-category-data').dataset.key; - if ( !this.action[key] ) return; + if (!this.action[key]) return; data[key].push({}); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } @@ -109,16 +109,16 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { data[key].splice(index, 1); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } - + static addDamage(event) { - if ( !this.action.damage.parts ) return; + if (!this.action.damage.parts) return; const data = this.action.toObject(); data.damage.parts.push({}); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } static removeDamage(event) { - if ( !this.action.damage.parts ) return; + if (!this.action.damage.parts) return; const data = this.action.toObject(), index = event.target.dataset.index; data.damage.parts.splice(index, 1); @@ -126,15 +126,15 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { } static async addEffect(event) { - if ( !this.action.effects ) return; + if (!this.action.effects) return; const effectData = this._addEffectData.bind(this)(), - [created] = await this.action.item.createEmbeddedDocuments("ActiveEffect", [effectData], { render: false }), + [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], { render: false }), data = this.action.toObject(); - data.effects.push( { '_id': created._id } ) + data.effects.push({ _id: created._id }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } - /** + /** * The data for a newly created applied effect. * @returns {object} * @protected @@ -149,14 +149,12 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { } static removeEffect(event) { - if ( !this.action.effects ) return; + if (!this.action.effects) return; const index = event.target.dataset.index, effectId = this.action.effects[index]._id; this.constructor.removeElement.bind(this)(event); - this.action.item.deleteEmbeddedDocuments("ActiveEffect", [effectId]); + this.action.item.deleteEmbeddedDocuments('ActiveEffect', [effectId]); } - static editEffect(event) { - - } + static editEffect(event) {} } diff --git a/module/applications/sheets/item.mjs b/module/applications/sheets/item.mjs index 7a50dc94..1b8c416b 100644 --- a/module/applications/sheets/item.mjs +++ b/module/applications/sheets/item.mjs @@ -22,7 +22,7 @@ export default function DHItemMixin(Base) { editAction: this.editAction, removeAction: this.removeAction } - } + }; static TABS = { description: { @@ -49,7 +49,7 @@ export default function DHItemMixin(Base) { icon: null, label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings' } - } + }; async _prepareContext(_options) { const context = await super._prepareContext(_options); @@ -67,8 +67,8 @@ export default function DHItemMixin(Base) { static async selectActionType() { const content = await foundry.applications.handlebars.renderTemplate( - "systems/daggerheart/templates/views/actionType.hbs", - {types: SYSTEM.ACTIONS.actionTypes} + 'systems/daggerheart/templates/views/actionType.hbs', + { types: SYSTEM.ACTIONS.actionTypes } ), title = 'Select Action Type', type = 'form', @@ -76,21 +76,22 @@ export default function DHItemMixin(Base) { return Dialog.prompt({ title, label: title, - content, type, + content, + type, callback: html => { - const form = html[0].querySelector("form"), + const form = html[0].querySelector('form'), fd = new foundry.applications.ux.FormDataExtended(form); foundry.utils.mergeObject(data, fd.object, { inplace: true }); // if (!data.name?.trim()) data.name = game.i18n.localize(SYSTEM.ACTIONS.actionTypes[data.type].name); return data; }, rejectClose: false - }) + }); } - + static async addAction() { const actionType = await DHItemSheetV2.selectActionType(), - actionIndexes = this.document.system.actions.map(x => x._id.split('-')[2]).sort((a, b) => a - b) + actionIndexes = this.document.system.actions.map(x => x._id.split('-')[2]).sort((a, b) => a - b); try { const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack, action = new cls( @@ -105,10 +106,12 @@ export default function DHItemMixin(Base) { parent: this.document } ); - await this.document.update({ 'system.actions': [...this.document.system.actions, action] }); - await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render(true); + await this.document.update({ 'system.actions': [...this.document.system.actions, action] }); + await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render( + true + ); } catch (error) { - console.log(error) + console.log(error); } } @@ -125,5 +128,5 @@ export default function DHItemMixin(Base) { ) }); } - } -} \ No newline at end of file + }; +} diff --git a/module/applications/sheets/items/consumable.mjs b/module/applications/sheets/items/consumable.mjs index fbab47f8..815c6b9b 100644 --- a/module/applications/sheets/items/consumable.mjs +++ b/module/applications/sheets/items/consumable.mjs @@ -1,4 +1,4 @@ -import DHItemSheetV2 from '../item.mjs' +import DHItemSheetV2 from '../item.mjs'; const { ItemSheetV2 } = foundry.applications.sheets; export default class ConsumableSheet extends DHItemSheetV2(ItemSheetV2) { diff --git a/module/applications/sheets/items/domainCard.mjs b/module/applications/sheets/items/domainCard.mjs index c1c32382..17a83f95 100644 --- a/module/applications/sheets/items/domainCard.mjs +++ b/module/applications/sheets/items/domainCard.mjs @@ -1,4 +1,4 @@ -import DHItemSheetV2 from '../item.mjs' +import DHItemSheetV2 from '../item.mjs'; const { ItemSheetV2 } = foundry.applications.sheets; export default class DomainCardSheet extends DHItemSheetV2(ItemSheetV2) { diff --git a/module/applications/sheets/items/feature.mjs b/module/applications/sheets/items/feature.mjs index c76127da..40fa9b02 100644 --- a/module/applications/sheets/items/feature.mjs +++ b/module/applications/sheets/items/feature.mjs @@ -1,4 +1,4 @@ -import DHItemSheetV2 from '../item.mjs' +import DHItemSheetV2 from '../item.mjs'; const { ItemSheetV2 } = foundry.applications.sheets; export default class FeatureSheet extends DHItemSheetV2(ItemSheetV2) { diff --git a/module/applications/sheets/items/miscellaneous.mjs b/module/applications/sheets/items/miscellaneous.mjs index a286af41..dd22d216 100644 --- a/module/applications/sheets/items/miscellaneous.mjs +++ b/module/applications/sheets/items/miscellaneous.mjs @@ -1,4 +1,4 @@ -import DHItemSheetV2 from '../item.mjs' +import DHItemSheetV2 from '../item.mjs'; const { ItemSheetV2 } = foundry.applications.sheets; export default class MiscellaneousSheet extends DHItemSheetV2(ItemSheetV2) { diff --git a/module/applications/sheets/items/weapon.mjs b/module/applications/sheets/items/weapon.mjs index fdb3973f..a54c0140 100644 --- a/module/applications/sheets/items/weapon.mjs +++ b/module/applications/sheets/items/weapon.mjs @@ -1,10 +1,10 @@ -import DHItemSheetV2 from '../item.mjs' +import DHItemSheetV2 from '../item.mjs'; const { ItemSheetV2 } = foundry.applications.sheets; export default class WeaponSheet extends DHItemSheetV2(ItemSheetV2) { static DEFAULT_OPTIONS = { classes: ['weapon'] - } + }; static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/weapon/header.hbs' }, @@ -18,5 +18,5 @@ export default class WeaponSheet extends DHItemSheetV2(ItemSheetV2) { template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs', scrollable: ['.settings'] } - } + }; } diff --git a/module/applications/sheets/pseudo-documents/_module.mjs b/module/applications/sheets/pseudo-documents/_module.mjs new file mode 100644 index 00000000..9dc4d356 --- /dev/null +++ b/module/applications/sheets/pseudo-documents/_module.mjs @@ -0,0 +1 @@ +export {default as PseudoDocumentSheet }from "./pseudo-documents-sheet.mjs"; \ No newline at end of file diff --git a/module/applications/sheets/pseudo-documents/pseudo-documents-sheet.mjs b/module/applications/sheets/pseudo-documents/pseudo-documents-sheet.mjs new file mode 100644 index 00000000..487da420 --- /dev/null +++ b/module/applications/sheets/pseudo-documents/pseudo-documents-sheet.mjs @@ -0,0 +1,66 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class PseudoDocumentSheet extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(options) { + super(options); + this.#pseudoDocument = options.document; + } + + /** + * The UUID of the associated pseudo-document + * @type {string} + */ + get pseudoUuid() { + return this.pseudoDocument.uuid; + } + + #pseudoDocument; + + /** + * The pseudo-document instance this sheet represents + * @type {object} + */ + get pseudoDocument() { + return this.#pseudoDocument; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'sheet'], + position: { width: 600 }, + form: { + handler: PseudoDocumentSheet.#onSubmitForm, + submitOnChange: true, + closeOnSubmit: false + }, + dragDrop: [{ dragSelector: null, dropSelector: null }], + }; + + static PARTS = { + header: { template: 'systems/daggerheart/templates/sheets/pseudo-documents/header.hbs' }, + }; + + /** @inheritDoc */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + const document = this.pseudoDocument; + return Object.assign(context, { + document, + source: document._source, + editable: this.isEditable, + user: game.user, + rootId: this.id, + }); + } + + /** + * Form submission handler + * @param {SubmitEvent | Event} event - The originating form submission or input change event + * @param {HTMLFormElement} form - The form element that was submitted + * @param {foundry.applications.ux.FormDataExtended} formData - Processed data for the submitted form + */ + static async #onSubmitForm(event, form, formData) { + const submitData = foundry.utils.expandObject(formData.object); + await this.pseudoDocument.update(submitData); + } +} diff --git a/module/config/actionConfig.mjs b/module/config/actionConfig.mjs index dcc3146d..4db96a64 100644 --- a/module/config/actionConfig.mjs +++ b/module/config/actionConfig.mjs @@ -2,42 +2,42 @@ export const actionTypes = { attack: { id: 'attack', name: 'DAGGERHEART.Actions.Types.Attack.Name', - icon: "fa-swords" + icon: 'fa-swords' }, spellcast: { id: 'spellcast', name: 'DAGGERHEART.Actions.Types.Spellcast.Name', - icon: "fa-book-sparkles" + icon: 'fa-book-sparkles' }, healing: { id: 'healing', name: 'DAGGERHEART.Actions.Types.Healing.Name', - icon: "fa-kit-medical" + icon: 'fa-kit-medical' }, resource: { id: 'resource', name: 'DAGGERHEART.Actions.Types.Resource.Name', - icon: "fa-honey-pot" + icon: 'fa-honey-pot' }, damage: { id: 'damage', name: 'DAGGERHEART.Actions.Types.Damage.Name', - icon: "fa-bone-break" + icon: 'fa-bone-break' }, summon: { id: 'summon', name: 'DAGGERHEART.Actions.Types.Summon.Name', - icon: "fa-ghost" + icon: 'fa-ghost' }, effect: { id: 'effect', name: 'DAGGERHEART.Actions.Types.Effect.Name', - icon: "fa-person-rays" + icon: 'fa-person-rays' }, macro: { id: 'macro', name: 'DAGGERHEART.Actions.Types.Macro.Name', - icon: "fa-scroll" + icon: 'fa-scroll' } }; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index f52b144f..978e32cb 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -336,4 +336,4 @@ export const rollTypes = { id: 'ability', label: 'DAGGERHEART.RollTypes.ability.name' } -} +}; diff --git a/module/config/pseudoConfig.mjs b/module/config/pseudoConfig.mjs new file mode 100644 index 00000000..297d42cf --- /dev/null +++ b/module/config/pseudoConfig.mjs @@ -0,0 +1,17 @@ +import { pseudoDocuments } from "../data/_module.mjs"; +import { pseudoDocumentSheet } from "../applications/_module.mjs"; + +//CONFIG.daggerheart.pseudoDocuments +export default { + sheetClass: pseudoDocumentSheet.PseudoDocumentSheet, + feature: { + label: "DAGGERHEART.Feature.Label", + documentClass: pseudoDocuments.feature.BaseFeatureData, + types: { + weapon: { + label: "DAGGERHEART.Feature.Weapon.Label", + documentClass: pseudoDocuments.feature.WeaponFeature, + } + } + } +}; \ No newline at end of file diff --git a/module/config/system.mjs b/module/config/system.mjs index fd198443..be2fc1aa 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -5,6 +5,7 @@ import * as ITEM from './itemConfig.mjs'; import * as SETTINGS from './settingsConfig.mjs'; import * as EFFECTS from './effectConfig.mjs'; import * as ACTIONS from './actionConfig.mjs'; +import pseudoDocuments from "./pseudoConfig.mjs"; export const SYSTEM_ID = 'daggerheart'; @@ -16,5 +17,6 @@ export const SYSTEM = { ITEM, SETTINGS, EFFECTS, - ACTIONS + ACTIONS, + pseudoDocuments }; diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 130c5653..e43ddb99 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -7,3 +7,5 @@ export * as actors from './actor/_module.mjs'; export * as items from './item/_module.mjs'; export { actionsTypes } from './action/_module.mjs'; export * as messages from './chat-message/_modules.mjs'; +export * as fields from './fields/_module.mjs'; +export * as pseudoDocuments from './pseudo-documents/_module.mjs'; diff --git a/module/data/action/_module.mjs b/module/data/action/_module.mjs index ccde347a..c9088886 100644 --- a/module/data/action/_module.mjs +++ b/module/data/action/_module.mjs @@ -1,4 +1,14 @@ -import { DHAttackAction, DHBaseAction, DHDamageAction, DHEffectAction, DHHealingAction, DHMacroAction, DHResourceAction, DHSpellCastAction, DHSummonAction } from "./action.mjs"; +import { + DHAttackAction, + DHBaseAction, + DHDamageAction, + DHEffectAction, + DHHealingAction, + DHMacroAction, + DHResourceAction, + DHSpellCastAction, + DHSummonAction +} from './action.mjs'; export const actionsTypes = { base: DHBaseAction, @@ -10,4 +20,4 @@ export const actionsTypes = { summon: DHSummonAction, effect: DHEffectAction, macro: DHMacroAction -} \ No newline at end of file +}; diff --git a/module/data/action/action.mjs b/module/data/action/action.mjs index 220ccde6..5de1e341 100644 --- a/module/data/action/action.mjs +++ b/module/data/action/action.mjs @@ -334,8 +334,14 @@ export class DHResourceAction extends DHBaseAction { ...extraDefineSchema('target'), ...extraDefineSchema('effects'), resource: new fields.SchemaField({ - type: new fields.StringField({ choices: [], blank: true, required: false, initial: "", label: "Resource" }), - value: new fields.NumberField({ initial: 0, label: "Value" }) + type: new fields.StringField({ + choices: [], + blank: true, + required: false, + initial: '', + label: 'Resource' + }), + value: new fields.NumberField({ initial: 0, label: 'Value' }) }) }; } diff --git a/module/data/action/actionDice.mjs b/module/data/action/actionDice.mjs index 072310ed..9fd445cc 100644 --- a/module/data/action/actionDice.mjs +++ b/module/data/action/actionDice.mjs @@ -1,4 +1,4 @@ -import FormulaField from "../fields/formulaField.mjs"; +import FormulaField from '../fields/formulaField.mjs'; const fields = foundry.data.fields; @@ -6,27 +6,33 @@ export class DHActionDiceData extends foundry.abstract.DataModel { /** @override */ static defineSchema() { return { - multiplier: new fields.StringField({ choices: SYSTEM.GENERAL.multiplierTypes, initial: 'proficiency', label: 'Multiplier' }), + multiplier: new fields.StringField({ + choices: SYSTEM.GENERAL.multiplierTypes, + initial: 'proficiency', + label: 'Multiplier' + }), dice: new fields.StringField({ choices: SYSTEM.GENERAL.diceTypes, initial: 'd6', label: 'Formula' }), bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }), custom: new fields.SchemaField({ enabled: new fields.BooleanField({ label: 'Custom Formula' }), - formula: new FormulaField( { label: 'Formula' } ) + formula: new FormulaField({ label: 'Formula' }) }) - } + }; } getFormula(actor) { - return this.custom.enabled ? this.custom.formula : `${(actor.system[this.multiplier] ?? 1)}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; + return this.custom.enabled + ? this.custom.formula + : `${actor.system[this.multiplier] ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; } } export class DHDamageField extends fields.SchemaField { - constructor(hasBase, options, context={}) { + constructor(hasBase, options, context = {}) { const damageFields = { parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)) - } - if(hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true }) + }; + if (hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true }); super(damageFields, options, context); } } @@ -44,6 +50,6 @@ export class DHDamageData extends DHActionDiceData { nullable: false, required: true }) - } + }; } -} \ No newline at end of file +} diff --git a/module/data/fields/_module.mjs b/module/data/fields/_module.mjs index 41436d4f..3a573a0b 100644 --- a/module/data/fields/_module.mjs +++ b/module/data/fields/_module.mjs @@ -1,2 +1,3 @@ -export { default as FormulaField } from "./formulaField.mjs"; -export { default as ForeignDocumentUUIDField } from "./foreignDocumentUUIDField.mjs"; \ No newline at end of file +export { default as FormulaField } from './formulaField.mjs'; +export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs'; +export { default as PseudoDocumentsField } from './pseudoDocumentsField.mjs'; diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index 2dea1153..da520fd1 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -1,8 +1,7 @@ -import { actionsTypes } from "../action/_module.mjs"; +import { actionsTypes } from '../action/_module.mjs'; // Temporary Solution export default class ActionField extends foundry.data.fields.ObjectField { - getModel(value) { return actionsTypes[value.type] ?? actionsTypes.attack; } @@ -11,10 +10,10 @@ export default class ActionField extends foundry.data.fields.ObjectField { /** @override */ _cleanType(value, options) { - if ( !(typeof value === "object") ) value = {}; + if (!(typeof value === 'object')) value = {}; const cls = this.getModel(value); - if ( cls ) return cls.cleanData(value, options); + if (cls) return cls.cleanData(value, options); return value; } @@ -23,7 +22,7 @@ export default class ActionField extends foundry.data.fields.ObjectField { /** @override */ initialize(value, model, options = {}) { const cls = this.getModel(value); - if ( cls ) return new cls(value, { parent: model, ...options }); + if (cls) return new cls(value, { parent: model, ...options }); return foundry.utils.deepClone(value); } @@ -36,6 +35,6 @@ export default class ActionField extends foundry.data.fields.ObjectField { */ migrateSource(sourceData, fieldData) { const cls = this.getModel(fieldData); - if ( cls ) cls.migrateDataSafe(fieldData); + if (cls) cls.migrateDataSafe(fieldData); } -} \ No newline at end of file +} diff --git a/module/data/fields/formulaField.mjs b/module/data/fields/formulaField.mjs index 82717740..68c26efc 100644 --- a/module/data/fields/formulaField.mjs +++ b/module/data/fields/formulaField.mjs @@ -16,78 +16,78 @@ * Special case StringField which represents a formula. */ export default class FormulaField extends foundry.data.fields.StringField { + /** + * @param {FormulaFieldOptions} [options] - Options which configure the behavior of the field + * @param {foundry.data.types.DataFieldContext} [context] - Additional context which describes the field + */ + constructor(options, context) { + super(options, context); + } - /** - * @param {FormulaFieldOptions} [options] - Options which configure the behavior of the field - * @param {foundry.data.types.DataFieldContext} [context] - Additional context which describes the field - */ - constructor(options, context) { - super(options, context); - } + /** @inheritDoc */ + static get _defaults() { + return foundry.utils.mergeObject(super._defaults, { + deterministic: false + }); + } - /** @inheritDoc */ - static get _defaults() { - return foundry.utils.mergeObject(super._defaults, { - deterministic: false - }); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @inheritDoc */ + _validateType(value) { + const roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, '1')); + roll.evaluateSync({ strict: false }); + if (this.options.deterministic && !roll.isDeterministic) + throw new Error(`must not contain dice terms: ${value}`); + super._validateType(value); + } - /** @inheritDoc */ - _validateType(value) { - const roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, "1")); - roll.evaluateSync({ strict: false }); - if (this.options.deterministic && !roll.isDeterministic) throw new Error(`must not contain dice terms: ${value}`); - super._validateType(value); - } + /* -------------------------------------------- */ + /* Active Effect Integration */ + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Active Effect Integration */ - /* -------------------------------------------- */ + /** @override */ + _castChangeDelta(delta) { + return this._cast(delta).trim(); + } - /** @override */ - _castChangeDelta(delta) { - return this._cast(delta).trim(); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + _applyChangeAdd(value, delta, model, change) { + if (!value) return delta; + const operator = delta.startsWith('-') ? '-' : '+'; + delta = delta.replace(/^[+-]/, '').trim(); + return `${value} ${operator} ${delta}`; + } - /** @override */ - _applyChangeAdd(value, delta, model, change) { - if (!value) return delta; - const operator = delta.startsWith("-") ? "-" : "+"; - delta = delta.replace(/^[+-]/, "").trim(); - return `${value} ${operator} ${delta}`; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + _applyChangeMultiply(value, delta, model, change) { + if (!value) return delta; + const terms = new Roll(value).terms; + if (terms.length > 1) return `(${value}) * ${delta}`; + return `${value} * ${delta}`; + } - /** @override */ - _applyChangeMultiply(value, delta, model, change) { - if (!value) return delta; - const terms = new Roll(value).terms; - if (terms.length > 1) return `(${value}) * ${delta}`; - return `${value} * ${delta}`; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + _applyChangeUpgrade(value, delta, model, change) { + if (!value) return delta; + const terms = new Roll(value).terms; + if (terms.length === 1 && terms[0].fn === 'max') return value.replace(/\)$/, `, ${delta})`); + return `max(${value}, ${delta})`; + } - /** @override */ - _applyChangeUpgrade(value, delta, model, change) { - if (!value) return delta; - const terms = new Roll(value).terms; - if ((terms.length === 1) && (terms[0].fn === "max")) return value.replace(/\)$/, `, ${delta})`); - return `max(${value}, ${delta})`; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ - - /** @override */ - _applyChangeDowngrade(value, delta, model, change) { - if (!value) return delta; - const terms = new Roll(value).terms; - if ((terms.length === 1) && (terms[0].fn === "min")) return value.replace(/\)$/, `, ${delta})`); - return `min(${value}, ${delta})`; - } -} \ No newline at end of file + /** @override */ + _applyChangeDowngrade(value, delta, model, change) { + if (!value) return delta; + const terms = new Roll(value).terms; + if (terms.length === 1 && terms[0].fn === 'min') return value.replace(/\)$/, `, ${delta})`); + return `min(${value}, ${delta})`; + } +} diff --git a/module/data/fields/pseudoDocumentsField.mjs b/module/data/fields/pseudoDocumentsField.mjs new file mode 100644 index 00000000..0f48af5b --- /dev/null +++ b/module/data/fields/pseudoDocumentsField.mjs @@ -0,0 +1,56 @@ +import PseudoDocument from '../pseudo-documents/base/pseudoDocument.mjs'; + +const { TypedObjectField, TypedSchemaField } = foundry.data.fields; + +/** + * @typedef _PseudoDocumentsFieldOptions + * @property {Number} [max] - The maximum amount of elements (default: `Infinity`) + * @property {String[]} [validTypes] - Allowed pseudo-documents types (default: `[]`) + * @property {Function} [validateKey] - callback for validate keys of the object; + + * @typedef {foundry.data.types.DataFieldOptions & _PseudoDocumentsFieldOptions} PseudoDocumentsFieldOptions + */ +export default class PseudoDocumentsField extends TypedObjectField { + /** + * @param {PseudoDocument} model - The PseudoDocument of each entry in this collection. + * @param {PseudoDocumentsFieldOptions} [options] - Options which configure the behavior of the field + * @param {foundry.data.types.DataFieldContext} [context] - Additional context which describes the field + */ + constructor(model, options = {}, context = {}) { + options.validateKey ||= key => foundry.data.validators.isValidId(key); + if (!foundry.utils.isSubclass(model, PseudoDocument)) throw new Error('The model must be a PseudoDocument'); + + const allTypes = model.TYPES; + + const filteredTypes = options.validTypes + ? Object.fromEntries( + Object.entries(allTypes).filter(([key]) => options.validTypes.includes(key)) + ) + : allTypes; + + const field = new TypedSchemaField(filteredTypes); + super(field, options, context); + } + + /** @inheritdoc */ + static get _defaults() { + return Object.assign(super._defaults, { + max: Infinity, + validTypes: [] + }); + } + + /** @override */ + _validateType(value, options = {}) { + if (Object.keys(value).length > this.max) throw new Error(`cannot have more than ${this.max} elements`); + return super._validateType(value, options); + } + + /** @override */ + initialize(value, model, options = {}) { + if (!value) return; + value = super.initialize(value, model, options); + const collection = new foundry.utils.Collection(Object.values(value).map(d => [d._id, d])); + return collection; + } +} diff --git a/module/data/item/_module.mjs b/module/data/item/_module.mjs index e29898cb..da3bf2d4 100644 --- a/module/data/item/_module.mjs +++ b/module/data/item/_module.mjs @@ -10,27 +10,27 @@ import DHSubclass from './subclass.mjs'; import DHWeapon from './weapon.mjs'; export { - DHAncestry, - DHArmor, - DHClass, - DHCommunity, - DHConsumable, - DHDomainCard, - DHFeature, - DHMiscellaneous, - DHSubclass, - DHWeapon -} + DHAncestry, + DHArmor, + DHClass, + DHCommunity, + DHConsumable, + DHDomainCard, + DHFeature, + DHMiscellaneous, + DHSubclass, + DHWeapon +}; export const config = { - ancestry: DHAncestry, - armor: DHArmor, - class: DHClass, - community: DHCommunity, - consumable: DHConsumable, - domainCard: DHDomainCard, - feature: DHFeature, - miscellaneous: DHMiscellaneous, - subclass: DHSubclass, - weapon: DHWeapon, -}; \ No newline at end of file + ancestry: DHAncestry, + armor: DHArmor, + class: DHClass, + community: DHCommunity, + consumable: DHConsumable, + domainCard: DHDomainCard, + feature: DHFeature, + miscellaneous: DHMiscellaneous, + subclass: DHSubclass, + weapon: DHWeapon +}; diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 8605a48e..3dd174d7 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -5,6 +5,7 @@ * @property {string} type - The system type that this data model represents. * @property {boolean} hasDescription - Indicates whether items of this type have description field * @property {boolean} isQuantifiable - Indicates whether items of this type have quantity field + * @property {Record} embedded - Record of document names of pseudo-documents and the path to the collection */ const fields = foundry.data.fields; @@ -16,7 +17,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { label: "Base Item", type: "base", hasDescription: false, - isQuantifiable: false + isQuantifiable: false, + embedded: {}, }; } diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index 9a097a23..d24f53f7 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -1,5 +1,5 @@ -import DHAction from "../action/action.mjs"; -import BaseDataItem from "./base.mjs"; +import DHAction from '../action/action.mjs'; +import BaseDataItem from './base.mjs'; export default class DHDomainCard extends BaseDataItem { /** @inheritDoc */ diff --git a/module/data/item/feature.mjs b/module/data/item/feature.mjs index 56ae2489..37e8ae3a 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -6,9 +6,9 @@ export default class DHFeature extends BaseDataItem { /** @inheritDoc */ static get metadata() { return foundry.utils.mergeObject(super.metadata, { - label: "TYPES.Item.feature", - type: "feature", - hasDescription: true, + label: 'TYPES.Item.feature', + type: 'feature', + hasDescription: true }); } @@ -17,7 +17,7 @@ export default class DHFeature extends BaseDataItem { const fields = foundry.data.fields; return { ...super.defineSchema(), - + //A type of feature seems unnecessary type: new fields.StringField({ choices: SYSTEM.ITEM.featureTypes }), @@ -26,7 +26,7 @@ export default class DHFeature extends BaseDataItem { choices: SYSTEM.ITEM.actionTypes, initial: SYSTEM.ITEM.actionTypes.passive.id }), - //TODO: remove featureType field + //TODO: remove featureType field featureType: new fields.SchemaField({ type: new fields.StringField({ choices: SYSTEM.ITEM.valueTypes, @@ -75,9 +75,10 @@ export default class DHFeature extends BaseDataItem { { nullable: true, initial: null } ), dataField: new fields.StringField({}), - appliesOn: new fields.StringField({ - choices: SYSTEM.EFFECTS.applyLocations, - }, + appliesOn: new fields.StringField( + { + choices: SYSTEM.EFFECTS.applyLocations + }, { nullable: true, initial: null } ), applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), { diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 928d523d..02fcd568 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -1,5 +1,7 @@ import BaseDataItem from './base.mjs'; import FormulaField from '../fields/formulaField.mjs'; +import PseudoDocumentsField from '../fields/pseudoDocumentsField.mjs'; +import BaseFeatureData from '../pseudo-documents/feature/baseFeatureData.mjs'; import ActionField from '../fields/actionField.mjs'; export default class DHWeapon extends BaseDataItem { @@ -9,7 +11,10 @@ export default class DHWeapon extends BaseDataItem { label: 'TYPES.Item.weapon', type: 'weapon', hasDescription: true, - isQuantifiable: true + isQuantifiable: true, + embedded: { + feature: 'featureTest' + } }); } @@ -35,6 +40,12 @@ export default class DHWeapon extends BaseDataItem { }) }), feature: new fields.StringField({ choices: SYSTEM.ITEM.weaponFeatures, blank: true }), + featureTest: new PseudoDocumentsField(BaseFeatureData, { + required: true, + nullable: true, + max: 1, + validTypes: ['weapon'] + }), // actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAttackAction)) actions: new fields.ArrayField(new ActionField()) }; diff --git a/module/data/pseudo-documents/_module.mjs b/module/data/pseudo-documents/_module.mjs new file mode 100644 index 00000000..6f50a137 --- /dev/null +++ b/module/data/pseudo-documents/_module.mjs @@ -0,0 +1,2 @@ +export { default as base } from './base/pseudoDocument.mjs'; +export * as feature from './feature/_module.mjs'; diff --git a/module/data/pseudo-documents/base/base.mjs b/module/data/pseudo-documents/base/base.mjs new file mode 100644 index 00000000..4d5313ba --- /dev/null +++ b/module/data/pseudo-documents/base/base.mjs @@ -0,0 +1,213 @@ +/** + * @typedef {object} PseudoDocumentMetadata + * @property {string} name - The document name of this pseudo-document + * @property {Record} embedded - Record of document names and their collection paths + * @property {typeof foundry.applications.api.ApplicationV2} [sheetClass] - The class used to render this pseudo-document + * @property {string} defaultArtwork - The default image used for newly created documents + */ + +/** + * @class Base class for pseudo-documents + * @extends {foundry.abstract.DataModel} + */ +export default class BasePseudoDocument extends foundry.abstract.DataModel { + /** + * Pseudo-document metadata. + * @returns {PseudoDocumentMetadata} + */ + static get metadata() { + return { + name: '', + embedded: {}, + defaultArtwork: foundry.documents.Item.DEFAULT_ICON, + sheetClass: CONFIG.daggerheart.pseudoDocuments.sheetClass, + }; + } + + /** @override */ + static LOCALIZATION_PREFIXES = ['DOCUMENT']; + + /** @inheritdoc */ + static defineSchema() { + const { fields } = foundry.data; + + return { + _id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }), + name: new fields.StringField({ required: true, blank: false, textSearch: true }), + img: new fields.FilePathField({ categories: ['IMAGE'], initial: this.metadata.defaultArtwork }), + description: new fields.HTMLField({ textSearch: true }) + }; + } + + /* -------------------------------------------- */ + /* Instance Properties */ + /* -------------------------------------------- */ + + /** + * The id of this pseudo-document. + * @type {string} + */ + get id() { + return this._id; + } + + /* -------------------------------------------- */ + + /** + * The uuid of this document. + * @type {string} + */ + get uuid() { + let parent = this.parent; + while (!(parent instanceof BasePseudoDocument) && !(parent instanceof foundry.abstract.Document)) + parent = parent.parent; + return [parent.uuid, this.constructor.metadata.name, this.id].join('.'); + } + + /* -------------------------------------------- */ + + /** + * The parent document of this pseudo-document. + * @type {foundry.abstract.Document} + */ + get document() { + let parent = this; + while (!(parent instanceof foundry.abstract.Document)) parent = parent.parent; + return parent; + } + + /* -------------------------------------------- */ + + /** + * Item to which this PseudoDocument belongs, if applicable. + * @type {foundry.documents.Item|null} + */ + get item() { + return this.parent?.parent instanceof Item ? this.parent.parent : null; + } + + /* -------------------------------------------- */ + + /** + * Actor to which this PseudoDocument's item belongs, if the item is embedded. + * @type {foundry.documents.Actor|null} + */ + get actor() { + return this.item?.parent ?? null; + } + + /* -------------------------------------------- */ + + /** + * The property path to this pseudo document relative to its parent document. + * @type {string} + */ + get fieldPath() { + const fp = this.schema.fieldPath; + let path = fp.slice(0, fp.lastIndexOf('element') - 1); + + if (this.parent instanceof BasePseudoDocument) { + path = [this.parent.fieldPath, this.parent.id, path].join('.'); + } + + return path; + } + + /* -------------------------------------------- */ + /* Embedded Document Methods */ + /* -------------------------------------------- */ + + /** + * Retrieve an embedded pseudo-document. + * @param {string} embeddedName The document name of the embedded pseudo-document. + * @param {string} id The id of the embedded pseudo-document. + * @param {object} [options] Retrieval options. + * @param {boolean} [options.strinct] Throw an error if the embedded pseudo-document does not exist? + * @returns {PseudoDocument|null} + */ + getEmbeddedDocument(embeddedName, id, { strict = false } = {}) { + const embeds = this.constructor.metadata.embedded ?? {}; + if (embeddedName in embeds) { + return foundry.utils.getProperty(this, embeds[embeddedName]).get(id, { strict }) ?? null; + } + return null; + } + + /* -------------------------------------------- */ + /* CRUD Operations */ + /* -------------------------------------------- */ + + /** + * Does this pseudo-document exist in the document's source? + * @type {boolean} + */ + get isSource() { + const source = foundry.utils.getProperty(this.document._source, this.fieldPath); + if (foundry.utils.getType(source) !== 'Object') { + throw new Error('Source is not an object!'); + } + return this.id in source; + } + + /** + * Create a new instance of this pseudo-document. + * @param {object} [data] The data used for the creation. + * @param {object} operation The context of the update operation. + * @param {foundry.abstract.Document} operation.parent The parent of this document. + * @returns {Promise} A promise that resolves to the updated document. + */ + static async create(data = {}, { parent, ...operation } = {}) { + if (!parent) { + throw new Error('A parent document must be specified for the creation of a pseudo-document!'); + } + const id = + operation.keepId && foundry.data.validators.isValidId(data._id) ? data._id : foundry.utils.randomID(); + + const fieldPath = parent.system.constructor.metadata.embedded?.[this.metadata.name]; + if (!fieldPath) { + throw new Error( + `A ${parent.documentName} of type '${parent.type}' does not support ${this.metadata.name}!` + ); + } + + const update = { [`system.${fieldPath}.${id}`]: { ...data, _id: id } }; + const updatedParent = await parent.update(update, operation); + return foundry.utils.getProperty(updatedParent, `system.${fieldPath}.${id}`); + } + + /** + * Delete this pseudo-document. + * @param {object} [operation] The context of the operation. + * @returns {Promise} A promise that resolves to the updated document. + */ + async delete(operation = {}) { + if (!this.isSource) throw new Error('You cannot delete a non-source pseudo-document!'); + const update = { [`${this.fieldPath}.-=${this.id}`]: null }; + return this.document.update(update, operation); + } + + /** + * Duplicate this pseudo-document. + * @returns {Promise} A promise that resolves to the updated document. + */ + async duplicate() { + if (!this.isSource) throw new Error('You cannot duplicate a non-source pseudo-document!'); + const docData = foundry.utils.mergeObject(this.toObject(), { + name: game.i18n.format('DOCUMENT.CopyOf', { name: this.name }) + }); + return this.constructor.create(docData, { parent: this.document }); + } + + /** + * Update this pseudo-document. + * @param {object} [change] The change to perform. + * @param {object} [operation] The context of the operation. + * @returns {Promise} A promise that resolves to the updated document. + */ + async update(change = {}, operation = {}) { + if (!this.isSource) throw new Error('You cannot update a non-source pseudo-document!'); + const path = [this.fieldPath, this.id].join('.'); + const update = { [path]: change }; + return this.document.update(update, operation); + } +} diff --git a/module/data/pseudo-documents/base/pseudoDocument.mjs b/module/data/pseudo-documents/base/pseudoDocument.mjs new file mode 100644 index 00000000..2db23ef5 --- /dev/null +++ b/module/data/pseudo-documents/base/pseudoDocument.mjs @@ -0,0 +1,59 @@ +import BasePseudoDocument from './base.mjs'; +import SheetManagementMixin from './sheetManagementMixin.mjs'; + +/** @extends BasePseudoDocument */ +export default class PseudoDocument extends SheetManagementMixin(BasePseudoDocument) { + static get TYPES() { + const { types } = CONFIG.daggerheart.pseudoDocuments[this.metadata.name]; + const typeEntries = Object.entries(types).map(([key, { documentClass }]) => [key, documentClass]); + return (this._TYPES ??= Object.freeze(Object.fromEntries(typeEntries))); + } + + static _TYPES; + + /** + * The type of this shape. + * @type {string} + */ + static TYPE = ''; + + /* -------------------------------------------- */ + + static getTypesChoices(validTypes) { + const { types } = CONFIG.daggerheart.pseudoDocuments[model.metadata.name]; + const typeEntries = Object.entries(types) + .map(([key, { label }]) => [key, label]) + .filter(([key]) => !validTypes || validTypes.includes(key)); + + return Object.entries(typeEntries); + } + + /* -------------------------------------------- */ + + /** @override */ + static defineSchema() { + const { fields } = foundry.data; + + return Object.assign(super.defineSchema(), { + type: new fields.StringField({ + required: true, + blank: false, + initial: this.TYPE, + validate: value => value === this.TYPE, + validationError: `must be equal to "${this.TYPE}"` + }) + }); + } + + /** @inheritdoc */ + static async create(data = {}, { parent, ...operation } = {}) { + data = foundry.utils.deepClone(data); + if (!data.type) data.type = Object.keys(this.TYPES)[0]; + if (!(data.type in this.TYPES)) { + throw new Error( + `The '${data.type}' type is not a valid type for a '${this.metadata.documentName}' pseudo-document!` + ); + } + return super.create(data, { parent, ...operation }); + } +} diff --git a/module/data/pseudo-documents/base/sheetManagementMixin.mjs b/module/data/pseudo-documents/base/sheetManagementMixin.mjs new file mode 100644 index 00000000..796faf51 --- /dev/null +++ b/module/data/pseudo-documents/base/sheetManagementMixin.mjs @@ -0,0 +1,158 @@ +import BasePseudoDocument from './base.mjs'; +const { ApplicationV2 } = foundry.applications.api; + +/** + * A mixin that adds sheet management capabilities to pseudo-documents + * @template {typeof BasePseudoDocument} T + * @param {T} Base + * @returns {T & typeof PseudoDocumentWithSheets} + */ +export default function SheetManagementMixin(Base) { + class PseudoDocumentWithSheets extends Base { + /** + * Reference to the sheet of this pseudo-document. + * @type {ApplicationV2|null} + */ + get sheet() { + if (this._sheet) return this._sheet; + const cls = this.constructor.metadata.sheetClass ?? ApplicationV2; + + if (!foundry.utils.isSubclass(cls, ApplicationV2)) { + return void ui.notifications.error( + 'Daggerheart | Error on PseudoDocument | sheetClass must be ApplicationV2' + ); + } + + const sheet = new cls({ document: this }); + this._sheet = sheet; + return sheet; + } + + /* -------------------------------------------- */ + /* Static Properties */ + /* -------------------------------------------- */ + + /** + * Set of apps what should be re-render. + * @type {Set} + * @internal + */ + _apps = new Set(); + + /* -------------------------------------------- */ + + /** + * Existing sheets of a specific type for a specific document. + * @type {ApplicationV2 | null} + */ + _sheet = null; + + /* -------------------------------------------- */ + /* Display Methods */ + /* -------------------------------------------- */ + + /** + * Render all the Application instances which are connected to this PseudoDocument. + * @param {ApplicationRenderOptions} [options] Rendering options. + */ + render(options) { + for (const app of this._apps ?? []) { + app.render({ window: { title: app.title }, ...options }); + } + } + + /* -------------------------------------------- */ + + /** + * Register an application to respond to updates to a certain document. + * @param {ApplicationV2} app Application to update. + * @internal + */ + _registerApp(app) { + this._apps.add(app); + } + + /* -------------------------------------------- */ + + /** + * Remove an application from the render registry. + * @param {ApplicationV2} app Application to stop watching. + */ + _unregisterApp(app) { + this._apps.delete(app); + } + + /* -------------------------------------------- */ + /* Drag and Drop */ + /* -------------------------------------------- */ + + /** + * Serialize salient information for this PseudoDocument when dragging it. + * @returns {object} An object of drag data. + */ + toDragData() { + const dragData = { type: this.documentName, data: this.toObject() }; + if (this.id) dragData.uuid = this.uuid; + return dragData; + } + + /* -------------------------------------------- */ + /* Dialog Methods */ + /* -------------------------------------------- */ + + /** + * Spawn a dialog for creating a new PseudoDocument. + * @param {object} [data] Data to pre-populate the document with. + * @param {object} context + * @param {foundry.documents.Item} context.parent A parent for the document. + * @param {string[]|null} [context.types] A list of types to restrict the choices to, or null for no restriction. + * @returns {Promise} + */ + static async createDialog(data = {}, { parent, types = null, ...options } = {}) { + // TODO + } + + /** + * Present a Dialog form to confirm deletion of this PseudoDocument. + * @param {object} [options] - Additional options passed to `DialogV2.confirm`; + * @returns {Promise} A Promise which resolves to the deleted PseudoDocument. + */ + async deleteDialog(options = {}) { + const type = game.i18n.localize(this.constructor.metadata.label); + const content = options.content ?? `

    + ${game.i18n.localize("AreYouSure")} + ${game.i18n.format("SIDEBAR.DeleteWarning", { type })} +

    `; + + return foundry.applications.api.DialogV2.confirm({ + content, + yes: { callback: () => this.delete(operation) }, + window: { + icon: "fa-solid fa-trash", + title: `${game.i18n.format("DOCUMENT.Delete", { type })}: ${this.name}` + }, + ...options + }); + } + + /** + * Gets the default new name for a Document + * @param {object} collection - Collection of Documents + * @returns {string} + */ + static defaultName(collection) { + const documentName = this.metadata.name; + const takenNames = new Set(); + for (const document of collection) takenNames.add(document.name); + + const config = CONFIG.daggerheart.pseudoDocuments[documentName]; + const baseName = game.i18n.localize(config.label); + let name = baseName; + let index = 1; + while (takenNames.has(name)) name = `${baseName} (${++index})`; + return name; + } + } + + return PseudoDocumentWithSheets; +} diff --git a/module/data/pseudo-documents/feature/_module.mjs b/module/data/pseudo-documents/feature/_module.mjs new file mode 100644 index 00000000..794f5d27 --- /dev/null +++ b/module/data/pseudo-documents/feature/_module.mjs @@ -0,0 +1,2 @@ +export { default as BaseFeatureData } from './baseFeatureData.mjs'; +export { default as WeaponFeature } from './weaponFeature.mjs'; diff --git a/module/data/pseudo-documents/feature/baseFeatureData.mjs b/module/data/pseudo-documents/feature/baseFeatureData.mjs new file mode 100644 index 00000000..61d1468d --- /dev/null +++ b/module/data/pseudo-documents/feature/baseFeatureData.mjs @@ -0,0 +1,24 @@ +import PseudoDocument from '../base/pseudoDocument.mjs'; + +export default class BaseFeatureData extends PseudoDocument { + /**@inheritdoc */ + static get metadata() { + return foundry.utils.mergeObject( + super.metadata, + { + name: 'feature', + embedded: {}, + //sheetClass: null //TODO: define feature-sheet + }, + { inplace: false } + ); + } + + static defineSchema() { + const { fields } = foundry.data; + const schema = super.defineSchema(); + return Object.assign(schema, { + subtype: new fields.StringField({ initial: 'test' }) + }); + } +} diff --git a/module/data/pseudo-documents/feature/weaponFeature.mjs b/module/data/pseudo-documents/feature/weaponFeature.mjs new file mode 100644 index 00000000..c8039ede --- /dev/null +++ b/module/data/pseudo-documents/feature/weaponFeature.mjs @@ -0,0 +1,6 @@ +import BaseFeatureData from './baseFeatureData.mjs'; + +export default class WeaponFeature extends BaseFeatureData { + /**@override */ + static TYPE = 'weapon'; +} diff --git a/module/documents/item.mjs b/module/documents/item.mjs index dc8beea7..54759542 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -1,12 +1,18 @@ export default class DhpItem extends Item { - prepareData() { - super.prepareData(); + /** @inheritdoc */ + getEmbeddedDocument(embeddedName, id, { invalid = false, strict = false } = {}) { + const systemEmbeds = this.system.constructor.metadata.embedded ?? {}; + if (embeddedName in systemEmbeds) { + const path = `system.${systemEmbeds[embeddedName]}`; + return foundry.utils.getProperty(this, path).get(id) ?? null; + } + return super.getEmbeddedDocument(embeddedName, id, { invalid, strict }); } - + /** @inheritDoc */ prepareEmbeddedDocuments() { super.prepareEmbeddedDocuments(); - for ( const action of this.system.actions ?? [] ) action.prepareData(); + for (const action of this.system.actions ?? []) action.prepareData(); } /** @@ -33,10 +39,6 @@ export default class DhpItem extends Item { return ['weapon', 'armor', 'miscellaneous', 'consumable'].includes(this.type); } - _onUpdate(data, options, userId) { - super._onUpdate(data, options, userId); - } - static async createDialog(data = {}, { parent = null, pack = null, ...options } = {}) { const documentName = this.metadata.name; const types = game.documentTypes[documentName].filter(t => t !== CONST.BASE_DOCUMENT_TYPE); @@ -99,8 +101,8 @@ export default class DhpItem extends Item { async selectActionDialog() { const content = await foundry.applications.handlebars.renderTemplate( - "systems/daggerheart/templates/views/actionSelect.hbs", - {actions: this.system.actions} + 'systems/daggerheart/templates/views/actionSelect.hbs', + { actions: this.system.actions } ), title = 'Select Action', type = 'div', @@ -108,26 +110,27 @@ export default class DhpItem extends Item { return Dialog.prompt({ title, // label: title, - content, type, + content, + type, callback: html => { - const form = html[0].querySelector("form"), + const form = html[0].querySelector('form'), fd = new foundry.applications.ux.FormDataExtended(form); return this.system.actions.find(a => a._id === fd.object.actionId); }, rejectClose: false - }) + }); } async use(event) { - const actions = this.system.actions + const actions = this.system.actions; let response; - if(actions?.length) { + if (actions?.length) { let action = actions[0]; - if(actions.length > 1 && !event?.shiftKey) { + if (actions.length > 1 && !event?.shiftKey) { // Actions Choice Dialog action = await this.selectActionDialog(); } - if(action) response = action.use(event); + if (action) response = action.use(event); // Check Target // If action.roll => Roll Dialog // Else If action.cost => Cost Dialog diff --git a/styles/application.less b/styles/application.less index 0c583acb..5319a35e 100644 --- a/styles/application.less +++ b/styles/application.less @@ -465,7 +465,7 @@ div.daggerheart.views.multiclass { .form-group { display: flex; align-items: center; - margin-bottom: .5rem; + margin-bottom: 0.5rem; label { flex: 2; } @@ -480,8 +480,8 @@ div.daggerheart.views.multiclass { .data-form-array { border: 1px solid var(--color-fieldset-border); - padding: .5rem; - margin-bottom: .5rem; + padding: 0.5rem; + margin-bottom: 0.5rem; } } } diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 3acf1598..bb603548 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -157,7 +157,8 @@ } } -dh-icon, dh-icon > img { +dh-icon, +dh-icon > img { width: 32px; height: 32px; display: flex; diff --git a/templates/sheets/pseudo-documents/header.hbs b/templates/sheets/pseudo-documents/header.hbs new file mode 100644 index 00000000..4e240356 --- /dev/null +++ b/templates/sheets/pseudo-documents/header.hbs @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file