Refactor/actions v2 (#402)

* Action Refactor Part #1

* Fixed Weapon/Armor features. Fixed Feature actions

* f

* Action Refactor Part #2

* Fixes

* Remove ActionsField from Companion

* Fixes

* Localization fix

* BaseDataItem hasActions false

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
This commit is contained in:
Dapoulp 2025-07-25 01:10:49 +02:00 committed by GitHub
parent 80744381f5
commit 0632a8c6bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 988 additions and 743 deletions

View file

@ -1,6 +1,6 @@
import { DHActionDiceData, DHActionRollData, DHDamageData, DHDamageField, DHResourceData } from './actionDice.mjs';
import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
import { ActionMixin } from '../fields/actionField.mjs';
const fields = foundry.data.fields;
@ -16,12 +16,12 @@ const fields = foundry.data.fields;
- Summon Action create method
*/
export default class DHBaseAction extends foundry.abstract.DataModel {
static extraSchemas = [];
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
static extraSchemas = ['cost', 'uses', 'range'];
static defineSchema() {
return {
_id: new fields.DocumentIdField(),
const schemaFields = {
_id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }),
systemPath: new fields.StringField({ required: true, initial: 'actions' }),
type: new fields.StringField({ initial: undefined, readonly: true, required: true }),
name: new fields.StringField({ initial: undefined }),
@ -32,87 +32,25 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
choices: CONFIG.DH.ITEM.actionTypes,
initial: 'action',
nullable: true
}),
cost: new fields.ArrayField(
new fields.SchemaField({
key: new fields.StringField({
nullable: false,
required: true,
initial: 'hope'
}),
keyIsID: new fields.BooleanField(),
value: new fields.NumberField({ nullable: true, initial: 1 }),
scalable: new fields.BooleanField({ initial: false }),
step: new fields.NumberField({ nullable: true, initial: null })
})
),
uses: new fields.SchemaField({
value: new fields.NumberField({ nullable: true, initial: null }),
max: new fields.NumberField({ nullable: true, initial: null }),
recovery: new fields.StringField({
choices: CONFIG.DH.GENERAL.refreshTypes,
initial: null,
nullable: true
})
}),
range: new fields.StringField({
choices: CONFIG.DH.GENERAL.range,
required: false,
blank: true
// initial: null
}),
...this.defineExtraSchema()
})
};
this.extraSchemas.forEach(s => {
let clsField;
if(clsField = this.getActionField(s)) schemaFields[s] = new clsField();
});
return schemaFields;
}
static defineExtraSchema() {
const extraFields = {
damage: new DHDamageField(),
roll: new fields.EmbeddedDataField(DHActionRollData),
save: new fields.SchemaField({
trait: new fields.StringField({
nullable: true,
initial: null,
choices: CONFIG.DH.ACTOR.abilities
}),
difficulty: new fields.NumberField({ nullable: true, initial: 10, integer: true, min: 0 }),
damageMod: new fields.StringField({
initial: CONFIG.DH.ACTIONS.damageOnSave.none.id,
choices: CONFIG.DH.ACTIONS.damageOnSave
})
}),
target: new fields.SchemaField({
type: new fields.StringField({
choices: CONFIG.DH.ACTIONS.targetTypes,
initial: CONFIG.DH.ACTIONS.targetTypes.any.id,
nullable: true,
initial: null
}),
amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
}),
effects: new fields.ArrayField( // ActiveEffect
new fields.SchemaField({
_id: new fields.DocumentIdField(),
onSave: new fields.BooleanField({ initial: false })
})
),
healing: new fields.EmbeddedDataField(DHResourceData),
beastform: new fields.SchemaField({
tierAccess: new fields.SchemaField({
exact: new fields.NumberField({ integer: true, nullable: true, initial: null })
})
})
},
extraSchemas = {};
this.extraSchemas.forEach(s => (extraSchemas[s] = extraFields[s]));
return extraSchemas;
static getActionField(name) {
const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`];
return fields.DataField.isPrototypeOf(field) && field;
}
prepareData() {}
get index() {
return foundry.utils.getProperty(this.parent, this.systemPath).indexOf(this);
prepareData() {
this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name);
this.img = this.img ?? this.parent?.parent?.img;
}
get id() {
@ -141,22 +79,21 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
static getSourceConfig(parent) {
const updateSource = {};
updateSource.img ??= parent?.img ?? parent?.system?.img;
if (parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) {
if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) {
updateSource['damage'] = { includeBase: true };
updateSource['range'] = parent?.system?.attack?.range;
updateSource['range'] = parent?.attack?.range;
updateSource['roll'] = {
useDefault: true
};
} else {
if (parent?.system?.trait) {
if (parent?.trait) {
updateSource['roll'] = {
type: this.getRollType(parent),
trait: parent.system.trait
trait: parent.trait
};
}
if (parent?.system?.range) {
updateSource['range'] = parent?.system?.range;
if (parent?.range) {
updateSource['range'] = parent?.range;
}
}
return updateSource;
@ -180,38 +117,14 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
async use(event, ...args) {
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
const isFastForward = event.shiftKey || (!this.hasRoll && !this.hasSave);
// Prepare base Config
const initConfig = this.initActionConfig(event);
// Prepare Targets
const targetConfig = this.prepareTarget();
if (isFastForward && !targetConfig) return ui.notifications.warn('Too many targets selected for that actions.');
// Prepare Range
const rangeConfig = this.prepareRange();
// Prepare Costs
const costsConfig = this.prepareCost();
if (isFastForward && !(await this.hasCost(costsConfig)))
return ui.notifications.warn("You don't have the resources to use that action.");
// Prepare Uses
const usesConfig = this.prepareUse();
if (isFastForward && !this.hasUses(usesConfig))
return ui.notifications.warn("That action doesn't have remaining uses.");
// Prepare Roll Data
const actorData = this.getRollData();
let config = {
...initConfig,
targets: targetConfig,
range: rangeConfig,
costs: costsConfig,
uses: usesConfig,
data: actorData
};
let config = this.prepareConfig(event);
for(let i = 0; i < this.constructor.extraSchemas.length; i++) {
let clsField = this.constructor.getActionField(this.constructor.extraSchemas[i]);
if(clsField?.prepareConfig) {
const keep = clsField.prepareConfig.call(this, config);
if(config.isFastForward && !keep) return;
}
}
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
@ -243,7 +156,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
}
/* */
initActionConfig(event) {
prepareConfig(event) {
return {
event,
title: this.item.name,
@ -257,7 +170,9 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
hasHealing: !!this.healing,
hasEffect: !!this.effects?.length,
hasSave: this.hasSave,
selectedRollMode: game.settings.get('core', 'rollMode')
selectedRollMode: game.settings.get('core', 'rollMode'),
isFastForward: event.shiftKey,
data: this.getRollData()
};
}
@ -265,36 +180,6 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
return !config.event.shiftKey && !this.hasRoll && (config.costs?.length || config.uses);
}
prepareCost() {
const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : [];
return this.calcCosts(costs);
}
prepareUse() {
const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null;
if (uses && !uses.value) uses.value = 0;
return uses;
}
prepareTarget() {
if (!this.target?.type) return [];
let targets;
if (this.target?.type === CONFIG.DH.ACTIONS.targetTypes.self.id)
targets = this.constructor.formatTarget(this.actor.token ?? this.actor.prototypeToken);
targets = Array.from(game.user.targets);
if (this.target.type !== CONFIG.DH.ACTIONS.targetTypes.any.id) {
targets = targets.filter(t => this.isTargetFriendly(t));
if (this.target.amount && targets.length > this.target.amount) targets = [];
}
targets = targets.map(t => this.constructor.formatTarget(t));
return targets;
}
prepareRange() {
const range = this.range ?? null;
return range;
}
prepareRoll() {
const roll = {
modifiers: this.modifiers,
@ -366,108 +251,6 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
}
/* SAVE */
/* COST */
getRealCosts(costs) {
const realCosts = costs?.length ? costs.filter(c => c.enabled) : [];
return realCosts;
}
calcCosts(costs) {
return costs.map(c => {
c.scale = c.scale ?? 1;
c.step = c.step ?? 1;
c.total = c.value * c.scale * c.step;
c.enabled = c.hasOwnProperty('enabled') ? c.enabled : true;
return c;
});
}
async getResources(costs) {
const actorResources = this.actor.system.resources;
const itemResources = {};
for (var itemResource of costs) {
if (itemResource.keyIsID) {
itemResources[itemResource.key] = {
value: this.parent.resource.value ?? 0
};
}
}
return {
...actorResources,
...itemResources
};
}
/* COST */
async hasCost(costs) {
const realCosts = this.getRealCosts(costs),
hasFearCost = realCosts.findIndex(c => c.key === 'fear');
if (hasFearCost > -1) {
const fearCost = realCosts.splice(hasFearCost, 1)[0];
if (
!game.user.isGM ||
fearCost.total > game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
)
return false;
}
/* isReversed is a sign that the resource is inverted, IE it counts upwards instead of down */
const resources = await this.getResources(realCosts);
return realCosts.reduce(
(a, c) =>
a && resources[c.key].isReversed
? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max
: resources[c.key]?.value >= (c.total ?? c.value),
true
);
}
/* USES */
calcUses(uses) {
if (!uses) return null;
return {
...uses,
enabled: uses.hasOwnProperty('enabled') ? uses.enabled : true
};
}
hasUses(uses) {
if (!uses) return true;
return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max;
}
/* TARGET */
isTargetFriendly(target) {
const actorDisposition = this.actor.token
? this.actor.token.disposition
: this.actor.prototypeToken.disposition,
targetDisposition = target.document.disposition;
return (
(this.target.type === CONFIG.DH.ACTIONS.targetTypes.friendly.id &&
actorDisposition === targetDisposition) ||
(this.target.type === CONFIG.DH.ACTIONS.targetTypes.hostile.id &&
actorDisposition + targetDisposition === 0)
);
}
static formatTarget(actor) {
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
};
}
/* TARGET */
/* RANGE */
/* RANGE */
/* EFFECTS */
async applyEffects(event, data, targets) {
targets ??= data.system.targets;
@ -552,27 +335,4 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
});
}
}
async toChat(origin) {
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: game.i18n.localize('DAGGERHEART.CONFIG.ActionType.action'),
origin: origin,
img: this.img,
name: this.name,
description: this.description,
actions: []
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
}
}