Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions module/applications/sheets/api/application-mixin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export default function DHApplicationMixin(Base) {
triggerContextMenu: DHSheetV2.#triggerContextMenu,
createDoc: DHSheetV2.#onCreateDoc,
editDoc: DHSheetV2.#editDoc,
deleteDoc: DHSheetV2.#deleteDoc,
deleteDoc: DHSheetV2.#onDeleteDoc,
deleteInvalidDoc: DHSheetV2.#onDeleteInvalidDoc,
toChat: DHSheetV2.#toChat,
useItem: DHSheetV2.#useItem,
viewItem: DHSheetV2.#viewItem,
Expand Down Expand Up @@ -845,14 +846,36 @@ export default function DHApplicationMixin(Base) {
* Delete an embedded document.
* @type {ApplicationClickAction}
*/
static async #deleteDoc(event, target) {
static async #onDeleteDoc(event, target) {
const doc = await getDocFromElement(target);
if (doc) {
if (event.shiftKey) return doc.delete();
else return await doc.deleteDialog();
}
}

/**
* Delete an embedded document or action that is invalid.
* @type {ApplicationClickAction}
* @this DHSheetV2
*/
static async #onDeleteInvalidDoc(event, target) {
const { invalidId, collection } = target.closest('[data-invalid-id]')?.dataset ?? {};
if (!invalidId) throw Error('Missing invalid document id');

if (collection === 'actions') {
this.document.update({
'system.actions': {
[invalidId]: _del
}
});
} else {
if (!this.document[collection]) throw new Error(`Invalid collection ${collection}`);
const document = this.document[collection].getInvalid(invalidId);
await document.delete(); // untested, test when implementing https://github.com/Foundryborne/daggerheart/issues/1891
}
}

/**
* Send item to Chat
* @type {ApplicationClickAction}
Expand Down
4 changes: 4 additions & 0 deletions module/applications/sheets/api/base-item.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
secrets: this.item.isOwner
})
break;
case 'actions':
context.invalidActions = Object.values(this.document._source.system.actions ?? {})
.filter(a => !this.document.system.actions?.has(a._id));
break;
case 'effects':
await this._prepareEffectsContext(context, options);
break;
Expand Down
8 changes: 5 additions & 3 deletions module/data/actor/adversary.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,12 @@ export default class DhpAdversary extends DhCreature {
return tags;
}

/** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */
/**
* Returns source data for this actor adjusted to a new tier, which can be used to create a new actor.
* @returns {object}
*/
adjustForTier(tier) {
const source = this.parent.toObject(true);
return getTierAdjustedAdversary(source, tier);
return getTierAdjustedAdversary(this.parent, tier);
}

/** @inheritdoc */
Expand Down
23 changes: 19 additions & 4 deletions module/data/actor/tierAdjustment.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { parseInlineParams } from '../../enrichers/parser.mjs';

/**
* Accepts source data for an adversary and a target tier, and returns new source data
* @type {object} source
* @type {DhActor} actor
* @type {number} tier
* @returns {object} adjusted source data
*/
export function getTierAdjustedAdversary(source, tier) {
export function getTierAdjustedAdversary(actor, tier) {
const source = actor.toObject(true);
const currentTier = source.system.tier ?? 1;

/** @type {(2 | 3 | 4)[]} */
Expand Down Expand Up @@ -67,8 +68,22 @@ export function getTierAdjustedAdversary(source, tier) {
const damageRegex = /@Damage\[([^[\]]*)\]({[^}]*})?/g;
for (const item of source.items) {
// Replace damage inlines with new formulas. Keep a record for a specific check later
const itemDocument = actor.items.get(item._id);
if (!itemDocument) {
console.error(`Item ${item.name} [${item._id}] was not in actor, possibly due to data preparation errors`);
continue;
}

const actions = Object.values(item.system.actions).filter(a => {
if (!itemDocument.system.actions?.has(a._id)) {
console.error(`Action ${a._id} in ${itemDocument.name} was not in actor, possibly due to errors in data prep`);
return false;
}
return true;
});

const descriptionFormulas = [];
for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
for (const withDescription of [item.system, ...actions]) {
withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
const { value: formula } = parseInlineParams(inner, { first: 'value' });
if (!formula) return match;
Expand All @@ -84,7 +99,7 @@ export function getTierAdjustedAdversary(source, tier) {
}

// Update damage in item actions and convert all formula matches in the descriptions to the new damage
for (const action of Object.values(item.system.actions)) {
for (const action of actions) {
if (!action.damage?.main) continue;
try {
// Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead
Expand Down
2 changes: 1 addition & 1 deletion module/data/fields/_module.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { ActionCollection } from './actionField.mjs';
export { ActionCollection } from './action-collection-field.mjs';
export { default as IterableTypedObjectField } from './iterableTypedObjectField.mjs';
export { default as FormulaField } from './formulaField.mjs';
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
Expand Down
92 changes: 92 additions & 0 deletions module/data/fields/action-collection-field.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { ActionField } from './actionField.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();
for (const [key, value] of entries) {
if (!(value instanceof game.system.api.models.actions.actionsTypes.base)) continue;
this.set(key, value);
}
}

/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */

/* -------------------------------------------- */
/* 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));
}
}

/* -------------------------------------------- */
/**
* Field that stores actions.
*/

export class ActionCollectionField extends foundry.data.fields.TypedObjectField {
constructor(options) {
super(new ActionField(), options);
}

/* -------------------------------------------- */
/** @inheritdoc */
initialize(value, model, options) {
const actions = Object.entries(super.initialize(value, model, options));
return new ActionCollection(model, actions);
}

/** @inheritdoc */
clean(value, options, state) {
// Before any migrations and such occurs, convert collections to objects
if (value instanceof Collection) {
value = value.entries().reduce((r, [k, v]) => {
r[k] = v;
return r;
}, {});
}
return super.clean(value, this.options, state);
}

/** @inheritdoc */
_cleanType(value, options, state) {
value = super._cleanType(value, options, state);
// Ensure ids match. If ids do not match, this causes issues in things like the action config's resync
// _id is considered more accurate, due to validation rules
if (value && typeof value === 'object') {
for (const [key, objValue] of Object.entries(value)) {
if (objValue._id && key !== objValue._id) {
delete value[key];
value[objValue._id] = objValue;
}
}
}
return value;
}
}
92 changes: 0 additions & 92 deletions module/data/fields/actionField.mjs
Original file line number Diff line number Diff line change
@@ -1,98 +1,6 @@
import DHActionConfig from '../../applications/sheets-configs/action-config.mjs';
import { itemAbleRollParse } from '../../helpers/utils.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();
for (const [key, value] of entries) {
if (!(value instanceof game.system.api.models.actions.actionsTypes.base)) continue;
this.set(key, value);
}
}

/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */

/* -------------------------------------------- */
/* 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));
}
}

/* -------------------------------------------- */

/**
* Field that stores actions.
*/
export class ActionsField extends foundry.data.fields.TypedObjectField {
constructor(options) {
super(new ActionField(), options);
}

/* -------------------------------------------- */

/** @inheritdoc */
initialize(value, model, options) {
const actions = Object.entries(super.initialize(value, model, options));
return new ActionCollection(model, actions);
}

/** @inheritdoc */
clean(value, options, state) {
// Before any migrations and such occurs, convert collections to objects
if (value instanceof Collection) {
value = value.entries().reduce((r, [k, v]) => {
r[k] = v;
return r;
}, {});
}
return super.clean(value, this.options, state);
}

/** @inheritdoc */
_cleanType(value, options, state) {
value = super._cleanType(value, options, state);
// Ensure ids match. If ids do not match, this causes issues in things like the action config's resync
// _id is considered more accurate, due to validation rules
if (value && typeof value === 'object') {
for (const [key, objValue] of Object.entries(value)) {
if (objValue._id && key !== objValue._id) {
delete value[key];
value[objValue._id] = objValue;
}
}
}
return value;
}
}

/* -------------------------------------------- */

/**
Expand Down
6 changes: 3 additions & 3 deletions module/data/item/base.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
updateLinkedItemApps
} from '../../helpers/utils.mjs';
import { DHActionDiceData } from '../fields/action/damageField.mjs';
import { ActionsField } from '../fields/actionField.mjs';
import { ActionCollectionField } from '../fields/action-collection-field.mjs';
import FormulaField from '../fields/formulaField.mjs';

const fields = foundry.data.fields;
Expand Down Expand Up @@ -95,7 +95,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
if (this.metadata.isInventoryItem)
schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true });

if (this.metadata.hasActions) schema.actions = new ActionsField();
if (this.metadata.hasActions) schema.actions = new ActionCollectionField();

return schema;
}
Expand Down Expand Up @@ -267,7 +267,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {

const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => {
const action = changed.system.actions[key];
if (action && Object.keys(action).length === 0) {
if (action && Object.keys(action).length === 0 && this.actions.has(key)) {
acc.push(...this.actions.get(key).triggers.map(x => x.trigger));
}

Expand Down
8 changes: 4 additions & 4 deletions module/data/settings/Homebrew.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defaultRestOptions } from '../../config/generalConfig.mjs';
import { resetAndRerenderActors } from '../../helpers/utils.mjs';
import { ActionsField } from '../fields/actionField.mjs';
import { ActionCollectionField } from '../fields/action-collection-field.mjs';

const currencyField = (initial, label, icon, initialAmount = 0) =>
new foundry.data.fields.SchemaField({
Expand Down Expand Up @@ -30,7 +30,7 @@ const restMoveField = () =>
base64: false
}),
description: new foundry.data.fields.HTMLField(),
actions: new ActionsField(),
actions: new ActionCollectionField(),
effects: new foundry.data.fields.ArrayField(new foundry.data.fields.ObjectField())
});

Expand Down Expand Up @@ -181,7 +181,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
base64: false
}),
description: new fields.HTMLField(),
actions: new ActionsField(),
actions: new ActionCollectionField(),
effects: new fields.ArrayField(new fields.ObjectField())
})
),
Expand All @@ -194,7 +194,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
base64: false
}),
description: new fields.HTMLField(),
actions: new ActionsField(),
actions: new ActionCollectionField(),
effects: new fields.ArrayField(new fields.ObjectField())
})
)
Expand Down
Loading
Loading