ds4/src/module/actor/actor.ts

236 lines
9.7 KiB
TypeScript
Raw Normal View History

import { ModifiableData } from "../common/common-data";
2021-01-25 01:09:51 +01:00
import { DS4 } from "../config";
2020-12-29 00:33:43 +01:00
import { DS4Item } from "../item/item";
2021-01-25 01:09:51 +01:00
import { DS4Armor, DS4EquippableItemDataType, DS4ItemDataType, DS4Shield, ItemType } from "../item/item-data";
import { DS4ActorDataType } from "./actor-data";
2020-12-23 18:23:26 +01:00
2021-01-25 01:09:51 +01:00
type DS4ActiveEffect = ActiveEffect<DS4ActorDataType, DS4ItemDataType, DS4Actor, DS4Item>;
2020-12-29 00:33:43 +01:00
export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item> {
2021-01-25 01:09:51 +01:00
/** @override */
prepareData(): void {
this.data = duplicate(this._data);
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();
}
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 = {};
// Organize non-disabled effects by their application priority
const changes = this.effects.reduce((changes: Array<ActiveEffectChange & { effect: DS4ActiveEffect }>, e) => {
if (e.data.disabled) return changes;
return changes.concat(
e.data.changes.filter(predicate).map((c) => {
c = duplicate(c);
c.priority = c.priority ?? c.mode * 10;
return { ...c, 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 });
}
2020-12-23 16:52:20 +01:00
/** @override */
2020-12-23 18:23:26 +01:00
prepareDerivedData(): void {
2020-12-23 16:52:20 +01:00
const data = this.data;
const attributes = data.data.attributes;
Object.values(attributes).forEach(
(attribute: ModifiableData<number>) => (attribute.total = attribute.base + attribute.mod),
2020-12-23 16:52:20 +01:00
);
const traits = data.data.traits;
Object.values(traits).forEach((trait: ModifiableData<number>) => (trait.total = trait.base + trait.mod));
2020-12-23 16:52:20 +01:00
this._prepareCombatValues();
2020-12-23 16:52:20 +01:00
}
2021-01-25 01:33:13 +01:00
/** The list of properties that are derived from others, given in dot notation */
2021-01-25 01:09:51 +01:00
get derivedDataProperties(): Array<string> {
return Object.keys(DS4.combatValues)
.map((combatValue) => `data.combatValues.${combatValue}.base`)
.concat("data.combatValues.hitPoints.max");
}
/**
* The list of item types that can be owned by this actor.
*/
get ownableItemTypes(): Array<ItemType> {
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:
2021-01-18 19:03:08 +01:00
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<number>) => (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
.filter((item) => ["armor", "shield"].includes(item.type))
.map((item) => item.data.data as DS4Armor | DS4Shield)
.filter((itemData) => itemData.equipped)
.map((itemData) => itemData.armorValue)
.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<DS4Actor> {
const current = getProperty(this.data.data, attribute);
// Determine the updates to make to the actor data
let updates: Record<string, number>;
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;
}
2021-01-25 01:09:51 +01:00
/** @override */
createEmbeddedEntity(
embeddedName: string,
createData: Record<string, unknown> | Array<Record<string, unknown>>,
options?: Record<string, unknown>,
): Promise<this> {
if (embeddedName === "OwnedItem") {
this._preCreateOwnedItem((createData as unknown) as ItemData<DS4ItemDataType>);
}
return super.createEmbeddedEntity(embeddedName, createData, options);
}
/**
* If the item that is going to be created is equippable, set it to be non equipped and disable all ActiveEffects
* contained in the item
* @param itemData The data of the item to be created
*/
private _preCreateOwnedItem(itemData: ItemData<DS4ItemDataType>): void {
if ("equipped" in itemData.data) {
itemData.effects = itemData.effects.map((effect) => ({ ...effect, disabled: true }));
const equippableUpdateData = itemData as ItemData<DS4EquippableItemDataType>;
equippableUpdateData.data.equipped = false;
}
}
/** @override */
updateEmbeddedEntity(
embeddedName: string,
updateData: Record<string, unknown> | Array<Record<string, unknown>>,
options?: Record<string, unknown>,
): Promise<this> {
if (embeddedName === "OwnedItem") {
this._preUpdateOwnedItem(updateData as Partial<ItemData<DS4ItemDataType>>);
}
return super.updateEmbeddedEntity(embeddedName, updateData, options);
}
/**
* If the equipped flag of an item changed, update all ActiveEffects originating from that item accordingly.
* @param updateData The change that is going to be applied to the owned item
*/
private _preUpdateOwnedItem(updateData: Partial<ItemData<DS4ItemDataType>>): void {
if ("equipped" in updateData.data) {
const equippableUpdateData = updateData as Partial<ItemData<DS4EquippableItemDataType>>;
const origin = `Actor.${this.id}.OwnedItem.${updateData._id}`;
const effects = this.effects
.filter((e) => e.data.origin === origin)
.map((e) => {
const data = duplicate(e.data);
data.disabled = !equippableUpdateData.data.equipped;
return data;
});
if (effects.length > 0)
this.updateEmbeddedEntity("ActiveEffect", (effects as unknown) as Record<string, unknown>);
}
}
2020-12-23 16:52:20 +01:00
}