Action Refactor Part #1

This commit is contained in:
Dapoolp 2025-07-22 20:35:11 +02:00
parent 42a705a870
commit 5243260b4d
23 changed files with 435 additions and 248 deletions

View file

@ -5,7 +5,7 @@ export default class DHAttackAction extends DHDamageAction {
static extraSchemas = [...super.extraSchemas, ...['roll', 'save']];
static getRollType(parent) {
return parent.type === 'weapon' ? 'attack' : 'spellcast';
return parent.parent.type === 'weapon' ? 'attack' : 'spellcast';
}
get chatTemplate() {

View file

@ -1,6 +1,7 @@
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 +17,12 @@ const fields = foundry.data.fields;
- Summon Action create method
*/
export default class DHBaseAction extends foundry.abstract.DataModel {
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
static extraSchemas = [];
static defineSchema() {
return {
_id: new fields.DocumentIdField(),
_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 }),
@ -109,7 +110,10 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
return extraSchemas;
}
prepareData() {}
prepareData() {
this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name);
this.img = this.img ?? this.parent?.parent?.img;
}
get index() {
return foundry.utils.getProperty(this.parent, this.systemPath).indexOf(this);
@ -141,22 +145,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;
@ -552,27 +555,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());
}
}

View file

@ -1,5 +1,5 @@
import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
import ActionField from '../fields/actionField.mjs';
import { ActionField } from '../fields/actionField.mjs';
import BaseDataActor from './base.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';

View file

@ -3,7 +3,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs';
import BaseDataActor from './base.mjs';
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
import ActionField from '../fields/actionField.mjs';
import { ActionField } from '../fields/actionField.mjs';
export default class DhCharacter extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character'];
@ -334,6 +334,7 @@ export default class DhCharacter extends BaseDataActor {
return !primaryWeaponEquipped && !secondaryWeaponEquipped
? {
...this.attack,
uuid: this.attack.uuid,
id: this.attack.id,
name: this.activeBeastform ? 'DAGGERHEART.ITEMS.Beastform.attackName' : this.attack.name,
img: this.activeBeastform ? 'icons/creatures/claws/claw-straight-brown.webp' : this.attack.img,

View file

@ -1,7 +1,7 @@
import BaseDataActor from './base.mjs';
import DhLevelData from '../levelData.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ActionField from '../fields/actionField.mjs';
import { ActionField } from '../fields/actionField.mjs';
import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';

View file

@ -1,4 +1,215 @@
export default class ActionField extends foundry.data.fields.ObjectField {
import DHActionConfig from "../../applications/sheets-configs/action-config.mjs";
/**
* Specialized collection type for stored actions.
* @param {DataModel} model The parent DataModel to which this ActionCollection belongs.
* @param {Action[]} entries The actions to store.
*/
export class ActionCollection extends Collection {
constructor(model, entries) {
super();
this.#model = model;
for ( const entry of entries ) {
if ( !(entry instanceof game.system.api.models.actions.actionsTypes.base) ) continue;
this.set(entry._id, entry);
}
}
/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */
/**
* The parent DataModel to which this ActionCollection belongs.
* @type {DataModel}
*/
#model;
/* -------------------------------------------- */
/* -------------------------------------------- */
/* Methods */
/* -------------------------------------------- */
/* -------------------------------------------- */
/**
* Test the given predicate against every entry in the Collection.
* @param {function(*, number, ActionCollection): boolean} predicate The predicate.
* @returns {boolean}
*/
every(predicate) {
return this.reduce((pass, v, i) => pass && predicate(v, i, this), true);
}
/* -------------------------------------------- */
/**
* Convert the ActionCollection to an array of simple objects.
* @param {boolean} [source=true] Draw data for contained Documents from the underlying data source?
* @returns {object[]} The extracted array of primitive objects.
*/
toObject(source=true) {
return this.map(doc => doc.toObject(source));
}
}
/* -------------------------------------------- */
/**
* A subclass of ObjectField that represents a mapping of keys to the provided DataField type.
*
* @param {DataField} model The class of DataField which should be embedded in this field.
* @param {MappingFieldOptions} [options={}] Options which configure the behavior of the field.
* @property {string[]} [initialKeys] Keys that will be created if no data is provided.
* @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key.
* @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided
* by `options.initialKeys`?
*/
export class MappingField extends foundry.data.fields.ObjectField {
constructor(model, options) {
if ( !(model instanceof foundry.data.fields.DataField) ) {
throw new Error("MappingField must have a DataField as its contained element");
}
super(options);
/**
* The embedded DataField definition which is contained in this field.
* @type {DataField}
*/
this.model = model;
model.parent = this;
}
/* -------------------------------------------- */
/** @inheritDoc */
static get _defaults() {
return foundry.utils.mergeObject(super._defaults, {
initialKeys: null,
initialValue: null,
initialKeysOnly: false
});
}
/* -------------------------------------------- */
/** @inheritDoc */
_cleanType(value, options) {
Object.entries(value).forEach(([k, v]) => {
if ( k.startsWith("-=") ) return;
value[k] = this.model.clean(v, options);
});
return value;
}
/* -------------------------------------------- */
/** @inheritDoc */
getInitialValue(data) {
let keys = this.initialKeys;
const initial = super.getInitialValue(data);
if ( !keys || !foundry.utils.isEmpty(initial) ) return initial;
if ( !(keys instanceof Array) ) keys = Object.keys(keys);
for ( const key of keys ) initial[key] = this._getInitialValueForKey(key);
return initial;
}
/* -------------------------------------------- */
/**
* Get the initial value for the provided key.
* @param {string} key Key within the object being built.
* @param {object} [object] Any existing mapping data.
* @returns {*} Initial value based on provided field type.
*/
_getInitialValueForKey(key, object) {
const initial = this.model.getInitialValue();
return this.initialValue?.(key, initial, object) ?? initial;
}
/* -------------------------------------------- */
/** @override */
_validateType(value, options={}) {
if ( foundry.utils.getType(value) !== "Object" ) throw new Error("must be an Object");
const errors = this._validateValues(value, options);
if ( !foundry.utils.isEmpty(errors) ) {
const failure = new foundry.data.validation.DataModelValidationFailure();
failure.elements = Object.entries(errors).map(([id, failure]) => ({ id, failure }));
throw failure.asError();
}
}
/* -------------------------------------------- */
/**
* Validate each value of the object.
* @param {object} value The object to validate.
* @param {object} options Validation options.
* @returns {Record<string, Error>} An object of value-specific errors by key.
*/
_validateValues(value, options) {
const errors = {};
for ( const [k, v] of Object.entries(value) ) {
if ( k.startsWith("-=") ) continue;
const error = this.model.validate(v, options);
if ( error ) errors[k] = error;
}
return errors;
}
/* -------------------------------------------- */
/** @override */
initialize(value, model, options={}) {
if ( !value ) return value;
const obj = {};
const initialKeys = (this.initialKeys instanceof Array) ? this.initialKeys : Object.keys(this.initialKeys ?? {});
const keys = this.initialKeysOnly ? initialKeys : Object.keys(value);
for ( const key of keys ) {
const data = value[key] ?? this._getInitialValueForKey(key, value);
obj[key] = this.model.initialize(data, model, options);
}
return obj;
}
/* -------------------------------------------- */
/** @inheritDoc */
_getField(path) {
if ( path.length === 0 ) return this;
else if ( path.length === 1 ) return this.model;
path.shift();
return this.model._getField(path);
}
}
/* -------------------------------------------- */
/**
* Field that stores actions.
*/
export class ActionsField extends MappingField {
constructor(options) {
super(new ActionField(), options);
}
/* -------------------------------------------- */
/** @inheritDoc */
initialize(value, model, options) {
const actions = Object.values(super.initialize(value, model, options));
return new ActionCollection(model, actions);
}
}
/* -------------------------------------------- */
/**
* Field that stores action data and swaps class based on action type.
*/
export class ActionField extends foundry.data.fields.ObjectField {
getModel(value) {
return game.system.api.models.actions.actionsTypes[value.type] ?? game.system.api.models.actions.actionsTypes.attack;
}
@ -35,3 +246,141 @@ export default class ActionField extends foundry.data.fields.ObjectField {
if (cls) cls.migrateDataSafe(fieldData);
}
}
/* -------------------------------------------- */
export function ActionMixin(Base) {
class Action extends Base {
static metadata = Object.freeze({
name: "Action",
label: "DAGGERHEART.GENERAL.Action.single",
sheetClass: DHActionConfig
});
static _sheets = new Map();
static get documentName() {
return this.metadata.name;
}
get documentName() {
return this.constructor.documentName;
}
static defaultName() {
return this.documentName;
}
get relativeUUID() {
return `.Item.${this.item.id}.Action.${this.id}`;
}
get uuid() {
return `${this.item.uuid}.${this.documentName}.${this.id}`;
}
get sheet() {
if(!this.constructor._sheets.has(this.uuid)) {
const sheet = new this.constructor.metadata.sheetClass(this);
this.constructor._sheets.set(this.uuid, sheet);
}
return this.constructor._sheets.get(this.uuid);
}
get inCollection() {
return foundry.utils.getProperty(this.parent, this.systemPath) instanceof Collection;
}
static async create(data, operation={}) {
const { parent, renderSheet } = operation;
let { type } = data;
if(!type || !game.system.api.models.actions.actionsTypes[type]) {
({ type } =
(await foundry.applications.api.DialogV2.input({
window: { title: 'Select Action Type' },
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/actionTypes/actionType.hbs',
{ types: CONFIG.DH.ACTIONS.actionTypes }
),
ok: {
label: game.i18n.format('DOCUMENT.Create', {
type: game.i18n.localize('DAGGERHEART.GENERAL.Action.single')
})
}
})) ?? {});
}
if (!type) return;
const cls = game.system.api.models.actions.actionsTypes[type];
const action = new cls(
{
type,
...cls.getSourceConfig(parent)
},
{
parent
}
);
const created = await parent.parent.update({ [`system.actions.${action.id}`]: action.toObject() });
const newAction = parent.actions.get(action.id);
if(!newAction) return null;
if( renderSheet ) newAction.sheet.render({ force: true });
return newAction;
}
async update(updates, options={}) {
const path = this.inCollection ? `system.${this.systemPath}.${this.id}` : `system.${this.systemPath}`,
result = await this.item.update({[path]: updates}, options);
return this.inCollection ? foundry.utils.getProperty(result, `system.${this.systemPath}`).get(this.id) : foundry.utils.getProperty(result, `system.${this.systemPath}`);
}
delete() {
if(!this.inCollection) return this.item;
const action = foundry.utils.getProperty(this.item, `system.${this.systemPath}`)?.get(this.id);
if ( !action ) return this.item;
this.item.update({ [`system.${this.systemPath}.-=${this.id}`]: null });
this.constructor._sheets.get(this.uuid)?.close();
}
async deleteDialog() {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`),
name: this.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', {
name: this.name
})
});
if (!confirmed) return;
return this.delete();
}
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 = {
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);
}
}
return Action;
}

View file

@ -1,5 +1,5 @@
import AttachableItem from './attachableItem.mjs';
import ActionField from '../fields/actionField.mjs';
import { ActionField } from '../fields/actionField.mjs';
import { armorFeatures } from '../../config/itemConfig.mjs';
import { actionsTypes } from '../action/_module.mjs';

View file

@ -1,5 +1,5 @@
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.mjs';
import { ActionField } from '../fields/actionField.mjs';
export default class DHConsumable extends BaseDataItem {
/** @inheritDoc */

View file

@ -1,5 +1,5 @@
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.mjs';
import { ActionField } from '../fields/actionField.mjs';
export default class DHDomainCard extends BaseDataItem {
/** @inheritDoc */

View file

@ -1,5 +1,5 @@
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.mjs';
import { ActionField } from '../fields/actionField.mjs';
export default class DHFeature extends BaseDataItem {
/** @inheritDoc */

View file

@ -1,5 +1,5 @@
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.mjs';
import { ActionField } from '../fields/actionField.mjs';
export default class DHMiscellaneous extends BaseDataItem {
/** @inheritDoc */

View file

@ -1,6 +1,6 @@
import AttachableItem from './attachableItem.mjs';
import { actionsTypes } from '../action/_module.mjs';
import ActionField from '../fields/actionField.mjs';
import { ActionsField, ActionField } from '../fields/actionField.mjs';
export default class DHWeapon extends AttachableItem {
/** @inheritDoc */
@ -65,7 +65,8 @@ export default class DHWeapon extends AttachableItem {
}
}
}),
actions: new fields.ArrayField(new ActionField())
actions: new ActionsField()
// actions: new fields.ArrayField(new ActionField())
};
}