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

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

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