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.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 fc32e152..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, @@ -134,43 +135,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 +181,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 +236,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 +270,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 997a2ba0..0b43ee8f 100755 --- a/lang/en.json +++ b/lang/en.json @@ -146,6 +146,7 @@ } }, "General": { + "Name": "Name", "Hope": "Hope", "Fear": "Fear", "Duality": "Duality", @@ -487,6 +488,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." @@ -1293,10 +1298,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/_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/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..6453f896 100644 --- a/module/applications/config/Action.mjs +++ b/module/applications/config/Action.mjs @@ -1,7 +1,7 @@ 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({}); @@ -9,21 +9,25 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio 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,91 @@ 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); - } - - await this.action.parent.parent.update({ 'system.actions': newActions }); + 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) {} } 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..1b8c416b --- /dev/null +++ b/module/applications/sheets/item.mjs @@ -0,0 +1,132 @@ +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) + ) + }); + } + }; +} 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..815c6b9b 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..17a83f95 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..40fa9b02 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..dd22d216 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..a54c0140 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/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 792df85d..4db96a64 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..978e32cb 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/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 e7712547..e43ddb99 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -5,4 +5,7 @@ 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'; +export * as fields from './fields/_module.mjs'; +export * as pseudoDocuments from './pseudo-documents/_module.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..c9088886 --- /dev/null +++ b/module/data/action/_module.mjs @@ -0,0 +1,23 @@ +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 +}; diff --git a/module/data/action/action.mjs b/module/data/action/action.mjs new file mode 100644 index 00000000..5de1e341 --- /dev/null +++ b/module/data/action/action.mjs @@ -0,0 +1,386 @@ +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..9fd445cc --- /dev/null +++ b/module/data/action/actionDice.mjs @@ -0,0 +1,55 @@ +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 + }) + }; + } +} 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/_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 new file mode 100644 index 00000000..da520fd1 --- /dev/null +++ b/module/data/fields/actionField.mjs @@ -0,0 +1,40 @@ +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); + } +} 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 30db2468..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 e11fc5a7..814626e3 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -1,4 +1,4 @@ -import DaggerheartAction from '../action.mjs'; +import DHAction from '../action/action.mjs'; import BaseDataItem from './base.mjs'; export default class DHDomainCard extends BaseDataItem { @@ -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..37e8ae3a 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -1,14 +1,14 @@ 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 { /** @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({}), { @@ -92,7 +93,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..02fcd568 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -1,14 +1,20 @@ -import BaseDataItem from "./base.mjs"; -import FormulaField from "../fields/formulaField.mjs"; +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 { /** @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, + embedded: { + feature: 'featureTest' + } }); } @@ -33,8 +39,15 @@ export default class DHWeapon extends BaseDataItem { initial: 'physical' }) }), - 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/actor.mjs b/module/documents/actor.mjs index 14639224..fea4a426 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) { @@ -246,87 +247,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}`, @@ -334,60 +300,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) { @@ -417,7 +446,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..54759542 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -1,6 +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(); } /** @@ -27,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); @@ -90,4 +98,47 @@ 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/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 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)}}