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,36 @@
import DHAncestry from './ancestry.mjs';
import DHArmor from './armor.mjs';
import DHClass from './class.mjs';
import DHCommunity from './community.mjs';
import DHConsumable from './consumable.mjs';
import DHDomainCard from './domainCard.mjs';
import DHFeature from './feature.mjs';
import DHMiscellaneous from './miscellaneous.mjs';
import DHSubclass from './subclass.mjs';
import DHWeapon from './weapon.mjs';
export {
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
};

View file

@ -0,0 +1,21 @@
import BaseDataItem from './base.mjs';
export default class DHAncestry extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.ancestry",
type: "ancestry",
hasDescription: true,
});
}
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
//TODO: add features field
};
}
}

View file

@ -0,0 +1,36 @@
import BaseDataItem from './base.mjs';
export default class DHArmor extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.armor",
type: "armor",
hasDescription: true,
isQuantifiable: true,
});
}
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
equipped: new fields.BooleanField({ initial: false }),
baseScore: new fields.NumberField({ integer: true, initial: 0 }),
feature: new fields.StringField({ choices: SYSTEM.ITEM.armorFeatures, blank: true }),
marks: new fields.SchemaField({
max: new fields.NumberField({ initial: 6, integer: true }),
value: new fields.NumberField({ initial: 0, integer: true })
}),
baseThresholds: new fields.SchemaField({
major: new fields.NumberField({ integer: true, initial: 0 }),
severe: new fields.NumberField({ integer: true, initial: 0 })
}),
};
}
get featureInfo() {
return this.feature ? CONFIG.daggerheart.ITEM.armorFeatures[this.feature] : null;
}
}

56
module/data/item/base.mjs Normal file
View file

@ -0,0 +1,56 @@
/**
* Describes metadata about the item data model type
* @typedef {Object} ItemDataModelMetadata
* @property {string} label - A localizable label used on application.
* @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<string,string>} embedded - Record of document names of pseudo-documents and the path to the collection
*/
const fields = foundry.data.fields;
export default class BaseDataItem extends foundry.abstract.TypeDataModel {
/** @returns {ItemDataModelMetadata}*/
static get metadata() {
return {
label: "Base Item",
type: "base",
hasDescription: false,
isQuantifiable: false,
embedded: {},
};
}
/** @inheritDoc */
static defineSchema() {
const schema = {};
if (this.metadata.hasDescription)
schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.isQuantifiable)
schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true });
return schema;
}
/**
* Convenient access to the item's actor, if it exists.
* @returns {foundry.documents.Actor | null}
*/
get actor() {
return this.parent.actor;
}
/**
* Obtain a data object used to evaluate any dice rolls associated with this Item Type
* @param {object} [options] - Options which modify the getRollData method.
* @returns {object}
*/
getRollData(options = {}) {
const actorRollData = this.actor?.getRollData() ?? {};
const data = { ...actorRollData, item: { ...this } };
return data;
}
}

View file

@ -0,0 +1,87 @@
import BaseDataItem from './base.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
export default class DHClass extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Item.class',
type: 'class',
hasDescription: true
});
}
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
domains: new fields.ArrayField(new fields.StringField(), { max: 2 }),
classItems: new fields.ArrayField(new ForeignDocumentUUIDField({ type: 'Item' })),
evasion: new fields.NumberField({ initial: 0, integer: true }),
features: new fields.ArrayField(new ForeignDocumentUUIDField({ type: 'Item' })),
subclasses: new fields.ArrayField(
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
),
inventory: new fields.SchemaField({
take: new fields.ArrayField(
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
),
choiceA: new fields.ArrayField(
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
),
choiceB: new fields.ArrayField(
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
)
}),
characterGuide: new fields.SchemaField({
suggestedTraits: new fields.SchemaField({
agility: new fields.NumberField({ initial: 0, integer: true }),
strength: new fields.NumberField({ initial: 0, integer: true }),
finesse: new fields.NumberField({ initial: 0, integer: true }),
instinct: new fields.NumberField({ initial: 0, integer: true }),
presence: new fields.NumberField({ initial: 0, integer: true }),
knowledge: new fields.NumberField({ initial: 0, integer: true })
}),
suggestedPrimaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }),
suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }),
suggestedArmor: new ForeignDocumentUUIDField({ type: 'Item' })
}),
isMulticlass: new fields.BooleanField({ initial: false })
};
}
async _preCreate(data, options, user) {
const allowed = await super._preCreate(data, options, user);
if (allowed === false) return;
if (this.actor?.type === 'character') {
const path = data.system.isMulticlass ? 'system.multiclass.value' : 'system.class.value';
if (foundry.utils.getProperty(this.actor, path)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.ClassAlreadySelected'));
return false;
}
}
}
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
if (options.parent?.type === 'character') {
const path = `system.${data.system.isMulticlass ? 'multiclass.value' : 'class.value'}`;
options.parent.update({ [path]: `${options.parent.uuid}.Item.${data._id}` });
}
}
_onDelete(options, userId) {
super._onDelete(options, userId);
if (options.parent?.type === 'character') {
const path = `system.${this.isMulticlass ? 'multiclass' : 'class'}`;
options.parent.update({
[`${path}.value`]: null
});
foundry.utils.getProperty(options.parent, `${path}.subclass`)?.delete();
}
}
}

View file

@ -0,0 +1,22 @@
import BaseDataItem from './base.mjs';
export default class DHCommunity extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.community",
type: "community",
hasDescription: true,
});
}
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
//TODO: add features field
};
}
}

View file

@ -0,0 +1,22 @@
import BaseDataItem from "./base.mjs";
export default class DHConsumable extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.consumable",
type: "consumable",
hasDescription: true,
isQuantifiable: true,
});
}
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
consumeOnUse: new fields.BooleanField({ initial: false })
};
}
}

View file

@ -0,0 +1,50 @@
import DHAction from '../action/action.mjs';
import BaseDataItem from './base.mjs';
export default class DHDomainCard extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Item.domainCard',
type: 'domainCard',
hasDescription: true
});
}
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
domain: new fields.StringField({ choices: SYSTEM.DOMAIN.domains, required: true, blank: true }),
level: new fields.NumberField({ initial: 1, integer: true }),
recallCost: new fields.NumberField({ initial: 0, integer: true }),
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(DHAction))
};
}
async _preCreate(data, options, user) {
const allowed = await super._preCreate(data, options, user);
if (allowed === false) return;
if (this.actor?.type === 'character') {
if (!this.actor.system.class.value) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.NoClassSelected'));
return false;
}
if (!this.actor.system.domains.find(x => x === this.domain)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.LacksDomain'));
return false;
}
if (this.actor.system.domainCards.total.find(x => x.name === this.parent.name)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.DuplicateDomainCard'));
return false;
}
}
}
}

View file

@ -0,0 +1,151 @@
import { getTier } from '../../helpers/utils.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
});
}
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
//A type of feature seems unnecessary
type: new fields.StringField({ choices: SYSTEM.ITEM.featureTypes }),
//TODO: remove actionType field
actionType: new fields.StringField({
choices: SYSTEM.ITEM.actionTypes,
initial: SYSTEM.ITEM.actionTypes.passive.id
}),
//TODO: remove featureType field
featureType: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.ITEM.valueTypes,
initial: Object.keys(SYSTEM.ITEM.valueTypes).find(x => x === 'normal')
}),
data: new fields.SchemaField({
value: new fields.StringField({}),
property: new fields.StringField({
choices: SYSTEM.ACTOR.featureProperties,
initial: Object.keys(SYSTEM.ACTOR.featureProperties).find(x => x === 'spellcastingTrait')
}),
max: new fields.NumberField({ initial: 1, integer: true }),
numbers: new fields.TypedObjectField(
new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
used: new fields.BooleanField({ initial: false })
})
)
})
}),
refreshData: new fields.SchemaField(
{
type: new fields.StringField({ choices: SYSTEM.GENERAL.refreshTypes }),
uses: new fields.NumberField({ initial: 1, integer: true }),
//TODO: remove refreshed field
refreshed: new fields.BooleanField({ initial: true })
},
{ nullable: true, initial: null }
),
//TODO: remove refreshed field
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
disabled: new fields.BooleanField({ initial: false }),
//TODO: re do it completely or just remove it
effects: new fields.TypedObjectField(
new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.EFFECTS.effectTypes }),
valueType: new fields.StringField({ choices: SYSTEM.EFFECTS.valueTypes }),
parseType: new fields.StringField({ choices: SYSTEM.EFFECTS.parseTypes }),
initiallySelected: new fields.BooleanField({ initial: true }),
options: new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField({}),
value: new fields.StringField({})
}),
{ nullable: true, initial: null }
),
dataField: new fields.StringField({}),
appliesOn: new fields.StringField(
{
choices: SYSTEM.EFFECTS.applyLocations
},
{ nullable: true, initial: null }
),
applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), {
nullable: true,
initial: null
}),
valueData: new fields.SchemaField({
value: new fields.StringField({}),
fromValue: new fields.StringField({ initial: null, nullable: true }),
type: new fields.StringField({ initial: null, nullable: true }),
hopeIncrease: new fields.StringField({ initial: null, nullable: true })
})
})
),
actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAction))
};
}
get multiclassTier() {
return getTier(this.multiclass);
}
async refresh() {
if (this.refreshData) {
if (this.featureType.type === SYSTEM.ITEM.valueTypes.dice.id) {
const update = { 'system.refreshData.refreshed': true };
Object.keys(this.featureType.data.numbers).forEach(
x => (update[`system.featureType.data.numbers.-=${x}`] = null)
);
await this.parent.update(update);
} else {
await this.parent.update({ 'system.refreshData.refreshed': true });
}
}
}
get effectData() {
const effectValues = Object.values(this.effects);
const effectCategories = Object.keys(SYSTEM.EFFECTS.effectTypes).reduce((acc, effectType) => {
acc[effectType] = effectValues.reduce((acc, effect) => {
if (effect.type === effectType) {
acc.push({ ...effect, valueData: this.#parseValues(effect.parseType, effect.valueData) });
}
return acc;
}, []);
return acc;
}, {});
return effectCategories;
}
#parseValues(parseType, values) {
return Object.keys(values).reduce((acc, prop) => {
acc[prop] = this.#parseValue(parseType, values[prop]);
return acc;
}, {});
}
#parseValue(parseType, value) {
switch (parseType) {
case SYSTEM.EFFECTS.parseTypes.number.id:
return Number.parseInt(value);
default:
return value;
}
}
}

View file

@ -0,0 +1,21 @@
import BaseDataItem from './base.mjs';
export default class DHMiscellaneous extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.miscellaneous",
type: "miscellaneous",
hasDescription: true,
isQuantifiable: true,
});
}
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
};
}
}

View file

@ -0,0 +1,82 @@
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import BaseDataItem from './base.mjs';
export default class DHSubclass extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Item.subclass',
type: 'subclass',
hasDescription: true
});
}
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
spellcastingTrait: new fields.StringField({
choices: SYSTEM.ACTOR.abilities,
integer: false,
nullable: true,
initial: null
}),
foundationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
specializationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
masteryFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
isMulticlass: new fields.BooleanField({ initial: false })
};
}
get features() {
return {
foundation: this.foundationFeature,
specialization: this.featureState >= 2 ? this.specializationFeature : null,
mastery: this.featureState === 3 ? this.masteryFeature : null
};
}
async _preCreate(data, options, user) {
const allowed = await super._preCreate(data, options, user);
if (allowed === false) return;
if (this.actor?.type === 'character') {
const classData = this.actor.items.find(
x => x.type === 'class' && x.system.isMulticlass === data.system.isMulticlass
);
const subclassData = this.actor.items.find(
x => x.type === 'subclass' && x.system.isMulticlass === data.system.isMulticlass
);
if (!classData) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.MissingClass'));
return false;
} else if (subclassData) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassAlreadySelected'));
return false;
} else if (classData.system.subclasses.every(x => x.uuid !== `Item.${data._id}`)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassNotInClass'));
return false;
}
}
}
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
if (options.parent?.type === 'character') {
const path = `system.${data.system.isMulticlass ? 'multiclass.subclass' : 'class.subclass'}`;
options.parent.update({ [path]: `${options.parent.uuid}.Item.${data._id}` });
}
}
_onDelete(options, userId) {
super._onDelete(options, userId);
if (options.parent?.type === 'character') {
const path = `system.${this.isMulticlass ? 'multiclass.subclass' : 'class.subclass'}`;
options.parent.update({ [path]: null });
}
}
}

View file

@ -0,0 +1,53 @@
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',
hasDescription: true,
isQuantifiable: true,
embedded: {
feature: 'featureTest'
}
});
}
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
equipped: new fields.BooleanField({ initial: false }),
//SETTINGS
secondary: new fields.BooleanField({ initial: false }),
trait: new fields.StringField({ required: true, choices: SYSTEM.ACTOR.abilities, initial: 'agility' }),
range: new fields.StringField({ required: true, choices: SYSTEM.GENERAL.range, initial: 'melee' }),
burden: new fields.StringField({ required: true, choices: SYSTEM.GENERAL.burden, initial: 'oneHanded' }),
//DAMAGE
damage: new fields.SchemaField({
value: new FormulaField({ initial: 'd6' }),
type: new fields.StringField({
required: true,
choices: SYSTEM.GENERAL.damageTypes,
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())
};
}
}