Merge branch 'main' into feature/313-preset-measured-templates

This commit is contained in:
Chris Ryan 2025-11-18 15:28:55 +10:00
commit 7d2de14709
81 changed files with 611 additions and 446 deletions

View file

@ -1,6 +1,7 @@
import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
import { ActionMixin } from '../fields/actionField.mjs';
import { originItemField } from '../chat-message/actorRoll.mjs';
const fields = foundry.data.fields;
@ -25,6 +26,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
description: new fields.HTMLField(),
img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }),
chatDisplay: new fields.BooleanField({ initial: true, label: 'DAGGERHEART.ACTIONS.Config.displayInChat' }),
originItem: originItemField(),
actionType: new fields.StringField({
choices: CONFIG.DH.ITEM.actionTypes,
initial: 'action',
@ -215,6 +217,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`,
source: {
item: this.item._id,
originItem: this.originItem,
action: this._id,
actor: this.actor.uuid
},

View file

@ -6,10 +6,12 @@ export default class BeastformEffect extends BaseEffect {
const fields = foundry.data.fields;
return {
characterTokenData: new fields.SchemaField({
usesDynamicToken: new fields.BooleanField({ initial: false }),
tokenImg: new fields.FilePathField({
categories: ['IMAGE'],
base64: false,
nullable: true
nullable: true,
wildcard: true
}),
tokenRingImg: new fields.FilePathField({
initial: 'icons/svg/mystery-man.svg',
@ -38,20 +40,39 @@ export default class BeastformEffect extends BaseEffect {
async _preDelete() {
if (this.parent.parent.type === 'character') {
const update = {
const baseUpdate = {
height: this.characterTokenData.tokenSize.height,
width: this.characterTokenData.tokenSize.width,
width: this.characterTokenData.tokenSize.width
};
const update = {
...baseUpdate,
texture: {
src: this.characterTokenData.tokenImg
},
ring: {
enabled: this.characterTokenData.usesDynamicToken,
subject: {
texture: this.characterTokenData.tokenRingImg
}
}
};
await updateActorTokens(this.parent.parent, update);
const updateToken = token => ({
...baseUpdate,
'texture': {
enabled: this.characterTokenData.usesDynamicToken,
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg
},
'ring': {
subject: {
texture:
token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg
}
},
'flags.daggerheart': { '-=beastformTokenImg': null, '-=beastformSubjectTexture': null }
});
await updateActorTokens(this.parent.parent, update, updateToken);
await this.parent.parent.deleteEmbeddedDocuments('Item', this.featureIds);
await this.parent.parent.deleteEmbeddedDocuments('ActiveEffect', this.effectIds);

View file

@ -675,6 +675,8 @@ export default class DhCharacter extends BaseDataActor {
}
_getTags() {
return [this.class.value?.name, this.class.subclass?.name, this.community?.name, this.ancestry?.name].filter((t) => !!t);
return [this.class.value?.name, this.class.subclass?.name, this.community?.name, this.ancestry?.name].filter(
t => !!t
);
}
}

View file

@ -7,7 +7,7 @@ export default class DhParty extends BaseDataActor {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }),
partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }, { prune: true }),
notes: new fields.HTMLField(),
gold: new fields.SchemaField({
coins: new fields.NumberField({ initial: 0, integer: true }),
@ -27,7 +27,6 @@ export default class DhParty extends BaseDataActor {
prepareBaseData() {
super.prepareBaseData();
this.partyMembers = this.partyMembers.filter(p => !!p);
// Register this party to all members
if (game.actors.get(this.parent.id) === this.parent) {
@ -42,7 +41,7 @@ export default class DhParty extends BaseDataActor {
// Clear this party from all members that aren't deleted
for (const member of this.partyMembers) {
member.parties?.delete(this.parent);
member?.parties?.delete(this.parent);
}
}
}

View file

@ -17,6 +17,16 @@ const targetsField = () =>
})
);
export const originItemField = () =>
new fields.SchemaField({
type: new fields.StringField({
choices: CONFIG.DH.ITEM.originItemType,
initial: CONFIG.DH.ITEM.originItemType.itemCollection
}),
itemPath: new fields.StringField(),
actionIndex: new fields.StringField()
});
export default class DHActorRoll extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
@ -35,6 +45,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
source: new fields.SchemaField({
actor: new fields.StringField(),
item: new fields.StringField(),
originItem: originItemField(),
action: new fields.StringField()
}),
damage: new fields.ObjectField(),
@ -51,14 +62,23 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
get actionItem() {
const actionActor = this.actionActor;
if (!actionActor || !this.source.item) return null;
return actionActor.items.get(this.source.item);
switch (this.source.originItem.type) {
case CONFIG.DH.ITEM.originItemType.restMove:
const restMoves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves;
return Array.from(foundry.utils.getProperty(restMoves, `${this.source.originItem.itemPath}`).actions)[
this.source.originItem.actionIndex
];
default:
const item = actionActor.items.get(this.source.item);
return item ? item.system.actionsList?.find(a => a.id === this.source.action) : null;
}
}
get action() {
const actionActor = this.actionActor,
actionItem = this.actionItem;
const { actionActor, actionItem: itemAction } = this;
if (!this.source.action) return null;
if (actionItem) return actionItem.system.actionsList?.find(a => a.id === this.source.action);
if (itemAction) return itemAction;
else if (actionActor?.system.attack?._id === this.source.action) return actionActor.system.attack;
return null;
}

View file

@ -41,9 +41,6 @@ export default class BeastformField extends fields.SchemaField {
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
static async execute(config) {
// Should not be useful anymore here
await BeastformField.handleActiveTransformations.call(this);
const { selected, evolved, hybrid } = await BeastformDialog.configure(config, this.item);
if (!selected) return false;

View file

@ -98,7 +98,9 @@ export default class DamageField extends fields.SchemaField {
});
}
const token = game.scenes.find(x => x.active).tokens.find(x => x.id === target.id);
const token = target.id
? game.scenes.find(x => x.active).tokens.find(x => x.id === target.id)
: actor.prototypeToken;
if (config.hasHealing)
damagePromises.push(
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates }))

View file

@ -24,7 +24,7 @@ export default class TargetField extends fields.SchemaField {
if (!this.target?.type) return (config.targets = []);
config.hasTarget = true;
let targets;
// If the Action is configured as self-targeted, set targets as the owner.
// If the Action is configured as self-targeted, set targets as the owner. Probably better way than to fallback to getDependentTokens
if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id)
targets = [this.actor.token ?? this.actor.prototypeToken];
else {
@ -72,17 +72,17 @@ export default class TargetField extends fields.SchemaField {
/**
* Format actor to useful datas for Action roll workflow.
* @param {*} actor Actor object to format.
* @param {*} token Token object to format.
* @returns {*} Formatted Actor.
*/
static formatTarget(actor) {
static formatTarget(token) {
return {
id: actor.id,
actorId: actor.actor.uuid,
name: actor.actor.name,
img: actor.actor.img,
difficulty: actor.actor.system.difficulty,
evasion: actor.actor.system.evasion,
id: token.id,
actorId: token.actor.uuid,
name: token.actor.name,
img: token.actor.img,
difficulty: token.actor.system.difficulty,
evasion: token.actor.system.evasion,
saved: {
value: null,
success: null

View file

@ -15,6 +15,9 @@ export default class ForeignDocumentUUIDArrayField extends foundry.data.fields.A
/** @inheritdoc */
initialize(value, model, options = {}) {
const v = super.initialize(value, model, options);
return () => v.map(entry => (typeof entry === 'function' ? entry() : entry));
return () => {
const data = v.map(entry => (typeof entry === 'function' ? entry() : entry));
return this.options.prune ? data.filter(d => !!d) : data;
};
}
}

View file

@ -160,7 +160,6 @@ export default class DHBeastform extends BaseDataItem {
this.parent.effects.filter(x => x.type !== 'beastform').map(x => x.toObject())
);
const tokenImages = await this.parent.parent.getTokenImages();
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
await beastformEffect.updateSource({
changes: [
@ -175,7 +174,8 @@ export default class DHBeastform extends BaseDataItem {
],
system: {
characterTokenData: {
tokenImg: tokenImages[0],
usesDynamicToken: this.parent.parent.prototypeToken.ring.enabled,
tokenImg: this.parent.parent.prototypeToken.texture.src,
tokenRingImg: this.parent.parent.prototypeToken.ring.subject.texture,
tokenSize: {
height: this.parent.parent.prototypeToken.height,
@ -190,7 +190,7 @@ export default class DHBeastform extends BaseDataItem {
await this.parent.parent.createEmbeddedDocuments('ActiveEffect', [beastformEffect.toObject()]);
await updateActorTokens(this.parent.parent, {
const prototypeTokenUpdate = {
height: this.tokenSize.height,
width: this.tokenSize.width,
texture: {
@ -201,22 +201,20 @@ export default class DHBeastform extends BaseDataItem {
texture: this.tokenRingImg
}
}
};
const tokenUpdate = token => ({
...prototypeTokenUpdate,
flags: {
daggerheart: {
beastformTokenImg: token.texture.src,
beastformSubjectTexture: token.ring.subject.texture
}
}
});
await updateActorTokens(this.parent.parent, prototypeTokenUpdate, tokenUpdate);
return false;
}
_onCreate(_data, _options, userId) {
if (userId !== game.user.id) return;
if (!this.parent.effects.find(x => x.type === 'beastform')) {
this.parent.createEmbeddedDocuments('ActiveEffect', [
{
type: 'beastform',
name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'),
img: 'icons/creatures/abilities/paw-print-pair-purple.webp'
}
]);
}
}
}

View file

@ -1,6 +1,16 @@
import { defaultRestOptions } from '../../config/generalConfig.mjs';
import { ActionsField } from '../fields/actionField.mjs';
const currencyField = (initial, label) =>
new foundry.data.fields.SchemaField({
enabled: new foundry.data.fields.BooleanField({ required: true, initial: true }),
label: new foundry.data.fields.StringField({
required: true,
initial,
label
})
});
export default class DhHomebrew extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
@ -30,36 +40,15 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
initial: () => [2, 1, 1, 0, 0, -1]
}),
currency: new fields.SchemaField({
enabled: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Homebrew.currency.enabled'
}),
title: new fields.StringField({
required: true,
initial: 'Gold',
label: 'DAGGERHEART.SETTINGS.Homebrew.currency.currencyName'
}),
coins: new fields.StringField({
required: true,
initial: 'Coins',
label: 'DAGGERHEART.SETTINGS.Homebrew.currency.coinName'
}),
handfuls: new fields.StringField({
required: true,
initial: 'Handfuls',
label: 'DAGGERHEART.SETTINGS.Homebrew.currency.handfulName'
}),
bags: new fields.StringField({
required: true,
initial: 'Bags',
label: 'DAGGERHEART.SETTINGS.Homebrew.currency.bagName'
}),
chests: new fields.StringField({
required: true,
initial: 'Chests',
label: 'DAGGERHEART.SETTINGS.Homebrew.currency.chestName'
})
coins: currencyField('Coins', 'DAGGERHEART.SETTINGS.Homebrew.currency.coinName'),
handfuls: currencyField('Handfuls', 'DAGGERHEART.SETTINGS.Homebrew.currency.handfulName'),
bags: currencyField('Bags', 'DAGGERHEART.SETTINGS.Homebrew.currency.bagName'),
chests: currencyField('Chests', 'DAGGERHEART.SETTINGS.Homebrew.currency.chestName')
}),
restMoves: new fields.SchemaField({
longRest: new fields.SchemaField({
@ -146,4 +135,26 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
})
};
}
/** @inheritDoc */
_initializeSource(source, options = {}) {
source = super._initializeSource(source, options);
source.currency.coins = {
enabled: source.currency.coins.enabled ?? true,
label: source.currency.coins.label || source.currency.coins
};
source.currency.handfuls = {
enabled: source.currency.handfuls.enabled ?? true,
label: source.currency.handfuls.label || source.currency.handfuls
};
source.currency.bags = {
enabled: source.currency.bags.enabled ?? true,
label: source.currency.bags.label || source.currency.bags
};
source.currency.chests = {
enabled: source.currency.chests.enabled ?? true,
label: source.currency.chests.label || source.currency.chests
};
return source;
}
}