[PR] [Feature] 590 - Daggerheart Menu (#1007)

* Added menu with refresh tools

* Replaced menu icon
This commit is contained in:
WBHarry 2025-09-07 00:30:29 +02:00 committed by GitHub
parent eefb28c312
commit f1b6d3851d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 730 additions and 350 deletions

View file

@ -1,4 +1,4 @@
import BeastformDialog from "../../../applications/dialogs/beastformDialog.mjs";
import BeastformDialog from '../../../applications/dialogs/beastformDialog.mjs';
const fields = foundry.data.fields;
@ -49,14 +49,14 @@ export default class BeastformField extends fields.SchemaField {
return await BeastformField.transform.call(this, selected, evolved, hybrid);
}
/**
* Update Action Workflow config object.
* Must be called within Action context.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
prepareConfig(config) {
if(this.actor.effects.find(x => x.type === 'beastform')) {
if (this.actor.effects.find(x => x.type === 'beastform')) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied'));
return false;
}
@ -73,10 +73,10 @@ export default class BeastformField extends fields.SchemaField {
/**
* TODO by Harry
* @param {*} selectedForm
* @param {*} evolvedData
* @param {*} hybridData
* @returns
* @param {*} selectedForm
* @param {*} evolvedData
* @param {*} hybridData
* @returns
*/
static async transform(selectedForm, evolvedData, hybridData) {
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject();

View file

@ -30,8 +30,13 @@ export default class DamageField extends fields.SchemaField {
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async execute(config, messageId = null, force = false) {
if(!this.hasDamage && !this.hasHealing) return;
if((this.hasRoll && DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id) && !force) return;
if (!this.hasDamage && !this.hasHealing) return;
if (
this.hasRoll &&
DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id &&
!force
)
return;
let formulas = this.damage.parts.map(p => ({
formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor),
@ -51,9 +56,10 @@ export default class DamageField extends fields.SchemaField {
};
delete damageConfig.evaluate;
if(DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) damageConfig.dialog.configure = false;
if (DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
damageConfig.dialog.configure = false;
if (config.hasSave) config.onSave = damageConfig.onSave = this.save.damageMod;
damageConfig.source.message = config.message?._id ?? messageId;
damageConfig.directDamage = !!damageConfig.source?.message;
@ -61,7 +67,7 @@ export default class DamageField extends fields.SchemaField {
// await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message);
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
if(!damageResult) return false;
if (!damageResult) return false;
config.damage = damageResult.damage;
config.message ??= damageConfig.message;
}
@ -70,19 +76,15 @@ export default class DamageField extends fields.SchemaField {
* Apply Damage/Healing Action Worflow part.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {*[]} targets Arrays of targets to bypass pre-selected ones.
* @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example.
* @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async applyDamage(config, targets = null, force = false) {
targets ??= config.targets.filter(target => target.hit);
if(!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return;
if (!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return;
for (let target of targets) {
const actor = fromUuidSync(target.actorId);
if(!actor) continue;
if (
!config.hasHealing &&
config.onSave &&
target.saved?.success === true
) {
if (!actor) continue;
if (!config.hasHealing && config.onSave && target.saved?.success === true) {
const mod = CONFIG.DH.ACTIONS.damageOnSave[config.onSave]?.mod ?? 1;
Object.entries(config.damage).forEach(([k, v]) => {
v.total = 0;
@ -97,17 +99,17 @@ export default class DamageField extends fields.SchemaField {
else actor.takeDamage(config.damage, config.isDirect);
}
}
/**
* Return value or valueAlt from damage part
* Must be called within Action context or similar.
* @param {object} part Damage Part
* @param {object} part Damage Part
* @param {object} data Action getRollData
* @returns Formula value object
*/
static getFormulaValue(part, data) {
let formulaValue = part.value;
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary';
@ -123,8 +125,8 @@ export default class DamageField extends fields.SchemaField {
* Prepare formulas for Damage Roll
* Must be called within Action context or similar.
* @param {object[]} formulas Array of formatted formulas object
* @param {object} data Action getRollData
* @returns
* @param {object} data Action getRollData
* @returns
*/
static formatFormulas(formulas, data) {
const formattedFormulas = [];
@ -145,16 +147,25 @@ export default class DamageField extends fields.SchemaField {
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
*/
static getAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players)
);
}
/**
* Return the automation setting for applyDamage method for current user role
* @returns {boolean} If applyDamage should be triggered automatically
*/
static getApplyAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players)
);
}
}

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from "../../../systemRegistration/socket.mjs";
import { emitAsGM, GMUpdateEvent } from '../../../systemRegistration/socket.mjs';
const fields = foundry.data.fields;
@ -25,21 +25,16 @@ export default class EffectsField extends fields.ArrayField {
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async execute(config, targets = null, force = false) {
if(!config.hasEffect) return;
if (!config.hasEffect) return;
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if(!message) {
if (!message) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
}
if(EffectsField.getAutomation() || force) {
if (EffectsField.getAutomation() || force) {
targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit);
await emitAsGM(
GMUpdateEvent.UpdateEffect,
EffectsField.applyEffects.bind(this),
targets,
this.uuid
);
await emitAsGM(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid);
// EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit));
}
}
@ -53,8 +48,7 @@ export default class EffectsField extends fields.ArrayField {
if (!this.effects?.length || !targets?.length) return;
let effects = this.effects;
targets.forEach(async token => {
if (this.hasSave && token.saved.success === true)
effects = this.effects.filter(e => e.onSave === true);
if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true);
if (!effects.length) return;
effects.forEach(async e => {
const actor = canvas.tokens.get(token.id)?.actor,
@ -96,6 +90,11 @@ export default class EffectsField extends fields.ArrayField {
* @returns {boolean} If execute should be triggered automatically
*/
static getAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.players)
);
}
}

View file

@ -8,7 +8,7 @@ export default class MacroField extends fields.DocumentUUIDField {
/** @inheritDoc */
constructor(context = {}) {
super({ type: "Macro" }, context);
super({ type: 'Macro' }, context);
}
/**

View file

@ -1,14 +1,13 @@
const fields = foundry.data.fields;
export default class RangeField extends fields.StringField {
/** @inheritDoc */
constructor(context = {}) {
const options = {
choices: CONFIG.DH.GENERAL.range,
required: false,
blank: true,
label: "DAGGERHEART.GENERAL.range"
label: 'DAGGERHEART.GENERAL.range'
};
super(options, context);
}

View file

@ -5,7 +5,12 @@ export class DHActionRollData extends foundry.abstract.DataModel {
static defineSchema() {
return {
type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }),
trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities, label: "DAGGERHEART.GENERAL.Trait.single" }),
trait: new fields.StringField({
nullable: true,
initial: null,
choices: CONFIG.DH.ACTOR.abilities,
label: 'DAGGERHEART.GENERAL.Trait.single'
}),
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }),
advState: new fields.StringField({
@ -86,14 +91,14 @@ export class DHActionRollData extends foundry.abstract.DataModel {
}
get rollTrait() {
if(this.parent?.actor?.type !== "character") return null;
if (this.parent?.actor?.type !== 'character') return null;
switch (this.type) {
case CONFIG.DH.GENERAL.rollTypes.spellcast.id:
return this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility';
case CONFIG.DH.GENERAL.rollTypes.attack.id:
case CONFIG.DH.GENERAL.rollTypes.trait.id:
return this.useDefault || !this.trait
? this.parent.item.system.attack?.roll?.trait ?? 'agility'
? (this.parent.item.system.attack?.roll?.trait ?? 'agility')
: this.trait;
default:
return null;
@ -118,21 +123,21 @@ export default class RollField extends fields.EmbeddedDataField {
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
static async execute(config) {
if(!config.hasRoll) return;
if (!config.hasRoll) return;
config = await this.actor.diceRoll(config);
if(!config) return false;
if (!config) return false;
}
/**
* Update Action Workflow config object.
* Must be called within Action context.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
prepareConfig(config) {
if(!config.hasRoll) return;
if (!config.hasRoll) return;
config.dialog.configure = RollField.getAutomation() ? !config.dialog.configure : config.dialog.configure;
const roll = {
baseModifiers: this.roll.getModifier(),
label: 'Attack',
@ -152,6 +157,11 @@ export default class RollField extends fields.EmbeddedDataField {
* @returns {boolean} If execute should be triggered automatically
*/
static getAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.players)
);
}
}

View file

@ -1,4 +1,4 @@
import { abilities } from "../../../config/actorConfig.mjs";
import { abilities } from '../../../config/actorConfig.mjs';
const fields = foundry.data.fields;
@ -33,15 +33,15 @@ export default class SaveField extends fields.SchemaField {
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async execute(config, targets = null, force = false) {
if(!config.hasSave) return;
if (!config.hasSave) return;
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if(!message) {
if (!message) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
}
if(SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) {
if (SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) {
targets ??= config.targets.filter(t => !config.hasRoll || t.hit);
await SaveField.rollAllSave.call(this, targets, config.event, message);
} else return false;
@ -52,35 +52,35 @@ export default class SaveField extends fields.SchemaField {
* Must be called within Action context.
* @param {object[]} targets Array of formatted targets.
* @param {Event} event Triggering event
* @param {ChatMessage} message The ChatMessage the triggered button comes from.
* @param {ChatMessage} message The ChatMessage the triggered button comes from.
*/
static async rollAllSave(targets, event, message) {
if(!targets) return;
if (!targets) return;
return new Promise(resolve => {
const aPromise = [];
targets.forEach(target => {
aPromise.push(
new Promise(async subResolve => {
const actor = fromUuidSync(target.actorId);
if(actor) {
const rollSave = game.user === actor.owner ?
SaveField.rollSave.call(this, actor, event)
: actor.owner
.query('reactionRoll', {
actionId: this.uuid,
actorId: actor.uuid,
event,
message
});
if (actor) {
const rollSave =
game.user === actor.owner
? SaveField.rollSave.call(this, actor, event)
: actor.owner.query('reactionRoll', {
actionId: this.uuid,
actorId: actor.uuid,
event,
message
});
const result = await rollSave;
await SaveField.updateSaveMessage.call(this, result, message, target.id);
subResolve();
} else subResolve();
})
)
);
});
Promise.all(aPromise).then(result => resolve());
})
});
}
/**
@ -93,10 +93,10 @@ export default class SaveField extends fields.SchemaField {
static async rollSave(actor, event) {
if (!actor) return;
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)
}),
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.save.trait]?.label)
}),
rollConfig = {
event,
title,
@ -109,7 +109,8 @@ export default class SaveField extends fields.SchemaField {
hasRoll: true,
data: actor.getRollData()
};
if(SaveField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) rollConfig.dialog = { configure: false };
if (SaveField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
rollConfig.dialog = { configure: false };
return actor.diceRoll(rollConfig);
}
@ -117,31 +118,32 @@ export default class SaveField extends fields.SchemaField {
* Update a Roll ChatMessage for a token according to his Reaction Roll result.
* @param {object} result Result from the Reaction Roll
* @param {object} message ChatMessage to update
* @param {string} targetId Token ID
* @param {string} targetId Token ID
*/
static async updateSaveMessage(result, message, targetId) {
if (!result) return;
const updateMsg = async function(message, targetId, result) {
const updateMsg = async function (message, targetId, result) {
// setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id),
changes = {
flags: {
[game.system.id]: {
reactionRolls: {
[targetId]:
{
result: result.roll.total,
success: result.roll.success
}
const chatMessage = ui.chat.collection.get(message._id),
changes = {
flags: {
[game.system.id]: {
reactionRolls: {
[targetId]: {
result: result.roll.total,
success: result.roll.success
}
}
}
};
await chatMessage.update(changes);
}
};
await chatMessage.update(changes);
// }, 100);
};
if (game.modules.get('dice-so-nice')?.active)
game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(async () => await updateMsg(message, targetId, result));
game.dice3d
.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id)
.then(async () => await updateMsg(message, targetId, result));
else await updateMsg(message, targetId, result);
}
@ -150,25 +152,29 @@ export default class SaveField extends fields.SchemaField {
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
*/
static getAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.players)
);
}
/**
* Send a query to an Actor owner to roll a Reaction Roll then send back the result.
* @param {object} param0
* @param {string} param0.actionId Action ID
* @param {string} param0.actorId Actor ID
* @param {Event} param0.event Triggering event
* @param {ChatMessage} param0.message Chat Message to update
* @returns
* @param {object} param0
* @param {string} param0.actionId Action ID
* @param {string} param0.actorId Actor ID
* @param {Event} param0.event Triggering event
* @param {ChatMessage} param0.message Chat Message to update
* @returns
*/
static rollSaveQuery({ actionId, actorId, event, message }) {
return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId),
action = await fromUuid(actionId);
if (!actor || !actor?.isOwner) reject();
SaveField.rollSave.call(action, actor, event, message)
.then(result => resolve(result));
SaveField.rollSave.call(action, actor, event, message).then(result => resolve(result));
});
}
}

View file

@ -1,7 +1,6 @@
const fields = foundry.data.fields;
export default class TargetField extends fields.SchemaField {
/** @inheritDoc */
constructor(options = {}, context = {}) {
const targetFields = {
@ -21,7 +20,7 @@ export default class TargetField extends fields.SchemaField {
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
prepareConfig(config) {
if (!this.target?.type) return config.targets = [];
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.
@ -62,15 +61,11 @@ export default class TargetField extends fields.SchemaField {
* @returns {boolean} If both actors respect the provided type.
*/
static isTargetFriendly(actor, target, type) {
const actorDisposition = actor.token
? actor.token.disposition
: actor.prototypeToken.disposition,
const actorDisposition = actor.token ? actor.token.disposition : actor.prototypeToken.disposition,
targetDisposition = target.document.disposition;
return (
(type === CONFIG.DH.GENERAL.targetTypes.friendly.id &&
actorDisposition === targetDisposition) ||
(type === CONFIG.DH.GENERAL.targetTypes.hostile.id &&
actorDisposition + targetDisposition === 0)
(type === CONFIG.DH.GENERAL.targetTypes.friendly.id && actorDisposition === targetDisposition) ||
(type === CONFIG.DH.GENERAL.targetTypes.hostile.id && actorDisposition + targetDisposition === 0)
);
}

View file

@ -7,7 +7,7 @@ export default class UsesField extends fields.SchemaField {
* Action Workflow order
*/
static order = 160;
/** @inheritDoc */
constructor(options = {}, context = {}) {
const usesFields = {
@ -62,7 +62,7 @@ export default class UsesField extends fields.SchemaField {
/**
* Prepare Uses object for Action Workflow
* Must be called within Action context.
* @param {object} uses
* @param {object} uses
* @returns {object}
*/
static calcUses(uses) {
@ -77,7 +77,7 @@ export default class UsesField extends fields.SchemaField {
/**
* Check if the Action still get atleast one unspent uses.
* Must be called within Action context.
* @param {*} uses
* @param {*} uses
* @returns {boolean}
*/
static hasUses(uses) {