Update Beastform action

This commit is contained in:
Dapoolp 2025-08-22 23:10:45 +02:00
parent 9d9c636dbc
commit cf55659097
15 changed files with 324 additions and 132 deletions

View file

@ -302,7 +302,8 @@
"toLoadout": "Send to Loadout", "toLoadout": "Send to Loadout",
"toVault": "Send to Vault", "toVault": "Send to Vault",
"unequip": "Unequip", "unequip": "Unequip",
"useItem": "Use Item" "useItem": "Use Item",
"cancelBeastform": "Cancel Beastform"
}, },
"Countdown": { "Countdown": {
"addCountdown": "Add Countdown", "addCountdown": "Add Countdown",
@ -2199,23 +2200,23 @@
"roll": { "roll": {
"roll": { "roll": {
"label": "Roll", "label": "Roll",
"hint": "Roll Hint" "hint": "Auto behavior for rolls like Attack, Spellcast, etc."
}, },
"damage": { "damage": {
"label": "Damage/Healing Roll", "label": "Damage/Healing Roll",
"hint": "Damage/Healing Roll Hint" "hint": "Auto behavior for Damage & Healing rolls after the Attack/Spellcast."
}, },
"save": { "save": {
"label": "Reaction Roll", "label": "Reaction Roll",
"hint": "Reaction Roll Hint" "hint": "Auto behavior if a Reaction Roll is needed. Targets must be selected before the action is made"
}, },
"damageApply": { "damageApply": {
"label": "Apply Damage/Healing", "label": "Apply Damage/Healing",
"hint": "Apply Damage/Healing Hint" "hint": "Automatically apply damages & healings. Targets must be selected before the action is made. Bypass users permissions."
}, },
"effect": { "effect": {
"label": "Apply Effects", "label": "Apply Effects",
"hint": "Apply Effects Hint" "hint": "Automatically apply effects. Targets must be selected before the action is made. Bypass users permissions."
} }
} }
}, },
@ -2223,7 +2224,7 @@
"title": "Defeated Handling" "title": "Defeated Handling"
}, },
"roll": { "roll": {
"title": "Roll" "title": "Actions"
} }
}, },
"Homebrew": { "Homebrew": {

View file

@ -42,7 +42,7 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
main: { main: {
tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll', cssClass: "with-hint" }], tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll' }],
initial: 'general', initial: 'general',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }

View file

@ -661,31 +661,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
}) })
}); });
this.consumeResource(result?.costs); game.system.api.fields.ActionFields.CostField.execute.call(this, result);
}
// Remove when Action Refactor part #2 done
async consumeResource(costs) {
if (!costs?.length) return;
const usefulResources = {
...foundry.utils.deepClone(this.actor.system.resources),
fear: {
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
reversed: false
}
};
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => {
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
};
});
await this.actor.modifyResource(resources);
} }
//TODO: redo toggleEquipItem method //TODO: redo toggleEquipItem method

View file

@ -412,6 +412,16 @@ export default function DHApplicationMixin(Base) {
]; ];
if (usable) { if (usable) {
options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.cancelBeastform',
icon: 'fa-solid fa-ban',
condition: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system?.actions?.some(a => a.type === "beastform");
},
callback: async target => game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call(await getDocFromElement(target))
});
options.unshift({ options.unshift({
name: 'DAGGERHEART.GENERAL.damage', name: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion', icon: 'fa-solid fa-explosion',

View file

@ -4,10 +4,6 @@ import { ActionMixin } from '../fields/actionField.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
/*
!!! I'm currently refactoring the whole Action thing, it's a WIP !!!
*/
/* /*
ToDo ToDo
- Target Check / Target Picker - Target Check / Target Picker
@ -19,6 +15,7 @@ const fields = foundry.data.fields;
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) { export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
static extraSchemas = ['cost', 'uses', 'range']; static extraSchemas = ['cost', 'uses', 'range'];
/** @inheritDoc */
static defineSchema() { static defineSchema() {
const schemaFields = { const schemaFields = {
_id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }), _id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }),
@ -36,23 +33,33 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}; };
this.extraSchemas.forEach(s => { this.extraSchemas.forEach(s => {
let clsField; let clsField = this.getActionField(s);
if ((clsField = this.getActionField(s))) if (clsField)
schemaFields[s] = new clsField(); schemaFields[s] = new clsField();
}); });
return schemaFields; return schemaFields;
} }
/**
* Create a Map containing each Action step based on fields define in schema. Ordered by Fields order property.
* @returns {Map}
*/
defineWorkflow() { defineWorkflow() {
const workflow = new Map(); const workflow = new Map();
Object.entries(this.schema.fields).forEach(([k,s]) => { this.constructor.extraSchemas.forEach(s => {
if(s.execute) workflow.set(k, { order: s.order, execute: s.execute.bind(this) } ); let clsField = this.constructor.getActionField(s);
if (clsField?.execute) {
workflow.set(s, { order: clsField.order, execute: clsField.execute.bind(this) } );
if( s === "damage" ) workflow.set("applyDamage", { order: 75, execute: clsField.applyDamage.bind(this) } );
}
}); });
if(this.schema.fields.damage) workflow.set("applyDamage", { order: 75, execute: game.system.api.fields.ActionFields.DamageField.applyDamage.bind(this) } );
return new Map([...workflow.entries()].sort(([aKey, aValue], [bKey, bValue]) => aValue.order - bValue.order)); return new Map([...workflow.entries()].sort(([aKey, aValue], [bKey, bValue]) => aValue.order - bValue.order));
} }
/**
* Getter returning the workflow property or creating it the first time the property is called
*/
get workflow() { get workflow() {
if ( this.hasOwnProperty("_workflow") ) return this._workflow; if ( this.hasOwnProperty("_workflow") ) return this._workflow;
const workflow = Object.freeze(this.defineWorkflow()); const workflow = Object.freeze(this.defineWorkflow());
@ -60,24 +67,39 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return workflow; return workflow;
} }
/**
* Get the Field class from ActionFields global config
* @param {string} name Field short name, equal to Action property
* @returns Action Field
*/
static getActionField(name) { static getActionField(name) {
const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`]; const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`];
return fields.DataField.isPrototypeOf(field) && field; return fields.DataField.isPrototypeOf(field) && field;
} }
/** @inheritDoc */
prepareData() { prepareData() {
this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name); this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name);
this.img = this.img ?? this.parent?.parent?.img; this.img = this.img ?? this.parent?.parent?.img;
} }
/**
* Get Action ID
*/
get id() { get id() {
return this._id; return this._id;
} }
/**
* Return Item the action is attached too.
*/
get item() { get item() {
return this.parent.parent; return this.parent.parent;
} }
/**
* Return the first Actor parent found.
*/
get actor() { get actor() {
return this.item instanceof DhpActor return this.item instanceof DhpActor
? this.item ? this.item
@ -90,6 +112,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return 'trait'; return 'trait';
} }
/**
* Prepare base data based on Action Type & Parent Type
* @param {object} parent
* @returns {object}
*/
static getSourceConfig(parent) { static getSourceConfig(parent) {
const updateSource = {}; const updateSource = {};
if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) { if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) {
@ -112,6 +139,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return updateSource; return updateSource;
} }
/**
* Obtain a data object used to evaluate any dice rolls associated with this particular Action
* @param {object} [data ={}] Optional data object from previous configuration/rolls
* @returns {object}
*/
getRollData(data = {}) { getRollData(data = {}) {
if (!this.actor) return null; if (!this.actor) return null;
const actorData = this.actor.getRollData(false); const actorData = this.actor.getRollData(false);
@ -127,6 +159,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return actorData; return actorData;
} }
/**
* Execute each part of the Action workflow in order, calling a specific event before and after each part.
* @param {object} config Config object usually created from prepareConfig method
*/
async executeWorkflow(config) { async executeWorkflow(config) {
for(const [key, part] of this.workflow) { for(const [key, part] of this.workflow) {
if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return;
@ -135,7 +171,12 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
} }
} }
async use(event, options = {}) { /**
* Main method to use the Action
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
async use(event) {
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.");
if (this.chatDisplay) await this.toChat(); if (this.chatDisplay) await this.toChat();
@ -159,7 +200,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return config; return config;
} }
/* */ /**
* Create the basic config common to every action type
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
prepareBaseConfig(event) { prepareBaseConfig(event) {
const config = { const config = {
event, event,
@ -174,7 +219,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
hasRoll: this.hasRoll, hasRoll: this.hasRoll,
hasDamage: this.hasDamage, hasDamage: this.hasDamage,
hasHealing: this.hasHealing, hasHealing: this.hasHealing,
hasEffect: !!this.effects?.length, hasEffect: this.hasEffect,
hasSave: this.hasSave, hasSave: this.hasSave,
isDirect: !!this.damage?.direct, isDirect: !!this.damage?.direct,
selectedRollMode: game.settings.get('core', 'rollMode'), selectedRollMode: game.settings.get('core', 'rollMode'),
@ -185,6 +230,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return config; return config;
} }
/**
* Create the config for that action used for its workflow
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
prepareConfig(event) { prepareConfig(event) {
const config = this.prepareBaseConfig(event); const config = this.prepareBaseConfig(event);
for(const clsField of Object.values(this.schema.fields)) { for(const clsField of Object.values(this.schema.fields)) {
@ -194,10 +244,20 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return config; return config;
} }
/**
* Method used to know if a configuration dialog must be shown or not when there is no roll.
* @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @returns {boolean}
*/
requireConfigurationDialog(config) { requireConfigurationDialog(config) {
return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses); return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses);
} }
/**
*
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {boolean} successCost
*/
async consume(config, successCost = false) { async consume(config, successCost = false) {
await this.workflow.get("cost")?.execute(config, successCost); await this.workflow.get("cost")?.execute(config, successCost);
await this.workflow.get("uses")?.execute(config, successCost); await this.workflow.get("uses")?.execute(config, successCost);
@ -209,12 +269,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
} }
} }
/**
* Set if a configuration dialog must be shown or not if a special keyboard key is pressed.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
static applyKeybindings(config) { static applyKeybindings(config) {
config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey); config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey);
} }
/** /**
* Getters to know which parts the action of composed of. A field can exist but configured to not be used. * Getters to know which parts the action is composed of. A field can exist but configured to not be used.
* @returns {boolean} If that part is in the action. * @returns {boolean} If that part is in the action.
*/ */
@ -230,10 +294,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return this.damage?.parts?.length && this.type === 'healing' return this.damage?.parts?.length && this.type === 'healing'
} }
get hasDamagePart() {
return this.damage?.parts?.length;
}
get hasSave() { get hasSave() {
return !!this.save?.trait; return !!this.save?.trait;
} }

View file

@ -1,10 +1,9 @@
import BeastformDialog from '../../applications/dialogs/beastformDialog.mjs';
import DHBaseAction from './baseAction.mjs'; import DHBaseAction from './baseAction.mjs';
export default class DhBeastformAction extends DHBaseAction { export default class DhBeastformAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'beastform']; static extraSchemas = [...super.extraSchemas, 'beastform'];
async use(event, options) { /* async use(event, options) {
const beastformConfig = this.prepareBeastformConfig(); const beastformConfig = this.prepareBeastformConfig();
const abort = await this.handleActiveTransformations(); const abort = await this.handleActiveTransformations();
@ -82,5 +81,5 @@ export default class DhBeastformAction extends DHBaseAction {
beastformEffects.map(x => x.id) beastformEffects.map(x => x.id)
); );
return existingEffects; return existingEffects;
} } */
} }

View file

@ -1,6 +1,13 @@
import BeastformDialog from "../../../applications/dialogs/beastformDialog.mjs";
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class BeastformField extends fields.SchemaField { export default class BeastformField extends fields.SchemaField {
/**
* Action Workflow order
*/
static order = 90;
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
const beastformFields = { const beastformFields = {
tierAccess: new fields.SchemaField({ tierAccess: new fields.SchemaField({
@ -27,4 +34,96 @@ export default class BeastformField extends fields.SchemaField {
}; };
super(beastformFields, options, context); super(beastformFields, options, context);
} }
/**
* Beastform Transformation Action Workflow part.
* Must be called within Action context or similar.
* @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;
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')) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied'));
return false;
}
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
const actorLevel = this.actor.system.levelData.level.current;
const actorTier =
Object.values(settingsTiers).find(
tier => actorLevel >= tier.levels.start && actorLevel <= tier.levels.end
) ?? 1;
config.tierLimit = this.beastform.tierAccess.exact ?? actorTier;
}
/**
* TODO by Harry
* @param {*} selectedForm
* @param {*} evolvedData
* @param {*} hybridData
* @returns
*/
static async transform(selectedForm, evolvedData, hybridData) {
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject();
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
if (!beastformEffect) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return false;
}
if (evolvedData?.form) {
const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform');
if (!evolvedForm) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return false;
}
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
}
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {
formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => {
Object.keys(formCategory).forEach(advantageKey => {
advantages[advantageKey] = formCategory[advantageKey];
});
return advantages;
}, {});
formData.system.features = [
...formData.system.features,
...Object.values(hybridData.features).flatMap(x => Object.keys(x))
];
}
this.actor.createEmbeddedDocuments('Item', [formData]);
}
/**
* Remove existing beastform effect and return true if there was one
* @returns {boolean}
*/
static async handleActiveTransformations() {
const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform');
const existingEffects = beastformEffects.length > 0;
await this.actor.deleteEmbeddedDocuments(
'ActiveEffect',
beastformEffects.map(x => x.id)
);
return existingEffects;
}
} }

View file

@ -4,7 +4,7 @@ export default class CostField extends fields.ArrayField {
/** /**
* Action Workflow order * Action Workflow order
*/ */
order = 150; static order = 150;
/** @inheritDoc */ /** @inheritDoc */
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
@ -29,11 +29,11 @@ export default class CostField extends fields.ArrayField {
/** /**
* Cost Consumption Action Workflow part. * Cost Consumption Action Workflow part.
* Consume configured action resources. * Consume configured action resources.
* Must be called within Action context. * Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed. * @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed.
*/ */
async execute(config, successCost = false) { static async execute(config, successCost = false) {
const actor= this.actor.system.partner ?? this.actor, const actor= this.actor.system.partner ?? this.actor,
usefulResources = { usefulResources = {
...foundry.utils.deepClone(actor.system.resources), ...foundry.utils.deepClone(actor.system.resources),
@ -44,13 +44,15 @@ export default class CostField extends fields.ArrayField {
} }
}; };
for (var cost of config.costs) { if(this.parent?.parent) {
if (cost.keyIsID) { for (var cost of config.costs) {
usefulResources[cost.key] = { if (cost.keyIsID) {
value: cost.value, usefulResources[cost.key] = {
target: this.parent.parent, value: cost.value,
keyIsID: true target: this.parent.parent,
}; keyIsID: true
};
}
} }
} }
@ -78,7 +80,7 @@ export default class CostField extends fields.ArrayField {
/** /**
* Update Action Workflow config object. * Update Action Workflow config object.
* Must be called within Action context. * Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @returns {boolean} Return false if fast-forwarded and no more uses. * @returns {boolean} Return false if fast-forwarded and no more uses.
*/ */

View file

@ -7,7 +7,7 @@ export default class DamageField extends fields.SchemaField {
/** /**
* Action Workflow order * Action Workflow order
*/ */
order = 20; static order = 20;
/** @inheritDoc */ /** @inheritDoc */
constructor(options, context = {}) { constructor(options, context = {}) {
@ -24,12 +24,12 @@ export default class DamageField extends fields.SchemaField {
/** /**
* Roll Damage/Healing Action Workflow part. * Roll Damage/Healing Action Workflow part.
* Must be called within Action context. * Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {string} [messageId=null] ChatMessage Id where the clicked button belong. * @param {string} [messageId=null] ChatMessage Id where the clicked button belong.
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example. * @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/ */
async execute(config, messageId = null, force = false) { static async execute(config, messageId = null, force = false) {
if((this.hasRoll && DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id) && !force) return; if((this.hasRoll && DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id) && !force) return;
let formulas = this.damage.parts.map(p => ({ let formulas = this.damage.parts.map(p => ({
@ -97,11 +97,11 @@ export default class DamageField extends fields.SchemaField {
} }
/** /**
* * Return value or valueAlt from damage part
* Must be called within Action context. * Must be called within Action context or similar.
* @param {*} part * @param {object} part Damage Part
* @param {*} data * @param {object} data Action getRollData
* @returns * @returns Formula value object
*/ */
static getFormulaValue(part, data) { static getFormulaValue(part, data) {
let formulaValue = part.value; let formulaValue = part.value;
@ -118,10 +118,10 @@ export default class DamageField extends fields.SchemaField {
} }
/** /**
* * Prepare formulas for Damage Roll
* Must be called within Action context. * Must be called within Action context or similar.
* @param {*} formulas * @param {object[]} formulas Array of formatted formulas object
* @param {*} systemData * @param {object} data Action getRollData
* @returns * @returns
*/ */
static formatFormulas(formulas, data) { static formatFormulas(formulas, data) {

View file

@ -4,7 +4,7 @@ export default class EffectsField extends fields.ArrayField {
/** /**
* Action Workflow order * Action Workflow order
*/ */
order = 100; static order = 100;
/** @inheritDoc */ /** @inheritDoc */
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
@ -17,12 +17,12 @@ export default class EffectsField extends fields.ArrayField {
/** /**
* Apply Effects Action Workflow part. * Apply Effects Action Workflow part.
* Must be called within Action context. * Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {object[]} [targets=null] Array of targets to override pre-selected ones. * @param {object[]} [targets=null] Array of targets to override pre-selected ones.
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example. * @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/ */
async execute(config, targets = null, force = false) { 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); let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if(!message) { if(!message) {
@ -37,10 +37,9 @@ export default class EffectsField extends fields.ArrayField {
} }
/** /**
* * Apply Action Effects to a list of Targets
* Must be called within Action context. * Must be called within Action context or similar.
* @param {*} targets * @param {object[]} targets Array of formatted targets
* @returns
*/ */
static async applyEffects(targets) { static async applyEffects(targets) {
if (!this.effects?.length || !targets?.length) return; if (!this.effects?.length || !targets?.length) return;
@ -59,10 +58,9 @@ export default class EffectsField extends fields.ArrayField {
} }
/** /**
* * Apply an Effect to a target or enable it if already on it
* @param {*} effect * @param {object} effect Effect object containing ActiveEffect UUID
* @param {*} actor * @param {object} actor Actor Document
* @returns
*/ */
static async applyEffect(effect, actor) { static async applyEffect(effect, actor) {
const existingEffect = actor.effects.find(e => e.origin === effect.uuid); const existingEffect = actor.effects.find(e => e.origin === effect.uuid);

View file

@ -1,7 +1,10 @@
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class MacroField extends fields.DocumentUUIDField { export default class MacroField extends fields.DocumentUUIDField {
order = 70; /**
* Action Workflow order
*/
static order = 70;
/** @inheritDoc */ /** @inheritDoc */
constructor(context = {}) { constructor(context = {}) {
@ -10,9 +13,10 @@ export default class MacroField extends fields.DocumentUUIDField {
/** /**
* Macro Action Workflow part. * Macro Action Workflow part.
* Must be called within Action context or similar or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. Currently not used. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. Currently not used.
*/ */
async execute(config) { static async execute(config) {
const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro, const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro,
macro = await fromUuid(fixUUID); macro = await fromUuid(fixUUID);
try { try {

View file

@ -110,7 +110,10 @@ export class DHActionRollData extends foundry.abstract.DataModel {
} }
export default class RollField extends fields.EmbeddedDataField { export default class RollField extends fields.EmbeddedDataField {
order = 10; /**
* Action Workflow order
*/
static order = 10;
/** @inheritDoc */ /** @inheritDoc */
constructor(options, context = {}) { constructor(options, context = {}) {
@ -119,10 +122,10 @@ export default class RollField extends fields.EmbeddedDataField {
/** /**
* Roll Action Workflow part. * Roll Action Workflow part.
* Must be called within Action context. * Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/ */
async execute(config) { static async execute(config) {
if(!config.hasRoll) return; if(!config.hasRoll) return;
config = await this.actor.diceRoll(config); config = await this.actor.diceRoll(config);
if(!config) return false; if(!config) return false;

View file

@ -6,7 +6,7 @@ export default class SaveField extends fields.SchemaField {
/** /**
* Action Workflow order * Action Workflow order
*/ */
order = 50; static order = 50;
/** @inheritDoc */ /** @inheritDoc */
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
@ -27,12 +27,12 @@ export default class SaveField extends fields.SchemaField {
/** /**
* Reaction Roll Action Workflow part. * Reaction Roll Action Workflow part.
* Must be called within Action context. * Must be called within Action context or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {object[]} [targets=null] Array of targets to override pre-selected ones. * @param {object[]} [targets=null] Array of targets to override pre-selected ones.
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example. * @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/ */
async execute(config, targets = null, force = false) { 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); let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if(!message) { if(!message) {

View file

@ -6,7 +6,7 @@ export default class UsesField extends fields.SchemaField {
/** /**
* Action Workflow order * Action Workflow order
*/ */
order = 160; static order = 160;
/** @inheritDoc */ /** @inheritDoc */
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
@ -29,11 +29,11 @@ export default class UsesField extends fields.SchemaField {
/** /**
* Uses Consumption Action Workflow part. * Uses Consumption Action Workflow part.
* Increment Action spent uses by 1. * Increment Action spent uses by 1.
* Must be called within Action context. * Must be called within Action context or similar or similar.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed. * @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed.
*/ */
async execute(config, successCost = false) { static async execute(config, successCost = false) {
if ( if (
config.uses?.enabled && config.uses?.enabled &&
((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) || ((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) ||
@ -60,9 +60,9 @@ export default class UsesField extends fields.SchemaField {
} }
/** /**
* * Prepare Uses object for Action Workflow
* Must be called within Action context. * Must be called within Action context.
* @param {*} uses * @param {object} uses
* @returns {object} * @returns {object}
*/ */
static calcUses(uses) { static calcUses(uses) {

View file

@ -19,6 +19,41 @@
&.start-align { &.start-align {
align-self: flex-start; align-self: flex-start;
} }
// .form-group {
// flex-wrap: wrap;
// .hint {
// flex: 0 0 100%;
// margin: 0;
// font-size: var(--font-size-12);
// color: var(--color-form-hint);
// }
// &:hover {
// .hint {
// color: var(--color-form-hint-hover);
// }
// }
// &.setting-two-values {
// display: grid;
// grid-template-columns: repeat(3, 1fr);
// gap: .25rem .5rem;
// label, .form-fields {
// flex: 1;
// }
// .form-group label {
// text-align: right;
// }
// .hint {
// grid-column: 1 / -1;
// }
// }
// }
} }
.setting-group-field { .setting-group-field {
@ -140,44 +175,49 @@
} }
} }
.tab.with-hint { // .tab.with-hint {
fieldset { // fieldset {
gap: .5rem; // gap: .5rem;
.form-group { // .form-group {
// flex-wrap: wrap; // flex-wrap: wrap;
gap: .25rem; // gap: .25rem .5rem;
label { // label, .form-fields {
flex: 1; // flex: 1;
} // }
.hint { // .hint {
flex: 0 0 100%; // flex: 0 0 100%;
margin: 0; // margin: 0;
font-size: var(--font-size-14); // font-size: var(--font-size-12);
color: var(--color-form-hint); // color: var(--color-form-hint);
} // }
&:hover { // &:hover {
.hint { // .hint {
color: var(--color-form-hint-hover); // color: var(--color-form-hint-hover);
} // }
} // }
} // }
.form-group.setting-two-values { // .form-group.setting-two-values {
display: grid; // display: grid;
grid-template-columns: repeat(3, 1fr); // grid-template-columns: repeat(3, 1fr);
// gap: .25rem .5rem;
.form-group label { // label, .form-fields {
text-align: right; // flex: 1;
} // }
.hint { // .form-group label {
grid-column: 1 / -1; // text-align: right;
} // }
}
} // .hint {
} // grid-column: 1 / -1;
// }
// }
// }
// }
} }