Merge branch 'main' into feature/granular-action-outcomes

This commit is contained in:
WBHarry 2026-06-30 20:49:37 +02:00
commit 2e9dede678
207 changed files with 2666 additions and 1798 deletions

View file

@ -79,8 +79,8 @@ export default class DHAttackAction extends DHDamageAction {
const str = damageString
? damageString
: game.i18n.format('DAGGERHEART.GENERAL.missingX', {
x: game.i18n.localize('DAGGERHEART.GENERAL.damage')
});
x: game.i18n.localize('DAGGERHEART.GENERAL.damage')
});
const icons = Array.from(type)
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)

View file

@ -54,6 +54,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return {};
}
get hasDescription() {
return Boolean(this.description);
}
/**
* Create a Map containing each Action step based on fields define in schema. Ordered by Fields order property.
*
@ -144,8 +148,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return this.item instanceof DhpActor
? this.item
: this.item?.parent instanceof DhpActor
? this.item.parent
: null;
? this.item.parent
: null;
}
/**
@ -223,12 +227,13 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @returns {object}
*/
async use(event, configOptions = {}) {
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
if (!this.actor) throw new Error('An Action can\'t be used outside of an Actor context.');
let config = this.prepareConfig(event, configOptions);
if (!config) return;
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item);
config.effects =
await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects(this.actor, this.item);
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
@ -300,17 +305,17 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
const groupAttackTokens = this.damage.groupAttack
? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens(
this.actor.id,
this.damage.groupAttack
)
this.actor.id,
this.damage.groupAttack
)
: null;
config.damageOptions = {
groupAttack: this.damage.groupAttack
? {
numAttackers: Math.max(groupAttackTokens.length, 1),
range: this.damage.groupAttack
}
numAttackers: Math.max(groupAttackTokens.length, 1),
range: this.damage.groupAttack
}
: null
};
}
@ -333,27 +338,45 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}
/**
* Get the all potentially applicable effects on the actor
* Get the all potentially applicable effects on the actor for the action's RollDialog
* @param {DHActor} actor The actor performing the action
* @param {DHItem|DhActor} effectParent The parent of the effect
* @returns {DhActiveEffect[]}
*/
static async getEffects(actor, effectParent) {
static async getActionRelevantEffects(actor, effectParent) {
if (!actor) return [];
return Array.from(await actor.allApplicableEffects({ noTransferArmor: true, noSelfArmor: true })).filter(
effect => {
/* Effects on weapons only ever apply for the weapon itself */
if (effect.parent.type === 'weapon') {
/* Unless they're secondary - then they apply only to other primary weapons */
if (effect.parent.system.secondary) {
if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false;
} else if (effectParent?.id !== effect.parent.id) return false;
}
// Changes on weapon effects are not typically only applicable to show in the roll dialog for the weapon itself
// The exemptions to this rule are listed below
const weaponTransferredEffectKeys = [
'system.bonuses.roll.spellcast.bonus'
];
return !effect.isSuppressed;
const results = [];
const applicableEffects = await actor.allApplicableEffects({ noTransferArmor: true, noSelfArmor: true });
for (const effect of [...applicableEffects].filter(e => !e.isSuppressed)) {
if (effect.parent.type === 'weapon') {
// Effects on weapons only ever apply for the weapon itself (with a few exceptions)
const restricted =
effect.parent.system.secondary
// Secondary applies only to other primary weapons
? effectParent?.type !== 'weapon' || effectParent?.system.secondary
// Primary only applies to itself
: effectParent?.id !== effect.parent.id;
if (restricted) {
const sourceChanges = effect._source.system.changes;
const changes = sourceChanges.filter(x => weaponTransferredEffectKeys.includes(x.key));
if (changes.length) {
results.push(effect.clone({ 'system.changes': changes }));
}
continue;
}
}
);
results.push(effect);
}
return results;
}
/**
@ -428,7 +451,15 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
static migrateData(source) {
if (source.damage?.parts && Array.isArray(source.damage.parts)) {
let hitPointsExists = source.damage.parts.some(x => x.applyTo === 'hitPoints');
source.damage.parts = source.damage.parts.reduce((acc, part) => {
if (!part.applyTo && hitPointsExists) return acc;
if (!part.applyTo) {
hitPointsExists = true;
part.applyTo = 'hitPoints';
}
acc[part.applyTo] = part;
return acc;
}, {});
@ -459,8 +490,7 @@ export class ResourceUpdateMap extends Map {
} else if (!existing?.clear) {
this.set(resource.key, {
...existing,
value: existing.value + (resource.value ?? 0),
total: existing.total + (resource.total ?? 0)
value: existing.value + (resource.value ?? 0)
});
}
}

View file

@ -90,13 +90,13 @@ export default class BeastformEffect extends BaseEffect {
...baseUpdate,
x,
y,
'texture': {
texture: {
enabled: this.characterTokenData.usesDynamicToken,
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg,
scaleX: this.characterTokenData.tokenSize.scale,
scaleY: this.characterTokenData.tokenSize.scale
},
'ring': {
ring: {
subject: {
texture:
token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg

View file

@ -166,10 +166,10 @@ export default class ArmorChange extends foundry.abstract.DataModel {
value:
change.type === 'armor'
? {
...change.value,
current: Math.min(change.value.current, newMax),
max: newMax
}
...change.value,
current: Math.min(change.value.current, newMax),
max: newMax
}
: change.value
}))
];

14
module/data/actor/_types.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
import DhpActor from '../../documents/actor.mjs'
import DhCharacter from './character.mjs';
declare module './base.mjs' {
export default interface BaseDataActor {
parent: DhpActor<this>;
}
}
declare module './companion.mjs' {
export default interface DhCompanion {
partner: DhpActor<DhCharacter>;
}
}

View file

@ -87,6 +87,7 @@ export default class DhpAdversary extends DhCreature {
parts: {
hitPoints: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
multiplier: 'flat'
}

View file

@ -1,9 +1,13 @@
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
import DHItem from '../../documents/item.mjs';
import { createShallowProxy, getScrollTextData } from '../../helpers/utils.mjs';
const fields = foundry.data.fields;
/**
* @import DHItem from '../../documents/item.mjs';
* @import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
*/
/** Function to generate resistance fields for damage types */
const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
new fields.SchemaField({
resistance: new fields.BooleanField({
@ -96,6 +100,8 @@ export const commonActorRules = (extendedData = { damageReduction: {}, attack: {
* @property {Boolean} isNPC - This data model represents a NPC?
* @property {typeof DHBaseActorSettings} settingSheet - The sheet class used to render the settings UI for this actor type.
*/
/** Base actor type data model for all actors in Daggerheart */
export default class BaseDataActor extends foundry.abstract.TypeDataModel {
/** @returns {ActorDataModelMetadata}*/
static get metadata() {

View file

@ -64,7 +64,18 @@ export default class DhCharacter extends DhCreature {
core: new fields.BooleanField({ initial: false })
})
),
gold: new GoldField(),
gold: new GoldField({
initial: () => {
const homebrew = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew);
const { coins, handfuls, bags, chests } = homebrew.currency;
return {
coins: coins.enabled ? coins.initialAmount : 0,
handfuls: handfuls.enabled ? handfuls.initialAmount : 0,
bags: bags.enabled ? bags.initialAmount : 0,
chests: chests.enabled ? chests.initialAmount : 0
};
}
}),
scars: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.scars' }),
biography: new fields.SchemaField({
background: new fields.HTMLField(),
@ -96,6 +107,7 @@ export default class DhCharacter extends DhCreature {
parts: {
hitPoints: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
custom: {
enabled: true,
@ -304,7 +316,12 @@ export default class DhCharacter extends DhCreature {
label: 'DAGGERHEART.ACTORS.Character.defaultDisadvantageDice'
})
})
})
}),
/** Accumulated armor score from all sources */
armorScore: new fields.SchemaField({
value: new fields.NumberField(),
max: new fields.NumberField()
}, { persisted: false })
};
}
@ -315,8 +332,8 @@ export default class DhCharacter extends DhCreature {
return currentLevel === 1
? 1
: Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find(
tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end
).tier;
tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end
).tier;
}
get ancestry() {
@ -400,8 +417,8 @@ export default class DhCharacter extends DhCreature {
get domainCards() {
const domainCards = this.parent.items.filter(x => x.type === 'domainCard');
const loadout = domainCards.filter(x => !x.system.inVault);
const vault = domainCards.filter(x => x.system.inVault);
const loadout = domainCards.filter(x => !x.system.inVault).sort((a, b) => a.sort - b.sort);
const vault = domainCards.filter(x => x.system.inVault).sort((a, b) => a.sort - b.sort);
return {
loadout: loadout,
@ -520,20 +537,20 @@ export default class DhCharacter extends DhCreature {
if (armorSource.type === 'armor') {
armorUpdates[armorSource.parent.id].updates.push({
'_id': armorSource.id,
_id: armorSource.id,
'system.armor.current': armorSource.system.armor.current + usedArmorChange
});
} else {
effectUpdates[armorSource.parent.id].updates.push({
'_id': armorSource.id,
_id: armorSource.id,
'system.changes': armorSource.system.changes.map(change => ({
...change,
value:
change.type === 'armor'
? {
...change.value,
current: armorSource.system.armorChange.value.current + usedArmorChange
}
...change.value,
current: armorSource.system.armorChange.value.current + usedArmorChange
}
: change.value
}))
});
@ -621,21 +638,21 @@ export default class DhCharacter extends DhCreature {
},
...(multiclassFeatures.length
? {
multiclassFeatures: {
title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} - ${this.multiclass.value?.name}`,
type: 'multiclass',
values: multiclassFeatures
}
}
multiclassFeatures: {
title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} - ${this.multiclass.value?.name}`,
type: 'multiclass',
values: multiclassFeatures
}
}
: {}),
...(multiclassSubclassFeatures.length
? {
multiclassSubclassFeatures: {
title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} ${game.i18n.localize('TYPES.Item.subclass')} - ${this.multiclass.subclass?.name}`,
type: 'multiclassSubclass',
values: multiclassSubclassFeatures
}
}
multiclassSubclassFeatures: {
title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} ${game.i18n.localize('TYPES.Item.subclass')} - ${this.multiclass.subclass?.name}`,
type: 'multiclassSubclass',
values: multiclassSubclassFeatures
}
}
: {}),
companionFeatures: {
title: game.i18n.localize('DAGGERHEART.ACTORS.Character.companionFeatures'),
@ -659,8 +676,8 @@ export default class DhCharacter extends DhCreature {
(this.primaryWeapon && this.secondaryWeapon)
? burden.twoHanded.value
: this.primaryWeapon || this.secondaryWeapon
? burden.oneHanded.value
: null;
? burden.oneHanded.value
: null;
}
get deathMoveViable() {
@ -726,8 +743,8 @@ export default class DhCharacter extends DhCreature {
currentLevel === 1
? null
: Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find(
tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end
).tier;
tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end
).tier;
if (game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto) {
for (let levelKey in this.levelData.levelups) {
const level = this.levelData.levelups[levelKey];

View file

@ -102,6 +102,7 @@ export default class DhCompanion extends DhCreature {
parts: {
hitPoints: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
dice: 'd6',
multiplier: 'prof'

View file

@ -1,5 +1,6 @@
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
import { parseInlineParams } from '../../enrichers/parser.mjs';
export function getTierAdjustedAdversary(source, tier) {
const currentTier = source.tier ?? 1;
@ -60,8 +61,8 @@ export function getTierAdjustedAdversary(source, tier) {
const descriptionFormulas = [];
for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
const { value: formula } = parseInlineParams(inner);
if (!formula || !type) return match;
const { value: formula } = parseInlineParams(inner, { first: 'value' });
if (!formula) return match;
try {
const newFormula = calculateAdjustedDamage(formula, 'action', damageMeta)?.formula;

View file

@ -22,12 +22,12 @@ export class DhCompanionLevelup extends foundry.abstract.DataModel {
const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {};
const experiences = initialAchievements.experience
? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = {
name: '',
modifier: initialAchievements.experience.modifier
};
return acc;
}, {})
acc[foundry.utils.randomID()] = {
name: '',
modifier: initialAchievements.experience.modifier
};
return acc;
}, {})
: {};
const currentChoices = pcLevelData.levelups[i]?.selections?.length;
@ -302,9 +302,9 @@ export class DhLevelupLevel extends foundry.abstract.DataModel {
experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {},
domainCards: levelData.achievements?.domainCards
? levelData.achievements.domainCards.reduce((acc, card, index) => {
acc[index] = { ...card };
return acc;
}, {})
acc[index] = { ...card };
return acc;
}, {})
: (achievements.domainCards ?? {}),
proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null
},

View file

@ -13,6 +13,54 @@ export default class DhCountdowns extends foundry.abstract.DataModel {
})
};
}
handleChange() {
const previousCountdowns = foundry.ui.countdowns.previousCountdownData;
const changedCountdowns = Object.entries(this.countdowns).reduce((acc, [key, countdown]) => {
const previousCountdown = previousCountdowns[key];
if (!previousCountdown || (previousCountdown.progress.current !== countdown.progress.current)) {
acc.push(key);
}
return acc;
}, []);
for (const countdownKey of changedCountdowns)
foundry.ui.countdowns.changedCountdownsForAnimation.add(countdownKey);
}
static migrateData(source) {
const migrateOldCountdowns = (data, type) => {
for (const key of Object.keys(data.countdowns)) {
const countdown = data.countdowns[key];
source.countdowns[key] = {
...countdown,
type: type,
ownership: Object.keys(countdown.ownership.players).reduce((acc, key) => {
acc[key] =
countdown.ownership.players[key].type === 1 ? 2 : countdown.ownership.players[key].type;
return acc;
}, {}),
progress: {
...countdown.progress,
type: countdown.progress.type.value
}
};
}
source[type] = null;
};
if (source.narrative) {
migrateOldCountdowns(source.narrative, 'narrative');
}
if (source.encounter) {
migrateOldCountdowns(source.encounter, 'encounter');
}
return super.migrateData(source);
}
}
export class DhCountdown extends foundry.abstract.DataModel {
@ -21,7 +69,8 @@ export class DhCountdown extends foundry.abstract.DataModel {
return {
type: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.countdownBaseTypes,
choices: CONFIG.DH.GENERAL.countdownTypes,
initial: CONFIG.DH.GENERAL.countdownTypes.encounter.id,
label: 'DAGGERHEART.GENERAL.type'
}),
name: new fields.StringField({
@ -77,15 +126,15 @@ export class DhCountdown extends foundry.abstract.DataModel {
static defaultCountdown(type, playerHidden) {
const ownership = playerHidden
? game.users.reduce((acc, user) => {
if (!user.isGM) {
acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
}
return acc;
}, {})
if (!user.isGM) {
acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
}
return acc;
}, {})
: undefined;
return {
type: type ?? CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id,
type: type ?? CONFIG.DH.GENERAL.countdownTypes.encounter.id,
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
img: 'icons/magic/time/hourglass-yellow-green.webp',
ownership: ownership,
@ -102,8 +151,8 @@ export class DhCountdown extends foundry.abstract.DataModel {
value: user.isGM
? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
: this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1
? this.ownership.players[user.id].type
: this.ownership.default,
? this.ownership.players[user.id].type
: this.ownership.default,
isGM: user.isGM
};

View file

@ -105,8 +105,8 @@ export default class BeastformField extends fields.SchemaField {
baseSize === 'custom'
? 'custom'
: (Object.keys(CONFIG.DH.ACTOR.tokenSize).find(
x => CONFIG.DH.ACTOR.tokenSize[x].value === CONFIG.DH.ACTOR.tokenSize[baseSize].value + 1
) ?? baseSize);
x => CONFIG.DH.ACTOR.tokenSize[x].value === CONFIG.DH.ACTOR.tokenSize[baseSize].value + 1
) ?? baseSize);
formData.system.tokenSize = {
...evolvedData.form.system.tokenSize,
size: evolvedSize

View file

@ -116,8 +116,8 @@ export default class CostField extends fields.ArrayField {
c.key === 'fear'
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
: resources[c.key].isReversed
? resources[c.key].max - resources[c.key].value
: resources[c.key].value;
? resources[c.key].max - resources[c.key].value
: resources[c.key].value;
if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step);
return c;
});
@ -149,8 +149,8 @@ export default class CostField extends fields.ArrayField {
!resources[c.key]
? a
: a && resources[c.key].isReversed
? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max
: resources[c.key]?.value >= (c.total ?? c.value),
? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max
: resources[c.key]?.value >= (c.total ?? c.value),
true
);
}

View file

@ -8,8 +8,8 @@ export default class CountdownField extends fields.ArrayField {
...game.system.api.data.countdowns.DhCountdown.defineSchema(),
type: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.countdownBaseTypes,
initial: CONFIG.DH.GENERAL.countdownBaseTypes.encounter.id,
choices: CONFIG.DH.GENERAL.countdownTypes,
initial: CONFIG.DH.GENERAL.countdownTypes.encounter.id,
label: 'DAGGERHEART.GENERAL.type'
}),
name: new fields.StringField({
@ -87,11 +87,11 @@ export default class CountdownField extends fields.ArrayField {
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Countdowns,
countdownSetting.toObject()
),
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown }
});
);
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
},
data,

View file

@ -61,11 +61,11 @@ export default class EffectsField extends fields.ArrayField {
token: messageToken,
conditionImmunities: Object.values(conditionImmunities).some(x => x)
? game.i18n.format('DAGGERHEART.UI.Chat.effectSummary.immunityTo', {
immunities: Object.keys(conditionImmunities)
.filter(x => conditionImmunities[x])
.map(x => game.i18n.localize(conditions[x].name))
.join(', ')
})
immunities: Object.keys(conditionImmunities)
.filter(x => conditionImmunities[x])
.map(x => game.i18n.localize(conditions[x].name))
.join(', ')
})
: null
});

View file

@ -69,11 +69,11 @@ export default class SaveField extends fields.SchemaField {
game.user === actor.owner
? SaveField.rollSave.call(this, actor, event)
: actor.owner.query('reactionRoll', {
actionId: this.uuid,
actorId: actor.uuid,
event,
message
});
actionId: this.uuid,
actorId: actor.uuid,
event,
message
});
const result = await rollSave;
await SaveField.updateSaveMessage.call(this, result, message, target.id);
subResolve();
@ -97,8 +97,8 @@ export default class SaveField extends fields.SchemaField {
const title = actor.isNPC
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.save.trait]?.label)
}),
ability: game.i18n.localize(abilities[this.save.trait]?.label)
}),
rollConfig = {
event,
title,

View file

@ -1,4 +1,4 @@
import { itemAbleRollParse, triggerChatRollFx } from '../../../helpers/utils.mjs';
import { getWorldActor, itemAbleRollParse, triggerChatRollFx } from '../../../helpers/utils.mjs';
import FormulaField from '../formulaField.mjs';
const fields = foundry.data.fields;
@ -42,7 +42,7 @@ export default class DHSummonField extends fields.ArrayField {
const count = roll.total;
if (!roll.isDeterministic) rolls.push(roll);
const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
const actor = await getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
summon.actor = actor.toObject();
@ -62,19 +62,6 @@ export default class DHSummonField extends fields.ArrayField {
DHSummonField.handleSummon(summonData, this.actor);
}
/* Check for any available instances of the actor present in the world if we're missing artwork in the compendium. If none exists, create one. */
static async getWorldActor(baseActor) {
const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`];
if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) {
const worldActorCopy = game.actors.find(x => x.name === baseActor.name);
if (worldActorCopy) return worldActorCopy;
return await game.system.api.documents.DhpActor.create(baseActor.toObject());
}
return baseActor;
}
static async handleSummon(summonData, actionActor) {
await CONFIG.ux.TokenManager.createTokensWithPreview(summonData, { elevation: actionActor.token?.elevation });

View file

@ -116,13 +116,16 @@ class ResourcesField extends fields.TypedObjectField {
}
class GoldField extends fields.SchemaField {
constructor() {
super({
coins: new fields.NumberField({ initial: 0, integer: true }),
handfuls: new fields.NumberField({ initial: 1, integer: true }),
bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true })
});
constructor(options = {}) {
super(
{
coins: new fields.NumberField({ initial: 0, integer: true }),
handfuls: new fields.NumberField({ initial: 1, integer: true }),
bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true })
},
options
);
}
}

View file

@ -39,7 +39,7 @@ export default class FormulaField extends foundry.data.fields.StringField {
let roll = null;
try {
roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, '1'));
} catch (_) {
} catch {
roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, 'd6'));
}
roll.evaluateSync({ strict: false });

8
module/data/item/_types.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
import DHItem from '../../documents/item.mjs';
declare module './base.mjs' {
export default interface BaseDataItem {
parent: DHItem<this>;
actor: DhpActor;
}
}

View file

@ -1,6 +1,6 @@
import BaseDataItem from './base.mjs';
import ItemLinkFields from '../../data/fields/itemLinkFields.mjs';
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs';
export default class DHAncestry extends BaseDataItem {
/** @inheritDoc */
@ -45,6 +45,10 @@ export default class DHAncestry extends BaseDataItem {
/**@inheritdoc */
async getDescriptionData() {
// Preload all ancestry features for acquisition from the cache
// todo: make feature acquisition async and replace feature helpers for methods
await fromUuids(this._source.features.map(f => f.item));
const baseDescription = this.description;
const features = await getFeaturesHTMLData(this.features);

View file

@ -52,6 +52,10 @@ export default class DHArmor extends AttachableItem {
);
}
get itemFeatures() {
return this.armorFeatures;
}
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
@ -169,8 +173,4 @@ export default class DHArmor extends AttachableItem {
const labels = [`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armor.max}`];
return labels;
}
get itemFeatures() {
return this.armorFeatures;
}
}

View file

@ -208,8 +208,8 @@ export default class DHBeastform extends BaseDataItem {
const autoTokenSize =
this.tokenSize.size !== 'custom'
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes[
this.tokenSize.size
]
this.tokenSize.size
]
: null;
const width = autoTokenSize ?? this.tokenSize.width;
const height = autoTokenSize ?? this.tokenSize.height;

View file

@ -70,14 +70,14 @@ export default class DHClass extends BaseDataItem {
}
async fetchSubclasses() {
const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u);
const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass));
const sourceUuid = this.parent.sourceUuid;
const subclasses = game.items.filter(x => x.type === 'subclass' && x.system.linkedClass === sourceUuid);
for (const pack of game.packs) {
const packIds = [];
const indexes = await pack.getIndex({ fields: ['system.linkedClass'] });
for (const index of indexes) {
if (index.type !== 'subclass') continue;
if (!uuids.includes(index.system?.linkedClass)) continue;
if (index.system?.linkedClass !== sourceUuid) continue;
if (subclasses.find(x => x.uuid === index.uuid)) continue;
packIds.push(index._id);
}

View file

@ -1,4 +1,4 @@
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import BaseDataItem from './base.mjs';
@ -27,6 +27,10 @@ export default class DHCommunity extends BaseDataItem {
/**@inheritdoc */
async getDescriptionData() {
// Preload all community features for acquisition from the cache
// todo: make feature acquisition async and replace feature helpers for methods
await fromUuids(this._source.features);
const baseDescription = this.description;
const features = await getFeaturesHTMLData(this.features);

View file

@ -70,9 +70,7 @@ export default class DHSubclass extends BaseDataItem {
return false;
}
const match = [multiclass, actorClass].find(
c => c && (c._stats.compendiumSource ?? c.uuid) === this.linkedClass
);
const match = [multiclass, actorClass].find(c => c && c.sourceUuid === this.linkedClass);
if (!match) {
const key = multiclass ? 'subclassNotInMulticlass' : 'subclassNotInClass';
ui.notifications.warn(`DAGGERHEART.UI.Notifications.${key}`, { localize: true });
@ -91,7 +89,7 @@ export default class DHSubclass extends BaseDataItem {
? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label)
: null;
// Preload all class features for acquisition from the cache
// Preload all subclass features for acquisition from the cache
// todo: make feature acquisition async and replace feature helpers for methods
await fromUuids(this._source.features.map(f => f.item));

View file

@ -113,6 +113,10 @@ export default class DHWeapon extends AttachableItem {
);
}
get itemFeatures() {
return this.weaponFeatures;
}
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
@ -269,8 +273,4 @@ export default class DHWeapon extends AttachableItem {
return labels;
}
get itemFeatures() {
return this.weaponFeatures;
}
}

View file

@ -19,12 +19,12 @@ export class DhLevelup extends foundry.abstract.DataModel {
const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {};
const experiences = initialAchievements.experience
? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = {
name: '',
modifier: initialAchievements.experience.modifier
};
return acc;
}, {})
acc[foundry.utils.randomID()] = {
name: '',
modifier: initialAchievements.experience.modifier
};
return acc;
}, {})
: {};
const domainCards = [...Array(tier.domainCardByLevel).keys()].reduce((acc, _) => {
@ -298,9 +298,9 @@ export class DhLevelupLevel extends foundry.abstract.DataModel {
experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {},
domainCards: levelData.achievements?.domainCards
? levelData.achievements.domainCards.reduce((acc, card, index) => {
acc[index] = { ...card };
return acc;
}, {})
acc[index] = { ...card };
return acc;
}, {})
: (achievements.domainCards ?? {}),
proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null
},

View file

@ -149,12 +149,15 @@ export default class RegisteredTriggers extends Map {
const result = await command(...args);
if (result?.updates?.length) updates.push(...result.updates);
} catch (_) {
} catch {
const item = await foundry.utils.fromUuid(itemUuid);
const triggerName = game.i18n.localize(triggerData.label);
ui.notifications.error(
console.error(
game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', {
trigger: triggerName,
actor: currentActor?.name
actor: currentActor?.name,
item: item?.name ?? '<Missing Item>',
itemUuid: item?.uuid ?? '<Missing UUID>'
})
);
}

View file

@ -2,7 +2,7 @@ import { defaultRestOptions } from '../../config/generalConfig.mjs';
import { resetAndRerenderActors } from '../../helpers/utils.mjs';
import { ActionsField } from '../fields/actionField.mjs';
const currencyField = (initial, label, icon) =>
const currencyField = (initial, label, icon, initialAmount = 0) =>
new foundry.data.fields.SchemaField({
enabled: new foundry.data.fields.BooleanField({ required: true, initial: true }),
label: new foundry.data.fields.StringField({
@ -10,7 +10,14 @@ const currencyField = (initial, label, icon) =>
initial,
label
}),
icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon })
icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon }),
initialAmount: new foundry.data.fields.NumberField({
required: true,
integer: true,
min: 0,
initial: initialAmount,
label: 'DAGGERHEART.SETTINGS.Homebrew.currency.initialAmount'
})
});
const restMoveField = () =>
@ -108,7 +115,8 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
handfuls: currencyField(
'Handfuls',
'DAGGERHEART.SETTINGS.Homebrew.currency.handfulName',
'fa-solid fa-coins'
'fa-solid fa-coins',
1
),
bags: currencyField('Bags', 'DAGGERHEART.SETTINGS.Homebrew.currency.bagName', 'fa-solid fa-sack'),
chests: currencyField(
@ -193,6 +201,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
for (const type of ['coins', 'handfuls', 'bags', 'chests']) {
const initial = this.schema.fields.currency.fields[type].getInitialValue();
source.currency[type] = foundry.utils.mergeObject(initial, source.currency[type], { inplace: false });
source.currency[type].initialAmount ??= 0;
}
return source;
}