import { ModifiableData } from "../common/common-data"; import { DS4 } from "../config"; import { DS4Item } from "../item/item"; import { DS4ItemData, ItemType } from "../item/item-data"; import { DS4ActorData } from "./actor-data"; /** * The Actor class for DS4 */ export class DS4Actor extends Actor { /** @override */ prepareData(): void { this.data = duplicate(this._data) as DS4ActorData; if (!this.data.img) this.data.img = CONST.DEFAULT_TOKEN; if (!this.data.name) this.data.name = "New " + this.entity; this.prepareBaseData(); this.prepareEmbeddedEntities(); this.applyActiveEffectsToNonDerivedData(); this.prepareDerivedData(); this.applyActiveEffectsToDerivedData(); } /** @override */ prepareBaseData(): void { const data = this.data; const attributes = data.data.attributes; Object.values(attributes).forEach( (attribute: ModifiableData) => (attribute.total = attribute.base + attribute.mod), ); const traits = data.data.traits; Object.values(traits).forEach((trait: ModifiableData) => (trait.total = trait.base + trait.mod)); } applyActiveEffectsToNonDerivedData(): void { this.applyActiveEffectsFiltered((change) => !this.derivedDataProperties.includes(change.key)); } applyActiveEffectsToDerivedData(): void { this.applyActiveEffectsFiltered((change) => this.derivedDataProperties.includes(change.key)); } /** * Apply ActiveEffectChanges to the Actor data which are caused by ActiveEffects and satisfy the given predicate. * * @param predicate - The predicate that ActiveEffectChanges need to satisfy in order to be applied */ applyActiveEffectsFiltered(predicate: (change: ActiveEffectChange) => boolean): void { const overrides: Record = {}; // Organize non-disabled effects by their application priority const changes = this.effects.reduce( (changes: Array }>, e) => { if (e.data.disabled) return changes; return changes.concat( e.data.changes.filter(predicate).map((c) => { const duplicatedChange = duplicate(c) as ActiveEffect.Change; duplicatedChange.priority = duplicatedChange.priority ?? duplicatedChange.mode * 10; return { ...duplicatedChange, effect: e }; }), ); }, [], ); changes.sort((a, b) => a.priority - b.priority); // Apply all changes for (const change of changes) { const result = change.effect.apply(this, change); if (result !== null) overrides[change.key] = result; } // Expand the set of final overrides this.overrides = expandObject({ ...flattenObject(this.overrides ?? {}), ...overrides }); } /** @override */ prepareDerivedData(): void { this._prepareCombatValues(); } /** The list of properties that are derived from others, given in dot notation */ get derivedDataProperties(): Array { return Object.keys(DS4.i18n.combatValues) .map((combatValue) => `data.combatValues.${combatValue}.total`) .concat("data.combatValues.hitPoints.max"); } /** * The list of item types that can be owned by this actor. */ get ownableItemTypes(): Array { switch (this.data.type) { case "character": return [ "weapon", "armor", "shield", "trinket", "equipment", "spell", "talent", "racialAbility", "language", "alphabet", ]; case "creature": return ["weapon", "armor", "shield", "trinket", "equipment", "spell", "specialCreatureAbility"]; default: return []; } } /** * Checks whether or not the given item type can be owned by the actor. * @param itemType - The item type to check */ canOwnItemType(itemType: ItemType): boolean { return this.ownableItemTypes.includes(itemType); } /** * Prepares the combat values of the actor. */ private _prepareCombatValues(): void { const data = this.data.data; const armorValueOfEquippedItems = this._calculateArmorValueOfEquippedItems(); data.combatValues.hitPoints.base = (data.attributes.body.total ?? 0) + (data.traits.constitution.total ?? 0) + 10; data.combatValues.defense.base = (data.attributes.body.total ?? 0) + (data.traits.constitution.total ?? 0) + armorValueOfEquippedItems; data.combatValues.initiative.base = (data.attributes.mobility.total ?? 0) + (data.traits.agility.total ?? 0); data.combatValues.movement.base = (data.attributes.mobility.total ?? 0) / 2 + 1; data.combatValues.meleeAttack.base = (data.attributes.body.total ?? 0) + (data.traits.strength.total ?? 0); data.combatValues.rangedAttack.base = (data.attributes.mobility.total ?? 0) + (data.traits.dexterity.total ?? 0); data.combatValues.spellcasting.base = (data.attributes.mind.total ?? 0) + (data.traits.aura.total ?? 0) - armorValueOfEquippedItems; data.combatValues.targetedSpellcasting.base = (data.attributes.mind.total ?? 0) + (data.traits.dexterity.total ?? 0) - armorValueOfEquippedItems; Object.values(data.combatValues).forEach( (combatValue: ModifiableData) => (combatValue.total = combatValue.base + combatValue.mod), ); data.combatValues.hitPoints.max = data.combatValues.hitPoints.total; } /** * Calculates the total armor value of all equipped items. */ private _calculateArmorValueOfEquippedItems(): number { return this.items .map((item) => { if (item.data.type === "armor" || item.data.type === "shield") { return item.data.data.equipped ? item.data.data.armorValue : 0; } else { return 0; } }) .reduce((a, b) => a + b, 0); } /** * Handle how changes to a Token attribute bar are applied to the Actor. * This only differs from the base implementation by also allowing negative values. * @override */ async modifyTokenAttribute(attribute: string, value: number, isDelta = false, isBar = true): Promise { const current = getProperty(this.data.data, attribute); // Determine the updates to make to the actor data let updates: Record; if (isBar) { if (isDelta) value = Math.min(Number(current.value) + value, current.max); updates = { [`data.${attribute}.value`]: value }; } else { if (isDelta) value = Number(current) + value; updates = { [`data.${attribute}`]: value }; } // Call a hook to handle token resource bar updates const allowed = Hooks.call("modifyTokenAttribute", { attribute, value, isDelta, isBar }, updates); return allowed !== false ? this.update(updates) : this; } /** @override */ // TODO(types): Improve typing once it's fixed in upstream (arrays can be passed!) createEmbeddedEntity( embeddedName: "OwnedItem", data: DeepPartial, options?: Record, ): Promise; createEmbeddedEntity( embeddedName: "ActiveEffect", data: DeepPartial, options?: Record, ): Promise; createEmbeddedEntity( embeddedName: "OwnedItem" | "ActiveEffect", data: DeepPartial | DeepPartial, options?: Record, ): Promise | Promise { if (embeddedName === "OwnedItem") { preCreateOwnedItem.bind(this)(data); return super.createEmbeddedEntity(embeddedName, data, options); } return super.createEmbeddedEntity(embeddedName, data, options); } /** @override */ // TODO(types): Improve typing once it's fixed in upstream updateEmbeddedEntity(embeddedName: string, data: unknown[], options?: Entity.UpdateOptions): Promise; updateEmbeddedEntity(embeddedName: string, data: unknown, options?: Entity.UpdateOptions): Promise; updateEmbeddedEntity( embeddedName: string, updateData: unknown | unknown[], options?: Record, ): Promise { if (embeddedName === "OwnedItem") { preUpdateOwnedItem.bind(this)(updateData as DeepPartial | Array>); } return super.updateEmbeddedEntity(embeddedName, updateData, options); } } /** * If the item that is going to be created is equipable, set it to be non equipped and disable all ActiveEffects * contained in the item * @param itemData - The data of the item to be created */ function preCreateOwnedItem(itemData: DeepPartial | DeepPartial[]): void { const dataArray = itemData instanceof Array ? itemData : [itemData]; dataArray.forEach((data) => { if (data.data && "equipped" in data.data) { data.effects = data.effects?.map((effect) => ({ ...effect, disabled: true })); data.data.equipped = false; } }); console.log(itemData); } /** * If the equipped flag of one or more items changed, update all ActiveEffects originating from those items * accordingly. * @param updateData - The change that is going to be applied to the owned item(s) */ function preUpdateOwnedItem( this: T, updateData: DeepPartial | Array>, ): void { const dataArray = updateData instanceof Array ? updateData : [updateData]; dataArray.forEach((data) => { if (data.data && "equipped" in data.data) { const equipped = data.data.equipped; const origin = `Actor.${this.id}.OwnedItem.${data._id}`; const effects = this.effects .filter((e) => e.data.origin === origin) .map((e) => { const effectData = duplicate(e.data); effectData.disabled = !(equipped ?? true); return effectData; }); if (effects.length > 0) this.updateEmbeddedEntity("ActiveEffect", (effects as unknown) as Record); } }); } const oldTokenCreateEmbeddedEntity = ActorTokenHelpers.prototype.createEmbeddedEntity; const oldTokenUpdateEmbeddedEntity = ActorTokenHelpers.prototype.updateEmbeddedEntity; function tokenCreateEmbeddedEntity( this: T, embeddedName: "OwnedItem" | "ActiveEffect", data: Expanded extends DeepPartial | DeepPartial ? U | U[] : | DeepPartial | DeepPartial[] | DeepPartial | DeepPartial[], options?: Entity.UpdateOptions, ) { if (embeddedName === "OwnedItem") { const itemDataArray = data instanceof Array ? data.map((it) => expandObject(it) as DS4ItemData) : [expandObject(data as Record) as DS4ItemData]; preCreateOwnedItem.bind(this)(itemDataArray); // eslint-disable-next-line // @ts-ignore return oldTokenCreateEmbeddedEntity.bind(this)(embeddedName, itemDataArray, options); } // eslint-disable-next-line // @ts-ignore return oldTokenCreateEmbeddedEntity.bind(this)(embeddedName, data, options); } function tokenUpdateEmbeddedEntity( this: T, embeddedName: "OwnedItem" | "ActiveEffect", data: Expanded extends | (DeepPartial & { _id: string }) | (DeepPartial & { _id: string }) ? U | U[] : | (DeepPartial & { _id: string }) | (DeepPartial & { _id: string })[] | (DeepPartial & { _id: string }) | (DeepPartial & { _id: string })[], options?: Entity.UpdateOptions, ) { if (embeddedName === "OwnedItem") { const itemDataArray = data instanceof Array ? data.map((it) => expandObject(it) as DS4ItemData) : [expandObject(data as Record) as DS4ItemData]; preUpdateOwnedItem.bind(this)(itemDataArray); // eslint-disable-next-line // @ts-ignore return oldTokenUpdateEmbeddedEntity.bind(this)(embeddedName, itemDataArray, options); } // eslint-disable-next-line // @ts-ignore return oldTokenUpdateEmbeddedEntity.bind(this)(embeddedName, data, options); } ActorTokenHelpers.prototype.createEmbeddedEntity = tokenCreateEmbeddedEntity; ActorTokenHelpers.prototype.updateEmbeddedEntity = tokenUpdateEmbeddedEntity;