Refactor/84 data models structure (#131)

* - Move all DataModel item files to a new 'items' subfolder for better organization
- Add _module.mjs file to simplify imports
- Update all import paths
- Rename class for use the new acronym DH

* FIX: remove unnecessary import

* FEAT: BaseDataItem class
add TODO comments for future improvements
FIX: Remove effect field on template
FIX: remove unused DhpEffects file

* FEAT: new FormulaField class
FEAT: add getRollData on BaseDataItem Class
FEAT: weapon
FIX: remove inventoryWeapon field on Weapon Data Model

* FEAT: add class prepareBaseData for domains

* FEAT: new ForeignDocumentUUIDField
FIX: Remove unnecessary fields
FEAT: use ForeignDocumentUUIDField in the Item Class DataModel

* FIX: remove wrong option in String Field

* FIX: remove unused import

* FIX: ADD htmlFields description in manifest

* FIX: minor fixes

* REFACTOR: rename folder `data/items` -> `data/item`
REFACTOR: rename folder `data/messages` -> `data/chat-message`.

* FIX: imports
FIX: items sheet new paths
FIX: ItemDataModelMetadata type jsdoc

* FEAT: formatting code
FIX: fix fields used
FEAT: add jsdoc

* 110 - Class Data Model (#111)

* Added PreCreate/Create/Delete logic for Class/Subclass and set it as foreignUUID fields in PC

* Moved methods into TypedModelData

* Simplified Subclass

* Fixed up data model and a basic placeholder template (#117)

* 118 - adversary data model (#119)

* Fixed datamodel and set up basic template in new style

* Added in a temp attack button, because why not

* Restored HitPoints counting up

* 113 - Character Data Model (#114)

* Improved Character datamodel

* Removed additional unneccessary getters

* Preliminary cleanup in the class sheet

* Cleanup of 'pc' references

* Corrected Duality rolling from Character

* Fix to damage roll

* Added a basic BaseDataActor data model

* Gathered exports

* getRollData recursion fix

* Feature/112 items use action datamodel (#127)

* Create new actions classes

* actions types - attack roll

* fixes before merge

* First PR

* Add daggerheart.css to gitignore

* Update ToDo

* Remove console log

* Fixed chat /dr roll

* Remove jQuery

* Fixed so the different chat themes work again

* Fixed duality roll buttons

* Fix to advantage/disadvantage shortcut

* Extand action to other item types

* Roll fixes

* Fixes to adversary rolls

* resources

* Fixed adversary dice

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>

* Feature/116-implementation-of-pseudo-documents (#125)

* FEAT: add baseDataModel logic

* FEAT: new PseudoDocumentsField
FIX: BasePseudoDocument 's getEmbeddedDocument

* FEAT: PseudoDocument class

* FEAT: add TypedPseudoDocument
REFACTOR: PreudoDocument
FIX: Typos Bug

* FIX: CONFIG types

* FEAT: basic PseudoDocumentSheet

* FIX: remove schema
ADD: input of example

---------

Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>
Co-authored-by: WBHarry <williambjrklund@gmail.com>

* Levelup Followup (#126)

* Levelup applies bonuses to character

* Added visualisation of domain card levels

* Fixed domaincard level max for selections in a tier

* A trait can now only be level up once within the same tier

---------

Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>
Co-authored-by: joaquinpereyra98 <24190917+joaquinpereyra98@users.noreply.github.com>
Co-authored-by: Dapoulp <74197441+Dapoulp@users.noreply.github.com>
This commit is contained in:
WBHarry 2025-06-13 14:17:13 +02:00 committed by GitHub
parent 90bc2dc488
commit 187ee3e1bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
153 changed files with 5481 additions and 4829 deletions

View file

@ -0,0 +1,2 @@
export { default as base } from './base/pseudoDocument.mjs';
export * as feature from './feature/_module.mjs';

View file

@ -0,0 +1,213 @@
/**
* @typedef {object} PseudoDocumentMetadata
* @property {string} name - The document name of this pseudo-document
* @property {Record<string, string>} 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<foundry.abstract.Document>} 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<foundry.abstract.Document>} 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<foundry.abstract.Document>} 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<foundry.abstract.Document>} 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);
}
}

View file

@ -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 });
}
}

View file

@ -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<ApplicationV2>}
* @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<BasePseudoDocument|null>}
*/
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<foundry.abstract.Document>} A Promise which resolves to the deleted PseudoDocument.
*/
async deleteDialog(options = {}) {
const type = game.i18n.localize(this.constructor.metadata.label);
const content = options.content ?? `<p>
<strong>${game.i18n.localize("AreYouSure")}</strong>
${game.i18n.format("SIDEBAR.DeleteWarning", { type })}
</p>`;
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;
}

View file

@ -0,0 +1,2 @@
export { default as BaseFeatureData } from './baseFeatureData.mjs';
export { default as WeaponFeature } from './weaponFeature.mjs';

View file

@ -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' })
});
}
}

View file

@ -0,0 +1,6 @@
import BaseFeatureData from './baseFeatureData.mjs';
export default class WeaponFeature extends BaseFeatureData {
/**@override */
static TYPE = 'weapon';
}