From 1b3d00a36ccc20961c8c89a0e94c330f0ec8fe17 Mon Sep 17 00:00:00 2001 From: Haxxer Date: Fri, 7 Jan 2022 16:30:36 +0000 Subject: [PATCH] Changelog, documentation, remove incompat check --- changelog.md | 1 - docs/api.md | 372 +++++---- scripts/api.js | 1924 +++++++++++++++++++++---------------------- scripts/module.js | 1 - scripts/settings.js | 17 - 5 files changed, 1167 insertions(+), 1148 deletions(-) diff --git a/changelog.md b/changelog.md index 786bea1c..928e4eb5 100644 --- a/changelog.md +++ b/changelog.md @@ -4,7 +4,6 @@ - Added API endpoints: - `ItemPiles.API.getItemPileItemTypeFilters(TokenDocument|Actor)` - Returns the item type filters for a given item pile - `ItemPiles.API.getItemPileItems(TokenDocument|Actor, Array|Boolean)` - Returns the items the item pile contains and can transfer -- Added warning on startup for module incompatibilities - Updated japanese localization - Fixed item piles not respecting item type filters - Fixed issue with `ItemPiles.API.turnTokenIntoItemPile` not actually turning the token into an item pile diff --git a/docs/api.md b/docs/api.md index 8f4447ba..16babfe4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -20,7 +20,7 @@ ## Functions -### Settings +### Setting Methods
setActorClassType(inClassType)Promise
@@ -39,7 +39,7 @@

Sets the filters for item types eligible for interaction within this system

-### Item Piles +### Item Pile Methods
createItemPile(position, [items], [pileActorName])Promise

Creates the default item pile token at a location.

@@ -108,28 +108,28 @@

Causes all connected users to re-render a specific pile's inventory UI

-### Items & Attributes +### Item and Attribute Methods -
transferItems(source, target, items, [itemTypeFilters])Promise.<object>
-

Transfers items from the source to the target, subtracting a number of quantity from the source's item and adding it to the target's item, deleting items from the source if their quantity reaches 0

+
addItems(target, items, [itemTypeFilters])Promise.<array>
+

Adds item to an actor, increasing item quantities if matches were found

removeItems(target, items, [itemTypeFilters])Promise.<array>

Subtracts the quantity of items on an actor. If the quantity of an item reaches 0, the item is removed from the actor.

-
addItems(target, items, [itemTypeFilters])Promise.<array>
-

Adds item to an actor, increasing item quantities if matches were found

+
transferItems(source, target, items, [itemTypeFilters])Promise.<object>
+

Transfers items from the source to the target, subtracting a number of quantity from the source's item and adding it to the target's item, deleting items from the source if their quantity reaches 0

transferAllItems(source, target, [itemTypeFilters])Promise.<array>

Transfers all items between the source and the target.

-
transferAttributes(source, target, attributes)Promise.<object>
-

Transfers a set quantity of an attribute from a source to a target, removing it or subtracting from the source and adds it the target

+
addAttributes(target, attributes)Promise.<object>
+

Adds to attributes on an actor

removeAttributes(target, attributes)Promise.<object>

Subtracts attributes on the target

-
addAttributes(target, attributes)Promise.<object>
-

Adds to attributes on an actor

+
transferAttributes(source, target, attributes)Promise.<object>
+

Transfers a set quantity of an attribute from a source to a target, removing it or subtracting from the source and adds it the target

transferAllAttributes(source, target)Promise.<object>

Transfers all dynamic attributes from a source to a target, removing it or subtracting from the source and adding them to the target

@@ -138,7 +138,7 @@

Transfers all items and attributes between the source and the target.

-### Utility +### Utility Methods
rerenderTokenHud()Promise

Causes every user's token HUD to rerender

@@ -151,196 +151,97 @@ or not, returning the type if it is NOT allowed.

-## ItemPiles.API.ACTOR\_CLASS\_TYPE ⇒ string +## ACTOR\_CLASS\_TYPE ⇒ string The actor class type used for the original item pile actor in this system -## ItemPiles.API.DYNAMIC\_ATTRIBUTES ⇒ array +## DYNAMIC\_ATTRIBUTES ⇒ array The attributes used to track dynamic attributes in this system -## ItemPiles.API.ITEM\_QUANTITY\_ATTRIBUTE ⇒ string +## ITEM\_QUANTITY\_ATTRIBUTE ⇒ string The attribute used to track the quantity of items in this system -## ItemPiles.API.ITEM\_TYPE\_ATTRIBUTE ⇒ string +## ITEM\_TYPE\_ATTRIBUTE ⇒ string The attribute used to track the item type in this system -## ItemPiles.API.ITEM\_TYPE\_FILTERS ⇒ Array +## ITEM\_TYPE\_FILTERS ⇒ Array The filters for item types eligible for interaction within this system -## ItemPiles.API.setActorClassType(inClassType) ⇒ Promise +## setActorClassType(inClassType) ⇒ Promise Sets the actor class type used for the original item pile actor in this system + | Param | Type | | --- | --- | | inClassType | string | -## ItemPiles.API.setDynamicAttributes(inAttributes) ⇒ Promise +## setDynamicAttributes(inAttributes) ⇒ Promise Sets the attributes used to track dynamic attributes in this system + | Param | Type | | --- | --- | | inAttributes | array | -## ItemPiles.API.setItemQuantityAttribute(inAttribute) ⇒ Promise +## setItemQuantityAttribute(inAttribute) ⇒ Promise Sets the inAttribute used to track the quantity of items in this system + | Param | Type | | --- | --- | | inAttribute | string | -## ItemPiles.API.setItemTypeAttribute(inAttribute) ⇒ string +## setItemTypeAttribute(inAttribute) ⇒ string Sets the attribute used to track the item type in this system + | Param | Type | | --- | --- | | inAttribute | string | -## ItemPiles.API.setItemTypeFilters(inFilters) ⇒ Promise +## setItemTypeFilters(inFilters) ⇒ Promise Sets the filters for item types eligible for interaction within this system + | Param | Type | | --- | --- | | inFilters | string/array | -## ItemPiles.API.createItemPile(position, [items], [pileActorName]) ⇒ Promise +## createItemPile(position, [items], [pileActorName]) ⇒ Promise Creates the default item pile token at a location. + | Param | Type | Default | Description | | --- | --- | --- | --- | | position | object | | The position to create the item pile at | | [items] | array/boolean | false | Any items to create on the item pile | | [pileActorName] | string/boolean | false | Whether to use an existing item pile actor as the basis of this new token | - - -## ItemPiles.API.transferItems(source, target, items, [itemTypeFilters]) ⇒ Promise.<object> -Transfers items from the source to the target, subtracting a number of quantity from the source's item and adding it to the target's item, deleting items from the source if their quantity reaches 0 -**Returns**: Promise.<object> - An object containing a key value pair for each item added to the target, key being item ID, value being quantities added - -| Param | Type | Default | Description | -| --- | --- | --- | --- | -| source | Actor/Token/TokenDocument | | The source to transfer the items from | -| target | Actor/Token/TokenDocument | | The target to transfer the items to | -| items | array | | An array of objects each containing the item id (key "_id") and the quantity to transfer (key "quantity") | -| [itemTypeFilters] | array/boolean | false | Array of item types disallowed - will default to module settings if none provided | - - - -## ItemPiles.API.removeItems(target, items, [itemTypeFilters]) ⇒ Promise.<array> -Subtracts the quantity of items on an actor. If the quantity of an item reaches 0, the item is removed from the actor. -**Returns**: Promise.<array> - An array containing the objects of each item that was removed, with their quantities set to the number removed - -| Param | Type | Default | Description | -| --- | --- | --- | --- | -| target | Actor/Token/TokenDocument | | The target to remove a items from | -| items | array | | An array of objects each containing the item id (key "_id") and the quantity to remove (key "quantity"), or an array of IDs to remove all quantities of | -| [itemTypeFilters] | array/boolean | false | Array of item types disallowed - will default to module settings if none provided | - - - -## ItemPiles.API.addItems(target, items, [itemTypeFilters]) ⇒ Promise.<array> -Adds item to an actor, increasing item quantities if matches were found -**Returns**: Promise.<array> - An array containing each item added as an object, with their quantities updated to match the new amounts - -| Param | Type | Default | Description | -| --- | --- | --- | --- | -| target | Actor/TokenDocument/Token | | The target to add an item to | -| items | array | | An array of item objects | -| [itemTypeFilters] | array/boolean | false | Array of item types disallowed - will default to module settings if none provided | - - - -## ItemPiles.API.transferAllItems(source, target, [itemTypeFilters]) ⇒ Promise.<array> -Transfers all items between the source and the target. -**Returns**: Promise.<array> - An array containing all of the items that were transferred to the target - -| Param | Type | Default | Description | -| --- | --- | --- | --- | -| source | Actor/Token/TokenDocument | | The actor to transfer all items from | -| target | Actor/Token/TokenDocument | | The actor to receive all the items | -| [itemTypeFilters] | array/boolean | false | Array of item types disallowed - will default to module settings if none provided | - - - -## ItemPiles.API.transferAttributes(source, target, attributes) ⇒ Promise.<object> -Transfers a set quantity of an attribute from a source to a target, removing it or subtracting from the source and adds it the target -**Returns**: Promise.<object> - An object containing a key value pair of each attribute transferred, the key being the attribute path and its value being the quantity that was transferred - -| Param | Type | Description | -| --- | --- | --- | -| source | Actor/Token/TokenDocument | The source to transfer the attribute from | -| target | Actor/Token/TokenDocument | The target to transfer the attribute to | -| attributes | array/object | This can be either an array of attributes to transfer (to transfer all of a given attribute), or an object with each key being an attribute path, and its value being the quantity to transfer | - - - -## ItemPiles.API.removeAttributes(target, attributes) ⇒ Promise.<object> -Subtracts attributes on the target -**Returns**: Promise.<object> - Returns an array containing a key value pair of the attribute path and the quantity of that attribute that was removed - -| Param | Type | Description | -| --- | --- | --- | -| target | Token/TokenDocument | The target whose attributes will be subtracted from | -| attributes | array/object | This can be either an array of attributes to subtract (to zero out a given attribute), or an object with each key being an attribute path, and its value being the quantity to subtract | - - - -## ItemPiles.API.addAttributes(target, attributes) ⇒ Promise.<object> -Adds to attributes on an actor -**Returns**: Promise.<object> - Returns an array containing a key value pair of the attribute path and the quantity of that attribute that was removed - -| Param | Type | Description | -| --- | --- | --- | -| target | Actor/Token/TokenDocument | The target whose attribute will have a set quantity added to it | -| attributes | object | An object with each key being an attribute path, and its value being the quantity to add | - - - -## ItemPiles.API.transferAllAttributes(source, target) ⇒ Promise.<object> -Transfers all dynamic attributes from a source to a target, removing it or subtracting from the source and adding them to the target -**Returns**: Promise.<object> - An object containing a key value pair of each attribute transferred, the key being the attribute path and its value being the quantity that was transferred - -| Param | Type | Description | -| --- | --- | --- | -| source | Actor/Token/TokenDocument | The source to transfer the attributes from | -| target | Actor/Token/TokenDocument | The target to transfer the attributes to | - - - -## ItemPiles.API.transferEverything(source, target, [itemTypeFilters]) ⇒ Promise.<object> -Transfers all items and attributes between the source and the target. -**Returns**: Promise.<object> - An object containing all items and attributes transferred to the target - -| Param | Type | Default | Description | -| --- | --- | --- | --- | -| source | Actor/Token/TokenDocument | | The actor to transfer all items and attributes from | -| target | Actor/Token/TokenDocument | | The actor to receive all the items and attributes | -| [itemTypeFilters] | array/boolean | false | Array of item types disallowed - will default to module settings if none provided | - -## ItemPiles.API.turnTokenIntoItemPile(target, pileSettings, tokenSettings) ⇒ Promise.<string> +## turnTokenIntoItemPile(target, pileSettings, tokenSettings) ⇒ Promise.<string> Turns a token and its actor into an item pile -**Returns**: Promise.<string> - The uuid of the target after it was turned into an item pile + +**Returns**: Promise.<string> - The uuid of the target after it was turned into an item pile | Param | Type | Description | | --- | --- | --- | @@ -350,24 +251,22 @@ Turns a token and its actor into an item pile -## ItemPiles.API.revertTokenFromItemPile(target, tokenSettings) ⇒ Promise.<string> +## revertTokenFromItemPile(target, tokenSettings) ⇒ Promise.<string> Reverts a token from an item pile into a normal token and actor -**Returns**: Promise.<string> - The uuid of the target after it was reverted from an item pile + +**Returns**: Promise.<string> - The uuid of the target after it was reverted from an item pile | Param | Type | Description | | --- | --- | --- | | target | Token/TokenDocument | The target to be reverted from an item pile | | tokenSettings | object | Overriding settings that will update the token | - - -## ItemPiles.API.rerenderTokenHud() ⇒ Promise -Causes every user's token HUD to rerender -## ItemPiles.API.openItemPile(target, [interactingToken]) ⇒ Promise +## openItemPile(target, [interactingToken]) ⇒ Promise Opens a pile if it is enabled and a container + | Param | Type | Default | | --- | --- | --- | | target | Token/TokenDocument | | @@ -375,9 +274,10 @@ Opens a pile if it is enabled and a container -## ItemPiles.API.closeItemPile(target, [interactingToken]) ⇒ Promise +## closeItemPile(target, [interactingToken]) ⇒ Promise Closes a pile if it is enabled and a container + | Param | Type | Default | Description | | --- | --- | --- | --- | | target | Token/TokenDocument | | Target pile to close | @@ -385,9 +285,10 @@ Closes a pile if it is enabled and a container -## ItemPiles.API.toggleItemPileClosed(target, [interactingToken]) ⇒ Promise +## toggleItemPileClosed(target, [interactingToken]) ⇒ Promise Toggles a pile's closed state if it is enabled and a container + | Param | Type | Default | Description | | --- | --- | --- | --- | | target | Token/TokenDocument | | Target pile to open or close | @@ -395,9 +296,10 @@ Toggles a pile's closed state if it is enabled and a container -## ItemPiles.API.lockItemPile(target, [interactingToken]) ⇒ Promise +## lockItemPile(target, [interactingToken]) ⇒ Promise Locks a pile if it is enabled and a container + | Param | Type | Default | Description | | --- | --- | --- | --- | | target | Token/TokenDocument | | Target pile to lock | @@ -405,9 +307,10 @@ Locks a pile if it is enabled and a container -## ItemPiles.API.unlockItemPile(target, [interactingToken]) ⇒ Promise +## unlockItemPile(target, [interactingToken]) ⇒ Promise Unlocks a pile if it is enabled and a container + | Param | Type | Default | Description | | --- | --- | --- | --- | | target | Token/TokenDocument | | Target pile to unlock | @@ -415,9 +318,10 @@ Unlocks a pile if it is enabled and a container -## ItemPiles.API.toggleItemPileLocked(target, [interactingToken]) ⇒ Promise +## toggleItemPileLocked(target, [interactingToken]) ⇒ Promise Toggles a pile's locked state if it is enabled and a container + | Param | Type | Default | Description | | --- | --- | --- | --- | | target | Token/TokenDocument | | Target pile to lock or unlock | @@ -425,45 +329,50 @@ Toggles a pile's locked state if it is enabled and a container -## ItemPiles.API.rattleItemPile(target) ⇒ Promise.<boolean> +## rattleItemPile(target) ⇒ Promise.<boolean> Causes the item pile to play a sound as it was attempted to be opened, but was locked + | Param | Type | | --- | --- | | target | Token/TokenDocument | -## ItemPiles.API.isItemPileLocked(target) ⇒ boolean +## isItemPileLocked(target) ⇒ boolean Whether an item pile is locked. If it is not enabled or not a container, it is always false. + | Param | Type | | --- | --- | | target | Token/TokenDocument | -## ItemPiles.API.isItemPileClosed(target) ⇒ boolean +## isItemPileClosed(target) ⇒ boolean Whether an item pile is closed. If it is not enabled or not a container, it is always false. + | Param | Type | | --- | --- | | target | Token/TokenDocument | -## ItemPiles.API.isItemPileContainer(target) ⇒ boolean +## isItemPileContainer(target) ⇒ boolean Whether an item pile is a container. If it is not enabled, it is always false. + | Param | Type | | --- | --- | | target | Token/TokenDocument | -## ItemPiles.API.updateItemPile(target, newData, [interactingToken], [tokenSettings]) ⇒ Promise +## updateItemPile(target, newData, [interactingToken], [tokenSettings]) ⇒ Promise Updates a pile with new data. + | Param | Type | Default | | --- | --- | --- | | target | Token/TokenDocument | | @@ -473,56 +382,50 @@ Updates a pile with new data. -## ItemPiles.API.deleteItemPile(target) ⇒ Promise +## deleteItemPile(target) ⇒ Promise Deletes a pile, calling the relevant hooks. + | Param | Type | | --- | --- | | target | Token/TokenDocument | - - -## ItemPiles.API.isItemTypeDisallowed(item, [itemTypeFilters]) ⇒ boolean/string -Checks whether an item (or item data) is of a type that is not allowed. If an array whether that type is allowed -or not, returning the type if it is NOT allowed. - -| Param | Type | Default | -| --- | --- | --- | -| item | Item/Object | | -| [itemTypeFilters] | array/boolean | false | - -## ItemPiles.API.isValidItemPile(document) ⇒ boolean +## isValidItemPile(document) ⇒ boolean Whether a given document is a valid pile or not + | Param | Type | | --- | --- | | document | TokenDocument \| Actor | -## ItemPiles.API.isItemPileEmpty(target) ⇒ boolean +## isItemPileEmpty(target) ⇒ boolean Whether the item pile is empty + | Param | Type | | --- | --- | | target | TokenDocument \| Actor | -## ItemPiles.API.getItemPileItemTypeFilters(target) ⇒ Array +## getItemPileItemTypeFilters(target) ⇒ Array Returns the item type filters for a given item pile + | Param | | --- | | target | -## ItemPiles.API.getItemPileItems(target, [itemTypeFilters]) ⇒ Array +## getItemPileItems(target, [itemTypeFilters]) ⇒ Array Returns the items this item pile can transfer + | Param | Type | Default | Description | | --- | --- | --- | --- | | target | TokenDocument \| Actor | | | @@ -530,29 +433,164 @@ Returns the items this item pile can transfer -## ItemPiles.API.getItemPileAttributes(target) ⇒ array +## getItemPileAttributes(target) ⇒ array Returns the attributes this item pile can transfer + | Param | Type | | --- | --- | | target | TokenDocument \| Actor | -## ItemPiles.API.refreshItemPile(target) ⇒ Promise +## refreshItemPile(target) ⇒ Promise Refreshes the target image of an item pile, ensuring it remains in sync + | Param | | --- | | target | -## ItemPiles.API.rerenderItemPileInventoryApplication(inPileUuid, [deleted]) ⇒ Promise +## rerenderItemPileInventoryApplication(inPileUuid, [deleted]) ⇒ Promise Causes all connected users to re-render a specific pile's inventory UI + | Param | Type | Default | Description | | --- | --- | --- | --- | | inPileUuid | string | | The uuid of the pile to be re-rendered | | [deleted] | boolean | false | Whether the pile was deleted as a part of this re-render | + + +## addItems(target, items, [itemTypeFilters]) ⇒ Promise.<array> +Adds item to an actor, increasing item quantities if matches were found + +**Returns**: Promise.<array> - An array containing each item added as an object, with their quantities updated to match the new amounts + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| target | Actor/TokenDocument/Token | | The target to add an item to | +| items | array | | An array of item objects | +| [itemTypeFilters] | array/boolean | false | Array of item types disallowed - will default to module settings if none provided | + + + +## removeItems(target, items, [itemTypeFilters]) ⇒ Promise.<array> +Subtracts the quantity of items on an actor. If the quantity of an item reaches 0, the item is removed from the actor. + +**Returns**: Promise.<array> - An array containing the objects of each item that was removed, with their quantities set to the number removed + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| target | Actor/Token/TokenDocument | | The target to remove a items from | +| items | array | | An array of objects each containing the item id (key "_id") and the quantity to remove (key "quantity"), or an array of IDs to remove all quantities of | +| [itemTypeFilters] | array/boolean | false | Array of item types disallowed - will default to module settings if none provided | + + + +## transferItems(source, target, items, [itemTypeFilters]) ⇒ Promise.<object> +Transfers items from the source to the target, subtracting a number of quantity from the source's item and adding it to the target's item, deleting items from the source if their quantity reaches 0 + +**Returns**: Promise.<object> - An object containing a key value pair for each item added to the target, key being item ID, value being quantities added + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| source | Actor/Token/TokenDocument | | The source to transfer the items from | +| target | Actor/Token/TokenDocument | | The target to transfer the items to | +| items | array | | An array of objects each containing the item id (key "_id") and the quantity to transfer (key "quantity") | +| [itemTypeFilters] | array/boolean | false | Array of item types disallowed - will default to module settings if none provided | + + + +## transferAllItems(source, target, [itemTypeFilters]) ⇒ Promise.<array> +Transfers all items between the source and the target. + +**Returns**: Promise.<array> - An array containing all of the items that were transferred to the target + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| source | Actor/Token/TokenDocument | | The actor to transfer all items from | +| target | Actor/Token/TokenDocument | | The actor to receive all the items | +| [itemTypeFilters] | array/boolean | false | Array of item types disallowed - will default to module settings if none provided | + + + +## addAttributes(target, attributes) ⇒ Promise.<object> +Adds to attributes on an actor + +**Returns**: Promise.<object> - Returns an array containing a key value pair of the attribute path and the quantity of that attribute that was removed + +| Param | Type | Description | +| --- | --- | --- | +| target | Actor/Token/TokenDocument | The target whose attribute will have a set quantity added to it | +| attributes | object | An object with each key being an attribute path, and its value being the quantity to add | + + + +## removeAttributes(target, attributes) ⇒ Promise.<object> +Subtracts attributes on the target + +**Returns**: Promise.<object> - Returns an array containing a key value pair of the attribute path and the quantity of that attribute that was removed + +| Param | Type | Description | +| --- | --- | --- | +| target | Token/TokenDocument | The target whose attributes will be subtracted from | +| attributes | array/object | This can be either an array of attributes to subtract (to zero out a given attribute), or an object with each key being an attribute path, and its value being the quantity to subtract | + + + +## transferAttributes(source, target, attributes) ⇒ Promise.<object> +Transfers a set quantity of an attribute from a source to a target, removing it or subtracting from the source and adds it the target + +**Returns**: Promise.<object> - An object containing a key value pair of each attribute transferred, the key being the attribute path and its value being the quantity that was transferred + +| Param | Type | Description | +| --- | --- | --- | +| source | Actor/Token/TokenDocument | The source to transfer the attribute from | +| target | Actor/Token/TokenDocument | The target to transfer the attribute to | +| attributes | array/object | This can be either an array of attributes to transfer (to transfer all of a given attribute), or an object with each key being an attribute path, and its value being the quantity to transfer | + + + +## transferAllAttributes(source, target) ⇒ Promise.<object> +Transfers all dynamic attributes from a source to a target, removing it or subtracting from the source and adding them to the target + +**Returns**: Promise.<object> - An object containing a key value pair of each attribute transferred, the key being the attribute path and its value being the quantity that was transferred + +| Param | Type | Description | +| --- | --- | --- | +| source | Actor/Token/TokenDocument | The source to transfer the attributes from | +| target | Actor/Token/TokenDocument | The target to transfer the attributes to | + + + +## transferEverything(source, target, [itemTypeFilters]) ⇒ Promise.<object> +Transfers all items and attributes between the source and the target. + +**Returns**: Promise.<object> - An object containing all items and attributes transferred to the target + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| source | Actor/Token/TokenDocument | | The actor to transfer all items and attributes from | +| target | Actor/Token/TokenDocument | | The actor to receive all the items and attributes | +| [itemTypeFilters] | array/boolean | false | Array of item types disallowed - will default to module settings if none provided | + + + +## rerenderTokenHud() ⇒ Promise +Causes every user's token HUD to rerender + + + +## isItemTypeDisallowed(item, [itemTypeFilters]) ⇒ boolean/string +Checks whether an item (or item data) is of a type that is not allowed. If an array whether that type is allowed +or not, returning the type if it is NOT allowed. + + +| Param | Type | Default | +| --- | --- | --- | +| item | Item/Object | | +| [itemTypeFilters] | array/boolean | false | + diff --git a/scripts/api.js b/scripts/api.js index bb6075de..3f764e53 100644 --- a/scripts/api.js +++ b/scripts/api.js @@ -257,720 +257,822 @@ export default class API { } /** - * Transfers items from the source to the target, subtracting a number of quantity from the source's item and adding it to the target's item, deleting items from the source if their quantity reaches 0 + * Turns a token and its actor into an item pile * - * @param {Actor/Token/TokenDocument} source The source to transfer the items from - * @param {Actor/Token/TokenDocument} target The target to transfer the items to - * @param {array} items An array of objects each containing the item id (key "_id") and the quantity to transfer (key "quantity") - * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to module settings if none provided + * @param {Token/TokenDocument} target The target to be turned into an item pile + * @param {object} pileSettings Overriding settings to be put on the item pile's settings + * @param {object} tokenSettings Overriding settings that will update the token * - * @returns {Promise} An object containing a key value pair for each item added to the target, key being item ID, value being quantities added + * @return {Promise} The uuid of the target after it was turned into an item pile */ - static async transferItems(source, target, items, { itemTypeFilters = false } = {}) { - - const hookResult = Hooks.call(HOOKS.ITEM.PRE_TRANSFER, source, target, items); + static async turnTokenIntoItemPile(target, { pileSettings = {}, tokenSettings = {} } = {}) { + const hookResult = Hooks.call(HOOKS.PILE.PRE_TURN_INTO, target, pileSettings, tokenSettings); if (hookResult === false) return; - - const sourceUuid = lib.getUuid(source); - if (!sourceUuid) throw lib.custom_error(`TransferItems | Could not determine the UUID, please provide a valid source`, true) - - if (itemTypeFilters) { - itemTypeFilters.forEach(filter => { - if (typeof filter !== "string") throw lib.custom_error(`TransferItems | entries in the itemTypeFilters must be of type string`); - }) - } - - const sourceActorItems = API.getItemPileItems(source); - - items.forEach(item => { - const actorItem = sourceActorItems.find(actorItem => actorItem.id === item._id); - if (!actorItem) { - throw lib.custom_error(`TransferItems | Could not find item with id "${item._id}" on source "${sourceUuid}"`, true) - } - const disallowedType = API.isItemTypeDisallowed(actorItem, itemTypeFilters); - if (disallowedType) { - throw lib.custom_error(`TransferItems | Could not transfer item of type "${disallowedType}"`, true) - } - }); - const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`TransferItems | Could not determine the UUID, please provide a valid target`, true) - - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.TRANSFER_ITEMS, sourceUuid, targetUuid, items, { itemTypeFilters }); - + if (!targetUuid) throw lib.custom_error(`TurnIntoItemPile | Could not determine the UUID, please provide a valid target`, true) + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.TURN_INTO_PILE, targetUuid, pileSettings, tokenSettings); } /** * @private */ - static async _transferItems(sourceUuid, targetUuid, items, { itemTypeFilters = false, isEverything = false } = {}) { - - const itemsRemoved = await API._removeItems(sourceUuid, items, { itemTypeFilters, isTransfer: true }); - - const itemsAdded = await API._addItems(targetUuid, itemsRemoved, { itemTypeFilters, isTransfer: true }); - - if (!isEverything) { + static async _turnTokenIntoItemPile(targetUuid, pileSettings = {}, tokenSettings = {}) { - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ITEM.TRANSFER, sourceUuid, targetUuid, itemsAdded); + const target = await fromUuid(targetUuid); - const macroData = { - action: "transferItems", - source: sourceUuid, - target: targetUuid, - itemsAdded: itemsAdded - }; - await API._executeItemPileMacro(sourceUuid, macroData); - await API._executeItemPileMacro(targetUuid, macroData); + const existingPileSettings = foundry.utils.mergeObject(CONSTANTS.PILE_DEFAULTS, lib.getItemPileData(target)); + const newPileSettings = foundry.utils.mergeObject(existingPileSettings, pileSettings); + newPileSettings.enabled = true; - const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(sourceUuid); - await API.rerenderItemPileInventoryApplication(sourceUuid, shouldBeDeleted); - await API.rerenderItemPileInventoryApplication(targetUuid); + await API._updateItemPile(targetUuid, newPileSettings, { tokenSettings }); - if (shouldBeDeleted) { - await API._deleteItemPile(sourceUuid); - } + setTimeout(API.rerenderTokenHud, 100); - } + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.PILE.TURN_INTO, targetUuid, newPileSettings); - return itemsAdded; + return targetUuid; } /** - * Subtracts the quantity of items on an actor. If the quantity of an item reaches 0, the item is removed from the actor. + * Reverts a token from an item pile into a normal token and actor * - * @param {Actor/Token/TokenDocument} target The target to remove a items from - * @param {array} items An array of objects each containing the item id (key "_id") and the quantity to remove (key "quantity"), or an array of IDs to remove all quantities of - * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to module settings if none provided + * @param {Token/TokenDocument} target The target to be reverted from an item pile + * @param {object} tokenSettings Overriding settings that will update the token * - * @returns {Promise} An array containing the objects of each item that was removed, with their quantities set to the number removed + * @return {Promise} The uuid of the target after it was reverted from an item pile */ - static async removeItems(target, items, { itemTypeFilters = false } = {}) { - - const hookResult = Hooks.call(HOOKS.ITEM.PRE_REMOVE, target, items); + static async revertTokenFromItemPile(target, { tokenSettings = {} } = {}) { + const hookResult = Hooks.call(HOOKS.PILE.PRE_REVERT_FROM, target, tokenSettings); if (hookResult === false) return; - const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`RemoveItems | Could not determine the UUID, please provide a valid target`, true); - - if (itemTypeFilters) { - itemTypeFilters.forEach(filter => { - if (typeof filter !== "string") throw lib.custom_error(`RemoveItems | entries in the itemTypeFilters must be of type string`); - }) - } - - const targetActorItems = API.getItemPileItems(target); - - items.forEach(item => { - const itemId = typeof item === "string" ? item : item._id; - const actorItem = targetActorItems.find(actorItem => actorItem.id === itemId); - if (!actorItem) { - throw lib.custom_error(`RemoveItems | Could not find item with id "${itemId}" on target "${targetUuid}"`, true) - } - const disallowedType = API.isItemTypeDisallowed(actorItem, itemTypeFilters); - if (disallowedType) { - throw lib.custom_error(`RemoveItems | Could not transfer item of type "${disallowedType}"`, true) - } - }); - - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.REMOVE_ITEMS, targetUuid, items); + if (!targetUuid) throw lib.custom_error(`RevertFromItemPile | Could not determine the UUID, please provide a valid target`, true) + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.REVERT_FROM_PILE, targetUuid, tokenSettings); } /** * @private */ - static async _removeItems(targetUuid, items, { isTransfer = false } = {}) { + static async _revertTokenFromItemPile(targetUuid, tokenSettings) { const target = await fromUuid(targetUuid); - const targetActor = target instanceof TokenDocument - ? target.actor - : target; - - const itemsRemoved = []; - const itemsToUpdate = []; - const itemsToDelete = []; - for (const item of items) { - const itemId = typeof item === "string" ? item : item._id; - - const actorItem = targetActor.items.get(itemId); - const removedItem = actorItem.toObject(); - - const currentQuantity = getProperty(actorItem.data, API.ITEM_QUANTITY_ATTRIBUTE); - - const quantityToRemove = getProperty(item, API.ITEM_QUANTITY_ATTRIBUTE) ?? item.quantity ?? currentQuantity; + const pileSettings = foundry.utils.mergeObject(CONSTANTS.PILE_DEFAULTS, lib.getItemPileData(target)); - const newQuantity = Math.max(0, currentQuantity - quantityToRemove); + pileSettings.enabled = false; - if (newQuantity >= 1) { - setProperty(removedItem, API.ITEM_QUANTITY_ATTRIBUTE, quantityToRemove); - itemsToUpdate.push({ _id: actorItem.id, [API.ITEM_QUANTITY_ATTRIBUTE]: newQuantity }); - } else { - setProperty(removedItem, API.ITEM_QUANTITY_ATTRIBUTE, currentQuantity); - itemsToDelete.push(actorItem.id); - } + await API._updateItemPile(targetUuid, pileSettings, { tokenSettings }); - itemsRemoved.push(removedItem); + setTimeout(API.rerenderTokenHud, 100); + if (target instanceof TokenDocument) { + await API.rerenderItemPileInventoryApplication(targetUuid); } - await targetActor.updateEmbeddedDocuments("Item", itemsToUpdate); - await targetActor.deleteEmbeddedDocuments("Item", itemsToDelete); - - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ITEM.REMOVE, targetUuid, itemsRemoved); - - if (!isTransfer) { - - const macroData = { - action: "removeItems", - target: targetUuid, - items: itemsRemoved - }; - - await API._executeItemPileMacro(targetUuid, macroData); - - const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(targetUuid); + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.PILE.REVERT_FROM, targetUuid); - await API.rerenderItemPileInventoryApplication(targetUuid, shouldBeDeleted); + return targetUuid; - if (shouldBeDeleted) { - await API._deleteItemPile(targetUuid); - } + } + /** + * Opens a pile if it is enabled and a container + * + * @param {Token/TokenDocument} target + * @param {Token/TokenDocument/boolean} [interactingToken=false] + * + * @return {Promise} + */ + static async openItemPile(target, interactingToken = false) { + const data = lib.getItemPileData(target); + if (!data?.enabled || !data?.isContainer) return false; + const wasLocked = data.locked; + data.closed = false; + data.locked = false; + if (wasLocked) { + const hookResult = Hooks.call(HOOKS.PILE.PRE_UNLOCK, target, data, interactingToken); + if (hookResult === false) return; } - - return itemsRemoved; - + const hookResult = Hooks.call(HOOKS.PILE.PRE_OPEN, target, data, interactingToken); + if (hookResult === false) return; + if (data.openSound) { + AudioHelper.play({ src: data.openSound }) + } + return API.updateItemPile(target, data, { interactingToken }); } /** - * Adds item to an actor, increasing item quantities if matches were found + * Closes a pile if it is enabled and a container * - * @param {Actor/TokenDocument/Token} target The target to add an item to - * @param {array} items An array of item objects - * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to module settings if none provided + * @param {Token/TokenDocument} target Target pile to close + * @param {Token/TokenDocument/boolean} [interactingToken=false] * - * @returns {Promise} An array containing each item added as an object, with their quantities updated to match the new amounts + * @return {Promise} */ - static async addItems(target, items, { itemTypeFilters = false } = {}) { - - const hookResult = Hooks.call(HOOKS.ITEM.PRE_ADD, target, items); + static async closeItemPile(target, interactingToken = false) { + const data = lib.getItemPileData(target); + if (!data?.enabled || !data?.isContainer) return false; + data.closed = true; + const hookResult = Hooks.call(HOOKS.PILE.PRE_CLOSE, target, data, interactingToken); if (hookResult === false) return; - - const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`AddItems | Could not determine the UUID, please provide a valid target`, true) - - if (itemTypeFilters) { - itemTypeFilters.forEach(filter => { - if (typeof filter !== "string") throw lib.custom_error(`AddItem | entries in the itemTypeFilters must be of type string`); - }) + if (data.closeSound) { + AudioHelper.play({ src: data.closeSound }) } + return API.updateItemPile(target, data, interactingToken); + } - for (const index in items) { - const item = items[index]; - if (item instanceof Item) { - items[index] = item.toObject(); - } - const disallowedType = API.isItemTypeDisallowed(item, itemTypeFilters); - if (disallowedType) { - throw lib.custom_error(`AddItems | Could not add item of type "${disallowedType}"`, true) - } + /** + * Toggles a pile's closed state if it is enabled and a container + * + * @param {Token/TokenDocument} target Target pile to open or close + * @param {Token/TokenDocument/boolean} [interactingToken=false] + * + * @return {Promise} + */ + static async toggleItemPileClosed(target, interactingToken = false) { + const data = lib.getItemPileData(target); + if (!data?.enabled || !data?.isContainer) return false; + if (data.closed) { + await API.openItemPile(target, interactingToken); + } else { + await API.closeItemPile(target, interactingToken); } - - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.ADD_ITEMS, targetUuid, items); + return !data.closed; } /** - * @private + * Locks a pile if it is enabled and a container + * + * @param {Token/TokenDocument} target Target pile to lock + * @param {Token/TokenDocument/boolean} [interactingToken=false] + * + * @return {Promise} */ - static async _addItems(targetUuid, items, { isTransfer = false } = {}) { - - const target = await fromUuid(targetUuid); - const targetActor = target instanceof TokenDocument - ? target.actor - : target; - - const targetActorItems = Array.from(targetActor.items); - - const itemsAdded = []; - const itemsToCreate = []; - const itemsToUpdate = []; - for (const itemData of items) { - - const item = lib.getSimilarItem(targetActorItems, { itemId: itemData._id, itemName: itemData.name, itemType: itemData.type }); - - const incomingQuantity = getProperty(itemData, API.ITEM_QUANTITY_ATTRIBUTE) ?? 1; - - const itemAdded = item ? item.toObject() : foundry.utils.duplicate(itemData); - - if (item) { - const currentQuantity = getProperty(item.data, API.ITEM_QUANTITY_ATTRIBUTE); - const newQuantity = currentQuantity + incomingQuantity; - itemsToUpdate.push({ - "_id": item.id, - [API.ITEM_QUANTITY_ATTRIBUTE]: newQuantity - }); - - const itemAdded = item.toObject(); - setProperty(itemAdded, API.ITEM_QUANTITY_ATTRIBUTE, newQuantity) - itemsAdded.push(itemAdded); - } else { - setProperty(itemAdded, API.ITEM_QUANTITY_ATTRIBUTE, incomingQuantity) - itemsToCreate.push(itemData); - } - + static async lockItemPile(target, interactingToken = false) { + const data = lib.getItemPileData(target); + if (!data?.enabled || !data?.isContainer) return false; + const wasClosed = data.closed; + data.closed = true; + data.locked = true; + if (!wasClosed) { + const hookResult = Hooks.call(HOOKS.PILE.PRE_CLOSE, target, data, interactingToken); + if (hookResult === false) return; } + const hookResult = Hooks.call(HOOKS.PILE.PRE_LOCK, target, data, interactingToken); + if (hookResult === false) return; + if (data.closeSound && !wasClosed) { + AudioHelper.play({ src: data.closeSound }) + } + return API.updateItemPile(target, data, interactingToken); + } - const itemsCreated = await targetActor.createEmbeddedDocuments("Item", itemsToCreate); - await targetActor.updateEmbeddedDocuments("Item", itemsToUpdate); - - itemsCreated.forEach(item => itemsAdded.push(item.toObject())); - - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ITEM.ADD, targetUuid, itemsAdded); - - if (!isTransfer) { - - const macroData = { - action: "addItems", - target: targetUuid, - items: itemsAdded - }; - - await API._executeItemPileMacro(targetUuid, macroData); - - await API.rerenderItemPileInventoryApplication(targetUuid); + /** + * Unlocks a pile if it is enabled and a container + * + * @param {Token/TokenDocument} target Target pile to unlock + * @param {Token/TokenDocument/boolean} [interactingToken=false] + * + * @return {Promise} + */ + static async unlockItemPile(target, interactingToken = false) { + const data = lib.getItemPileData(target); + if (!data?.enabled || !data?.isContainer) return false; + data.locked = false; + Hooks.call(HOOKS.PILE.PRE_UNLOCK, target, data, interactingToken); + return API.updateItemPile(target, data, interactingToken); + } + /** + * Toggles a pile's locked state if it is enabled and a container + * + * @param {Token/TokenDocument} target Target pile to lock or unlock + * @param {Token/TokenDocument/boolean} [interactingToken=false] + * + * @return {Promise} + */ + static async toggleItemPileLocked(target, interactingToken = false) { + const data = lib.getItemPileData(target); + if (!data?.enabled || !data?.isContainer) return false; + if (data.locked) { + return API.unlockItemPile(target, interactingToken); } + return API.lockItemPile(target, interactingToken); + } - return itemsAdded; + /** + * Causes the item pile to play a sound as it was attempted to be opened, but was locked + * + * @param {Token/TokenDocument} target + * + * @return {Promise} + */ + static async rattleItemPile(target) { + const data = lib.getItemPileData(target); + if (!data?.enabled || !data?.isContainer) return false; + if (data.locked && data.lockedSound) { + AudioHelper.play({ src: data.lockedSound }) + } + return true; + } + /** + * Whether an item pile is locked. If it is not enabled or not a container, it is always false. + * + * @param {Token/TokenDocument} target + * + * @return {boolean} + */ + static isItemPileLocked(target) { + const data = lib.getItemPileData(target); + if (!data?.enabled || !data?.isContainer) return false; + return data.locked; } /** - * Transfers all items between the source and the target. + * Whether an item pile is closed. If it is not enabled or not a container, it is always false. * - * @param {Actor/Token/TokenDocument} source The actor to transfer all items from - * @param {Actor/Token/TokenDocument} target The actor to receive all the items - * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to module settings if none provided + * @param {Token/TokenDocument} target * - * @returns {Promise} An array containing all of the items that were transferred to the target + * @return {boolean} */ - static async transferAllItems(source, target, { itemTypeFilters = false } = {}) { + static isItemPileClosed(target) { + const data = lib.getItemPileData(target); + if (!data?.enabled || !data?.isContainer) return false; + return data.closed; + } - const hookResult = Hooks.call(HOOKS.ITEM.PRE_TRANSFER_ALL, source, target, itemTypeFilters); - if (hookResult === false) return; + /** + * Whether an item pile is a container. If it is not enabled, it is always false. + * + * @param {Token/TokenDocument} target + * + * @return {boolean} + */ + static isItemPileContainer(target) { + const data = lib.getItemPileData(target); + return data?.enabled && data?.isContainer; + } - const sourceUuid = lib.getUuid(source); - if (!sourceUuid) throw lib.custom_error(`TransferAllItems | Could not determine the UUID, please provide a valid source`, true) + /** + * Updates a pile with new data. + * + * @param {Token/TokenDocument} target + * @param {object} newData + * @param {Token/TokenDocument/boolean} [interactingToken=false] + * @param {object/boolean} [tokenSettings=false] + * + * @return {Promise} + */ + static async updateItemPile(target, newData, { interactingToken = false, tokenSettings = false } = {}) { const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`TransferAllItems | Could not determine the UUID, please provide a valid target`, true) + if (!targetUuid) throw lib.custom_error(`updateItemPile | Could not determine the UUID, please provide a valid target`, true); - if (itemTypeFilters) { - itemTypeFilters.forEach(filter => { - if (typeof filter !== "string") throw lib.custom_error(`RevertFromItemPile | entries in the itemTypeFilters must be of type string`); - }) - } + const interactingTokenUuid = interactingToken ? lib.getUuid(interactingToken) : false; + if (interactingToken && !interactingTokenUuid) throw lib.custom_error(`updateItemPile | Could not determine the UUID, please provide a valid target`, true); - return itemPileSocket.executeAsGM( - SOCKET_HANDLERS.TRANSFER_ALL_ITEMS, - sourceUuid, - targetUuid, - { - itemTypeFilters - } - ); + const hookResult = Hooks.call(HOOKS.PILE.PRE_UPDATE, target, newData, interactingToken, tokenSettings); + if (hookResult === false) return; + + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.UPDATE_PILE, targetUuid, newData, { + interactingTokenUuid, + tokenSettings + }) } /** * @private */ - static async _transferAllItems(sourceUuid, targetUuid, { itemTypeFilters = false, isEverything = false } = {}) { - - const source = await fromUuid(sourceUuid); - if (!source) throw lib.custom_error(`TransferAllItems | Could not find source with UUID "${sourceUuid}"`, true) + static async _updateItemPile(targetUuid, newData, { interactingTokenUuid = false, tokenSettings = false } = {}) { const target = await fromUuid(targetUuid); - if (!target) throw lib.custom_error(`TransferAllItems | Could not find target with UUID "${targetUuid}"`, true) - if (!Array.isArray(itemTypeFilters)) { - itemTypeFilters = API.ITEM_TYPE_FILTERS; - } else { - itemTypeFilters.forEach(filter => { - if (typeof filter !== "string") throw lib.custom_error(`TransferAllItems | entries in the itemTypeFilters must be of type string`); - }) + const oldData = lib.getItemPileData(target); + + const data = foundry.utils.mergeObject( + foundry.utils.duplicate(oldData), + foundry.utils.duplicate(newData) + ); + + const diff = foundry.utils.diffObject(oldData, data); + + await lib.wait(25); + + await lib.updateItemPile(target, data, tokenSettings); + + if (data.isEnabled && data.isContainer) { + if (diff?.closed === true) { + await API._executeItemPileMacro(targetUuid, { + action: "closeItemPile", + source: interactingTokenUuid, + target: targetUuid + }); + } + if (diff?.locked === true) { + await API._executeItemPileMacro(targetUuid, { + action: "lockItemPile", + source: interactingTokenUuid, + target: targetUuid + }); + } + if (diff?.locked === false) { + await API._executeItemPileMacro(targetUuid, { + action: "unlockItemPile", + source: interactingTokenUuid, + target: targetUuid + }); + } + if (diff?.closed === false) { + await API._executeItemPileMacro(targetUuid, { + action: "openItemPile", + source: interactingTokenUuid, + target: targetUuid + }); + } } - const itemsToRemove = API.getItemPileItems(target, itemTypeFilters).map(item => item.toObject()); + return itemPileSocket.executeForEveryone(SOCKET_HANDLERS.UPDATED_PILE, targetUuid, diff, interactingTokenUuid); + } - const itemsRemoved = await API._removeItems(sourceUuid, itemsToRemove, { itemTypeFilters, isTransfer: true }); - const itemsAdded = await API._addItems(targetUuid, itemsRemoved, { itemTypeFilters, isTransfer: true }); + /** + * @private + */ + static async _updatedItemPile(targetUuid, diffData, interactingTokenUuid) { - if (!isEverything) { + const target = await lib.getToken(targetUuid); - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.TRANSFER_ALL_ITEMS, HOOKS.ITEM.TRANSFER_ALL, sourceUuid, targetUuid, itemsAdded); + const interactingToken = interactingTokenUuid ? await fromUuid(interactingTokenUuid) : false; - const macroData = { - action: "transferAllItems", - source: sourceUuid, - target: targetUuid, - items: itemsAdded - }; - await API._executeItemPileMacro(sourceUuid, macroData); - await API._executeItemPileMacro(targetUuid, macroData); + if (foundry.utils.isObjectEmpty(diffData)) return; - const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(sourceUuid); - await API.rerenderItemPileInventoryApplication(sourceUuid, shouldBeDeleted); - await API.rerenderItemPileInventoryApplication(targetUuid); + const data = lib.getItemPileData(target); - if (shouldBeDeleted) { - await API._deleteItemPile(sourceUuid); - } + Hooks.callAll(HOOKS.PILE.UPDATE, target, diffData, interactingToken) + if (data.isEnabled && data.isContainer) { + if (diffData?.closed === true) { + Hooks.callAll(HOOKS.PILE.CLOSE, target, interactingToken) + } + if (diffData?.locked === true) { + Hooks.callAll(HOOKS.PILE.LOCK, target, interactingToken) + } + if (diffData?.locked === false) { + Hooks.callAll(HOOKS.PILE.UNLOCK, target, interactingToken) + } + if (diffData?.closed === false) { + Hooks.callAll(HOOKS.PILE.OPEN, target, interactingToken) + } } - - return itemsAdded; } /** - * Transfers a set quantity of an attribute from a source to a target, removing it or subtracting from the source and adds it the target + * Deletes a pile, calling the relevant hooks. * - * @param {Actor/Token/TokenDocument} source The source to transfer the attribute from - * @param {Actor/Token/TokenDocument} target The target to transfer the attribute to - * @param {array/object} attributes This can be either an array of attributes to transfer (to transfer all of a given attribute), or an object with each key being an attribute path, and its value being the quantity to transfer + * @param {Token/TokenDocument} target * - * @returns {Promise} An object containing a key value pair of each attribute transferred, the key being the attribute path and its value being the quantity that was transferred + * @return {Promise} */ - static async transferAttributes(source, target, attributes) { - - const hookResult = Hooks.call(HOOKS.ATTRIBUTE.PRE_TRANSFER, source, target, attributes); - if (hookResult === false) return; - - const sourceUuid = lib.getUuid(source); - if (!sourceUuid) throw lib.custom_error(`TransferAttributes | Could not determine the UUID, please provide a valid source`, true) - + static async deleteItemPile(target) { + if (!API.isValidItemPile(target)) { + if (!targetUuid) throw lib.custom_error(`deleteItemPile | This is not an item pile, please provide a valid target`, true); + } const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`TransferAttributes | Could not determine the UUID, please provide a valid target`, true) + if (!targetUuid) throw lib.custom_error(`deleteItemPile | Could not determine the UUID, please provide a valid target`, true); + if (!targetUuid.includes("Token")) { + throw lib.custom_error(`deleteItemPile | Please provide a Token or TokenDocument`, true); + } + const hookResult = Hooks.call(HOOKS.PILE.PRE_DELETE, target); + if (hookResult === false) return; + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.DELETE_PILE, targetUuid); + } - const sourceActor = source instanceof TokenDocument - ? source.actor - : source; + static async _deleteItemPile(targetUuid) { + const target = await lib.getToken(targetUuid); + return target.delete(); + } + /** + * Whether a given document is a valid pile or not + * + * @param {TokenDocument|Actor} document + * @return {boolean} + */ + static isValidItemPile(document) { + const documentActor = document instanceof TokenDocument ? document.actor : document; + return document && !document.destroyed && documentActor && lib.getItemPileData(document)?.enabled; + } + /** + * Whether the item pile is empty + * + * @param {TokenDocument|Actor} target + * @returns {boolean} + */ + static isItemPileEmpty(target){ const targetActor = target instanceof TokenDocument ? target.actor : target; - if (Array.isArray(attributes)) { - attributes.forEach(attribute => { - if (typeof attribute !== "string") { - throw lib.custom_error(`TransferAttributes | Each attribute in the array must be of type string`, true) - } - if (!hasProperty(sourceActor.data, attribute)) { - throw lib.custom_error(`TransferAttributes | Could not find attribute ${attribute} on source's actor with UUID "${targetUuid}"`, true) - } - if (!hasProperty(targetActor.data, attribute)) { - throw lib.custom_error(`TransferAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`, true) - } - }); - } else { - Object.entries(attributes).forEach(entry => { - const [attribute, quantity] = entry; - if (!hasProperty(sourceActor.data, attribute)) { - throw lib.custom_error(`TransferAttributes | Could not find attribute ${attribute} on source's actor with UUID "${targetUuid}"`, true) - } - if (!hasProperty(targetActor.data, attribute)) { - throw lib.custom_error(`TransferAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`, true) - } - if (!lib.is_real_number(quantity) && quantity > 0) { - throw lib.custom_error(`TransferAttributes | Attribute "${attribute}" must be of type number and greater than 0`, true) - } - }); - } + if(!targetActor) return false; - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.TRANSFER_ATTRIBUTES, sourceUuid, targetUuid, attributes); + const hasNoItems = API.getItemPileItems(target).length === 0; + + const attributes = API.getItemPileAttributes(target); + const hasEmptyAttributes = attributes.find(attribute => { + return hasProperty(targetActor.data, attribute.path) && getProperty(targetActor.data, attribute.path) === 0; + }) + return hasNoItems && hasEmptyAttributes; + } + /** + * Returns the item type filters for a given item pile + * + * @param target + * @returns {Array} + */ + static getItemPileItemTypeFilters(target){ + if(!API.isValidItemPile(target)) return []; + const pileData = lib.getItemPileData(target); + return pileData.itemTypeFilters + ? pileData.itemTypeFilters.split(',').map(str => str.trim().toLowerCase()) + : API.ITEM_TYPE_FILTERS; } /** - * @private + * Returns the items this item pile can transfer + * + * @param {TokenDocument|Actor} target + * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to pile settings or module settings if none provided + * @returns {Array} */ - static async _transferAttributes(sourceUuid, targetUuid, attributes, { isEverything = false } = {}) { + static getItemPileItems(target, itemTypeFilters = false){ - const attributesRemoved = await API._removeAttributes(sourceUuid, attributes, { isTransfer: true }); + if(!API.isValidItemPile(target)) return []; - const attributesAdded = await API._addAttributes(targetUuid, attributesRemoved, { isTransfer: true }); + const pileItemFilters = Array.isArray(itemTypeFilters) + ? new Set(itemTypeFilters) + : new Set(API.getItemPileItemTypeFilters(target)); - if (!isEverything) { + const targetActor = target instanceof TokenDocument + ? target.actor + : target; - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ATTRIBUTE.TRANSFER, sourceUuid, targetUuid, attributesAdded); + return Array.from(targetActor.items).filter(item => { + const itemType = getProperty(item.data, API.ITEM_TYPE_ATTRIBUTE); + return !pileItemFilters.has(itemType); + }) - const macroData = { - action: "transferAttributes", - source: sourceUuid, - target: targetUuid, - attributes: attributesAdded - }; - await API._executeItemPileMacro(sourceUuid, macroData); - await API._executeItemPileMacro(targetUuid, macroData); + } - const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(sourceUuid); - await API.rerenderItemPileInventoryApplication(sourceUuid, shouldBeDeleted); - await API.rerenderItemPileInventoryApplication(targetUuid); + /** + * Returns the attributes this item pile can transfer + * + * @param {TokenDocument|Actor} target + * @returns {array} + */ + static getItemPileAttributes(target){ + const pileData = lib.getItemPileData(target); + return pileData.overrideAttributes || API.DYNAMIC_ATTRIBUTES; + } - if (shouldBeDeleted) { - await API._deleteItemPile(sourceUuid); - } + /** + * Refreshes the target image of an item pile, ensuring it remains in sync + * + * @param target + * @return {Promise} + */ + static async refreshItemPile(target) { + if (!API.isValidItemPile(target)) return; + const targetUuid = lib.getUuid(target); + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.REFRESH_PILE, targetUuid) + } + + /** + * @private + */ + static async _refreshItemPile(targetUuid) { + const targetDocument = await fromUuid(targetUuid); + if (!API.isValidItemPile(targetDocument)) return; + + let targets = [targetDocument] + if (targetDocument instanceof Actor) { + targets = Array.from(canvas.tokens.getDocuments()).filter(token => token.actor === targetDocument); } - return attributesAdded + return Promise.allSettled(targets.map(_target => { + return new Promise(async (resolve) => { + const uuid = lib.getUuid(_target); + const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(uuid); + if (!shouldBeDeleted) { + await _target.update({ + "img": API._getItemPileTokenImage(targetDocument), + "scale": API._getItemPileTokenScale(targetDocument), + }) + } + resolve(); + }) + })); + } + /** + * Causes all connected users to re-render a specific pile's inventory UI + * + * @param {string} inPileUuid The uuid of the pile to be re-rendered + * @param {boolean} [deleted=false] Whether the pile was deleted as a part of this re-render + * @return {Promise} + */ + static async rerenderItemPileInventoryApplication(inPileUuid, deleted = false) { + return itemPileSocket.executeForEveryone(SOCKET_HANDLERS.RERENDER_PILE_INVENTORY, inPileUuid, deleted); } /** - * Subtracts attributes on the target + * @private + */ + static async _rerenderItemPileInventoryApplication(inPileUuid, deleted = false) { + return ItemPileInventory.rerenderActiveApp(inPileUuid, deleted); + } + + /* --- ITEM AND ATTRIBUTE METHODS --- */ + + /** + * Adds item to an actor, increasing item quantities if matches were found * - * @param {Token/TokenDocument} target The target whose attributes will be subtracted from - * @param {array/object} attributes This can be either an array of attributes to subtract (to zero out a given attribute), or an object with each key being an attribute path, and its value being the quantity to subtract + * @param {Actor/TokenDocument/Token} target The target to add an item to + * @param {array} items An array of item objects + * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to module settings if none provided * - * @returns {Promise} Returns an array containing a key value pair of the attribute path and the quantity of that attribute that was removed + * @returns {Promise} An array containing each item added as an object, with their quantities updated to match the new amounts */ - static async removeAttributes(target, attributes) { + static async addItems(target, items, { itemTypeFilters = false } = {}) { - const hookResult = Hooks.call(HOOKS.ATTRIBUTE.PRE_REMOVE, target, attributes); + const hookResult = Hooks.call(HOOKS.ITEM.PRE_ADD, target, items); if (hookResult === false) return; const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`RemoveAttributes | Could not determine the UUID, please provide a valid target`, true) - - const targetActor = target instanceof TokenDocument - ? target.actor - : target; + if (!targetUuid) throw lib.custom_error(`AddItems | Could not determine the UUID, please provide a valid target`, true) - if (Array.isArray(attributes)) { - attributes.forEach(attribute => { - if (typeof attribute !== "string") { - throw lib.custom_error(`RemoveAttributes | Each attribute in the array must be of type string`, true) - } - if (!hasProperty(targetActor.data, attribute)) { - throw lib.custom_error(`RemoveAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`, true) - } - }); - } else { - Object.entries(attributes).forEach(entry => { - const [attribute, quantity] = entry; - if (!hasProperty(targetActor.data, attribute)) { - throw lib.custom_error(`RemoveAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`, true) - } - if (!lib.is_real_number(quantity) && quantity > 0) { - throw lib.custom_error(`RemoveAttributes | Attribute "${attribute}" must be of type number and greater than 0`, true) - } - }); + if (itemTypeFilters) { + itemTypeFilters.forEach(filter => { + if (typeof filter !== "string") throw lib.custom_error(`AddItem | entries in the itemTypeFilters must be of type string`); + }) } - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.REMOVE_ATTRIBUTES, targetUuid, attributes); + for (const index in items) { + const item = items[index]; + if (item instanceof Item) { + items[index] = item.toObject(); + } + const disallowedType = API.isItemTypeDisallowed(item, itemTypeFilters); + if (disallowedType) { + throw lib.custom_error(`AddItems | Could not add item of type "${disallowedType}"`, true) + } + } + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.ADD_ITEMS, targetUuid, items); } /** * @private */ - static async _removeAttributes(targetUuid, attributes, { isTransfer = false } = {}) { + static async _addItems(targetUuid, items, { isTransfer = false } = {}) { const target = await fromUuid(targetUuid); const targetActor = target instanceof TokenDocument ? target.actor : target; - const updates = {}; - const attributesRemoved = {}; + const targetActorItems = Array.from(targetActor.items); - if (Array.isArray(attributes)) { - attributes = Object.fromEntries(attributes.map(attribute => { - return [attribute, getProperty(targetActor.data, attribute)]; - })) - } + const itemsAdded = []; + const itemsToCreate = []; + const itemsToUpdate = []; + for (const itemData of items) { - for (const [attribute, quantityToRemove] of Object.entries(attributes)) { + const item = lib.getSimilarItem(targetActorItems, { itemId: itemData._id, itemName: itemData.name, itemType: itemData.type }); - const currentQuantity = getProperty(targetActor.data, attribute); - const newQuantity = Math.max(0, currentQuantity - quantityToRemove); + const incomingQuantity = getProperty(itemData, API.ITEM_QUANTITY_ATTRIBUTE) ?? 1; - updates[attribute] = newQuantity; + const itemAdded = item ? item.toObject() : foundry.utils.duplicate(itemData); + + if (item) { + const currentQuantity = getProperty(item.data, API.ITEM_QUANTITY_ATTRIBUTE); + const newQuantity = currentQuantity + incomingQuantity; + itemsToUpdate.push({ + "_id": item.id, + [API.ITEM_QUANTITY_ATTRIBUTE]: newQuantity + }); + + const itemAdded = item.toObject(); + setProperty(itemAdded, API.ITEM_QUANTITY_ATTRIBUTE, newQuantity) + itemsAdded.push(itemAdded); + } else { + setProperty(itemAdded, API.ITEM_QUANTITY_ATTRIBUTE, incomingQuantity) + itemsToCreate.push(itemData); + } - // if the target's quantity is above 1, we've removed the amount we expected, otherwise however many were left - attributesRemoved[attribute] = newQuantity ? quantityToRemove : currentQuantity; } - await targetActor.update(updates); + const itemsCreated = await targetActor.createEmbeddedDocuments("Item", itemsToCreate); + await targetActor.updateEmbeddedDocuments("Item", itemsToUpdate); - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ATTRIBUTE.REMOVE, targetUuid, attributesRemoved); + itemsCreated.forEach(item => itemsAdded.push(item.toObject())); + + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ITEM.ADD, targetUuid, itemsAdded); if (!isTransfer) { const macroData = { - action: "removeAttributes", + action: "addItems", target: targetUuid, - attributes: attributesRemoved + items: itemsAdded }; + await API._executeItemPileMacro(targetUuid, macroData); - const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(targetUuid); - await API.rerenderItemPileInventoryApplication(targetUuid, shouldBeDeleted); + await API.rerenderItemPileInventoryApplication(targetUuid); - if (shouldBeDeleted) { - await API._deleteItemPile(targetUuid); - } } - return attributesRemoved; + return itemsAdded; } /** - * Adds to attributes on an actor - * - * @param {Actor/Token/TokenDocument} target The target whose attribute will have a set quantity added to it - * @param {object} attributes An object with each key being an attribute path, and its value being the quantity to add + * Subtracts the quantity of items on an actor. If the quantity of an item reaches 0, the item is removed from the actor. * - * @returns {Promise} Returns an array containing a key value pair of the attribute path and the quantity of that attribute that was removed + * @param {Actor/Token/TokenDocument} target The target to remove a items from + * @param {array} items An array of objects each containing the item id (key "_id") and the quantity to remove (key "quantity"), or an array of IDs to remove all quantities of + * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to module settings if none provided * + * @returns {Promise} An array containing the objects of each item that was removed, with their quantities set to the number removed */ - static async addAttributes(target, attributes) { + static async removeItems(target, items, { itemTypeFilters = false } = {}) { - const hookResult = Hooks.call(HOOKS.ATTRIBUTE.PRE_ADD, target, attributes); + const hookResult = Hooks.call(HOOKS.ITEM.PRE_REMOVE, target, items); if (hookResult === false) return; const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`AddAttributes | Could not determine the UUID, please provide a valid target`, true) + if (!targetUuid) throw lib.custom_error(`RemoveItems | Could not determine the UUID, please provide a valid target`, true); - const targetActor = target instanceof TokenDocument - ? target.actor - : target; + if (itemTypeFilters) { + itemTypeFilters.forEach(filter => { + if (typeof filter !== "string") throw lib.custom_error(`RemoveItems | entries in the itemTypeFilters must be of type string`); + }) + } - Object.entries(attributes).forEach(entry => { - const [attribute, quantity] = entry; - if (!hasProperty(targetActor.data, attribute)) { - throw lib.custom_error(`AddAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`, true) + const targetActorItems = API.getItemPileItems(target); + + items.forEach(item => { + const itemId = typeof item === "string" ? item : item._id; + const actorItem = targetActorItems.find(actorItem => actorItem.id === itemId); + if (!actorItem) { + throw lib.custom_error(`RemoveItems | Could not find item with id "${itemId}" on target "${targetUuid}"`, true) } - if (!lib.is_real_number(quantity) && quantity > 0) { - throw lib.custom_error(`AddAttributes | Attribute "${attribute}" must be of type number and greater than 0`, true) + const disallowedType = API.isItemTypeDisallowed(actorItem, itemTypeFilters); + if (disallowedType) { + throw lib.custom_error(`RemoveItems | Could not transfer item of type "${disallowedType}"`, true) } }); - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.ADD_ATTRIBUTE, targetUuid, attributes); - + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.REMOVE_ITEMS, targetUuid, items); } /** * @private */ - static async _addAttributes(targetUuid, attributes, { isTransfer = false } = {}) { + static async _removeItems(targetUuid, items, { isTransfer = false } = {}) { const target = await fromUuid(targetUuid); const targetActor = target instanceof TokenDocument ? target.actor : target; - const updates = {}; - const attributesAdded = {}; + const itemsRemoved = []; + const itemsToUpdate = []; + const itemsToDelete = []; + for (const item of items) { - for (const [attribute, quantityToAdd] of Object.entries(attributes)) { + const itemId = typeof item === "string" ? item : item._id; - const currentQuantity = getProperty(targetActor.data, attribute); + const actorItem = targetActor.items.get(itemId); + const removedItem = actorItem.toObject(); - updates[attribute] = currentQuantity + quantityToAdd; - attributesAdded[attribute] = currentQuantity + quantityToAdd; + const currentQuantity = getProperty(actorItem.data, API.ITEM_QUANTITY_ATTRIBUTE); + + const quantityToRemove = getProperty(item, API.ITEM_QUANTITY_ATTRIBUTE) ?? item.quantity ?? currentQuantity; + + const newQuantity = Math.max(0, currentQuantity - quantityToRemove); + + if (newQuantity >= 1) { + setProperty(removedItem, API.ITEM_QUANTITY_ATTRIBUTE, quantityToRemove); + itemsToUpdate.push({ _id: actorItem.id, [API.ITEM_QUANTITY_ATTRIBUTE]: newQuantity }); + } else { + setProperty(removedItem, API.ITEM_QUANTITY_ATTRIBUTE, currentQuantity); + itemsToDelete.push(actorItem.id); + } + + itemsRemoved.push(removedItem); } - await targetActor.update(updates); + await targetActor.updateEmbeddedDocuments("Item", itemsToUpdate); + await targetActor.deleteEmbeddedDocuments("Item", itemsToDelete); - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ATTRIBUTE.ADD, targetUuid, attributesAdded); + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ITEM.REMOVE, targetUuid, itemsRemoved); - if (isTransfer) { + if (!isTransfer) { const macroData = { - action: "addAttributes", + action: "removeItems", target: targetUuid, - attributes: attributesAdded + items: itemsRemoved }; + await API._executeItemPileMacro(targetUuid, macroData); - await API.rerenderItemPileInventoryApplication(targetUuid); + const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(targetUuid); + + await API.rerenderItemPileInventoryApplication(targetUuid, shouldBeDeleted); + + if (shouldBeDeleted) { + await API._deleteItemPile(targetUuid); + } + } - return attributesAdded; + return itemsRemoved; } - /** - * Transfers all dynamic attributes from a source to a target, removing it or subtracting from the source and adding them to the target + * Transfers items from the source to the target, subtracting a number of quantity from the source's item and adding it to the target's item, deleting items from the source if their quantity reaches 0 * - * @param {Actor/Token/TokenDocument} source The source to transfer the attributes from - * @param {Actor/Token/TokenDocument} target The target to transfer the attributes to + * @param {Actor/Token/TokenDocument} source The source to transfer the items from + * @param {Actor/Token/TokenDocument} target The target to transfer the items to + * @param {array} items An array of objects each containing the item id (key "_id") and the quantity to transfer (key "quantity") + * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to module settings if none provided * - * @returns {Promise} An object containing a key value pair of each attribute transferred, the key being the attribute path and its value being the quantity that was transferred + * @returns {Promise} An object containing a key value pair for each item added to the target, key being item ID, value being quantities added */ - static async transferAllAttributes(source, target) { + static async transferItems(source, target, items, { itemTypeFilters = false } = {}) { - const hookResult = Hooks.call(HOOKS.ATTRIBUTE.PRE_TRANSFER_ALL, source, target); + const hookResult = Hooks.call(HOOKS.ITEM.PRE_TRANSFER, source, target, items); if (hookResult === false) return; const sourceUuid = lib.getUuid(source); - if (!sourceUuid) throw lib.custom_error(`TransferAllAttributes | Could not determine the UUID, please provide a valid source`, true); + if (!sourceUuid) throw lib.custom_error(`TransferItems | Could not determine the UUID, please provide a valid source`, true) + + if (itemTypeFilters) { + itemTypeFilters.forEach(filter => { + if (typeof filter !== "string") throw lib.custom_error(`TransferItems | entries in the itemTypeFilters must be of type string`); + }) + } + + const sourceActorItems = API.getItemPileItems(source); + + items.forEach(item => { + const actorItem = sourceActorItems.find(actorItem => actorItem.id === item._id); + if (!actorItem) { + throw lib.custom_error(`TransferItems | Could not find item with id "${item._id}" on source "${sourceUuid}"`, true) + } + const disallowedType = API.isItemTypeDisallowed(actorItem, itemTypeFilters); + if (disallowedType) { + throw lib.custom_error(`TransferItems | Could not transfer item of type "${disallowedType}"`, true) + } + }); const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`TransferAllAttributes | Could not determine the UUID, please provide a valid target`, true); + if (!targetUuid) throw lib.custom_error(`TransferItems | Could not determine the UUID, please provide a valid target`, true) - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.TRANSFER_ALL_ATTRIBUTES, sourceUuid, targetUuid); + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.TRANSFER_ITEMS, sourceUuid, targetUuid, items, { itemTypeFilters }); } /** * @private */ - static async _transferAllAttributes(sourceUuid, targetUuid, { isEverything = false } = {}) { - - const source = await fromUuid(sourceUuid); - - const sourceActor = source instanceof TokenDocument - ? source.actor - : source; - - const target = await fromUuid(targetUuid); - - const targetActor = target instanceof TokenDocument - ? target.actor - : target; - - const sourceAttributes = API.getItemPileAttributes(sourceActor); - - const attributesToTransfer = sourceAttributes.filter(attribute => { - return hasProperty(sourceActor.data, attribute.path) - && getProperty(sourceActor.data, attribute.path) > 0 - && hasProperty(targetActor.data, attribute.path); - }).map(attribute => attribute.path); + static async _transferItems(sourceUuid, targetUuid, items, { itemTypeFilters = false, isEverything = false } = {}) { - const attributesRemoved = await API._removeAttributes(sourceUuid, attributesToTransfer, { isTransfer: true }); - const attributesAdded = await API._addAttributes(targetUuid, attributesRemoved, { isTransfer: true }); + const itemsRemoved = await API._removeItems(sourceUuid, items, { itemTypeFilters, isTransfer: true }); - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ATTRIBUTE.TRANSFER_ALL, sourceUuid, targetUuid, attributesAdded); + const itemsAdded = await API._addItems(targetUuid, itemsRemoved, { itemTypeFilters, isTransfer: true }); if (!isEverything) { + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ITEM.TRANSFER, sourceUuid, targetUuid, itemsAdded); + const macroData = { - action: "transferAllAttributes", + action: "transferItems", source: sourceUuid, target: targetUuid, - attributes: attributesAdded + itemsAdded: itemsAdded }; await API._executeItemPileMacro(sourceUuid, macroData); await API._executeItemPileMacro(targetUuid, macroData); const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(sourceUuid); await API.rerenderItemPileInventoryApplication(sourceUuid, shouldBeDeleted); + await API.rerenderItemPileInventoryApplication(targetUuid); if (shouldBeDeleted) { await API._deleteItemPile(sourceUuid); @@ -978,651 +1080,549 @@ export default class API { } - return attributesAdded; + return itemsAdded; } /** - * Transfers all items and attributes between the source and the target. + * Transfers all items between the source and the target. * - * @param {Actor/Token/TokenDocument} source The actor to transfer all items and attributes from - * @param {Actor/Token/TokenDocument} target The actor to receive all the items and attributes + * @param {Actor/Token/TokenDocument} source The actor to transfer all items from + * @param {Actor/Token/TokenDocument} target The actor to receive all the items * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to module settings if none provided * - * @returns {Promise} An object containing all items and attributes transferred to the target + * @returns {Promise} An array containing all of the items that were transferred to the target */ - static async transferEverything(source, target, { itemTypeFilters = false } = {}) { + static async transferAllItems(source, target, { itemTypeFilters = false } = {}) { - const hookResult = Hooks.call(HOOKS.PRE_TRANSFER_EVERYTHING, source, target, itemTypeFilters); + const hookResult = Hooks.call(HOOKS.ITEM.PRE_TRANSFER_ALL, source, target, itemTypeFilters); if (hookResult === false) return; const sourceUuid = lib.getUuid(source); - if (!sourceUuid) throw lib.custom_error(`TransferEverything | Could not determine the UUID, please provide a valid source`, true) + if (!sourceUuid) throw lib.custom_error(`TransferAllItems | Could not determine the UUID, please provide a valid source`, true) const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`TransferEverything | Could not determine the UUID, please provide a valid target`, true) + if (!targetUuid) throw lib.custom_error(`TransferAllItems | Could not determine the UUID, please provide a valid target`, true) if (itemTypeFilters) { itemTypeFilters.forEach(filter => { - if (typeof filter !== "string") throw lib.custom_error(`TransferEverything | entries in the itemTypeFilters must be of type string`); + if (typeof filter !== "string") throw lib.custom_error(`RevertFromItemPile | entries in the itemTypeFilters must be of type string`); }) } - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.TRANSFER_EVERYTHING, sourceUuid, targetUuid, { itemTypeFilters }) - - } - - /** - * @private - */ - static async _transferEverything(sourceUuid, targetUuid, { itemTypeFilters = false } = {}) { - - const itemsTransferred = await API._transferAllItems(sourceUuid, targetUuid, { - itemTypeFilters, - isEverything: true - }); - const attributesTransferred = await API._transferAllAttributes(sourceUuid, targetUuid, { isEverything: true }); - - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.TRANSFER_EVERYTHING, sourceUuid, targetUuid, itemsTransferred, attributesTransferred); - - const macroData = { - action: "transferEverything", - source: sourceUuid, - target: targetUuid, - items: itemsTransferred, - attributes: attributesTransferred - }; - await API._executeItemPileMacro(sourceUuid, macroData); - await API._executeItemPileMacro(targetUuid, macroData); - - const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(sourceUuid); - await API.rerenderItemPileInventoryApplication(sourceUuid, shouldBeDeleted); - await API.rerenderItemPileInventoryApplication(targetUuid); - - if (shouldBeDeleted) { - await API._deleteItemPile(sourceUuid); - } - - return { - itemsTransferred, - attributesTransferred - }; - - } - - /** - * Turns a token and its actor into an item pile - * - * @param {Token/TokenDocument} target The target to be turned into an item pile - * @param {object} pileSettings Overriding settings to be put on the item pile's settings - * @param {object} tokenSettings Overriding settings that will update the token - * - * @return {Promise} The uuid of the target after it was turned into an item pile - */ - static async turnTokenIntoItemPile(target, { pileSettings = {}, tokenSettings = {} } = {}) { - const hookResult = Hooks.call(HOOKS.PILE.PRE_TURN_INTO, target, pileSettings, tokenSettings); - if (hookResult === false) return; - const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`TurnIntoItemPile | Could not determine the UUID, please provide a valid target`, true) - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.TURN_INTO_PILE, targetUuid, pileSettings, tokenSettings); - } - - /** - * @private - */ - static async _turnTokenIntoItemPile(targetUuid, pileSettings = {}, tokenSettings = {}) { - - const target = await fromUuid(targetUuid); - - const existingPileSettings = foundry.utils.mergeObject(CONSTANTS.PILE_DEFAULTS, lib.getItemPileData(target)); - const newPileSettings = foundry.utils.mergeObject(existingPileSettings, pileSettings); - newPileSettings.enabled = true; - - await API._updateItemPile(targetUuid, newPileSettings, { tokenSettings }); - - setTimeout(API.rerenderTokenHud, 100); - - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.PILE.TURN_INTO, targetUuid, newPileSettings); - - return targetUuid; - - } - - /** - * Reverts a token from an item pile into a normal token and actor - * - * @param {Token/TokenDocument} target The target to be reverted from an item pile - * @param {object} tokenSettings Overriding settings that will update the token - * - * @return {Promise} The uuid of the target after it was reverted from an item pile - */ - static async revertTokenFromItemPile(target, { tokenSettings = {} } = {}) { - const hookResult = Hooks.call(HOOKS.PILE.PRE_REVERT_FROM, target, tokenSettings); - if (hookResult === false) return; - const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`RevertFromItemPile | Could not determine the UUID, please provide a valid target`, true) - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.REVERT_FROM_PILE, targetUuid, tokenSettings); + return itemPileSocket.executeAsGM( + SOCKET_HANDLERS.TRANSFER_ALL_ITEMS, + sourceUuid, + targetUuid, + { + itemTypeFilters + } + ); } /** * @private */ - static async _revertTokenFromItemPile(targetUuid, tokenSettings) { + static async _transferAllItems(sourceUuid, targetUuid, { itemTypeFilters = false, isEverything = false } = {}) { + + const source = await fromUuid(sourceUuid); + if (!source) throw lib.custom_error(`TransferAllItems | Could not find source with UUID "${sourceUuid}"`, true) const target = await fromUuid(targetUuid); + if (!target) throw lib.custom_error(`TransferAllItems | Could not find target with UUID "${targetUuid}"`, true) - const pileSettings = foundry.utils.mergeObject(CONSTANTS.PILE_DEFAULTS, lib.getItemPileData(target)); + if (!Array.isArray(itemTypeFilters)) { + itemTypeFilters = API.ITEM_TYPE_FILTERS; + } else { + itemTypeFilters.forEach(filter => { + if (typeof filter !== "string") throw lib.custom_error(`TransferAllItems | entries in the itemTypeFilters must be of type string`); + }) + } - pileSettings.enabled = false; + const itemsToRemove = API.getItemPileItems(target, itemTypeFilters).map(item => item.toObject()); - await API._updateItemPile(targetUuid, pileSettings, { tokenSettings }); + const itemsRemoved = await API._removeItems(sourceUuid, itemsToRemove, { itemTypeFilters, isTransfer: true }); + const itemsAdded = await API._addItems(targetUuid, itemsRemoved, { itemTypeFilters, isTransfer: true }); - setTimeout(API.rerenderTokenHud, 100); + if (!isEverything) { - if (target instanceof TokenDocument) { - await API.rerenderItemPileInventoryApplication(targetUuid); - } + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.TRANSFER_ALL_ITEMS, HOOKS.ITEM.TRANSFER_ALL, sourceUuid, targetUuid, itemsAdded); - await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.PILE.REVERT_FROM, targetUuid); + const macroData = { + action: "transferAllItems", + source: sourceUuid, + target: targetUuid, + items: itemsAdded + }; + await API._executeItemPileMacro(sourceUuid, macroData); + await API._executeItemPileMacro(targetUuid, macroData); - return targetUuid; + const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(sourceUuid); + await API.rerenderItemPileInventoryApplication(sourceUuid, shouldBeDeleted); + await API.rerenderItemPileInventoryApplication(targetUuid); - } + if (shouldBeDeleted) { + await API._deleteItemPile(sourceUuid); + } - /** - * Causes every user's token HUD to rerender - * - * @return {Promise} - */ - static async rerenderTokenHud() { - return itemPileSocket.executeForEveryone(SOCKET_HANDLERS.RERENDER_TOKEN_HUD); - } + } - /** - * @private - */ - static async _rerenderTokenHud() { - if (!canvas.tokens.hud.rendered) return; - await canvas.tokens.hud.render(true) - return true; + return itemsAdded; } /** - * Opens a pile if it is enabled and a container - * - * @param {Token/TokenDocument} target - * @param {Token/TokenDocument/boolean} [interactingToken=false] + * Adds to attributes on an actor * - * @return {Promise} - */ - static async openItemPile(target, interactingToken = false) { - const data = lib.getItemPileData(target); - if (!data?.enabled || !data?.isContainer) return false; - const wasLocked = data.locked; - data.closed = false; - data.locked = false; - if (wasLocked) { - const hookResult = Hooks.call(HOOKS.PILE.PRE_UNLOCK, target, data, interactingToken); - if (hookResult === false) return; - } - const hookResult = Hooks.call(HOOKS.PILE.PRE_OPEN, target, data, interactingToken); - if (hookResult === false) return; - if (data.openSound) { - AudioHelper.play({ src: data.openSound }) - } - return API.updateItemPile(target, data, { interactingToken }); - } - - /** - * Closes a pile if it is enabled and a container + * @param {Actor/Token/TokenDocument} target The target whose attribute will have a set quantity added to it + * @param {object} attributes An object with each key being an attribute path, and its value being the quantity to add * - * @param {Token/TokenDocument} target Target pile to close - * @param {Token/TokenDocument/boolean} [interactingToken=false] + * @returns {Promise} Returns an array containing a key value pair of the attribute path and the quantity of that attribute that was removed * - * @return {Promise} */ - static async closeItemPile(target, interactingToken = false) { - const data = lib.getItemPileData(target); - if (!data?.enabled || !data?.isContainer) return false; - data.closed = true; - const hookResult = Hooks.call(HOOKS.PILE.PRE_CLOSE, target, data, interactingToken); + static async addAttributes(target, attributes) { + + const hookResult = Hooks.call(HOOKS.ATTRIBUTE.PRE_ADD, target, attributes); if (hookResult === false) return; - if (data.closeSound) { - AudioHelper.play({ src: data.closeSound }) - } - return API.updateItemPile(target, data, interactingToken); - } - /** - * Toggles a pile's closed state if it is enabled and a container - * - * @param {Token/TokenDocument} target Target pile to open or close - * @param {Token/TokenDocument/boolean} [interactingToken=false] - * - * @return {Promise} - */ - static async toggleItemPileClosed(target, interactingToken = false) { - const data = lib.getItemPileData(target); - if (!data?.enabled || !data?.isContainer) return false; - if (data.closed) { - await API.openItemPile(target, interactingToken); - } else { - await API.closeItemPile(target, interactingToken); - } - return !data.closed; - } + const targetUuid = lib.getUuid(target); + if (!targetUuid) throw lib.custom_error(`AddAttributes | Could not determine the UUID, please provide a valid target`, true) - /** - * Locks a pile if it is enabled and a container - * - * @param {Token/TokenDocument} target Target pile to lock - * @param {Token/TokenDocument/boolean} [interactingToken=false] - * - * @return {Promise} - */ - static async lockItemPile(target, interactingToken = false) { - const data = lib.getItemPileData(target); - if (!data?.enabled || !data?.isContainer) return false; - const wasClosed = data.closed; - data.closed = true; - data.locked = true; - if (!wasClosed) { - const hookResult = Hooks.call(HOOKS.PILE.PRE_CLOSE, target, data, interactingToken); - if (hookResult === false) return; - } - const hookResult = Hooks.call(HOOKS.PILE.PRE_LOCK, target, data, interactingToken); - if (hookResult === false) return; - if (data.closeSound && !wasClosed) { - AudioHelper.play({ src: data.closeSound }) - } - return API.updateItemPile(target, data, interactingToken); - } + const targetActor = target instanceof TokenDocument + ? target.actor + : target; - /** - * Unlocks a pile if it is enabled and a container - * - * @param {Token/TokenDocument} target Target pile to unlock - * @param {Token/TokenDocument/boolean} [interactingToken=false] - * - * @return {Promise} - */ - static async unlockItemPile(target, interactingToken = false) { - const data = lib.getItemPileData(target); - if (!data?.enabled || !data?.isContainer) return false; - data.locked = false; - Hooks.call(HOOKS.PILE.PRE_UNLOCK, target, data, interactingToken); - return API.updateItemPile(target, data, interactingToken); - } + Object.entries(attributes).forEach(entry => { + const [attribute, quantity] = entry; + if (!hasProperty(targetActor.data, attribute)) { + throw lib.custom_error(`AddAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`, true) + } + if (!lib.is_real_number(quantity) && quantity > 0) { + throw lib.custom_error(`AddAttributes | Attribute "${attribute}" must be of type number and greater than 0`, true) + } + }); + + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.ADD_ATTRIBUTE, targetUuid, attributes); - /** - * Toggles a pile's locked state if it is enabled and a container - * - * @param {Token/TokenDocument} target Target pile to lock or unlock - * @param {Token/TokenDocument/boolean} [interactingToken=false] - * - * @return {Promise} - */ - static async toggleItemPileLocked(target, interactingToken = false) { - const data = lib.getItemPileData(target); - if (!data?.enabled || !data?.isContainer) return false; - if (data.locked) { - return API.unlockItemPile(target, interactingToken); - } - return API.lockItemPile(target, interactingToken); } /** - * Causes the item pile to play a sound as it was attempted to be opened, but was locked - * - * @param {Token/TokenDocument} target - * - * @return {Promise} + * @private */ - static async rattleItemPile(target) { - const data = lib.getItemPileData(target); - if (!data?.enabled || !data?.isContainer) return false; - if (data.locked && data.lockedSound) { - AudioHelper.play({ src: data.lockedSound }) + static async _addAttributes(targetUuid, attributes, { isTransfer = false } = {}) { + + const target = await fromUuid(targetUuid); + const targetActor = target instanceof TokenDocument + ? target.actor + : target; + + const updates = {}; + const attributesAdded = {}; + + for (const [attribute, quantityToAdd] of Object.entries(attributes)) { + + const currentQuantity = getProperty(targetActor.data, attribute); + + updates[attribute] = currentQuantity + quantityToAdd; + attributesAdded[attribute] = currentQuantity + quantityToAdd; + } - return true; - } - /** - * Whether an item pile is locked. If it is not enabled or not a container, it is always false. - * - * @param {Token/TokenDocument} target - * - * @return {boolean} - */ - static isItemPileLocked(target) { - const data = lib.getItemPileData(target); - if (!data?.enabled || !data?.isContainer) return false; - return data.locked; - } + await targetActor.update(updates); + + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ATTRIBUTE.ADD, targetUuid, attributesAdded); + + if (isTransfer) { + + const macroData = { + action: "addAttributes", + target: targetUuid, + attributes: attributesAdded + }; + await API._executeItemPileMacro(targetUuid, macroData); + + await API.rerenderItemPileInventoryApplication(targetUuid); + } - /** - * Whether an item pile is closed. If it is not enabled or not a container, it is always false. - * - * @param {Token/TokenDocument} target - * - * @return {boolean} - */ - static isItemPileClosed(target) { - const data = lib.getItemPileData(target); - if (!data?.enabled || !data?.isContainer) return false; - return data.closed; - } + return attributesAdded; - /** - * Whether an item pile is a container. If it is not enabled, it is always false. - * - * @param {Token/TokenDocument} target - * - * @return {boolean} - */ - static isItemPileContainer(target) { - const data = lib.getItemPileData(target); - return data?.enabled && data?.isContainer; } /** - * Updates a pile with new data. + * Subtracts attributes on the target * - * @param {Token/TokenDocument} target - * @param {object} newData - * @param {Token/TokenDocument/boolean} [interactingToken=false] - * @param {object/boolean} [tokenSettings=false] + * @param {Token/TokenDocument} target The target whose attributes will be subtracted from + * @param {array/object} attributes This can be either an array of attributes to subtract (to zero out a given attribute), or an object with each key being an attribute path, and its value being the quantity to subtract * - * @return {Promise} + * @returns {Promise} Returns an array containing a key value pair of the attribute path and the quantity of that attribute that was removed */ - static async updateItemPile(target, newData, { interactingToken = false, tokenSettings = false } = {}) { + static async removeAttributes(target, attributes) { + + const hookResult = Hooks.call(HOOKS.ATTRIBUTE.PRE_REMOVE, target, attributes); + if (hookResult === false) return; const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`updateItemPile | Could not determine the UUID, please provide a valid target`, true); + if (!targetUuid) throw lib.custom_error(`RemoveAttributes | Could not determine the UUID, please provide a valid target`, true) - const interactingTokenUuid = interactingToken ? lib.getUuid(interactingToken) : false; - if (interactingToken && !interactingTokenUuid) throw lib.custom_error(`updateItemPile | Could not determine the UUID, please provide a valid target`, true); + const targetActor = target instanceof TokenDocument + ? target.actor + : target; - const hookResult = Hooks.call(HOOKS.PILE.PRE_UPDATE, target, newData, interactingToken, tokenSettings); - if (hookResult === false) return; + if (Array.isArray(attributes)) { + attributes.forEach(attribute => { + if (typeof attribute !== "string") { + throw lib.custom_error(`RemoveAttributes | Each attribute in the array must be of type string`, true) + } + if (!hasProperty(targetActor.data, attribute)) { + throw lib.custom_error(`RemoveAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`, true) + } + }); + } else { + Object.entries(attributes).forEach(entry => { + const [attribute, quantity] = entry; + if (!hasProperty(targetActor.data, attribute)) { + throw lib.custom_error(`RemoveAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`, true) + } + if (!lib.is_real_number(quantity) && quantity > 0) { + throw lib.custom_error(`RemoveAttributes | Attribute "${attribute}" must be of type number and greater than 0`, true) + } + }); + } + + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.REMOVE_ATTRIBUTES, targetUuid, attributes); - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.UPDATE_PILE, targetUuid, newData, { - interactingTokenUuid, - tokenSettings - }) } /** * @private */ - static async _updateItemPile(targetUuid, newData, { interactingTokenUuid = false, tokenSettings = false } = {}) { + static async _removeAttributes(targetUuid, attributes, { isTransfer = false } = {}) { const target = await fromUuid(targetUuid); + const targetActor = target instanceof TokenDocument + ? target.actor + : target; - const oldData = lib.getItemPileData(target); + const updates = {}; + const attributesRemoved = {}; - const data = foundry.utils.mergeObject( - foundry.utils.duplicate(oldData), - foundry.utils.duplicate(newData) - ); + if (Array.isArray(attributes)) { + attributes = Object.fromEntries(attributes.map(attribute => { + return [attribute, getProperty(targetActor.data, attribute)]; + })) + } - const diff = foundry.utils.diffObject(oldData, data); + for (const [attribute, quantityToRemove] of Object.entries(attributes)) { - await lib.wait(25); + const currentQuantity = getProperty(targetActor.data, attribute); + const newQuantity = Math.max(0, currentQuantity - quantityToRemove); - await lib.updateItemPile(target, data, tokenSettings); + updates[attribute] = newQuantity; - if (data.isEnabled && data.isContainer) { - if (diff?.closed === true) { - await API._executeItemPileMacro(targetUuid, { - action: "closeItemPile", - source: interactingTokenUuid, - target: targetUuid - }); - } - if (diff?.locked === true) { - await API._executeItemPileMacro(targetUuid, { - action: "lockItemPile", - source: interactingTokenUuid, - target: targetUuid - }); - } - if (diff?.locked === false) { - await API._executeItemPileMacro(targetUuid, { - action: "unlockItemPile", - source: interactingTokenUuid, - target: targetUuid - }); - } - if (diff?.closed === false) { - await API._executeItemPileMacro(targetUuid, { - action: "openItemPile", - source: interactingTokenUuid, - target: targetUuid - }); - } + // if the target's quantity is above 1, we've removed the amount we expected, otherwise however many were left + attributesRemoved[attribute] = newQuantity ? quantityToRemove : currentQuantity; } - return itemPileSocket.executeForEveryone(SOCKET_HANDLERS.UPDATED_PILE, targetUuid, diff, interactingTokenUuid); - } - - /** - * @private - */ - static async _updatedItemPile(targetUuid, diffData, interactingTokenUuid) { - - const target = await lib.getToken(targetUuid); + await targetActor.update(updates); - const interactingToken = interactingTokenUuid ? await fromUuid(interactingTokenUuid) : false; + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ATTRIBUTE.REMOVE, targetUuid, attributesRemoved); - if (foundry.utils.isObjectEmpty(diffData)) return; + if (!isTransfer) { - const data = lib.getItemPileData(target); + const macroData = { + action: "removeAttributes", + target: targetUuid, + attributes: attributesRemoved + }; + await API._executeItemPileMacro(targetUuid, macroData); - Hooks.callAll(HOOKS.PILE.UPDATE, target, diffData, interactingToken) + const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(targetUuid); + await API.rerenderItemPileInventoryApplication(targetUuid, shouldBeDeleted); - if (data.isEnabled && data.isContainer) { - if (diffData?.closed === true) { - Hooks.callAll(HOOKS.PILE.CLOSE, target, interactingToken) - } - if (diffData?.locked === true) { - Hooks.callAll(HOOKS.PILE.LOCK, target, interactingToken) - } - if (diffData?.locked === false) { - Hooks.callAll(HOOKS.PILE.UNLOCK, target, interactingToken) - } - if (diffData?.closed === false) { - Hooks.callAll(HOOKS.PILE.OPEN, target, interactingToken) + if (shouldBeDeleted) { + await API._deleteItemPile(targetUuid); } } + + return attributesRemoved; + } /** - * Deletes a pile, calling the relevant hooks. + * Transfers a set quantity of an attribute from a source to a target, removing it or subtracting from the source and adds it the target * - * @param {Token/TokenDocument} target + * @param {Actor/Token/TokenDocument} source The source to transfer the attribute from + * @param {Actor/Token/TokenDocument} target The target to transfer the attribute to + * @param {array/object} attributes This can be either an array of attributes to transfer (to transfer all of a given attribute), or an object with each key being an attribute path, and its value being the quantity to transfer * - * @return {Promise} + * @returns {Promise} An object containing a key value pair of each attribute transferred, the key being the attribute path and its value being the quantity that was transferred */ - static async deleteItemPile(target) { - if (!API.isValidItemPile(target)) { - if (!targetUuid) throw lib.custom_error(`deleteItemPile | This is not an item pile, please provide a valid target`, true); - } + static async transferAttributes(source, target, attributes) { + + const hookResult = Hooks.call(HOOKS.ATTRIBUTE.PRE_TRANSFER, source, target, attributes); + if (hookResult === false) return; + + const sourceUuid = lib.getUuid(source); + if (!sourceUuid) throw lib.custom_error(`TransferAttributes | Could not determine the UUID, please provide a valid source`, true) + const targetUuid = lib.getUuid(target); - if (!targetUuid) throw lib.custom_error(`deleteItemPile | Could not determine the UUID, please provide a valid target`, true); - if (!targetUuid.includes("Token")) { - throw lib.custom_error(`deleteItemPile | Please provide a Token or TokenDocument`, true); + if (!targetUuid) throw lib.custom_error(`TransferAttributes | Could not determine the UUID, please provide a valid target`, true) + + const sourceActor = source instanceof TokenDocument + ? source.actor + : source; + + const targetActor = target instanceof TokenDocument + ? target.actor + : target; + + if (Array.isArray(attributes)) { + attributes.forEach(attribute => { + if (typeof attribute !== "string") { + throw lib.custom_error(`TransferAttributes | Each attribute in the array must be of type string`, true) + } + if (!hasProperty(sourceActor.data, attribute)) { + throw lib.custom_error(`TransferAttributes | Could not find attribute ${attribute} on source's actor with UUID "${targetUuid}"`, true) + } + if (!hasProperty(targetActor.data, attribute)) { + throw lib.custom_error(`TransferAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`, true) + } + }); + } else { + Object.entries(attributes).forEach(entry => { + const [attribute, quantity] = entry; + if (!hasProperty(sourceActor.data, attribute)) { + throw lib.custom_error(`TransferAttributes | Could not find attribute ${attribute} on source's actor with UUID "${targetUuid}"`, true) + } + if (!hasProperty(targetActor.data, attribute)) { + throw lib.custom_error(`TransferAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`, true) + } + if (!lib.is_real_number(quantity) && quantity > 0) { + throw lib.custom_error(`TransferAttributes | Attribute "${attribute}" must be of type number and greater than 0`, true) + } + }); } - const hookResult = Hooks.call(HOOKS.PILE.PRE_DELETE, target); - if (hookResult === false) return; - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.DELETE_PILE, targetUuid); + + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.TRANSFER_ATTRIBUTES, sourceUuid, targetUuid, attributes); + } - static async _deleteItemPile(targetUuid) { - const target = await lib.getToken(targetUuid); - return target.delete(); - } + /** + * @private + */ + static async _transferAttributes(sourceUuid, targetUuid, attributes, { isEverything = false } = {}) { + + const attributesRemoved = await API._removeAttributes(sourceUuid, attributes, { isTransfer: true }); + + const attributesAdded = await API._addAttributes(targetUuid, attributesRemoved, { isTransfer: true }); + + if (!isEverything) { + + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ATTRIBUTE.TRANSFER, sourceUuid, targetUuid, attributesAdded); + + const macroData = { + action: "transferAttributes", + source: sourceUuid, + target: targetUuid, + attributes: attributesAdded + }; + await API._executeItemPileMacro(sourceUuid, macroData); + await API._executeItemPileMacro(targetUuid, macroData); + + const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(sourceUuid); + await API.rerenderItemPileInventoryApplication(sourceUuid, shouldBeDeleted); + await API.rerenderItemPileInventoryApplication(targetUuid); - /* -------- UTILITY METHODS -------- */ + if (shouldBeDeleted) { + await API._deleteItemPile(sourceUuid); + } - /** - * Checks whether an item (or item data) is of a type that is not allowed. If an array whether that type is allowed - * or not, returning the type if it is NOT allowed. - * - * @param {Item/Object} item - * @param {array/boolean} [itemTypeFilters=false] - * @return {boolean/string} - */ - static isItemTypeDisallowed(item, itemTypeFilters = false) { - if (!API.ITEM_TYPE_ATTRIBUTE) return false; - if (!Array.isArray(itemTypeFilters)) itemTypeFilters = API.ITEM_TYPE_FILTERS; - const itemType = getProperty(item, API.ITEM_TYPE_ATTRIBUTE); - if (itemTypeFilters.includes(itemType)) { - return itemType; } - return false; - } - /** - * Whether a given document is a valid pile or not - * - * @param {TokenDocument|Actor} document - * @return {boolean} - */ - static isValidItemPile(document) { - const documentActor = document instanceof TokenDocument ? document.actor : document; - return document && !document.destroyed && documentActor && lib.getItemPileData(document)?.enabled; + return attributesAdded + } /** - * Whether the item pile is empty + * Transfers all dynamic attributes from a source to a target, removing it or subtracting from the source and adding them to the target * - * @param {TokenDocument|Actor} target - * @returns {boolean} + * @param {Actor/Token/TokenDocument} source The source to transfer the attributes from + * @param {Actor/Token/TokenDocument} target The target to transfer the attributes to + * + * @returns {Promise} An object containing a key value pair of each attribute transferred, the key being the attribute path and its value being the quantity that was transferred */ - static isItemPileEmpty(target){ - const targetActor = target instanceof TokenDocument - ? target.actor - : target; + static async transferAllAttributes(source, target) { - if(!targetActor) return false; + const hookResult = Hooks.call(HOOKS.ATTRIBUTE.PRE_TRANSFER_ALL, source, target); + if (hookResult === false) return; - const hasNoItems = API.getItemPileItems(target).length === 0; + const sourceUuid = lib.getUuid(source); + if (!sourceUuid) throw lib.custom_error(`TransferAllAttributes | Could not determine the UUID, please provide a valid source`, true); - const attributes = API.getItemPileAttributes(target); - const hasEmptyAttributes = attributes.find(attribute => { - return hasProperty(targetActor.data, attribute.path) && getProperty(targetActor.data, attribute.path) === 0; - }) - return hasNoItems && hasEmptyAttributes; - } + const targetUuid = lib.getUuid(target); + if (!targetUuid) throw lib.custom_error(`TransferAllAttributes | Could not determine the UUID, please provide a valid target`, true); + + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.TRANSFER_ALL_ATTRIBUTES, sourceUuid, targetUuid); - /** - * Returns the item type filters for a given item pile - * - * @param target - * @returns {Array} - */ - static getItemPileItemTypeFilters(target){ - if(!API.isValidItemPile(target)) return []; - const pileData = lib.getItemPileData(target); - return pileData.itemTypeFilters - ? pileData.itemTypeFilters.split(',').map(str => str.trim().toLowerCase()) - : API.ITEM_TYPE_FILTERS; } /** - * Returns the items this item pile can transfer - * - * @param {TokenDocument|Actor} target - * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to pile settings or module settings if none provided - * @returns {Array} + * @private */ - static getItemPileItems(target, itemTypeFilters = false){ + static async _transferAllAttributes(sourceUuid, targetUuid, { isEverything = false } = {}) { - if(!API.isValidItemPile(target)) return []; + const source = await fromUuid(sourceUuid); - const pileItemFilters = Array.isArray(itemTypeFilters) - ? new Set(itemTypeFilters) - : new Set(API.getItemPileItemTypeFilters(target)); + const sourceActor = source instanceof TokenDocument + ? source.actor + : source; + + const target = await fromUuid(targetUuid); const targetActor = target instanceof TokenDocument ? target.actor : target; - return Array.from(targetActor.items).filter(item => { - const itemType = getProperty(item.data, API.ITEM_TYPE_ATTRIBUTE); - return !pileItemFilters.has(itemType); - }) + const sourceAttributes = API.getItemPileAttributes(sourceActor); - } + const attributesToTransfer = sourceAttributes.filter(attribute => { + return hasProperty(sourceActor.data, attribute.path) + && getProperty(sourceActor.data, attribute.path) > 0 + && hasProperty(targetActor.data, attribute.path); + }).map(attribute => attribute.path); + + const attributesRemoved = await API._removeAttributes(sourceUuid, attributesToTransfer, { isTransfer: true }); + const attributesAdded = await API._addAttributes(targetUuid, attributesRemoved, { isTransfer: true }); + + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.ATTRIBUTE.TRANSFER_ALL, sourceUuid, targetUuid, attributesAdded); + + if (!isEverything) { + + const macroData = { + action: "transferAllAttributes", + source: sourceUuid, + target: targetUuid, + attributes: attributesAdded + }; + await API._executeItemPileMacro(sourceUuid, macroData); + await API._executeItemPileMacro(targetUuid, macroData); + + const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(sourceUuid); + await API.rerenderItemPileInventoryApplication(sourceUuid, shouldBeDeleted); + + if (shouldBeDeleted) { + await API._deleteItemPile(sourceUuid); + } + + } + + return attributesAdded; - /** - * Returns the attributes this item pile can transfer - * - * @param {TokenDocument|Actor} target - * @returns {array} - */ - static getItemPileAttributes(target){ - const pileData = lib.getItemPileData(target); - return pileData.overrideAttributes || API.DYNAMIC_ATTRIBUTES; } /** - * Refreshes the target image of an item pile, ensuring it remains in sync + * Transfers all items and attributes between the source and the target. * - * @param target - * @return {Promise} + * @param {Actor/Token/TokenDocument} source The actor to transfer all items and attributes from + * @param {Actor/Token/TokenDocument} target The actor to receive all the items and attributes + * @param {array/boolean} [itemTypeFilters=false] Array of item types disallowed - will default to module settings if none provided + * + * @returns {Promise} An object containing all items and attributes transferred to the target */ - static async refreshItemPile(target) { - if (!API.isValidItemPile(target)) return; + static async transferEverything(source, target, { itemTypeFilters = false } = {}) { + + const hookResult = Hooks.call(HOOKS.PRE_TRANSFER_EVERYTHING, source, target, itemTypeFilters); + if (hookResult === false) return; + + const sourceUuid = lib.getUuid(source); + if (!sourceUuid) throw lib.custom_error(`TransferEverything | Could not determine the UUID, please provide a valid source`, true) + const targetUuid = lib.getUuid(target); - return itemPileSocket.executeAsGM(SOCKET_HANDLERS.REFRESH_PILE, targetUuid) + if (!targetUuid) throw lib.custom_error(`TransferEverything | Could not determine the UUID, please provide a valid target`, true) + + if (itemTypeFilters) { + itemTypeFilters.forEach(filter => { + if (typeof filter !== "string") throw lib.custom_error(`TransferEverything | entries in the itemTypeFilters must be of type string`); + }) + } + + return itemPileSocket.executeAsGM(SOCKET_HANDLERS.TRANSFER_EVERYTHING, sourceUuid, targetUuid, { itemTypeFilters }) + } /** * @private */ - static async _refreshItemPile(targetUuid) { - const targetDocument = await fromUuid(targetUuid); + static async _transferEverything(sourceUuid, targetUuid, { itemTypeFilters = false } = {}) { - if (!API.isValidItemPile(targetDocument)) return; + const itemsTransferred = await API._transferAllItems(sourceUuid, targetUuid, { + itemTypeFilters, + isEverything: true + }); + const attributesTransferred = await API._transferAllAttributes(sourceUuid, targetUuid, { isEverything: true }); - let targets = [targetDocument] - if (targetDocument instanceof Actor) { - targets = Array.from(canvas.tokens.getDocuments()).filter(token => token.actor === targetDocument); + await itemPileSocket.executeForEveryone(SOCKET_HANDLERS.CALL_HOOK, HOOKS.TRANSFER_EVERYTHING, sourceUuid, targetUuid, itemsTransferred, attributesTransferred); + + const macroData = { + action: "transferEverything", + source: sourceUuid, + target: targetUuid, + items: itemsTransferred, + attributes: attributesTransferred + }; + await API._executeItemPileMacro(sourceUuid, macroData); + await API._executeItemPileMacro(targetUuid, macroData); + + const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(sourceUuid); + await API.rerenderItemPileInventoryApplication(sourceUuid, shouldBeDeleted); + await API.rerenderItemPileInventoryApplication(targetUuid); + + if (shouldBeDeleted) { + await API._deleteItemPile(sourceUuid); } - return Promise.allSettled(targets.map(_target => { - return new Promise(async (resolve) => { - const uuid = lib.getUuid(_target); - const shouldBeDeleted = await API._checkItemPileShouldBeDeleted(uuid); - if (!shouldBeDeleted) { - await _target.update({ - "img": API._getItemPileTokenImage(targetDocument), - "scale": API._getItemPileTokenScale(targetDocument), - }) - } - resolve(); - }) - })); + return { + itemsTransferred, + attributesTransferred + }; + } + /* -------- UTILITY METHODS -------- */ + /** - * Causes all connected users to re-render a specific pile's inventory UI + * Causes every user's token HUD to rerender * - * @param {string} inPileUuid The uuid of the pile to be re-rendered - * @param {boolean} [deleted=false] Whether the pile was deleted as a part of this re-render * @return {Promise} */ - static async rerenderItemPileInventoryApplication(inPileUuid, deleted = false) { - return itemPileSocket.executeForEveryone(SOCKET_HANDLERS.RERENDER_PILE_INVENTORY, inPileUuid, deleted); + static async rerenderTokenHud() { + return itemPileSocket.executeForEveryone(SOCKET_HANDLERS.RERENDER_TOKEN_HUD); } /** * @private */ - static async _rerenderItemPileInventoryApplication(inPileUuid, deleted = false) { - return ItemPileInventory.rerenderActiveApp(inPileUuid, deleted); + static async _rerenderTokenHud() { + if (!canvas.tokens.hud.rendered) return; + await canvas.tokens.hud.render(true) + return true; + } + + /** + * Checks whether an item (or item data) is of a type that is not allowed. If an array whether that type is allowed + * or not, returning the type if it is NOT allowed. + * + * @param {Item/Object} item + * @param {array/boolean} [itemTypeFilters=false] + * @return {boolean/string} + */ + static isItemTypeDisallowed(item, itemTypeFilters = false) { + if (!API.ITEM_TYPE_ATTRIBUTE) return false; + if (!Array.isArray(itemTypeFilters)) itemTypeFilters = API.ITEM_TYPE_FILTERS; + const itemType = getProperty(item, API.ITEM_TYPE_ATTRIBUTE); + if (itemTypeFilters.includes(itemType)) { + return itemType; + } + return false; } /* -------- PRIVATE ITEM PILE METHODS -------- */ diff --git a/scripts/module.js b/scripts/module.js index b915765d..00f93b53 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -58,7 +58,6 @@ Hooks.once("ready", () => { throw lib.custom_error(`Item Piles requires the 'socketlib' module. Please ${word} it.`) } checkSystem(); - checkIncompatibilities(); registerHotkeys(); Hooks.callAll(HOOKS.READY); }) diff --git a/scripts/settings.js b/scripts/settings.js index a6f22d7c..ddc4c138 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -214,21 +214,4 @@ export async function checkSystem(){ applyDefaultSettings(); } -} - -export async function checkIncompatibilities(){ - - - if(game.settings.get('monks-active-tiles', 'drop-item') && !game.settings.get(CONSTANTS.MODULE_NAME, 'monksActiveTilesDropItemWarning')){ - await Dialog.prompt({ - title: "Module Incompatibility", - content: lib.dialogWarning(`The module "Monks Active Tile Triggers" has a setting called "Drop Item on Canvas", which when enabled creates a tile when an item is dropped on the canvas. This clashes with Item Piles, and it is recommended you turn this setting off.`), - label: "OK", - callback: () => { - game.settings.set(CONSTANTS.MODULE_NAME, 'monksActiveTilesDropItemWarning', true) - } - }); - } - - } \ No newline at end of file