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

@ -3,6 +3,7 @@ import NpcRollSelectionDialog from '../applications/npcRollSelectionDialog.mjs';
import RollSelectionDialog from '../applications/rollSelectionDialog.mjs';
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
import DHDualityRoll from '../data/chat-message/dualityRoll.mjs';
export default class DhpActor extends Actor {
async _preCreate(data, options, user) {
@ -10,7 +11,7 @@ export default class DhpActor extends Actor {
// Configure prototype token settings
const prototypeToken = {};
if (this.type === 'pc')
if (this.type === 'character')
Object.assign(prototypeToken, {
sight: { enabled: true },
actorLink: true,
@ -28,7 +29,7 @@ export default class DhpActor extends Actor {
}
async updateLevel(newLevel) {
if (this.type !== 'pc' || newLevel === this.system.levelData.level.changed) return;
if (this.type !== 'character' || newLevel === this.system.levelData.level.changed) return;
if (newLevel > this.system.levelData.level.current) {
await this.update({ 'system.levelData.level.changed': newLevel });
@ -39,17 +40,65 @@ export default class DhpActor extends Actor {
return acc;
}, {});
const domainCards = Object.keys(this.system.levelData.levelups)
const domainCards = [];
const experiences = [];
const subclassFeatureState = { class: null, multiclass: null };
let multiclass = null;
Object.keys(this.system.levelData.levelups)
.filter(x => x > newLevel)
.flatMap(levelKey => {
.forEach(levelKey => {
const level = this.system.levelData.levelups[levelKey];
const achievementCards = level.achievements.domainCards.map(x => x.itemUuid);
const advancementCards = level.selections.filter(x => x.type === 'domainCard').map(x => x.itemUuid);
return [...achievementCards, ...advancementCards];
domainCards.push(...achievementCards, ...advancementCards);
experiences.push(...Object.keys(level.achievements.experiences));
const subclass = level.selections.find(x => x.type === 'subclass');
if (subclass) {
const path = subclass.secondaryData.isMulticlass === 'true' ? 'multiclass' : 'class';
const subclassState = Number(subclass.secondaryData.featureState) - 1;
subclassFeatureState[path] = subclassFeatureState[path]
? Math.min(subclassState, subclassFeatureState[path])
: subclassState;
}
multiclass = level.selections.find(x => x.type === 'multiclass');
});
for (var domainCard of domainCards) {
const itemCard = await this.items.find(x => x.uuid === domainCard);
if (experiences.length > 0) {
this.update({
'system.experiences': experiences.reduce((acc, key) => {
acc[`-=${key}`] = null;
return acc;
}, {})
});
}
if (subclassFeatureState.class) {
this.system.class.subclass.update({ 'system.featureState': subclassFeatureState.class });
}
if (subclassFeatureState.multiclass) {
this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass });
}
if (multiclass) {
const multiclassSubclass = this.items.find(x => x.type === 'subclass' && x.system.isMulticlass);
const multiclassItem = this.items.find(x => x.uuid === multiclass.itemUuid);
multiclassSubclass.delete();
multiclassItem.delete();
this.update({
'system.multiclass': {
value: null,
subclass: null
}
});
}
for (let domainCard of domainCards) {
const itemCard = this.items.find(x => x.uuid === domainCard);
itemCard.delete();
}
@ -71,6 +120,94 @@ export default class DhpActor extends Actor {
const levelups = {};
for (var levelKey of Object.keys(levelupData)) {
const level = levelupData[levelKey];
for (var experienceKey in level.achievements.experiences) {
const experience = level.achievements.experiences[experienceKey];
await this.update({
[`system.experiences.${experienceKey}`]: {
description: experience.name,
value: experience.modifier
}
});
}
let multiclass = null;
const domainCards = [];
const subclassFeatureState = { class: null, multiclass: null };
const selections = [];
for (var optionKey of Object.keys(level.choices)) {
const selection = level.choices[optionKey];
for (var checkboxNr of Object.keys(selection)) {
const checkbox = selection[checkboxNr];
if (checkbox.type === 'multiclass') {
multiclass = {
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr)
};
} else if (checkbox.type === 'domainCard') {
domainCards.push({
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr)
});
} else {
if (checkbox.type === 'subclass') {
const path = checkbox.secondaryData.isMulticlass === 'true' ? 'multiclass' : 'class';
subclassFeatureState[path] = Math.max(
Number(checkbox.secondaryData.featureState),
subclassFeatureState[path]
);
}
selections.push({
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr)
});
}
}
}
if (multiclass) {
const subclassItem = await foundry.utils.fromUuid(multiclass.secondaryData.subclass);
const subclassData = subclassItem.toObject();
const multiclassItem = await foundry.utils.fromUuid(multiclass.data[0]);
const multiclassData = multiclassItem.toObject();
const embeddedItem = await this.createEmbeddedDocuments('Item', [
{
...multiclassData,
system: {
...multiclassData.system,
domains: [multiclass.secondaryData.domain],
isMulticlass: true
}
}
]);
await this.createEmbeddedDocuments('Item', [
{
...subclassData,
system: {
...subclassData.system,
isMulticlass: true
}
}
]);
selections.push({ ...multiclass, itemUuid: embeddedItem[0].uuid });
}
for (var domainCard of domainCards) {
const item = await foundry.utils.fromUuid(domainCard.data[0]);
const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]);
selections.push({ ...domainCard, itemUuid: embeddedItem[0].uuid });
}
const achievementDomainCards = [];
for (var card of Object.values(level.achievements.domainCards)) {
const item = await foundry.utils.fromUuid(card.uuid);
@ -79,27 +216,14 @@ export default class DhpActor extends Actor {
achievementDomainCards.push(card);
}
const selections = [];
for (var optionKey of Object.keys(level.choices)) {
const selection = level.choices[optionKey];
for (var checkboxNr of Object.keys(selection)) {
const checkbox = selection[checkboxNr];
let itemUuid = null;
if (subclassFeatureState.class) {
await this.system.class.subclass.update({ 'system.featureState': subclassFeatureState.class });
}
if (checkbox.type === 'domainCard') {
const item = await foundry.utils.fromUuid(checkbox.data[0]);
const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]);
itemUuid = embeddedItem[0].uuid;
}
selections.push({
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr),
itemUuid
});
}
if (subclassFeatureState.multiclass) {
await this.system.multiclass.subclass.update({
'system.featureState': subclassFeatureState.multiclass
});
}
levelups[levelKey] = {
@ -123,154 +247,176 @@ export default class DhpActor extends Actor {
});
}
async diceRoll(modifier, shiftKey) {
if (this.type === 'pc') {
return await this.dualityRoll(modifier, shiftKey);
} else {
return await this.npcRoll(modifier, shiftKey);
}
}
async npcRoll(modifier, shiftKey) {
let advantage = null;
const modifiers = [
{
value: Number.parseInt(modifier.value),
label: modifier.value >= 0 ? `+${modifier.value}` : `-${modifier.value}`,
title: modifier.title
}
];
if (!shiftKey) {
const dialogClosed = new Promise((resolve, _) => {
new NpcRollSelectionDialog(this.system.experiences, resolve).render(true);
});
const result = await dialogClosed;
advantage = result.advantage;
result.experiences.forEach(x =>
modifiers.push({
value: x.value,
label: x.value >= 0 ? `+${x.value}` : `-${x.value}`,
title: x.description
})
);
}
const roll = Roll.create(
`${advantage === true || advantage === false ? 2 : 1}d20${advantage === true ? 'kh' : advantage === false ? 'kl' : ''} ${modifiers.map(x => `+ ${x.value}`).join(' ')}`
);
let rollResult = await roll.evaluate();
const dice = [];
for (var i = 0; i < rollResult.terms.length; i++) {
const term = rollResult.terms[i];
if (term.faces) {
dice.push({ type: `d${term.faces}`, rolls: term.results.map(x => ({ value: x.result })) });
}
}
// There is Only ever one dice term here
return { roll, dice: dice[0], modifiers, advantageState: advantage === true ? 1 : advantage === false ? 2 : 0 };
}
async dualityRoll(modifier, shiftKey, bonusDamage = []) {
/**
* @param {object} config
* @param {Event} config.event
* @param {string} config.title
* @param {object} config.roll
* @param {number} config.roll.modifier
* @param {boolean} [config.roll.simple=false]
* @param {string} [config.roll.type]
* @param {number} [config.roll.difficulty]
* @param {any} [config.damage]
* @param {object} [config.chatMessage]
* @param {string} config.chatMessage.template
* @param {boolean} [config.chatMessage.mute]
* @param {boolean} [config.checkTarget]
*/
async diceRoll(config) {
let hopeDice = 'd12',
fearDice = 'd12',
advantageDice = null,
disadvantageDice = null,
bonusDamageString = '';
advantageDice = 'd6',
disadvantageDice = 'd6',
advantage = config.event.altKey ? true : config.event.ctrlKey ? false : null,
targets,
damage = config.damage,
modifiers = this.formatRollModifier(config.roll),
rollConfig,
formula,
hope,
fear;
const modifiers =
modifier.value !== null
? [
{
value: modifier.value ? Number.parseInt(modifier.value) : 0,
label:
modifier.value >= 0
? `${modifier.title} +${modifier.value}`
: `${modifier.title} ${modifier.value}`,
title: modifier.title
}
]
: [];
if (!shiftKey) {
if (!config.event.shiftKey && !config.event.altKey && !config.event.ctrlKey) {
const dialogClosed = new Promise((resolve, _) => {
new RollSelectionDialog(
this.system.experiences,
bonusDamage,
this.system.resources.hope.value,
resolve
).render(true);
this.type === 'character'
? new RollSelectionDialog(
this.system.experiences,
this.system.resources.hope.value,
resolve
).render(true)
: new NpcRollSelectionDialog(this.system.experiences, resolve).render(true);
});
const result = await dialogClosed;
(hopeDice = result.hope),
(fearDice = result.fear),
(advantageDice = result.advantage),
(disadvantageDice = result.disadvantage);
result.experiences.forEach(x =>
rollConfig = await dialogClosed;
advantage = rollConfig.advantage;
hopeDice = rollConfig.hope;
fearDice = rollConfig.fear;
rollConfig.experiences.forEach(x =>
modifiers.push({
value: x.value,
label: x.value >= 0 ? `+${x.value}` : `-${x.value}`,
title: x.description
})
);
bonusDamageString = result.bonusDamage;
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if (this.type === 'character') {
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if (automateHope && result.hopeUsed) {
await this.update({
'system.resources.hope.value': this.system.resources.hope.value - result.hopeUsed
});
if (automateHope && result.hopeUsed) {
await this.update({
'system.resources.hope.value': this.system.resources.hope.value - result.hopeUsed
});
}
}
}
const roll = new Roll(
`1${hopeDice} + 1${fearDice}${advantageDice ? ` + 1${advantageDice}` : disadvantageDice ? ` - 1${disadvantageDice}` : ''} ${modifiers.map(x => `+ ${x.value}`).join(' ')}`
);
let rollResult = await roll.evaluate();
setDiceSoNiceForDualityRoll(rollResult, advantageDice, disadvantageDice);
const hope = rollResult.dice[0].results[0].result;
const fear = rollResult.dice[1].results[0].result;
const advantage = advantageDice ? rollResult.dice[2].results[0].result : null;
const disadvantage = disadvantageDice ? rollResult.dice[2].results[0].result : null;
if (disadvantage) {
rollResult = { ...rollResult, total: rollResult.total - Math.max(hope, disadvantage) };
}
if (advantage) {
rollResult = { ...rollResult, total: 'Select Hope Die' };
if (this.type === 'character') {
formula = `1${hopeDice} + 1${fearDice}${advantage === true ? ` + 1d6` : advantage === false ? ` - 1d6` : ''}`;
} else {
formula = `${advantage === true || advantage === false ? 2 : 1}d20${advantage === true ? 'kh' : advantage === false ? 'kl' : ''}`;
}
formula += ` ${modifiers.map(x => `+ ${x.value}`).join(' ')}`;
const roll = await Roll.create(formula).evaluate();
const dice = roll.dice.flatMap(dice => ({
denomination: dice.denomination,
number: dice.number,
total: dice.total,
results: dice.results.map(result => ({ result: result.result, discarded: !result.active }))
}));
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if (automateHope && hope > fear) {
await this.update({
'system.resources.hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
)
});
}
if (automateHope && hope === fear) {
await this.update({
'system.resources': {
'hope.value': Math.min(this.system.resources.hope.value + 1, this.system.resources.hope.max),
'stress.value': Math.max(this.system.resources.stress.value - 1, 0)
if (this.type === 'character') {
setDiceSoNiceForDualityRoll(roll, advantage);
hope = roll.dice[0].results[0].result;
fear = roll.dice[1].results[0].result;
if (
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope) &&
config.roll.type === 'action'
) {
if (hope > fear) {
await this.update({
'system.resources.hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
)
});
} else if (hope === fear) {
await this.update({
'system.resources': {
'hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
),
'stress.value': Math.max(this.system.resources.stress.value - 1, 0)
}
});
}
}
}
if (config.checkTarget) {
targets = Array.from(game.user.targets).map(x => {
const target = {
id: x.id,
name: x.actor.name,
img: x.actor.img,
difficulty: x.actor.system.difficulty,
evasion: x.actor.system.evasion?.value
};
target.hit = target.difficulty ? roll.total >= target.difficulty : roll.total >= target.evasion;
return target;
});
}
return {
roll,
rollResult,
hope: { dice: hopeDice, value: hope },
fear: { dice: fearDice, value: fear },
advantage: { dice: advantageDice, value: advantage },
disadvantage: { dice: disadvantageDice, value: disadvantage },
modifiers: modifiers,
bonusDamageString
};
if (config.chatMessage) {
const configRoll = {
title: config.title,
origin: this.id,
dice,
roll,
modifiers: modifiers.filter(x => x.label),
advantageState: advantage
};
if (this.type === 'character') {
configRoll.hope = { dice: hopeDice, value: hope };
configRoll.fear = { dice: fearDice, value: fear };
configRoll.advantage = { dice: advantageDice, value: roll.dice[2]?.results[0].result ?? null };
}
if (damage) configRoll.damage = damage;
if (targets) configRoll.targets = targets;
const systemData =
this.type === 'character' && !config.roll.simple ? new DHDualityRoll(configRoll) : configRoll,
cls = getDocumentClass('ChatMessage'),
msg = new cls({
type: config.chatMessage.type ?? 'dualityRoll',
sound: config.chatMessage.mute ? null : CONFIG.sounds.dice,
system: systemData,
content: config.chatMessage.template,
rolls: [roll]
});
await cls.create(msg.toObject());
}
return roll;
}
formatRollModifier(roll) {
const modifier = roll.modifier !== null ? Number.parseInt(roll.modifier) : null;
return modifier !== null
? [
{
value: modifier,
label: roll.label
? modifier >= 0
? `${roll.label} +${modifier}`
: `${roll.label} ${modifier}`
: null,
title: roll.label
}
]
: [];
}
async damageRoll(title, damage, targets, shiftKey) {
@ -300,7 +446,11 @@ export default class DhpActor extends Actor {
for (var i = 0; i < rollResult.terms.length; i++) {
const term = rollResult.terms[i];
if (term.faces) {
dice.push({ type: `d${term.faces}`, rolls: term.results.map(x => x.result) });
dice.push({
type: `d${term.faces}`,
rolls: term.results.map(x => x.result),
total: term.results.reduce((acc, x) => acc + x.result, 0)
});
} else if (term.operator) {
} else if (term.number) {
const operator = i === 0 ? '' : rollResult.terms[i - 1].operator;
@ -401,11 +551,6 @@ export default class DhpActor extends Actor {
}
}
async emulateItemDrop(data) {
const event = new DragEvent('drop', { altKey: game.keyboard.isModifierActive('Alt') });
return this.sheet._onDropItem(event, { data: data });
}
//Move to action-scope?
async useAction(action) {
const userTargets = Array.from(game.user.targets);