From 85ec5faec2b230b3d73f568fc9cee629cebac37d Mon Sep 17 00:00:00 2001
From: Johannes Loher <johannes.loher@fg4f.de>
Date: Mon, 25 Jan 2021 01:09:51 +0100
Subject: [PATCH] implement basic active effects

---
 package-lock.json             |   2 +-
 src/module/actor/actor.ts     | 121 +++++++++++++++++++++++++++++++++-
 src/module/item/item-data.ts  |   2 +
 src/module/item/item-sheet.ts |   1 -
 4 files changed, 123 insertions(+), 3 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 70e4ebaa..77ef1fe0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2718,7 +2718,7 @@
             }
         },
         "foundry-pc-types": {
-            "version": "git+https://git.f3l.de/dungeonslayers/foundry-pc-types.git#ac45653fdec5fb935bf7db72889cb40cd6b80b20",
+            "version": "git+https://git.f3l.de/dungeonslayers/foundry-pc-types.git#3779bbbd30dbb04fa8f18615496882d6c66e1af4",
             "from": "git+https://git.f3l.de/dungeonslayers/foundry-pc-types.git#f3l-fixes",
             "dev": true,
             "requires": {
diff --git a/src/module/actor/actor.ts b/src/module/actor/actor.ts
index 270fee10..4f182ac1 100644
--- a/src/module/actor/actor.ts
+++ b/src/module/actor/actor.ts
@@ -1,9 +1,64 @@
 import { ModifiableData } from "../common/common-data";
+import { DS4 } from "../config";
 import { DS4Item } from "../item/item";
-import { DS4Armor, DS4ItemDataType, DS4Shield, ItemType } from "../item/item-data";
+import { DS4Armor, DS4EquippableItemDataType, DS4ItemDataType, DS4Shield, ItemType } from "../item/item-data";
 import { DS4ActorDataType } from "./actor-data";
 
+type DS4ActiveEffect = ActiveEffect<DS4ActorDataType, DS4ItemDataType, DS4Actor, DS4Item>;
+
 export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item> {
+    /** @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 });
+    }
+
     /** @override */
     prepareDerivedData(): void {
         const data = this.data;
@@ -18,6 +73,13 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
         this._prepareCombatValues();
     }
 
+    /** The list of properties that are dericed, in dot notation */
+    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.
      */
@@ -113,4 +175,61 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
         const allowed = Hooks.call("modifyTokenAttribute", { attribute, value, isDelta, isBar }, updates);
         return allowed !== false ? this.update(updates) : this;
     }
+
+    /** @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>);
+        }
+    }
 }
diff --git a/src/module/item/item-data.ts b/src/module/item/item-data.ts
index d7467f8a..00d7a696 100644
--- a/src/module/item/item-data.ts
+++ b/src/module/item/item-data.ts
@@ -16,6 +16,8 @@ export type DS4ItemDataType =
     | DS4Alphabet
     | DS4SpecialCreatureAbility;
 
+export type DS4EquippableItemDataType = DS4Weapon | DS4Armor | DS4Shield | DS4Trinket;
+
 // types
 
 interface DS4Weapon extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable {
diff --git a/src/module/item/item-sheet.ts b/src/module/item/item-sheet.ts
index 6464defc..bd143f31 100644
--- a/src/module/item/item-sheet.ts
+++ b/src/module/item/item-sheet.ts
@@ -34,7 +34,6 @@ export class DS4ItemSheet extends ItemSheet<DS4ItemDataType, DS4Item> {
             actor: this.item.actor,
             isPhysical: isDS4ItemDataTypePhysical(this.item.data.data),
         };
-        console.log(data);
         return data;
     }