2021-01-11 00:55:49 +01:00
|
|
|
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-02-08 03:24:36 +01:00
|
|
|
import { DS4ItemData, ItemType } from "../item/item-data";
|
2021-01-26 03:55:18 +01:00
|
|
|
import { DS4ActorData } from "./actor-data";
|
2020-12-23 18:23:26 +01:00
|
|
|
|
2021-02-07 11:51:36 +01:00
|
|
|
/**
|
|
|
|
* The Actor class for DS4
|
|
|
|
*/
|
2021-01-31 19:57:14 +01:00
|
|
|
export class DS4Actor extends Actor<DS4ActorData, DS4Item> {
|
2021-01-25 01:09:51 +01:00
|
|
|
/** @override */
|
|
|
|
prepareData(): void {
|
2021-02-08 03:24:36 +01:00
|
|
|
this.data = duplicate(this._data) as DS4ActorData;
|
2021-01-25 01:09:51 +01:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2021-01-25 10:32:13 +01:00
|
|
|
/** @override */
|
|
|
|
prepareBaseData(): void {
|
|
|
|
const data = this.data;
|
|
|
|
const attributes = data.data.attributes;
|
|
|
|
Object.values(attributes).forEach(
|
|
|
|
(attribute: ModifiableData<number>) => (attribute.total = attribute.base + attribute.mod),
|
|
|
|
);
|
|
|
|
|
|
|
|
const traits = data.data.traits;
|
|
|
|
Object.values(traits).forEach((trait: ModifiableData<number>) => (trait.total = trait.base + trait.mod));
|
|
|
|
}
|
|
|
|
|
2021-01-25 01:09:51 +01:00
|
|
|
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.
|
|
|
|
*
|
2021-02-08 03:38:50 +01:00
|
|
|
* @param predicate - The predicate that ActiveEffectChanges need to satisfy in order to be applied
|
2021-01-25 01:09:51 +01:00
|
|
|
*/
|
|
|
|
applyActiveEffectsFiltered(predicate: (change: ActiveEffectChange) => boolean): void {
|
2021-02-08 03:24:36 +01:00
|
|
|
const overrides: Record<string, unknown> = {};
|
2021-01-25 01:09:51 +01:00
|
|
|
|
|
|
|
// Organize non-disabled effects by their application priority
|
2021-02-08 03:24:36 +01:00
|
|
|
const changes = this.effects.reduce(
|
|
|
|
(changes: Array<ActiveEffectChange & { effect: ActiveEffect<DS4Actor> }>, 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 };
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
[],
|
|
|
|
);
|
2021-01-25 01:09:51 +01:00
|
|
|
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
|
2021-02-08 03:24:36 +01:00
|
|
|
this.overrides = expandObject({ ...flattenObject(this.overrides ?? {}), ...overrides });
|
2021-01-25 01:09:51 +01:00
|
|
|
}
|
|
|
|
|
2020-12-23 16:52:20 +01:00
|
|
|
/** @override */
|
2020-12-23 18:23:26 +01:00
|
|
|
prepareDerivedData(): void {
|
2021-01-19 03:31:40 +01:00
|
|
|
this._prepareCombatValues();
|
2020-12-23 16:52:20 +01:00
|
|
|
}
|
2021-01-11 00:55:49 +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> {
|
2021-02-08 03:24:36 +01:00
|
|
|
return Object.keys(DS4.i18n.combatValues)
|
2021-01-25 10:32:13 +01:00
|
|
|
.map((combatValue) => `data.combatValues.${combatValue}.total`)
|
2021-01-25 01:09:51 +01:00
|
|
|
.concat("data.combatValues.hitPoints.max");
|
|
|
|
}
|
|
|
|
|
2021-01-11 00:55:49 +01:00
|
|
|
/**
|
|
|
|
* 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",
|
2021-01-13 17:20:25 +01:00
|
|
|
"spell",
|
2021-01-11 00:55:49 +01:00
|
|
|
"talent",
|
|
|
|
"racialAbility",
|
|
|
|
"language",
|
|
|
|
"alphabet",
|
|
|
|
];
|
|
|
|
case "creature":
|
2021-01-13 17:20:25 +01:00
|
|
|
return ["weapon", "armor", "shield", "trinket", "equipment", "spell", "specialCreatureAbility"];
|
2021-01-11 00:55:49 +01:00
|
|
|
default:
|
2021-01-18 19:03:08 +01:00
|
|
|
return [];
|
2021-01-11 00:55:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether or not the given item type can be owned by the actor.
|
2021-02-07 11:51:36 +01:00
|
|
|
* @param itemType - The item type to check
|
2021-01-11 00:55:49 +01:00
|
|
|
*/
|
|
|
|
canOwnItemType(itemType: ItemType): boolean {
|
|
|
|
return this.ownableItemTypes.includes(itemType);
|
|
|
|
}
|
2021-01-19 03:31:40 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2021-02-05 03:42:42 +01:00
|
|
|
.map((item) => {
|
|
|
|
if (item.data.type === "armor" || item.data.type === "shield") {
|
|
|
|
return item.data.data.equipped ? item.data.data.armorValue : 0;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
})
|
2021-01-19 03:31:40 +01:00
|
|
|
.reduce((a, b) => a + b, 0);
|
|
|
|
}
|
2021-01-23 23:51:21 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-01-26 03:55:18 +01:00
|
|
|
async modifyTokenAttribute(attribute: string, value: number, isDelta = false, isBar = true): Promise<this> {
|
2021-01-23 23:51:21 +01:00
|
|
|
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 */
|
2021-02-08 03:24:36 +01:00
|
|
|
// TODO(types): Improve typing once it's fixed in upstream (arrays can be passed!)
|
2021-01-25 01:09:51 +01:00
|
|
|
createEmbeddedEntity(
|
2021-02-08 03:24:36 +01:00
|
|
|
embeddedName: "OwnedItem",
|
|
|
|
data: DeepPartial<DS4ItemData>,
|
2021-01-25 01:09:51 +01:00
|
|
|
options?: Record<string, unknown>,
|
2021-02-08 03:24:36 +01:00
|
|
|
): Promise<DS4ItemData>;
|
|
|
|
createEmbeddedEntity(
|
|
|
|
embeddedName: "ActiveEffect",
|
|
|
|
data: DeepPartial<DS4ItemData>,
|
|
|
|
options?: Record<string, unknown>,
|
|
|
|
): Promise<ActiveEffect.Data>;
|
|
|
|
createEmbeddedEntity(
|
|
|
|
embeddedName: "OwnedItem" | "ActiveEffect",
|
|
|
|
data: DeepPartial<DS4ItemData> | DeepPartial<ActiveEffect.Data>,
|
|
|
|
options?: Record<string, unknown>,
|
|
|
|
): Promise<DS4ItemData> | Promise<ActiveEffect.Data> {
|
2021-01-25 01:09:51 +01:00
|
|
|
if (embeddedName === "OwnedItem") {
|
2021-02-08 03:24:36 +01:00
|
|
|
this._preCreateOwnedItem(data);
|
|
|
|
return super.createEmbeddedEntity(embeddedName, data, options);
|
2021-01-25 01:09:51 +01:00
|
|
|
}
|
2021-02-08 03:24:36 +01:00
|
|
|
return super.createEmbeddedEntity(embeddedName, data, options);
|
2021-01-25 01:09:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-02-08 03:24:36 +01:00
|
|
|
* If the item that is going to be created is equipable, set it to be non equipped and disable all ActiveEffects
|
2021-01-25 01:09:51 +01:00
|
|
|
* contained in the item
|
2021-02-08 03:38:50 +01:00
|
|
|
* @param itemData - The data of the item to be created
|
2021-01-25 01:09:51 +01:00
|
|
|
*/
|
2021-02-08 03:24:36 +01:00
|
|
|
protected _preCreateOwnedItem(itemData: DeepPartial<DS4ItemData>): void {
|
|
|
|
if (itemData.data && "equipped" in itemData.data) {
|
|
|
|
itemData.effects = itemData.effects?.map((effect) => ({ ...effect, disabled: true }));
|
|
|
|
itemData.data.equipped = false;
|
2021-01-25 01:09:51 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @override */
|
2021-02-08 03:24:36 +01:00
|
|
|
// TODO(types): Improve typing once it's fixed in upstream
|
|
|
|
updateEmbeddedEntity(embeddedName: string, data: unknown[], options?: Entity.UpdateOptions): Promise<unknown[]>;
|
|
|
|
updateEmbeddedEntity(embeddedName: string, data: unknown, options?: Entity.UpdateOptions): Promise<unknown>;
|
2021-01-25 01:09:51 +01:00
|
|
|
updateEmbeddedEntity(
|
|
|
|
embeddedName: string,
|
2021-02-08 03:24:36 +01:00
|
|
|
updateData: unknown | unknown[],
|
2021-01-25 01:09:51 +01:00
|
|
|
options?: Record<string, unknown>,
|
2021-02-08 03:24:36 +01:00
|
|
|
): Promise<unknown> {
|
2021-01-25 01:09:51 +01:00
|
|
|
if (embeddedName === "OwnedItem") {
|
2021-02-08 03:24:36 +01:00
|
|
|
this._preUpdateOwnedItem(updateData as DeepPartial<DS4ItemData> | Array<DeepPartial<DS4ItemData>>);
|
2021-01-25 01:09:51 +01:00
|
|
|
}
|
|
|
|
return super.updateEmbeddedEntity(embeddedName, updateData, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-02-08 03:24:36 +01:00
|
|
|
* If the equipped flag of one or more items changed, update all ActiveEffects originating from those items
|
|
|
|
* accordingly.
|
2021-02-08 03:38:50 +01:00
|
|
|
* @param updateData - The change that is going to be applied to the owned item(s)
|
2021-01-25 01:09:51 +01:00
|
|
|
*/
|
2021-02-08 03:24:36 +01:00
|
|
|
private _preUpdateOwnedItem(updateData: DeepPartial<DS4ItemData> | Array<DeepPartial<DS4ItemData>>): 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<string, unknown>);
|
|
|
|
}
|
|
|
|
});
|
2021-01-25 01:09:51 +01:00
|
|
|
}
|
2020-12-23 16:52:20 +01:00
|
|
|
}
|