Merge branch 'main' into feature/death-moves

This commit is contained in:
Chris Ryan 2025-12-23 11:17:37 +10:00
commit 9a3355175b
229 changed files with 2452 additions and 893 deletions

View file

@ -206,6 +206,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
// Execute the Action Worflow in order based of schema fields
await this.executeWorkflow(config);
await config.resourceUpdates.updateResources();
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
@ -239,8 +240,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
isDirect: !!this.damage?.direct,
selectedRollMode: game.settings.get('core', 'rollMode'),
data: this.getRollData(),
evaluate: this.hasRoll
evaluate: this.hasRoll,
resourceUpdates: new ResourceUpdateMap(this.actor)
};
DHBaseAction.applyKeybindings(config);
return config;
}
@ -322,11 +325,46 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @returns {string[]} An array of localized tag strings.
*/
_getTags() {
const tags = [
game.i18n.localize(`DAGGERHEART.ACTIONS.TYPES.${this.type}.name`),
game.i18n.localize(`DAGGERHEART.CONFIG.ActionType.${this.actionType}`)
];
const tags = [game.i18n.localize(`DAGGERHEART.ACTIONS.TYPES.${this.type}.name`)];
return tags;
}
}
export class ResourceUpdateMap extends Map {
#actor;
constructor(actor) {
super();
this.#actor = actor;
}
addResources(resources) {
for (const resource of resources) {
if (!resource.key) continue;
const existing = this.get(resource.key);
if (existing) {
this.set(resource.key, {
...existing,
value: existing.value + (resource.value ?? 0),
total: existing.total + (resource.total ?? 0)
});
} else {
this.set(resource.key, resource);
}
}
}
#getResources() {
return Array.from(this.values());
}
async updateResources() {
if (this.#actor) {
const target = this.#actor.system.partner ?? this.#actor;
await target.modifyResource(this.#getResources());
}
}
}

View file

@ -65,20 +65,30 @@ export default class BeastformEffect extends BaseEffect {
}
};
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 }
});
const updateToken = token => {
const { x, y } = game.system.api.documents.DhToken.getSnappedPositionInSquareGrid(
token.object.scene.grid,
{ x: token.x, y: token.y, elevation: token.elevation },
baseUpdate.width,
baseUpdate.height
);
return {
...baseUpdate,
x,
y,
'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);

View file

@ -11,7 +11,8 @@ export default class DhpAdversary extends BaseDataActor {
label: 'TYPES.Actor.adversary',
type: 'adversary',
settingSheet: DHAdversarySettings,
hasAttribution: true
hasAttribution: true,
usesSize: true
});
}
@ -142,7 +143,7 @@ export default class DhpAdversary extends BaseDataActor {
}
isItemValid(source) {
return source.type === "feature";
return source.type === 'feature';
}
async _preUpdate(changes, options, user) {

View file

@ -1,4 +1,5 @@
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
import DHItem from '../../documents/item.mjs';
import { getScrollTextData } from '../../helpers/utils.mjs';
const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
@ -41,7 +42,8 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
settingSheet: null,
hasResistances: true,
hasAttribution: false,
hasLimitedView: true
hasLimitedView: true,
usesSize: false
};
}
@ -76,6 +78,13 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
'DAGGERHEART.GENERAL.DamageResistance.magicalReduction'
)
});
if (this.metadata.usesSize)
schema.size = new fields.StringField({
required: true,
nullable: false,
choices: CONFIG.DH.ACTOR.tokenSize,
initial: CONFIG.DH.ACTOR.tokenSize.custom.id
});
return schema;
}
@ -106,6 +115,17 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
return data;
}
/**
* Checks if an item is available for use, such as multiclass features being disabled
* on a character.
*
* @param {DHItem} item The item being checked for availability
* @return {boolean} whether the item is available
*/
isItemAvailable(item) {
return true;
}
async _preDelete() {
/* Clear all partyMembers from tagTeam setting.*/
/* Revisit this when tagTeam is improved for many parties */

View file

@ -435,6 +435,34 @@ export default class DhCharacter extends BaseDataActor {
return attack;
}
/** @inheritDoc */
isItemAvailable(item) {
if (!super.isItemAvailable(this)) return false;
/**
* Preventing subclass features from being available if the chacaracter does not
* have the right subclass advancement
*/
if (item.system.originItemType !== CONFIG.DH.ITEM.featureTypes.subclass.id) {
return true;
}
if (!this.class.subclass) return false;
const prop = item.system.multiclassOrigin ? 'multiclass' : 'class';
const subclassState = this[prop].subclass?.system?.featureState;
if (!subclassState) return false;
if (
item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.foundation ||
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.specialization &&
subclassState >= 2) ||
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
) {
return true;
} else {
return false;
}
}
get sheetLists() {
const ancestryFeatures = [],
communityFeatures = [],
@ -443,7 +471,7 @@ export default class DhCharacter extends BaseDataActor {
companionFeatures = [],
features = [];
for (let item of this.parent.items) {
for (let item of this.parent.items.filter(x => this.isItemAvailable(x))) {
if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.ancestry.id) {
ancestryFeatures.push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
@ -451,20 +479,7 @@ export default class DhCharacter extends BaseDataActor {
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
classFeatures.push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
if (this.class.subclass) {
const prop = item.system.multiclassOrigin ? 'multiclass' : 'class';
const subclassState = this[prop].subclass?.system?.featureState;
if (!subclassState) continue;
if (
item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.foundation ||
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.specialization &&
subclassState >= 2) ||
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
) {
subclassFeatures.push(item);
}
}
subclassFeatures.push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
companionFeatures.push(item);
} else if (item.type === 'feature' && !item.system.type) {

View file

@ -26,7 +26,7 @@ export default class DhParty extends BaseDataActor {
/* -------------------------------------------- */
isItemValid(source) {
return ["weapon", "armor", "consumable", "loot"].includes(source.type);
return ['weapon', 'armor', 'consumable', 'loot'].includes(source.type);
}
prepareBaseData() {

View file

@ -92,6 +92,18 @@ export default class BeastformField extends fields.SchemaField {
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
const baseSize = evolvedData.form.system.tokenSize.size;
const evolvedSize =
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);
formData.system.tokenSize = {
...evolvedData.form.system.tokenSize,
size: evolvedSize
};
}
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {

View file

@ -75,7 +75,7 @@ export default class CostField extends fields.ArrayField {
}
}, []);
await actor.modifyResource(resources);
config.resourceUpdates.addResources(resources);
}
/**

View file

@ -79,7 +79,7 @@ export default class TargetField extends fields.SchemaField {
return {
id: token.id,
actorId: token.actor.uuid,
name: token.actor.name,
name: token.name,
img: token.actor.img,
difficulty: token.actor.system.difficulty,
evasion: token.actor.system.evasion,

View file

@ -141,6 +141,12 @@ export function ActionMixin(Base) {
return this.documentName;
}
//Getter for icons
get typeIcon() {
const config = CONFIG.DH.ACTIONS.actionTypes[this.type];
return config?.icon || 'fa-question'; // Fallback icon just in case
}
get relativeUUID() {
return `.Item.${this.item.id}.Action.${this.id}`;
}
@ -256,7 +262,7 @@ export function ActionMixin(Base) {
async toChat(origin) {
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: game.i18n.localize('DAGGERHEART.CONFIG.ActionType.action'),
title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'),
origin: origin,
action: {
name: this.name,

View file

@ -43,6 +43,12 @@ export default class DHBeastform extends BaseDataItem {
base64: false
}),
tokenSize: new fields.SchemaField({
size: new fields.StringField({
required: true,
nullable: false,
choices: CONFIG.DH.ACTOR.tokenSize,
initial: CONFIG.DH.ACTOR.tokenSize.custom.id
}),
height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true })
}),
@ -190,9 +196,18 @@ export default class DHBeastform extends BaseDataItem {
await this.parent.parent.createEmbeddedDocuments('ActiveEffect', [beastformEffect.toObject()]);
const autoTokenSize =
this.tokenSize.size !== 'custom'
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes[
this.tokenSize.size
]
: null;
const width = autoTokenSize ?? this.tokenSize.width;
const height = autoTokenSize ?? this.tokenSize.height;
const prototypeTokenUpdate = {
height: this.tokenSize.height,
width: this.tokenSize.width,
height,
width,
texture: {
src: this.tokenImg
},
@ -202,16 +217,25 @@ export default class DHBeastform extends BaseDataItem {
}
}
};
const tokenUpdate = token => ({
...prototypeTokenUpdate,
flags: {
daggerheart: {
beastformTokenImg: token.texture.src,
beastformSubjectTexture: token.ring.subject.texture
const tokenUpdate = token => {
const { x, y } = game.system.api.documents.DhToken.getSnappedPositionInSquareGrid(
token.object.scene.grid,
{ x: token.x, y: token.y, elevation: token.elevation },
width ?? token.width,
height ?? token.height
);
return {
...prototypeTokenUpdate,
x,
y,
flags: {
daggerheart: {
beastformTokenImg: token.texture.src,
beastformSubjectTexture: token.ring.subject.texture
}
}
}
});
};
};
await updateActorTokens(this.parent.parent, prototypeTokenUpdate, tokenUpdate);

View file

@ -30,7 +30,13 @@ export default class DHFeature extends BaseDataItem {
initial: null
}),
multiclassOrigin: new fields.BooleanField({ initial: false }),
identifier: new fields.StringField()
identifier: new fields.StringField(),
featureForm: new fields.StringField({
required: true,
initial: 'passive',
choices: CONFIG.DH.ITEM.featureForm,
label: 'DAGGERHEART.CONFIG.FeatureForm.label'
})
};
}
}

View file

@ -40,6 +40,38 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
traitArray: new fields.ArrayField(new fields.NumberField({ required: true, integer: true }), {
initial: () => [2, 1, 1, 0, 0, -1]
}),
tokenSizes: new fields.SchemaField({
tiny: new fields.NumberField({
integer: false,
initial: 0.5,
label: 'DAGGERHEART.CONFIG.TokenSize.tiny'
}),
small: new fields.NumberField({
integer: false,
initial: 0.8,
label: 'DAGGERHEART.CONFIG.TokenSize.small'
}),
medium: new fields.NumberField({
integer: false,
initial: 1,
label: 'DAGGERHEART.CONFIG.TokenSize.medium'
}),
large: new fields.NumberField({
integer: false,
initial: 2,
label: 'DAGGERHEART.CONFIG.TokenSize.large'
}),
huge: new fields.NumberField({
integer: false,
initial: 3,
label: 'DAGGERHEART.CONFIG.TokenSize.huge'
}),
gargantuan: new fields.NumberField({
integer: false,
initial: 4,
label: 'DAGGERHEART.CONFIG.TokenSize.gargantuan'
})
}),
currency: new fields.SchemaField({
title: new fields.StringField({
required: true,