Merged with main

This commit is contained in:
WBHarry 2026-04-22 15:32:10 +02:00
commit 4e555dd314
174 changed files with 3707 additions and 1217 deletions

View file

@ -15,3 +15,4 @@ export * as chatMessages from './chat-message/_modules.mjs';
export * as fields from './fields/_module.mjs';
export * as items from './item/_module.mjs';
export * as scenes from './scene/_module.mjs';
export * as regionBehaviors from './regionBehavior/_module.mjs';

View file

@ -13,7 +13,7 @@ export default class DHAttackAction extends DHDamageAction {
if (!!this.item?.system?.attack) {
if (this.damage.includeBase) {
const baseDamage = this.getParentDamage();
this.damage.parts.unshift(new DHDamageData(baseDamage));
this.damage.parts.hitPoints = new DHDamageData(baseDamage);
}
if (this.roll.useDefault) {
this.roll.trait = this.item.system.attack.roll.trait;
@ -51,7 +51,7 @@ export default class DHAttackAction extends DHDamageAction {
async use(event, options) {
const result = await super.use(event, options);
if (result?.message?.system.action.roll?.type === 'attack') {
if (result?.message?.system.action?.roll?.type === 'attack') {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id);
}

View file

@ -15,7 +15,7 @@ const fields = foundry.data.fields;
*/
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
static extraSchemas = ['cost', 'uses', 'range'];
static extraSchemas = ['areas', 'cost', 'uses', 'range'];
/** @inheritDoc */
static defineSchema() {
@ -110,6 +110,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return this._id;
}
/** Returns true if the current user is the owner of the containing item */
get isOwner() {
return this.item?.isOwner ?? true;
}
/**
* Return Item the action is attached too.
*/
@ -143,6 +148,12 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
: null;
}
/** Returns true if the action is usable */
get usable() {
const actor = this.actor;
return this.isOwner && actor?.type === 'character';
}
static getRollType(parent) {
return 'trait';
}

View file

@ -93,6 +93,12 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
max: new fields.NumberField({ integer: true, label: 'DAGGERHEART.GENERAL.max' })
},
{ nullable: true, initial: null }
),
targetDispositions: new fields.SetField(
new fields.NumberField({
choices: CONFIG.DH.GENERAL.simpleDispositions
}),
{ label: 'DAGGERHEART.ACTIVEEFFECT.Config.targetDispositions' }
)
};
}
@ -131,13 +137,14 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
return armorChange.getArmorData();
}
static getDefaultObject() {
static getDefaultObject(options = { transfer: true }) {
return {
name: 'New Effect',
id: foundry.utils.randomID(),
disabled: false,
img: 'icons/magic/life/heart-cross-blue.webp',
description: '',
transfer: options.transfer,
statuses: [],
changes: [],
system: {

View file

@ -20,7 +20,7 @@ export default class DhCharacter extends DhCreature {
settingSheet: DHCharacterSettings,
isNPC: false,
hasInventory: true,
quantifiable: ["loot", "consumable"]
quantifiable: ['loot', 'consumable']
});
}
@ -470,7 +470,7 @@ export default class DhCharacter extends DhCreature {
/* All items are valid on characters */
isItemValid() {
return true;
return true;
}
/** @inheritDoc */

View file

@ -9,7 +9,7 @@ export default class DhParty extends BaseDataActor {
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
hasInventory: true,
quantifiable: ["weapon", "armor", "loot", "consumable"]
quantifiable: ['weapon', 'armor', 'loot', 'consumable']
});
}

View file

@ -7,26 +7,31 @@ export default class DHAbilityUse extends foundry.abstract.TypeDataModel {
img: new fields.StringField({}),
name: new fields.StringField({}),
description: new fields.StringField({}),
actions: new fields.ArrayField(
new fields.ObjectField({
name: new fields.StringField({}),
damage: new fields.SchemaField({
type: new fields.StringField({}),
value: new fields.StringField({})
}),
healing: new fields.SchemaField({
type: new fields.StringField({}),
value: new fields.StringField({})
}),
cost: new fields.SchemaField({
type: new fields.StringField({}),
value: new fields.NumberField({})
}),
target: new fields.SchemaField({
type: new fields.StringField({ nullable: true })
})
})
)
source: new fields.SchemaField({
actor: new fields.StringField(),
item: new fields.StringField(),
action: new fields.StringField()
})
};
}
get actionActor() {
if (!this.source.actor) return null;
return fromUuidSync(this.source.actor);
}
get actionItem() {
const actionActor = this.actionActor;
if (!actionActor || !this.source.item) return null;
const item = actionActor.items.get(this.source.item);
return item ? item.system.actions?.find(a => a.id === this.source.action) : null;
}
get action() {
const { actionItem: itemAction } = this;
if (!this.source.action) return null;
if (itemAction) return itemAction;
return null;
}
}

View file

@ -1,3 +1,4 @@
export { default as AreasField } from './areasField.mjs';
export { default as CostField } from './costField.mjs';
export { default as CountdownField } from './countdownField.mjs';
export { default as UsesField } from './usesField.mjs';

View file

@ -0,0 +1,40 @@
const fields = foundry.data.fields;
export default class AreasField extends fields.ArrayField {
/**
* Action Workflow order
*/
static order = 150;
/** @inheritDoc */
constructor(options = {}, context = {}) {
const element = new fields.SchemaField({
name: new fields.StringField({
nullable: false,
initial: 'Area',
label: 'DAGGERHEART.GENERAL.name'
}),
type: new fields.StringField({
nullable: false,
choices: CONFIG.DH.ACTIONS.areaTypes,
initial: CONFIG.DH.ACTIONS.areaTypes.placed.id,
label: 'DAGGERHEART.GENERAL.type'
}),
shape: new fields.StringField({
nullable: false,
choices: CONFIG.DH.GENERAL.templateTypes,
initial: CONFIG.DH.GENERAL.templateTypes.circle.id,
label: 'DAGGERHEART.ACTIONS.Config.area.shape'
}),
/* Could be opened up to allow numbers to be input aswell. Probably best handled via an autocomplete in that case to allow the select options but also free text */
size: new fields.StringField({
nullable: false,
choices: CONFIG.DH.GENERAL.range,
initial: CONFIG.DH.GENERAL.range.veryClose.id,
label: 'DAGGERHEART.ACTIONS.Config.area.size'
}),
effects: new fields.ArrayField(new fields.DocumentIdField())
});
super(element, options, context);
}
}

View file

@ -40,9 +40,7 @@ export default class DHSummonField extends fields.ArrayField {
const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item));
await roll.evaluate();
const count = roll.total;
if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active)
rolls.push(roll);
if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active) rolls.push(roll);
const actor = await DHSummonField.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. */

View file

@ -281,8 +281,14 @@ export function ActionMixin(Base) {
name: this.name,
img: this.baseAction ? this.parent.parent.img : this.img,
tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'],
areas: this.areas,
summon: this.summon
},
source: {
actor: this.actor.uuid,
item: this.item.id,
action: this.id
},
itemOrigin: this.item,
description: this.description || (this.item instanceof Item ? this.item.system.description : '')
};

View file

@ -108,6 +108,10 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
}
get actionsList() {
// No actions on non-characters
if (this.metadata.isInventoryItem && this.actor && this.actor.type !== 'character') {
return [];
}
return this.actions;
}

View file

@ -99,7 +99,9 @@ export default class DHWeapon extends AttachableItem {
/* -------------------------------------------- */
get actionsList() {
return [this.attack, ...this.actions];
// No actions on non-characters
if (this.actor && this.actor.type !== 'character') return [];
return [this.attack, ...super.actionsList];
}
get customActions() {

View file

@ -0,0 +1 @@
export { default as applyActiveEffect } from './applyActiveEffect.mjs';

View file

@ -0,0 +1,40 @@
export default class DhApplyActiveEffect extends CONFIG.RegionBehavior.dataModels.applyActiveEffect {
static async #getApplicableEffects(token) {
const effects = await Promise.all(this.effects.map(foundry.utils.fromUuid));
return effects.filter(
effect => !effect.system.targetDispositions.size || effect.system.targetDispositions.has(token.disposition)
);
}
static async #onTokenEnter(event) {
if (!event.user.isSelf) return;
const { token, movement } = event.data;
const actor = token.actor;
if (!actor) return;
const resumeMovement = movement ? token.pauseMovement() : undefined;
const effects = await DhApplyActiveEffect.#getApplicableEffects.bind(this)(event.data.token);
const toCreate = [];
for (const effect of effects) {
const data = effect.toObject();
delete data._id;
if (effect.compendium) {
data._stats.duplicateSource = null;
data._stats.compendiumSource = effect.uuid;
} else {
data._stats.duplicateSource = effect.uuid;
data._stats.compendiumSource = null;
}
data._stats.exportSource = null;
data.origin = this.parent.uuid;
toCreate.push(data);
}
if (toCreate.length) await actor.createEmbeddedDocuments('ActiveEffect', toCreate);
await resumeMovement?.();
}
/** @override */
static events = {
...CONFIG.RegionBehavior.dataModels.applyActiveEffect.events,
[CONST.REGION_EVENTS.TOKEN_ENTER]: this.#onTokenEnter
};
}