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

@ -0,0 +1,10 @@
export { default as CostField } from './costField.mjs';
export { default as UsesField } from './usesField.mjs';
export { default as RangeField } from './rangeField.mjs';
export { default as TargetField } from './targetField.mjs';
export { default as EffectsField } from './effectsField.mjs';
export { default as SaveField } from './saveField.mjs';
export { default as BeastformField } from './beastformField.mjs';
export { default as DamageField } from './damageField.mjs';
export { default as HealingField } from './healingField.mjs';
export { default as RollField } from './rollField.mjs';

View file

@ -0,0 +1,12 @@
const fields = foundry.data.fields;
export default class BeastformField extends fields.SchemaField {
constructor(options={}, context={}) {
const beastformFields = {
tierAccess: new fields.SchemaField({
exact: new fields.NumberField({ integer: true, nullable: true, initial: null })
})
};
super(beastformFields, options, context);
}
}

View file

@ -0,0 +1,82 @@
const fields = foundry.data.fields;
export default class CostField extends fields.ArrayField {
constructor(options={}, context={}) {
const element = 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 })
});
super(element, options, context);
}
static prepareConfig(config) {
const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : [];
config.costs = CostField.calcCosts.call(this, costs);
const hasCost = CostField.hasCost.call(this, config.costs);
if(config.isFastForward && !hasCost)
return ui.notifications.warn("You don't have the resources to use that action.");
return hasCost;
}
static 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;
});
}
static hasCost(costs) {
const realCosts = CostField.getRealCosts.call(this, 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 = CostField.getResources.call(this, 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
);
}
static 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
};
}
static getRealCosts(costs) {
const realCosts = costs?.length ? costs.filter(c => c.enabled) : [];
return realCosts;
}
}

View file

@ -0,0 +1,84 @@
import FormulaField from "../formulaField.mjs";
const fields = foundry.data.fields;
export default class DamageField extends fields.SchemaField {
constructor(options, context = {}) {
const damageFields = {
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
includeBase: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
})
};
super(damageFields, options, context);
}
}
export class DHActionDiceData extends foundry.abstract.DataModel {
/** @override */
static defineSchema() {
return {
multiplier: new fields.StringField({
choices: CONFIG.DH.GENERAL.multiplierTypes,
initial: 'prof',
label: 'Multiplier'
}),
flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }),
dice: new fields.StringField({ choices: CONFIG.DH.GENERAL.diceTypes, initial: 'd6', label: 'Dice' }),
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }),
custom: new fields.SchemaField({
enabled: new fields.BooleanField({ label: 'Custom Formula' }),
formula: new FormulaField({ label: 'Formula' })
})
};
}
getFormula() {
const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`,
bonus = this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : '';
return this.custom.enabled ? this.custom.formula : `${multiplier ?? 1}${this.dice}${bonus}`;
}
}
export class DHResourceData extends foundry.abstract.DataModel {
/** @override */
static defineSchema() {
return {
applyTo: new fields.StringField({
choices: CONFIG.DH.GENERAL.healingTypes,
required: true,
blank: false,
initial: CONFIG.DH.GENERAL.healingTypes.hitPoints.id,
label: 'DAGGERHEART.ACTIONS.Settings.applyTo.label'
}),
resultBased: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.resultBased.label'
}),
value: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData)
};
}
}
export class DHDamageData extends DHResourceData {
/** @override */
static defineSchema() {
return {
...super.defineSchema(),
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
type: new fields.SetField(
new fields.StringField({
choices: CONFIG.DH.GENERAL.damageTypes,
initial: 'physical',
nullable: false,
required: true
}),
{
label: 'Type'
}
)
};
}
}

View file

@ -0,0 +1,11 @@
const fields = foundry.data.fields;
export default class EffectsField extends fields.ArrayField {
constructor(options={}, context={}) {
const element = new fields.SchemaField({
_id: new fields.DocumentIdField(),
onSave: new fields.BooleanField({ initial: false })
});
super(element, options, context);
}
}

View file

@ -0,0 +1,9 @@
import { DHDamageData } from "./damageField.mjs";
const fields = foundry.data.fields;
export default class HealingField extends fields.EmbeddedDataField {
constructor(options, context = {}) {
super(DHDamageData, options, context);
}
}

View file

@ -0,0 +1,16 @@
const fields = foundry.data.fields;
export default class RangeField extends fields.StringField {
constructor(context={}) {
const options = {
choices: CONFIG.DH.GENERAL.range,
required: false,
blank: true
};
super(options, context);
}
static prepareConfig(config) {
return true;
}
}

View file

@ -0,0 +1,58 @@
const fields = foundry.data.fields;
export class DHActionRollData extends foundry.abstract.DataModel {
/** @override */
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 }),
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({ choices: CONFIG.DH.ACTIONS.advandtageState, initial: 'neutral' }),
diceRolling: new fields.SchemaField({
multiplier: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceSetNumbers,
initial: 'prof',
label: 'Dice Number'
}),
flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }),
dice: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceTypes,
initial: 'd6',
label: 'Dice Type'
}),
compare: new fields.StringField({
choices: CONFIG.DH.ACTIONS.diceCompare,
initial: 'above',
label: 'Should be'
}),
treshold: new fields.NumberField({ initial: 1, integer: true, min: 1, label: 'Treshold' })
}),
useDefault: new fields.BooleanField({ initial: false })
};
}
getFormula() {
if (!this.type) return;
let formula = '';
switch (this.type) {
case 'diceSet':
const multiplier =
this.diceRolling.multiplier === 'flat'
? this.diceRolling.flatMultiplier
: `@${this.diceRolling.multiplier}`;
formula = `${multiplier}${this.diceRolling.dice}cs${CONFIG.DH.ACTIONS.diceCompare[this.diceRolling.compare].operator}${this.diceRolling.treshold}`;
break;
default:
formula = '';
break;
}
return formula;
}
}
export default class RollField extends fields.EmbeddedDataField {
constructor(options, context = {}) {
super(DHActionRollData, options, context);
}
}

View file

@ -0,0 +1,19 @@
const fields = foundry.data.fields;
export default class SaveField extends fields.SchemaField {
constructor(options={}, context={}) {
const saveFields = {
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
})
};
super(saveFields, options, context);
}
}

View file

@ -0,0 +1,62 @@
const fields = foundry.data.fields;
export default class TargetField extends fields.SchemaField {
constructor(options={}, context={}) {
const targetFields = {
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 })
};
super(targetFields, options, context);
}
static prepareConfig(config) {
if (!this.target?.type) return [];
let targets;
if (this.target?.type === CONFIG.DH.ACTIONS.targetTypes.self.id)
targets = TargetField.formatTarget.call(this, 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 => TargetField.isTargetFriendly.call(this, t));
if (this.target.amount && targets.length > this.target.amount) targets = [];
}
config.targets = targets.map(t => TargetField.formatTarget.call(this, t));
const hasTargets = TargetField.checkTargets.call(this, this.target.amount, config.targets);
if(config.isFastForward && !hasTargets)
return ui.notifications.warn('Too many targets selected for that actions.');
return hasTargets;
}
static checkTargets(amount, targets) {
return true
// return !amount || (targets.length > amount);
}
static 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
};
}
}

View file

@ -0,0 +1,39 @@
const fields = foundry.data.fields;
export default class UsesField extends fields.SchemaField {
constructor(options={}, context={}) {
const usesFields = {
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
})
};
super(usesFields, options, context);
}
static prepareConfig(config) {
const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null;
if (uses && !uses.value) uses.value = 0;
config.uses = uses;
const hasUses = UsesField.hasUses.call(this, config.uses);
if(config.isFastForward && !hasUses)
return ui.notifications.warn("That action doesn't have remaining uses.");
return hasUses;
}
static calcUses(uses) {
if (!uses) return null;
return {
...uses,
enabled: uses.hasOwnProperty('enabled') ? uses.enabled : true
};
}
static hasUses(uses) {
if (!uses) return true;
return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max;
}
}