diff --git a/changelog.md b/changelog.md index e5ab450e..b08d1ca5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,24 @@ # Item Piles Changelog +## Version 2.8.2 + +- Added API endpoints: + - `game.itempiles.API.getItemCategories()` + - `game.itempiles.API.getCostOfItem()` + - `game.itempiles.API.getItemQuantity()` + - `game.itempiles.API.calculateCurrencies()` which can be used to calculate different currency strings; + - `game.itempiles.API.calculateCurrencies("10gp", "5gp")` would result in 5GP (wow, impressive right?) + - `game.itempiles.API.calculateCurrencies("9gp 4sp 9cp", "5sp 4cp")` would result in `8GP 9SP 4CP`, which is a bit more impressive + - `game.itempiles.API.calculateCurrencies("9gp 4sp 9cp", "5sp 4cp", false)` would result in `10GP 3CP` + - `game.itempiles.API.calculateCurrencies("9gp", 0.5)` would result in `4GP 5EP` +- Deprecated `game.itempiles.API.getPaymentDataFromString()` in favor of `game.itempiles.API.getPaymentData()` +- Updated `game.itempiles.API.getPaymentData()` to also accept a number as its first argument +- Added support for Custom System Builder property paths +- Added support for soft migrations to systems, which will avoid customized settings being overwritten +- Fixed DnD5e's race type item being able to be dropped +- Fixed Alien RPG's currency quantity +- Fixed Forbidden Land's currency paths + ## Version 2.8.1 - Fixed D&D5e displaying all characters for players when giving items, instead of just the ones they have at least limited visibility of diff --git a/docs/api.md b/docs/api.md index 40256941..2929d6c8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -66,9 +66,14 @@ * [transferCurrencies](#transferCurrencies) * [transferAllCurrencies](#transferAllCurrencies) * [getCurrenciesFromString](#getCurrenciesFromString) - * [getPaymentDataFromString](#getPaymentDataFromString) + * [calculateCurrencies](#calculateCurrencies) + * [getPaymentData](#getPaymentData) * [getActorItems](#getActorItems) * [getActorCurrencies](#getActorCurrencies) + * [getCostOfItem](#getCostOfItem) + * [isItemInvalid](#isItemInvalid) + * [canItemStack](#canItemStack) + * [getItemQuantity](#getItemQuantity) * [Misc methods](#misc-methods) @@ -285,6 +290,8 @@ Sets the types of items that will always be considered unique when transferring |------------|----------| | inDefaults | `Object` | +--- + ### setTokenFlagDefaults `game.itempiles.API.setTokenFlagDefaults(inDefaults)` ⇒ `Promise` @@ -303,24 +310,49 @@ Sets the types of items that will always be considered unique when transferring A combination of all the methods above, but this integrates a system's specific settings more readily into item piles, allowing users to also change the settings afterwards. -| Param | Type | Description | -|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| -| data | `object` | | -| data.VERSION | `string` | The integration version | -| data.ACTOR_CLASS_TYPE | `string` | The system's actor class type to represent item piles | -| data.ITEM_PRICE_ATTRIBUTE | `string` | The property path to the system's item price attribute | -| data.ITEM_QUANTITY_ATTRIBUTE | `string` | The property path to the system's item quantity attribute | -| data.ITEM_FILTERS | `Array<{path: string, filters: string}>` | The filters to determine which items to not include as droppable or tradeable | -| data.ITEM_SIMILARITIES | `Array` | The array of property path strings used to determine item similarities | -| data.UNSTACKABLE_ITEM_TYPES | `Array` | The array of property path strings used to determine item types that cannot stack, no matter what | -| data.PILE_DEFAULTS | `Object` | The system specific default values for item pile actors created in this system | -| data.TOKEN_FLAG_DEFAULTS | `Object` | The system specific default values for item pile tokens created in this system | -| data.ITEM_TRANSFORMER | `undefined/Function` | An optional function that gets run over items before picked up, traded, or bought | -| data.PRICE_MODIFIER_TRANSFORMER | `undefined/Function` | An optional function that gets run when fetching the price modifier of an actor | -| data.SYSTEM_HOOKS | `undefined/Function` | An optional function that runs and sets up system specific hooks relating to Item Piles | -| data.CURRENCIES | `Array<{ primary: boolean, type: string ["attribute"/"item"], img: string, abbreviation: string, data: Object<{ path: string } / { uuid: string } / { item: object }>, exchangeRate: number }>` | The array of currencies for this system | -| data.SECONDARY_CURRENCIES | `Array<{ type: string ["attribute"/"item"], img: string, abbreviation: string, data: Object<{ path: string } / { uuid: string } / { item: object }> }>` | The array of secondary currencies for this system | -| data.CURRENCY_DECIMAL_DIGITS | `undefined/number` | How many decimals should be shown for fractional amounts of currency (only works when only 1 currency is configured) | +| Param | Type | Description | +|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| +| data | `object` | | +| data.VERSION | `string` | The integration version | +| data.ACTOR_CLASS_TYPE | `string` | The system's actor class type to represent item piles | +| data.ITEM_PRICE_ATTRIBUTE | `string` | The property path to the system's item price attribute | +| data.ITEM_QUANTITY_ATTRIBUTE | `string` | The property path to the system's item quantity attribute | +| data.QUANTITY_FOR_PRICE_ATTRIBUTE | `string` | The property path to the system's item quantity per price attribute | +| data.ITEM_FILTERS | `Array<{path: string, filters: string}>` | The filters to determine which items to not include as droppable or tradeable | +| data.ITEM_SIMILARITIES | `Array` | The array of property path strings used to determine item similarities | +| data.UNSTACKABLE_ITEM_TYPES | `Array` | The array of property path strings used to determine item types that cannot stack, no matter what | +| data.PILE_DEFAULTS | `Object` | The system specific default values for item pile actors created in this system | +| data.TOKEN_FLAG_DEFAULTS | `Object` | The system specific default values for item pile tokens created in this system | +| data.ITEM_TRANSFORMER | `undefined/Function` | An optional function that gets run over items before picked up, traded, or bought | +| data.PRICE_MODIFIER_TRANSFORMER | `undefined/Function` | An optional function that gets run when fetching the price modifier of an actor | +| data.SYSTEM_HOOKS | `undefined/Function` | An optional function that runs and sets up system specific hooks relating to Item Piles | +| data.SHEET_OVERRIDES | `undefined/Function` | An optional function that runs and sets up system specific sheet hooks to handle system specific implementations | +| data.CURRENCIES | `Array<{ primary: boolean, type: string ["attribute"/"item"], img: string, abbreviation: string, data: Object<{ path: string } / { uuid: string } / { item: object }>, exchangeRate: number }>` | The array of currencies for this system | +| data.SECONDARY_CURRENCIES | `Array<{ type: string ["attribute"/"item"], img: string, abbreviation: string, data: Object<{ path: string } / { uuid: string } / { item: object }> }>` | The array of secondary currencies for this system | +| data.CURRENCY_DECIMAL_DIGITS | `undefined/number` | How many decimals should be shown for fractional amounts of currency (only works when only 1 currency is configured) | + +--- + +### getPrimaryCurrency + +`game.itempiles.API.getPrimaryCurrency(actor)` ⇒ `Promise` + +Retrieves the system default currencies, or an actor's default currencies + +**Returns +**: `Array<{primary: boolean, name: string, data: Object, img: string, abbreviation: string, exchange: number}>` + +| Param | Type | +|-------|-----------------| +| actor | `boolean/Actor` | + +--- + +### getItemCategories + +`game.itempiles.API.getItemCategories()` ⇒ `Object` + +Retrieves all the system item types, including custom item piles item categories --- @@ -898,11 +930,11 @@ Transfers all currencies between the source and the target. ### getCurrenciesFromString -`game.itempiles.API.getCurrenciesFromString(currencies)` ⇒ `Promise` +`game.itempiles.API.getCurrenciesFromString(currencies)` ⇒ `object` Turns a string of currencies into an array containing the data and quantities for each currency -**Returns**: `Promise` - An array of object containing the data and quantity for each currency +**Returns**: `object` - An array of object containing the data and quantity for each currency | Param | Type | Default | Description | |------------|----------|---------|------------------------------------------------| @@ -910,19 +942,39 @@ Turns a string of currencies into an array containing the data and quantities fo --- -### getPaymentDataFromString +### calculateCurrencies -`game.itempiles.API.getPaymentDataFromString(currencies, options)` ⇒ `Promise` +`game.itempiles.API.calculateCurrencies(firstCurrencies, secondCurrencies, subtract)` ⇒ `string` -Turns a string of currencies into an object containing payment data, and the change an optional target would receive back +This method takes a string, and another string or number, an alternatively a boolean, to modify the first string's currencies.Whether to subtract the second currencies from the first; not needed if the second argument is a number -**Returns**: `Promise` - An object containing the price data +**Returns**: `string` - The resulting currency string -| Param | Type | Default | Description | -|------------------|------------------------------------|---------|------------------------------------------------| -| price | `string` | | A string of currencies to add (eg, "5gp 25sp") | -| options | `object` | | Options to pass to the function | -| [options.target] | `string/Actor/TokenDocument/Token` | `false` | The target whose currencies to check against | +| Param | Type | Default | Description | +|------------------|-----------------|---------|---------------------------------------------------------------------------------------------------------| +| firstCurrencies | `string` | | The starting sum of money as strings (eg, "5gp 25sp") | +| secondCurrencies | `string/number` | | A string of currencies to alter the first with, or a number to multiply it | +| subtract | `boolean` | `false` | Whether to subtract the second currencies from the first; not needed if the second argument is a number | + +--- + +### getPaymentData + +`game.itempiles.API.getPaymentData(currencies, options)` ⇒ `object` + +Turns a string of currencies or a number into an object containing payment data, and the change an optional target would receive back + +**Returns**: `object` - An object containing the price data + +| Param | Type | Default | Description | +|--------------------|------------------------------------|---------|------------------------------------------------------------| +| price | `string/number` | | A string of currencies to add (eg, "5gp 25sp") or a number | +| options | `object` | | Options to pass to the function | +| [options.quantity] | `number` | `1` | The number of this to buy | +| [options.target] | `string/Actor/TokenDocument/Token` | `false` | The target whose currencies to check against | + +**Previously (now deprecated):** +`game.itempiles.API.getPaymentDataFromString` --- @@ -956,6 +1008,62 @@ Turns a string of currencies into an object containing payment data, and the cha --- +### getCostOfItem + +`game.itempiles.API.getCostOfItem(item)` ⇒ `number` + +Retrieves the total numerical cost of an item + +**Returns**: `number` + +| Param | Type | Default | Description | +|-------|--------|---------|----------------------------| +| item | `Item` | | The item whose cost to get | + +--- + +### isItemInvalid + +`game.itempiles.API.isItemInvalid(item)` ⇒ `boolean` + +Returns a boolean whether the item is a valid physical item + +**Returns**: `boolean` + +| Param | Type | Default | Description | +|-------|--------|---------|-------------------| +| item | `Item` | | The item to check | + +--- + +### canItemStack + +`game.itempiles.API.canItemStack(item)` ⇒ `boolean` + +Returns whether an item can be stacked or not + +**Returns**: `boolean` + +| Param | Type | Default | Description | +|-------|--------|---------|-------------------| +| item | `Item` | | The item to check | + +--- + +### getItemQuantity + +`game.itempiles.API.getItemQuantity(item)` ⇒ `number` + +Returns the item's quantity + +**Returns**: `number` + +| Param | Type | Default | Description | +|-------|--------|---------|-------------------| +| item | `Item` | | The item to check | + +--- + ## Misc methods ### rollItemTable diff --git a/languages/en.json b/languages/en.json index c4fe0c96..1dc3b183 100644 --- a/languages/en.json +++ b/languages/en.json @@ -526,6 +526,12 @@ "DeleteWhenEmptyDefault": "Default module setting", "DeleteWhenEmptyYes": "Yes, delete when empty", "DeleteWhenEmptyNo": "No, don't delete when empty", + "CanStackItems": "Can Stack Items", + "CanStackItemsExplanation": "With this setting, items added to the item pile will stack with similar items. This can also be controlled per-item in their configuration. Note: This is dependent on the system as well, since some items do not have quantity so they can never be stacked.", + "CanStackItemsYes": "Yes, stack items (unless item says otherwise)", + "CanStackItemsNo": "No, stack items (unless item says otherwise)", + "CanStackItemsYesAlways": "Yes, always stack items", + "CanStackItemsNoAlways": "No, never stack items", "OverrideCurrencies": "Override Currencies", "OverrideCurrenciesExplanation": "Configure if this pile should be able to transfer other currencies than the default.", "ConfigureOverrideCurrencies": "Configure Override Currencies", @@ -612,6 +618,8 @@ "OpenStatusOpen": "Open", "OpenStatusClosed": "Closed", "OpenStatusAuto": "Automatic (Simple Calendar)", + "HideTokenWhenClosed": "Hide Token When Closed", + "HideTokenWhenClosedExplanation": "When this is enabled, the token(s) of this merchant are hidden when closed.", "ClosedDays": "Closed Days (Simple Calendar)", "ClosedDaysExplanation": "On the days checked, the merchant will be closed (if the merchant open status is set to automatic).", "ClosedHolidays": "Closed Holidays (Simple Calendar)", @@ -647,8 +655,6 @@ "LayoutExplanation": "This is how many columns and rows should be visible in the vault.", "Columns": "Columns", "Rows": "Rows", - "CanStackItems": "Can Stack Items", - "CanStackItemsExplanation": "With this enabled, items added to the vault will stack with similar items. This can also be controlled per-item in their configuration.", "EnableExpansion": "Enable Expansion Items", "EnableExpansionExplanation": "With this enabled, items that are configured as vault expanders will expand the vault's available space.", "BaseExpansion": "Base Expansion", diff --git a/src/API/api.js b/src/API/api.js index c9ca1cb4..0be1cff6 100644 --- a/src/API/api.js +++ b/src/API/api.js @@ -9,2269 +9,2382 @@ import ItemPileSocket from "../socket.js"; import TradeAPI from "./trade-api.js"; import PrivateAPI from "./private-api.js"; import { SYSTEMS } from "../systems.js"; +import ItemPileConfig from "../applications/item-pile-config/item-pile-config.js"; class API { - /** - * @class API - */ - - /** - * The actor class type used for the original item pile actor in this system - * - * @returns {string} - */ - static get ACTOR_CLASS_TYPE() { - return Helpers.getSetting(SETTINGS.ACTOR_CLASS_TYPE); - } - - /** - * The currencies used in this system - * - * @returns {Array<{primary: boolean, name: string, data: Object, img: string, abbreviation: string, exchange: number}>} - */ - static get CURRENCIES() { - return Helpers.getSetting(SETTINGS.CURRENCIES).map(currency => { - if (currency.type === "item" && currency.data.uuid) { - const compendiumItem = CompendiumUtilities.getItemFromCache(currency.data.uuid); - if (compendiumItem) { - currency.data.item = compendiumItem; - } - } - return currency; - }); - } - - /** - * The secondary currencies used in this system - * - * @returns {Array<{name: string, data: Object, img: string, abbreviation: string}>} - */ - static get SECONDARY_CURRENCIES() { - return Helpers.getSetting(SETTINGS.SECONDARY_CURRENCIES).map(currency => { - if (currency.type === "item" && currency.data.uuid) { - const compendiumItem = CompendiumUtilities.getItemFromCache(currency.data.uuid); - if (compendiumItem) { - currency.data.item = compendiumItem; - } - } - return currency; - }); - } - - /** - * The smallest decimal digits shown for any fractional currency amounts. Only used when there is only one currency. - * - * @returns {Number} - */ - static get CURRENCY_DECIMAL_DIGITS() { - return Helpers.getSetting(SETTINGS.CURRENCY_DECIMAL_DIGITS); - } - - /** - * The attribute used to track the price of items in this system - * - * @returns {string} - */ - static get ITEM_PRICE_ATTRIBUTE() { - return Helpers.getSetting(SETTINGS.ITEM_PRICE_ATTRIBUTE); - } - - /** - * The attribute used to track the quantity of items you would get for the price of an item - * - * @returns {string} - */ - static get QUANTITY_FOR_PRICE_ATTRIBUTE() { - return Helpers.getSetting(SETTINGS.QUANTITY_FOR_PRICE_ATTRIBUTE); - } - - /** - * The attribute used to track the quantity of items in this system - * - * @returns {string} - */ - static get ITEM_QUANTITY_ATTRIBUTE() { - return Helpers.getSetting(SETTINGS.ITEM_QUANTITY_ATTRIBUTE); - } - - /** - * The filters for item types eligible for interaction within this system - * - * @returns {Array<{name: string, filters: string}>} - */ - static get ITEM_FILTERS() { - return Helpers.getSetting(SETTINGS.ITEM_FILTERS); - } - - /** - * The attributes for detecting item similarities - * - * @returns {Array} - */ - static get ITEM_SIMILARITIES() { - return Helpers.getSetting(SETTINGS.ITEM_SIMILARITIES); - } - - /** - * The types of items that will always be considered unique when transferring between actors - * - * @returns {Array} - */ - static get UNSTACKABLE_ITEM_TYPES() { - return Helpers.getSetting(SETTINGS.UNSTACKABLE_ITEM_TYPES); - } - - /** - * The system specific default values for item pile actors created in this system - * - * @returns {Object} - */ - static get PILE_DEFAULTS() { - return Helpers.getSetting(SETTINGS.PILE_DEFAULTS); - } - - /** - * The system specific default values for item pile tokens created in this system - * - * @returns {Object} - */ - static get TOKEN_FLAG_DEFAULTS() { - return Helpers.getSetting(SETTINGS.TOKEN_FLAG_DEFAULTS); - } - - /** - * Sets the actor class type used for the original item pile actor in this system - * - * @param {string} inClassType - * @returns {Promise} - */ - static async setActorClassType(inClassType) { - if (typeof inClassType !== "string") { - throw Helpers.custom_error("setActorTypeClass | inClassType must be of type string"); - } - return Helpers.setSetting(SETTINGS.ACTOR_CLASS_TYPE, inClassType); - } - - /** - * Sets the currencies used in this system - * - * @param {Array} inCurrencies - * @returns {Promise} - */ - static async setCurrencies(inCurrencies) { - if (!Array.isArray(inCurrencies)) { - throw Helpers.custom_error("setCurrencies | inCurrencies must be an array"); - } - inCurrencies.forEach(currency => { - if (typeof currency !== "object") { - throw Helpers.custom_error("setCurrencies | each entry in inCurrencies must be of type object"); - } - if (typeof currency.primary !== "boolean") { - throw Helpers.custom_error("setCurrencies | currency.primary must be of type boolean"); - } - if (typeof currency.name !== "string") { - throw Helpers.custom_error("setCurrencies | currency.name must be of type string"); - } - if (typeof currency.abbreviation !== "string") { - throw Helpers.custom_error("setCurrencies | currency.abbreviation must be of type string"); - } - if (typeof currency.exchangeRate !== "number") { - throw Helpers.custom_error("setCurrencies | currency.exchangeRate must be of type number"); - } - if (typeof currency.data !== "object") { - throw Helpers.custom_error("setCurrencies | currency.data must be of type object"); - } - if (typeof currency.data.path !== "string" && typeof currency.data.uuid !== "string" && typeof currency.data.item !== "object") { - throw Helpers.custom_error("setCurrencies | currency.data must contain either \"path\" (string), \"uuid\" (string), or \"item\" (object)"); - } - if (currency.img && typeof currency.img !== "string") { - throw Helpers.custom_error("setCurrencies | currency.img must be of type string"); - } - }); - return Helpers.setSetting(SETTINGS.CURRENCIES, inCurrencies); - } - - /** - * Sets the secondary currencies used in this system - * - * @param {Array} inSecondaryCurrencies - * @returns {Promise} - */ - static async setSecondaryCurrencies(inSecondaryCurrencies) { - if (!Array.isArray(inSecondaryCurrencies)) { - throw Helpers.custom_error("setSecondaryCurrencies | inSecondaryCurrencies must be an array"); - } - inSecondaryCurrencies.forEach(currency => { - if (typeof currency !== "object") { - throw Helpers.custom_error("setSecondaryCurrencies | each entry in inSecondaryCurrencies must be of type object"); - } - if (typeof currency.name !== "string") { - throw Helpers.custom_error("setSecondaryCurrencies | currency.name must be of type string"); - } - if (typeof currency.abbreviation !== "string") { - throw Helpers.custom_error("setSecondaryCurrencies | currency.abbreviation must be of type string"); - } - if (typeof currency.data !== "object") { - throw Helpers.custom_error("setSecondaryCurrencies | currency.data must be of type object"); - } - if (typeof currency.data.path !== "string" && typeof currency.data.uuid !== "string" && typeof currency.data.item !== "object") { - throw Helpers.custom_error("setSecondaryCurrencies | currency.data must contain either \"path\" (string), \"uuid\" (string), or \"item\" (object)"); - } - if (currency.img && typeof currency.img !== "string") { - throw Helpers.custom_error("setSecondaryCurrencies | currency.img must be of type string"); - } - }); - return Helpers.setSetting(SETTINGS.SECONDARY_CURRENCIES, inSecondaryCurrencies); - } - - /** - * Set the smallest decimal digits shown for any fractional currency amounts. Only used when there is only one currency. - * - * @param {Number} inDecimalDigits - * @returns {Promise} - */ - static setCurrencyDecimalDigits(inDecimalDigits) { - if (typeof inDecimalDigits !== "number") { - throw Helpers.custom_error("setCurrencyDecimalDigits | inDecimalDigits must be of type string"); - } - return Helpers.setSetting(SETTINGS.CURRENCY_DECIMAL_DIGITS, inDecimalDigits); - } - - /** - * Sets the attribute used to track the quantity of items in this system - * - * @param {string} inAttribute - * @returns {Promise} - */ - static async setItemQuantityAttribute(inAttribute) { - if (typeof inAttribute !== "string") { - throw Helpers.custom_error("setItemQuantityAttribute | inAttribute must be of type string"); - } - return Helpers.setSetting(SETTINGS.ITEM_QUANTITY_ATTRIBUTE, inAttribute); - } - - /** - * Sets the attribute used to track the price of items in this system - * - * @param {string} inAttribute - * @returns {Promise} - */ - static async setItemPriceAttribute(inAttribute) { - if (typeof inAttribute !== "string") { - throw Helpers.custom_error("setItemPriceAttribute | inAttribute must be of type string"); - } - return Helpers.setSetting(SETTINGS.ITEM_PRICE_ATTRIBUTE, inAttribute); - } - - /** - * Sets the attribute used to track the price of items in this system - * - * @param {string} inAttribute - * @returns {Promise} - */ - static async setQuantityForPriceAttribute(inAttribute) { - if (typeof inAttribute !== "string") { - throw Helpers.custom_error("setQuantityForPriceAttribute | inAttribute must be of type string"); - } - return Helpers.setSetting(SETTINGS.QUANTITY_FOR_PRICE_ATTRIBUTE, inAttribute); - } - - /** - * Sets the items filters for interaction within this system - * - * @param {Array<{path: string, filters: string}>} inFilters - * @returns {Promise} - */ - static async setItemFilters(inFilters) { - if (!Array.isArray(inFilters)) { - throw Helpers.custom_error("setItemFilters | inFilters must be of type array"); - } - inFilters.forEach(filter => { - if (typeof filter?.path !== "string") { - throw Helpers.custom_error("setItemFilters | each entry in inFilters must have a \"path\" property with a value that is of type string"); - } - if (typeof filter?.filters !== "string") { - throw Helpers.custom_error("setItemFilters | each entry in inFilters must have a \"filters\" property with a value that is of type string"); - } - }); - return Helpers.setSetting(SETTINGS.ITEM_FILTERS, inFilters); - } - - /** - * Sets the attributes for detecting item similarities - * - * @param {Array} inPaths - * @returns {Promise} - */ - static async setItemSimilarities(inPaths) { - if (!Array.isArray(inPaths)) { - throw Helpers.custom_error("setItemSimilarities | inPaths must be of type array"); - } - inPaths.forEach(path => { - if (typeof path !== "string") { - throw Helpers.custom_error("setItemSimilarities | each entry in inPaths must be of type string"); - } - }); - return Helpers.setSetting(SETTINGS.ITEM_SIMILARITIES, inPaths); - } - - /** - * Sets the types of items that will always be considered unique when transferring between actors - * - * @param {Array} inTypes - * @returns {Promise} - */ - static async setUnstackableItemTypes(inTypes) { - if (!Array.isArray(inTypes)) { - throw Helpers.custom_error("setUnstackableItemTypes | inTypes must be of type array"); - } - inTypes.forEach(path => { - if (typeof path !== "string") { - throw Helpers.custom_error("setUnstackableItemTypes | each entry in inTypes must be of type string"); - } - }); - return Helpers.setSetting(SETTINGS.UNSTACKABLE_ITEM_TYPES, inTypes); - } - - /** - * Sets the types of items that will always be considered unique when transferring between actors - * - * @param {Object} inDefaults - * @returns {Promise} - */ - static async setPileDefaults(inDefaults) { - if (typeof inDefaults !== "object") { - throw Helpers.custom_error("setPileDefaults | inDefaults must be of type object"); - } - const validKeys = new Set(Object.keys(CONSTANTS.PILE_DEFAULTS)); - for (const key of Object.keys(inDefaults)) { - if (!validKeys.has(key)) { - throw Helpers.custom_error(`setPileDefaults | ${key} is not a valid pile setting`); - } - } - return Helpers.setSetting(SETTINGS.PILE_DEFAULTS, inDefaults); - } - - /** - * Set the flags that will be applied to any tokens created through item piles - * - * @param {Object} inDefaults - * @returns {Promise} - */ - static async setTokenFlagDefaults(inDefaults) { - if (typeof inDefaults !== "object") { - throw Helpers.custom_error("setTokenFlagDefaults | inDefaults must be of type object"); - } - return Helpers.setSetting(SETTINGS.TOKEN_FLAG_DEFAULTS, inDefaults); - } - - /** - * A combination of all the methods above, but this integrates a system's specific - * settings more readily into item piles, allowing users to also change the settings - * afterward. - * - * @param {Object<{ - * VERSION: string, - * ACTOR_CLASS_TYPE: string, - * ITEM_QUANTITY_ATTRIBUTE: string, - * ITEM_PRICE_ATTRIBUTE: string, - * QUANTITY_FOR_PRICE_ATTRIBUTE: string, - * ITEM_FILTERS: Array<{path: string, filters: string}>, - * ITEM_SIMILARITIES: Array, - * UNSTACKABLE_ITEM_TYPES: Array, - * PILE_DEFAULTS: Object, - * TOKEN_FLAG_DEFAULTS: Object, - * ITEM_TRANSFORMER: undefined/Function, - * PRICE_MODIFIER_TRANSFORMER: undefined/Function, - * SYSTEM_HOOKS: undefined/Function, - * SHEET_OVERRIDES: undefined/Function, - * CURRENCIES: Array<{ - * primary: boolean, - * type: string ["attribute"/"item"], - * img: string, - * abbreviation: string, - * data: Object<{ path: string }|{ uuid: string }|{ item: object }>, - * exchangeRate: number - * }>, - * SECONDARY_CURRENCIES: Array<{ - * type: string ["attribute"/"item"], - * img: string, - * abbreviation: string, - * data: Object<{ path: string }|{ uuid: string }|{ item: object }> - * }>, - * CURRENCY_DECIMAL_DIGITS: undefined/number - * }>} inData - */ - static addSystemIntegration(inData) { - - const data = foundry.utils.mergeObject({ - VERSION: "", - ACTOR_CLASS_TYPE: "", - ITEM_QUANTITY_ATTRIBUTE: "", - ITEM_PRICE_ATTRIBUTE: "", - QUANTITY_FOR_PRICE_ATTRIBUTE: "flags.item-piles.system.quantityForPrice", - ITEM_FILTERS: [], - ITEM_SIMILARITIES: [], - UNSTACKABLE_ITEM_TYPES: [], - PILE_DEFAULTS: {}, - TOKEN_FLAG_DEFAULTS: {}, - ITEM_TRANSFORMER: null, - PRICE_MODIFIER_TRANSFORMER: null, - SYSTEM_HOOKS: null, - SHEET_OVERRIDES: null, - CURRENCIES: [], - SECONDARY_CURRENCIES: [], - CURRENCY_DECIMAL_DIGITS: 0.00001 - }, inData) - - if (typeof data["VERSION"] !== "string") { - throw Helpers.custom_error("addSystemIntegration | data.VERSION must be of type string"); - } - - if (typeof data["ACTOR_CLASS_TYPE"] !== "string") { - throw Helpers.custom_error("addSystemIntegration | data.ACTOR_CLASS_TYPE must be of type string"); - } - - if (typeof data["ITEM_QUANTITY_ATTRIBUTE"] !== "string") { - throw Helpers.custom_error("addSystemIntegration | data.ITEM_QUANTITY_ATTRIBUTE must be of type string"); - } - - if (typeof data["ITEM_PRICE_ATTRIBUTE"] !== "string") { - throw Helpers.custom_error("addSystemIntegration | data.ITEM_PRICE_ATTRIBUTE must be of type string"); - } - - if (data["QUANTITY_FOR_PRICE_ATTRIBUTE"] && typeof data["QUANTITY_FOR_PRICE_ATTRIBUTE"] !== "string") { - throw Helpers.custom_error("addSystemIntegration | data.QUANTITY_FOR_PRICE_ATTRIBUTE must be of type string"); - } - - if (!Array.isArray(data["ITEM_FILTERS"])) { - throw Helpers.custom_error("addSystemIntegration | data.ITEM_FILTERS must be of type array"); - } - - data["ITEM_FILTERS"].forEach(filter => { - if (typeof filter?.path !== "string") { - throw Helpers.custom_error("addSystemIntegration | each entry in data.ITEM_FILTERS must have a \"path\" property with a value that is of type string"); - } - if (typeof filter?.filters !== "string") { - throw Helpers.custom_error("addSystemIntegration | each entry in data.ITEM_FILTERS must have a \"filters\" property with a value that is of type string"); - } - }); - - if (data['ITEM_TRANSFORMER']) { - if (!Helpers.isFunction(data['ITEM_TRANSFORMER'])) { - throw Helpers.custom_error("addSystemIntegration | data.ITEM_TRANSFORMER must be of type function"); - } - if (typeof data['ITEM_TRANSFORMER']({}) !== "object") { - throw Helpers.custom_error("addSystemIntegration | data.ITEM_TRANSFORMER's return value must be of type object"); - } - } - - if (data['PRICE_MODIFIER_TRANSFORMER']) { - if (!Helpers.isFunction(data['PRICE_MODIFIER_TRANSFORMER'])) { - throw Helpers.custom_error("addSystemIntegration | data.PRICE_MODIFIER_TRANSFORMER must be of type function"); - } - if (typeof data['PRICE_MODIFIER_TRANSFORMER']({}) !== "object") { - throw Helpers.custom_error("addSystemIntegration | data.PRICE_MODIFIER_TRANSFORMER's return value must be of type object"); - } - } - - if (data['SYSTEM_HOOKS']) { - if (!Helpers.isFunction(data['SYSTEM_HOOKS'])) { - throw Helpers.custom_error("addSystemIntegration | data.SYSTEM_HOOKS must be of type function"); - } - } - - if (data['SHEET_OVERRIDES']) { - if (!Helpers.isFunction(data['SHEET_OVERRIDES'])) { - throw Helpers.custom_error("addSystemIntegration | data.SHEET_OVERRIDES must be of type function"); - } - } - - if (typeof data['PILE_DEFAULTS'] !== "object") { - throw Helpers.custom_error("addSystemIntegration | data.PILE_DEFAULTS must be of type object"); - } - const validKeys = new Set(Object.keys(CONSTANTS.PILE_DEFAULTS)); - for (const key of Object.keys(data['PILE_DEFAULTS'])) { - if (!validKeys.has(key)) { - throw Helpers.custom_error(`addSystemIntegration | data.PILE_DEFAULTS contains illegal key "${key}" that is not a valid pile default`); - } - } - - if (typeof data['TOKEN_FLAG_DEFAULTS'] !== "object") { - throw Helpers.custom_error("addSystemIntegration | data.TOKEN_FLAG_DEFAULTS must be of type object"); - } - - if (!Array.isArray(data['ITEM_SIMILARITIES'])) { - throw Helpers.custom_error("addSystemIntegration | data.ITEM_SIMILARITIES must be of type array"); - } - data['ITEM_SIMILARITIES'].forEach(path => { - if (typeof path !== "string") { - throw Helpers.custom_error("addSystemIntegration | each entry in data.ITEM_SIMILARITIES must be of type string"); - } - }); - - if (data['UNSTACKABLE_ITEM_TYPES']) { - if (!Array.isArray(data['UNSTACKABLE_ITEM_TYPES'])) { - throw Helpers.custom_error("addSystemIntegration | data.UNSTACKABLE_ITEM_TYPES must be of type array"); - } - data['UNSTACKABLE_ITEM_TYPES'].forEach(path => { - if (typeof path !== "string") { - throw Helpers.custom_error("addSystemIntegration | each entry in data.UNSTACKABLE_ITEM_TYPES must be of type string"); - } - }); - } - - if (!Array.isArray(data['CURRENCIES'])) { - throw Helpers.custom_error("addSystemIntegration | data.CURRENCIES must be an array"); - } - data['CURRENCIES'].forEach(currency => { - if (typeof currency !== "object") { - throw Helpers.custom_error("addSystemIntegration | CURRENCIES | each entry in data.CURRENCIES must be of type object"); - } - if (typeof currency.primary !== "boolean") { - throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.primary must be of type boolean"); - } - if (typeof currency.name !== "string") { - throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.name must be of type string"); - } - if (typeof currency.abbreviation !== "string") { - throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.abbreviation must be of type string"); - } - if (typeof currency.exchangeRate !== "number") { - throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.exchangeRate must be of type number"); - } - if (typeof currency.data !== "object") { - throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.data must be of type object"); - } - if (typeof currency.data.path !== "string" && typeof currency.data.uuid !== "string" && typeof currency.data.item !== "object") { - throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.data must contain either \"path\" (string), \"uuid\" (string), or \"item\" (object)"); - } - if (currency.img && typeof currency.img !== "string") { - throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.img must be of type string"); - } - }); - - if (!Array.isArray(data['SECONDARY_CURRENCIES'])) { - throw Helpers.custom_error("addSystemIntegration | data.SECONDARY_CURRENCIES must be an array"); - } - data['SECONDARY_CURRENCIES'].forEach(currency => { - if (typeof currency !== "object") { - throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | each entry in data.SECONDARY_CURRENCIES must be of type object"); - } - if (typeof currency.name !== "string") { - throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | currency.name must be of type string"); - } - if (typeof currency.abbreviation !== "string") { - throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | currency.abbreviation must be of type string"); - } - if (typeof currency.data !== "object") { - throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | currency.data must be of type object"); - } - if (typeof currency.data.path !== "string" && typeof currency.data.uuid !== "string" && typeof currency.data.item !== "object") { - throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | currency.data must contain either \"path\" (string), \"uuid\" (string), or \"item\" (object)"); - } - if (currency.img && typeof currency.img !== "string") { - throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | currency.img must be of type string"); - } - }); - - if (data["CURRENCY_DECIMAL_DIGITS"] && typeof data['CURRENCY_DECIMAL_DIGITS'] !== "number") { - throw Helpers.custom_error("addSystemIntegration | data.CURRENCY_DECIMAL_DIGITS must be of type number"); - } - - data['INTEGRATION'] = true; - - SYSTEMS.addSystem(data); - - Helpers.debug(`Registered system settings for ${game.system.id}`, data) - - } - - static async getPrimaryCurrency(actor = false) { - if (actor && actor instanceof Actor) { - return PileUtilities.getActorPrimaryCurrency(actor); - } - return this.CURRENCIES.find(currency => currency.primary); - } - - /* ================= ITEM PILE METHODS ================= */ - - /** - * Creates an item pile token at a location, or an item pile actor, or both at the same time. - * - * @param {object} options Options to pass to the function - * @param {object/boolean} [options.position=false] Where to create the item pile, with x and y coordinates - * @param {string/boolean} [options.sceneId=game.user.viewedScene] Which scene to create the item pile on - * @param {object} [options.tokenOverrides={}] Token data to apply to the newly created token - * @param {object} [options.actorOverrides={}] Actor data to apply to the newly created actor (if unlinked) - * @param {object} [options.itemPileFlags={}] Item pile specific flags to apply to the token and actor - * @param {Array/boolean} [options.items=false] Any items to create on the item pile - * @param {boolean} [options.createActor=false] Whether to create a new item pile actor - * @param {string/boolean} [options.actor=false] The UUID, ID, or name of the actor to use when creating this item pile - * @param {Array/>string/boolean} [options.folders=false] The folder to create the actor in, this can be an array of folder names, which will be traversed and created - * - * @returns {Promise} - */ - static async createItemPile({ - position = false, - sceneId = game.user.viewedScene, - tokenOverrides = {}, - actorOverrides = {}, - itemPileFlags = {}, - items = false, - createActor = false, - actor = false, - folders = false - } = {}) { - - if (position) { - if (typeof position !== "object") { - throw Helpers.custom_error(`createItemPile | position must be of type object`); - } else if (!Helpers.isRealNumber(position.x) || !Helpers.isRealNumber(position.y)) { - throw Helpers.custom_error(`createItemPile | position.x and position.y must be of type numbers`); - } - } - - if (folders) { - if (!Array.isArray(folders)) { - folders = [folders]; - } - folders.forEach(f => { - if (typeof f !== 'string') { - throw Helpers.custom_error(`createItemPile | folder must be of type string or array of strings`); - } - }); - } - - if (actor && !createActor) { - if (typeof actor !== "string") { - throw Helpers.custom_error(`createItemPile | actor must be of type string`); - } - let pileActor = await fromUuid(actor); - if (!pileActor) { - pileActor = game.actors.getName(actor); - } - if (!pileActor) { - pileActor = game.actors.get(actor); - } - if (!pileActor) { - throw Helpers.custom_error(`createItemPile | Could not find actor with the identifier of "${actor}"`); - } - actor = pileActor.uuid; - } - - if (typeof sceneId !== "string") { - throw Helpers.custom_error(`createItemPile | sceneId must be of type string`); - } - - if (typeof tokenOverrides !== "object") { - throw Helpers.custom_error(`createItemPile | tokenOverrides must be of type object`); - } - - if (typeof actorOverrides !== "object") { - throw Helpers.custom_error(`createItemPile | tokenOverrides must be of type object`); - } - - if (typeof itemPileFlags !== "object") { - throw Helpers.custom_error(`createItemPile | tokenOverrides must be of type object`); - } - - if (items) { - if (!Array.isArray(items)) items = [items] - items = items.map(item => { - return item instanceof Item ? item.toObject() : item; - }) - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.CREATE_PILE, { - sceneId, position, actor, createActor, items, tokenOverrides, actorOverrides, itemPileFlags, folders - }); - } - - /** - * Turns tokens and its actors into item piles - * - * @param {Token/TokenDocument/Array} targets The targets to be turned into item piles - * @param {object} options Options to pass to the function - * @param {object} options.pileSettings Overriding settings to be put on the item piles' settings - * @param {object/Function} options.tokenSettings Overriding settings that will update the tokens' settings - * - * @return {Promise} The uuids of the targets after they were turned into item piles - */ - static turnTokensIntoItemPiles(targets, { pileSettings = {}, tokenSettings = {} } = {}) { - - if (!Array.isArray(targets)) targets = [targets]; - - const targetUuids = targets.map(target => { - if (!(target instanceof Token || target instanceof TokenDocument)) { - throw Helpers.custom_error(`turnTokensIntoItemPiles | Target must be of type Token or TokenDocument`) - } - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`turnTokensIntoItemPiles | Could not determine the UUID, please provide a valid target`) - return targetUuid; - }) - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TURN_INTO_PILE, targetUuids, pileSettings, tokenSettings); - } - - /** - * Reverts tokens from an item pile into a normal token and actor - * - * @param {Token/TokenDocument/Array} targets The targets to be reverted from item piles - * @param {object} options Options to pass to the function - * @param {object/Function} options.tokenSettings Overriding settings that will update the tokens - * - * @return {Promise} The uuids of the targets after they were reverted from being item piles - */ - static revertTokensFromItemPiles(targets, { tokenSettings = {} } = {}) { - - if (!Array.isArray(targets)) targets = [targets]; - - const targetUuids = targets.map(target => { - if (!(target instanceof Token || target instanceof TokenDocument)) { - throw Helpers.custom_error(`revertTokensFromItemPiles | Target must be of type Token or TokenDocument`) - } - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`revertTokensFromItemPiles | Could not determine the UUID, please provide a valid target`) - return targetUuid; - }) - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.REVERT_FROM_PILE, targetUuids, tokenSettings); - } - - /** - * Opens a pile if it is enabled and a container - * - * @param {Token/TokenDocument} target - * @param {Token/TokenDocument/boolean} [interactingToken=false] - * - * @return {Promise/boolean} - */ - static openItemPile(target, interactingToken = false) { - const targetActor = Utilities.getActor(target); - if (!PileUtilities.isItemPileContainer(target)) return false; - const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; - const pileData = PileUtilities.getActorFlagData(targetActor); - const wasLocked = pileData.locked; - const wasClosed = pileData.closed; - pileData.closed = false; - pileData.locked = false; - if (wasLocked) { - const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_UNLOCK, targetActor, pileData, interactingTokenDocument); - if (hookResult === false) return false; - } - const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_OPEN, targetActor, pileData, interactingTokenDocument); - if (hookResult === false) return false; - if (wasClosed && pileData.openSound) { - let sound = pileData.openSound; - if (pileData.openSound.includes("*")) { - sound = Helpers.random_array_element(pileData.openSounds) - } - AudioHelper.play({ src: sound }, true) - } - return this.updateItemPile(targetActor, pileData, { interactingToken: interactingTokenDocument }); - } - - /** - * Closes a pile if it is enabled and a container - * - * @param {Token/TokenDocument} target Target pile to close - * @param {Token/TokenDocument/boolean} [interactingToken=false] - * - * @return {Promise/boolean} - */ - static closeItemPile(target, interactingToken = false) { - const targetActor = Utilities.getActor(target); - if (!PileUtilities.isItemPileContainer(target)) return false; - const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; - const pileData = PileUtilities.getActorFlagData(targetActor); - - const wasOpen = !pileData.closed; - pileData.closed = true; - - const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_CLOSE, targetActor, pileData, interactingTokenDocument); - if (hookResult === false) return false; - - if (wasOpen && pileData.closeSound) { - let sound = pileData.closeSound; - if (pileData.closeSound.includes("*")) { - sound = Helpers.random_array_element(pileData.closeSounds) - } - AudioHelper.play({ src: sound }, true) - } - - return this.updateItemPile(targetActor, pileData, { interactingToken: interactingTokenDocument }); - } - - /** - * 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/boolean} - */ - static async toggleItemPileClosed(target, interactingToken = false) { - const targetActor = Utilities.getActor(target); - if (!PileUtilities.isItemPileContainer(target)) return false; - const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; - const pileData = PileUtilities.getActorFlagData(targetActor); - if (pileData.closed) { - await this.openItemPile(targetActor, interactingTokenDocument); - } else { - await this.closeItemPile(targetActor, interactingTokenDocument); - } - return !pileData.closed; - } - - /** - * 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/boolean} - */ - static lockItemPile(target, interactingToken = false) { - const targetActor = Utilities.getActor(target); - if (!PileUtilities.isItemPileContainer(target)) return false; - const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; - const pileData = PileUtilities.getActorFlagData(targetActor); - const wasClosed = pileData.closed; - pileData.closed = true; - pileData.locked = true; - if (!wasClosed) { - const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_CLOSE, targetActor, pileData, interactingTokenDocument); - if (hookResult === false) return false; - } - const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_LOCK, targetActor, pileData, interactingTokenDocument); - if (hookResult === false) return false; - if (!wasClosed && pileData.closeSound) { - let sound = pileData.closeSound; - if (pileData.closeSound.includes("*")) { - sound = Helpers.random_array_element(pileData.closeSounds) - } - AudioHelper.play({ src: sound }, true) - } - return this.updateItemPile(targetActor, pileData, { interactingToken: interactingTokenDocument }); - } - - /** - * 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/boolean} - */ - static unlockItemPile(target, interactingToken = false) { - const targetActor = Utilities.getActor(target); - if (!PileUtilities.isItemPileContainer(target)) return false; - const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; - const pileData = PileUtilities.getActorFlagData(targetActor); - pileData.locked = false; - Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_UNLOCK, targetActor, pileData, interactingTokenDocument); - return this.updateItemPile(targetActor, pileData, { interactingToken: interactingTokenDocument }); - } - - /** - * 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/boolean} - */ - static toggleItemPileLocked(target, interactingToken = false) { - const targetActor = Utilities.getActor(target); - if (!PileUtilities.isItemPileContainer(target)) return false; - const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; - const pileData = PileUtilities.getActorFlagData(targetActor); - if (pileData.locked) { - return this.unlockItemPile(targetActor, interactingTokenDocument); - } - return this.lockItemPile(targetActor, interactingTokenDocument); - } - - /** - * Causes the item pile to play a sound as it was attempted to be opened, but was locked - * - * @param {Token/TokenDocument} target - * @param {Token/TokenDocument/boolean} [interactingToken=false] - * - * @return {Promise} - */ - static rattleItemPile(target, interactingToken = false) { - const targetActor = Utilities.getActor(target); - if (!PileUtilities.isItemPileContainer(target)) return false; - const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; - const pileData = PileUtilities.getActorFlagData(targetActor); - - Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_RATTLE, targetActor, pileData, interactingTokenDocument); - - if (pileData.lockedSound) { - let sound = pileData.lockedSound; - if (pileData.lockedSound.includes("*")) { - sound = Helpers.random_array_element(pileData.lockedSounds) - } - AudioHelper.play({ src: sound }, true); - } - - return ItemPileSocket.executeForEveryone(ItemPileSocket.HANDLERS.CALL_HOOK, CONSTANTS.HOOKS.PILE.RATTLE, Utilities.getUuid(targetActor), pileData, Utilities.getUuid(interactingTokenDocument)); - } - - /** - * 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) { - return PileUtilities.isItemPileLocked(target); - } - - /** - * 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) { - return PileUtilities.isItemPileClosed(target); - } - - /** - * 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) { - return PileUtilities.isItemPileContainer(target); - } - - /** - * Updates a pile with new data. - * - * @param {Actor/TokenDocument} target Target token or actor to update - * @param {object} newData New data to update the actor with - * @param {object} options Options to pass to the function - * @param {Token/TokenDocument/boolean} [options.interactingToken=false] If an actor caused this update, you can pass one here to pass it along to macros that the item pile may run - * @param {Object/boolean} [options.tokenSettings=false] Updates to make to the target token - * - * @return {Promise} - */ - static updateItemPile(target, newData, { interactingToken = false, tokenSettings = false } = {}) { - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`updateItemPile | Could not determine the UUID, please provide a valid target`); - - const interactingTokenUuid = interactingToken ? Utilities.getUuid(interactingToken) : false; - if (interactingToken && !interactingTokenUuid) throw Helpers.custom_error(`updateItemPile | Could not determine the UUID, please provide a valid target`); - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.UPDATE_PILE, targetUuid, newData, { - interactingTokenUuid, tokenSettings - }); - } - - /** - * Deletes a pile, calling the relevant hooks. - * - * @param {Token/TokenDocument} target - * - * @return {Promise} - */ - static deleteItemPile(target) { - if (!PileUtilities.isValidItemPile(target)) { - throw Helpers.custom_error(`deleteItemPile | This is not an item pile, please provide a valid target`); - } - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`deleteItemPile | Could not determine the UUID, please provide a valid target`); - if (!targetUuid.includes("Token")) { - throw Helpers.custom_error(`deleteItemPile | Please provide a Token or TokenDocument`); - } - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.DELETE_PILE, targetUuid); - } - - /** - * Splits an item pile's content between all players (or a specified set of target actors). - * - * @param target {Token/TokenDocument/Actor} The item pile to split - * @param {object} options Options to pass to the function - * @param {boolean/TokenDocument/Actor/Array} [options.targets=false] The targets to receive the split contents - * @param {boolean/TokenDocument/Actor} [options.instigator=false] Whether this was triggered by a specific actor - * - * @returns {Promise} - */ - static async splitItemPileContents(target, { targets = false, instigator = false } = {}) { - - if (!PileUtilities.isItemPileLootable(target)) return false; - - const itemPileUuid = Utilities.getUuid(target); - if (!itemPileUuid) throw Helpers.custom_error(`SplitItemPileContents | Could not determine the UUID, please provide a valid item pile`) - - const itemPileActor = Utilities.getActor(target); - - if (targets) { - if (!Array.isArray(targets)) { - targets = [targets] - } - targets.forEach(actor => { - if (!(actor instanceof TokenDocument || actor instanceof Actor)) { - throw Helpers.custom_error("SplitItemPileContents | Each of the entries in targets must be of type TokenDocument or Actor") - } - }) - targets = targets.map(target => Utilities.getActor(target)); - } - - if (instigator && !(instigator instanceof TokenDocument || instigator instanceof Actor)) { - throw Helpers.custom_error("SplitItemPileContents | instigator must be of type TokenDocument or Actor") - } - - const actorUuids = (targets || SharingUtilities.getPlayersForItemPile(itemPileActor) - .map(u => u.character)) - .map(actor => Utilities.getUuid(actor)); - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.SPLIT_PILE, itemPileUuid, actorUuids, game.user.id, instigator); - - } - - /** - * Retrieves the price modifiers for a given item piles merchant - * - * @param {Actor/TokenDocument} target Target token or actor to retrieve the modifiers from - * @param {object} options Options to pass to the function - * @param {Token/TokenDocument/Actor/string/boolean} [options.actor=false] The actor whose price modifiers to consider - * @param {boolean} [options.absolute=false] Whether to only consider the actor's modifiers (true means not considering the merchant's base modifiers) - * - * @return {Object} - */ - static getMerchantPriceModifiers(target, { actor = false, absolute = false } = {}) { - - const merchantActor = Utilities.getActor(target); - - if (!(merchantActor instanceof Actor)) { - throw Helpers.custom_error(`getMerchantPriceModifiers | target must be of type Actor`); - } - - if (!PileUtilities.isItemPileMerchant(merchantActor)) { - throw Helpers.custom_error(`getMerchantPriceModifiers | target is not an item pile merchant`); - } - - if (actor) { - if (!(actor instanceof Actor) && typeof actor !== "string") { - throw Helpers.custom_error(`getMerchantPriceModifiers | actor must be of type Actor or string (UUID)`); - } - if (typeof actor === "string") { - actor = fromUuidSync(actor) || false; - } - } - - if (typeof absolute !== "boolean") { - throw Helpers.custom_error(`getMerchantPriceModifiers | absolute must be of type boolean`); - } - - return PileUtilities.getMerchantModifiersForActor(target, { actor, absolute }); - - } - - /** - * Updates the price modifiers for a given item piles merchant - * - * @param {Actor/TokenDocument} target Target token or actor to update modifiers on - * @param {Array<{ - * actor?: Actor, - * actorUuid?: string, - * relative?: boolean, - * override?: boolean, - * buyPriceModifier?: number, - * sellPriceModifier?: number - * }>} priceModifierData The price modifier data to update on the merchant - * - * @return {Promise} - */ - static updateMerchantPriceModifiers(target, priceModifierData = []) { - - const merchantActor = Utilities.getActor(target); - - const targetUuid = Utilities.getUuid(merchantActor); - if (!targetUuid) throw Helpers.custom_error(`updateMerchantPriceModifiers | Could not determine the UUID, please provide a valid target`); - - if (!PileUtilities.isItemPileMerchant(merchantActor)) { - throw Helpers.custom_error(`updateMerchantPriceModifiers | Target is not an item pile merchant`); - } - - const flagData = PileUtilities.getActorFlagData(merchantActor); - - const actorPriceModifiers = flagData?.actorPriceModifiers ?? []; - - for (const priceModifier of priceModifierData) { - if (priceModifier.actor && !(priceModifier.actor instanceof Actor)) { - throw Helpers.custom_error(`updateMerchantPriceModifiers | priceModifierData.actor must be of type Actor`); - } - if (priceModifier.actor) { - priceModifier.actorUuid = priceModifier.actor.uuid; - } - if (priceModifier.actorUuid && typeof priceModifier.actorUuid !== "string") { - throw Helpers.custom_error(`updateMerchantPriceModifiers | if priceModifierData.actor if not provided, priceModifierData.actorUuid must be of type string `); - } - if (!priceModifier.actorUuid) { - throw Helpers.custom_error(`updateMerchantPriceModifiers | Could not find the UUID for the given actor`); - } - if (priceModifier.relative !== undefined && typeof priceModifier.relative !== "boolean") { - throw Helpers.custom_error(`updateMerchantPriceModifiers | priceModifierData.relative must be of type boolean`); - } - if (priceModifier.override !== undefined && typeof priceModifier.override !== "boolean") { - throw Helpers.custom_error(`updateMerchantPriceModifiers | priceModifierData.override must be of type boolean`); - } - if (priceModifier.buyPriceModifier !== undefined && typeof priceModifier.buyPriceModifier !== "number") { - throw Helpers.custom_error(`updateMerchantPriceModifiers | priceModifierData.buyPriceModifier must be of type number`); - } - if (priceModifier.sellPriceModifier !== undefined && typeof priceModifier.sellPriceModifier !== "number") { - throw Helpers.custom_error(`updateMerchantPriceModifiers | priceModifierData.sellPriceModifier must be of type number`); - } - - let actorPriceModifierIndex = actorPriceModifiers.findIndex(existingPriceModifier => existingPriceModifier.actorUuid === priceModifier.actorUuid); - if (actorPriceModifierIndex === -1) { - actorPriceModifierIndex = actorPriceModifiers.push({}) - 1; - } - - const oldBuyPriceModifier = actorPriceModifiers[actorPriceModifierIndex]?.buyPriceModifier ?? flagData?.buyPriceModifier ?? 1; - const newBuyPriceModifier = Math.max(0, priceModifier.relative ? oldBuyPriceModifier + priceModifier.buyPriceModifier : priceModifier.buyPriceModifier ?? oldBuyPriceModifier); - - const oldSellPriceModifier = actorPriceModifiers[actorPriceModifierIndex]?.sellPriceModifier ?? flagData?.sellPriceModifier ?? 0.5; - const newSellPriceModifier = Math.max(0, priceModifier.relative ? oldSellPriceModifier + priceModifier.sellPriceModifier : priceModifier.sellPriceModifier ?? oldSellPriceModifier); - - actorPriceModifiers[actorPriceModifierIndex] = foundry.utils.mergeObject(actorPriceModifiers[actorPriceModifierIndex], { - actorUuid: priceModifier.actorUuid, - buyPriceModifier: newBuyPriceModifier, - sellPriceModifier: newSellPriceModifier, - override: priceModifier.override ?? false, - }); - - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.UPDATE_PILE, targetUuid, { actorPriceModifiers }); - - } - - /* ================= ITEM AND ATTRIBUTE METHODS ================= */ - - /** - * Adds item to an actor, increasing item quantities if matches were found - * - * @param {Actor/TokenDocument/Token} target The target to add an item to - * @param {Array} items An array of objects, with the key "item" being an item object or an Item class (the foundry class), with an optional key of "quantity" being the amount of the item to add - * @param {object} options Options to pass to the function - * @param {boolean} [options.mergeSimilarItems=true] Whether to merge similar items based on their name and type - * @param {boolean} [options.removeExistingActorItems=false] Whether to remove the actor's existing items before adding the new ones - * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault - * @param {string/boolean} [options.interactionId=false] The interaction ID of this action - * - * @returns {Promise} An array of objects, each containing the item that was added or updated, and the quantity that was added - */ - static addItems(target, items, { - mergeSimilarItems = true, removeExistingActorItems = false, skipVaultLogging = false, interactionId = false - } = {}) { - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`addItems | Could not determine the UUID, please provide a valid target`) - - const itemsToAdd = [] - items.forEach(itemData => { - - let item = itemData; - if (itemData instanceof Item) { - item = itemData.toObject(); - } else if (itemData.item) { - item = itemData.item instanceof Item ? itemData.item.toObject() : itemData.item; - if (itemData.flags) { - setProperty(item, "flags", foundry.utils.mergeObject(getProperty(item, "flags") ?? {}, getProperty(itemData, "flags"))); - } - } else if (itemData.id) { - item = target.items.get(itemData.id); - if (item) { - item = item.toObject(); - } else { - throw Helpers.custom_error(`addItems | Could not find item with id ${itemData.id} on actor with UUID ${targetUuid}!`) - } - } - - if (itemData?.quantity !== undefined) { - Utilities.setItemQuantity(item, itemData.quantity, true); - } - - const existingItems = mergeSimilarItems ? Utilities.findSimilarItem(itemsToAdd, item) : false; - if (existingItems && PileUtilities.canItemStack(item, target)) { - Utilities.setItemQuantity(existingItems, Utilities.getItemQuantity(existingItems) + Utilities.getItemQuantity(item)); - } else { - itemsToAdd.push(item); - } - - }); - - if (interactionId && typeof interactionId !== "string") throw Helpers.custom_error(`addItems | interactionId must be of type string`); - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.ADD_ITEMS, targetUuid, itemsToAdd, game.user.id, { - removeExistingActorItems, interactionId, skipVaultLogging - }); - } - - /** - * Subtracts the quantity of items on an actor. If the quantity of an item reaches 0, the item is removed from the 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 Items (the foundry class) or strings of IDs to remove all quantities of - * @param {object} options Options to pass to the function - * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault - * @param {string/boolean} [options.interactionId=false] The interaction ID of this action - * - * @returns {Promise} An array of objects, each containing the item that was removed or updated, the quantity that was removed, and whether the item was deleted - */ - static removeItems(target, items, { skipVaultLogging = false, interactionId = false } = {}) { - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`removeItems | Could not determine the UUID, please provide a valid target`); - - const targetActorItems = PileUtilities.getActorItems(target, { getItemCurrencies: true }); - - items = items.map(itemData => { - - let item; - if (typeof itemData === "string" || itemData._id) { - const itemId = typeof itemData === "string" ? itemData : itemData._id; - item = targetActorItems.find(actorItem => actorItem.id === itemId); - if (!item) { - throw Helpers.custom_error(`removeItems | Could not find item with id "${itemId}" on target "${targetUuid}"`) - } - item = item.toObject(); - } else { - if (itemData.item instanceof Item) { - item = itemData.item.toObject(); - } else if (itemData instanceof Item) { - item = itemData.toObject(); - } else { - item = itemData.item; - } - let foundActorItem = targetActorItems.find(actorItem => actorItem.id === item._id); - if (!foundActorItem) { - throw Helpers.custom_error(`removeItems | Could not find item with id "${item._id}" on target "${targetUuid}"`) - } - } - - return { - _id: item._id, quantity: itemData?.quantity ?? Utilities.getItemQuantity(item) - } - }); - - if (interactionId) { - if (typeof interactionId !== "string") throw Helpers.custom_error(`removeItems | interactionId must be of type string`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.REMOVE_ITEMS, targetUuid, items, game.user.id, { - interactionId, - skipVaultLogging - }); - } - - /** - * 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 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"), or Items (the foundry class) or strings of IDs to transfer all quantities of - * @param {object} options Options to pass to the function - * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault - * @param {string/boolean} [options.interactionId=false] The interaction ID of this action - * - * @returns {Promise} An array of objects, each containing the item that was added or updated, and the quantity that was transferred - */ - static transferItems(source, target, items, { skipVaultLogging = false, interactionId = false } = {}) { - - const sourceUuid = Utilities.getUuid(source); - if (!sourceUuid) throw Helpers.custom_error(`transferItems | Could not determine the UUID, please provide a valid source`) - - const sourceActorItems = PileUtilities.getActorItems(source, { getItemCurrencies: true }); - - items = items.map(itemData => { - - let item; - if (typeof itemData === "string" || itemData._id) { - const itemId = typeof itemData === "string" ? itemData : itemData._id; - item = sourceActorItems.find(actorItem => actorItem.id === itemId); - if (!item) { - throw Helpers.custom_error(`transferItems | Could not find item with id "${itemId}" on target "${sourceUuid}"`) - } - item = item.toObject(); - } else if (itemData instanceof Item) { - item = itemData.toObject(); - } else if (itemData.item instanceof Item) { - item = itemData.item.toObject(); - } else { - item = itemData.item; - } - - let foundActorItem = sourceActorItems.find(actorItem => actorItem.id === item._id); - if (!foundActorItem) { - throw Helpers.custom_error(`transferItems | Could not find item with id "${item._id}" on target "${sourceUuid}"`) - } - - return { - _id: item._id, - quantity: Math.max(itemData?.quantity ?? Utilities.getItemQuantity(itemData), 0), - flags: getProperty(itemData, "flags") - } - }); - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`transferItems | Could not determine the UUID, please provide a valid target`) - - if (interactionId) { - if (typeof interactionId !== "string") throw Helpers.custom_error(`transferItems | interactionId must be of type string`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_ITEMS, sourceUuid, targetUuid, items, game.user.id, { - interactionId, skipVaultLogging - }); - - } - - /** - * Transfers all items between the source and the target. - * - * @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 {object} options Options to pass to the function - * @param {Array/boolean} [options.itemFilters=false] Array of item types disallowed - will default to module settings if none provided - * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault - * @param {string/boolean} [options.interactionId=false] The interaction ID of this action - * - * @returns {Promise} An array containing all the items that were transferred to the target - */ - static transferAllItems(source, target, { - itemFilters = false, - skipVaultLogging = false, - interactionId = false - } = {}) { - - const sourceUuid = Utilities.getUuid(source); - if (!sourceUuid) throw Helpers.custom_error(`transferAllItems | Could not determine the UUID, please provide a valid source`) - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`transferAllItems | Could not determine the UUID, please provide a valid target`) - - if (itemFilters) { - if (!Array.isArray(itemFilters)) throw Helpers.custom_error(`transferAllItems | itemFilters must be of type array`); - itemFilters.forEach(entry => { - if (typeof entry?.path !== "string") throw Helpers.custom_error(`transferAllItems | each entry in the itemFilters must have a "path" property that is of type string`); - if (typeof entry?.filter !== "string") throw Helpers.custom_error(`transferAllItems | each entry in the itemFilters must have a "filter" property that is of type string`); - }) - } - - if (interactionId) { - if (typeof interactionId !== "string") throw Helpers.custom_error(`transferAllItems | interactionId must be of type string`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_ALL_ITEMS, sourceUuid, targetUuid, game.user.id, { - itemFilters, skipVaultLogging, interactionId - }); - } - - /** - * Sets attributes on an actor - * - * @param {Actor/Token/TokenDocument} target The target whose attribute will have their quantity set - * @param {object} attributes An object with each key being an attribute path, and its value being the quantity to set - * @param {object} options Options to pass to the function - * @param {string/boolean} [options.interactionId=false] The interaction ID of this action - * - * @returns {Promise} An array containing a key value pair of the attribute path and the quantity of that attribute that was set - * - */ - static setAttributes(target, attributes, { interactionId = false } = {}) { - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`setAttributes | Could not determine the UUID, please provide a valid target`); - - const targetActor = Utilities.getActor(target); - - Object.entries(attributes).forEach(entry => { - const [attribute, quantity] = entry; - if (!hasProperty(targetActor, attribute)) { - throw Helpers.custom_error(`setAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); - } - if (!Helpers.isRealNumber(quantity)) { - throw Helpers.custom_error(`setAttributes | Attribute "${attribute}" must be of type number`); - } - }); - - if (interactionId) { - if (typeof interactionId !== "string") throw Helpers.custom_error(`setAttributes | interactionId must be of type string`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.SET_ATTRIBUTES, targetUuid, attributes, game.user.id, { - interactionId - }); - - } - - /** - * Adds 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 - * @param {object} options Options to pass to the function - * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault - * @param {string/boolean} [options.interactionId=false] The interaction ID of this action - * - * @returns {Promise} An array containing a key value pair of the attribute path and the quantity of that attribute that was added - * - */ - static addAttributes(target, attributes, { skipVaultLogging = false, interactionId = false } = {}) { - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`addAttributes | Could not determine the UUID, please provide a valid target`); - - const targetActor = Utilities.getActor(target); - - Object.entries(attributes).forEach(entry => { - const [attribute, quantity] = entry; - if (!hasProperty(targetActor, attribute)) { - throw Helpers.custom_error(`addAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); - } - if (!Helpers.isRealNumber(quantity) && quantity > 0) { - throw Helpers.custom_error(`addAttributes | Attribute "${attribute}" must be of type number and greater than 0`); - } - }); - - if (interactionId) { - if (typeof interactionId !== "string") throw Helpers.custom_error(`addAttributes | interactionId must be of type string`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.ADD_ATTRIBUTES, targetUuid, attributes, game.user.id, { - skipVaultLogging, - interactionId - }); - - } - - /** - * Subtracts attributes on the target - * - * @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 {object} options Options to pass to the function - * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault - * @param {string/boolean} [options.interactionId=false] The interaction ID of this action - * - * @returns {Promise} An array containing a key value pair of the attribute path and the quantity of that attribute that was removed - */ - static removeAttributes(target, attributes, { skipVaultLogging = false, interactionId = false } = {}) { - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`removeAttributes | Could not determine the UUID, please provide a valid target`); - - const targetActor = Utilities.getActor(target); - - let attributesToSend = {}; - if (Array.isArray(attributes)) { - attributes.forEach(attribute => { - if (typeof attribute !== "string") { - throw Helpers.custom_error(`removeAttributes | Each attribute in the array must be of type string`); - } - if (!hasProperty(targetActor, attribute)) { - throw Helpers.custom_error(`removeAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); - } - attributesToSend[attribute] = Number(getProperty(targetActor, attribute)); - }); - } else { - Object.entries(attributes).forEach(entry => { - const [attribute, quantity] = entry; - if (!hasProperty(targetActor, attribute)) { - throw Helpers.custom_error(`removeAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); - } - if (!Helpers.isRealNumber(quantity) && quantity > 0) { - throw Helpers.custom_error(`removeAttributes | Attribute "${attribute}" must be of type number and greater than 0`); - } - }); - attributesToSend = attributes; - } - - if (interactionId) { - if (typeof interactionId !== "string") throw Helpers.custom_error(`removeAttributes | interactionId must be of type string`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.REMOVE_ATTRIBUTES, targetUuid, attributesToSend, game.user.id, { - skipVaultLogging, - interactionId - }); - - } - - /** - * 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 {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 {object} options Options to pass to the function - * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault - * @param {string/boolean} [options.interactionId=false] The interaction ID of this action - * - * @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 transferAttributes(source, target, attributes, { skipVaultLogging = false, interactionId = false } = {}) { - - const sourceUuid = Utilities.getUuid(source); - if (!sourceUuid) throw Helpers.custom_error(`transferAttributes | Could not determine the UUID, please provide a valid source`); - const sourceActor = Utilities.getActor(source); - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`transferAttributes | Could not determine the UUID, please provide a valid target`); - const targetActor = Utilities.getActor(target); - - if (Array.isArray(attributes)) { - attributes.forEach(attribute => { - if (typeof attribute !== "string") { - throw Helpers.custom_error(`transferAttributes | Each attribute in the array must be of type string`); - } - if (!hasProperty(sourceActor, attribute)) { - throw Helpers.custom_error(`transferAttributes | Could not find attribute ${attribute} on source's actor with UUID "${targetUuid}"`); - } - if (!hasProperty(targetActor, attribute)) { - throw Helpers.custom_error(`transferAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); - } - }); - } else { - Object.entries(attributes).forEach(entry => { - const [attribute, quantity] = entry; - if (!hasProperty(sourceActor, attribute)) { - throw Helpers.custom_error(`transferAttributes | Could not find attribute ${attribute} on source's actor with UUID "${targetUuid}"`); - } - if (!hasProperty(targetActor, attribute)) { - throw Helpers.custom_error(`transferAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); - } - if (!Helpers.isRealNumber(quantity) && quantity > 0) { - throw Helpers.custom_error(`transferAttributes | Attribute "${attribute}" must be of type number and greater than 0`); - } - }); - } - - if (interactionId) { - if (typeof interactionId !== "string") throw Helpers.custom_error(`transferAttributes | interactionId must be of type string`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_ATTRIBUTES, sourceUuid, targetUuid, attributes, game.user.id, { - skipVaultLogging, - interactionId - }); - - } - - /** - * Transfers all dynamic attributes from a source to a target, removing it or subtracting from the source and adding them to the target - * - * @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 {object} options Options to pass to the function - * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault - * @param {string/boolean} [options.interactionId=false] The interaction ID of this action - * - * @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 transferAllAttributes(source, target, { skipVaultLogging = false, interactionId = false } = {}) { - - const sourceUuid = Utilities.getUuid(source); - if (!sourceUuid) throw Helpers.custom_error(`transferAllAttributes | Could not determine the UUID, please provide a valid source`); - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`transferAllAttributes | Could not determine the UUID, please provide a valid target`); - - if (interactionId) { - if (typeof interactionId !== "string") throw Helpers.custom_error(`transferAllAttributes | interactionId must be of type string`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_ALL_ATTRIBUTES, sourceUuid, targetUuid, game.user.id, { - skipVaultLogging, - interactionId - }); - - } - - /** - * Transfers all items and attributes 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 {object} options Options to pass to the function - * @param {Array/boolean} [options.itemFilters=false] Array of item types disallowed - will default to module settings if none provided - * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault - * @param {string/boolean} [options.interactionId=false] The ID of this interaction - * - * @returns {Promise} An object containing all items and attributes transferred to the target - */ - static transferEverything(source, target, { - itemFilters = false, - skipVaultLogging = false, - interactionId = false - } = {}) { - - const sourceUuid = Utilities.getUuid(source); - if (!sourceUuid) throw Helpers.custom_error(`transferEverything | Could not determine the UUID, please provide a valid source`); - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`transferEverything | Could not determine the UUID, please provide a valid target`); - - if (itemFilters) { - if (!Array.isArray(itemFilters)) throw Helpers.custom_error(`transferEverything | itemFilters must be of type array`); - itemFilters.forEach(entry => { - if (typeof entry?.path !== "string") throw Helpers.custom_error(`transferEverything | each entry in the itemFilters must have a "path" property that is of type string`); - if (typeof entry?.filter !== "string") throw Helpers.custom_error(`transferEverything | each entry in the itemFilters must have a "filter" property that is of type string`); - }) - } - - if (interactionId) { - if (typeof interactionId !== "string") throw Helpers.custom_error(`transferEverything | interactionId must be of type string`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_EVERYTHING, sourceUuid, targetUuid, game.user.id, { - itemFilters, skipVaultLogging, interactionId - }); - - } - - /** - * Turns a string of currencies into an array containing the data and quantities for each currency - * - * @param {string} currencies A string of currencies to convert (eg, "5gp 25sp") - * - * @returns {Array} An array of object containing the data and quantity for each currency - */ - static getCurrenciesFromString(currencies) { - if (typeof currencies !== "string") { - throw Helpers.custom_error(`getCurrenciesFromString | currencies must be of type string`) - } - return PileUtilities.getPriceFromString(currencies).currencies; - } - - /** - * Turns a string of currencies into an object containing payment data, and the change an optional target would receive back - * - * @param {string} price A string of currencies to convert (eg, "5gp 25sp") - * @param {object} options Options to pass to the function - * @param {string/boolean} [options.target=false] The target whose currencies to check against - * - * @returns {object} An object containing the price data - */ - static getPaymentDataFromString(price, { target = false } = {}) { - - let targetActor = false; - if (target) { - targetActor = Utilities.getActor(target); - if (!targetActor) throw Helpers.custom_error(`removeCurrencies | Could not determine target actor`); - } - - const priceData = PileUtilities.getPriceFromString(price) - const currenciesToRemove = priceData.currencies.filter(currency => currency.quantity); - const overallCost = priceData.overallCost; - - if (!currenciesToRemove.length) { - throw Helpers.custom_error(`removeCurrencies | Could not determine currencies to remove with string "${price}"`); - } - - return PileUtilities.getPaymentData({ - purchaseData: [{ cost: overallCost, quantity: 1 }], buyer: targetActor - }); - - } - - /** - * Adds currencies to the target - * - * @param {Actor/Token/TokenDocument} target The actor to add the currencies to - * @param {string} currencies A string of currencies to add (eg, "5gp 25sp") - * @param {object} options Options to pass to the function - * @param {string/boolean} [options.interactionId=false] The ID of this interaction - * - * @returns {Promise} An object containing the items and attributes added to the target - */ - static addCurrencies(target, currencies, { interactionId = false } = {}) { - - const targetActor = Utilities.getActor(target); - const targetUuid = Utilities.getUuid(targetActor); - if (!targetUuid) throw Helpers.custom_error(`addCurrency | Could not determine the UUID, please provide a valid target`); - - if (typeof currencies !== "string") { - throw Helpers.custom_error(`addCurrency | currencies must be of type string`) - } - - const currenciesToAdd = PileUtilities.getPriceFromString(currencies).currencies - .filter(currency => currency.quantity); - - if (!currenciesToAdd.length) { - throw Helpers.custom_error(`addCurrency | Could not determine currencies to add with string "${currencies}"`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.ADD_CURRENCIES, targetUuid, currencies, game.user.id, { interactionId }); - - } - - /** - * Removes currencies from the target - * - * @param {Actor/Token/TokenDocument} target The actor to remove currencies from - * @param {string} currencies A string of currencies to remove (eg, "5gp 25sp") - * @param {object} options Options to pass to the function - * @param {boolean} [options.change=true] Whether the actor can get change back - * @param {string/boolean} [options.interactionId=false] The ID of this interaction - * - * @returns {Promise} An object containing the items and attributes removed from the target - */ - static removeCurrencies(target, currencies, { change = true, interactionId = false } = {}) { - - const targetActor = Utilities.getActor(target); - const targetUuid = Utilities.getUuid(targetActor); - if (!targetUuid) throw Helpers.custom_error(`removeCurrencies | Could not determine the UUID, please provide a valid target`); - - if (typeof currencies !== "string") { - throw Helpers.custom_error(`removeCurrencies | currencies must be of type string`) - } - - const priceData = PileUtilities.getPriceFromString(currencies) - const currenciesToRemove = priceData.currencies.filter(currency => currency.quantity); - const overallCost = priceData.overallCost; - - if (!currenciesToRemove.length) { - throw Helpers.custom_error(`removeCurrencies | Could not determine currencies to remove with string "${currencies}"`); - } - - const paymentData = PileUtilities.getPaymentData({ - purchaseData: [{ cost: overallCost, quantity: 1 }], buyer: targetActor - }); - - if (!paymentData.canBuy) { - throw Helpers.custom_error(`removeCurrencies | ${targetActor.name} cannot afford "${currencies}"`); - } - - if (!change && paymentData.buyerChange.length) { - throw Helpers.custom_error(`removeCurrencies | ${targetActor.name} cannot afford "${currencies}" without receiving change!`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.REMOVE_CURRENCIES, targetUuid, currencies, game.user.id, { interactionId }); - - } - - /** - * Transfers currencies between the source and the target. - * - * @param {Actor/Token/TokenDocument} source The actor to transfer currencies from - * @param {Actor/Token/TokenDocument} target The actor to receive the currencies - * @param {string} currencies A string of currencies to transfer (eg, "5gp 25sp") - * @param {object} options Options to pass to the function - * @param {boolean} [options.change=true] Whether the source actor can get change back - * @param {string/boolean} [options.interactionId=false] The ID of this interaction - * - * @returns {Promise} An object containing the items and attributes transferred to the target - */ - static transferCurrencies(source, target, currencies, { change = true, interactionId = false } = {}) { - - const sourceActor = Utilities.getActor(source); - const sourceUuid = Utilities.getUuid(sourceActor); - if (!sourceUuid) throw Helpers.custom_error(`transferCurrencies | Could not determine the UUID, please provide a valid source`); - - const targetActor = Utilities.getActor(target); - const targetUuid = Utilities.getUuid(targetActor); - if (!targetUuid) throw Helpers.custom_error(`transferCurrencies | Could not determine the UUID, please provide a valid target`); - - if (typeof currencies !== "string") { - throw Helpers.custom_error(`transferCurrencies | currencies must be of type string`) - } - - const priceData = PileUtilities.getPriceFromString(currencies) - const currenciesToTransfer = priceData.currencies.filter(currency => currency.quantity); - const overallCost = priceData.overallCost; - - if (!currenciesToTransfer.length) { - throw Helpers.custom_error(`transferCurrencies | Could not determine currencies to remove with string "${currencies}"`); - } - - const paymentData = PileUtilities.getPaymentData({ - purchaseData: [{ cost: overallCost, quantity: 1 }], buyer: sourceActor - }); - - if (!paymentData.canBuy) { - throw Helpers.custom_error(`transferCurrencies | ${sourceActor.name} cannot afford to transfer "${currencies}"`); - } - - if (!change && paymentData.buyerChange.length) { - throw Helpers.custom_error(`transferCurrencies | ${sourceActor.name} cannot afford to transfer "${currencies}" without receiving change!`); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_CURRENCIES, sourceUuid, targetUuid, currencies, game.user.id, { interactionId }); - - } - - /** - * Transfers all currencies between the source and the target. - * - * @param {Actor/Token/TokenDocument} source The actor to transfer all currencies from - * @param {Actor/Token/TokenDocument} target The actor to receive all the currencies - * @param {object} options Options to pass to the function - * @param {string/boolean} [options.interactionId=false] The ID of this interaction - * - * @returns {Promise} An object containing all items and attributes transferred to the target - */ - static transferAllCurrencies(source, target, { interactionId = false } = {}) { - - const sourceUuid = Utilities.getUuid(source); - if (!sourceUuid) throw Helpers.custom_error(`transferCurrencies | Could not determine the UUID, please provide a valid source`); - - const targetUuid = Utilities.getUuid(target); - if (!targetUuid) throw Helpers.custom_error(`transferCurrencies | Could not determine the UUID, please provide a valid target`); - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_ALL_CURRENCIES, sourceUuid, targetUuid, game.user.id, { interactionId }); - - } - - /** - * Rolls on a table of items and collates them to be able to be added to actors and such - * - * @param {string/RollTable} table The name, ID, UUID, or the table itself, or an array of such - * @param {object} options Options to pass to the function - * @param {string/number} [options.timesToRoll="1"] The number of times to roll on the tables, which can be a roll formula - * @param {boolean} [options.resetTable=true] Whether to reset the table before rolling it - * @param {boolean} [options.normalizeTable=true] Whether to normalize the table before rolling it - * @param {boolean} [options.displayChat=false] Whether to display the rolls to the chat - * @param {object} [options.rollData={}] Data to inject into the roll formula - * @param {Actor/string/boolean} [options.targetActor=false] The target actor to add the items to, or the UUID of an actor - * @param {boolean} [options.removeExistingActorItems=false] Whether to clear the target actor's items before adding the ones rolled - * @param {boolean/string} [options.customCategory=false] Whether to apply a custom category to the items rolled - * - * @returns {Promise>} An array of object containing the item data and their quantity - */ - static async rollItemTable(table, { - timesToRoll = "1", - resetTable = true, - normalizeTable = false, - displayChat = false, - rollData = {}, - targetActor = false, - removeExistingActorItems = false, - customCategory = false - } = {}) { - - let rollTable = table; - if (typeof table === "string") { - let potentialTable = await fromUuid(table); - if (!potentialTable) { - potentialTable = game.tables.get(table) - } - if (!potentialTable) { - potentialTable = game.tables.getName(table) - } - if (!potentialTable) { - throw Helpers.custom_error(`rollItemTable | could not find table with string "${table}"`); - } - if (resetTable && table.startsWith("Compendium")) { - resetTable = false; - } - rollTable = potentialTable; - } - - if (!(rollTable instanceof RollTable)) { - throw Helpers.custom_error(`rollItemTable | table must be of type RollTable`); - } - - table = rollTable.uuid; - - if (!(typeof timesToRoll === "string" || typeof timesToRoll === "number")) { - throw Helpers.custom_error(`rollItemTable | timesToRoll must be of type string or number`); - } - - if (typeof rollData !== "object") { - throw Helpers.custom_error(`rollItemTable | rollData must be of type object`); - } - - if (typeof removeExistingActorItems !== "boolean") { - throw Helpers.custom_error(`rollItemTable | removeExistingActorItems of type boolean`); - } - - if (targetActor) { - targetActor = Utilities.getActor(targetActor); - if (!(targetActor instanceof Actor)) { - throw Helpers.custom_error(`rollItemTable | could not find the actor of the target actor`); - } - } - - const items = await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.ROLL_ITEM_TABLE, { - table, - timesToRoll, - resetTable, - normalizeTable, - displayChat, - rollData, - customCategory, - targetActor: Utilities.getUuid(targetActor), - removeExistingActorItems, - userId: game.user.id - }); - - if (items) { - for (const entry of items) { - entry.item = targetActor ? targetActor.items.get(entry.item._id) : await Item.implementation.create(entry.item, { temporary: true }); - } - } - - return items; - - } - - /** - * Refreshes the merchant's inventory, potentially removing existing items and populating it based on its item tables - * - * @param {Actor/Token/TokenDocument} target The merchant actor to refresh the inventory of - * @param {object} options Options to pass to the function - * @param {boolean} [options.removeExistingActorItems=true] Whether to clear the merchant's existing inventory before adding the new items - * @returns {Promise} - */ - static async refreshMerchantInventory(target, { removeExistingActorItems = true } = {}) { - - if (target) { - target = Utilities.getActor(target); - if (!(target instanceof Actor)) { - throw Helpers.custom_error(`refreshMerchantInventory | could not find the actor of the target actor`); - } - } - - const targetUuid = Utilities.getUuid(target); - - if (!PileUtilities.isItemPileMerchant(target)) { - throw Helpers.custom_error(`refreshMerchantInventory | target of uuid ${targetUuid} is not a merchant`); - } - - if (typeof removeExistingActorItems !== "boolean") { - throw Helpers.custom_error(`refreshMerchantInventory | removeExistingActorItems of type boolean`); - } - - const items = await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.REFRESH_MERCHANT_INVENTORY, targetUuid, { - removeExistingActorItems, - userId: game.user.id - }); - - if (items) { - for (const entry of items) { - entry.item = target.items.get(entry.item._id); - } - } - - return items; - - } - - - /** - * Gets all the valid items from a given actor or token, excluding items based on its item type filters - * - * @param {Actor/TokenDocument/Token} target The target to get the items from - * - * @returns {Array} Array containing the target's valid items - */ - static getActorItems(target) { - return PileUtilities.getActorItems(target); - } - - static findSimilarItem(itemsToSearch, itemToFind) { - return Utilities.findSimilarItem(itemsToSearch, itemToFind); - } - - /** - * Gets the valid currencies from a given actor or token - * - * @param {Actor/TokenDocument/Token} target The target to get the currencies from - * @param {object} [options] Object containing optional parameters - * @param {Boolean} [options.getAll] Whether to get all the currencies, regardless of quantity - * - * @returns {Array} An array of objects containing the data about each currency - */ - static getActorCurrencies(target, { getAll = false } = {}) { - return PileUtilities.getActorCurrencies(target, { getAll }); - } - - static updateTokenHud() { - return ItemPileSocket.executeForEveryone(ItemPileSocket.HANDLERS.RERENDER_TOKEN_HUD); - } - - static requestTrade(user) { - return TradeAPI._requestTrade(user); - } - - static spectateTrade(tradeId) { - return TradeAPI._spectateTrade(tradeId); - } - - /** - * Renders the appropriate interface for a given actor - * - * @param {Actor/TokenDocument} target The actor whose interface to render - * @param {object} options An object containing the options for this method - * @param {Array} [options.userIds] An array of users or user ids for each user to render the interface for (defaults to only self) - * @param {Actor/TokenDocument} [options.inspectingTarget] Sets what actor should be viewing the interface - * @param {boolean} [options.useDefaultCharacter] Whether other users should use their assigned character when rendering the interface - * - * @returns {Promise} - */ - static renderItemPileInterface(target, { - userIds = null, inspectingTarget = null, useDefaultCharacter = false - } = {}) { - - const targetDocument = Utilities.getDocument(target); - const targetUuid = Utilities.getUuid(targetDocument); - if (!targetUuid) throw Helpers.custom_error(`renderItemPileInterface | Could not determine the UUID, please provide a valid target item pile`); - - if (!PileUtilities.isValidItemPile(targetDocument)) { - throw Helpers.custom_error("renderItemPileInterface | This target is not a valid item pile") - } - - if (!inspectingTarget && !useDefaultCharacter) { - useDefaultCharacter = true; - } - - if (inspectingTarget && useDefaultCharacter) { - throw Helpers.custom_error("renderItemPileInterface | You cannot force users to use both their default character and a specific character to inspect the pile") - } - - const inspectingTargetUuid = inspectingTarget ? Utilities.getUuid(inspectingTarget) : false; - if (inspectingTarget && !inspectingTargetUuid) throw Helpers.custom_error(`renderItemPileInterface | Could not determine the UUID, please provide a valid inspecting target`); - - if (!Array.isArray(userIds)) { - if (userIds === null) { - userIds = [game.user.id]; - } else { - userIds = [userIds] - } - } else { - userIds = userIds.map(user => { - return user instanceof User ? user.id : user; - }) - } - - if (!game.user.isGM) { - if (userIds.length > 1 || !userIds.includes(game.user.id)) { - throw Helpers.custom_error(`renderItemPileInterface | You are not a GM, so you cannot force others to render an item pile's interface`); - } - userIds = [game.user.id]; - } - - if (userIds.length === 1 && userIds[0] === game.user.id) { - return PrivateAPI._renderItemPileInterface(targetUuid, { - inspectingTargetUuid, useDefaultCharacter, remote: true - }) - } - - for (const userId of userIds) { - const user = game.users.get(userId); - if (!user) throw Helpers.custom_error(`renderItemPileInterface | No user with ID "${userId}" exists`); - if (user.isGM) continue; - if (useDefaultCharacter) { - if (!user.character) { - Helpers.custom_warning(`renderItemPileInterface | User "${user.name}" has no default character`, true); - return; - } - } - } - - return ItemPileSocket.executeForUsers(ItemPileSocket.HANDLERS.RENDER_INTERFACE, userIds, targetUuid, { - inspectingTargetUuid, useDefaultCharacter, remote: true - }); - - } - - /** - * Closes any open interfaces from a given item pile actor - * - * @param {Actor/TokenDocument} target The actor whose interface to potentially close - * @param {object} options An object containing the options for this method - * @param {Array} [options.userIds] An array of users or user ids for each user to close the interface for (defaults to only self) - * - * @returns {Promise} - */ - static unrenderItemPileInterface(target, { userIds = null } = {}) { - - const targetDocument = Utilities.getDocument(target); - const targetUuid = Utilities.getUuid(targetDocument); - if (!targetUuid) throw Helpers.custom_error(`unrenderItemPileInterface | Could not determine the UUID, please provide a valid target item pile`); - - if (!PileUtilities.isValidItemPile(targetDocument)) { - throw Helpers.custom_error("unrenderItemPileInterface | This target is not a valid item pile") - } - - if (!Array.isArray(userIds)) { - if (userIds === null) { - userIds = [game.user.id]; - } else { - userIds = [userIds] - } - } else { - userIds = userIds.map(user => { - return user instanceof User ? user.id : user; - }) - } - - if (!game.user.isGM) { - if (userIds.length > 1 || !userIds.includes(game.user.id)) { - throw Helpers.custom_error(`unrenderItemPileInterface | You are not a GM, so you cannot force others to close an item pile's interface`); - } - userIds = [game.user.id]; - } - - if (userIds.length === 1 && userIds[0] === game.user.id) { - return PrivateAPI._unrenderItemPileInterface(targetUuid, { remote: true }); - } - - for (const userId of userIds) { - const user = game.users.get(userId); - if (!user) throw Helpers.custom_error(`unrenderItemPileInterface | No user with ID "${userId}" exists`); - } - - return ItemPileSocket.executeForUsers(ItemPileSocket.HANDLERS.UNRENDER_INTERFACE, userIds, targetUuid, { remote: true }); - - } - - /** - * Get the prices array for a given item - * - * @param {Item} item Item to get the price for - * @param {object} options Options to pass to the function - * @param {Actor/boolean} [options.seller=false] Actor that is selling the item - * @param {Actor/boolean} [options.buyer=false] Actor that is buying the item - * @param {number} [options.quantity=1] Quantity of item to buy - * - * @returns {Array} Array containing all the different purchase options for this item - */ - static getPricesForItem(item, { seller = false, buyer = false, quantity = 1 } = {}) { - - if (!(item instanceof Item)) { - throw Helpers.custom_error("getPricesForItem | The given item must be of type Item"); - } - - if (seller) { - seller = Utilities.getActor(seller); - if (!seller) { - throw Helpers.custom_error("getPricesForItem | Could not determine actor for the seller"); - } - } else { - if (!item.parent) { - throw Helpers.custom_error("getPricesForItem | If no seller was given, the item must belong to an actor"); - } - seller = Utilities.getActor(item.parent); - } - - if (buyer) { - buyer = Utilities.getActor(buyer); - if (!buyer) { - throw Helpers.custom_error(`getPricesForItem | Could not determine the actor for the buyer`); - } - } - - return PileUtilities.getPriceData({ item, seller, buyer, quantity }); - - } - - /** - * Trades multiple items between one actor to another, and currencies and/or change is exchanged between them - * - * @param {Actor/Token/TokenDocument} seller The actor that is selling the item - * @param {Actor/Token/TokenDocument} buyer The actor that is buying the item - * @param {Array>} items An array of objects containing the item or the id of the - * item to be sold, the quantity to be sold, and the payment - * index to be used - * @param {string/boolean} [interactionId=false] The ID of this interaction - * - * @returns {Promise} The items that were created and the attributes that were changed - */ - static tradeItems(seller, buyer, items, { interactionId = false } = {}) { - - const sellerActor = Utilities.getActor(seller); - const sellerUuid = Utilities.getUuid(sellerActor); - if (!sellerUuid) { - throw Helpers.custom_error(`tradeItems | Could not determine the UUID of the seller, please provide a valid actor or token`, true); - } - - const buyerActor = Utilities.getActor(buyer); - const buyerUuid = Utilities.getUuid(buyer); - if (!buyerUuid) { - throw Helpers.custom_error(`tradeItems | Could not determine the UUID of the buyer, please provide a valid actor or token`, true); - } - - const itemsToSell = items.map(data => { - - data = foundry.utils.mergeObject({ - item: "", quantity: 1, paymentIndex: 0 - }, data); - - if (!data.item) { - throw Helpers.custom_error(`tradeItems | You must provide an item!`, true); - } - - let actorItem; - if (typeof data.item === "string") { - actorItem = sellerActor.items.get(data.item) || sellerActor.items.getName(data.item); - if (!actorItem) { - throw Helpers.custom_error(`tradeItems | Could not find item on seller with identifier "${data.item}"`); - } - } else { - actorItem = sellerActor.items.get(data.item instanceof Item ? data.item.id : data.item._id) || sellerActor.items.getName(data.item.name); - if (!actorItem) { - throw Helpers.custom_error(`tradeItems | Could not find provided item on seller`); - } - } - - const itemPrices = PileUtilities.getPriceData({ - item: actorItem, seller: sellerActor, buyer: buyerActor, quantity: data.quantity - }); - if (itemPrices.length) { - if (data.paymentIndex >= itemPrices.length || data.paymentIndex < 0) { - throw Helpers.custom_error(`tradeItems | That payment index does not exist on ${actorItem.name}`, true); - } - - const selectedPrice = itemPrices[data.paymentIndex]; - if (data.quantity > selectedPrice.maxQuantity) { - throw Helpers.custom_error(`tradeItems | The buyer actor cannot afford ${data.quantity} of ${actorItem.name} (max ${selectedPrice.maxQuantity})`, true); - } - } - - return { - id: actorItem.id, quantity: data.quantity, paymentIndex: data.paymentIndex - }; - - }); - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRADE_ITEMS, sellerUuid, buyerUuid, itemsToSell, game.user.id, { interactionId }); - - } - - static async registerItemPileType(type, label, flags = []) { - game.i18n.translations['ITEM-PILES'].Types[type] = "Custom: " + label; - CONSTANTS.CUSTOM_PILE_TYPES[type] = flags; - } - - static isItemInvalid(item) { - return PileUtilities.isItemInvalid(item.parent, item); - } - - static canItemStack(item) { - return PileUtilities.canItemStack(item); - } - - static getVaultGridData(vaultActor) { - return PileUtilities.getVaultGridData(vaultActor); - } - - static getActorFlagData(actor) { - return PileUtilities.getActorFlagData(actor); - } + /** + * @class API + */ + + /** + * The actor class type used for the original item pile actor in this system + * + * @returns {string} + */ + static get ACTOR_CLASS_TYPE() { + return Helpers.getSetting(SETTINGS.ACTOR_CLASS_TYPE); + } + + /** + * The currencies used in this system + * + * @returns {Array<{primary: boolean, name: string, data: Object, img: string, abbreviation: string, exchange: number}>} + */ + static get CURRENCIES() { + return foundry.utils.deepClone(Helpers.getSetting(SETTINGS.CURRENCIES).map(currency => { + if (currency.type === "item" && currency.data.uuid) { + const compendiumItem = CompendiumUtilities.getItemFromCache(currency.data.uuid); + if (compendiumItem) { + currency.data.item = compendiumItem; + } + } + delete currency["quantity"]; + return currency; + })); + } + + /** + * The secondary currencies used in this system + * + * @returns {Array<{name: string, data: Object, img: string, abbreviation: string}>} + */ + static get SECONDARY_CURRENCIES() { + return foundry.utils.deepClone(Helpers.getSetting(SETTINGS.SECONDARY_CURRENCIES).map(currency => { + if (currency.type === "item" && currency.data.uuid) { + const compendiumItem = CompendiumUtilities.getItemFromCache(currency.data.uuid); + if (compendiumItem) { + currency.data.item = compendiumItem; + } + } + delete currency["quantity"]; + return currency; + })); + } + + /** + * The smallest decimal digits shown for any fractional currency amounts. Only used when there is only one currency. + * + * @returns {Number} + */ + static get CURRENCY_DECIMAL_DIGITS() { + return Helpers.getSetting(SETTINGS.CURRENCY_DECIMAL_DIGITS); + } + + /** + * The attribute used to track the price of items in this system + * + * @returns {string} + */ + static get ITEM_PRICE_ATTRIBUTE() { + return Helpers.getSetting(SETTINGS.ITEM_PRICE_ATTRIBUTE); + } + + /** + * The attribute used to track the quantity of items you would get for the price of an item + * + * @returns {string} + */ + static get QUANTITY_FOR_PRICE_ATTRIBUTE() { + return Helpers.getSetting(SETTINGS.QUANTITY_FOR_PRICE_ATTRIBUTE); + } + + /** + * The attribute used to track the quantity of items in this system + * + * @returns {string} + */ + static get ITEM_QUANTITY_ATTRIBUTE() { + return Helpers.getSetting(SETTINGS.ITEM_QUANTITY_ATTRIBUTE); + } + + /** + * The filters for item types eligible for interaction within this system + * + * @returns {Array<{name: string, filters: string}>} + */ + static get ITEM_FILTERS() { + return Helpers.getSetting(SETTINGS.ITEM_FILTERS); + } + + /** + * The attributes for detecting item similarities + * + * @returns {Array} + */ + static get ITEM_SIMILARITIES() { + return Helpers.getSetting(SETTINGS.ITEM_SIMILARITIES); + } + + /** + * The types of items that will always be considered unique when transferring between actors + * + * @returns {Array} + */ + static get UNSTACKABLE_ITEM_TYPES() { + return Helpers.getSetting(SETTINGS.UNSTACKABLE_ITEM_TYPES); + } + + /** + * The system specific default values for item pile actors created in this system + * + * @returns {Object} + */ + static get PILE_DEFAULTS() { + return Helpers.getSetting(SETTINGS.PILE_DEFAULTS); + } + + /** + * The system specific default values for item pile tokens created in this system + * + * @returns {Object} + */ + static get TOKEN_FLAG_DEFAULTS() { + return Helpers.getSetting(SETTINGS.TOKEN_FLAG_DEFAULTS); + } + + /** + * Sets the actor class type used for the original item pile actor in this system + * + * @param {string} inClassType + * @returns {Promise} + */ + static async setActorClassType(inClassType) { + if (typeof inClassType !== "string") { + throw Helpers.custom_error("setActorTypeClass | inClassType must be of type string"); + } + return Helpers.setSetting(SETTINGS.ACTOR_CLASS_TYPE, inClassType); + } + + /** + * Sets the currencies used in this system + * + * @param {Array} inCurrencies + * @returns {Promise} + */ + static async setCurrencies(inCurrencies) { + if (!Array.isArray(inCurrencies)) { + throw Helpers.custom_error("setCurrencies | inCurrencies must be an array"); + } + inCurrencies.forEach(currency => { + if (typeof currency !== "object") { + throw Helpers.custom_error("setCurrencies | each entry in inCurrencies must be of type object"); + } + if (typeof currency.primary !== "boolean") { + throw Helpers.custom_error("setCurrencies | currency.primary must be of type boolean"); + } + if (typeof currency.name !== "string") { + throw Helpers.custom_error("setCurrencies | currency.name must be of type string"); + } + if (typeof currency.abbreviation !== "string") { + throw Helpers.custom_error("setCurrencies | currency.abbreviation must be of type string"); + } + if (typeof currency.exchangeRate !== "number") { + throw Helpers.custom_error("setCurrencies | currency.exchangeRate must be of type number"); + } + if (typeof currency.data !== "object") { + throw Helpers.custom_error("setCurrencies | currency.data must be of type object"); + } + if (typeof currency.data.path !== "string" && typeof currency.data.uuid !== "string" && typeof currency.data.item !== "object") { + throw Helpers.custom_error("setCurrencies | currency.data must contain either \"path\" (string), \"uuid\" (string), or \"item\" (object)"); + } + if (currency.img && typeof currency.img !== "string") { + throw Helpers.custom_error("setCurrencies | currency.img must be of type string"); + } + }); + return Helpers.setSetting(SETTINGS.CURRENCIES, inCurrencies); + } + + /** + * Sets the secondary currencies used in this system + * + * @param {Array} inSecondaryCurrencies + * @returns {Promise} + */ + static async setSecondaryCurrencies(inSecondaryCurrencies) { + if (!Array.isArray(inSecondaryCurrencies)) { + throw Helpers.custom_error("setSecondaryCurrencies | inSecondaryCurrencies must be an array"); + } + inSecondaryCurrencies.forEach(currency => { + if (typeof currency !== "object") { + throw Helpers.custom_error("setSecondaryCurrencies | each entry in inSecondaryCurrencies must be of type object"); + } + if (typeof currency.name !== "string") { + throw Helpers.custom_error("setSecondaryCurrencies | currency.name must be of type string"); + } + if (typeof currency.abbreviation !== "string") { + throw Helpers.custom_error("setSecondaryCurrencies | currency.abbreviation must be of type string"); + } + if (typeof currency.data !== "object") { + throw Helpers.custom_error("setSecondaryCurrencies | currency.data must be of type object"); + } + if (typeof currency.data.path !== "string" && typeof currency.data.uuid !== "string" && typeof currency.data.item !== "object") { + throw Helpers.custom_error("setSecondaryCurrencies | currency.data must contain either \"path\" (string), \"uuid\" (string), or \"item\" (object)"); + } + if (currency.img && typeof currency.img !== "string") { + throw Helpers.custom_error("setSecondaryCurrencies | currency.img must be of type string"); + } + }); + return Helpers.setSetting(SETTINGS.SECONDARY_CURRENCIES, inSecondaryCurrencies); + } + + /** + * Set the smallest decimal digits shown for any fractional currency amounts. Only used when there is only one currency. + * + * @param {Number} inDecimalDigits + * @returns {Promise} + */ + static setCurrencyDecimalDigits(inDecimalDigits) { + if (typeof inDecimalDigits !== "number") { + throw Helpers.custom_error("setCurrencyDecimalDigits | inDecimalDigits must be of type string"); + } + return Helpers.setSetting(SETTINGS.CURRENCY_DECIMAL_DIGITS, inDecimalDigits); + } + + /** + * Sets the attribute used to track the quantity of items in this system + * + * @param {string} inAttribute + * @returns {Promise} + */ + static async setItemQuantityAttribute(inAttribute) { + if (typeof inAttribute !== "string") { + throw Helpers.custom_error("setItemQuantityAttribute | inAttribute must be of type string"); + } + return Helpers.setSetting(SETTINGS.ITEM_QUANTITY_ATTRIBUTE, inAttribute); + } + + /** + * Sets the attribute used to track the price of items in this system + * + * @param {string} inAttribute + * @returns {Promise} + */ + static async setItemPriceAttribute(inAttribute) { + if (typeof inAttribute !== "string") { + throw Helpers.custom_error("setItemPriceAttribute | inAttribute must be of type string"); + } + return Helpers.setSetting(SETTINGS.ITEM_PRICE_ATTRIBUTE, inAttribute); + } + + /** + * Sets the attribute used to track the price of items in this system + * + * @param {string} inAttribute + * @returns {Promise} + */ + static async setQuantityForPriceAttribute(inAttribute) { + if (typeof inAttribute !== "string") { + throw Helpers.custom_error("setQuantityForPriceAttribute | inAttribute must be of type string"); + } + return Helpers.setSetting(SETTINGS.QUANTITY_FOR_PRICE_ATTRIBUTE, inAttribute); + } + + /** + * Sets the items filters for interaction within this system + * + * @param {Array<{path: string, filters: string}>} inFilters + * @returns {Promise} + */ + static async setItemFilters(inFilters) { + if (!Array.isArray(inFilters)) { + throw Helpers.custom_error("setItemFilters | inFilters must be of type array"); + } + inFilters.forEach(filter => { + if (typeof filter?.path !== "string") { + throw Helpers.custom_error("setItemFilters | each entry in inFilters must have a \"path\" property with a value that is of type string"); + } + if (typeof filter?.filters !== "string") { + throw Helpers.custom_error("setItemFilters | each entry in inFilters must have a \"filters\" property with a value that is of type string"); + } + }); + return Helpers.setSetting(SETTINGS.ITEM_FILTERS, inFilters); + } + + /** + * Sets the attributes for detecting item similarities + * + * @param {Array} inPaths + * @returns {Promise} + */ + static async setItemSimilarities(inPaths) { + if (!Array.isArray(inPaths)) { + throw Helpers.custom_error("setItemSimilarities | inPaths must be of type array"); + } + inPaths.forEach(path => { + if (typeof path !== "string") { + throw Helpers.custom_error("setItemSimilarities | each entry in inPaths must be of type string"); + } + }); + return Helpers.setSetting(SETTINGS.ITEM_SIMILARITIES, inPaths); + } + + /** + * Sets the types of items that will always be considered unique when transferring between actors + * + * @param {Array} inTypes + * @returns {Promise} + */ + static async setUnstackableItemTypes(inTypes) { + if (!Array.isArray(inTypes)) { + throw Helpers.custom_error("setUnstackableItemTypes | inTypes must be of type array"); + } + inTypes.forEach(path => { + if (typeof path !== "string") { + throw Helpers.custom_error("setUnstackableItemTypes | each entry in inTypes must be of type string"); + } + }); + return Helpers.setSetting(SETTINGS.UNSTACKABLE_ITEM_TYPES, inTypes); + } + + /** + * Sets the types of items that will always be considered unique when transferring between actors + * + * @param {Object} inDefaults + * @returns {Promise} + */ + static async setPileDefaults(inDefaults) { + if (typeof inDefaults !== "object") { + throw Helpers.custom_error("setPileDefaults | inDefaults must be of type object"); + } + const validKeys = new Set(Object.keys(CONSTANTS.PILE_DEFAULTS)); + for (const key of Object.keys(inDefaults)) { + if (!validKeys.has(key)) { + throw Helpers.custom_error(`setPileDefaults | ${key} is not a valid pile setting`); + } + } + return Helpers.setSetting(SETTINGS.PILE_DEFAULTS, inDefaults); + } + + /** + * Set the flags that will be applied to any tokens created through item piles + * + * @param {Object} inDefaults + * @returns {Promise} + */ + static async setTokenFlagDefaults(inDefaults) { + if (typeof inDefaults !== "object") { + throw Helpers.custom_error("setTokenFlagDefaults | inDefaults must be of type object"); + } + return Helpers.setSetting(SETTINGS.TOKEN_FLAG_DEFAULTS, inDefaults); + } + + /** + * A combination of all the methods above, but this integrates a system's specific + * settings more readily into item piles, allowing users to also change the settings + * afterward. + * + * @param {Object<{ + * VERSION: string, + * ACTOR_CLASS_TYPE: string, + * ITEM_QUANTITY_ATTRIBUTE: string, + * ITEM_PRICE_ATTRIBUTE: string, + * QUANTITY_FOR_PRICE_ATTRIBUTE: string, + * ITEM_FILTERS: Array<{path: string, filters: string}>, + * ITEM_SIMILARITIES: Array, + * UNSTACKABLE_ITEM_TYPES: Array, + * PILE_DEFAULTS: Object, + * TOKEN_FLAG_DEFAULTS: Object, + * ITEM_TRANSFORMER: undefined/Function, + * PRICE_MODIFIER_TRANSFORMER: undefined/Function, + * SYSTEM_HOOKS: undefined/Function, + * SHEET_OVERRIDES: undefined/Function, + * CURRENCIES: Array<{ + * primary: boolean, + * type: string ["attribute"/"item"], + * img: string, + * abbreviation: string, + * data: Object<{ path: string }|{ uuid: string }|{ item: object }>, + * exchangeRate: number + * }>, + * SECONDARY_CURRENCIES: Array<{ + * type: string ["attribute"/"item"], + * img: string, + * abbreviation: string, + * data: Object<{ path: string }|{ uuid: string }|{ item: object }> + * }>, + * CURRENCY_DECIMAL_DIGITS: undefined/number + * }>} inData + */ + static addSystemIntegration(inData) { + + const data = foundry.utils.mergeObject({ + VERSION: "", + ACTOR_CLASS_TYPE: "", + ITEM_QUANTITY_ATTRIBUTE: "", + ITEM_PRICE_ATTRIBUTE: "", + QUANTITY_FOR_PRICE_ATTRIBUTE: "flags.item-piles.system.quantityForPrice", + ITEM_FILTERS: [], + ITEM_SIMILARITIES: [], + UNSTACKABLE_ITEM_TYPES: [], + PILE_DEFAULTS: {}, + TOKEN_FLAG_DEFAULTS: {}, + ITEM_TRANSFORMER: null, + PRICE_MODIFIER_TRANSFORMER: null, + SYSTEM_HOOKS: null, + SHEET_OVERRIDES: null, + CURRENCIES: [], + SECONDARY_CURRENCIES: [], + CURRENCY_DECIMAL_DIGITS: 0.00001 + }, inData) + + if (typeof data["VERSION"] !== "string") { + throw Helpers.custom_error("addSystemIntegration | data.VERSION must be of type string"); + } + + if (typeof data["ACTOR_CLASS_TYPE"] !== "string") { + throw Helpers.custom_error("addSystemIntegration | data.ACTOR_CLASS_TYPE must be of type string"); + } + + if (typeof data["ITEM_QUANTITY_ATTRIBUTE"] !== "string") { + throw Helpers.custom_error("addSystemIntegration | data.ITEM_QUANTITY_ATTRIBUTE must be of type string"); + } + + if (typeof data["ITEM_PRICE_ATTRIBUTE"] !== "string") { + throw Helpers.custom_error("addSystemIntegration | data.ITEM_PRICE_ATTRIBUTE must be of type string"); + } + + if (data["QUANTITY_FOR_PRICE_ATTRIBUTE"] && typeof data["QUANTITY_FOR_PRICE_ATTRIBUTE"] !== "string") { + throw Helpers.custom_error("addSystemIntegration | data.QUANTITY_FOR_PRICE_ATTRIBUTE must be of type string"); + } + + if (!Array.isArray(data["ITEM_FILTERS"])) { + throw Helpers.custom_error("addSystemIntegration | data.ITEM_FILTERS must be of type array"); + } + + data["ITEM_FILTERS"].forEach(filter => { + if (typeof filter?.path !== "string") { + throw Helpers.custom_error("addSystemIntegration | each entry in data.ITEM_FILTERS must have a \"path\" property with a value that is of type string"); + } + if (typeof filter?.filters !== "string") { + throw Helpers.custom_error("addSystemIntegration | each entry in data.ITEM_FILTERS must have a \"filters\" property with a value that is of type string"); + } + }); + + if (data['ITEM_TRANSFORMER']) { + if (!Helpers.isFunction(data['ITEM_TRANSFORMER'])) { + throw Helpers.custom_error("addSystemIntegration | data.ITEM_TRANSFORMER must be of type function"); + } + if (typeof data['ITEM_TRANSFORMER']({}) !== "object") { + throw Helpers.custom_error("addSystemIntegration | data.ITEM_TRANSFORMER's return value must be of type object"); + } + } + + if (data['PRICE_MODIFIER_TRANSFORMER']) { + if (!Helpers.isFunction(data['PRICE_MODIFIER_TRANSFORMER'])) { + throw Helpers.custom_error("addSystemIntegration | data.PRICE_MODIFIER_TRANSFORMER must be of type function"); + } + if (typeof data['PRICE_MODIFIER_TRANSFORMER']({}) !== "object") { + throw Helpers.custom_error("addSystemIntegration | data.PRICE_MODIFIER_TRANSFORMER's return value must be of type object"); + } + } + + if (data['SYSTEM_HOOKS']) { + if (!Helpers.isFunction(data['SYSTEM_HOOKS'])) { + throw Helpers.custom_error("addSystemIntegration | data.SYSTEM_HOOKS must be of type function"); + } + } + + if (data['SHEET_OVERRIDES']) { + if (!Helpers.isFunction(data['SHEET_OVERRIDES'])) { + throw Helpers.custom_error("addSystemIntegration | data.SHEET_OVERRIDES must be of type function"); + } + } + + if (typeof data['PILE_DEFAULTS'] !== "object") { + throw Helpers.custom_error("addSystemIntegration | data.PILE_DEFAULTS must be of type object"); + } + const validKeys = new Set(Object.keys(CONSTANTS.PILE_DEFAULTS)); + for (const key of Object.keys(data['PILE_DEFAULTS'])) { + if (!validKeys.has(key)) { + throw Helpers.custom_error(`addSystemIntegration | data.PILE_DEFAULTS contains illegal key "${key}" that is not a valid pile default`); + } + } + + if (typeof data['TOKEN_FLAG_DEFAULTS'] !== "object") { + throw Helpers.custom_error("addSystemIntegration | data.TOKEN_FLAG_DEFAULTS must be of type object"); + } + + if (!Array.isArray(data['ITEM_SIMILARITIES'])) { + throw Helpers.custom_error("addSystemIntegration | data.ITEM_SIMILARITIES must be of type array"); + } + data['ITEM_SIMILARITIES'].forEach(path => { + if (typeof path !== "string") { + throw Helpers.custom_error("addSystemIntegration | each entry in data.ITEM_SIMILARITIES must be of type string"); + } + }); + + if (data['UNSTACKABLE_ITEM_TYPES']) { + if (!Array.isArray(data['UNSTACKABLE_ITEM_TYPES'])) { + throw Helpers.custom_error("addSystemIntegration | data.UNSTACKABLE_ITEM_TYPES must be of type array"); + } + data['UNSTACKABLE_ITEM_TYPES'].forEach(path => { + if (typeof path !== "string") { + throw Helpers.custom_error("addSystemIntegration | each entry in data.UNSTACKABLE_ITEM_TYPES must be of type string"); + } + }); + } + + if (!Array.isArray(data['CURRENCIES'])) { + throw Helpers.custom_error("addSystemIntegration | data.CURRENCIES must be an array"); + } + data['CURRENCIES'].forEach(currency => { + if (typeof currency !== "object") { + throw Helpers.custom_error("addSystemIntegration | CURRENCIES | each entry in data.CURRENCIES must be of type object"); + } + if (typeof currency.primary !== "boolean") { + throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.primary must be of type boolean"); + } + if (typeof currency.name !== "string") { + throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.name must be of type string"); + } + if (typeof currency.abbreviation !== "string") { + throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.abbreviation must be of type string"); + } + if (typeof currency.exchangeRate !== "number") { + throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.exchangeRate must be of type number"); + } + if (typeof currency.data !== "object") { + throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.data must be of type object"); + } + if (typeof currency.data.path !== "string" && typeof currency.data.uuid !== "string" && typeof currency.data.item !== "object") { + throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.data must contain either \"path\" (string), \"uuid\" (string), or \"item\" (object)"); + } + if (currency.img && typeof currency.img !== "string") { + throw Helpers.custom_error("addSystemIntegration | CURRENCIES | currency.img must be of type string"); + } + }); + + if (!Array.isArray(data['SECONDARY_CURRENCIES'])) { + throw Helpers.custom_error("addSystemIntegration | data.SECONDARY_CURRENCIES must be an array"); + } + data['SECONDARY_CURRENCIES'].forEach(currency => { + if (typeof currency !== "object") { + throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | each entry in data.SECONDARY_CURRENCIES must be of type object"); + } + if (typeof currency.name !== "string") { + throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | currency.name must be of type string"); + } + if (typeof currency.abbreviation !== "string") { + throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | currency.abbreviation must be of type string"); + } + if (typeof currency.data !== "object") { + throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | currency.data must be of type object"); + } + if (typeof currency.data.path !== "string" && typeof currency.data.uuid !== "string" && typeof currency.data.item !== "object") { + throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | currency.data must contain either \"path\" (string), \"uuid\" (string), or \"item\" (object)"); + } + if (currency.img && typeof currency.img !== "string") { + throw Helpers.custom_error("addSystemIntegration | SECONDARY_CURRENCIES | currency.img must be of type string"); + } + }); + + if (data["CURRENCY_DECIMAL_DIGITS"] && typeof data['CURRENCY_DECIMAL_DIGITS'] !== "number") { + throw Helpers.custom_error("addSystemIntegration | data.CURRENCY_DECIMAL_DIGITS must be of type number"); + } + + data['INTEGRATION'] = true; + + SYSTEMS.addSystem(data); + + Helpers.debug(`Registered system settings for ${game.system.id}`, data) + + } + + /** + * Gets all the system item types, including custom item piles item categories + * + * @returns {Array<{primary: boolean, name: string, data: Object, img: string, abbreviation: string, exchange: number}>} + */ + static getPrimaryCurrency(actor = false) { + if (actor && actor instanceof Actor) { + return PileUtilities.getActorPrimaryCurrency(actor); + } + return this.CURRENCIES.find(currency => currency.primary); + } + + /** + * Retrieves all the system item types, including custom item piles item categories + * + * @returns {Object} + */ + static getItemCategories() { + let systemTypes = Object.entries(CONFIG.Item.typeLabels).map(([key, value]) => { + return [key, game.i18n.localize(value)]; + }); + systemTypes.shift(); + let customItemPilesCategories = Array.from(new Set(Helpers.getSetting(SETTINGS.CUSTOM_ITEM_CATEGORIES))).map(cat => { + return [cat, cat]; + }); + return Object.fromEntries(systemTypes.concat(customItemPilesCategories) + .sort((a, b) => a[1] > b[1] ? 1 : -1)); + } + + /* ================= ITEM PILE METHODS ================= */ + + /** + * Creates an item pile token at a location, or an item pile actor, or both at the same time. + * + * @param {object} options Options to pass to the function + * @param {object/boolean} [options.position=false] Where to create the item pile, with x and y coordinates + * @param {string/boolean} [options.sceneId=game.user.viewedScene] Which scene to create the item pile on + * @param {object} [options.tokenOverrides={}] Token data to apply to the newly created token + * @param {object} [options.actorOverrides={}] Actor data to apply to the newly created actor (if unlinked) + * @param {object} [options.itemPileFlags={}] Item pile specific flags to apply to the token and actor + * @param {Array/boolean} [options.items=false] Any items to create on the item pile + * @param {boolean} [options.createActor=false] Whether to create a new item pile actor + * @param {string/boolean} [options.actor=false] The UUID, ID, or name of the actor to use when creating this item pile + * @param {Array/>string/boolean} [options.folders=false] The folder to create the actor in, this can be an array of folder names, which will be traversed and created + * + * @returns {Promise} + */ + static async createItemPile({ + position = false, + sceneId = game.user.viewedScene, + tokenOverrides = {}, + actorOverrides = {}, + itemPileFlags = {}, + items = false, + createActor = false, + actor = false, + folders = false + } = {}) { + + if (position) { + if (typeof position !== "object") { + throw Helpers.custom_error(`createItemPile | position must be of type object`); + } else if (!Helpers.isRealNumber(position.x) || !Helpers.isRealNumber(position.y)) { + throw Helpers.custom_error(`createItemPile | position.x and position.y must be of type numbers`); + } + } + + if (folders) { + if (!Array.isArray(folders)) { + folders = [folders]; + } + folders.forEach(f => { + if (typeof f !== 'string') { + throw Helpers.custom_error(`createItemPile | folder must be of type string or array of strings`); + } + }); + } + + if (actor && !createActor) { + if (typeof actor !== "string") { + throw Helpers.custom_error(`createItemPile | actor must be of type string`); + } + let pileActor = await fromUuid(actor); + if (!pileActor) { + pileActor = game.actors.getName(actor); + } + if (!pileActor) { + pileActor = game.actors.get(actor); + } + if (!pileActor) { + throw Helpers.custom_error(`createItemPile | Could not find actor with the identifier of "${actor}"`); + } + actor = pileActor.uuid; + } + + if (typeof sceneId !== "string") { + throw Helpers.custom_error(`createItemPile | sceneId must be of type string`); + } + + if (typeof tokenOverrides !== "object") { + throw Helpers.custom_error(`createItemPile | tokenOverrides must be of type object`); + } + + if (typeof actorOverrides !== "object") { + throw Helpers.custom_error(`createItemPile | tokenOverrides must be of type object`); + } + + if (typeof itemPileFlags !== "object") { + throw Helpers.custom_error(`createItemPile | tokenOverrides must be of type object`); + } + + if (items) { + if (!Array.isArray(items)) items = [items] + items = items.map(item => { + return item instanceof Item ? item.toObject() : item; + }) + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.CREATE_PILE, { + sceneId, position, actor, createActor, items, tokenOverrides, actorOverrides, itemPileFlags, folders + }); + } + + /** + * Turns tokens and its actors into item piles + * + * @param {Token/TokenDocument/Array} targets The targets to be turned into item piles + * @param {object} options Options to pass to the function + * @param {object} options.pileSettings Overriding settings to be put on the item piles' settings + * @param {object/Function} options.tokenSettings Overriding settings that will update the tokens' settings + * + * @return {Promise} The uuids of the targets after they were turned into item piles + */ + static turnTokensIntoItemPiles(targets, { pileSettings = {}, tokenSettings = {} } = {}) { + + if (!Array.isArray(targets)) targets = [targets]; + + const targetUuids = targets.map(target => { + if (!(target instanceof Token || target instanceof TokenDocument)) { + throw Helpers.custom_error(`turnTokensIntoItemPiles | Target must be of type Token or TokenDocument`) + } + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`turnTokensIntoItemPiles | Could not determine the UUID, please provide a valid target`) + return targetUuid; + }) + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TURN_INTO_PILE, targetUuids, pileSettings, tokenSettings); + } + + /** + * Reverts tokens from an item pile into a normal token and actor + * + * @param {Token/TokenDocument/Array} targets The targets to be reverted from item piles + * @param {object} options Options to pass to the function + * @param {object/Function} options.tokenSettings Overriding settings that will update the tokens + * + * @return {Promise} The uuids of the targets after they were reverted from being item piles + */ + static revertTokensFromItemPiles(targets, { tokenSettings = {} } = {}) { + + if (!Array.isArray(targets)) targets = [targets]; + + const targetUuids = targets.map(target => { + if (!(target instanceof Token || target instanceof TokenDocument)) { + throw Helpers.custom_error(`revertTokensFromItemPiles | Target must be of type Token or TokenDocument`) + } + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`revertTokensFromItemPiles | Could not determine the UUID, please provide a valid target`) + return targetUuid; + }) + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.REVERT_FROM_PILE, targetUuids, tokenSettings); + } + + /** + * Opens a pile if it is enabled and a container + * + * @param {Token/TokenDocument} target + * @param {Token/TokenDocument/boolean} [interactingToken=false] + * + * @return {Promise/boolean} + */ + static openItemPile(target, interactingToken = false) { + const targetActor = Utilities.getActor(target); + if (!PileUtilities.isItemPileContainer(target)) return false; + const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; + const pileData = PileUtilities.getActorFlagData(targetActor); + const wasLocked = pileData.locked; + const wasClosed = pileData.closed; + pileData.closed = false; + pileData.locked = false; + if (wasLocked) { + const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_UNLOCK, targetActor, pileData, interactingTokenDocument); + if (hookResult === false) return false; + } + const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_OPEN, targetActor, pileData, interactingTokenDocument); + if (hookResult === false) return false; + if (wasClosed && pileData.openSound) { + let sound = pileData.openSound; + if (pileData.openSound.includes("*")) { + sound = Helpers.random_array_element(pileData.openSounds) + } + AudioHelper.play({ src: sound }, true) + } + return this.updateItemPile(targetActor, pileData, { interactingToken: interactingTokenDocument }); + } + + /** + * Closes a pile if it is enabled and a container + * + * @param {Token/TokenDocument} target Target pile to close + * @param {Token/TokenDocument/boolean} [interactingToken=false] + * + * @return {Promise/boolean} + */ + static closeItemPile(target, interactingToken = false) { + const targetActor = Utilities.getActor(target); + if (!PileUtilities.isItemPileContainer(target)) return false; + const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; + const pileData = PileUtilities.getActorFlagData(targetActor); + + const wasOpen = !pileData.closed; + pileData.closed = true; + + const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_CLOSE, targetActor, pileData, interactingTokenDocument); + if (hookResult === false) return false; + + if (wasOpen && pileData.closeSound) { + let sound = pileData.closeSound; + if (pileData.closeSound.includes("*")) { + sound = Helpers.random_array_element(pileData.closeSounds) + } + AudioHelper.play({ src: sound }, true) + } + + return this.updateItemPile(targetActor, pileData, { interactingToken: interactingTokenDocument }); + } + + /** + * 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/boolean} + */ + static async toggleItemPileClosed(target, interactingToken = false) { + const targetActor = Utilities.getActor(target); + if (!PileUtilities.isItemPileContainer(target)) return false; + const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; + const pileData = PileUtilities.getActorFlagData(targetActor); + if (pileData.closed) { + await this.openItemPile(targetActor, interactingTokenDocument); + } else { + await this.closeItemPile(targetActor, interactingTokenDocument); + } + return !pileData.closed; + } + + /** + * 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/boolean} + */ + static lockItemPile(target, interactingToken = false) { + const targetActor = Utilities.getActor(target); + if (!PileUtilities.isItemPileContainer(target)) return false; + const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; + const pileData = PileUtilities.getActorFlagData(targetActor); + const wasClosed = pileData.closed; + pileData.closed = true; + pileData.locked = true; + if (!wasClosed) { + const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_CLOSE, targetActor, pileData, interactingTokenDocument); + if (hookResult === false) return false; + } + const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_LOCK, targetActor, pileData, interactingTokenDocument); + if (hookResult === false) return false; + if (!wasClosed && pileData.closeSound) { + let sound = pileData.closeSound; + if (pileData.closeSound.includes("*")) { + sound = Helpers.random_array_element(pileData.closeSounds) + } + AudioHelper.play({ src: sound }, true) + } + return this.updateItemPile(targetActor, pileData, { interactingToken: interactingTokenDocument }); + } + + /** + * 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/boolean} + */ + static unlockItemPile(target, interactingToken = false) { + const targetActor = Utilities.getActor(target); + if (!PileUtilities.isItemPileContainer(target)) return false; + const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; + const pileData = PileUtilities.getActorFlagData(targetActor); + pileData.locked = false; + Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_UNLOCK, targetActor, pileData, interactingTokenDocument); + return this.updateItemPile(targetActor, pileData, { interactingToken: interactingTokenDocument }); + } + + /** + * 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/boolean} + */ + static toggleItemPileLocked(target, interactingToken = false) { + const targetActor = Utilities.getActor(target); + if (!PileUtilities.isItemPileContainer(target)) return false; + const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; + const pileData = PileUtilities.getActorFlagData(targetActor); + if (pileData.locked) { + return this.unlockItemPile(targetActor, interactingTokenDocument); + } + return this.lockItemPile(targetActor, interactingTokenDocument); + } + + /** + * Causes the item pile to play a sound as it was attempted to be opened, but was locked + * + * @param {Token/TokenDocument} target + * @param {Token/TokenDocument/boolean} [interactingToken=false] + * + * @return {Promise} + */ + static rattleItemPile(target, interactingToken = false) { + const targetActor = Utilities.getActor(target); + if (!PileUtilities.isItemPileContainer(target)) return false; + const interactingTokenDocument = interactingToken ? Utilities.getActor(interactingToken) : false; + const pileData = PileUtilities.getActorFlagData(targetActor); + + Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_RATTLE, targetActor, pileData, interactingTokenDocument); + + if (pileData.lockedSound) { + let sound = pileData.lockedSound; + if (pileData.lockedSound.includes("*")) { + sound = Helpers.random_array_element(pileData.lockedSounds) + } + AudioHelper.play({ src: sound }, true); + } + + return ItemPileSocket.executeForEveryone(ItemPileSocket.HANDLERS.CALL_HOOK, CONSTANTS.HOOKS.PILE.RATTLE, Utilities.getUuid(targetActor), pileData, Utilities.getUuid(interactingTokenDocument)); + } + + /** + * 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) { + return PileUtilities.isItemPileLocked(target); + } + + /** + * 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) { + return PileUtilities.isItemPileClosed(target); + } + + /** + * 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) { + return PileUtilities.isItemPileContainer(target); + } + + /** + * Updates a pile with new data. + * + * @param {Actor/TokenDocument} target Target token or actor to update + * @param {object} newData New data to update the actor with + * @param {object} options Options to pass to the function + * @param {Token/TokenDocument/boolean} [options.interactingToken=false] If an actor caused this update, you can pass one here to pass it along to macros that the item pile may run + * @param {Object/boolean} [options.tokenSettings=false] Updates to make to the target token + * + * @return {Promise} + */ + static updateItemPile(target, newData, { interactingToken = false, tokenSettings = false } = {}) { + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`updateItemPile | Could not determine the UUID, please provide a valid target`); + + const interactingTokenUuid = interactingToken ? Utilities.getUuid(interactingToken) : false; + if (interactingToken && !interactingTokenUuid) throw Helpers.custom_error(`updateItemPile | Could not determine the UUID, please provide a valid target`); + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.UPDATE_PILE, targetUuid, newData, { + interactingTokenUuid, tokenSettings + }); + } + + /** + * Deletes a pile, calling the relevant hooks. + * + * @param {Token/TokenDocument} target + * + * @return {Promise} + */ + static deleteItemPile(target) { + if (!PileUtilities.isValidItemPile(target)) { + throw Helpers.custom_error(`deleteItemPile | This is not an item pile, please provide a valid target`); + } + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`deleteItemPile | Could not determine the UUID, please provide a valid target`); + if (!targetUuid.includes("Token")) { + throw Helpers.custom_error(`deleteItemPile | Please provide a Token or TokenDocument`); + } + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.DELETE_PILE, targetUuid); + } + + /** + * Splits an item pile's content between all players (or a specified set of target actors). + * + * @param target {Token/TokenDocument/Actor} The item pile to split + * @param {object} options Options to pass to the function + * @param {boolean/TokenDocument/Actor/Array} [options.targets=false] The targets to receive the split contents + * @param {boolean/TokenDocument/Actor} [options.instigator=false] Whether this was triggered by a specific actor + * + * @returns {Promise} + */ + static async splitItemPileContents(target, { targets = false, instigator = false } = {}) { + + if (!PileUtilities.isItemPileLootable(target)) return false; + + const itemPileUuid = Utilities.getUuid(target); + if (!itemPileUuid) throw Helpers.custom_error(`SplitItemPileContents | Could not determine the UUID, please provide a valid item pile`) + + const itemPileActor = Utilities.getActor(target); + + if (targets) { + if (!Array.isArray(targets)) { + targets = [targets] + } + targets.forEach(actor => { + if (!(actor instanceof TokenDocument || actor instanceof Actor)) { + throw Helpers.custom_error("SplitItemPileContents | Each of the entries in targets must be of type TokenDocument or Actor") + } + }) + targets = targets.map(target => Utilities.getActor(target)); + } + + if (instigator && !(instigator instanceof TokenDocument || instigator instanceof Actor)) { + throw Helpers.custom_error("SplitItemPileContents | instigator must be of type TokenDocument or Actor") + } + + const actorUuids = (targets || SharingUtilities.getPlayersForItemPile(itemPileActor) + .map(u => u.character)) + .map(actor => Utilities.getUuid(actor)); + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.SPLIT_PILE, itemPileUuid, actorUuids, game.user.id, instigator); + + } + + /** + * Retrieves the price modifiers for a given item piles merchant + * + * @param {Actor/TokenDocument} target Target token or actor to retrieve the modifiers from + * @param {object} options Options to pass to the function + * @param {Token/TokenDocument/Actor/string/boolean} [options.actor=false] The actor whose price modifiers to consider + * @param {boolean} [options.absolute=false] Whether to only consider the actor's modifiers (true means not considering the merchant's base modifiers) + * + * @return {Object} + */ + static getMerchantPriceModifiers(target, { actor = false, absolute = false } = {}) { + + const merchantActor = Utilities.getActor(target); + + if (!(merchantActor instanceof Actor)) { + throw Helpers.custom_error(`getMerchantPriceModifiers | target must be of type Actor`); + } + + if (!PileUtilities.isItemPileMerchant(merchantActor)) { + throw Helpers.custom_error(`getMerchantPriceModifiers | target is not an item pile merchant`); + } + + if (actor) { + if (!(actor instanceof Actor) && typeof actor !== "string") { + throw Helpers.custom_error(`getMerchantPriceModifiers | actor must be of type Actor or string (UUID)`); + } + if (typeof actor === "string") { + actor = fromUuidSync(actor) || false; + } + } + + if (typeof absolute !== "boolean") { + throw Helpers.custom_error(`getMerchantPriceModifiers | absolute must be of type boolean`); + } + + return PileUtilities.getMerchantModifiersForActor(target, { actor, absolute }); + + } + + /** + * Updates the price modifiers for a given item piles merchant + * + * @param {Actor/TokenDocument} target Target token or actor to update modifiers on + * @param {Array<{ + * actor?: Actor, + * actorUuid?: string, + * relative?: boolean, + * override?: boolean, + * buyPriceModifier?: number, + * sellPriceModifier?: number + * }>} priceModifierData The price modifier data to update on the merchant + * + * @return {Promise} + */ + static updateMerchantPriceModifiers(target, priceModifierData = []) { + + const merchantActor = Utilities.getActor(target); + + const targetUuid = Utilities.getUuid(merchantActor); + if (!targetUuid) throw Helpers.custom_error(`updateMerchantPriceModifiers | Could not determine the UUID, please provide a valid target`); + + if (!PileUtilities.isItemPileMerchant(merchantActor)) { + throw Helpers.custom_error(`updateMerchantPriceModifiers | Target is not an item pile merchant`); + } + + const flagData = PileUtilities.getActorFlagData(merchantActor); + + const actorPriceModifiers = flagData?.actorPriceModifiers ?? []; + + for (const priceModifier of priceModifierData) { + if (priceModifier.actor && !(priceModifier.actor instanceof Actor)) { + throw Helpers.custom_error(`updateMerchantPriceModifiers | priceModifierData.actor must be of type Actor`); + } + if (priceModifier.actor) { + priceModifier.actorUuid = priceModifier.actor.uuid; + } + if (priceModifier.actorUuid && typeof priceModifier.actorUuid !== "string") { + throw Helpers.custom_error(`updateMerchantPriceModifiers | if priceModifierData.actor if not provided, priceModifierData.actorUuid must be of type string `); + } + if (!priceModifier.actorUuid) { + throw Helpers.custom_error(`updateMerchantPriceModifiers | Could not find the UUID for the given actor`); + } + if (priceModifier.relative !== undefined && typeof priceModifier.relative !== "boolean") { + throw Helpers.custom_error(`updateMerchantPriceModifiers | priceModifierData.relative must be of type boolean`); + } + if (priceModifier.override !== undefined && typeof priceModifier.override !== "boolean") { + throw Helpers.custom_error(`updateMerchantPriceModifiers | priceModifierData.override must be of type boolean`); + } + if (priceModifier.buyPriceModifier !== undefined && typeof priceModifier.buyPriceModifier !== "number") { + throw Helpers.custom_error(`updateMerchantPriceModifiers | priceModifierData.buyPriceModifier must be of type number`); + } + if (priceModifier.sellPriceModifier !== undefined && typeof priceModifier.sellPriceModifier !== "number") { + throw Helpers.custom_error(`updateMerchantPriceModifiers | priceModifierData.sellPriceModifier must be of type number`); + } + + let actorPriceModifierIndex = actorPriceModifiers.findIndex(existingPriceModifier => existingPriceModifier.actorUuid === priceModifier.actorUuid); + if (actorPriceModifierIndex === -1) { + actorPriceModifierIndex = actorPriceModifiers.push({}) - 1; + } + + const oldBuyPriceModifier = actorPriceModifiers[actorPriceModifierIndex]?.buyPriceModifier ?? flagData?.buyPriceModifier ?? 1; + const newBuyPriceModifier = Math.max(0, priceModifier.relative ? oldBuyPriceModifier + priceModifier.buyPriceModifier : priceModifier.buyPriceModifier ?? oldBuyPriceModifier); + + const oldSellPriceModifier = actorPriceModifiers[actorPriceModifierIndex]?.sellPriceModifier ?? flagData?.sellPriceModifier ?? 0.5; + const newSellPriceModifier = Math.max(0, priceModifier.relative ? oldSellPriceModifier + priceModifier.sellPriceModifier : priceModifier.sellPriceModifier ?? oldSellPriceModifier); + + actorPriceModifiers[actorPriceModifierIndex] = foundry.utils.mergeObject(actorPriceModifiers[actorPriceModifierIndex], { + actorUuid: priceModifier.actorUuid, + buyPriceModifier: newBuyPriceModifier, + sellPriceModifier: newSellPriceModifier, + override: priceModifier.override ?? false, + }); + + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.UPDATE_PILE, targetUuid, { actorPriceModifiers }); + + } + + /* ================= ITEM AND ATTRIBUTE METHODS ================= */ + + /** + * Adds item to an actor, increasing item quantities if matches were found + * + * @param {Actor/TokenDocument/Token} target The target to add an item to + * @param {Array} items An array of objects, with the key "item" being an item object or an Item class (the foundry class), with an optional key of "quantity" being the amount of the item to add + * @param {object} options Options to pass to the function + * @param {boolean} [options.mergeSimilarItems=true] Whether to merge similar items based on their name and type + * @param {boolean} [options.removeExistingActorItems=false] Whether to remove the actor's existing items before adding the new ones + * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault + * @param {string/boolean} [options.interactionId=false] The interaction ID of this action + * + * @returns {Promise} An array of objects, each containing the item that was added or updated, and the quantity that was added + */ + static addItems(target, items, { + mergeSimilarItems = true, removeExistingActorItems = false, skipVaultLogging = false, interactionId = false + } = {}) { + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`addItems | Could not determine the UUID, please provide a valid target`) + + const itemsToAdd = [] + items.forEach(itemData => { + + let item = itemData; + if (itemData instanceof Item) { + item = itemData.toObject(); + } else if (itemData.item) { + item = itemData.item instanceof Item ? itemData.item.toObject() : itemData.item; + if (itemData.flags) { + setProperty(item, "flags", foundry.utils.mergeObject(getProperty(item, "flags") ?? {}, getProperty(itemData, "flags"))); + } + } else if (itemData.id) { + item = target.items.get(itemData.id); + if (item) { + item = item.toObject(); + } else { + throw Helpers.custom_error(`addItems | Could not find item with id ${itemData.id} on actor with UUID ${targetUuid}!`) + } + } + + if (itemData?.quantity !== undefined) { + Utilities.setItemQuantity(item, itemData.quantity, true); + } + + const existingItems = mergeSimilarItems ? Utilities.findSimilarItem(itemsToAdd, item) : false; + if (existingItems && PileUtilities.canItemStack(item, target)) { + Utilities.setItemQuantity(existingItems, Utilities.getItemQuantity(existingItems) + Utilities.getItemQuantity(item)); + } else { + itemsToAdd.push(item); + } + + }); + + if (interactionId && typeof interactionId !== "string") throw Helpers.custom_error(`addItems | interactionId must be of type string`); + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.ADD_ITEMS, targetUuid, itemsToAdd, game.user.id, { + removeExistingActorItems, interactionId, skipVaultLogging + }); + } + + /** + * Subtracts the quantity of items on an actor. If the quantity of an item reaches 0, the item is removed from the 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 Items (the foundry class) or strings of IDs to remove all quantities of + * @param {object} options Options to pass to the function + * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault + * @param {string/boolean} [options.interactionId=false] The interaction ID of this action + * + * @returns {Promise} An array of objects, each containing the item that was removed or updated, the quantity that was removed, and whether the item was deleted + */ + static removeItems(target, items, { skipVaultLogging = false, interactionId = false } = {}) { + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`removeItems | Could not determine the UUID, please provide a valid target`); + + const targetActorItems = PileUtilities.getActorItems(target, { getItemCurrencies: true }); + + items = items.map(itemData => { + + let item; + if (typeof itemData === "string" || itemData._id) { + const itemId = typeof itemData === "string" ? itemData : itemData._id; + item = targetActorItems.find(actorItem => actorItem.id === itemId); + if (!item) { + throw Helpers.custom_error(`removeItems | Could not find item with id "${itemId}" on target "${targetUuid}"`) + } + item = item.toObject(); + } else { + if (itemData.item instanceof Item) { + item = itemData.item.toObject(); + } else if (itemData instanceof Item) { + item = itemData.toObject(); + } else { + item = itemData.item; + } + let foundActorItem = targetActorItems.find(actorItem => actorItem.id === item._id); + if (!foundActorItem) { + throw Helpers.custom_error(`removeItems | Could not find item with id "${item._id}" on target "${targetUuid}"`) + } + } + + return { + _id: item._id, quantity: itemData?.quantity ?? Utilities.getItemQuantity(item) + } + }); + + if (interactionId) { + if (typeof interactionId !== "string") throw Helpers.custom_error(`removeItems | interactionId must be of type string`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.REMOVE_ITEMS, targetUuid, items, game.user.id, { + interactionId, + skipVaultLogging + }); + } + + /** + * 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 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"), or Items (the foundry class) or strings of IDs to transfer all quantities of + * @param {object} options Options to pass to the function + * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault + * @param {string/boolean} [options.interactionId=false] The interaction ID of this action + * + * @returns {Promise} An array of objects, each containing the item that was added or updated, and the quantity that was transferred + */ + static transferItems(source, target, items, { skipVaultLogging = false, interactionId = false } = {}) { + + const sourceUuid = Utilities.getUuid(source); + if (!sourceUuid) throw Helpers.custom_error(`transferItems | Could not determine the UUID, please provide a valid source`) + + const sourceActorItems = PileUtilities.getActorItems(source, { getItemCurrencies: true }); + + items = items.map(itemData => { + + let item; + if (typeof itemData === "string" || itemData._id) { + const itemId = typeof itemData === "string" ? itemData : itemData._id; + item = sourceActorItems.find(actorItem => actorItem.id === itemId); + if (!item) { + throw Helpers.custom_error(`transferItems | Could not find item with id "${itemId}" on target "${sourceUuid}"`) + } + item = item.toObject(); + } else if (itemData instanceof Item) { + item = itemData.toObject(); + } else if (itemData.item instanceof Item) { + item = itemData.item.toObject(); + } else { + item = itemData.item; + } + + let foundActorItem = sourceActorItems.find(actorItem => actorItem.id === item._id); + if (!foundActorItem) { + throw Helpers.custom_error(`transferItems | Could not find item with id "${item._id}" on target "${sourceUuid}"`) + } + + return { + _id: item._id, + quantity: Math.max(itemData?.quantity ?? Utilities.getItemQuantity(itemData), 0), + flags: getProperty(itemData, "flags") + } + }); + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`transferItems | Could not determine the UUID, please provide a valid target`) + + if (interactionId) { + if (typeof interactionId !== "string") throw Helpers.custom_error(`transferItems | interactionId must be of type string`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_ITEMS, sourceUuid, targetUuid, items, game.user.id, { + interactionId, skipVaultLogging + }); + + } + + /** + * Transfers all items between the source and the target. + * + * @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 {object} options Options to pass to the function + * @param {Array/boolean} [options.itemFilters=false] Array of item types disallowed - will default to module settings if none provided + * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault + * @param {string/boolean} [options.interactionId=false] The interaction ID of this action + * + * @returns {Promise} An array containing all the items that were transferred to the target + */ + static transferAllItems(source, target, { + itemFilters = false, + skipVaultLogging = false, + interactionId = false + } = {}) { + + const sourceUuid = Utilities.getUuid(source); + if (!sourceUuid) throw Helpers.custom_error(`transferAllItems | Could not determine the UUID, please provide a valid source`) + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`transferAllItems | Could not determine the UUID, please provide a valid target`) + + if (itemFilters) { + if (!Array.isArray(itemFilters)) throw Helpers.custom_error(`transferAllItems | itemFilters must be of type array`); + itemFilters.forEach(entry => { + if (typeof entry?.path !== "string") throw Helpers.custom_error(`transferAllItems | each entry in the itemFilters must have a "path" property that is of type string`); + if (typeof entry?.filter !== "string") throw Helpers.custom_error(`transferAllItems | each entry in the itemFilters must have a "filter" property that is of type string`); + }) + } + + if (interactionId) { + if (typeof interactionId !== "string") throw Helpers.custom_error(`transferAllItems | interactionId must be of type string`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_ALL_ITEMS, sourceUuid, targetUuid, game.user.id, { + itemFilters, skipVaultLogging, interactionId + }); + } + + /** + * Sets attributes on an actor + * + * @param {Actor/Token/TokenDocument} target The target whose attribute will have their quantity set + * @param {object} attributes An object with each key being an attribute path, and its value being the quantity to set + * @param {object} options Options to pass to the function + * @param {string/boolean} [options.interactionId=false] The interaction ID of this action + * + * @returns {Promise} An array containing a key value pair of the attribute path and the quantity of that attribute that was set + * + */ + static setAttributes(target, attributes, { interactionId = false } = {}) { + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`setAttributes | Could not determine the UUID, please provide a valid target`); + + const targetActor = Utilities.getActor(target); + + Object.entries(attributes).forEach(entry => { + const [attribute, quantity] = entry; + if (!hasProperty(targetActor, attribute)) { + throw Helpers.custom_error(`setAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); + } + if (!Helpers.isRealNumber(quantity)) { + throw Helpers.custom_error(`setAttributes | Attribute "${attribute}" must be of type number`); + } + }); + + if (interactionId) { + if (typeof interactionId !== "string") throw Helpers.custom_error(`setAttributes | interactionId must be of type string`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.SET_ATTRIBUTES, targetUuid, attributes, game.user.id, { + interactionId + }); + + } + + /** + * Adds 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 + * @param {object} options Options to pass to the function + * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault + * @param {string/boolean} [options.interactionId=false] The interaction ID of this action + * + * @returns {Promise} An array containing a key value pair of the attribute path and the quantity of that attribute that was added + * + */ + static addAttributes(target, attributes, { skipVaultLogging = false, interactionId = false } = {}) { + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`addAttributes | Could not determine the UUID, please provide a valid target`); + + const targetActor = Utilities.getActor(target); + + Object.entries(attributes).forEach(entry => { + const [attribute, quantity] = entry; + if (!hasProperty(targetActor, attribute)) { + throw Helpers.custom_error(`addAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); + } + if (!Helpers.isRealNumber(quantity) && quantity > 0) { + throw Helpers.custom_error(`addAttributes | Attribute "${attribute}" must be of type number and greater than 0`); + } + }); + + if (interactionId) { + if (typeof interactionId !== "string") throw Helpers.custom_error(`addAttributes | interactionId must be of type string`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.ADD_ATTRIBUTES, targetUuid, attributes, game.user.id, { + skipVaultLogging, + interactionId + }); + + } + + /** + * Subtracts attributes on the target + * + * @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 {object} options Options to pass to the function + * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault + * @param {string/boolean} [options.interactionId=false] The interaction ID of this action + * + * @returns {Promise} An array containing a key value pair of the attribute path and the quantity of that attribute that was removed + */ + static removeAttributes(target, attributes, { skipVaultLogging = false, interactionId = false } = {}) { + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`removeAttributes | Could not determine the UUID, please provide a valid target`); + + const targetActor = Utilities.getActor(target); + + let attributesToSend = {}; + if (Array.isArray(attributes)) { + attributes.forEach(attribute => { + if (typeof attribute !== "string") { + throw Helpers.custom_error(`removeAttributes | Each attribute in the array must be of type string`); + } + if (!hasProperty(targetActor, attribute)) { + throw Helpers.custom_error(`removeAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); + } + attributesToSend[attribute] = Number(getProperty(targetActor, attribute)); + }); + } else { + Object.entries(attributes).forEach(entry => { + const [attribute, quantity] = entry; + if (!hasProperty(targetActor, attribute)) { + throw Helpers.custom_error(`removeAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); + } + if (!Helpers.isRealNumber(quantity) && quantity > 0) { + throw Helpers.custom_error(`removeAttributes | Attribute "${attribute}" must be of type number and greater than 0`); + } + }); + attributesToSend = attributes; + } + + if (interactionId) { + if (typeof interactionId !== "string") throw Helpers.custom_error(`removeAttributes | interactionId must be of type string`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.REMOVE_ATTRIBUTES, targetUuid, attributesToSend, game.user.id, { + skipVaultLogging, + interactionId + }); + + } + + /** + * 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 {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 {object} options Options to pass to the function + * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault + * @param {string/boolean} [options.interactionId=false] The interaction ID of this action + * + * @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 transferAttributes(source, target, attributes, { skipVaultLogging = false, interactionId = false } = {}) { + + const sourceUuid = Utilities.getUuid(source); + if (!sourceUuid) throw Helpers.custom_error(`transferAttributes | Could not determine the UUID, please provide a valid source`); + const sourceActor = Utilities.getActor(source); + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`transferAttributes | Could not determine the UUID, please provide a valid target`); + const targetActor = Utilities.getActor(target); + + if (Array.isArray(attributes)) { + attributes.forEach(attribute => { + if (typeof attribute !== "string") { + throw Helpers.custom_error(`transferAttributes | Each attribute in the array must be of type string`); + } + if (!hasProperty(sourceActor, attribute)) { + throw Helpers.custom_error(`transferAttributes | Could not find attribute ${attribute} on source's actor with UUID "${targetUuid}"`); + } + if (!hasProperty(targetActor, attribute)) { + throw Helpers.custom_error(`transferAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); + } + }); + } else { + Object.entries(attributes).forEach(entry => { + const [attribute, quantity] = entry; + if (!hasProperty(sourceActor, attribute)) { + throw Helpers.custom_error(`transferAttributes | Could not find attribute ${attribute} on source's actor with UUID "${targetUuid}"`); + } + if (!hasProperty(targetActor, attribute)) { + throw Helpers.custom_error(`transferAttributes | Could not find attribute ${attribute} on target's actor with UUID "${targetUuid}"`); + } + if (!Helpers.isRealNumber(quantity) && quantity > 0) { + throw Helpers.custom_error(`transferAttributes | Attribute "${attribute}" must be of type number and greater than 0`); + } + }); + } + + if (interactionId) { + if (typeof interactionId !== "string") throw Helpers.custom_error(`transferAttributes | interactionId must be of type string`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_ATTRIBUTES, sourceUuid, targetUuid, attributes, game.user.id, { + skipVaultLogging, + interactionId + }); + + } + + /** + * Transfers all dynamic attributes from a source to a target, removing it or subtracting from the source and adding them to the target + * + * @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 {object} options Options to pass to the function + * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault + * @param {string/boolean} [options.interactionId=false] The interaction ID of this action + * + * @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 transferAllAttributes(source, target, { skipVaultLogging = false, interactionId = false } = {}) { + + const sourceUuid = Utilities.getUuid(source); + if (!sourceUuid) throw Helpers.custom_error(`transferAllAttributes | Could not determine the UUID, please provide a valid source`); + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`transferAllAttributes | Could not determine the UUID, please provide a valid target`); + + if (interactionId) { + if (typeof interactionId !== "string") throw Helpers.custom_error(`transferAllAttributes | interactionId must be of type string`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_ALL_ATTRIBUTES, sourceUuid, targetUuid, game.user.id, { + skipVaultLogging, + interactionId + }); + + } + + /** + * Transfers all items and attributes 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 {object} options Options to pass to the function + * @param {Array/boolean} [options.itemFilters=false] Array of item types disallowed - will default to module settings if none provided + * @param {boolean} [options.skipVaultLogging=false] Whether to skip logging this action to the target actor if it is a vault + * @param {string/boolean} [options.interactionId=false] The ID of this interaction + * + * @returns {Promise} An object containing all items and attributes transferred to the target + */ + static transferEverything(source, target, { + itemFilters = false, + skipVaultLogging = false, + interactionId = false + } = {}) { + + const sourceUuid = Utilities.getUuid(source); + if (!sourceUuid) throw Helpers.custom_error(`transferEverything | Could not determine the UUID, please provide a valid source`); + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`transferEverything | Could not determine the UUID, please provide a valid target`); + + if (itemFilters) { + if (!Array.isArray(itemFilters)) throw Helpers.custom_error(`transferEverything | itemFilters must be of type array`); + itemFilters.forEach(entry => { + if (typeof entry?.path !== "string") throw Helpers.custom_error(`transferEverything | each entry in the itemFilters must have a "path" property that is of type string`); + if (typeof entry?.filter !== "string") throw Helpers.custom_error(`transferEverything | each entry in the itemFilters must have a "filter" property that is of type string`); + }) + } + + if (interactionId) { + if (typeof interactionId !== "string") throw Helpers.custom_error(`transferEverything | interactionId must be of type string`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_EVERYTHING, sourceUuid, targetUuid, game.user.id, { + itemFilters, skipVaultLogging, interactionId + }); + + } + + /** + * Turns a string of currencies into an array containing the data and quantities for each currency + * + * @param {string} currencies A string of currencies to convert (eg, "5gp 25sp") + * + * @returns {Array} An array of object containing the data and quantity for each currency + */ + static getCurrenciesFromString(currencies) { + if (typeof currencies !== "string") { + throw Helpers.custom_error(`getCurrenciesFromString | currencies must be of type string`) + } + return PileUtilities.getPriceFromString(currencies).currencies; + } + + /** + * This method takes a string, and another string or number, an alternatively a boolean, to modify the first string's currencies.Whether to subtract the second currencies from the first; not needed if the second argument is a number + * + * @param firstCurrencies {string} The starting sum of money as strings + * @param secondCurrencies {string/number} A string of currencies to alter the first with, or a number to multiply it + * @param subtract {boolean} Whether to subtract the second currencies from the first; not needed if the second argument is a number + * + * @returns {string} The resulting currency string + */ + static calculateCurrencies(firstCurrencies, secondCurrencies, subtract = true) { + if (typeof firstCurrencies !== "string") { + throw Helpers.custom_error(`getCurrencyDifference | firstCurrencies must be of type string`) + } + if (!(typeof secondCurrencies === "string" || typeof secondCurrencies === "number")) { + throw Helpers.custom_error(`getCurrencyDifference | secondCurrencies must be of type string or number`) + } + + const firstCurrenciesData = PileUtilities.getPriceFromString(firstCurrencies); + const secondCurrenciesData = typeof secondCurrencies === "string" + ? PileUtilities.getPriceFromString(secondCurrencies) + : secondCurrencies; + + const totalCost = typeof secondCurrencies === "string" + ? firstCurrenciesData.overallCost + (secondCurrenciesData.overallCost * (subtract ? -1 : 1)) + : firstCurrenciesData.overallCost * secondCurrencies; + + const differenceCost = PileUtilities.getPriceArray(totalCost); + const newBaseCurrencies = differenceCost + .filter(currency => !currency.secondary && currency.cost) + .reduce((acc, currency) => { + return acc + currency.abbreviation.replace("{#}", currency.cost) + " "; + }, ""); + + const newSecondaryCurrencies = firstCurrenciesData.currencies + .reduce((acc, currency, index) => { + if (!currency.secondary) return acc; + const quantity = typeof secondCurrencies === "string" + ? currency.quantity + (secondCurrenciesData.currencies[index].quantity * (subtract ? -1 : 1)) + : Math.round(currency.quantity * secondCurrencies); + if (quantity <= 0) return acc; + return acc + currency.abbreviation.replace("{#}", quantity) + " "; + }, ""); + + return (newBaseCurrencies + newSecondaryCurrencies).trim(); + } + + /** + * Turns a string of currencies or a number into an object containing payment data, and the change an optional target would receive back + * + * @param {string/number} price A string of currencies to convert (eg, "5gp 25sp") or a number + * @param {object} options Options to pass to the function + * @param {number/boolean} [options.quantity=1] The number of this to buy + * @param {string/boolean} [options.target=false] The target whose currencies to check against + * + * @returns {object} An object containing the price data + */ + static getPaymentData(price, { quantity = 1, target = false } = {}) { + + let targetActor = false; + if (target) { + targetActor = Utilities.getActor(target); + if (!targetActor) throw Helpers.custom_error(`getPaymentData | Could not determine target actor`); + } + + let overallCost; + let secondaryPrices = false; + if (typeof price === "string") { + const priceData = PileUtilities.getPriceFromString(price) + const currenciesToRemove = priceData.currencies.filter(currency => currency.quantity); + if (!currenciesToRemove.length) { + throw Helpers.custom_error(`getPaymentData | Could not determine currencies to remove with string "${price}"`); + } + overallCost = priceData.overallCost; + secondaryPrices = currenciesToRemove.filter(currency => currency.secondary); + } else if (typeof price === "number") { + overallCost = price; + } else { + if (!targetActor) throw Helpers.custom_error(`getPaymentData | price must be of type string or number`); + } + + return PileUtilities.getPaymentData({ + purchaseData: [{ cost: overallCost, quantity, secondaryPrices }], + buyer: targetActor + }); + + } + + /** + * TODO: Remove in future update + * @deprecated + */ + static getPaymentDataFromString(...args) { + Helpers.custom_warning(`game.itempiles.API.getPaymentDataFromString has been deprecated in favor of game.itempiles.API.getPaymentData`, false); + return this.getPaymentData(...args); + } + + /** + * Adds currencies to the target + * + * @param {Actor/Token/TokenDocument} target The actor to add the currencies to + * @param {string} currencies A string of currencies to add (eg, "5gp 25sp") + * @param {object} options Options to pass to the function + * @param {string/boolean} [options.interactionId=false] The ID of this interaction + * + * @returns {Promise} An object containing the items and attributes added to the target + */ + static addCurrencies(target, currencies, { interactionId = false } = {}) { + + const targetActor = Utilities.getActor(target); + const targetUuid = Utilities.getUuid(targetActor); + if (!targetUuid) throw Helpers.custom_error(`addCurrency | Could not determine the UUID, please provide a valid target`); + + if (typeof currencies !== "string") { + throw Helpers.custom_error(`addCurrency | currencies must be of type string`) + } + + const currenciesToAdd = PileUtilities.getPriceFromString(currencies).currencies + .filter(currency => currency.quantity); + + if (!currenciesToAdd.length) { + throw Helpers.custom_error(`addCurrency | Could not determine currencies to add with string "${currencies}"`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.ADD_CURRENCIES, targetUuid, currencies, game.user.id, { interactionId }); + + } + + /** + * Removes currencies from the target + * + * @param {Actor/Token/TokenDocument} target The actor to remove currencies from + * @param {string} currencies A string of currencies to remove (eg, "5gp 25sp") + * @param {object} options Options to pass to the function + * @param {boolean} [options.change=true] Whether the actor can get change back + * @param {string/boolean} [options.interactionId=false] The ID of this interaction + * + * @returns {Promise} An object containing the items and attributes removed from the target + */ + static removeCurrencies(target, currencies, { change = true, interactionId = false } = {}) { + + const targetActor = Utilities.getActor(target); + const targetUuid = Utilities.getUuid(targetActor); + if (!targetUuid) throw Helpers.custom_error(`removeCurrencies | Could not determine the UUID, please provide a valid target`); + + let overallCost; + let secondaryPrices = false; + if (typeof currencies === "string") { + const priceData = PileUtilities.getPriceFromString(currencies) + const currenciesToRemove = priceData.currencies.filter(currency => currency.quantity); + if (!currenciesToRemove.length) { + throw Helpers.custom_error(`removeCurrencies | Could not determine currencies to remove with string "${currencies}"`); + } + overallCost = priceData.overallCost; + secondaryPrices = currenciesToRemove.filter(currency => currency.secondary); + } else { + throw Helpers.custom_error(`removeCurrencies | price must be of type string`); + } + + const paymentData = PileUtilities.getPaymentData({ + purchaseData: [{ cost: overallCost, quantity: 1, secondaryPrices }], buyer: targetActor + }); + + if (!paymentData.canBuy) { + throw Helpers.custom_error(`removeCurrencies | ${targetActor.name} cannot afford "${currencies}"`); + } + + if (!change && paymentData.buyerChange.length) { + throw Helpers.custom_error(`removeCurrencies | ${targetActor.name} cannot afford "${currencies}" without receiving change!`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.REMOVE_CURRENCIES, targetUuid, currencies, game.user.id, { interactionId }); + + } + + /** + * Transfers currencies between the source and the target. + * + * @param {Actor/Token/TokenDocument} source The actor to transfer currencies from + * @param {Actor/Token/TokenDocument} target The actor to receive the currencies + * @param {string} currencies A string of currencies to transfer (eg, "5gp 25sp") + * @param {object} options Options to pass to the function + * @param {boolean} [options.change=true] Whether the source actor can get change back + * @param {string/boolean} [options.interactionId=false] The ID of this interaction + * + * @returns {Promise} An object containing the items and attributes transferred to the target + */ + static transferCurrencies(source, target, currencies, { change = true, interactionId = false } = {}) { + + const sourceActor = Utilities.getActor(source); + const sourceUuid = Utilities.getUuid(sourceActor); + if (!sourceUuid) throw Helpers.custom_error(`transferCurrencies | Could not determine the UUID, please provide a valid source`); + + const targetActor = Utilities.getActor(target); + const targetUuid = Utilities.getUuid(targetActor); + if (!targetUuid) throw Helpers.custom_error(`transferCurrencies | Could not determine the UUID, please provide a valid target`); + + let overallCost; + let secondaryPrices = false; + if (typeof currencies === "string") { + const priceData = PileUtilities.getPriceFromString(currencies) + const currenciesToRemove = priceData.currencies.filter(currency => currency.quantity); + if (!currenciesToRemove.length) { + throw Helpers.custom_error(`transferCurrencies | Could not determine currencies to remove with string "${currencies}"`); + } + overallCost = priceData.overallCost; + secondaryPrices = currenciesToRemove.filter(currency => currency.secondary); + } else { + throw Helpers.custom_error(`transferCurrencies | price must be of type string`); + } + + const paymentData = PileUtilities.getPaymentData({ + purchaseData: [{ cost: overallCost, quantity: 1, secondaryPrices }], buyer: sourceActor + }); + + if (!paymentData.canBuy) { + throw Helpers.custom_error(`transferCurrencies | ${sourceActor.name} cannot afford to transfer "${currencies}"`); + } + + if (!change && paymentData.buyerChange.length) { + throw Helpers.custom_error(`transferCurrencies | ${sourceActor.name} cannot afford to transfer "${currencies}" without receiving change!`); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_CURRENCIES, sourceUuid, targetUuid, currencies, game.user.id, { interactionId }); + + } + + /** + * Transfers all currencies between the source and the target. + * + * @param {Actor/Token/TokenDocument} source The actor to transfer all currencies from + * @param {Actor/Token/TokenDocument} target The actor to receive all the currencies + * @param {object} options Options to pass to the function + * @param {string/boolean} [options.interactionId=false] The ID of this interaction + * + * @returns {Promise} An object containing all items and attributes transferred to the target + */ + static transferAllCurrencies(source, target, { interactionId = false } = {}) { + + const sourceUuid = Utilities.getUuid(source); + if (!sourceUuid) throw Helpers.custom_error(`transferCurrencies | Could not determine the UUID, please provide a valid source`); + + const targetUuid = Utilities.getUuid(target); + if (!targetUuid) throw Helpers.custom_error(`transferCurrencies | Could not determine the UUID, please provide a valid target`); + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRANSFER_ALL_CURRENCIES, sourceUuid, targetUuid, game.user.id, { interactionId }); + + } + + /** + * Rolls on a table of items and collates them to be able to be added to actors and such + * + * @param {string/RollTable} table The name, ID, UUID, or the table itself, or an array of such + * @param {object} options Options to pass to the function + * @param {string/number} [options.timesToRoll="1"] The number of times to roll on the tables, which can be a roll formula + * @param {boolean} [options.resetTable=true] Whether to reset the table before rolling it + * @param {boolean} [options.normalizeTable=true] Whether to normalize the table before rolling it + * @param {boolean} [options.displayChat=false] Whether to display the rolls to the chat + * @param {object} [options.rollData={}] Data to inject into the roll formula + * @param {Actor/string/boolean} [options.targetActor=false] The target actor to add the items to, or the UUID of an actor + * @param {boolean} [options.removeExistingActorItems=false] Whether to clear the target actor's items before adding the ones rolled + * @param {boolean/string} [options.customCategory=false] Whether to apply a custom category to the items rolled + * + * @returns {Promise>} An array of object containing the item data and their quantity + */ + static async rollItemTable(table, { + timesToRoll = "1", + resetTable = true, + normalizeTable = false, + displayChat = false, + rollData = {}, + targetActor = false, + removeExistingActorItems = false, + customCategory = false + } = {}) { + + let rollTable = table; + if (typeof table === "string") { + let potentialTable = await fromUuid(table); + if (!potentialTable) { + potentialTable = game.tables.get(table) + } + if (!potentialTable) { + potentialTable = game.tables.getName(table) + } + if (!potentialTable) { + throw Helpers.custom_error(`rollItemTable | could not find table with string "${table}"`); + } + if (resetTable && table.startsWith("Compendium")) { + resetTable = false; + } + rollTable = potentialTable; + } + + if (!(rollTable instanceof RollTable)) { + throw Helpers.custom_error(`rollItemTable | table must be of type RollTable`); + } + + table = rollTable.uuid; + + if (!(typeof timesToRoll === "string" || typeof timesToRoll === "number")) { + throw Helpers.custom_error(`rollItemTable | timesToRoll must be of type string or number`); + } + + if (typeof rollData !== "object") { + throw Helpers.custom_error(`rollItemTable | rollData must be of type object`); + } + + if (typeof removeExistingActorItems !== "boolean") { + throw Helpers.custom_error(`rollItemTable | removeExistingActorItems of type boolean`); + } + + if (targetActor) { + targetActor = Utilities.getActor(targetActor); + if (!(targetActor instanceof Actor)) { + throw Helpers.custom_error(`rollItemTable | could not find the actor of the target actor`); + } + } + + const items = await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.ROLL_ITEM_TABLE, { + table, + timesToRoll, + resetTable, + normalizeTable, + displayChat, + rollData, + customCategory, + targetActor: Utilities.getUuid(targetActor), + removeExistingActorItems, + userId: game.user.id + }); + + if (items) { + for (const entry of items) { + entry.item = targetActor ? targetActor.items.get(entry.item._id) : await Item.implementation.create(entry.item, { temporary: true }); + } + } + + return items; + + } + + /** + * Refreshes the merchant's inventory, potentially removing existing items and populating it based on its item tables + * + * @param {Actor/Token/TokenDocument} target The merchant actor to refresh the inventory of + * @param {object} options Options to pass to the function + * @param {boolean} [options.removeExistingActorItems=true] Whether to clear the merchant's existing inventory before adding the new items + * @returns {Promise} + */ + static async refreshMerchantInventory(target, { removeExistingActorItems = true } = {}) { + + if (target) { + target = Utilities.getActor(target); + if (!(target instanceof Actor)) { + throw Helpers.custom_error(`refreshMerchantInventory | could not find the actor of the target actor`); + } + } + + const targetUuid = Utilities.getUuid(target); + + if (!PileUtilities.isItemPileMerchant(target)) { + throw Helpers.custom_error(`refreshMerchantInventory | target of uuid ${targetUuid} is not a merchant`); + } + + if (typeof removeExistingActorItems !== "boolean") { + throw Helpers.custom_error(`refreshMerchantInventory | removeExistingActorItems of type boolean`); + } + + const items = await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.REFRESH_MERCHANT_INVENTORY, targetUuid, { + removeExistingActorItems, + userId: game.user.id + }); + + if (items) { + for (const entry of items) { + entry.item = target.items.get(entry.item._id); + } + } + + return items; + + } + + + /** + * Gets all the valid items from a given actor or token, excluding items based on its item type filters + * + * @param {Actor/TokenDocument/Token} target The target to get the items from + * + * @returns {Array} Array containing the target's valid items + */ + static getActorItems(target) { + return PileUtilities.getActorItems(target); + } + + static findSimilarItem(itemsToSearch, itemToFind) { + return Utilities.findSimilarItem(itemsToSearch, itemToFind); + } + + /** + * Gets the valid currencies from a given actor or token + * + * @param {Actor/TokenDocument/Token} target The target to get the currencies from + * @param {object} [options] Object containing optional parameters + * @param {Boolean} [options.getAll] Whether to get all the currencies, regardless of quantity + * + * @returns {Array} An array of objects containing the data about each currency + */ + static getActorCurrencies(target, { getAll = false, secondary = True } = {}) { + return PileUtilities.getActorCurrencies(target, { getAll, secondary }); + } + + static updateTokenHud() { + return ItemPileSocket.executeForEveryone(ItemPileSocket.HANDLERS.RERENDER_TOKEN_HUD); + } + + static requestTrade(user) { + return TradeAPI._requestTrade(user); + } + + static spectateTrade(tradeId) { + return TradeAPI._spectateTrade(tradeId); + } + + /** + * Renders the appropriate interface for a given actor + * + * @param {Actor/TokenDocument} target The actor whose interface to render + * @param {object} options An object containing the options for this method + * @param {Array} [options.userIds] An array of users or user ids for each user to render the interface for (defaults to only self) + * @param {Actor/TokenDocument} [options.inspectingTarget] Sets what actor should be viewing the interface + * @param {boolean} [options.useDefaultCharacter] Whether other users should use their assigned character when rendering the interface + * + * @returns {Promise} + */ + static renderItemPileInterface(target, { + userIds = null, inspectingTarget = null, useDefaultCharacter = false + } = {}) { + + const targetDocument = Utilities.getDocument(target); + const targetUuid = Utilities.getUuid(targetDocument); + if (!targetUuid) throw Helpers.custom_error(`renderItemPileInterface | Could not determine the UUID, please provide a valid target item pile`); + + if (!PileUtilities.isValidItemPile(targetDocument)) { + throw Helpers.custom_error("renderItemPileInterface | This target is not a valid item pile") + } + + if (!inspectingTarget && !useDefaultCharacter) { + useDefaultCharacter = true; + } + + if (inspectingTarget && useDefaultCharacter) { + throw Helpers.custom_error("renderItemPileInterface | You cannot force users to use both their default character and a specific character to inspect the pile") + } + + const inspectingTargetUuid = inspectingTarget ? Utilities.getUuid(inspectingTarget) : false; + if (inspectingTarget && !inspectingTargetUuid) throw Helpers.custom_error(`renderItemPileInterface | Could not determine the UUID, please provide a valid inspecting target`); + + if (!Array.isArray(userIds)) { + if (userIds === null) { + userIds = [game.user.id]; + } else { + userIds = [userIds] + } + } else { + userIds = userIds.map(user => { + return user instanceof User ? user.id : user; + }) + } + + if (!game.user.isGM) { + if (userIds.length > 1 || !userIds.includes(game.user.id)) { + throw Helpers.custom_error(`renderItemPileInterface | You are not a GM, so you cannot force others to render an item pile's interface`); + } + userIds = [game.user.id]; + } + + if (userIds.length === 1 && userIds[0] === game.user.id) { + return PrivateAPI._renderItemPileInterface(targetUuid, { + inspectingTargetUuid, useDefaultCharacter, remote: true + }) + } + + for (const userId of userIds) { + const user = game.users.get(userId); + if (!user) throw Helpers.custom_error(`renderItemPileInterface | No user with ID "${userId}" exists`); + if (user.isGM) continue; + if (useDefaultCharacter) { + if (!user.character) { + Helpers.custom_warning(`renderItemPileInterface | User "${user.name}" has no default character`, true); + return; + } + } + } + + return ItemPileSocket.executeForUsers(ItemPileSocket.HANDLERS.RENDER_INTERFACE, userIds, targetUuid, { + inspectingTargetUuid, useDefaultCharacter, remote: true + }); + + } + + /** + * Closes any open interfaces from a given item pile actor + * + * @param {Actor/TokenDocument} target The actor whose interface to potentially close + * @param {object} options An object containing the options for this method + * @param {Array} [options.userIds] An array of users or user ids for each user to close the interface for (defaults to only self) + * + * @returns {Promise} + */ + static unrenderItemPileInterface(target, { userIds = null } = {}) { + + const targetDocument = Utilities.getDocument(target); + const targetUuid = Utilities.getUuid(targetDocument); + if (!targetUuid) throw Helpers.custom_error(`unrenderItemPileInterface | Could not determine the UUID, please provide a valid target item pile`); + + if (!PileUtilities.isValidItemPile(targetDocument)) { + throw Helpers.custom_error("unrenderItemPileInterface | This target is not a valid item pile") + } + + if (!Array.isArray(userIds)) { + if (userIds === null) { + userIds = [game.user.id]; + } else { + userIds = [userIds] + } + } else { + userIds = userIds.map(user => { + return user instanceof User ? user.id : user; + }) + } + + if (!game.user.isGM) { + if (userIds.length > 1 || !userIds.includes(game.user.id)) { + throw Helpers.custom_error(`unrenderItemPileInterface | You are not a GM, so you cannot force others to close an item pile's interface`); + } + userIds = [game.user.id]; + } + + if (userIds.length === 1 && userIds[0] === game.user.id) { + return PrivateAPI._unrenderItemPileInterface(targetUuid, { remote: true }); + } + + for (const userId of userIds) { + const user = game.users.get(userId); + if (!user) throw Helpers.custom_error(`unrenderItemPileInterface | No user with ID "${userId}" exists`); + } + + return ItemPileSocket.executeForUsers(ItemPileSocket.HANDLERS.UNRENDER_INTERFACE, userIds, targetUuid, { remote: true }); + + } + + /** + * Retrieves the total numerical cost of an item + * + * @param item + * @returns {number} + */ + static getCostOfItem(item) { + return PileUtilities.getCostOfItem(item); + } + + /** + * Get the prices array for a given item + * + * @param {Item} item Item to get the price for + * @param {object} options Options to pass to the function + * @param {Actor/boolean} [options.seller=false] Actor that is selling the item + * @param {Actor/boolean} [options.buyer=false] Actor that is buying the item + * @param {number} [options.quantity=1] Quantity of item to buy + * + * @returns {Array} Array containing all the different purchase options for this item + */ + static getPricesForItem(item, { seller = false, buyer = false, quantity = 1 } = {}) { + + if (!(item instanceof Item)) { + throw Helpers.custom_error("getPricesForItem | The given item must be of type Item"); + } + + if (seller) { + seller = Utilities.getActor(seller); + if (!seller) { + throw Helpers.custom_error("getPricesForItem | Could not determine actor for the seller"); + } + } else { + if (!item.parent) { + throw Helpers.custom_error("getPricesForItem | If no seller was given, the item must belong to an actor"); + } + seller = Utilities.getActor(item.parent); + } + + if (buyer) { + buyer = Utilities.getActor(buyer); + if (!buyer) { + throw Helpers.custom_error(`getPricesForItem | Could not determine the actor for the buyer`); + } + } + + return PileUtilities.getPriceData({ item, seller, buyer, quantity }); + + } + + /** + * Trades multiple items between one actor to another, and currencies and/or change is exchanged between them + * + * @param {Actor/Token/TokenDocument} seller The actor that is selling the item + * @param {Actor/Token/TokenDocument} buyer The actor that is buying the item + * @param {Array>} items An array of objects containing the item or the id of the + * item to be sold, the quantity to be sold, and the payment + * index to be used + * @param {string/boolean} [interactionId=false] The ID of this interaction + * + * @returns {Promise} The items that were created and the attributes that were changed + */ + static tradeItems(seller, buyer, items, { interactionId = false } = {}) { + + const sellerActor = Utilities.getActor(seller); + const sellerUuid = Utilities.getUuid(sellerActor); + if (!sellerUuid) { + throw Helpers.custom_error(`tradeItems | Could not determine the UUID of the seller, please provide a valid actor or token`, true); + } + + const buyerActor = Utilities.getActor(buyer); + const buyerUuid = Utilities.getUuid(buyer); + if (!buyerUuid) { + throw Helpers.custom_error(`tradeItems | Could not determine the UUID of the buyer, please provide a valid actor or token`, true); + } + + const itemsToSell = items.map(data => { + + data = foundry.utils.mergeObject({ + item: "", quantity: 1, paymentIndex: 0 + }, data); + + if (!data.item) { + throw Helpers.custom_error(`tradeItems | You must provide an item!`, true); + } + + let actorItem; + if (typeof data.item === "string") { + actorItem = sellerActor.items.get(data.item) || sellerActor.items.getName(data.item); + if (!actorItem) { + throw Helpers.custom_error(`tradeItems | Could not find item on seller with identifier "${data.item}"`); + } + } else { + actorItem = sellerActor.items.get(data.item instanceof Item ? data.item.id : data.item._id) || sellerActor.items.getName(data.item.name); + if (!actorItem) { + throw Helpers.custom_error(`tradeItems | Could not find provided item on seller`); + } + } + + const itemPrices = PileUtilities.getPriceData({ + item: actorItem, seller: sellerActor, buyer: buyerActor, quantity: data.quantity + }); + if (itemPrices.length) { + if (data.paymentIndex >= itemPrices.length || data.paymentIndex < 0) { + throw Helpers.custom_error(`tradeItems | That payment index does not exist on ${actorItem.name}`, true); + } + + const selectedPrice = itemPrices[data.paymentIndex]; + if (data.quantity > selectedPrice.maxQuantity) { + throw Helpers.custom_error(`tradeItems | The buyer actor cannot afford ${data.quantity} of ${actorItem.name} (max ${selectedPrice.maxQuantity})`, true); + } + } + + return { + id: actorItem.id, quantity: data.quantity, paymentIndex: data.paymentIndex + }; + + }); + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.TRADE_ITEMS, sellerUuid, buyerUuid, itemsToSell, game.user.id, { interactionId }); + + } + + static async registerItemPileType(type, label, flags = []) { + game.i18n.translations['ITEM-PILES'].Types[type] = "Custom: " + label; + CONSTANTS.CUSTOM_PILE_TYPES[type] = flags; + } + + static isItemInvalid(item) { + return PileUtilities.isItemInvalid(item.parent, item); + } + + static canItemStack(item) { + return PileUtilities.canItemStack(item); + } + + static getVaultGridData(vaultActor) { + return PileUtilities.getVaultGridData(vaultActor); + } + + static getActorFlagData(actor) { + return PileUtilities.getActorFlagData(actor); + } + + static getItemQuantity(item) { + return Utilities.getItemQuantity(item); + } + + static showItemPileConfig(actor) { + if (!PileUtilities.isValidItemPile(actor)) return; + return ItemPileConfig.show(actor); + } } diff --git a/src/API/chat-api.js b/src/API/chat-api.js index c687a663..a0963e51 100644 --- a/src/API/chat-api.js +++ b/src/API/chat-api.js @@ -8,634 +8,634 @@ import TradeAPI from "./trade-api.js"; export default class ChatAPI { - static initialize() { - - Helpers.hooks.on("preCreateChatMessage", this._preCreateChatMessage.bind(this)); - Helpers.hooks.on("renderChatMessage", this._renderChatMessage.bind(this)); - Helpers.hooks.on(CONSTANTS.HOOKS.ITEM.TRANSFER, this._outputTransferItem.bind(this)); - Helpers.hooks.on(CONSTANTS.HOOKS.ATTRIBUTE.TRANSFER, this._outputTransferCurrency.bind(this)); - Helpers.hooks.on(CONSTANTS.HOOKS.TRANSFER_EVERYTHING, this._outputTransferEverything.bind(this)); - Helpers.hooks.on(CONSTANTS.HOOKS.PILE.SPLIT_INVENTORY, this._outputSplitItemPileInventory.bind(this)); - Helpers.hooks.on(CONSTANTS.HOOKS.TRADE.STARTED, this._outputTradeStarted.bind(this)); - Helpers.hooks.on(CONSTANTS.HOOKS.TRADE.COMPLETE, this._outputTradeComplete.bind(this)); - Helpers.hooks.on(CONSTANTS.HOOKS.ITEM.TRADE, this._outputMerchantTradeComplete.bind(this)); - Helpers.hooks.on(CONSTANTS.HOOKS.ITEM.GIVE, this._outputGiveItem.bind(this)); - - $(document).on("click", ".item-piles-chat-card .item-piles-collapsible", async function () { - if ($(this).attr("open")) return; - await Helpers.wait(25); - $(this).parent()[0].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' }); - }); - } - - static _preCreateChatMessage(chatMessage) { - - if (!Helpers.getSetting(SETTINGS.ENABLE_TRADING)) return; - - const content = chatMessage.content.toLowerCase(); - - if (!(content.startsWith("!itempiles") || content.startsWith("!ip"))) return; - - const args = content.split(" ").slice(1); - - if (args[0] === "trade") { - setTimeout(() => { - game.itempiles.API.requestTrade(); - }); - } - - return false; - - } - - static _renderChatMessage(app, html) { - html.find(".item-piles-specate-trade").click(function () { - game.itempiles.API.spectateTrade($(this).data()); - }); - } - - static _disableTradingButton(publicTradeId) { - const message = Array.from(game.messages).find(message => { - return getProperty(message, CONSTANTS.FLAGS.PUBLIC_TRADE_ID) === publicTradeId; - }); - if (!message) return; - const update = this._replaceChatContent(message); - return message.update(update) - } - - static async disablePastTradingButtons() { - if (!game.user.isGM) return; - - const messages = Array.from(game.messages).filter(message => { - return getProperty(message, CONSTANTS.FLAGS.PUBLIC_TRADE_ID); - }); - - if (!messages.length) return; - const updates = []; - for (let message of messages) { - const update = this._replaceChatContent(message); - const tradeId = getProperty(message, CONSTANTS.FLAGS.PUBLIC_TRADE_ID); - const tradeUsers = getProperty(message, CONSTANTS.FLAGS.TRADE_USERS); - const bothUsersActive = tradeUsers.filter(userId => game.users.get(userId).active).length === tradeUsers.length; - if (!bothUsersActive) { - updates.push(update); - } else { - const otherUsers = tradeUsers.filter(userId => userId !== game.user.id); - const tradeData = await TradeAPI._requestTradeData({ tradeId, tradeUser: otherUsers[0] }); - if (!tradeData) { - updates.push(update); - } - } - } - - if (!updates.length) return; - - return ChatMessage.updateDocuments(updates); - - } - - static _replaceChatContent(message) { - const tradeId = getProperty(message, CONSTANTS.FLAGS.PUBLIC_TRADE_ID); - const stringToFind = `data-trade-id="${tradeId}"`; - let content = message.content; - content = content.replace(stringToFind, ""); - content = content.replace(stringToFind, "disabled"); - content = content.replace(game.i18n.localize("ITEM-PILES.Chat.TradeSpectate"), game.i18n.localize("ITEM-PILES.Chat.SpectateDisabled")); - return { - _id: message.id, - content, - [`flags.-=${CONSTANTS.MODULE_NAME}`]: null - }; - } - - - /** - * Outputs to chat based on transferring an item from or to an item pile - * - * @param source - * @param target - * @param items - * @param userId - * @param interactionId - * @returns {Promise} - */ - static async _outputTransferItem(source, target, items, userId, interactionId) { - if (!PileUtilities.isItemPileLootable(source)) return; - if (!interactionId || game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; - const [itemData, itemCurrencies] = await this._formatItemData(source, items); - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.PICKUP_CHAT_MESSAGE, source.uuid, target.uuid, itemData, itemCurrencies, userId, interactionId); - } - - /** - * Outputs to chat based on transferring a currency from or to an item pile - * - * @param source - * @param target - * @param currencies - * @param userId - * @param interactionId - * @returns {Promise} - */ - static async _outputTransferCurrency(source, target, currencies, userId, interactionId) { - if (!PileUtilities.isItemPileLootable(source)) return; - if (!interactionId || game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; - const currencyData = this._formatCurrencyData(source, currencies); - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.PICKUP_CHAT_MESSAGE, source.uuid, target.uuid, [], currencyData, userId, interactionId); - } - - /** - * Outputs to chat based on giving an item from one actor to another - * - * @param source - * @param target - * @param item - * @param userId - * @returns {Promise} - */ - static async _outputGiveItem(source, target, item, userId) { - if (game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; - const [itemData, itemCurrencies] = await this._formatItemData(source, [item]); - return this._giveChatMessage(source, target, itemData.concat(itemCurrencies), userId); - } - - /** - * Outputs to chat based on transferring everything from or to an item pile - * - * @param source - * @param target - * @param items - * @param currencies - * @param userId - * @param interactionId - * @returns {Promise} - */ - static async _outputTransferEverything(source, target, items, currencies, userId, interactionId) { - if (!PileUtilities.isItemPileLootable(source)) return; - if (!interactionId || game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; - const [itemData, itemCurrencies] = await this._formatItemData(source, items); - const currencyData = this._formatCurrencyData(source, currencies).concat(itemCurrencies); - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.PICKUP_CHAT_MESSAGE, source.uuid, target.uuid, itemData, currencyData, userId, interactionId); - } - - static _outputSplitItemPileInventory(source, pileDeltas, actorDeltas, userId) { - if (!PileUtilities.isItemPileLootable(source)) return; - if (game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.SPLIT_CHAT_MESSAGE, source.uuid, pileDeltas, actorDeltas, userId); - } - - static async _outputTradeStarted(party_1, party_2, publicTradeId, isPrivate) { - if (party_1.user !== game.user.id || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT) || isPrivate) return; - return this._outputTradeStartedToChat(party_1, party_2, publicTradeId); - } - - static async _outputTradeComplete(instigator, party_1, party_2, publicTradeId, isPrivate) { - if (!Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; - return this._outputTradeCompleteToChat(instigator, party_1, party_2, publicTradeId, isPrivate); - } - - static async _outputMerchantTradeComplete(source, target, priceInformation, userId, interactionId) { - if (!Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; - if (!PileUtilities.isItemPileMerchant(source)) return; - if (!interactionId || game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.MERCHANT_TRADE_CHAT_MESSAGE, source.uuid, target.uuid, priceInformation, userId, interactionId); - } - - /** - * Formats item data to a chat friendly structure - * - * @param itemPile - * @param items - * @param divideBy - * @returns {Promise} - */ - static async _formatItemData(itemPile, items, divideBy = 1) { - const formattedItems = []; - const formattedCurrencies = []; - const currencyList = PileUtilities.getActorCurrencies(itemPile, { getAll: true }); - for (const itemData of items) { - const tempItem = await Item.implementation.create(itemData.item, { temporary: true }); - const data = { - name: game.i18n.localize(tempItem.name), - img: tempItem.img ?? itemData?.item?.img ?? "", - quantity: Math.abs(itemData.quantity) / divideBy - }; - if (PileUtilities.isItemCurrency(tempItem, { actorCurrencies: currencyList })) { - formattedCurrencies.push(data) - } else { - formattedItems.push(data); - } - } - return [formattedItems, formattedCurrencies]; - } - - /** - * Formats currency data to a chat friendly structure - * - * @param itemPile - * @param currencies - * @param divideBy - * @returns {Array} - */ - static _formatCurrencyData(itemPile, currencies, divideBy = 1) { - const currencyList = PileUtilities.getActorCurrencies(itemPile, { getAll: true }); - return Object.entries(currencies).map(entry => { - const currency = currencyList.find(currency => currency.id === entry[0]); - return { - name: game.i18n.localize(currency.name), - img: currency.img ?? "", - quantity: Math.abs(entry[1]) / divideBy, - index: currencyList.indexOf(currency) - } - }); - } - - /** - * Outputs the transferred data in chat - * - * @param sourceUuid - * @param targetUuid - * @param items - * @param currencies - * @param userId - * @param interactionId - * @returns {Promise} - */ - static async _outputPickupToChat(sourceUuid, targetUuid, items, currencies, userId, interactionId) { - - const sourceActor = Utilities.getActor(sourceUuid); - const targetActor = Utilities.getActor(targetUuid); - - const now = (+new Date()); - - // Get all messages younger than 3 hours, and grab the last 10, then reverse them (latest to oldest) - const messages = Array.from(game.messages).filter(message => (now - message.timestamp) <= (10800000)).slice(-10); - messages.reverse() - - for (const message of messages) { - const flags = getProperty(message, CONSTANTS.FLAGS.PILE); - if (flags && flags.version && !foundry.utils.isNewerVersion(Helpers.getModuleVersion(), flags.version) && flags.source === sourceUuid && flags.target === targetUuid && flags.interactionId === interactionId) { - return this._updateExistingPickupMessage(message, sourceActor, targetActor, items, currencies, interactionId) - } - } - - const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/looted.html", { - message: game.i18n.format("ITEM-PILES.Chat.Pickup", { name: targetActor.name }), - itemPile: sourceActor, - actor: targetActor, - items: items, - currencies: currencies - }); - - return this._createNewChatMessage(userId, { - user: game.user.id, - type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: chatCardHtml, - flavor: "Item Piles", - speaker: ChatMessage.getSpeaker({ alias: game.user.name }), - [CONSTANTS.FLAGS.PILE]: { - version: Helpers.getModuleVersion(), - source: sourceUuid, - target: targetUuid, - items: items, - currencies: currencies, - interactionId: interactionId - } - }) - - } - - static _matchEntries(existingEntries, incomingEntries) { - - const combinedEntries = existingEntries.map(existingEntry => { - const foundEntry = incomingEntries.find(item => item.name === existingEntry.name && existingEntry.img === item.img); - if (foundEntry) { - existingEntry.quantity += foundEntry.quantity; - incomingEntries.splice(incomingEntries.indexOf(foundEntry), 1) - } - return existingEntry; - }); - - incomingEntries.forEach(item => combinedEntries.push(item)); - - return combinedEntries; - - } - - static async _updateExistingPickupMessage(message, sourceActor, targetActor, items, currencies, interactionId) { - - const flags = getProperty(message, CONSTANTS.FLAGS.PILE); - - const newItems = this._matchEntries(flags.items, items); - const newCurrencies = this._matchEntries(flags.currencies, currencies); - - newCurrencies.sort((a, b) => { - return a.index - b.index; - }) - - const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/looted.html", { - message: game.i18n.format("ITEM-PILES.Chat.Pickup", { name: targetActor.name }), - itemPile: sourceActor, - actor: targetActor, - items: newItems, - currencies: newCurrencies - }); - - return message.update({ - content: chatCardHtml, - [`${CONSTANTS.FLAGS.PILE}.interactionId`]: interactionId, - [`${CONSTANTS.FLAGS.PILE}.items`]: newItems, - [`${CONSTANTS.FLAGS.PILE}.currencies`]: newCurrencies, - }); - - } - - static async _outputSplitToChat(sourceUuid, pileDeltas, actorDeltas, userId) { - - const sourceActor = Utilities.getActor(sourceUuid); - - const divideBy = Object.values(actorDeltas).length; - - const [items, itemCurrencies] = await this._formatItemData(sourceActor, pileDeltas.itemDeltas, divideBy); - const currencies = this._formatCurrencyData(sourceActor, pileDeltas.attributeDeltas, divideBy).concat(itemCurrencies); - - const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/looted.html", { - message: game.i18n.format("ITEM-PILES.Chat.Split", { num_players: divideBy }), - itemPile: sourceActor, - items: items, - currencies: currencies - }); - - return this._createNewChatMessage(userId, { - user: game.user.id, - type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: chatCardHtml, - flavor: "Item Piles", - speaker: ChatMessage.getSpeaker({ alias: game.user.name }) - }); - - } - - static async _outputTradeStartedToChat(party_1, party_2, publicTradeId) { - - const party_1_actor = Utilities.getActor(party_1.actor); - const party_2_actor = Utilities.getActor(party_2.actor); - - const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/trade-started.html", { - party_1_actor, - party_2_actor, - publicTradeId, - userId: game.user.id - }); - - return this._createNewChatMessage(game.user.id, { - user: game.user.id, - type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: chatCardHtml, - flavor: "Item Piles", - speaker: ChatMessage.getSpeaker({ alias: game.user.name }), - [CONSTANTS.FLAGS.PUBLIC_TRADE_ID]: publicTradeId, - [CONSTANTS.FLAGS.TRADE_USERS]: [party_1.user, party_2.user] - }); - } - - static async _outputTradeCompleteToChat(instigator, party_1, party_2, publicTradeId, isPrivate) { - - if (instigator !== game.user.id) return; - - const party_1_actor = Utilities.getActor(party_1.actor); - const party_1_data = { - actor: party_1_actor, - items: party_2.items, - currencies: party_2.currencies.concat(party_2.itemCurrencies) - } - party_1_data.got_nothing = !party_1_data.items.length && !party_1_data.currencies.length; - - const party_2_actor = Utilities.getActor(party_2.actor); - const party_2_data = { - actor: party_2_actor, - items: party_1.items, - currencies: party_1.currencies.concat(party_1.itemCurrencies) - } - party_2_data.got_nothing = !party_2_data.items.length && !party_2_data.currencies.length; - - if (party_1.got_nothing && party_2.got_nothing) return; - - const enableCollapse = (party_1_data.items.length + party_1_data.currencies.length + party_2_data.items.length + party_2_data.currencies.length) > 6; - - const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/trade-complete.html", { - party_1: party_1_data, - party_2: party_2_data, - publicTradeId, - isPrivate, - enableCollapse - }); - - return this._createNewChatMessage(game.user.id, { - user: game.user.id, - type: isPrivate ? CONST.CHAT_MESSAGE_TYPES.WHISPER : CONST.CHAT_MESSAGE_TYPES.OTHER, - content: chatCardHtml, - flavor: "Item Piles" + (isPrivate ? ": " + game.i18n.localize("ITEM-PILES.Chat.PrivateTrade") : ""), - speaker: ChatMessage.getSpeaker({ alias: game.user.name }), - whisper: isPrivate ? [party_2.user] : [] - }); - - } - - static async _outputMerchantTradeToChat(sourceUuid, targetUuid, priceInformation, userId, interactionId) { - - const sourceActor = Utilities.getActor(sourceUuid); - const targetActor = Utilities.getActor(targetUuid); - - const now = (+new Date()); - - priceInformation.id = randomID(); - - // Get all messages younger than 3 hours, and grab the last 10, then reverse them (latest to oldest) - const messages = Array.from(game.messages).filter(message => (now - message.timestamp) <= (10800000)).slice(-10); - messages.reverse(); - - for (const message of messages) { - const flags = getProperty(message, CONSTANTS.FLAGS.PILE); - if (flags && flags.version && !foundry.utils.isNewerVersion(Helpers.getModuleVersion(), flags.version) && flags.source === sourceUuid && flags.target === targetUuid && flags.interactionId === interactionId) { - return this._updateExistingMerchantMessage(message, sourceActor, targetActor, priceInformation, interactionId) - } - } - - const pileData = PileUtilities.getActorFlagData(sourceActor); - - const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/merchant-traded.html", { - message: game.i18n.format("ITEM-PILES.Chat.MerchantTraded", { - name: targetActor.name, - merchant: sourceActor.name - }), - merchant: { - name: sourceActor.name, - img: pileData.merchantImage || sourceActor.img - }, - actor: targetActor, - priceInformation: [priceInformation] - }); - - return this._createNewChatMessage(userId, { - user: game.user.id, - type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: chatCardHtml, - flavor: "Item Piles", - speaker: ChatMessage.getSpeaker({ alias: game.user.name }), - [CONSTANTS.FLAGS.PILE]: { - version: Helpers.getModuleVersion(), - source: sourceUuid, - target: targetUuid, - priceInformation: [priceInformation], - interactionId: interactionId - } - }); - - } - - static async _giveChatMessage(source, target, items) { - - const now = (+new Date()); - - const sourceActor = Utilities.getActor(source); - const targetActor = Utilities.getActor(target); - - // Get all messages younger than 1 minute, and grab the last 5, then reverse them (latest to oldest) - const messages = Array.from(game.messages) - .filter(message => (now - message.timestamp) <= (60000)) - .slice(-5) - .reverse(); - - for (const message of messages) { - const flags = getProperty(message, CONSTANTS.FLAGS.PILE); - if (flags && flags.version && !foundry.utils.isNewerVersion(Helpers.getModuleVersion(), flags.version) && flags.source === sourceActor.uuid && flags.target === targetActor.uuid && message.isAuthor) { - return this._updateExistingGiveMessage(message, sourceActor, targetActor, items) - } - } - - const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/gave-items.html", { - message: game.i18n.format("ITEM-PILES.Chat.GaveItems", { source: sourceActor.name, target: targetActor.name }), - source: sourceActor, - target: targetActor, - items: items - }); - - return this._createNewChatMessage(game.user.id, { - user: game.user.id, - type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: chatCardHtml, - flavor: "Item Piles", - speaker: ChatMessage.getSpeaker({ alias: game.user.name }), - [CONSTANTS.FLAGS.PILE]: { - version: Helpers.getModuleVersion(), - source: sourceActor.uuid, - target: targetActor.uuid, - items: items - } - }) - - } - - static async _updateExistingGiveMessage(message, sourceActor, targetActor, items) { - - const flags = getProperty(message, CONSTANTS.FLAGS.PILE); - - const newItems = this._matchEntries(flags.items, items); - - const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/gave-items.html", { - message: game.i18n.format("ITEM-PILES.Chat.GaveItems", { source: sourceActor.name, target: targetActor.name }), - source: sourceActor, - target: targetActor, - items: newItems - }); - - return message.update({ - content: chatCardHtml, - [`${CONSTANTS.FLAGS.PILE}.items`]: newItems - }); - - } - - static async _updateExistingMerchantMessage(message, sourceActor, targetActor, incomingPriceInformation, interactionId) { - - const flags = getProperty(message, CONSTANTS.FLAGS.PILE); - - const newPriceInformation = flags.priceInformation - .map(priceInformation => { - const boughtItem = incomingPriceInformation.buyerReceive[0]; - const foundEntry = Utilities.findSimilarItem(priceInformation.buyerReceive, boughtItem); - if (foundEntry) { - if (incomingPriceInformation.primary && priceInformation.primary) { - foundEntry.quantity += boughtItem.quantity; - incomingPriceInformation.buyerReceive.splice(0, 1); - priceInformation.totalCurrencyCost += incomingPriceInformation.totalCurrencyCost; - priceInformation.basePriceString = PileUtilities.getPriceArray(priceInformation.totalCurrencyCost, priceInformation.finalPrices) - .filter(price => price.cost).map(price => price.string).join(" "); - } else { - const sameTypePrice = incomingPriceInformation.finalPrices - .map(price => { - const foundItem = Utilities.findSimilarItem(priceInformation.buyerReceive, price); - if (foundItem) { - return { foundItem, price } - } - return false; - }) - .filter(Boolean); - if (sameTypePrice.length) { - incomingPriceInformation.buyerReceive.splice(0, 1); - sameTypePrice.forEach(match => { - match.price.quantity += match.foundItem.quantity; - }); - } - } - } - return priceInformation; - }) - .concat([incomingPriceInformation].filter(priceInformation => priceInformation.buyerReceive.length)); - - const pileData = PileUtilities.getActorFlagData(sourceActor); - - const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/merchant-traded.html", { - message: game.i18n.format("ITEM-PILES.Chat.MerchantTraded", { - name: targetActor.name, - merchant: sourceActor.name - }), - merchant: { - name: sourceActor.name, - img: pileData.merchantImage || sourceActor.img - }, - actor: targetActor, - priceInformation: newPriceInformation - }); - - return message.update({ - content: chatCardHtml, - [`${CONSTANTS.FLAGS.PILE}.interactionId`]: interactionId, - [`${CONSTANTS.FLAGS.PILE}.priceInformation`]: newPriceInformation - }); - - } - - static _createNewChatMessage(userId, chatData) { - - if (!chatData.whisper) { - - const mode = Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT); - - if (mode > 1) { - chatData.whisper = Array.from(game.users) - .filter(user => user.isGM) - .map(user => user.id); - if (mode === 2) { - chatData.whisper.push(userId); - } - chatData.type = CONST.CHAT_MESSAGE_TYPES.WHISPER; - } - - } - - return ChatMessage.create(chatData); - - } + static initialize() { + + Helpers.hooks.on("preCreateChatMessage", this._preCreateChatMessage.bind(this)); + Helpers.hooks.on("renderChatMessage", this._renderChatMessage.bind(this)); + Helpers.hooks.on(CONSTANTS.HOOKS.ITEM.TRANSFER, this._outputTransferItem.bind(this)); + Helpers.hooks.on(CONSTANTS.HOOKS.ATTRIBUTE.TRANSFER, this._outputTransferCurrency.bind(this)); + Helpers.hooks.on(CONSTANTS.HOOKS.TRANSFER_EVERYTHING, this._outputTransferEverything.bind(this)); + Helpers.hooks.on(CONSTANTS.HOOKS.PILE.SPLIT_INVENTORY, this._outputSplitItemPileInventory.bind(this)); + Helpers.hooks.on(CONSTANTS.HOOKS.TRADE.STARTED, this._outputTradeStarted.bind(this)); + Helpers.hooks.on(CONSTANTS.HOOKS.TRADE.COMPLETE, this._outputTradeComplete.bind(this)); + Helpers.hooks.on(CONSTANTS.HOOKS.ITEM.TRADE, this._outputMerchantTradeComplete.bind(this)); + Helpers.hooks.on(CONSTANTS.HOOKS.ITEM.GIVE, this._outputGiveItem.bind(this)); + + $(document).on("click", ".item-piles-chat-card .item-piles-collapsible", async function () { + if ($(this).attr("open")) return; + await Helpers.wait(25); + $(this).parent()[0].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' }); + }); + } + + static _preCreateChatMessage(chatMessage) { + + if (!Helpers.getSetting(SETTINGS.ENABLE_TRADING)) return; + + const content = chatMessage.content.toLowerCase(); + + if (!(content.startsWith("!itempiles") || content.startsWith("!ip"))) return; + + const args = content.split(" ").slice(1); + + if (args[0] === "trade") { + setTimeout(() => { + game.itempiles.API.requestTrade(); + }); + } + + return false; + + } + + static _renderChatMessage(app, html) { + html.find(".item-piles-specate-trade").click(function () { + game.itempiles.API.spectateTrade($(this).data()); + }); + } + + static _disableTradingButton(publicTradeId) { + const message = Array.from(game.messages).find(message => { + return getProperty(message, CONSTANTS.FLAGS.PUBLIC_TRADE_ID) === publicTradeId; + }); + if (!message) return; + const update = this._replaceChatContent(message); + return message.update(update) + } + + static async disablePastTradingButtons() { + if (!game.user.isGM) return; + + const messages = Array.from(game.messages).filter(message => { + return getProperty(message, CONSTANTS.FLAGS.PUBLIC_TRADE_ID); + }); + + if (!messages.length) return; + const updates = []; + for (let message of messages) { + const update = this._replaceChatContent(message); + const tradeId = getProperty(message, CONSTANTS.FLAGS.PUBLIC_TRADE_ID); + const tradeUsers = getProperty(message, CONSTANTS.FLAGS.TRADE_USERS); + const bothUsersActive = tradeUsers.filter(userId => game.users.get(userId).active).length === tradeUsers.length; + if (!bothUsersActive) { + updates.push(update); + } else { + const otherUsers = tradeUsers.filter(userId => userId !== game.user.id); + const tradeData = await TradeAPI._requestTradeData({ tradeId, tradeUser: otherUsers[0] }); + if (!tradeData) { + updates.push(update); + } + } + } + + if (!updates.length) return; + + return ChatMessage.updateDocuments(updates); + + } + + static _replaceChatContent(message) { + const tradeId = getProperty(message, CONSTANTS.FLAGS.PUBLIC_TRADE_ID); + const stringToFind = `data-trade-id="${tradeId}"`; + let content = message.content; + content = content.replace(stringToFind, ""); + content = content.replace(stringToFind, "disabled"); + content = content.replace(game.i18n.localize("ITEM-PILES.Chat.TradeSpectate"), game.i18n.localize("ITEM-PILES.Chat.SpectateDisabled")); + return { + _id: message.id, + content, + [`flags.-=${CONSTANTS.MODULE_NAME}`]: null + }; + } + + + /** + * Outputs to chat based on transferring an item from or to an item pile + * + * @param source + * @param target + * @param items + * @param userId + * @param interactionId + * @returns {Promise} + */ + static async _outputTransferItem(source, target, items, userId, interactionId) { + if (!PileUtilities.isItemPileLootable(source)) return; + if (!interactionId || game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; + const [itemData, itemCurrencies] = await this._formatItemData(source, items); + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.PICKUP_CHAT_MESSAGE, source.uuid, target.uuid, itemData, itemCurrencies, userId, interactionId); + } + + /** + * Outputs to chat based on transferring a currency from or to an item pile + * + * @param source + * @param target + * @param currencies + * @param userId + * @param interactionId + * @returns {Promise} + */ + static async _outputTransferCurrency(source, target, currencies, userId, interactionId) { + if (!PileUtilities.isItemPileLootable(source)) return; + if (!interactionId || game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; + const currencyData = this._formatCurrencyData(source, currencies); + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.PICKUP_CHAT_MESSAGE, source.uuid, target.uuid, [], currencyData, userId, interactionId); + } + + /** + * Outputs to chat based on giving an item from one actor to another + * + * @param source + * @param target + * @param item + * @param userId + * @returns {Promise} + */ + static async _outputGiveItem(source, target, item, userId) { + if (game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; + const [itemData, itemCurrencies] = await this._formatItemData(source, [item]); + return this._giveChatMessage(source, target, itemData.concat(itemCurrencies), userId); + } + + /** + * Outputs to chat based on transferring everything from or to an item pile + * + * @param source + * @param target + * @param items + * @param currencies + * @param userId + * @param interactionId + * @returns {Promise} + */ + static async _outputTransferEverything(source, target, items, currencies, userId, interactionId) { + if (!PileUtilities.isItemPileLootable(source)) return; + if (!interactionId || game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; + const [itemData, itemCurrencies] = await this._formatItemData(source, items); + const currencyData = this._formatCurrencyData(source, currencies).concat(itemCurrencies); + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.PICKUP_CHAT_MESSAGE, source.uuid, target.uuid, itemData, currencyData, userId, interactionId); + } + + static _outputSplitItemPileInventory(source, pileDeltas, actorDeltas, userId) { + if (!PileUtilities.isItemPileLootable(source)) return; + if (game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.SPLIT_CHAT_MESSAGE, source.uuid, pileDeltas, actorDeltas, userId); + } + + static async _outputTradeStarted(party_1, party_2, publicTradeId, isPrivate) { + if (party_1.user !== game.user.id || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT) || isPrivate) return; + return this._outputTradeStartedToChat(party_1, party_2, publicTradeId); + } + + static async _outputTradeComplete(instigator, party_1, party_2, publicTradeId, isPrivate) { + if (!Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; + return this._outputTradeCompleteToChat(instigator, party_1, party_2, publicTradeId, isPrivate); + } + + static async _outputMerchantTradeComplete(source, target, priceInformation, userId, interactionId) { + if (!Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; + if (!PileUtilities.isItemPileMerchant(source)) return; + if (!interactionId || game.user.id !== userId || !Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT)) return; + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.MERCHANT_TRADE_CHAT_MESSAGE, source.uuid, target.uuid, priceInformation, userId, interactionId); + } + + /** + * Formats item data to a chat friendly structure + * + * @param itemPile + * @param items + * @param divideBy + * @returns {Promise} + */ + static async _formatItemData(itemPile, items, divideBy = 1) { + const formattedItems = []; + const formattedCurrencies = []; + const currencyList = PileUtilities.getActorCurrencies(itemPile, { getAll: true }); + for (const itemData of items) { + const tempItem = await Item.implementation.create(itemData.item, { temporary: true }); + const data = { + name: game.i18n.localize(tempItem.name), + img: tempItem.img ?? itemData?.item?.img ?? "", + quantity: Math.abs(itemData.quantity) / divideBy + }; + if (PileUtilities.isItemCurrency(tempItem, { actorCurrencies: currencyList })) { + formattedCurrencies.push(data) + } else { + formattedItems.push(data); + } + } + return [formattedItems, formattedCurrencies]; + } + + /** + * Formats currency data to a chat friendly structure + * + * @param itemPile + * @param currencies + * @param divideBy + * @returns {Array} + */ + static _formatCurrencyData(itemPile, currencies, divideBy = 1) { + const currencyList = PileUtilities.getActorCurrencies(itemPile, { getAll: true }); + return Object.entries(currencies).map(entry => { + const currency = currencyList.find(currency => currency.id === entry[0]); + return { + name: game.i18n.localize(currency.name), + img: currency.img ?? "", + quantity: Math.abs(entry[1]) / divideBy, + index: currencyList.indexOf(currency) + } + }); + } + + /** + * Outputs the transferred data in chat + * + * @param sourceUuid + * @param targetUuid + * @param items + * @param currencies + * @param userId + * @param interactionId + * @returns {Promise} + */ + static async _outputPickupToChat(sourceUuid, targetUuid, items, currencies, userId, interactionId) { + + const sourceActor = Utilities.getActor(sourceUuid); + const targetActor = Utilities.getActor(targetUuid); + + const now = (+new Date()); + + // Get all messages younger than 3 hours, and grab the last 10, then reverse them (latest to oldest) + const messages = Array.from(game.messages).filter(message => (now - message.timestamp) <= (10800000)).slice(-10); + messages.reverse() + + for (const message of messages) { + const flags = getProperty(message, CONSTANTS.FLAGS.PILE); + if (flags && flags.version && !foundry.utils.isNewerVersion(Helpers.getModuleVersion(), flags.version) && flags.source === sourceUuid && flags.target === targetUuid && flags.interactionId === interactionId) { + return this._updateExistingPickupMessage(message, sourceActor, targetActor, items, currencies, interactionId) + } + } + + const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/looted.html", { + message: game.i18n.format("ITEM-PILES.Chat.Pickup", { name: targetActor.name }), + itemPile: sourceActor, + actor: targetActor, + items: items, + currencies: currencies + }); + + return this._createNewChatMessage(userId, { + user: game.user.id, + type: CONST.CHAT_MESSAGE_TYPES.OTHER, + content: chatCardHtml, + flavor: "Item Piles", + speaker: ChatMessage.getSpeaker({ alias: game.user.name }), + [CONSTANTS.FLAGS.PILE]: { + version: Helpers.getModuleVersion(), + source: sourceUuid, + target: targetUuid, + items: items, + currencies: currencies, + interactionId: interactionId + } + }) + + } + + static _matchEntries(existingEntries, incomingEntries) { + + const combinedEntries = existingEntries.map(existingEntry => { + const foundEntry = incomingEntries.find(item => item.name === existingEntry.name && existingEntry.img === item.img); + if (foundEntry) { + existingEntry.quantity += foundEntry.quantity; + incomingEntries.splice(incomingEntries.indexOf(foundEntry), 1) + } + return existingEntry; + }); + + incomingEntries.forEach(item => combinedEntries.push(item)); + + return combinedEntries; + + } + + static async _updateExistingPickupMessage(message, sourceActor, targetActor, items, currencies, interactionId) { + + const flags = getProperty(message, CONSTANTS.FLAGS.PILE); + + const newItems = this._matchEntries(flags.items, items); + const newCurrencies = this._matchEntries(flags.currencies, currencies); + + newCurrencies.sort((a, b) => { + return a.index - b.index; + }) + + const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/looted.html", { + message: game.i18n.format("ITEM-PILES.Chat.Pickup", { name: targetActor.name }), + itemPile: sourceActor, + actor: targetActor, + items: newItems, + currencies: newCurrencies + }); + + return message.update({ + content: chatCardHtml, + [`${CONSTANTS.FLAGS.PILE}.interactionId`]: interactionId, + [`${CONSTANTS.FLAGS.PILE}.items`]: newItems, + [`${CONSTANTS.FLAGS.PILE}.currencies`]: newCurrencies, + }); + + } + + static async _outputSplitToChat(sourceUuid, pileDeltas, actorDeltas, userId) { + + const sourceActor = Utilities.getActor(sourceUuid); + + const divideBy = Object.values(actorDeltas).length; + + const [items, itemCurrencies] = await this._formatItemData(sourceActor, pileDeltas.itemDeltas, divideBy); + const currencies = this._formatCurrencyData(sourceActor, pileDeltas.attributeDeltas, divideBy).concat(itemCurrencies); + + const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/looted.html", { + message: game.i18n.format("ITEM-PILES.Chat.Split", { num_players: divideBy }), + itemPile: sourceActor, + items: items, + currencies: currencies + }); + + return this._createNewChatMessage(userId, { + user: game.user.id, + type: CONST.CHAT_MESSAGE_TYPES.OTHER, + content: chatCardHtml, + flavor: "Item Piles", + speaker: ChatMessage.getSpeaker({ alias: game.user.name }) + }); + + } + + static async _outputTradeStartedToChat(party_1, party_2, publicTradeId) { + + const party_1_actor = Utilities.getActor(party_1.actor); + const party_2_actor = Utilities.getActor(party_2.actor); + + const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/trade-started.html", { + party_1_actor, + party_2_actor, + publicTradeId, + userId: game.user.id + }); + + return this._createNewChatMessage(game.user.id, { + user: game.user.id, + type: CONST.CHAT_MESSAGE_TYPES.OTHER, + content: chatCardHtml, + flavor: "Item Piles", + speaker: ChatMessage.getSpeaker({ alias: game.user.name }), + [CONSTANTS.FLAGS.PUBLIC_TRADE_ID]: publicTradeId, + [CONSTANTS.FLAGS.TRADE_USERS]: [party_1.user, party_2.user] + }); + } + + static async _outputTradeCompleteToChat(instigator, party_1, party_2, publicTradeId, isPrivate) { + + if (instigator !== game.user.id) return; + + const party_1_actor = Utilities.getActor(party_1.actor); + const party_1_data = { + actor: party_1_actor, + items: party_2.items, + currencies: party_2.currencies.concat(party_2.itemCurrencies) + } + party_1_data.got_nothing = !party_1_data.items.length && !party_1_data.currencies.length; + + const party_2_actor = Utilities.getActor(party_2.actor); + const party_2_data = { + actor: party_2_actor, + items: party_1.items, + currencies: party_1.currencies.concat(party_1.itemCurrencies) + } + party_2_data.got_nothing = !party_2_data.items.length && !party_2_data.currencies.length; + + if (party_1.got_nothing && party_2.got_nothing) return; + + const enableCollapse = (party_1_data.items.length + party_1_data.currencies.length + party_2_data.items.length + party_2_data.currencies.length) > 6; + + const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/trade-complete.html", { + party_1: party_1_data, + party_2: party_2_data, + publicTradeId, + isPrivate, + enableCollapse + }); + + return this._createNewChatMessage(game.user.id, { + user: game.user.id, + type: isPrivate ? CONST.CHAT_MESSAGE_TYPES.WHISPER : CONST.CHAT_MESSAGE_TYPES.OTHER, + content: chatCardHtml, + flavor: "Item Piles" + (isPrivate ? ": " + game.i18n.localize("ITEM-PILES.Chat.PrivateTrade") : ""), + speaker: ChatMessage.getSpeaker({ alias: game.user.name }), + whisper: isPrivate ? [party_2.user] : [] + }); + + } + + static async _outputMerchantTradeToChat(sourceUuid, targetUuid, priceInformation, userId, interactionId) { + + const sourceActor = Utilities.getActor(sourceUuid); + const targetActor = Utilities.getActor(targetUuid); + + const now = (+new Date()); + + priceInformation.id = randomID(); + + // Get all messages younger than 3 hours, and grab the last 10, then reverse them (latest to oldest) + const messages = Array.from(game.messages).filter(message => (now - message.timestamp) <= (10800000)).slice(-10); + messages.reverse(); + + for (const message of messages) { + const flags = getProperty(message, CONSTANTS.FLAGS.PILE); + if (flags && flags.version && !foundry.utils.isNewerVersion(Helpers.getModuleVersion(), flags.version) && flags.source === sourceUuid && flags.target === targetUuid && flags.interactionId === interactionId) { + return this._updateExistingMerchantMessage(message, sourceActor, targetActor, priceInformation, interactionId) + } + } + + const pileData = PileUtilities.getActorFlagData(sourceActor); + + const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/merchant-traded.html", { + message: game.i18n.format("ITEM-PILES.Chat.MerchantTraded", { + name: targetActor.name, + merchant: sourceActor.name + }), + merchant: { + name: sourceActor.name, + img: pileData.merchantImage || sourceActor.img + }, + actor: targetActor, + priceInformation: [priceInformation] + }); + + return this._createNewChatMessage(userId, { + user: game.user.id, + type: CONST.CHAT_MESSAGE_TYPES.OTHER, + content: chatCardHtml, + flavor: "Item Piles", + speaker: ChatMessage.getSpeaker({ alias: game.user.name }), + [CONSTANTS.FLAGS.PILE]: { + version: Helpers.getModuleVersion(), + source: sourceUuid, + target: targetUuid, + priceInformation: [priceInformation], + interactionId: interactionId + } + }); + + } + + static async _giveChatMessage(source, target, items) { + + const now = (+new Date()); + + const sourceActor = Utilities.getActor(source); + const targetActor = Utilities.getActor(target); + + // Get all messages younger than 1 minute, and grab the last 5, then reverse them (latest to oldest) + const messages = Array.from(game.messages) + .filter(message => (now - message.timestamp) <= (60000)) + .slice(-5) + .reverse(); + + for (const message of messages) { + const flags = getProperty(message, CONSTANTS.FLAGS.PILE); + if (flags && flags.version && !foundry.utils.isNewerVersion(Helpers.getModuleVersion(), flags.version) && flags.source === sourceActor.uuid && flags.target === targetActor.uuid && message.isAuthor) { + return this._updateExistingGiveMessage(message, sourceActor, targetActor, items) + } + } + + const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/gave-items.html", { + message: game.i18n.format("ITEM-PILES.Chat.GaveItems", { source: sourceActor.name, target: targetActor.name }), + source: sourceActor, + target: targetActor, + items: items + }); + + return this._createNewChatMessage(game.user.id, { + user: game.user.id, + type: CONST.CHAT_MESSAGE_TYPES.OTHER, + content: chatCardHtml, + flavor: "Item Piles", + speaker: ChatMessage.getSpeaker({ alias: game.user.name }), + [CONSTANTS.FLAGS.PILE]: { + version: Helpers.getModuleVersion(), + source: sourceActor.uuid, + target: targetActor.uuid, + items: items + } + }) + + } + + static async _updateExistingGiveMessage(message, sourceActor, targetActor, items) { + + const flags = getProperty(message, CONSTANTS.FLAGS.PILE); + + const newItems = this._matchEntries(flags.items, items); + + const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/gave-items.html", { + message: game.i18n.format("ITEM-PILES.Chat.GaveItems", { source: sourceActor.name, target: targetActor.name }), + source: sourceActor, + target: targetActor, + items: newItems + }); + + return message.update({ + content: chatCardHtml, + [`${CONSTANTS.FLAGS.PILE}.items`]: newItems + }); + + } + + static async _updateExistingMerchantMessage(message, sourceActor, targetActor, incomingPriceInformation, interactionId) { + + const flags = getProperty(message, CONSTANTS.FLAGS.PILE); + + const newPriceInformation = flags.priceInformation + .map(priceInformation => { + const boughtItem = incomingPriceInformation.buyerReceive[0]; + const foundEntry = Utilities.findSimilarItem(priceInformation.buyerReceive, boughtItem); + if (foundEntry) { + if (incomingPriceInformation.primary && priceInformation.primary) { + foundEntry.quantity += boughtItem.quantity; + incomingPriceInformation.buyerReceive.splice(0, 1); + priceInformation.totalCurrencyCost += incomingPriceInformation.totalCurrencyCost; + priceInformation.basePriceString = PileUtilities.getPriceArray(priceInformation.totalCurrencyCost, priceInformation.finalPrices) + .filter(price => price.cost).map(price => price.string).join(" "); + } else { + const sameTypePrice = incomingPriceInformation.finalPrices + .map(price => { + const foundItem = Utilities.findSimilarItem(priceInformation.buyerReceive, price); + if (foundItem) { + return { foundItem, price } + } + return false; + }) + .filter(Boolean); + if (sameTypePrice.length) { + incomingPriceInformation.buyerReceive.splice(0, 1); + sameTypePrice.forEach(match => { + match.price.quantity += match.foundItem.quantity; + }); + } + } + } + return priceInformation; + }) + .concat([incomingPriceInformation].filter(priceInformation => priceInformation.buyerReceive.length)); + + const pileData = PileUtilities.getActorFlagData(sourceActor); + + const chatCardHtml = await renderTemplate(CONSTANTS.PATH + "templates/chat/merchant-traded.html", { + message: game.i18n.format("ITEM-PILES.Chat.MerchantTraded", { + name: targetActor.name, + merchant: sourceActor.name + }), + merchant: { + name: sourceActor.name, + img: pileData.merchantImage || sourceActor.img + }, + actor: targetActor, + priceInformation: newPriceInformation + }); + + return message.update({ + content: chatCardHtml, + [`${CONSTANTS.FLAGS.PILE}.interactionId`]: interactionId, + [`${CONSTANTS.FLAGS.PILE}.priceInformation`]: newPriceInformation + }); + + } + + static _createNewChatMessage(userId, chatData) { + + if (!chatData.whisper) { + + const mode = Helpers.getSetting(SETTINGS.OUTPUT_TO_CHAT); + + if (mode > 1) { + chatData.whisper = Array.from(game.users) + .filter(user => user.isGM) + .map(user => user.id); + if (mode === 2) { + chatData.whisper.push(userId); + } + chatData.type = CONST.CHAT_MESSAGE_TYPES.WHISPER; + } + + } + + return ChatMessage.create(chatData); + + } } diff --git a/src/API/private-api.js b/src/API/private-api.js index a3c456bc..87c45c94 100644 --- a/src/API/private-api.js +++ b/src/API/private-api.js @@ -416,10 +416,11 @@ export default class PrivateAPI { const transaction = new Transaction(targetActor); const priceData = PileUtilities.getPriceFromString(currencies) + const secondaryPrices = priceData.currencies.filter(currency => currency.secondary && currency.quantity); const overallCost = priceData.overallCost; const paymentData = PileUtilities.getPaymentData({ - purchaseData: [{ cost: overallCost, quantity: 1 }], buyer: targetActor + purchaseData: [{ cost: overallCost, quantity: 1, secondaryPrices }], buyer: targetActor }); const itemsToRemove = paymentData.finalPrices.filter(currency => currency.type === "item") diff --git a/src/API/trade-api.js b/src/API/trade-api.js index 7230c08b..3717f0ea 100644 --- a/src/API/trade-api.js +++ b/src/API/trade-api.js @@ -15,478 +15,478 @@ const ongoingTrades = new Map(); export default class TradeAPI { - static initialize() { - Hooks.on("renderPlayerList", this._userDisconnected.bind(this)); - } - - static async _requestTrade(user = false) { - - // Grab all the active users (not self) - const users = game.users.filter(user => user.active && user !== game.user); - - // No users! - if (!users.length) { - return TJSDialog.prompt({ - title: game.i18n.localize("ITEM-PILES.Trade.Title"), content: { - class: CustomDialog, props: { - header: game.i18n.localize("ITEM-PILES.Trade.NoActiveUsers.Title"), - content: game.i18n.localize("ITEM-PILES.Trade.NoActiveUsers.Content"), - icon: "fas fa-heart-broken" - } - }, modal: true, draggable: false, options: { - height: "auto" - } - }); - } - - let userId; - let actor; - let isPrivate; - - // Find actors you own - const actors = game.actors.filter(actor => actor.isOwner); - - // If you only own one actor, and the user was already preselected (through the right click menu in the actors list) - if (actors.length === 1 && user) { - userId = user.id; - actor = actors[0]; - isPrivate = false; - } else { - // If you have more than 1 owned actor, prompt to choose which one - const result = await TradePromptDialog.show({ actors, users, user }); - if (!result) return; - userId = result.user.id; - actor = result.actor; - isPrivate = result.private; - } - - if (!actor) return false; - - actor = Utilities.getActor(actor); - - const actorOwner = game.users.find(user => user.character === actor && user !== game.user); - if (actorOwner) { - - const doContinue = TJSDialog.confirm({ - title: game.i18n.localize("ITEM-PILES.Trade.Title"), content: { - class: CustomDialog, props: { - header: game.i18n.localize("ITEM-PILES.Trade.Title"), - content: actorOwner.active ? game.i18n.format("ITEM-PILES.Trade.UserActiveCharacterWarning", { - actor_name: actor.name, player_name: actorOwner.name - }) : game.i18n.format("ITEM-PILES.Trade.UserCharacterWarning", { - actor_name: actor.name, player_name: actorOwner.name - }), - icon: "fas fa-exclamation-triangle", - } - }, modal: true, draggable: false, rejectClose: false, defaultYes: true, options: { - height: "auto" - } - }); - if (!doContinue) { - return; - } - } - - const privateTradeId = randomID(); - const publicTradeId = randomID(); - - // Spawn a cancel dialog - const cancelDialog = new Dialog({ - title: game.i18n.localize("ITEM-PILES.Trade.Title"), - content: `

${game.i18n.format("ITEM-PILES.Trade.OngoingRequest.Content", { user_name: game.users.get(userId).name })}

`, - buttons: { - confirm: { - icon: '', - label: game.i18n.localize("ITEM-PILES.Trade.OngoingRequest.Label"), - callback: () => { - ItemPileSocket.executeAsUser(ItemPileSocket.HANDLERS.TRADE_REQUEST_CANCELLED, userId, game.user.id, privateTradeId); - } - } - } - }, { - top: 50, width: 300 - }).render(true); - - // Send out the request - return ItemPileSocket.executeAsUser(ItemPileSocket.HANDLERS.TRADE_REQUEST_PROMPT, userId, game.user.id, actor.uuid, privateTradeId, publicTradeId, isPrivate) - .then(async (data) => { - - if (data === "cancelled") return; - cancelDialog.close(); - - if (data === "same-actor") { - return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.SameActor"), true); - } - - // If they declined, show warning - if (!data || !data.fullPrivateTradeId.includes(privateTradeId)) { - return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.Declined"), true); - } - - const traderActor = Utilities.getActor(data.actorUuid); - - if (traderActor === actor) { - return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.SameActor"), true); - } - - const store = new TradeStore(game.user.id, { - user: game.user, actor - }, { - user: game.users.get(userId), actor: traderActor - }, data.fullPublicTradeId, data.fullPrivateTradeId, isPrivate); - - const [actorSheet, tradeApp] = Helpers.getApplicationPositions(actor.sheet); - - const app = new TradingApp(store, tradeApp).render(true); - - ongoingTrades.set(data.fullPublicTradeId, { app, store }); - - actorSheet.byassItemPiles = true; - actor.sheet.render(true, actorSheet); - - if (isPrivate) { - return ItemPileSocket.callHookForUsers(CONSTANTS.HOOKS.TRADE.STARTED, [game.user.id, userId], { - user: game.user.id, actor: actor.uuid - }, { user: userId, actor: data.actorUuid }, data.fullPublicTradeId, isPrivate); - } - - return ItemPileSocket.callHook(CONSTANTS.HOOKS.TRADE.STARTED, { - user: game.user.id, actor: actor.uuid - }, { user: userId, actor: data.actorUuid }, data.fullPublicTradeId, isPrivate); - - }).catch((err) => { - console.error(err); - // If the counterparty disconnected, show that and close dialog - Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.Disconnected"), true); - cancelDialog.close() - }); - - } - - static async _respondPrompt(tradingUserId, tradingActorUuid, privateTradeId, publicTradeId, isPrivate) { - - // If the user was previously muted, wait for a random amount of time and respond with false - if (mutedUsers.has(tradingUserId)) { - await Helpers.wait(Math.random() * 15000); - return false; - } - - // Complete the private and public trade IDs - const fullPrivateTradeId = privateTradeId + randomID(); - const fullPublicTradeId = publicTradeId + randomID(); - - const tradingUser = game.users.get(tradingUserId); - const tradingActor = Utilities.getActor(tradingActorUuid); - - // Make em pick an actor (if more than one) and accept/decline/mute - const result = await TradeRequestDialog.show({ tradeId: privateTradeId, tradingUser, tradingActor, isPrivate }); - - if (!result) return false; - - if (result === "cancelled") { - return "cancelled"; - } + static initialize() { + Hooks.on("renderPlayerList", this._userDisconnected.bind(this)); + } + + static async _requestTrade(user = false) { + + // Grab all the active users (not self) + const users = game.users.filter(user => user.active && user !== game.user); + + // No users! + if (!users.length) { + return TJSDialog.prompt({ + title: game.i18n.localize("ITEM-PILES.Trade.Title"), content: { + class: CustomDialog, props: { + header: game.i18n.localize("ITEM-PILES.Trade.NoActiveUsers.Title"), + content: game.i18n.localize("ITEM-PILES.Trade.NoActiveUsers.Content"), + icon: "fas fa-heart-broken" + } + }, modal: true, draggable: false, options: { + height: "auto" + } + }); + } + + let userId; + let actor; + let isPrivate; + + // Find actors you own + const actors = game.actors.filter(actor => actor.isOwner); + + // If you only own one actor, and the user was already preselected (through the right click menu in the actors list) + if (actors.length === 1 && user) { + userId = user.id; + actor = actors[0]; + isPrivate = false; + } else { + // If you have more than 1 owned actor, prompt to choose which one + const result = await TradePromptDialog.show({ actors, users, user }); + if (!result) return; + userId = result.user.id; + actor = result.actor; + isPrivate = result.private; + } + + if (!actor) return false; + + actor = Utilities.getActor(actor); + + const actorOwner = game.users.find(user => user.character === actor && user !== game.user); + if (actorOwner) { + + const doContinue = TJSDialog.confirm({ + title: game.i18n.localize("ITEM-PILES.Trade.Title"), content: { + class: CustomDialog, props: { + header: game.i18n.localize("ITEM-PILES.Trade.Title"), + content: actorOwner.active ? game.i18n.format("ITEM-PILES.Trade.UserActiveCharacterWarning", { + actor_name: actor.name, player_name: actorOwner.name + }) : game.i18n.format("ITEM-PILES.Trade.UserCharacterWarning", { + actor_name: actor.name, player_name: actorOwner.name + }), + icon: "fas fa-exclamation-triangle", + } + }, modal: true, draggable: false, rejectClose: false, defaultYes: true, options: { + height: "auto" + } + }); + if (!doContinue) { + return; + } + } + + const privateTradeId = randomID(); + const publicTradeId = randomID(); + + // Spawn a cancel dialog + const cancelDialog = new Dialog({ + title: game.i18n.localize("ITEM-PILES.Trade.Title"), + content: `

${game.i18n.format("ITEM-PILES.Trade.OngoingRequest.Content", { user_name: game.users.get(userId).name })}

`, + buttons: { + confirm: { + icon: '', + label: game.i18n.localize("ITEM-PILES.Trade.OngoingRequest.Label"), + callback: () => { + ItemPileSocket.executeAsUser(ItemPileSocket.HANDLERS.TRADE_REQUEST_CANCELLED, userId, game.user.id, privateTradeId); + } + } + } + }, { + top: 50, width: 300 + }).render(true); + + // Send out the request + return ItemPileSocket.executeAsUser(ItemPileSocket.HANDLERS.TRADE_REQUEST_PROMPT, userId, game.user.id, actor.uuid, privateTradeId, publicTradeId, isPrivate) + .then(async (data) => { + + if (data === "cancelled") return; + cancelDialog.close(); + + if (data === "same-actor") { + return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.SameActor"), true); + } + + // If they declined, show warning + if (!data || !data.fullPrivateTradeId.includes(privateTradeId)) { + return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.Declined"), true); + } + + const traderActor = Utilities.getActor(data.actorUuid); + + if (traderActor === actor) { + return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.SameActor"), true); + } + + const store = new TradeStore(game.user.id, { + user: game.user, actor + }, { + user: game.users.get(userId), actor: traderActor + }, data.fullPublicTradeId, data.fullPrivateTradeId, isPrivate); + + const [actorSheet, tradeApp] = Helpers.getApplicationPositions(actor.sheet); + + const app = new TradingApp(store, tradeApp).render(true); + + ongoingTrades.set(data.fullPublicTradeId, { app, store }); + + actorSheet.byassItemPiles = true; + actor.sheet.render(true, actorSheet); + + if (isPrivate) { + return ItemPileSocket.callHookForUsers(CONSTANTS.HOOKS.TRADE.STARTED, [game.user.id, userId], { + user: game.user.id, actor: actor.uuid + }, { user: userId, actor: data.actorUuid }, data.fullPublicTradeId, isPrivate); + } + + return ItemPileSocket.callHook(CONSTANTS.HOOKS.TRADE.STARTED, { + user: game.user.id, actor: actor.uuid + }, { user: userId, actor: data.actorUuid }, data.fullPublicTradeId, isPrivate); + + }).catch((err) => { + console.error(err); + // If the counterparty disconnected, show that and close dialog + Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.Disconnected"), true); + cancelDialog.close() + }); + + } + + static async _respondPrompt(tradingUserId, tradingActorUuid, privateTradeId, publicTradeId, isPrivate) { + + // If the user was previously muted, wait for a random amount of time and respond with false + if (mutedUsers.has(tradingUserId)) { + await Helpers.wait(Math.random() * 15000); + return false; + } + + // Complete the private and public trade IDs + const fullPrivateTradeId = privateTradeId + randomID(); + const fullPublicTradeId = publicTradeId + randomID(); + + const tradingUser = game.users.get(tradingUserId); + const tradingActor = Utilities.getActor(tradingActorUuid); + + // Make em pick an actor (if more than one) and accept/decline/mute + const result = await TradeRequestDialog.show({ tradeId: privateTradeId, tradingUser, tradingActor, isPrivate }); + + if (!result) return false; + + if (result === "cancelled") { + return "cancelled"; + } - // If muted, add user to blacklist locally - if (result === "mute") { - mutedUsers.push(tradingUserId); - return false; - } + // If muted, add user to blacklist locally + if (result === "mute") { + mutedUsers.push(tradingUserId); + return false; + } - const actor = result.actor ?? result; + const actor = result.actor ?? result; - if (actor === tradingActor) { - Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.SameActor"), true); - return "same-actor"; - } + if (actor === tradingActor) { + Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.SameActor"), true); + return "same-actor"; + } - const store = new TradeStore(tradingUserId, { - user: game.user, actor - }, { - user: tradingUser, actor: tradingActor - }, fullPublicTradeId, fullPrivateTradeId, isPrivate); + const store = new TradeStore(tradingUserId, { + user: game.user, actor + }, { + user: tradingUser, actor: tradingActor + }, fullPublicTradeId, fullPrivateTradeId, isPrivate); - const [actorSheet, tradeApp] = Helpers.getApplicationPositions(actor.sheet); + const [actorSheet, tradeApp] = Helpers.getApplicationPositions(actor.sheet); - const app = new TradingApp(store, tradeApp).render(true); + const app = new TradingApp(store, tradeApp).render(true); - ongoingTrades.set(fullPublicTradeId, { app, store }); + ongoingTrades.set(fullPublicTradeId, { app, store }); - actorSheet.byassItemPiles = true; - actor.sheet.render(true, actorSheet); + actorSheet.byassItemPiles = true; + actor.sheet.render(true, actorSheet); - return { - fullPrivateTradeId, fullPublicTradeId, actorUuid: result.uuid - }; + return { + fullPrivateTradeId, fullPublicTradeId, actorUuid: result.uuid + }; - } + } - static getAppOptions(actor) { - const midPoint = (window.innerWidth / 2) - 200; - return { - actorSheet: { left: midPoint - actor.sheet.position.width - 25 }, tradeApp: { left: midPoint + 25 } - } - } + static getAppOptions(actor) { + const midPoint = (window.innerWidth / 2) - 200; + return { + actorSheet: { left: midPoint - actor.sheet.position.width - 25 }, tradeApp: { left: midPoint + 25 } + } + } - static async _tradeCancelled(userId, privateTradeId) { - - TJSDialog.prompt({ - title: game.i18n.localize("ITEM-PILES.Trade.Title"), content: { - class: CustomDialog, props: { - header: game.i18n.localize("ITEM-PILES.Trade.Title"), - content: game.i18n.format("ITEM-PILES.Trade.CancelledRequest.Content", { user_name: game.users.get(userId).name }), - icon: "fas fa-exclamation-triangle" - } - }, modal: true, draggable: false, options: { - height: "auto" - } - }); - - return TradeRequestDialog.cancel(privateTradeId); - - } - - static async _requestTradeData({ tradeId, tradeUser } = {}) { - - const ongoingTrade = this._getOngoingTrade(tradeId); - if (ongoingTrade) { - return ongoingTrade.store.export(); - } - - const user = game.users.get(tradeUser); - if (!user?.active) { - return false; - } - - const ongoingTradeData = await ItemPileSocket.executeAsUser(ItemPileSocket.HANDLERS.REQUEST_TRADE_DATA, tradeUser, tradeId, game.user.id); - if (!ongoingTradeData) { - return false; - } - - return ongoingTradeData; - - } - - static async _spectateTrade({ tradeId, tradeUser } = {}) { - - const existingApp = TradingApp.getActiveApp(tradeId); - if (existingApp) { - return existingApp.render(false, { focus: true }); - } - - const ongoingTradeData = await this._requestTradeData({ tradeId, tradeUser }); - if (!ongoingTradeData) { - if (Helpers.isGMConnected()) { - ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.DISABLE_CHAT_TRADE_BUTTON, tradeId); - } - return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.Over"), true); - } - - const store = TradeStore.import(ongoingTradeData); - - const app = new TradingApp(store).render(true); - - ongoingTrades.set(store.publicTradeId, { app, store }); - - } - - static async _respondActiveTradeData(tradeId, requesterId) { - const trade = this._getOngoingTrade(tradeId, requesterId); - if (!trade) return; - return trade.store.export(); - } - - static _getOngoingTrade(tradeId, requesterId = game.user.id) { - if (!ongoingTrades.has(tradeId)) return false; - const trade = ongoingTrades.get(tradeId); - if (!trade.store.isPrivate) { - return trade; - } - if (trade.store.leftTraderUser.id !== requesterId && trade.store.rightTraderUser.id !== requesterId) return false; - return trade; - } - - static async _updateItems(tradeId, userId, items) { - const trade = this._getOngoingTrade(tradeId); - if (!trade) return; - trade.store.updateItems(userId, items); - } - - static async _updateItemCurrencies(tradeId, userId, items) { - const trade = this._getOngoingTrade(tradeId); - if (!trade) return; - trade.store.updateItemCurrencies(userId, items); - } - - static async _updateCurrencies(tradeId, userId, currencies) { - const trade = this._getOngoingTrade(tradeId); - if (!trade) return; - trade.store.updateCurrencies(userId, currencies); - } - - static async _updateAcceptedState(tradeId, userId, status) { - const trade = this._getOngoingTrade(tradeId); - if (!trade) return; - trade.store.updateAcceptedState(userId, status); - if (userId === game.user.id && (trade.store.leftTraderUser.id === game.user.id || trade.store.rightTraderUser.id === game.user.id)) { - if (trade.store.tradeIsAccepted) { - setTimeout(async () => { - if (trade.store.tradeIsAccepted) { - ItemPileSocket.executeForUsers( - ItemPileSocket.HANDLERS.EXECUTE_TRADE, - [trade.store.leftTraderUser.id, trade.store.rightTraderUser.id], - trade.store.publicTradeId, - trade.store.privateTradeId, - userId); - } - }, 2000); - } - } - } - - static async _userDisconnected() { - const tradesToDelete = []; - const activeUsers = game.users.filter(user => user.active); - for (let [tradeId, trade] of ongoingTrades) { - const foundLeft = activeUsers.find(u => u === trade.store.leftTraderUser); - const foundRight = activeUsers.find(u => u === trade.store.rightTraderUser); - if (foundLeft && foundRight) continue; - tradesToDelete.push(tradeId); - Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.Disconnected"), true); - await trade.app.close({ callback: true }); - if (foundLeft === game.user || foundRight === game.user) { - if (Helpers.isGMConnected()) { - await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.DISABLE_CHAT_TRADE_BUTTON, tradeId); - } - } - } - tradesToDelete.forEach(tradeId => ongoingTrades.delete(tradeId)); - } - - static async _tradeClosed(tradeId, closeUserId) { - const trade = this._getOngoingTrade(tradeId); - if (!trade) return; - - if (trade.store.leftTraderUser.id === game.user.id || trade.store.rightTraderUser.id === game.user.id) { - - if (closeUserId === trade.store.rightTraderUser.id) { - - TJSDialog.prompt({ - title: game.i18n.localize("ITEM-PILES.Trade.Closed.Title"), content: { - class: CustomDialog, props: { - header: game.i18n.localize("ITEM-PILES.Trade.Closed.Title"), - content: game.i18n.format("ITEM-PILES.Trade.Closed.Them", { - user_name: trade.store.rightTraderUser.name - }), - icon: "fas fa-exclamation-triangle", - } - }, modal: false, draggable: true, options: { - height: "auto" - } - }); - - } else { - - if (trade.store.isPrivate) { - const otherUserId = trade.store.leftTraderUser.id === game.user.id ? trade.store.rightTraderUser.id : trade.store.leftTraderUser.id; - ItemPileSocket.executeAsUser(ItemPileSocket.HANDLERS.TRADE_CLOSED, otherUserId, tradeId, game.user.id); - } else { - ItemPileSocket.executeForOthers(ItemPileSocket.HANDLERS.TRADE_CLOSED, tradeId, game.user.id); - } - - TJSDialog.prompt({ - title: game.i18n.localize("ITEM-PILES.Trade.Closed.Title"), content: { - class: CustomDialog, props: { - header: game.i18n.localize("ITEM-PILES.Trade.Closed.Title"), - content: game.i18n.format("ITEM-PILES.Trade.Closed.You"), - icon: "fas fa-exclamation-triangle", - } - }, modal: false, draggable: true, options: { - height: "auto" - } - }); - - if (Helpers.isGMConnected()) { - await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.DISABLE_CHAT_TRADE_BUTTON, tradeId); - } - } - - } else { - - Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.Closed.Someone"), true); - - } - - trade.app.close({ callback: true }); - ongoingTrades.delete(tradeId); - } - - static async _executeTrade(tradeId, privateId, userId) { - const trade = this._getOngoingTrade(tradeId); - if (!trade) return; - if (trade.store.privateTradeId !== privateId) return; - const updates = trade.store.getTradeData(); - - const itemsToAdd = []; - for (const entry of updates.add.items) { - let item = updates.targetActor.items.get(entry.id); - if (!item) { - item = await fromUuid(entry.uuid); - if (!item) continue; - } - const itemData = item.toObject(); - itemsToAdd.push(Utilities.setItemQuantity(itemData, entry.quantity, true)); - } - - const itemsToRemove = [] - for (const entry of updates.remove.items) { - const item = updates.sourceActor.items.get(entry.id); - if (!item) continue; - const itemData = item.toObject(); - itemsToRemove.push(Utilities.setItemQuantity(itemData, entry.quantity, true)); - } - - const transaction = new Transaction(updates.sourceActor); - await transaction.appendItemChanges(itemsToAdd); - await transaction.appendItemChanges(itemsToRemove, { remove: true }); - await transaction.appendActorChanges(updates.add.attributes); - await transaction.appendActorChanges(updates.remove.attributes, { remove: true }); - await transaction.commit(); - - if (trade.store.isPrivate) { - Hooks.callAll(CONSTANTS.HOOKS.TRADE.COMPLETE, trade.store.instigator, data[0], data[1], tradeId); - trade.app.close({ callback: true }); - ongoingTrades.delete(tradeId); - } else if (userId === game.user.id) { - if (Helpers.isGMConnected()) { - await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.DISABLE_CHAT_TRADE_BUTTON, tradeId); - } - return ItemPileSocket.executeForEveryone(ItemPileSocket.HANDLERS.TRADE_COMPLETED, tradeId, updates); - } - } - - static async _tradeCompleted(tradeId) { - const trade = this._getOngoingTrade(tradeId); - if (!trade) return; - const data = trade.store.export(); - if (data.instigator === game.user.id) { - if (trade.store.isPrivate) { - Hooks.callAll(CONSTANTS.HOOKS.TRADE.COMPLETE, data.instigator, data.leftTraderData, data.rightTraderData, tradeId); - } else { - ItemPileSocket.executeForEveryone( - ItemPileSocket.HANDLERS.CALL_HOOK, - CONSTANTS.HOOKS.TRADE.COMPLETE, - trade.store.instigator, - data.leftTraderData, - data.rightTraderData, - tradeId, - trade.store.isPrivate - ) - } - } - trade.app.close({ callback: true }); - ongoingTrades.delete(tradeId); - } + static async _tradeCancelled(userId, privateTradeId) { + + TJSDialog.prompt({ + title: game.i18n.localize("ITEM-PILES.Trade.Title"), content: { + class: CustomDialog, props: { + header: game.i18n.localize("ITEM-PILES.Trade.Title"), + content: game.i18n.format("ITEM-PILES.Trade.CancelledRequest.Content", { user_name: game.users.get(userId).name }), + icon: "fas fa-exclamation-triangle" + } + }, modal: true, draggable: false, options: { + height: "auto" + } + }); + + return TradeRequestDialog.cancel(privateTradeId); + + } + + static async _requestTradeData({ tradeId, tradeUser } = {}) { + + const ongoingTrade = this._getOngoingTrade(tradeId); + if (ongoingTrade) { + return ongoingTrade.store.export(); + } + + const user = game.users.get(tradeUser); + if (!user?.active) { + return false; + } + + const ongoingTradeData = await ItemPileSocket.executeAsUser(ItemPileSocket.HANDLERS.REQUEST_TRADE_DATA, tradeUser, tradeId, game.user.id); + if (!ongoingTradeData) { + return false; + } + + return ongoingTradeData; + + } + + static async _spectateTrade({ tradeId, tradeUser } = {}) { + + const existingApp = TradingApp.getActiveApp(tradeId); + if (existingApp) { + return existingApp.render(false, { focus: true }); + } + + const ongoingTradeData = await this._requestTradeData({ tradeId, tradeUser }); + if (!ongoingTradeData) { + if (Helpers.isGMConnected()) { + ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.DISABLE_CHAT_TRADE_BUTTON, tradeId); + } + return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.Over"), true); + } + + const store = TradeStore.import(ongoingTradeData); + + const app = new TradingApp(store).render(true); + + ongoingTrades.set(store.publicTradeId, { app, store }); + + } + + static async _respondActiveTradeData(tradeId, requesterId) { + const trade = this._getOngoingTrade(tradeId, requesterId); + if (!trade) return; + return trade.store.export(); + } + + static _getOngoingTrade(tradeId, requesterId = game.user.id) { + if (!ongoingTrades.has(tradeId)) return false; + const trade = ongoingTrades.get(tradeId); + if (!trade.store.isPrivate) { + return trade; + } + if (trade.store.leftTraderUser.id !== requesterId && trade.store.rightTraderUser.id !== requesterId) return false; + return trade; + } + + static async _updateItems(tradeId, userId, items) { + const trade = this._getOngoingTrade(tradeId); + if (!trade) return; + trade.store.updateItems(userId, items); + } + + static async _updateItemCurrencies(tradeId, userId, items) { + const trade = this._getOngoingTrade(tradeId); + if (!trade) return; + trade.store.updateItemCurrencies(userId, items); + } + + static async _updateCurrencies(tradeId, userId, currencies) { + const trade = this._getOngoingTrade(tradeId); + if (!trade) return; + trade.store.updateCurrencies(userId, currencies); + } + + static async _updateAcceptedState(tradeId, userId, status) { + const trade = this._getOngoingTrade(tradeId); + if (!trade) return; + trade.store.updateAcceptedState(userId, status); + if (userId === game.user.id && (trade.store.leftTraderUser.id === game.user.id || trade.store.rightTraderUser.id === game.user.id)) { + if (trade.store.tradeIsAccepted) { + setTimeout(async () => { + if (trade.store.tradeIsAccepted) { + ItemPileSocket.executeForUsers( + ItemPileSocket.HANDLERS.EXECUTE_TRADE, + [trade.store.leftTraderUser.id, trade.store.rightTraderUser.id], + trade.store.publicTradeId, + trade.store.privateTradeId, + userId); + } + }, 2000); + } + } + } + + static async _userDisconnected() { + const tradesToDelete = []; + const activeUsers = game.users.filter(user => user.active); + for (let [tradeId, trade] of ongoingTrades) { + const foundLeft = activeUsers.find(u => u === trade.store.leftTraderUser); + const foundRight = activeUsers.find(u => u === trade.store.rightTraderUser); + if (foundLeft && foundRight) continue; + tradesToDelete.push(tradeId); + Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.Disconnected"), true); + await trade.app.close({ callback: true }); + if (foundLeft === game.user || foundRight === game.user) { + if (Helpers.isGMConnected()) { + await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.DISABLE_CHAT_TRADE_BUTTON, tradeId); + } + } + } + tradesToDelete.forEach(tradeId => ongoingTrades.delete(tradeId)); + } + + static async _tradeClosed(tradeId, closeUserId) { + const trade = this._getOngoingTrade(tradeId); + if (!trade) return; + + if (trade.store.leftTraderUser.id === game.user.id || trade.store.rightTraderUser.id === game.user.id) { + + if (closeUserId === trade.store.rightTraderUser.id) { + + TJSDialog.prompt({ + title: game.i18n.localize("ITEM-PILES.Trade.Closed.Title"), content: { + class: CustomDialog, props: { + header: game.i18n.localize("ITEM-PILES.Trade.Closed.Title"), + content: game.i18n.format("ITEM-PILES.Trade.Closed.Them", { + user_name: trade.store.rightTraderUser.name + }), + icon: "fas fa-exclamation-triangle", + } + }, modal: false, draggable: true, options: { + height: "auto" + } + }); + + } else { + + if (trade.store.isPrivate) { + const otherUserId = trade.store.leftTraderUser.id === game.user.id ? trade.store.rightTraderUser.id : trade.store.leftTraderUser.id; + ItemPileSocket.executeAsUser(ItemPileSocket.HANDLERS.TRADE_CLOSED, otherUserId, tradeId, game.user.id); + } else { + ItemPileSocket.executeForOthers(ItemPileSocket.HANDLERS.TRADE_CLOSED, tradeId, game.user.id); + } + + TJSDialog.prompt({ + title: game.i18n.localize("ITEM-PILES.Trade.Closed.Title"), content: { + class: CustomDialog, props: { + header: game.i18n.localize("ITEM-PILES.Trade.Closed.Title"), + content: game.i18n.format("ITEM-PILES.Trade.Closed.You"), + icon: "fas fa-exclamation-triangle", + } + }, modal: false, draggable: true, options: { + height: "auto" + } + }); + + if (Helpers.isGMConnected()) { + await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.DISABLE_CHAT_TRADE_BUTTON, tradeId); + } + } + + } else { + + Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Trade.Closed.Someone"), true); + + } + + trade.app.close({ callback: true }); + ongoingTrades.delete(tradeId); + } + + static async _executeTrade(tradeId, privateId, userId) { + const trade = this._getOngoingTrade(tradeId); + if (!trade) return; + if (trade.store.privateTradeId !== privateId) return; + const updates = trade.store.getTradeData(); + + const itemsToAdd = []; + for (const entry of updates.add.items) { + let item = updates.targetActor.items.get(entry.id); + if (!item) { + item = await fromUuid(entry.uuid); + if (!item) continue; + } + const itemData = item.toObject(); + itemsToAdd.push(Utilities.setItemQuantity(itemData, entry.quantity, true)); + } + + const itemsToRemove = [] + for (const entry of updates.remove.items) { + const item = updates.sourceActor.items.get(entry.id); + if (!item) continue; + const itemData = item.toObject(); + itemsToRemove.push(Utilities.setItemQuantity(itemData, entry.quantity, true)); + } + + const transaction = new Transaction(updates.sourceActor); + await transaction.appendItemChanges(itemsToAdd); + await transaction.appendItemChanges(itemsToRemove, { remove: true }); + await transaction.appendActorChanges(updates.add.attributes); + await transaction.appendActorChanges(updates.remove.attributes, { remove: true }); + await transaction.commit(); + + if (trade.store.isPrivate) { + Hooks.callAll(CONSTANTS.HOOKS.TRADE.COMPLETE, trade.store.instigator, data[0], data[1], tradeId); + trade.app.close({ callback: true }); + ongoingTrades.delete(tradeId); + } else if (userId === game.user.id) { + if (Helpers.isGMConnected()) { + await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.DISABLE_CHAT_TRADE_BUTTON, tradeId); + } + return ItemPileSocket.executeForEveryone(ItemPileSocket.HANDLERS.TRADE_COMPLETED, tradeId, updates); + } + } + + static async _tradeCompleted(tradeId) { + const trade = this._getOngoingTrade(tradeId); + if (!trade) return; + const data = trade.store.export(); + if (data.instigator === game.user.id) { + if (trade.store.isPrivate) { + Hooks.callAll(CONSTANTS.HOOKS.TRADE.COMPLETE, data.instigator, data.leftTraderData, data.rightTraderData, tradeId); + } else { + ItemPileSocket.executeForEveryone( + ItemPileSocket.HANDLERS.CALL_HOOK, + CONSTANTS.HOOKS.TRADE.COMPLETE, + trade.store.instigator, + data.leftTraderData, + data.rightTraderData, + tradeId, + trade.store.isPrivate + ) + } + } + trade.app.close({ callback: true }); + ongoingTrades.delete(tradeId); + } } diff --git a/src/applications/components/ActorPicker.svelte b/src/applications/components/ActorPicker.svelte index b6cd4efa..7f6d2d58 100644 --- a/src/applications/components/ActorPicker.svelte +++ b/src/applications/components/ActorPicker.svelte @@ -1,29 +1,29 @@ diff --git a/src/applications/components/BasicItemDialog.svelte b/src/applications/components/BasicItemDialog.svelte index f7eb8db0..01ac4d99 100644 --- a/src/applications/components/BasicItemDialog.svelte +++ b/src/applications/components/BasicItemDialog.svelte @@ -1,6 +1,6 @@ @@ -18,18 +18,18 @@ + tabindex="-1"> + data-placeholder="Traits" + role="textbox" tabindex="0"> ​ + tabindex="-1" value="[]"> diff --git a/src/applications/components/CurrencyList.svelte b/src/applications/components/CurrencyList.svelte index 2a5899e6..f995beac 100644 --- a/src/applications/components/CurrencyList.svelte +++ b/src/applications/components/CurrencyList.svelte @@ -1,19 +1,19 @@ diff --git a/src/applications/components/CurrencyListEntry.svelte b/src/applications/components/CurrencyListEntry.svelte index c841562c..7adeff6d 100644 --- a/src/applications/components/CurrencyListEntry.svelte +++ b/src/applications/components/CurrencyListEntry.svelte @@ -1,36 +1,36 @@
+ data-fast-tooltip-activation-speed="0" data-fast-tooltip-deactivation-speed="0" style="flex:0 1 auto;">
+ style="min-height: {options.imgSize}px; min-width: {options.imgSize}px; max-width: {options.imgSize}px; max-height: {options.imgSize}px;">
diff --git a/src/applications/components/CustomCategoryInput.svelte b/src/applications/components/CustomCategoryInput.svelte index 811351a1..e7ff4e11 100644 --- a/src/applications/components/CustomCategoryInput.svelte +++ b/src/applications/components/CustomCategoryInput.svelte @@ -1,23 +1,23 @@ diff --git a/src/applications/components/CustomDialog.svelte b/src/applications/components/CustomDialog.svelte index 94c76e66..f2e6f590 100644 --- a/src/applications/components/CustomDialog.svelte +++ b/src/applications/components/CustomDialog.svelte @@ -1,8 +1,8 @@ diff --git a/src/applications/components/DropZone.svelte b/src/applications/components/DropZone.svelte index e1c2a9a0..4cae3650 100644 --- a/src/applications/components/DropZone.svelte +++ b/src/applications/components/DropZone.svelte @@ -1,50 +1,51 @@
- export let value; - export let type; - export let placeholder = ""; - export let showImage = false; - export let showInput = true; - - let filePicker = false; - - function handleClick() { - if (!filePicker) { - filePicker = new FilePicker({ - type: type, - current: value, - callback: path => { - value = path; - filePicker = false; - } - }); - } - - filePicker.render(true, { focus: true }); - } + export let value; + export let type; + export let placeholder = ""; + export let showImage = false; + export let showInput = true; + + let filePicker = false; + + function handleClick() { + if (!filePicker) { + filePicker = new FilePicker({ + type: type, + current: value, + callback: path => { + value = path; + filePicker = false; + } + }); + } + + filePicker.render(true, { focus: true }); + } diff --git a/src/applications/components/FloatingElement/FloatingElement.js b/src/applications/components/FloatingElement/FloatingElement.js index efa6c876..873a776c 100644 --- a/src/applications/components/FloatingElement/FloatingElement.js +++ b/src/applications/components/FloatingElement/FloatingElement.js @@ -3,36 +3,36 @@ import { writable } from "svelte/store"; export class FloatingElement { - static id = void 0; - static element = void 0; - static positionStore = writable(false); - - static create({ id, x, y, zIndex = Number.MAX_SAFE_INTEGER - 100, style = {}, component, componentData } = {}) { - - if (this.element) return this.element; - - this.positionStore.set({ x, y }) - this.id = id; - - this.element = new FloatingElementImpl({ - target: document.body, - props: { - position: this.positionStore, - zIndex, - style, - component, - componentData - } - }); - - } - - static destroy() { - this.element.$destroy(); - this.element = void 0; - this.id = void 0; - this.positionStore.set(false); - } + static id = void 0; + static element = void 0; + static positionStore = writable(false); + + static create({ id, x, y, zIndex = Number.MAX_SAFE_INTEGER - 100, style = {}, component, componentData } = {}) { + + if (this.element) return this.element; + + this.positionStore.set({ x, y }) + this.id = id; + + this.element = new FloatingElementImpl({ + target: document.body, + props: { + position: this.positionStore, + zIndex, + style, + component, + componentData + } + }); + + } + + static destroy() { + this.element.$destroy(); + this.element = void 0; + this.id = void 0; + this.positionStore.set(false); + } } diff --git a/src/applications/components/FloatingElement/FloatingElementImpl.svelte b/src/applications/components/FloatingElement/FloatingElementImpl.svelte index 73007eb3..f458a1de 100644 --- a/src/applications/components/FloatingElement/FloatingElementImpl.svelte +++ b/src/applications/components/FloatingElement/FloatingElementImpl.svelte @@ -1,22 +1,22 @@ diff --git a/src/applications/components/Grid/Grid.svelte b/src/applications/components/Grid/Grid.svelte index 12f29b2e..70d2faa5 100644 --- a/src/applications/components/Grid/Grid.svelte +++ b/src/applications/components/Grid/Grid.svelte @@ -1,84 +1,84 @@ @@ -123,7 +123,7 @@ {#each Array(options.rows) as _, rowIndex (rowIndex)} {#each Array(options.cols) as _, colIndex (colIndex)}
= options.enabledCols || rowIndex >= options.enabledRows} - style="width: {options.gridSize + (options.gap/2)}px; height: {options.gridSize + (options.gap/2)}">
+ style="width: {options.gridSize + (options.gap/2)}px; height: {options.gridSize + (options.gap/2)}">
{/each} {/each}
diff --git a/src/applications/components/Grid/GridItem.svelte b/src/applications/components/Grid/GridItem.svelte index 9017766b..b45df1dd 100644 --- a/src/applications/components/Grid/GridItem.svelte +++ b/src/applications/components/Grid/GridItem.svelte @@ -1,296 +1,296 @@ diff --git a/src/applications/components/Grid/grid-utils.js b/src/applications/components/Grid/grid-utils.js index 12c00938..ecde505f 100644 --- a/src/applications/components/Grid/grid-utils.js +++ b/src/applications/components/Grid/grid-utils.js @@ -2,55 +2,55 @@ import { clamp } from "../../../helpers/helpers"; import { get } from "svelte/store"; export function isItemColliding(item, otherItem) { - const transform = get(item.transform); - const otherTransform = get(otherItem.transform); - return ( - item.id !== otherItem.id && - transform.x <= otherTransform.x + otherTransform.w - 1 && - transform.y <= otherTransform.y + otherTransform.h - 1 && - transform.x + transform.w - 1 >= otherTransform.x && - transform.y + transform.h - 1 >= otherTransform.y - ); + const transform = get(item.transform); + const otherTransform = get(otherItem.transform); + return ( + item.id !== otherItem.id && + transform.x <= otherTransform.x + otherTransform.w - 1 && + transform.y <= otherTransform.y + otherTransform.h - 1 && + transform.x + transform.w - 1 >= otherTransform.x && + transform.y + transform.h - 1 >= otherTransform.y + ); } export function getCollisions(originalItem, items) { - return items.filter((item) => isItemColliding(originalItem, item)); + return items.filter((item) => isItemColliding(originalItem, item)); } export function coordinate2position(coordinate, cellSize, gap) { - return coordinate * cellSize + (coordinate + 1) * gap; + return coordinate * cellSize + (coordinate + 1) * gap; } export function coordinate2size(coordinate, cellSize, gap) { - return coordinate * cellSize + (coordinate - 1) * gap; + return coordinate * cellSize + (coordinate - 1) * gap; } export function position2coordinate(position, cellSize, size, gap) { - return Math.round(position / (cellSize + gap)); + return Math.round(position / (cellSize + gap)); } export function snapOnMove(left, top, transform, options) { - const { gridSize, gap, cols, enabledCols, rows, enabledRows } = options; - const { w, h } = transform; + const { gridSize, gap, cols, enabledCols, rows, enabledRows } = options; + const { w, h } = transform; - const width = w * gridSize; - const height = h * gridSize; + const width = w * gridSize; + const height = h * gridSize; - let x = position2coordinate(left, gridSize, width, gap); - let y = position2coordinate(top, gridSize, height, gap); + let x = position2coordinate(left, gridSize, width, gap); + let y = position2coordinate(top, gridSize, height, gap); - x = clamp(x, 0, Math.min(cols, enabledCols) - w); - y = clamp(y, 0, Math.min(rows, enabledRows) - h); + x = clamp(x, 0, Math.min(cols, enabledCols) - w); + y = clamp(y, 0, Math.min(rows, enabledRows) - h); - return { x, y }; + return { x, y }; } export function calcPosition(transform, options) { - const { gridSize, gap } = options; - return { - left: coordinate2position(transform.x, gridSize, gap), - top: coordinate2position(transform.y, gridSize, gap), - width: coordinate2size(transform.w, gridSize, gap), - height: coordinate2size(transform.h, gridSize, gap) - }; + const { gridSize, gap } = options; + return { + left: coordinate2position(transform.x, gridSize, gap), + top: coordinate2position(transform.y, gridSize, gap), + width: coordinate2size(transform.w, gridSize, gap), + height: coordinate2size(transform.h, gridSize, gap) + }; } diff --git a/src/applications/components/MacroSelector.svelte b/src/applications/components/MacroSelector.svelte index d038597a..9c0ab225 100644 --- a/src/applications/components/MacroSelector.svelte +++ b/src/applications/components/MacroSelector.svelte @@ -1,92 +1,92 @@
{ filterMacros() }} - on:keyup={() => { filterMacros() }} - placeholder={localize("ITEM-PILES.Applications.ItemPileConfig.Main.MacroPlaceholder")} - style="flex:1; margin-right:5px;" - type="text" + list={id} + on:change={() => { filterMacros() }} + on:keyup={() => { filterMacros() }} + placeholder={localize("ITEM-PILES.Applications.ItemPileConfig.Main.MacroPlaceholder")} + style="flex:1; margin-right:5px;" + type="text" /> {#each $macros as m (m.id)} diff --git a/src/applications/components/PriceList.svelte b/src/applications/components/PriceList.svelte index cb767a33..18470b97 100644 --- a/src/applications/components/PriceList.svelte +++ b/src/applications/components/PriceList.svelte @@ -1,166 +1,164 @@ @@ -192,18 +190,18 @@ {/if} {#each prices as price, index (price.id)}
+ animate:flip="{{ duration: flipDurationMs }}">
+ max="{price.percent ? 100 : 1000000000000000}"/>
@@ -233,7 +231,7 @@
{localize("ITEM-PILES.Applications.ItemEditor.PricePreset")} + bind:value={attribute.currentQuantity}/> {:else} + bind:value={attribute.currentQuantity}/> { + bind:value={attribute.currentQuantity} + on:click={() => { attribute.currentQuantity = Math.max(0, Math.min(attribute.quantity, attribute.currentQuantity)); }}/>
/ {abbreviateNumbers(attribute.quantity)}
@@ -139,13 +139,13 @@ {#if settings?.unlimitedCurrencies} + bind:value={item.currentQuantity}/> {:else} + bind:value={item.currentQuantity}/> { + bind:value={item.currentQuantity} + on:click={() => { item.currentQuantity = Math.max(0, Math.min(item.quantity, item.currentQuantity)); }}/>
/ {abbreviateNumbers(item.quantity)}
@@ -168,13 +168,13 @@ {#if settings?.unlimitedCurrencies} + bind:value={attribute.currentQuantity}/> {:else} + bind:value={attribute.currentQuantity}/> { + bind:value={attribute.currentQuantity} + on:click={() => { attribute.currentQuantity = Math.max(0, Math.min(attribute.quantity, attribute.currentQuantity)); }}/>
/ {abbreviateNumbers(attribute.quantity)}
@@ -193,13 +193,13 @@ {#if settings?.unlimitedCurrencies} + bind:value={item.currentQuantity}/> {:else} + bind:value={item.currentQuantity}/> { + bind:value={item.currentQuantity} + on:click={() => { item.currentQuantity = Math.max(0, Math.min(item.quantity, item.currentQuantity)); }}/>
/ {abbreviateNumbers(item.quantity)}
diff --git a/src/applications/dialogs/drop-currency-dialog/drop-currency-dialog.js b/src/applications/dialogs/drop-currency-dialog/drop-currency-dialog.js index 1e5faf5a..812d6780 100644 --- a/src/applications/dialogs/drop-currency-dialog/drop-currency-dialog.js +++ b/src/applications/dialogs/drop-currency-dialog/drop-currency-dialog.js @@ -4,59 +4,59 @@ import { getActiveApps } from "../../../helpers/helpers"; export default class DropCurrencyDialog extends SvelteApplication { - /** - * - * @param sourceActor - * @param targetActor - * @param settings - * @param options - */ - constructor(sourceActor, targetActor, settings = {}, options = {}) { - const localization = settings.localization || "DropCurrencies"; - super({ - id: `item-pile-drop-currency-${sourceActor ? (sourceActor.id + (targetActor ? "-" + targetActor.id : "")) : ""}-${randomID()}`, - title: settings.title ?? game.i18n.localize(`ITEM-PILES.Applications.${localization}.Title`), - svelte: { - class: DropCurrencyDialogShell, - target: document.body, - props: { - sourceActor, - targetActor, - localization, - settings - } - }, - close: () => this.options.resolve?.(null), - ...options - }) - } + /** + * + * @param sourceActor + * @param targetActor + * @param settings + * @param options + */ + constructor(sourceActor, targetActor, settings = {}, options = {}) { + const localization = settings.localization || "DropCurrencies"; + super({ + id: `item-pile-drop-currency-${sourceActor ? (sourceActor.id + (targetActor ? "-" + targetActor.id : "")) : ""}-${randomID()}`, + title: settings.title ?? game.i18n.localize(`ITEM-PILES.Applications.${localization}.Title`), + svelte: { + class: DropCurrencyDialogShell, + target: document.body, + props: { + sourceActor, + targetActor, + localization, + settings + } + }, + close: () => this.options.resolve?.(null), + ...options + }) + } - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - width: 430, - height: "auto", - classes: ["item-piles-app"] - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + width: 430, + height: "auto", + classes: ["item-piles-app"] + }) + } - static getActiveApps(id) { - return getActiveApps(`item-pile-drop-currency-${id}`); - } + static getActiveApps(id) { + return getActiveApps(`item-pile-drop-currency-${id}`); + } - static async show(sourceActor, targetActor, settings = {}, options = {}) { - if (sourceActor) { - const apps = this.getActiveApps(targetActor ? sourceActor.uuid + "-" + targetActor.uuid : sourceActor.uuid); - if (apps.length) { - for (let app of apps) { - app.render(false, { focus: true }); - } - return; - } - } - return new Promise((resolve) => { - options.resolve = resolve; - new this(sourceActor, targetActor, settings, options).render(true, { focus: true }); - }) - } + static async show(sourceActor, targetActor, settings = {}, options = {}) { + if (sourceActor) { + const apps = this.getActiveApps(targetActor ? sourceActor.uuid + "-" + targetActor.uuid : sourceActor.uuid); + if (apps.length) { + for (let app of apps) { + app.render(false, { focus: true }); + } + return; + } + } + return new Promise((resolve) => { + options.resolve = resolve; + new this(sourceActor, targetActor, settings, options).render(true, { focus: true }); + }) + } } diff --git a/src/applications/dialogs/drop-item-dialog/drop-item-dialog-shell.svelte b/src/applications/dialogs/drop-item-dialog/drop-item-dialog-shell.svelte index 2999624e..36c78827 100644 --- a/src/applications/dialogs/drop-item-dialog/drop-item-dialog-shell.svelte +++ b/src/applications/dialogs/drop-item-dialog/drop-item-dialog-shell.svelte @@ -1,34 +1,34 @@ @@ -36,20 +36,20 @@
+ style="padding:0.5rem;">

{localize(`ITEM-PILES.Applications.${application.options.localizationTitle}.Header`, { - item_name: item.name - })} + item_name: item.name + })}

{#if target}

{localize(`ITEM-PILES.Applications.${application.options.localizationTitle}.Content`, { - target_name: target.name - })} + target_name: target.name + })}

{/if} @@ -58,10 +58,10 @@
+ target_name: target?.name ?? "", + quantity: itemQuantity, + itemName: item.name + })}
{#if unlimitedQuantity} diff --git a/src/applications/dialogs/drop-item-dialog/drop-item-dialog.js b/src/applications/dialogs/drop-item-dialog/drop-item-dialog.js index 140ce72d..85d58d19 100644 --- a/src/applications/dialogs/drop-item-dialog/drop-item-dialog.js +++ b/src/applications/dialogs/drop-item-dialog/drop-item-dialog.js @@ -4,60 +4,60 @@ import { getActiveApps } from "../../../helpers/helpers"; export default class DropItemDialog extends SvelteApplication { - /** - * - * @param item - * @param target - * @param options - */ - constructor(item, target, options = { - localizationTitle: "DropItem" - }) { - super({ - title: game.i18n.localize(`ITEM-PILES.Applications.${options.localizationTitle}.Title`), - id: `item-pile-drop-item-${item.id}${target ? "-" + target.id : ""}-${randomID()}`, - svelte: { - class: DropItemDialogShell, - target: document.body, - props: { - item, - target - } - }, - close: () => this.options.resolve?.(null), - ...options - }); - this.item = item; - this.target = target; - } + /** + * + * @param item + * @param target + * @param options + */ + constructor(item, target, options = { + localizationTitle: "DropItem" + }) { + super({ + title: game.i18n.localize(`ITEM-PILES.Applications.${options.localizationTitle}.Title`), + id: `item-pile-drop-item-${item.id}${target ? "-" + target.id : ""}-${randomID()}`, + svelte: { + class: DropItemDialogShell, + target: document.body, + props: { + item, + target + } + }, + close: () => this.options.resolve?.(null), + ...options + }); + this.item = item; + this.target = target; + } - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - width: 430, - height: "auto", - classes: ["item-piles-app"] - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + width: 430, + height: "auto", + classes: ["item-piles-app"] + }) + } - static getActiveApps(id) { - return getActiveApps(`item-pile-drop-item-${id}`); - } + static getActiveApps(id) { + return getActiveApps(`item-pile-drop-item-${id}`); + } - static async show(item, target, options = {}) { - if (!options?.localizationTitle) { - options.localizationTitle = "DropItem"; - } - const apps = this.getActiveApps(item.uuid + (target ? "-" + target.uuid : "")); - if (apps.length) { - for (let app of apps) { - app.render(false, { focus: true }); - } - return; - } - return new Promise((resolve) => { - options.resolve = resolve; - new this(item, target, options).render(true, { focus: true }); - }) - } + static async show(item, target, options = {}) { + if (!options?.localizationTitle) { + options.localizationTitle = "DropItem"; + } + const apps = this.getActiveApps(item.uuid + (target ? "-" + target.uuid : "")); + if (apps.length) { + for (let app of apps) { + app.render(false, { focus: true }); + } + return; + } + return new Promise((resolve) => { + options.resolve = resolve; + new this(item, target, options).render(true, { focus: true }); + }) + } } diff --git a/src/applications/dialogs/give-items-dialog/give-items-shell.svelte b/src/applications/dialogs/give-items-dialog/give-items-shell.svelte index 77d4cedd..0ae2d68c 100644 --- a/src/applications/dialogs/give-items-dialog/give-items-shell.svelte +++ b/src/applications/dialogs/give-items-dialog/give-items-shell.svelte @@ -86,8 +86,8 @@ --height="calc(var(--form-field-height) + 1px)" --input-color="black" --item-padding="0.25rem" - --padding="0 8px" --margin="0.25rem 0" + --padding="0 8px" --text-overflow="ellipsis" bind:value={selectedActor} floatingConfig={{ strategy: "fixed", placement: "bottom" }} diff --git a/src/applications/dialogs/text-editor-dialog/text-editor-dialog-shell.svelte b/src/applications/dialogs/text-editor-dialog/text-editor-dialog-shell.svelte index 6258c963..08abd6de 100644 --- a/src/applications/dialogs/text-editor-dialog/text-editor-dialog-shell.svelte +++ b/src/applications/dialogs/text-editor-dialog/text-editor-dialog-shell.svelte @@ -1,28 +1,28 @@ @@ -30,7 +30,7 @@ + style="padding:0.5rem;"> this.options.resolve?.(null), - ...options - }); - } + constructor(text, options) { + super({ + title: game.i18n.localize("ITEM-PILES.Dialogs.TextEditor.Title"), + id: `item-piles-text-editor${options?.id ? "-" + options.id : ""}-${randomID()}`, + svelte: { + class: TextEditorDialogShell, + target: document.body, + props: { + text + } + }, + close: () => this.options.resolve?.(null), + ...options + }); + } - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - width: 550, - height: 450, - classes: ["item-piles-app"], - resizable: true - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + width: 550, + height: 450, + classes: ["item-piles-app"], + resizable: true + }) + } - static getActiveApps(id) { - return getActiveApps(`item-pile-text-editor-${id}`); - } + static getActiveApps(id) { + return getActiveApps(`item-pile-text-editor-${id}`); + } - static async show(text, options = {}) { - const apps = options.id ? this.getActiveApps(options.id) : []; - if (apps.length) { - for (let app of apps) { - app.render(false, { focus: true }); - } - return; - } - return new Promise((resolve) => { - options.resolve = resolve; - new this(text, options).render(true, { focus: true }); - }) - } + static async show(text, options = {}) { + const apps = options.id ? this.getActiveApps(options.id) : []; + if (apps.length) { + for (let app of apps) { + app.render(false, { focus: true }); + } + return; + } + return new Promise((resolve) => { + options.resolve = resolve; + new this(text, options).render(true, { focus: true }); + }) + } } diff --git a/src/applications/dialogs/trade-merchant-item-dialog/trade-merchant-item-dialog-shell.svelte b/src/applications/dialogs/trade-merchant-item-dialog/trade-merchant-item-dialog-shell.svelte index 3936ba78..fbbf60ae 100644 --- a/src/applications/dialogs/trade-merchant-item-dialog/trade-merchant-item-dialog-shell.svelte +++ b/src/applications/dialogs/trade-merchant-item-dialog/trade-merchant-item-dialog-shell.svelte @@ -1,74 +1,74 @@ @@ -92,8 +92,8 @@ {localize("ITEM-PILES.Applications.TradeMerchantItem.Quantity")} ({localize("ITEM-PILES.Applications.TradeMerchantItem.MaxQuantity", { - quantity: maxItemPurchaseQuantity - })}) + quantity: maxItemPurchaseQuantity + })})
{ @@ -124,7 +124,7 @@ + style="margin-bottom:0.25rem; padding-bottom:0.25rem;"> {localize("ITEM-PILES.Applications.TradeMerchantItem." + (settings.selling ? "TheyReceive" : "YouReceive"))}: diff --git a/src/applications/dialogs/trade-merchant-item-dialog/trade-merchant-item-dialog.js b/src/applications/dialogs/trade-merchant-item-dialog/trade-merchant-item-dialog.js index 9f1a1ff4..462b8588 100644 --- a/src/applications/dialogs/trade-merchant-item-dialog/trade-merchant-item-dialog.js +++ b/src/applications/dialogs/trade-merchant-item-dialog/trade-merchant-item-dialog.js @@ -5,57 +5,57 @@ import { getActiveApps } from "../../../helpers/helpers"; export default class TradeMerchantItemDialog extends SvelteApplication { - /** - * - * @param item - * @param seller - * @param buyer - * @param settings - * @param options - */ - constructor(item, seller, buyer, settings = {}, options = {}) { - super({ - id: `item-pile-buy-item-dialog-${item.id}-${seller.id}-${buyer.id}-${randomID()}`, - title: game.i18n.format("ITEM-PILES.Applications.TradeMerchantItem.Title", { item_name: get(item.name) }), - svelte: { - class: TradeMerchantItemDialogShell, - target: document.body, - props: { - item, - seller, - buyer, - settings - } - }, - close: () => this.options.resolve?.(null), - ...options - }); - } + /** + * + * @param item + * @param seller + * @param buyer + * @param settings + * @param options + */ + constructor(item, seller, buyer, settings = {}, options = {}) { + super({ + id: `item-pile-buy-item-dialog-${item.id}-${seller.id}-${buyer.id}-${randomID()}`, + title: game.i18n.format("ITEM-PILES.Applications.TradeMerchantItem.Title", { item_name: get(item.name) }), + svelte: { + class: TradeMerchantItemDialogShell, + target: document.body, + props: { + item, + seller, + buyer, + settings + } + }, + close: () => this.options.resolve?.(null), + ...options + }); + } - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - width: 330, - height: "auto", - classes: ["item-piles-app"] - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + width: 330, + height: "auto", + classes: ["item-piles-app"] + }) + } - static getActiveApps(id) { - return getActiveApps(`item-pile-buy-item-dialog-${id}`); - } + static getActiveApps(id) { + return getActiveApps(`item-pile-buy-item-dialog-${id}`); + } - static async show(item, seller, buyer, settings = {}, options = {}) { - const apps = this.getActiveApps(item.uuid + "-" + seller.uuid + "-" + buyer.uuid); - if (apps.length) { - for (let app of apps) { - app.render(false, { focus: true }); - } - return; - } - return new Promise((resolve) => { - options.resolve = resolve; - new this(item, seller, buyer, settings, options).render(true, { focus: true }); - }) - } + static async show(item, seller, buyer, settings = {}, options = {}) { + const apps = this.getActiveApps(item.uuid + "-" + seller.uuid + "-" + buyer.uuid); + if (apps.length) { + for (let app of apps) { + app.render(false, { focus: true }); + } + return; + } + return new Promise((resolve) => { + options.resolve = resolve; + new this(item, seller, buyer, settings, options).render(true, { focus: true }); + }) + } } diff --git a/src/applications/dialogs/user-select-dialog/user-select-dialog-shell.svelte b/src/applications/dialogs/user-select-dialog/user-select-dialog-shell.svelte index 771f228c..506b46bb 100644 --- a/src/applications/dialogs/user-select-dialog/user-select-dialog-shell.svelte +++ b/src/applications/dialogs/user-select-dialog/user-select-dialog-shell.svelte @@ -1,34 +1,34 @@ @@ -37,7 +37,7 @@ + style="padding:0.5rem;">

{localize("ITEM-PILES.Dialogs.UserSelect.Content")} diff --git a/src/applications/dialogs/user-select-dialog/user-select-dialog.js b/src/applications/dialogs/user-select-dialog/user-select-dialog.js index 678da4a2..32f5c8a0 100644 --- a/src/applications/dialogs/user-select-dialog/user-select-dialog.js +++ b/src/applications/dialogs/user-select-dialog/user-select-dialog.js @@ -3,34 +3,34 @@ import { SvelteApplication } from '@typhonjs-fvtt/runtime/svelte/application'; export default class UserSelectDialog extends SvelteApplication { - /** - * @param options - */ - constructor(options = {}) { - super({ - title: game.i18n.localize("ITEM-PILES.Dialogs.UserSelect.Title"), - svelte: { - class: UserSelectDialogShell, - target: document.body, - }, - close: () => this.options.resolve?.(null), - ...options - }) - } + /** + * @param options + */ + constructor(options = {}) { + super({ + title: game.i18n.localize("ITEM-PILES.Dialogs.UserSelect.Title"), + svelte: { + class: UserSelectDialogShell, + target: document.body, + }, + close: () => this.options.resolve?.(null), + ...options + }) + } - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - width: 200, - height: "auto", - classes: ["item-piles-app"] - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + width: 200, + height: "auto", + classes: ["item-piles-app"] + }) + } - static async show(options = {}) { - return new Promise((resolve) => { - options.resolve = resolve; - new this(options).render(true, { focus: true }); - }) - } + static async show(options = {}) { + return new Promise((resolve) => { + options.resolve = resolve; + new this(options).render(true, { focus: true }); + }) + } } diff --git a/src/applications/editors/Editor.js b/src/applications/editors/Editor.js index fa27d9f2..c8727cdf 100644 --- a/src/applications/editors/Editor.js +++ b/src/applications/editors/Editor.js @@ -3,35 +3,35 @@ import { getActiveApps } from "../../helpers/helpers.js"; export default class Editor extends SvelteApplication { - constructor(data, options, dialogOptions) { - super({ - svelte: { - props: { - data - } - }, - ...options - }, dialogOptions); - } + constructor(data, options, dialogOptions) { + super({ + svelte: { + props: { + data + } + }, + ...options + }, dialogOptions); + } - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - width: 400, - height: "auto", - classes: ["item-piles-app"], - close: () => this.options.resolve(null), - svelte: { - target: document.body, - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + width: 400, + height: "auto", + classes: ["item-piles-app"], + close: () => this.options.resolve(null), + svelte: { + target: document.body, + } + }) + } - static async show(data, options = {}, dialogOptions = {}) { - const app = options?.id ? getActiveApps(options?.id, true) : false; - if (app) return app.render(false, { focus: true }); - return new Promise(resolve => { - options.resolve = resolve; - return new this(data, options, dialogOptions).render(true, { focus: true }); - }); - } + static async show(data, options = {}, dialogOptions = {}) { + const app = options?.id ? getActiveApps(options?.id, true) : false; + if (app) return app.render(false, { focus: true }); + return new Promise(resolve => { + options.resolve = resolve; + return new this(data, options, dialogOptions).render(true, { focus: true }); + }); + } } diff --git a/src/applications/editors/currencies-editor/CurrencyList.svelte b/src/applications/editors/currencies-editor/CurrencyList.svelte index c29dd875..71b354c5 100644 --- a/src/applications/editors/currencies-editor/CurrencyList.svelte +++ b/src/applications/editors/currencies-editor/CurrencyList.svelte @@ -1,37 +1,37 @@ diff --git a/src/applications/editors/currencies-editor/currencies-editor-shell.svelte b/src/applications/editors/currencies-editor/currencies-editor-shell.svelte index 40a62735..e3fc1b41 100644 --- a/src/applications/editors/currencies-editor/currencies-editor-shell.svelte +++ b/src/applications/editors/currencies-editor/currencies-editor-shell.svelte @@ -1,32 +1,32 @@ diff --git a/src/applications/editors/currencies-editor/currencies-editor.js b/src/applications/editors/currencies-editor/currencies-editor.js index e923e4ab..a4985f57 100644 --- a/src/applications/editors/currencies-editor/currencies-editor.js +++ b/src/applications/editors/currencies-editor/currencies-editor.js @@ -2,26 +2,26 @@ import CurrenciesEditorShell from './currencies-editor-shell.svelte'; import Editor from "../Editor.js"; export class CurrenciesEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: game.i18n.localize("ITEM-PILES.Applications.CurrenciesEditor.Title"), - width: 630, - svelte: { - class: CurrenciesEditorShell, - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: game.i18n.localize("ITEM-PILES.Applications.CurrenciesEditor.Title"), + width: 630, + svelte: { + class: CurrenciesEditorShell, + } + }) + } } export class SecondaryCurrenciesEditor extends CurrenciesEditor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: game.i18n.localize("ITEM-PILES.Applications.SecondaryCurrenciesEditor.Title"), - svelte: { - props: { - secondary: true - } - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: game.i18n.localize("ITEM-PILES.Applications.SecondaryCurrenciesEditor.Title"), + svelte: { + props: { + secondary: true + } + } + }) + } } diff --git a/src/applications/editors/currencies-editor/currency-store.js b/src/applications/editors/currencies-editor/currency-store.js index 5dd40771..500e7089 100644 --- a/src/applications/editors/currencies-editor/currency-store.js +++ b/src/applications/editors/currencies-editor/currency-store.js @@ -7,132 +7,132 @@ import * as CompendiumUtilities from "../../../helpers/compendium-utilities.js"; export default class CurrencyStore { - constructor(data, secondary = false) { - this.secondary = secondary; - this.currencies = writable(data.map((entry, index) => { - return { - ...entry, - index, - id: entry.data?.path ?? entry.data?._id ?? randomID() - } - })); - } + constructor(data, secondary = false) { + this.secondary = secondary; + this.currencies = writable(data.map((entry, index) => { + return { + ...entry, + index, + id: entry.data?.path ?? entry.data?._id ?? randomID() + } + })); + } - setPrimary(index) { - if (this.secondary) return; - this.currencies.update(currencies => { - currencies.forEach((entry, entryIndex) => { - entry.primary = entryIndex === index; - }); - return currencies; - }); - } + setPrimary(index) { + if (this.secondary) return; + this.currencies.update(currencies => { + currencies.forEach((entry, entryIndex) => { + entry.primary = entryIndex === index; + }); + return currencies; + }); + } - sortCurrencies() { - if (this.secondary) return; - this.currencies.update(currencies => { - currencies.sort((a, b) => { - return b.exchangeRate - a.exchangeRate; - }); - return currencies; - }); - } + sortCurrencies() { + if (this.secondary) return; + this.currencies.update(currencies => { + currencies.sort((a, b) => { + return b.exchangeRate - a.exchangeRate; + }); + return currencies; + }); + } - addAttribute() { - this.currencies.update(currencies => { - currencies.push(foundry.utils.mergeObject({ - type: "attribute", - name: "New Attribute", - img: "", - abbreviation: "{#}N", - data: { - path: "" - } - }, this.secondary ? {} : { - primary: !currencies.length, - exchangeRate: 1 - })); - return currencies; - }); - } + addAttribute() { + this.currencies.update(currencies => { + currencies.push(foundry.utils.mergeObject({ + type: "attribute", + name: "New Attribute", + img: "", + abbreviation: "{#}N", + data: { + path: "" + } + }, this.secondary ? {} : { + primary: !currencies.length, + exchangeRate: 1 + })); + return currencies; + }); + } - async addItem(data) { + async addItem(data) { - let uuid = false; - if (data.pack) { - uuid = "Compendium" + data.pack + "." + data.id; - } + let uuid = false; + if (data.pack) { + uuid = "Compendium" + data.pack + "." + data.id; + } - let item = await Item.implementation.fromDropData(data); - let itemData = item.toObject(); + let item = await Item.implementation.fromDropData(data); + let itemData = item.toObject(); - if (!itemData) { - console.error(data); - throw Helpers.custom_error("Something went wrong when dropping this item!") - } + if (!itemData) { + console.error(data); + throw Helpers.custom_error("Something went wrong when dropping this item!") + } - if (!uuid) { - uuid = (await CompendiumUtilities.findOrCreateItemInCompendium(itemData)).uuid; - } + if (!uuid) { + uuid = (await CompendiumUtilities.findOrCreateItemInCompendium(itemData)).uuid; + } - this.currencies.update(currencies => { - const itemCurrencies = currencies.map(entry => entry.data?.item ?? {}); - const foundItem = Utilities.findSimilarItem(itemCurrencies, itemData); - if (foundItem) { - const index = itemCurrencies.indexOf(foundItem); - currencies[index].data = { - uuid, - } - Helpers.custom_notify(`Updated item data for ${localize(currencies[index].name)} (item name ${itemData.name})`) - } else { - currencies.push(foundry.utils.mergeObject({ - id: randomID(), - type: "item", - name: itemData.name, - img: itemData.img, - abbreviation: "{#} " + itemData.name, - data: { - uuid - }, - }, this.secondary ? {} : { - primary: !currencies.length, - exchangeRate: 1 - })); - } - return currencies; - }); - } + this.currencies.update(currencies => { + const itemCurrencies = currencies.map(entry => entry.data?.item ?? {}); + const foundItem = Utilities.findSimilarItem(itemCurrencies, itemData); + if (foundItem) { + const index = itemCurrencies.indexOf(foundItem); + currencies[index].data = { + uuid, + } + Helpers.custom_notify(`Updated item data for ${localize(currencies[index].name)} (item name ${itemData.name})`) + } else { + currencies.push(foundry.utils.mergeObject({ + id: randomID(), + type: "item", + name: itemData.name, + img: itemData.img, + abbreviation: "{#} " + itemData.name, + data: { + uuid + }, + }, this.secondary ? {} : { + primary: !currencies.length, + exchangeRate: 1 + })); + } + return currencies; + }); + } - async editItem(index) { - const currencies = get(this.currencies); - const data = currencies[index].data; - let item; - if (data.uuid) { - item = await fromUuid(data.uuid); - } else { - let itemData = data.item; - if (itemData._id) delete itemData._id; - if (itemData.ownership) delete itemData.ownership; - const items = Array.from(game.items); - item = Utilities.findSimilarItem(items, itemData); - if (!item) { - setProperty(itemData, CONSTANTS.FLAGS.TEMPORARY_ITEM, true); - item = await Item.implementation.create(itemData); - Helpers.custom_notify(`An item has been created for ${item.name} - drag and drop it into the list to update the stored item data`) - } - } - item.sheet.render(true); - } + async editItem(index) { + const currencies = get(this.currencies); + const data = currencies[index].data; + let item; + if (data.uuid) { + item = await fromUuid(data.uuid); + } else { + let itemData = data.item; + if (itemData._id) delete itemData._id; + if (itemData.ownership) delete itemData.ownership; + const items = Array.from(game.items); + item = Utilities.findSimilarItem(items, itemData); + if (!item) { + setProperty(itemData, CONSTANTS.FLAGS.TEMPORARY_ITEM, true); + item = await Item.implementation.create(itemData); + Helpers.custom_notify(`An item has been created for ${item.name} - drag and drop it into the list to update the stored item data`) + } + } + item.sheet.render(true); + } - removeEntry(index) { - this.currencies.update(currencies => { - currencies.splice(index, 1); - return currencies; - }) - } + removeEntry(index) { + this.currencies.update(currencies => { + currencies.splice(index, 1); + return currencies; + }) + } - export() { - return get(this.currencies); - } + export() { + return get(this.currencies); + } } diff --git a/src/applications/editors/index.js b/src/applications/editors/index.js index 2bb460b8..a750f68c 100644 --- a/src/applications/editors/index.js +++ b/src/applications/editors/index.js @@ -8,14 +8,14 @@ import VaultStylesEditor from "./vault-styles-editor/vault-styles-editor.js"; import StylesEditor from "./styles-editor/styles-editor.js"; export default { - "currencies": CurrenciesEditor, - "secondary-currencies": SecondaryCurrenciesEditor, - "item-filters": ItemFiltersEditor, - "item-similarities": StringListEditor, - "item-categories": StringListEditor, - "styles": StylesEditor, - "vault-styles": VaultStylesEditor, - "price-modifiers": PriceModifiersEditor, - "unstackable-item-types": UnstackableItemTypesEditor, - "price-presets": PricePresetEditor + "currencies": CurrenciesEditor, + "secondary-currencies": SecondaryCurrenciesEditor, + "item-filters": ItemFiltersEditor, + "item-similarities": StringListEditor, + "item-categories": StringListEditor, + "styles": StylesEditor, + "vault-styles": VaultStylesEditor, + "price-modifiers": PriceModifiersEditor, + "unstackable-item-types": UnstackableItemTypesEditor, + "price-presets": PricePresetEditor } diff --git a/src/applications/editors/item-filters-editor/item-filters-editor.js b/src/applications/editors/item-filters-editor/item-filters-editor.js index 7629cf71..2924990f 100644 --- a/src/applications/editors/item-filters-editor/item-filters-editor.js +++ b/src/applications/editors/item-filters-editor/item-filters-editor.js @@ -2,12 +2,12 @@ import ItemFiltersShell from './item-filters-editor.svelte'; import Editor from "../Editor.js"; export default class ItemFiltersEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: "ITEM-PILES.Applications.FilterEditor.Title", - svelte: { - class: ItemFiltersShell, - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: "ITEM-PILES.Applications.FilterEditor.Title", + svelte: { + class: ItemFiltersShell, + } + }) + } } diff --git a/src/applications/editors/item-filters-editor/item-filters-editor.svelte b/src/applications/editors/item-filters-editor/item-filters-editor.svelte index 6c2f61e1..56b6399c 100644 --- a/src/applications/editors/item-filters-editor/item-filters-editor.svelte +++ b/src/applications/editors/item-filters-editor/item-filters-editor.svelte @@ -1,43 +1,43 @@ diff --git a/src/applications/editors/item-type-price-modifiers-editor/item-type-price-modifiers-editor.js b/src/applications/editors/item-type-price-modifiers-editor/item-type-price-modifiers-editor.js index 629b1cb0..3bcc6dbc 100644 --- a/src/applications/editors/item-type-price-modifiers-editor/item-type-price-modifiers-editor.js +++ b/src/applications/editors/item-type-price-modifiers-editor/item-type-price-modifiers-editor.js @@ -2,13 +2,13 @@ import ItemTypePriceModifiersShell from './item-type-price-modifiers-editor.svel import Editor from "../Editor.js"; export default class ItemTypePriceModifiersEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: game.i18n.localize("ITEM-PILES.Applications.ItemTypePriceModifiersEditor.Title"), - width: 600, - svelte: { - class: ItemTypePriceModifiersShell, - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: game.i18n.localize("ITEM-PILES.Applications.ItemTypePriceModifiersEditor.Title"), + width: 600, + svelte: { + class: ItemTypePriceModifiersShell, + } + }) + } } diff --git a/src/applications/editors/item-type-price-modifiers-editor/item-type-price-modifiers-editor.svelte b/src/applications/editors/item-type-price-modifiers-editor/item-type-price-modifiers-editor.svelte index a3c78a6c..973d041f 100644 --- a/src/applications/editors/item-type-price-modifiers-editor/item-type-price-modifiers-editor.svelte +++ b/src/applications/editors/item-type-price-modifiers-editor/item-type-price-modifiers-editor.svelte @@ -1,60 +1,60 @@ @@ -95,8 +95,8 @@ {#each systemTypes as [itemType, label] (itemType)} {/each} @@ -105,8 +105,8 @@ {#each currentCustomCategories as customCategory} {/each} diff --git a/src/applications/editors/merchant-columns-editor/merchant-columns-editor-shell.svelte b/src/applications/editors/merchant-columns-editor/merchant-columns-editor-shell.svelte index 1d08371a..11f876a5 100644 --- a/src/applications/editors/merchant-columns-editor/merchant-columns-editor-shell.svelte +++ b/src/applications/editors/merchant-columns-editor/merchant-columns-editor-shell.svelte @@ -1,71 +1,71 @@ @@ -125,7 +125,7 @@

removeColumn(index)} class="item-piles-clickable-red" - style="margin-right: 0.5rem; text-align: center;"> + style="margin-right: 0.5rem; text-align: center;">
diff --git a/src/applications/editors/merchant-columns-editor/merchant-columns-editor.js b/src/applications/editors/merchant-columns-editor/merchant-columns-editor.js index bc36278c..4918a891 100644 --- a/src/applications/editors/merchant-columns-editor/merchant-columns-editor.js +++ b/src/applications/editors/merchant-columns-editor/merchant-columns-editor.js @@ -2,13 +2,13 @@ import MerchantColumnsEditorShell from "./merchant-columns-editor-shell.svelte"; import Editor from "../Editor.js"; export default class MerchantColumnsEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: "ITEM-PILES.Applications.MerchantColumnsEditor.Title", - width: 600, - svelte: { - class: MerchantColumnsEditorShell - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: "ITEM-PILES.Applications.MerchantColumnsEditor.Title", + width: 600, + svelte: { + class: MerchantColumnsEditorShell + } + }) + } } diff --git a/src/applications/editors/price-modifiers-editor/price-modifiers-editor-shell.svelte b/src/applications/editors/price-modifiers-editor/price-modifiers-editor-shell.svelte index 3eea2113..bd89f33f 100644 --- a/src/applications/editors/price-modifiers-editor/price-modifiers-editor-shell.svelte +++ b/src/applications/editors/price-modifiers-editor/price-modifiers-editor-shell.svelte @@ -1,80 +1,80 @@ @@ -87,7 +87,7 @@

{localize("ITEM-PILES.Applications.PriceModifiersEditor.Explanation")}

+ on:drop={dropData}> {#if $priceModifiers.length} @@ -107,7 +107,7 @@
{priceData.actor.name} + on:click={(priceData.actor.sheet.render(true, { bypassItemPiles: true }))}>{priceData.actor.name}
diff --git a/src/applications/editors/price-modifiers-editor/price-modifiers-editor.js b/src/applications/editors/price-modifiers-editor/price-modifiers-editor.js index 7f7c6eeb..4e0f4ff2 100644 --- a/src/applications/editors/price-modifiers-editor/price-modifiers-editor.js +++ b/src/applications/editors/price-modifiers-editor/price-modifiers-editor.js @@ -2,13 +2,13 @@ import PriceModifiersEditorShell from './price-modifiers-editor-shell.svelte'; import Editor from "../Editor.js"; export default class PriceModifiersEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: game.i18n.localize("ITEM-PILES.Applications.PriceModifiersEditor.Title"), - width: 600, - svelte: { - class: PriceModifiersEditorShell - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: game.i18n.localize("ITEM-PILES.Applications.PriceModifiersEditor.Title"), + width: 600, + svelte: { + class: PriceModifiersEditorShell + } + }) + } } diff --git a/src/applications/editors/price-preset-editor/price-preset-editor-shell.svelte b/src/applications/editors/price-preset-editor/price-preset-editor-shell.svelte index 74cb769c..4024c220 100644 --- a/src/applications/editors/price-preset-editor/price-preset-editor-shell.svelte +++ b/src/applications/editors/price-preset-editor/price-preset-editor-shell.svelte @@ -1,25 +1,25 @@ diff --git a/src/applications/editors/price-preset-editor/price-preset-editor.js b/src/applications/editors/price-preset-editor/price-preset-editor.js index be699c7b..9128ed80 100644 --- a/src/applications/editors/price-preset-editor/price-preset-editor.js +++ b/src/applications/editors/price-preset-editor/price-preset-editor.js @@ -2,14 +2,14 @@ import PricePresetEditorShell from "./price-preset-editor-shell.svelte"; import Editor from "../Editor.js"; export default class PricePresetEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - id: `item-pile-price-preset-editor-${randomID()}`, - title: game.i18n.format("ITEM-PILES.Applications.PricePresetEditor.Title"), - width: 500, - svelte: { - class: PricePresetEditorShell, - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: `item-pile-price-preset-editor-${randomID()}`, + title: game.i18n.format("ITEM-PILES.Applications.PricePresetEditor.Title"), + width: 500, + svelte: { + class: PricePresetEditorShell, + } + }) + } } diff --git a/src/applications/editors/string-list-editor/string-list-editor.js b/src/applications/editors/string-list-editor/string-list-editor.js index b6b7c757..7fef3983 100644 --- a/src/applications/editors/string-list-editor/string-list-editor.js +++ b/src/applications/editors/string-list-editor/string-list-editor.js @@ -2,11 +2,11 @@ import StringListEditorShell from './string-list-editor.svelte'; import Editor from "../Editor.js"; export default class StringListEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - svelte: { - class: StringListEditorShell - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + svelte: { + class: StringListEditorShell + } + }) + } } diff --git a/src/applications/editors/string-list-editor/string-list-editor.svelte b/src/applications/editors/string-list-editor/string-list-editor.svelte index 222e93d9..667bbfe8 100644 --- a/src/applications/editors/string-list-editor/string-list-editor.svelte +++ b/src/applications/editors/string-list-editor/string-list-editor.svelte @@ -1,45 +1,45 @@ diff --git a/src/applications/editors/styles-editor/styles-editor.js b/src/applications/editors/styles-editor/styles-editor.js index dc0684a3..361150ed 100644 --- a/src/applications/editors/styles-editor/styles-editor.js +++ b/src/applications/editors/styles-editor/styles-editor.js @@ -2,12 +2,12 @@ import StylesEditorShell from './styles-editor.svelte'; import Editor from "../Editor.js"; export default class StylesEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: game.i18n.localize("ITEM-PILES.Applications.StylesEditor.Title"), - svelte: { - class: StylesEditorShell, - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: game.i18n.localize("ITEM-PILES.Applications.StylesEditor.Title"), + svelte: { + class: StylesEditorShell, + } + }) + } } diff --git a/src/applications/editors/styles-editor/styles-editor.svelte b/src/applications/editors/styles-editor/styles-editor.svelte index a077b809..ba051ca2 100644 --- a/src/applications/editors/styles-editor/styles-editor.svelte +++ b/src/applications/editors/styles-editor/styles-editor.svelte @@ -1,56 +1,56 @@ @@ -61,12 +61,12 @@
+ style="display: grid; grid-template-columns: 1.25fr 2fr {options.readOnly ? '' : 'auto'}; gap: 5px;"> {localize("ITEM-PILES.Applications.StylesEditor." + (options.variables ? "Variable" : "Style"))} {localize("ITEM-PILES.Applications.StylesEditor.Value")} {#if !options.readOnly} add()}> + on:click={() => add()}> {/if} diff --git a/src/applications/editors/unstackable-item-types-editor/unstackable-item-types-editor.js b/src/applications/editors/unstackable-item-types-editor/unstackable-item-types-editor.js index 45ba78bf..c64df4a0 100644 --- a/src/applications/editors/unstackable-item-types-editor/unstackable-item-types-editor.js +++ b/src/applications/editors/unstackable-item-types-editor/unstackable-item-types-editor.js @@ -3,13 +3,13 @@ import Editor from "../Editor.js"; export default class UnstackableItemTypesEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: game.i18n.localize("ITEM-PILES.Applications.UnstackableItemTypesEditor.Title"), - width: 300, - svelte: { - class: UnstackableItemTypesEditorShell, - }, - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: game.i18n.localize("ITEM-PILES.Applications.UnstackableItemTypesEditor.Title"), + width: 300, + svelte: { + class: UnstackableItemTypesEditorShell, + }, + }) + } } diff --git a/src/applications/editors/unstackable-item-types-editor/unstackable-item-types-editor.svelte b/src/applications/editors/unstackable-item-types-editor/unstackable-item-types-editor.svelte index 1deedffa..05c78bec 100644 --- a/src/applications/editors/unstackable-item-types-editor/unstackable-item-types-editor.svelte +++ b/src/applications/editors/unstackable-item-types-editor/unstackable-item-types-editor.svelte @@ -1,51 +1,51 @@ diff --git a/src/applications/editors/vault-access-editor/vault-access-editor-shell.svelte b/src/applications/editors/vault-access-editor/vault-access-editor-shell.svelte index ce06223c..6c9318af 100644 --- a/src/applications/editors/vault-access-editor/vault-access-editor-shell.svelte +++ b/src/applications/editors/vault-access-editor/vault-access-editor-shell.svelte @@ -1,83 +1,83 @@ diff --git a/src/applications/editors/vault-access-editor/vault-access-editor.js b/src/applications/editors/vault-access-editor/vault-access-editor.js index 69f3aadc..9fa234f7 100644 --- a/src/applications/editors/vault-access-editor/vault-access-editor.js +++ b/src/applications/editors/vault-access-editor/vault-access-editor.js @@ -2,13 +2,13 @@ import VaultAccessEditorShell from './vault-access-editor-shell.svelte'; import Editor from "../Editor.js"; export default class VaultAccessEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: "ITEM-PILES.Applications.VaultAccessEditor.Title", - width: 600, - svelte: { - class: VaultAccessEditorShell - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: "ITEM-PILES.Applications.VaultAccessEditor.Title", + width: 600, + svelte: { + class: VaultAccessEditorShell + } + }) + } } diff --git a/src/applications/editors/vault-styles-editor/StyleEntry.svelte b/src/applications/editors/vault-styles-editor/StyleEntry.svelte index b0ba78b0..af2d7b5e 100644 --- a/src/applications/editors/vault-styles-editor/StyleEntry.svelte +++ b/src/applications/editors/vault-styles-editor/StyleEntry.svelte @@ -1,34 +1,34 @@ diff --git a/src/applications/editors/vault-styles-editor/vault-styles-editor.js b/src/applications/editors/vault-styles-editor/vault-styles-editor.js index 91ccae5d..188da635 100644 --- a/src/applications/editors/vault-styles-editor/vault-styles-editor.js +++ b/src/applications/editors/vault-styles-editor/vault-styles-editor.js @@ -3,12 +3,12 @@ import Editor from "../Editor.js"; export default class VaultStylesEditor extends Editor { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: game.i18n.localize("ITEM-PILES.Applications.VaultStylesEditor.Title"), - svelte: { - class: VaultStylesEditorShell, - } - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: game.i18n.localize("ITEM-PILES.Applications.VaultStylesEditor.Title"), + svelte: { + class: VaultStylesEditorShell, + } + }) + } } diff --git a/src/applications/editors/vault-styles-editor/vault-styles-editor.svelte b/src/applications/editors/vault-styles-editor/vault-styles-editor.svelte index e0f6f977..16c91694 100644 --- a/src/applications/editors/vault-styles-editor/vault-styles-editor.svelte +++ b/src/applications/editors/vault-styles-editor/vault-styles-editor.svelte @@ -1,50 +1,50 @@ diff --git a/src/applications/item-editor/ItemPriceStore.js b/src/applications/item-editor/ItemPriceStore.js index 15a1167b..5e12de7f 100644 --- a/src/applications/item-editor/ItemPriceStore.js +++ b/src/applications/item-editor/ItemPriceStore.js @@ -8,68 +8,68 @@ const existingStores = new Map(); export default class ItemPriceStore { - constructor(item) { + constructor(item) { - this.item = item; - this.itemDoc = new TJSDocument(this.item); + this.item = item; + this.itemDoc = new TJSDocument(this.item); - const quantityForPriceProp = game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE; + const quantityForPriceProp = game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE; - this.price = writable(0); - this.quantityForPrice = writable(getProperty(item, quantityForPriceProp) ?? 1); + this.price = writable(0); + this.quantityForPrice = writable(getProperty(item, quantityForPriceProp) ?? 1); - const data = PileUtilities.getItemFlagData(this.item); + const data = PileUtilities.getItemFlagData(this.item); - data.prices.forEach(group => { - group.forEach(price => { - if (!price.id) { - price.id = randomID(); - } - }); - }); + data.prices.forEach(group => { + group.forEach(price => { + if (!price.id) { + price.id = randomID(); + } + }); + }); - this.data = writable(data); + this.data = writable(data); - this.itemDoc.subscribe((item, changes) => { - const { data } = changes; - if (hasProperty(data, CONSTANTS.FLAGS.ITEM)) { - const newData = getProperty(data, CONSTANTS.FLAGS.ITEM); - const oldData = get(this.data); - this.data.set(foundry.utils.mergeObject(oldData, newData)); - } - this.price.set(getItemCost(this.item)); - const quantityForPriceProp = game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE; - if (quantityForPriceProp && hasProperty(data, quantityForPriceProp)) { - this.quantityForPrice.set(getProperty(item, quantityForPriceProp)) - } - }); + this.itemDoc.subscribe((item, changes) => { + const { data } = changes; + if (hasProperty(data, CONSTANTS.FLAGS.ITEM)) { + const newData = getProperty(data, CONSTANTS.FLAGS.ITEM); + const oldData = get(this.data); + this.data.set(foundry.utils.mergeObject(oldData, newData)); + } + this.price.set(getItemCost(this.item)); + const quantityForPriceProp = game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE; + if (quantityForPriceProp && hasProperty(data, quantityForPriceProp)) { + this.quantityForPrice.set(getProperty(item, quantityForPriceProp)) + } + }); - } + } - static make(item) { - if (existingStores.has(item.id)) { - return existingStores.get(item.id); - } - return new this(item); - } + static make(item) { + if (existingStores.has(item.id)) { + return existingStores.get(item.id); + } + return new this(item); + } - removeGroup(groupIndex) { - const data = get(this.data); - data.prices.splice(groupIndex, 1); - this.data.set(data); - } + removeGroup(groupIndex) { + const data = get(this.data); + data.prices.splice(groupIndex, 1); + this.data.set(data); + } - export() { - const data = { - data: { - [game.itempiles.API.ITEM_PRICE_ATTRIBUTE]: get(this.price), - }, - flags: get(this.data) - }; - if (game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE) { - data["data"][game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE] = get(this.quantityForPrice); - } - return data; - } + export() { + const data = { + data: { + [game.itempiles.API.ITEM_PRICE_ATTRIBUTE]: get(this.price), + }, + flags: get(this.data) + }; + if (game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE) { + data["data"][game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE] = get(this.quantityForPrice); + } + return data; + } } diff --git a/src/applications/item-editor/item-editor-shell.svelte b/src/applications/item-editor/item-editor-shell.svelte index 8607bb50..b6b1bd7a 100644 --- a/src/applications/item-editor/item-editor-shell.svelte +++ b/src/applications/item-editor/item-editor-shell.svelte @@ -1,59 +1,59 @@ @@ -288,7 +288,7 @@
+ bind:value={itemFlagData.addsCols}/> x
diff --git a/src/applications/item-editor/item-editor.js b/src/applications/item-editor/item-editor.js index a23bf257..cee44f18 100644 --- a/src/applications/item-editor/item-editor.js +++ b/src/applications/item-editor/item-editor.js @@ -4,40 +4,40 @@ import { getActiveApps } from '../../helpers/helpers'; export default class ItemEditor extends SvelteApplication { - constructor(item = false, options) { - super({ - id: `item-pile-item-editor-${item.id}-${randomID()}`, - title: game.i18n.format("ITEM-PILES.Applications.ItemEditor.Title", { item_name: item.name }), - svelte: { - class: ItemEditorShell, - target: document.body, - props: { - item - } - }, - close: () => this.options.resolve(null), - ...options - }); - } + constructor(item = false, options) { + super({ + id: `item-pile-item-editor-${item.id}-${randomID()}`, + title: game.i18n.format("ITEM-PILES.Applications.ItemEditor.Title", { item_name: item.name }), + svelte: { + class: ItemEditorShell, + target: document.body, + props: { + item + } + }, + close: () => this.options.resolve(null), + ...options + }); + } - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - width: 500, - height: "auto", - classes: ["item-piles-app"] - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + width: 500, + height: "auto", + classes: ["item-piles-app"] + }) + } - static getActiveApp(id) { - return getActiveApps(`item-pile-item-editor-${id}`, true); - } + static getActiveApp(id) { + return getActiveApps(`item-pile-item-editor-${id}`, true); + } - static async show(item = false, options = {}, dialogData = {}) { - const app = this.getActiveApp(item.uuid); - if (app) return app.render(false, { focus: true }); - return new Promise((resolve) => { - options.resolve = resolve; - new this(item, options, dialogData).render(true, { focus: true }); - }) - } + static async show(item = false, options = {}, dialogData = {}) { + const app = this.getActiveApp(item.uuid); + if (app) return app.render(false, { focus: true }); + return new Promise((resolve) => { + options.resolve = resolve; + new this(item, options, dialogData).render(true, { focus: true }); + }) + } } diff --git a/src/applications/item-pile-config/item-pile-config.js b/src/applications/item-pile-config/item-pile-config.js index 72cb5c92..92ba9c1f 100644 --- a/src/applications/item-pile-config/item-pile-config.js +++ b/src/applications/item-pile-config/item-pile-config.js @@ -5,52 +5,52 @@ import { getActiveApps } from "../../helpers/helpers.js"; export default class ItemPileConfig extends SvelteApplication { - constructor(pileActor, options = {}) { + constructor(pileActor, options = {}) { - super({ - id: `item-pile-config-${pileActor.id}-${randomID()}`, - title: game.i18n.format("ITEM-PILES.Applications.ItemPileConfig.Title", { actor_name: pileActor.name }), - svelte: { - class: ItemPileConfigShell, - target: document.body, - props: { - pileActor - } - }, - close: () => this.options.resolve?.(null), - ...options - }); - } + super({ + id: `item-pile-config-${pileActor.id}-${randomID()}`, + title: game.i18n.format("ITEM-PILES.Applications.ItemPileConfig.Title", { actor_name: pileActor.name }), + svelte: { + class: ItemPileConfigShell, + target: document.body, + props: { + pileActor + } + }, + close: () => this.options.resolve?.(null), + ...options + }); + } - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - width: 430, - height: 627, - classes: ["item-piles-config", "item-piles-app"], - resizable: true - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + width: 430, + height: 627, + classes: ["item-piles-config", "item-piles-app"], + resizable: true + }) + } - static getActiveApp(id) { - return getActiveApps(`item-pile-config-${id}`, true) - } + static getActiveApp(id) { + return getActiveApps(`item-pile-config-${id}`, true) + } - static async show(target, options = {}, dialogData = {}) { - const targetActor = Utilities.getActor(target); - const app = this.getActiveApp(targetActor.uuid); - if (app) return app.render(false, { focus: true }); - return new Promise((resolve) => { - options.resolve = resolve; - new this(targetActor, options, dialogData).render(true); - }) - } + static async show(target, options = {}, dialogData = {}) { + const targetActor = Utilities.getActor(target); + const app = this.getActiveApp(targetActor.uuid); + if (app) return app.render(false, { focus: true }); + return new Promise((resolve) => { + options.resolve = resolve; + new this(targetActor, options, dialogData).render(true); + }) + } - async close(options) { - Object.values(ui.windows).forEach(app => { - if (app !== this && app.rendered && app.options?.parentApp === this) { - app.close(); - } - }) - return super.close(options); - } + async close(options) { + Object.values(ui.windows).forEach(app => { + if (app !== this && app.rendered && app.options?.parentApp === this) { + app.close(); + } + }) + return super.close(options); + } } diff --git a/src/applications/item-pile-config/item-pile-config.svelte b/src/applications/item-pile-config/item-pile-config.svelte index e99c290a..b86acb92 100644 --- a/src/applications/item-pile-config/item-pile-config.svelte +++ b/src/applications/item-pile-config/item-pile-config.svelte @@ -1,143 +1,143 @@ @@ -146,7 +146,7 @@ + on:submit|once|preventDefault={updateSettings}> diff --git a/src/applications/item-pile-config/settings/container.svelte b/src/applications/item-pile-config/settings/container.svelte index a7a85517..be5913df 100644 --- a/src/applications/item-pile-config/settings/container.svelte +++ b/src/applications/item-pile-config/settings/container.svelte @@ -1,9 +1,9 @@ diff --git a/src/applications/item-pile-config/settings/custom.svelte b/src/applications/item-pile-config/settings/custom.svelte index c2553a89..62008234 100644 --- a/src/applications/item-pile-config/settings/custom.svelte +++ b/src/applications/item-pile-config/settings/custom.svelte @@ -2,6 +2,7 @@ import CONSTANTS from "../../../constants/constants.js"; import { localize } from "@typhonjs-fvtt/runtime/svelte/helper"; + import DropZone from "../../components/DropZone.svelte"; export let pileData; @@ -12,6 +13,24 @@ } } + async function handleDropData(dropData, key, data) { + if (!data.type.implementation) { + return; + } + const doc = await data.type.implementation.fromDropData(dropData); + pileData[key] = { + data: doc.toObject(), + uuid: dropData.uuid + } + } + + async function previewDocument(key) { + if (!pileData[key].uuid) return; + const doc = fromUuidSync(pileData[key].uuid); + if (!doc) return; + doc.sheet.render(true); + } + @@ -25,7 +44,28 @@ {/if} {#if data.type === String} - + {#if data.options} + + {:else} + + {/if} + {:else if data.type === Item} + handleDropData(dropData, key, data)}> +
+ + { previewDocument(key); }}> + {pileData[key]?.data?.name ?? "Drop item to add"} + + { pileData[key] = {}; }} + class="fas fa-times drop-item-remove item-piles-clickable-red item-piles-clickable-link" + > +
+
{:else if data.type === Number} {:else if data.type === Boolean} @@ -35,3 +75,32 @@
{/each} + + diff --git a/src/applications/item-pile-config/settings/itempile.svelte b/src/applications/item-pile-config/settings/itempile.svelte index d7b1cea1..5e9277f1 100644 --- a/src/applications/item-pile-config/settings/itempile.svelte +++ b/src/applications/item-pile-config/settings/itempile.svelte @@ -1,8 +1,8 @@ @@ -37,11 +37,11 @@ {localize("ITEM-PILES.Applications.ItemPileConfig.SingleItem.Scale")} + disabled="{!pileData.overrideSingleItemScale}" max="3" min="0.2" step="0.01" + style="flex:3;" type="range"/> + disabled="{!pileData.overrideSingleItemScale}" step="0.01" + style="flex:0.5; margin-left:1rem;" type="number"/> diff --git a/src/applications/item-pile-config/settings/main.svelte b/src/applications/item-pile-config/settings/main.svelte index d7f90045..b9fbc0c9 100644 --- a/src/applications/item-pile-config/settings/main.svelte +++ b/src/applications/item-pile-config/settings/main.svelte @@ -1,78 +1,78 @@ @@ -139,6 +139,36 @@ +
+ + +
+ +
+ +
+ +
+
+
+ + +
+
+ style="text-align: right;" + type="number"/> : + type="number"/>
@@ -315,11 +323,11 @@
+ style="text-align: right;" + type="number"/> : + type="number"/>
@@ -337,9 +345,9 @@
{ + bind:checked={weekday.selected} + disabled={!pileData.openTimes.enabled} + on:change={() => { let weekdaySet = new Set(pileData.closedDays); if(weekday.selected){ weekdaySet.add(weekday.name) @@ -364,9 +372,9 @@ {#each holidays as holiday, index (holiday.name + "-" + index)}
{ + bind:checked={holiday.selected} + disabled={!pileData.openTimes.enabled} + on:change={() => { let holidaySet = new Set(pileData.closedHolidays); if(holiday.selected){ holidaySet.add(holiday.name) @@ -408,9 +416,9 @@
{ + bind:checked={weekday.selected} + disabled={!pileData.openTimes.enabled} + on:change={() => { let weekdaySet = new Set(pileData.refreshItemsDays); if(weekday.selected){ weekdaySet.add(weekday.name) @@ -435,9 +443,9 @@ {#each refreshItemsHolidays as holiday, index (holiday.name + "-remove-" + index)}
{ + bind:checked={holiday.selected} + disabled={!pileData.openTimes.enabled} + on:change={() => { let holidaySet = new Set(pileData.refreshItemsHolidays); if(holiday.selected){ holidaySet.add(holiday.name) diff --git a/src/applications/item-pile-config/settings/sharing.svelte b/src/applications/item-pile-config/settings/sharing.svelte index a14aaf98..b1fc21c5 100644 --- a/src/applications/item-pile-config/settings/sharing.svelte +++ b/src/applications/item-pile-config/settings/sharing.svelte @@ -1,35 +1,35 @@ @@ -86,7 +86,7 @@
diff --git a/src/applications/item-pile-config/settings/vault.svelte b/src/applications/item-pile-config/settings/vault.svelte index 07194eca..ba79b4e6 100644 --- a/src/applications/item-pile-config/settings/vault.svelte +++ b/src/applications/item-pile-config/settings/vault.svelte @@ -1,41 +1,41 @@ @@ -57,14 +57,6 @@
-
- - -
-
+ style="text-align: right;" type="number"/> x + type="number"/>
diff --git a/src/applications/item-pile-inventory-app/CategorizedItemList.svelte b/src/applications/item-pile-inventory-app/CategorizedItemList.svelte index 27f04677..820089b8 100644 --- a/src/applications/item-pile-inventory-app/CategorizedItemList.svelte +++ b/src/applications/item-pile-inventory-app/CategorizedItemList.svelte @@ -1,13 +1,13 @@ diff --git a/src/applications/item-pile-inventory-app/CurrencyList.svelte b/src/applications/item-pile-inventory-app/CurrencyList.svelte index ce1de602..7e72cb7a 100644 --- a/src/applications/item-pile-inventory-app/CurrencyList.svelte +++ b/src/applications/item-pile-inventory-app/CurrencyList.svelte @@ -1,13 +1,13 @@ @@ -18,7 +18,7 @@

{localize("ITEM-PILES.Currencies")}:

{/if} store.addCurrency(store.recipient)}> + on:click={() => store.addCurrency(store.recipient)}> {localize("ITEM-PILES.Inspect.AddCurrency")} diff --git a/src/applications/item-pile-inventory-app/ItemList.svelte b/src/applications/item-pile-inventory-app/ItemList.svelte index 7e928183..1b0c78a4 100644 --- a/src/applications/item-pile-inventory-app/ItemList.svelte +++ b/src/applications/item-pile-inventory-app/ItemList.svelte @@ -1,12 +1,12 @@ diff --git a/src/applications/item-pile-inventory-app/ListEntry.svelte b/src/applications/item-pile-inventory-app/ListEntry.svelte index 941bf65d..5e6f5bd5 100644 --- a/src/applications/item-pile-inventory-app/ListEntry.svelte +++ b/src/applications/item-pile-inventory-app/ListEntry.svelte @@ -1,38 +1,38 @@
{ dragStart(event) }} - transition:fade={{duration: 250}}> + class:item-piles-disabled={!$editQuantities && (!$quantityLeft || !$quantity)} + draggable={!!entry.id} + on:dragstart={(event) => { dragStart(event) }} + transition:fade={{duration: 250}}>
@@ -41,8 +41,8 @@

{ entry.preview() }} - style="color: {$rarityColor || 'inherit'};" + on:click={() => { entry.preview() }} + style="color: {$rarityColor || 'inherit'};" > {$name}

@@ -60,7 +60,7 @@
+ draggable="true" on:dragstart|stopPropagation|preventDefault/>
{:else} @@ -68,7 +68,7 @@ {#if $quantityLeft && $quantity}
+ max="{$quantity}" disabled="{!$quantity}"/> / {$quantityLeft} diff --git a/src/applications/item-pile-inventory-app/item-pile-inventory-app.js b/src/applications/item-pile-inventory-app/item-pile-inventory-app.js index bbb09d9b..575d7fdd 100644 --- a/src/applications/item-pile-inventory-app/item-pile-inventory-app.js +++ b/src/applications/item-pile-inventory-app/item-pile-inventory-app.js @@ -9,127 +9,127 @@ import CONSTANTS from "../../constants/constants.js"; export default class ItemPileInventoryApp extends SvelteApplication { - /** - * - * @param actor - * @param recipient - * @param overrides - * @param options - * @param dialogData - */ - constructor(actor, recipient, options = {}, dialogData = {}) { - super({ - id: `item-pile-inventory-${actor?.token?.id ?? actor.id}-${randomID()}`, - title: actor.name, - svelte: { - class: ItemPileInventoryShell, - target: document.body, - props: { - actor, - recipient - } - }, - zIndex: 100, - ...options - }, dialogData); - - this.actor = actor; - this.recipient = recipient; - - Helpers.hooks.callAll(CONSTANTS.HOOKS.OPEN_INTERFACE, this, actor, recipient, options, dialogData); - - } - - /** @inheritdoc */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - closeOnSubmit: false, - classes: ["app", "window-app", "sheet", "item-pile-inventory", "item-piles", "item-piles-app"], - width: 550, - height: "auto", - }); - } - - static getActiveApps(id) { - return Helpers.getActiveApps(`item-pile-inventory-${id}`); - } - - static async show(source, recipient = false, options = {}, dialogData = {}) { - source = Utilities.getActor(source); - recipient = Utilities.getActor(recipient); - const result = Helpers.hooks.call(CONSTANTS.HOOKS.PRE_OPEN_INTERFACE, source, recipient, options, dialogData); - if (result === false) return; - const apps = this.getActiveApps(source?.token?.uuid ?? source.uuid); - if (apps.length) { - for (let app of apps) { - app.render(false, { focus: true }); - } - return; - } - return new Promise((resolve) => { - options.resolve = resolve; - new this(source, recipient, options, dialogData).render(true, { focus: true, bypassItemPiles: true }); - }) - } - - async close(options) { - const result = Helpers.hooks.call(CONSTANTS.HOOKS.PRE_CLOSE_INTERFACE, this, this.actor, this.recipient, options); - if (result === false) return; - Helpers.hooks.callAll(CONSTANTS.HOOKS.CLOSE_INTERFACE, this, this.actor, this.recipient, options); - return super.close(options); - } - - /* -------------------------------------------- */ - - /** @override */ - _getHeaderButtons() { - let buttons = super._getHeaderButtons(); - - const newButtons = []; - - if (this.actor.isOwner) { - newButtons.push({ - label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.Inspect.OpenSheet" : "", - class: "item-piles-open-actor-sheet", - icon: "fas fa-user", - onclick: () => { - this.actor.sheet.render(true, { focus: true, bypassItemPiles: true }); - } - } - ); - } - - if (game.user.isGM) { - newButtons.push({ - label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.ContextMenu.ShowToPlayers" : "", - class: "item-piles-show-to-players", - icon: "fas fa-eye", - onclick: async (event) => { - const activeUsers = Array.from(game.users).filter(u => u.active && u !== game.user).map(u => u.id); - if (!activeUsers.length) { - return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Warnings.NoPlayersActive"), true); - } - const users = event.altKey ? activeUsers : await UserSelectDialog.show({ excludeSelf: true }); - if (!users || !users.length) return; - Helpers.custom_notify(game.i18n.format("ITEM-PILES.Notifications.ShownToPlayers", { actor_name: this.actor.name })) - return game.itempiles.API.renderItemPileInterface(this.actor, { - userIds: users, - useDefaultCharacter: true - }); - } - }, - { - label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.HUD.Configure" : "", - class: "item-piles-configure-pile", - icon: "fas fa-box-open", - onclick: () => { - ItemPileConfig.show(this.actor); - } - } - ); - } - - return newButtons.concat(buttons) - } + /** + * + * @param actor + * @param recipient + * @param overrides + * @param options + * @param dialogData + */ + constructor(actor, recipient, options = {}, dialogData = {}) { + super({ + id: `item-pile-inventory-${actor?.token?.id ?? actor.id}-${randomID()}`, + title: actor.name, + svelte: { + class: ItemPileInventoryShell, + target: document.body, + props: { + actor, + recipient + } + }, + zIndex: 100, + ...options + }, dialogData); + + this.actor = actor; + this.recipient = recipient; + + Helpers.hooks.callAll(CONSTANTS.HOOKS.OPEN_INTERFACE, this, actor, recipient, options, dialogData); + + } + + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + closeOnSubmit: false, + classes: ["app", "window-app", "sheet", "item-pile-inventory", "item-piles", "item-piles-app"], + width: 550, + height: "auto", + }); + } + + static getActiveApps(id) { + return Helpers.getActiveApps(`item-pile-inventory-${id}`); + } + + static async show(source, recipient = false, options = {}, dialogData = {}) { + source = Utilities.getActor(source); + recipient = Utilities.getActor(recipient); + const result = Helpers.hooks.call(CONSTANTS.HOOKS.PRE_OPEN_INTERFACE, source, recipient, options, dialogData); + if (result === false) return; + const apps = this.getActiveApps(source?.token?.uuid ?? source.uuid); + if (apps.length) { + for (let app of apps) { + app.render(false, { focus: true }); + } + return; + } + return new Promise((resolve) => { + options.resolve = resolve; + new this(source, recipient, options, dialogData).render(true, { focus: true, bypassItemPiles: true }); + }) + } + + async close(options) { + const result = Helpers.hooks.call(CONSTANTS.HOOKS.PRE_CLOSE_INTERFACE, this, this.actor, this.recipient, options); + if (result === false) return; + Helpers.hooks.callAll(CONSTANTS.HOOKS.CLOSE_INTERFACE, this, this.actor, this.recipient, options); + return super.close(options); + } + + /* -------------------------------------------- */ + + /** @override */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + + const newButtons = []; + + if (this.actor.isOwner) { + newButtons.push({ + label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.Inspect.OpenSheet" : "", + class: "item-piles-open-actor-sheet", + icon: "fas fa-user", + onclick: () => { + this.actor.sheet.render(true, { focus: true, bypassItemPiles: true }); + } + } + ); + } + + if (game.user.isGM) { + newButtons.push({ + label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.ContextMenu.ShowToPlayers" : "", + class: "item-piles-show-to-players", + icon: "fas fa-eye", + onclick: async (event) => { + const activeUsers = Array.from(game.users).filter(u => u.active && u !== game.user).map(u => u.id); + if (!activeUsers.length) { + return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Warnings.NoPlayersActive"), true); + } + const users = event.altKey ? activeUsers : await UserSelectDialog.show({ excludeSelf: true }); + if (!users || !users.length) return; + Helpers.custom_notify(game.i18n.format("ITEM-PILES.Notifications.ShownToPlayers", { actor_name: this.actor.name })) + return game.itempiles.API.renderItemPileInterface(this.actor, { + userIds: users, + useDefaultCharacter: true + }); + } + }, + { + label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.HUD.Configure" : "", + class: "item-piles-configure-pile", + icon: "fas fa-box-open", + onclick: () => { + ItemPileConfig.show(this.actor); + } + } + ); + } + + return newButtons.concat(buttons) + } } diff --git a/src/applications/item-pile-inventory-app/item-pile-inventory-shell.svelte b/src/applications/item-pile-inventory-app/item-pile-inventory-shell.svelte index be59be90..de6a935c 100644 --- a/src/applications/item-pile-inventory-app/item-pile-inventory-shell.svelte +++ b/src/applications/item-pile-inventory-app/item-pile-inventory-shell.svelte @@ -1,116 +1,116 @@ @@ -121,7 +121,7 @@
+ on:drop={dropData}> {#if $deleted}

@@ -133,7 +133,7 @@ {#if showSearchBar}

+ style="margin-bottom: 0.5rem; align-items: center;" transition:fade={{duration: 250}}>
diff --git a/src/applications/merchant-app/MerchantActivityLog.svelte b/src/applications/merchant-app/MerchantActivityLog.svelte index a92024f9..c4c5747a 100644 --- a/src/applications/merchant-app/MerchantActivityLog.svelte +++ b/src/applications/merchant-app/MerchantActivityLog.svelte @@ -1,28 +1,28 @@
+ style="margin: 0.25rem 0; align-items: center; flex: 0 1 auto;"> - +
+ style="max-height: {$applicationHeight-130}px; overflow-y: scroll; font-size: 0.75rem; padding-right: 0.5rem;"> {#each $logStore.slice(0, $visibleLogItems).filter(log => log.visible) as log, index (index)}
diff --git a/src/applications/merchant-app/MerchantFooter.svelte b/src/applications/merchant-app/MerchantFooter.svelte index ed7b89dd..ae7398e9 100644 --- a/src/applications/merchant-app/MerchantFooter.svelte +++ b/src/applications/merchant-app/MerchantFooter.svelte @@ -1,20 +1,20 @@ @@ -24,20 +24,20 @@ {localize("ITEM-PILES.Merchant.ShoppingAs", { actorName: $recipientDocument.name })}
+ options={{ abbreviations: false, imgSize: 18, abbreviateNumbers: true }} + class="item-piles-currency-list"/>
{:else if game.user.isGM && !$merchantPileData.infiniteCurrencies}
+ options={{ abbreviations: false, imgSize: 18, abbreviateNumbers: true }} + class="item-piles-currency-list"/>
{/if} diff --git a/src/applications/merchant-app/MerchantItemEntry.svelte b/src/applications/merchant-app/MerchantItemEntry.svelte index 1a12c2c6..6adff609 100644 --- a/src/applications/merchant-app/MerchantItemEntry.svelte +++ b/src/applications/merchant-app/MerchantItemEntry.svelte @@ -1,37 +1,37 @@
+ class:item-piles-child-even-color={index%2===0} + class:item-piles-child-odd-color={index%2===1} + class:merchant-item-hidden={itemFlagData.hidden} + style="flex: 1 0 auto;" + transition:fade|local={{duration: 250}}> {#each columns as column} {#if column?.data} diff --git a/src/applications/merchant-app/MerchantItemTab.svelte b/src/applications/merchant-app/MerchantItemTab.svelte index 2508ba24..d9c34977 100644 --- a/src/applications/merchant-app/MerchantItemTab.svelte +++ b/src/applications/merchant-app/MerchantItemTab.svelte @@ -1,44 +1,44 @@
@@ -76,8 +76,8 @@ }}> {@html column.label}
@@ -104,8 +104,8 @@ }}> {@html column.label}
diff --git a/src/applications/merchant-app/MerchantLeftPane.svelte b/src/applications/merchant-app/MerchantLeftPane.svelte index f81fd62c..90e9c937 100644 --- a/src/applications/merchant-app/MerchantLeftPane.svelte +++ b/src/applications/merchant-app/MerchantLeftPane.svelte @@ -1,48 +1,48 @@ @@ -69,8 +69,8 @@ /> {#if game.user.isGM && !description} {/if}
diff --git a/src/applications/merchant-app/MerchantPopulateItemsTab.svelte b/src/applications/merchant-app/MerchantPopulateItemsTab.svelte index df55a7b2..2b10de49 100644 --- a/src/applications/merchant-app/MerchantPopulateItemsTab.svelte +++ b/src/applications/merchant-app/MerchantPopulateItemsTab.svelte @@ -1,275 +1,275 @@ @@ -349,43 +349,43 @@
{localize("ITEM-PILES.Merchant.RollableTables")}
{#each $populationTables as table}
+ style="min-height: 28px; padding: 3px 3px 3px 5px;">
{$tables[table.uuid].name}
{#if table.open}
+ transition:slide={{ duration: 200, easing: quintOut }}>
@@ -394,21 +394,21 @@
+ for={"table-id-"+table.uuid}>{localize("ITEM-PILES.Merchant.TableAddAllItems")} { + bind:checked={table.addAll} + on:change={() => { if(!table.addAll) return; table.items = Object.fromEntries($tables[table.uuid].items.map(item => [item.id, "1d4"])); }} - type="checkbox"/> + type="checkbox"/>
{#if !table.addAll}
{/if} @@ -471,11 +471,11 @@
@@ -547,9 +547,9 @@ diff --git a/src/applications/merchant-app/MerchantRightPane.svelte b/src/applications/merchant-app/MerchantRightPane.svelte index c4c543a9..e4c09c3d 100644 --- a/src/applications/merchant-app/MerchantRightPane.svelte +++ b/src/applications/merchant-app/MerchantRightPane.svelte @@ -1,28 +1,28 @@
+ style="flex: 1; max-height: calc(100% - {recipientStore && $currencies.length ? '34px' : '0px'})"> {#if $closed && !game.user.isGM}
diff --git a/src/applications/merchant-app/MerchantTopBar.svelte b/src/applications/merchant-app/MerchantTopBar.svelte index f3f3c7ff..58e540d1 100644 --- a/src/applications/merchant-app/MerchantTopBar.svelte +++ b/src/applications/merchant-app/MerchantTopBar.svelte @@ -1,27 +1,27 @@ @@ -37,12 +37,12 @@ {#if aboutTimeEnabled && $pileDataStore.openTimes.status !== "auto"} { store.setOpenStatus("auto"); }}> - {localize(`ITEM-PILES.Merchant.OpenCloseAuto`)} + {localize(`ITEM-PILES.Merchant.OpenCloseAuto`)} - {/if} - { store.setOpenStatus($closed ? "open" : "closed"); }}> + {/if} + { store.setOpenStatus($closed ? "open" : "closed"); }}> - {localize(`ITEM-PILES.Merchant.${!$closed ? "Open" : "Closed"}`)} + {localize(`ITEM-PILES.Merchant.${!$closed ? "Open" : "Closed"}`)} {/if} {openTimeText} diff --git a/src/applications/merchant-app/components/CategoryHeader.svelte b/src/applications/merchant-app/components/CategoryHeader.svelte index bef99e52..4f6a28d8 100644 --- a/src/applications/merchant-app/components/CategoryHeader.svelte +++ b/src/applications/merchant-app/components/CategoryHeader.svelte @@ -1,24 +1,24 @@ @@ -35,10 +35,10 @@
{#if $priceModifiersPerType[type]} { store.removeOverrideTypePrice(type) }}> + on:click={() => { store.removeOverrideTypePrice(type) }}> {:else} { store.addOverrideTypePrice(type) }}> + on:click={() => { store.addOverrideTypePrice(type) }}> {/if}
{/if} diff --git a/src/applications/merchant-app/components/CustomColumn.svelte b/src/applications/merchant-app/components/CustomColumn.svelte index 0c13772a..09ff7541 100644 --- a/src/applications/merchant-app/components/CustomColumn.svelte +++ b/src/applications/merchant-app/components/CustomColumn.svelte @@ -1,19 +1,19 @@ diff --git a/src/applications/merchant-app/components/EntryButtons.svelte b/src/applications/merchant-app/components/EntryButtons.svelte index e24d204c..c1da405b 100644 --- a/src/applications/merchant-app/components/EntryButtons.svelte +++ b/src/applications/merchant-app/components/EntryButtons.svelte @@ -1,20 +1,20 @@ @@ -26,34 +26,34 @@ {/if} { item.toggleProperty("hidden"); }}> + on:click={() => { item.toggleProperty("hidden"); }}> { item.toggleProperty("notForSale"); }}> + on:click={() => { item.toggleProperty("notForSale"); }}> + class:fa-store-slash={$itemFlagDataStore.notForSale}> {/if} {#if displayBuyButton} {#if isMerchant} { + class:item-piles-clickable-link={!$itemFlagDataStore.notForSale || game.user.isGM} + class:item-piles-clickable-link-disabled={$quantity <= 0 || ($itemFlagDataStore.notForSale && !game.user.isGM)} + class:buy-button={displayControlButtons} + on:click={() => { if($quantity <= 0 || ($itemFlagDataStore.notForSale && !game.user.isGM)) return; store.tradeItem(item) }}> - {#if !displayControlButtons} {localize("ITEM-PILES.Merchant.Buy")}{/if} + {#if !displayControlButtons} {localize("ITEM-PILES.Merchant.Buy")}{/if} {:else} 0 && !$itemFlagDataStore.cantBeSoldToMerchants} - class:item-piles-clickable-link-disabled={$quantity <= 0 || $itemFlagDataStore.cantBeSoldToMerchants} - on:click={() => { + style="margin-left: 0.25rem;" + class:item-piles-clickable-link={$quantity > 0 && !$itemFlagDataStore.cantBeSoldToMerchants} + class:item-piles-clickable-link-disabled={$quantity <= 0 || $itemFlagDataStore.cantBeSoldToMerchants} + on:click={() => { if(($quantity <= 0 || $itemFlagDataStore.cantBeSoldToMerchants)) return; store.tradeItem(item, true) }}> diff --git a/src/applications/merchant-app/components/ItemEntry.svelte b/src/applications/merchant-app/components/ItemEntry.svelte index bfba6235..91348242 100644 --- a/src/applications/merchant-app/components/ItemEntry.svelte +++ b/src/applications/merchant-app/components/ItemEntry.svelte @@ -1,37 +1,37 @@
+ class:merchant-item-hidden={itemFlagData.hidden}>
- export let item; - export let showX = false; + export let item; + export let showX = false; - const store = item.store; - const displayQuantityStore = item.displayQuantity; - const infiniteQuantityStore = item.infiniteQuantity; - const quantityStore = item.quantity; + const store = item.store; + const displayQuantityStore = item.displayQuantity; + const infiniteQuantityStore = item.infiniteQuantity; + const quantityStore = item.quantity; - let showEditQuantity = false; - $: editQuantity = $quantityStore; + let showEditQuantity = false; + $: editQuantity = $quantityStore; @@ -33,11 +33,11 @@ /> {:else if !showEditQuantity} { + class:item-piles-clickable-link={game.user.isGM} + on:click={() => { if (game.user.isGM) showEditQuantity = true; }}>{showX ? "x" : ""}{$quantityStore} + > {/if} {/if}
diff --git a/src/applications/merchant-app/merchant-app-shell.svelte b/src/applications/merchant-app/merchant-app-shell.svelte index 069858d6..da9969be 100644 --- a/src/applications/merchant-app/merchant-app-shell.svelte +++ b/src/applications/merchant-app/merchant-app-shell.svelte @@ -1,113 +1,113 @@ @@ -119,10 +119,10 @@
{#if $activeTab !== "tables"} diff --git a/src/applications/merchant-app/merchant-app.js b/src/applications/merchant-app/merchant-app.js index e25736d2..110e9ae1 100644 --- a/src/applications/merchant-app/merchant-app.js +++ b/src/applications/merchant-app/merchant-app.js @@ -9,119 +9,119 @@ import * as Utilities from "../../helpers/utilities.js"; export default class MerchantApp extends SvelteApplication { - constructor(merchant, recipient = false, options = {}, dialogData = {}) { - super({ - title: `Merchant: ${merchant.name}`, - id: `item-pile-merchant-${merchant.id}-${randomID()}`, - svelte: { - class: MerchantAppShell, - target: document.body, - props: { - merchant, - recipient - } - }, - zIndex: 100, - ...options - }, dialogData); - this.merchant = merchant; - this.recipient = recipient; - Helpers.hooks.callAll(CONSTANTS.HOOKS.OPEN_INTERFACE, this, merchant, recipient, options, dialogData); - } + constructor(merchant, recipient = false, options = {}, dialogData = {}) { + super({ + title: `Merchant: ${merchant.name}`, + id: `item-pile-merchant-${merchant.id}-${randomID()}`, + svelte: { + class: MerchantAppShell, + target: document.body, + props: { + merchant, + recipient + } + }, + zIndex: 100, + ...options + }, dialogData); + this.merchant = merchant; + this.recipient = recipient; + Helpers.hooks.callAll(CONSTANTS.HOOKS.OPEN_INTERFACE, this, merchant, recipient, options, dialogData); + } - /** @inheritdoc */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["app window-app sheet", "item-piles-merchant-sheet", "item-piles", "item-piles-app"], - width: 800, - height: 700, - closeOnSubmit: false, - resizable: true - }); - } + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["app window-app sheet", "item-piles-merchant-sheet", "item-piles", "item-piles-app"], + width: 800, + height: 700, + closeOnSubmit: false, + resizable: true + }); + } - static getActiveApp(id) { - return Helpers.getActiveApps(`item-pile-merchant-${id}`, true); - } + static getActiveApp(id) { + return Helpers.getActiveApps(`item-pile-merchant-${id}`, true); + } - static async show(merchant, recipient = false, options = {}, dialogData = {}) { - const merchantActor = Utilities.getActor(merchant); - const recipientActor = Utilities.getActor(recipient); - const result = Helpers.hooks.call(CONSTANTS.HOOKS.PRE_OPEN_INTERFACE, merchantActor, recipientActor, options, dialogData); - if (result === false) return; - const app = this.getActiveApp(merchant.uuid); - if (app) return app.render(false, { focus: true }); - return new Promise((resolve) => { - options.resolve = resolve; - new this(merchant, recipientActor, options, dialogData).render(true, { focus: true, bypassItemPiles: true }); - }) - } + static async show(merchant, recipient = false, options = {}, dialogData = {}) { + const merchantActor = Utilities.getActor(merchant); + const recipientActor = Utilities.getActor(recipient); + const result = Helpers.hooks.call(CONSTANTS.HOOKS.PRE_OPEN_INTERFACE, merchantActor, recipientActor, options, dialogData); + if (result === false) return; + const app = this.getActiveApp(merchant.uuid); + if (app) return app.render(false, { focus: true }); + return new Promise((resolve) => { + options.resolve = resolve; + new this(merchant, recipientActor, options, dialogData).render(true, { focus: true, bypassItemPiles: true }); + }) + } - refreshItems() { - this.svelte.applicationShell.store.refreshItems(); - } + refreshItems() { + this.svelte.applicationShell.store.refreshItems(); + } - /** @override */ - _getHeaderButtons() { - let buttons = super._getHeaderButtons(); - const newButtons = []; + /** @override */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + const newButtons = []; - if (this.merchant.isOwner) { - newButtons.push( - { - label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.Inspect.OpenSheet" : "", - class: "item-piles-open-actor-sheet", - icon: "fas fa-user", - onclick: () => { - this.merchant.sheet.render(true, { focus: true, bypassItemPiles: true }); - } - } - ) - } + if (this.merchant.isOwner) { + newButtons.push( + { + label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.Inspect.OpenSheet" : "", + class: "item-piles-open-actor-sheet", + icon: "fas fa-user", + onclick: () => { + this.merchant.sheet.render(true, { focus: true, bypassItemPiles: true }); + } + } + ) + } - if (game.user.isGM) { - newButtons.push( - { - label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.ContextMenu.ShowToPlayers" : "", - class: "item-piles-show-to-players", - icon: "fas fa-eye", - onclick: async (event) => { - const activeUsers = Array.from(game.users).filter(u => u.active && u !== game.user).map(u => u.id); - if (!activeUsers.length) { - return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Warnings.NoPlayersActive"), true); - } - const users = event.altKey ? activeUsers : await UserSelectDialog.show({ excludeSelf: true }); - if (!users || !users.length) return; - Helpers.custom_notify(game.i18n.format("ITEM-PILES.Notifications.ShownToPlayers", { actor_name: this.merchant.name })) - return game.itempiles.API.renderItemPileInterface(this.merchant, { - userIds: users, - useDefaultCharacter: true - }); - } - }, - { - label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.HUD.Configure" : "", - class: "item-piles-configure-pile", - icon: "fas fa-box-open", - onclick: () => { - ItemPileConfig.show(this.merchant); - } - } - ); - } - return newButtons.concat(buttons); - } + if (game.user.isGM) { + newButtons.push( + { + label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.ContextMenu.ShowToPlayers" : "", + class: "item-piles-show-to-players", + icon: "fas fa-eye", + onclick: async (event) => { + const activeUsers = Array.from(game.users).filter(u => u.active && u !== game.user).map(u => u.id); + if (!activeUsers.length) { + return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Warnings.NoPlayersActive"), true); + } + const users = event.altKey ? activeUsers : await UserSelectDialog.show({ excludeSelf: true }); + if (!users || !users.length) return; + Helpers.custom_notify(game.i18n.format("ITEM-PILES.Notifications.ShownToPlayers", { actor_name: this.merchant.name })) + return game.itempiles.API.renderItemPileInterface(this.merchant, { + userIds: users, + useDefaultCharacter: true + }); + } + }, + { + label: !Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "ITEM-PILES.HUD.Configure" : "", + class: "item-piles-configure-pile", + icon: "fas fa-box-open", + onclick: () => { + ItemPileConfig.show(this.merchant); + } + } + ); + } + return newButtons.concat(buttons); + } - async close(options) { - const result = Helpers.hooks.call(CONSTANTS.HOOKS.PRE_CLOSE_INTERFACE, this, this.merchant, this.recipient); - if (result === false) return; - for (const app of Object.values(ui.windows)) { - if (app !== this && this.svelte.applicationShell.store === app?.svelte?.applicationShell?.store) { - app.close(); - } - } - Helpers.hooks.callAll(CONSTANTS.HOOKS.CLOSE_INTERFACE, this, this.merchant, this.recipient); - return super.close(options); - } + async close(options) { + const result = Helpers.hooks.call(CONSTANTS.HOOKS.PRE_CLOSE_INTERFACE, this, this.merchant, this.recipient); + if (result === false) return; + for (const app of Object.values(ui.windows)) { + if (app !== this && this.svelte.applicationShell.store === app?.svelte?.applicationShell?.store) { + app.close(); + } + } + Helpers.hooks.callAll(CONSTANTS.HOOKS.CLOSE_INTERFACE, this, this.merchant, this.recipient); + return super.close(options); + } } diff --git a/src/applications/settings-app/Setting.svelte b/src/applications/settings-app/Setting.svelte index 033caa40..a17fc65c 100644 --- a/src/applications/settings-app/Setting.svelte +++ b/src/applications/settings-app/Setting.svelte @@ -1,9 +1,9 @@ @@ -12,7 +12,7 @@

{localize(data.hint)}

diff --git a/src/applications/settings-app/SettingButton.svelte b/src/applications/settings-app/SettingButton.svelte index 1202bfc7..bf7b4c44 100644 --- a/src/applications/settings-app/SettingButton.svelte +++ b/src/applications/settings-app/SettingButton.svelte @@ -1,42 +1,42 @@ diff --git a/src/applications/settings-app/settings-app.js b/src/applications/settings-app/settings-app.js index 3c8ea696..45d188d4 100644 --- a/src/applications/settings-app/settings-app.js +++ b/src/applications/settings-app/settings-app.js @@ -5,115 +5,115 @@ import SETTINGS from "../../constants/settings.js"; class SettingsApp extends SvelteApplication { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - id: `item-piles-application-system-settings-${randomID()}`, - title: "Item Piles Module Configuration", - width: 600, - svelte: { - class: SettingsShell, - target: document.body - }, - zIndex: 100, - classes: ["item-piles-app"], - }); - } - - static getActiveApp() { - return getActiveApps("item-piles-application-system-settings", true); - } - - static async show(options = {}, dialogData = {}) { - const app = this.getActiveApp() - if (app) return app.render(false, { focus: true }); - return new Promise((resolve) => { - options.resolve = resolve; - new this(options, dialogData).render(true, { focus: true }); - }) - } - - - /** @override */ - _getHeaderButtons() { - let buttons = super._getHeaderButtons(); - if (game.user.isGM) { - buttons = [ - { - label: "ITEM-PILES.Applications.Settings.Export", - class: "item-piles-export-settings", - icon: "fas fa-file-export", - onclick: () => { - const settingKeys = Object.fromEntries(Object.entries(SETTINGS) - .filter(([_, value]) => typeof value === "string") - .map(([key, value]) => [value, key])); - const settings = Object.entries(this.svelte.applicationShell.settings) - .filter(([_, setting]) => { - return setting.system && setting.name; - }).map(([key, setting]) => { - return [settingKeys[key], setting.value]; - }); - const a = document.createElement("a"); - const file = new Blob([JSON.stringify(Object.fromEntries(settings), null, 4)], { type: "text/json" }); - a.href = URL.createObjectURL(file); - a.download = `item-piles-${game.system.id}.json`; - a.click(); - a.remove(); - } - }, - { - label: "ITEM-PILES.Applications.Settings.Import", - class: "item-piles-import-settings", - icon: "fas fa-file-import", - onclick: () => { - - const input = document.createElement('input'); - input.type = 'file'; - - input.onchange = e => { - - input.remove(); - - // getting a hold of the file reference - const file = e.target.files[0]; - - const reader = new FileReader(); - reader.addEventListener('load', async () => { - try { - const incomingSettings = JSON.parse(reader.result); - this.svelte.applicationShell.importSettings(incomingSettings) - } catch (err) { - console.error(err); - } - }); - - reader.readAsText(file); - - } - - input.click(); - } - }, - ].concat(buttons); - } - return buttons - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: `item-piles-application-system-settings-${randomID()}`, + title: "Item Piles Module Configuration", + width: 600, + svelte: { + class: SettingsShell, + target: document.body + }, + zIndex: 100, + classes: ["item-piles-app"], + }); + } + + static getActiveApp() { + return getActiveApps("item-piles-application-system-settings", true); + } + + static async show(options = {}, dialogData = {}) { + const app = this.getActiveApp() + if (app) return app.render(false, { focus: true }); + return new Promise((resolve) => { + options.resolve = resolve; + new this(options, dialogData).render(true, { focus: true }); + }) + } + + + /** @override */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + if (game.user.isGM) { + buttons = [ + { + label: "ITEM-PILES.Applications.Settings.Export", + class: "item-piles-export-settings", + icon: "fas fa-file-export", + onclick: () => { + const settingKeys = Object.fromEntries(Object.entries(SETTINGS) + .filter(([_, value]) => typeof value === "string") + .map(([key, value]) => [value, key])); + const settings = Object.entries(this.svelte.applicationShell.settings) + .filter(([_, setting]) => { + return setting.system && setting.name; + }).map(([key, setting]) => { + return [settingKeys[key], setting.value]; + }); + const a = document.createElement("a"); + const file = new Blob([JSON.stringify(Object.fromEntries(settings), null, 4)], { type: "text/json" }); + a.href = URL.createObjectURL(file); + a.download = `item-piles-${game.system.id}.json`; + a.click(); + a.remove(); + } + }, + { + label: "ITEM-PILES.Applications.Settings.Import", + class: "item-piles-import-settings", + icon: "fas fa-file-import", + onclick: () => { + + const input = document.createElement('input'); + input.type = 'file'; + + input.onchange = e => { + + input.remove(); + + // getting a hold of the file reference + const file = e.target.files[0]; + + const reader = new FileReader(); + reader.addEventListener('load', async () => { + try { + const incomingSettings = JSON.parse(reader.result); + this.svelte.applicationShell.importSettings(incomingSettings) + } catch (err) { + console.error(err); + } + }); + + reader.readAsText(file); + + } + + input.click(); + } + }, + ].concat(buttons); + } + return buttons + } } export default class SettingsShim extends FormApplication { - /** - * @inheritDoc - */ - constructor() { - super({}); - SettingsApp.show(); - } + /** + * @inheritDoc + */ + constructor() { + super({}); + SettingsApp.show(); + } - async _updateObject(event, formData) { - } + async _updateObject(event, formData) { + } - render() { - this.close(); - } + render() { + this.close(); + } } diff --git a/src/applications/settings-app/settings-shell.svelte b/src/applications/settings-app/settings-shell.svelte index 7f698722..10876c97 100644 --- a/src/applications/settings-app/settings-shell.svelte +++ b/src/applications/settings-app/settings-shell.svelte @@ -1,107 +1,107 @@ @@ -132,8 +132,8 @@

{localize("ITEM-PILES.Applications.Settings.Request")}

@@ -183,13 +183,13 @@ getSettings(); }}/> + options={game.system.template.Actor.types}/> + disabled="{settings[SETTINGS.CURRENCIES].value.length !== 1}"/> diff --git a/src/applications/trade-dialogs/ActorDropSelect.svelte b/src/applications/trade-dialogs/ActorDropSelect.svelte index b03378d5..e2071974 100644 --- a/src/applications/trade-dialogs/ActorDropSelect.svelte +++ b/src/applications/trade-dialogs/ActorDropSelect.svelte @@ -1,57 +1,57 @@

0} - on:dragenter={dragEnter} - on:dragleave={dragLeave} - on:dragover={preventDefault} - on:dragstart={preventDefault} - on:drop={dropData} + class:item-piles-box-highlight={counter > 0} + on:dragenter={dragEnter} + on:dragleave={dragLeave} + on:dragover={preventDefault} + on:dragstart={preventDefault} + on:drop={dropData} > {#if actor}
@@ -63,16 +63,16 @@ {#if changingActor} {:else} { changingActor = true }}>{ actor.name } + on:click={() => { changingActor = true }}>{ actor.name } {/if} {:else} diff --git a/src/applications/trade-dialogs/trade-dialog-prompt.svelte b/src/applications/trade-dialogs/trade-dialog-prompt.svelte index 03eee64a..83582269 100644 --- a/src/applications/trade-dialogs/trade-dialog-prompt.svelte +++ b/src/applications/trade-dialogs/trade-dialog-prompt.svelte @@ -1,34 +1,34 @@ diff --git a/src/applications/trade-dialogs/trade-dialog-request.svelte b/src/applications/trade-dialogs/trade-dialog-request.svelte index 94b69578..e396c9a8 100644 --- a/src/applications/trade-dialogs/trade-dialog-request.svelte +++ b/src/applications/trade-dialogs/trade-dialog-request.svelte @@ -1,83 +1,83 @@ @@ -98,14 +98,14 @@
{#if isPrivate}

{@html localize("ITEM-PILES.Trade.Request.PrivateContent", { - trading_user_name: tradingUser.name, - trading_actor_name: tradingActor.name - })}

+ trading_user_name: tradingUser.name, + trading_actor_name: tradingActor.name + })}

{:else}

{localize("ITEM-PILES.Trade.Request.Content", { - trading_user_name: tradingUser.name, - trading_actor_name: tradingActor.name - })}

+ trading_user_name: tradingUser.name, + trading_actor_name: tradingActor.name + })}

{/if}

{localize("ITEM-PILES.Trade.Request.AcceptQuery")}

diff --git a/src/applications/trade-dialogs/trade-dialogs.js b/src/applications/trade-dialogs/trade-dialogs.js index d361a17d..6e75696f 100644 --- a/src/applications/trade-dialogs/trade-dialogs.js +++ b/src/applications/trade-dialogs/trade-dialogs.js @@ -4,79 +4,79 @@ import TradeDialogRequest from './trade-dialog-request.svelte'; export class TradePromptDialog extends SvelteApplication { - constructor(tradeOptions, options = {}) { - super({ - svelte: { - class: TradeDialogPrompt, - target: document.body, - props: { - ...tradeOptions - } - }, - close: () => this.options.resolve?.(null), - ...options - }); - } + constructor(tradeOptions, options = {}) { + super({ + svelte: { + class: TradeDialogPrompt, + target: document.body, + props: { + ...tradeOptions + } + }, + close: () => this.options.resolve?.(null), + ...options + }); + } - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: game.i18n.localize("ITEM-PILES.Trade.Title"), - width: 400, - height: "auto", - classes: ["dialog", "item-piles-app"], - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: game.i18n.localize("ITEM-PILES.Trade.Title"), + width: 400, + height: "auto", + classes: ["dialog", "item-piles-app"], + }) + } - static show(tradeOptions, options = {}, dialogData = {}) { - return new Promise(resolve => { - options.resolve = resolve; - new this(tradeOptions, options, dialogData).render(true); - }) - } + static show(tradeOptions, options = {}, dialogData = {}) { + return new Promise(resolve => { + options.resolve = resolve; + new this(tradeOptions, options, dialogData).render(true); + }) + } } export class TradeRequestDialog extends SvelteApplication { - constructor(tradeOptions, options = {}) { + constructor(tradeOptions, options = {}) { - super({ - svelte: { - class: TradeDialogRequest, - target: document.body, - props: { - ...tradeOptions - } - }, - close: () => this.options.resolve?.(null), - ...options - }); - this.tradeId = tradeOptions.tradeId; - } + super({ + svelte: { + class: TradeDialogRequest, + target: document.body, + props: { + ...tradeOptions + } + }, + close: () => this.options.resolve?.(null), + ...options + }); + this.tradeId = tradeOptions.tradeId; + } - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - title: game.i18n.localize("ITEM-PILES.Trade.Title"), - width: 400, - height: "auto", - classes: ["dialog"], - }) - } + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: game.i18n.localize("ITEM-PILES.Trade.Title"), + width: 400, + height: "auto", + classes: ["dialog"], + }) + } - static show(tradeOptions, options = {}) { - return new Promise(resolve => { - options.resolve = resolve; - new this(tradeOptions, options).render(true); - }) - } + static show(tradeOptions, options = {}) { + return new Promise(resolve => { + options.resolve = resolve; + new this(tradeOptions, options).render(true); + }) + } - static cancel(tradeId) { - for (const app of Object.values(ui.windows)) { - if (app instanceof this && app.tradeId === tradeId) { - app.options.resolve({ type: "cancelled" }); - return app.close(); - } - } - return false; - } + static cancel(tradeId) { + for (const app of Object.values(ui.windows)) { + if (app instanceof this && app.tradeId === tradeId) { + app.options.resolve({ type: "cancelled" }); + return app.close(); + } + } + return false; + } } diff --git a/src/applications/trading-app/TradeEntry.svelte b/src/applications/trading-app/TradeEntry.svelte index 88ea3e6b..2ade47c8 100644 --- a/src/applications/trading-app/TradeEntry.svelte +++ b/src/applications/trading-app/TradeEntry.svelte @@ -1,40 +1,40 @@ @@ -73,7 +73,7 @@ {/if}
evt.stopPropagation()}> + on:click={(evt) => evt.stopPropagation()}> {#if editable}
{#if data.editing} @@ -87,7 +87,7 @@
{:else} + on:click="{ () => { data.editing = true } }"> {data.quantity} {/if} diff --git a/src/applications/trading-app/trade-store.js b/src/applications/trading-app/trade-store.js index 5430a409..c10748e7 100644 --- a/src/applications/trading-app/trade-store.js +++ b/src/applications/trading-app/trade-store.js @@ -4,241 +4,241 @@ import * as PileUtilities from "../../helpers/pile-utilities.js"; export default class TradeStore { - constructor(instigator, leftTrader, rightTrader, publicTradeId, privateTradeId = false, isPrivate = false) { - - this.instigator = instigator; - this.publicTradeId = publicTradeId; - this.privateTradeId = privateTradeId; - this.isPrivate = isPrivate; - - this.leftTraderUser = leftTrader.user; - this.leftTraderActor = leftTrader.actor; - this.leftTraderItems = writable(leftTrader.items ?? []); - this.leftTraderCurrencies = writable(leftTrader.currencies ?? []); - this.leftTraderItemCurrencies = writable(leftTrader.itemCurrencies ?? []); - this.leftTraderAccepted = writable(leftTrader.accepted ?? false); - - this.rightTraderUser = rightTrader.user; - this.rightTraderActor = rightTrader.actor; - this.rightTraderItems = writable(rightTrader.items ?? []); - this.rightTraderCurrencies = writable(rightTrader.currencies ?? []); - this.rightTraderItemCurrencies = writable(rightTrader.itemCurrencies ?? []); - this.rightTraderAccepted = writable(rightTrader?.accepted ?? false); - - } - - get isUserParticipant() { - return game.user === this.leftTraderUser || game.user === this.rightTraderUser; - } - - get tradeIsAccepted() { - return get(this.leftTraderAccepted) && get(this.rightTraderAccepted); - } - - static import(tradeData) { - - const leftTrader = { - user: game.users.get(tradeData.leftTraderData.user), - actor: fromUuidSync(tradeData.leftTraderData.actor), - items: tradeData.leftTraderData.items, - currencies: tradeData.leftTraderData.currencies, - itemCurrencies: tradeData.leftTraderData.itemCurrencies, - accepted: tradeData.leftTraderData.accepted - }; - - const rightTrader = { - user: game.users.get(tradeData.rightTraderData.user), - actor: fromUuidSync(tradeData.rightTraderData.actor), - items: tradeData.rightTraderData.items, - currencies: tradeData.rightTraderData.currencies, - itemCurrencies: tradeData.rightTraderData.itemCurrencies, - accepted: tradeData.rightTraderData.accepted - }; - - return new this(tradeData.instigator, leftTrader, rightTrader, tradeData.publicTradeId); - } - - export() { - return { - instigator: this.instigator, - publicTradeId: this.publicTradeId, - leftTraderData: { - user: this.leftTraderUser.id, - actor: Utilities.getUuid(this.leftTraderActor), - items: get(this.leftTraderItems), - currencies: get(this.leftTraderCurrencies), - itemCurrencies: get(this.leftTraderItemCurrencies), - accepted: get(this.leftTraderAccepted) - }, - rightTraderData: { - user: this.rightTraderUser.id, - actor: Utilities.getUuid(this.rightTraderActor), - items: get(this.rightTraderItems), - currencies: get(this.rightTraderCurrencies), - itemCurrencies: get(this.rightTraderItemCurrencies), - accepted: get(this.rightTraderAccepted) - } - } - } - - getTradeData() { - return { - sourceActor: this.leftTraderActor, - targetActor: this.rightTraderActor, - remove: { - items: get(this.leftTraderItems).concat(get(this.leftTraderItemCurrencies)), - attributes: get(this.leftTraderCurrencies) - }, add: { - items: get(this.rightTraderItems).concat(get(this.rightTraderItemCurrencies)), - attributes: get(this.rightTraderCurrencies) - } - }; - } - - getExistingCurrencies() { - return [...get(this.leftTraderCurrencies), ...get(this.leftTraderItemCurrencies)] - } - - async toggleAccepted() { - this.leftTraderAccepted.set(!get(this.leftTraderAccepted)); - } - - updateItems(userId, inItems) { - if (userId === game.user.id) return; - this.leftTraderAccepted.set(false); - this.rightTraderAccepted.set(false); - if (userId === this.leftTraderUser.id) { - this.leftTraderItems.set(inItems) - } - if (userId === this.rightTraderUser.id) { - this.rightTraderItems.set(inItems) - } - } - - updateItemCurrencies(userId, itemCurrencies) { - if (userId === game.user.id) return; - this.leftTraderAccepted.set(false); - this.rightTraderAccepted.set(false); - if (userId === this.leftTraderUser.id) { - this.leftTraderItemCurrencies.set(itemCurrencies) - } - if (userId === this.rightTraderUser.id) { - this.rightTraderItemCurrencies.set(itemCurrencies) - } - } - - updateCurrencies(userId, inCurrencies) { - if (userId === game.user.id) return; - this.leftTraderAccepted.set(false); - this.rightTraderAccepted.set(false); - if (userId === this.leftTraderUser.id) { - this.leftTraderCurrencies.set(inCurrencies) - } - if (userId === this.rightTraderUser.id) { - this.rightTraderCurrencies.set(inCurrencies) - } - } - - updateAcceptedState(userId, state) { - if (userId === game.user.id) return; - if (userId === this.leftTraderUser.id) { - this.leftTraderAccepted.set(state); - } - if (userId === this.rightTraderUser.id) { - this.rightTraderAccepted.set(state); - } - } - - addItem(newItem, { uuid = false, quantity = false, currency = false } = {}) { - - const items = !currency - ? get(this.leftTraderItems) - : get(this.leftTraderItemCurrencies); - - const item = Utilities.findSimilarItem(items, newItem) - - const maxQuantity = game.user.isGM ? Infinity : Utilities.getItemQuantity(newItem); - - if (item && PileUtilities.canItemStack(item)) { - if (item.quantity >= maxQuantity) return; - if (quantity) { - item.quantity = Math.min(quantity ? quantity : item.quantity + 1, maxQuantity); - item.newQuantity = item.quantity; - item.maxQuantity = maxQuantity; - } else { - items.splice(items.indexOf(item)); - } - } else if (!item && quantity) { - items.push({ - id: newItem._id ?? newItem.id, - uuid: uuid, - name: newItem.name, - img: newItem?.img ?? "", - type: newItem?.type, - currency: currency, - quantity: quantity ? quantity : 1, - newQuantity: quantity ? quantity : 1, - maxQuantity: maxQuantity, - data: newItem instanceof Item ? newItem.toObject() : newItem - }) - } - - if (!currency) { - this.leftTraderItems.set(items); - } else { - this.leftTraderItemCurrencies.set(items); - } - - } - - addAttribute(newCurrency) { - - const currencies = get(this.leftTraderCurrencies); - - const existingCurrency = currencies.find(currency => currency.path === newCurrency.path); - - if (existingCurrency) { - existingCurrency.quantity = newCurrency.quantity; - existingCurrency.newQuantity = newCurrency.quantity; - } else { - currencies.push(newCurrency); - } - - currencies.sort((a, b) => a.index - b.index); - - this.leftTraderCurrencies.set(currencies); - - } - - removeEntry(entry) { - - if (entry.id) { + constructor(instigator, leftTrader, rightTrader, publicTradeId, privateTradeId = false, isPrivate = false) { + + this.instigator = instigator; + this.publicTradeId = publicTradeId; + this.privateTradeId = privateTradeId; + this.isPrivate = isPrivate; + + this.leftTraderUser = leftTrader.user; + this.leftTraderActor = leftTrader.actor; + this.leftTraderItems = writable(leftTrader.items ?? []); + this.leftTraderCurrencies = writable(leftTrader.currencies ?? []); + this.leftTraderItemCurrencies = writable(leftTrader.itemCurrencies ?? []); + this.leftTraderAccepted = writable(leftTrader.accepted ?? false); + + this.rightTraderUser = rightTrader.user; + this.rightTraderActor = rightTrader.actor; + this.rightTraderItems = writable(rightTrader.items ?? []); + this.rightTraderCurrencies = writable(rightTrader.currencies ?? []); + this.rightTraderItemCurrencies = writable(rightTrader.itemCurrencies ?? []); + this.rightTraderAccepted = writable(rightTrader?.accepted ?? false); + + } + + get isUserParticipant() { + return game.user === this.leftTraderUser || game.user === this.rightTraderUser; + } + + get tradeIsAccepted() { + return get(this.leftTraderAccepted) && get(this.rightTraderAccepted); + } + + static import(tradeData) { + + const leftTrader = { + user: game.users.get(tradeData.leftTraderData.user), + actor: fromUuidSync(tradeData.leftTraderData.actor), + items: tradeData.leftTraderData.items, + currencies: tradeData.leftTraderData.currencies, + itemCurrencies: tradeData.leftTraderData.itemCurrencies, + accepted: tradeData.leftTraderData.accepted + }; + + const rightTrader = { + user: game.users.get(tradeData.rightTraderData.user), + actor: fromUuidSync(tradeData.rightTraderData.actor), + items: tradeData.rightTraderData.items, + currencies: tradeData.rightTraderData.currencies, + itemCurrencies: tradeData.rightTraderData.itemCurrencies, + accepted: tradeData.rightTraderData.accepted + }; + + return new this(tradeData.instigator, leftTrader, rightTrader, tradeData.publicTradeId); + } + + export() { + return { + instigator: this.instigator, + publicTradeId: this.publicTradeId, + leftTraderData: { + user: this.leftTraderUser.id, + actor: Utilities.getUuid(this.leftTraderActor), + items: get(this.leftTraderItems), + currencies: get(this.leftTraderCurrencies), + itemCurrencies: get(this.leftTraderItemCurrencies), + accepted: get(this.leftTraderAccepted) + }, + rightTraderData: { + user: this.rightTraderUser.id, + actor: Utilities.getUuid(this.rightTraderActor), + items: get(this.rightTraderItems), + currencies: get(this.rightTraderCurrencies), + itemCurrencies: get(this.rightTraderItemCurrencies), + accepted: get(this.rightTraderAccepted) + } + } + } + + getTradeData() { + return { + sourceActor: this.leftTraderActor, + targetActor: this.rightTraderActor, + remove: { + items: get(this.leftTraderItems).concat(get(this.leftTraderItemCurrencies)), + attributes: get(this.leftTraderCurrencies) + }, add: { + items: get(this.rightTraderItems).concat(get(this.rightTraderItemCurrencies)), + attributes: get(this.rightTraderCurrencies) + } + }; + } + + getExistingCurrencies() { + return [...get(this.leftTraderCurrencies), ...get(this.leftTraderItemCurrencies)] + } + + async toggleAccepted() { + this.leftTraderAccepted.set(!get(this.leftTraderAccepted)); + } + + updateItems(userId, inItems) { + if (userId === game.user.id) return; + this.leftTraderAccepted.set(false); + this.rightTraderAccepted.set(false); + if (userId === this.leftTraderUser.id) { + this.leftTraderItems.set(inItems) + } + if (userId === this.rightTraderUser.id) { + this.rightTraderItems.set(inItems) + } + } + + updateItemCurrencies(userId, itemCurrencies) { + if (userId === game.user.id) return; + this.leftTraderAccepted.set(false); + this.rightTraderAccepted.set(false); + if (userId === this.leftTraderUser.id) { + this.leftTraderItemCurrencies.set(itemCurrencies) + } + if (userId === this.rightTraderUser.id) { + this.rightTraderItemCurrencies.set(itemCurrencies) + } + } + + updateCurrencies(userId, inCurrencies) { + if (userId === game.user.id) return; + this.leftTraderAccepted.set(false); + this.rightTraderAccepted.set(false); + if (userId === this.leftTraderUser.id) { + this.leftTraderCurrencies.set(inCurrencies) + } + if (userId === this.rightTraderUser.id) { + this.rightTraderCurrencies.set(inCurrencies) + } + } + + updateAcceptedState(userId, state) { + if (userId === game.user.id) return; + if (userId === this.leftTraderUser.id) { + this.leftTraderAccepted.set(state); + } + if (userId === this.rightTraderUser.id) { + this.rightTraderAccepted.set(state); + } + } + + addItem(newItem, { uuid = false, quantity = false, currency = false } = {}) { + + const items = !currency + ? get(this.leftTraderItems) + : get(this.leftTraderItemCurrencies); + + const item = Utilities.findSimilarItem(items, newItem) + + const maxQuantity = game.user.isGM ? Infinity : Utilities.getItemQuantity(newItem); + + if (item && PileUtilities.canItemStack(item)) { + if (item.quantity >= maxQuantity) return; + if (quantity) { + item.quantity = Math.min(quantity ? quantity : item.quantity + 1, maxQuantity); + item.newQuantity = item.quantity; + item.maxQuantity = maxQuantity; + } else { + items.splice(items.indexOf(item)); + } + } else if (!item && quantity) { + items.push({ + id: newItem._id ?? newItem.id, + uuid: uuid, + name: newItem.name, + img: newItem?.img ?? "", + type: newItem?.type, + currency: currency, + quantity: quantity ? quantity : 1, + newQuantity: quantity ? quantity : 1, + maxQuantity: maxQuantity, + data: newItem instanceof Item ? newItem.toObject() : newItem + }) + } + + if (!currency) { + this.leftTraderItems.set(items); + } else { + this.leftTraderItemCurrencies.set(items); + } + + } + + addAttribute(newCurrency) { + + const currencies = get(this.leftTraderCurrencies); + + const existingCurrency = currencies.find(currency => currency.path === newCurrency.path); + + if (existingCurrency) { + existingCurrency.quantity = newCurrency.quantity; + existingCurrency.newQuantity = newCurrency.quantity; + } else { + currencies.push(newCurrency); + } + + currencies.sort((a, b) => a.index - b.index); + + this.leftTraderCurrencies.set(currencies); + + } + + removeEntry(entry) { + + if (entry.id) { - if (!entry.currency) { - - const items = get(this.leftTraderItems) - .filter(item => item.id !== entry.id); + if (!entry.currency) { + + const items = get(this.leftTraderItems) + .filter(item => item.id !== entry.id); - this.leftTraderItems.set(items); + this.leftTraderItems.set(items); - } else { + } else { - const items = get(this.leftTraderItemCurrencies) - .filter(item => item.id !== entry.id); + const items = get(this.leftTraderItemCurrencies) + .filter(item => item.id !== entry.id); - this.leftTraderItemCurrencies.set(items); + this.leftTraderItemCurrencies.set(items); - } + } - } else { + } else { - const items = get(this.leftTraderCurrencies) - .filter(currency => currency.path !== entry.path); + const items = get(this.leftTraderCurrencies) + .filter(currency => currency.path !== entry.path); - this.leftTraderCurrencies.set(items); + this.leftTraderCurrencies.set(items); - } + } - } + } } diff --git a/src/applications/trading-app/trading-app-shell.svelte b/src/applications/trading-app/trading-app-shell.svelte index 6ea5a43c..2a2588c9 100644 --- a/src/applications/trading-app/trading-app-shell.svelte +++ b/src/applications/trading-app/trading-app-shell.svelte @@ -1,138 +1,138 @@ @@ -191,19 +191,19 @@ {#if systemHasCurrencies}
+ class:item-piles-top-divider={$leftCurrencies.length || $leftItemCurrencies.length}> {#if store.isUserParticipant}
{#if isGM} { addCurrency(true) }} - class="item-piles-text-right item-piles-small-text item-piles-middle item-piles-gm-add-currency"> + class="item-piles-text-right item-piles-small-text item-piles-middle item-piles-gm-add-currency"> {localize("ITEM-PILES.Trade.GMAddCurrency")} {/if} { addCurrency() }} - class="item-piles-text-right item-piles-small-text item-piles-middle item-piles-add-currency"> + class="item-piles-text-right item-piles-small-text item-piles-middle item-piles-add-currency"> {localize("ITEM-PILES.Inspect.AddCurrency")} @@ -227,7 +227,7 @@ {#if store.isUserParticipant} `) + const minimalUI = game.modules.get('minimal-ui')?.active; + const classes = "item-piles-player-list-trade-button" + (minimalUI ? " item-piles-minimal-ui" : "") + const text = !minimalUI ? " Request Trade" : "" + const button = $(``) - button.click(() => { - game.itempiles.API.requestTrade(); - }); - html.append(button); + button.click(() => { + game.itempiles.API.requestTrade(); + }); + html.append(button); } function insertActorContextMenuItems(html, menuItems) { - menuItems.push({ - name: "Item Piles: " + game.i18n.localize("ITEM-PILES.ContextMenu.ShowToPlayers"), - icon: ``, - callback: async (html) => { - const actorId = html[0].dataset.documentId; - const actor = game.actors.get(actorId); - const activeUsers = Array.from(game.users).filter(u => u.active && u !== game.user).map(u => u.id); - if (!activeUsers.length) { - return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Warnings.NoPlayersActive"), true); - } - const users = await UserSelectDialog.show(); - if (!users || !users.length) return; - Helpers.custom_notify(game.i18n.format("ITEM-PILES.Notifications.ShownToPlayers", { actor_name: actor.name })) - return game.itempiles.API.renderItemPileInterface(actor, { userIds: users, useDefaultCharacter: true }); - }, - condition: (html) => { - const actorId = html[0].dataset.documentId; - const actor = game.actors.get(actorId); - return game.user.isGM && PileUtilities.isValidItemPile(actor); - } - }, { - name: "Item Piles: " + game.i18n.localize("ITEM-PILES.ContextMenu.RequestTrade"), - icon: ``, - callback: (html) => { - const actorId = html[0].dataset.documentId; - const actor = game.actors.get(actorId); - const user = Array.from(game.users).find(u => u.character === actor && u.active); - return game.itempiles.API.requestTrade(user); - }, - condition: (html) => { - const actorId = html[0].dataset.documentId; - const actor = game.actors.get(actorId); - return Helpers.getSetting(SETTINGS.ENABLE_TRADING) - && (game.user?.character !== actor || Array.from(game.users).find(u => u.character === actor && u.active)); - } - }); + menuItems.push({ + name: "Item Piles: " + game.i18n.localize("ITEM-PILES.ContextMenu.ShowToPlayers"), + icon: ``, + callback: async (html) => { + const actorId = html[0].dataset.documentId; + const actor = game.actors.get(actorId); + const activeUsers = Array.from(game.users).filter(u => u.active && u !== game.user).map(u => u.id); + if (!activeUsers.length) { + return Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Warnings.NoPlayersActive"), true); + } + const users = await UserSelectDialog.show(); + if (!users || !users.length) return; + Helpers.custom_notify(game.i18n.format("ITEM-PILES.Notifications.ShownToPlayers", { actor_name: actor.name })) + return game.itempiles.API.renderItemPileInterface(actor, { userIds: users, useDefaultCharacter: true }); + }, + condition: (html) => { + const actorId = html[0].dataset.documentId; + const actor = game.actors.get(actorId); + return game.user.isGM && PileUtilities.isValidItemPile(actor); + } + }, { + name: "Item Piles: " + game.i18n.localize("ITEM-PILES.ContextMenu.RequestTrade"), + icon: ``, + callback: (html) => { + const actorId = html[0].dataset.documentId; + const actor = game.actors.get(actorId); + const user = Array.from(game.users).find(u => u.character === actor && u.active); + return game.itempiles.API.requestTrade(user); + }, + condition: (html) => { + const actorId = html[0].dataset.documentId; + const actor = game.actors.get(actorId); + return Helpers.getSetting(SETTINGS.ENABLE_TRADING) + && (game.user?.character !== actor || Array.from(game.users).find(u => u.character === actor && u.active)); + } + }); } function insertActorHeaderButtons(actorSheet, buttons) { - if (!game.user.isGM || Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_BUTTON)) return; + if (!game.user.isGM || Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_BUTTON)) return; - let obj = actorSheet?.object ?? actorSheet?.actor; + let obj = actorSheet?.object ?? actorSheet?.actor; - buttons.unshift({ - label: Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "" : "Configure", - icon: "fas fa-box-open", - class: "item-piles-config-button", - onclick: () => { - ItemPileConfig.show(obj); - } - }) + buttons.unshift({ + label: Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "" : "Configure", + icon: "fas fa-box-open", + class: "item-piles-config-button", + onclick: () => { + ItemPileConfig.show(obj); + } + }) } function insertItemHeaderButtons(itemSheet, buttons) { - if (!game.user.isGM || Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_BUTTON)) return; + if (!game.user.isGM || Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_BUTTON)) return; - let obj = itemSheet?.object ?? itemSheet?.item; + let obj = itemSheet?.object ?? itemSheet?.item; - buttons.unshift({ - label: Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "" : "Configure", - icon: "fas fa-box-open", - class: "item-piles-config-button", - onclick: () => { - ItemEditor.show(obj); - } - }) + buttons.unshift({ + label: Helpers.getSetting(SETTINGS.HIDE_ACTOR_HEADER_TEXT) ? "" : "Configure", + icon: "fas fa-box-open", + class: "item-piles-config-button", + onclick: () => { + ItemEditor.show(obj); + } + }) } function renderPileHUD(app, html) { - const document = app?.object?.document; + const document = app?.object?.document; - if (!document) return; + if (!document) return; - if (!PileUtilities.isValidItemPile(document)) return; + if (!PileUtilities.isValidItemPile(document)) return; - if (PileUtilities.isItemPileContainer(document)) { + if (PileUtilities.isItemPileContainer(document)) { - const pileData = PileUtilities.getActorFlagData(document); + const pileData = PileUtilities.getActorFlagData(document); - const container = $(`
`); + const container = $(`
`); - const lock_button = $(`
`); - lock_button.click(async function () { - $(this).find('.fas').toggleClass('fa-lock').toggleClass('fa-lock-open'); - await game.itempiles.API.toggleItemPileLocked(document); - }); - container.append(lock_button); + const lock_button = $(`
`); + lock_button.click(async function () { + $(this).find('.fas').toggleClass('fa-lock').toggleClass('fa-lock-open'); + await game.itempiles.API.toggleItemPileLocked(document); + }); + container.append(lock_button); - const open_button = $(`
`); - open_button.click(async function () { - $(this).find('.fas').toggleClass('fa-box').toggleClass('fa-box-open'); - await game.itempiles.API.toggleItemPileClosed(document); - }); - container.append(open_button); + const open_button = $(`
`); + open_button.click(async function () { + $(this).find('.fas').toggleClass('fa-box').toggleClass('fa-box-open'); + await game.itempiles.API.toggleItemPileClosed(document); + }); + container.append(open_button); - const configure_button = $(`
`); - configure_button.click(async function () { - ItemPileConfig.show(document); - }); - container.append(configure_button); + const configure_button = $(`
`); + configure_button.click(async function () { + ItemPileConfig.show(document); + }); + container.append(configure_button); - html.append(container) + html.append(container) - } + } } class FastTooltipManager extends TooltipManager { - /** - * An amount of margin which is used to offset tooltips from their anchored element. - * @type {number} - */ - static TOOLTIP_MARGIN_PX = 5; - /** - * The number of milliseconds delay which activates a tooltip on a "long hover". - * @type {number} - */ - static TOOLTIP_ACTIVATION_MS = 500; - /** - * The number of milliseconds delay which activates a tooltip on a "long hover". - * @type {number} - */ - static TOOLTIP_DEACTIVATION_MS = 500; - /** - * The directions in which a tooltip can extend, relative to its tool-tipped element. - * @enum {string} - */ - static TOOLTIP_DIRECTIONS = { - UP: "UP", - DOWN: "DOWN", - LEFT: "LEFT", - RIGHT: "RIGHT", - CENTER: "CENTER" - }; - /** - * A reference to the HTML element which is currently tool-tipped, if any. - * @type {HTMLElement|null} - */ - element = null; - /** - * Is the tooltip currently active? - * @type {boolean} - */ - #active = false; - /** - * A reference to a window timeout function when an element is activated. - * @private - */ - #activationTimeout; - /** - * A reference to a window timeout function when an element is deactivated. - * @private - */ - #deactivationTimeout; - /** - * An element which is pending tooltip activation if hover is sustained - * @type {HTMLElement|null} - */ - #pending; - - constructor() { - super(); - const tooltipElem = $(``); - $("body").append(tooltipElem); - this.tooltip = document.getElementById("fast-tooltip"); - } - - /* -------------------------------------------- */ - - /** - * Activate interactivity by listening for hover events on HTML elements which have a data-fast-tooltip defined. - */ - activateEventListeners() { - document.body.addEventListener("pointerenter", this.#onActivate.bind(this), true); - document.body.addEventListener("pointerleave", this.#onDeactivate.bind(this), true); - } - - /* -------------------------------------------- */ - - /** - * Handle hover events which activate a tooltipped element. - * @param {PointerEvent} event The initiating pointerenter event - */ - #onActivate(event) { - if (Tour.tourInProgress) return; // Don't activate tooltips during a tour - const element = event.target; - if (!element.dataset.fastTooltip) { - // Check if the element has moved out from underneath the cursor and pointerenter has fired on a non-child of the - // tooltipped element. - if (this.#active && this.element && !this.element.contains(element)) this.#startDeactivation(); - return; - } - - // Don't activate tooltips if the element contains an active context menu - if (element.matches("#context-menu") || element.querySelector("#context-menu")) return; - - // If the tooltip is currently active, we can move it to a new element immediately - if (this.#active) this.activate(element); - else this.#clearDeactivation(); - - // Otherwise, delay activation to determine user intent - this.#pending = element; - this.#activationTimeout = window.setTimeout(() => { - this.activate(element); - }, Number(element?.dataset?.tooltipActivationSpeed) ?? this.constructor.TOOLTIP_ACTIVATION_MS); - } - - activate(element, { text, direction, cssClass } = {}) { - - // Check if the element still exists in the DOM. - if (!document.body.contains(element)) return; - this.#clearDeactivation(); - - // Mark the element as active - this.#active = true; - this.element = element; - element.setAttribute("aria-describedby", "fast-tooltip"); - this.tooltip.innerHTML = text || game.i18n.localize(element.dataset.fastTooltip); - - // Activate display of the tooltip - this.tooltip.removeAttribute("class"); - this.tooltip.classList.add("active"); - if (cssClass) this.tooltip.classList.add(cssClass); - - // Set tooltip position - direction = direction || element.closest("[data-tooltip-direction]")?.dataset.tooltipDirection; - if (!direction) direction = this._determineDirection(); - this._setAnchor(direction); - } - - /* -------------------------------------------- */ - - /** - * Handle hover events which deactivate a tooltipped element. - * @param {PointerEvent} event The initiating pointerleave event - */ - #onDeactivate(event) { - if (event.target !== (this.element ?? this.#pending)) return; - this.#startDeactivation(); - } - - /* -------------------------------------------- */ - - /** - * Start the deactivation process. - */ - #startDeactivation() { - // Clear any existing activation workflow - window.clearTimeout(this.#activationTimeout); - this.#pending = this.#activationTimeout = null; - - // Delay deactivation to confirm whether some new element is now pending - window.clearTimeout(this.#deactivationTimeout); - this.#deactivationTimeout = window.setTimeout(() => { - if (!this.#pending) this.deactivate(); - }, Number(this.element?.dataset?.tooltipDeactivationSpeed) ?? this.constructor.TOOLTIP_DEACTIVATION_MS); - } - - /* -------------------------------------------- */ - - /** - * Clear any existing deactivation workflow. - */ - #clearDeactivation() { - window.clearTimeout(this.#deactivationTimeout); - this.#pending = this.#deactivationTimeout = null; - } + /** + * An amount of margin which is used to offset tooltips from their anchored element. + * @type {number} + */ + static TOOLTIP_MARGIN_PX = 5; + /** + * The number of milliseconds delay which activates a tooltip on a "long hover". + * @type {number} + */ + static TOOLTIP_ACTIVATION_MS = 500; + /** + * The number of milliseconds delay which activates a tooltip on a "long hover". + * @type {number} + */ + static TOOLTIP_DEACTIVATION_MS = 500; + /** + * The directions in which a tooltip can extend, relative to its tool-tipped element. + * @enum {string} + */ + static TOOLTIP_DIRECTIONS = { + UP: "UP", + DOWN: "DOWN", + LEFT: "LEFT", + RIGHT: "RIGHT", + CENTER: "CENTER" + }; + /** + * A reference to the HTML element which is currently tool-tipped, if any. + * @type {HTMLElement|null} + */ + element = null; + /** + * Is the tooltip currently active? + * @type {boolean} + */ + #active = false; + /** + * A reference to a window timeout function when an element is activated. + * @private + */ + #activationTimeout; + /** + * A reference to a window timeout function when an element is deactivated. + * @private + */ + #deactivationTimeout; + /** + * An element which is pending tooltip activation if hover is sustained + * @type {HTMLElement|null} + */ + #pending; + + constructor() { + super(); + const tooltipElem = $(``); + $("body").append(tooltipElem); + this.tooltip = document.getElementById("fast-tooltip"); + } + + /* -------------------------------------------- */ + + /** + * Activate interactivity by listening for hover events on HTML elements which have a data-fast-tooltip defined. + */ + activateEventListeners() { + document.body.addEventListener("pointerenter", this.#onActivate.bind(this), true); + document.body.addEventListener("pointerleave", this.#onDeactivate.bind(this), true); + } + + /* -------------------------------------------- */ + + /** + * Handle hover events which activate a tooltipped element. + * @param {PointerEvent} event The initiating pointerenter event + */ + #onActivate(event) { + if (Tour.tourInProgress) return; // Don't activate tooltips during a tour + const element = event.target; + if (!element.dataset.fastTooltip) { + // Check if the element has moved out from underneath the cursor and pointerenter has fired on a non-child of the + // tooltipped element. + if (this.#active && this.element && !this.element.contains(element)) this.#startDeactivation(); + return; + } + + // Don't activate tooltips if the element contains an active context menu + if (element.matches("#context-menu") || element.querySelector("#context-menu")) return; + + // If the tooltip is currently active, we can move it to a new element immediately + if (this.#active) this.activate(element); + else this.#clearDeactivation(); + + // Otherwise, delay activation to determine user intent + this.#pending = element; + this.#activationTimeout = window.setTimeout(() => { + this.activate(element); + }, Number(element?.dataset?.tooltipActivationSpeed) ?? this.constructor.TOOLTIP_ACTIVATION_MS); + } + + activate(element, { text, direction, cssClass } = {}) { + + // Check if the element still exists in the DOM. + if (!document.body.contains(element)) return; + this.#clearDeactivation(); + + // Mark the element as active + this.#active = true; + this.element = element; + element.setAttribute("aria-describedby", "fast-tooltip"); + this.tooltip.innerHTML = text || game.i18n.localize(element.dataset.fastTooltip); + + // Activate display of the tooltip + this.tooltip.removeAttribute("class"); + this.tooltip.classList.add("active"); + if (cssClass) this.tooltip.classList.add(cssClass); + + // Set tooltip position + direction = direction || element.closest("[data-tooltip-direction]")?.dataset.tooltipDirection; + if (!direction) direction = this._determineDirection(); + this._setAnchor(direction); + } + + /* -------------------------------------------- */ + + /** + * Handle hover events which deactivate a tooltipped element. + * @param {PointerEvent} event The initiating pointerleave event + */ + #onDeactivate(event) { + if (event.target !== (this.element ?? this.#pending)) return; + this.#startDeactivation(); + } + + /* -------------------------------------------- */ + + /** + * Start the deactivation process. + */ + #startDeactivation() { + // Clear any existing activation workflow + window.clearTimeout(this.#activationTimeout); + this.#pending = this.#activationTimeout = null; + + // Delay deactivation to confirm whether some new element is now pending + window.clearTimeout(this.#deactivationTimeout); + this.#deactivationTimeout = window.setTimeout(() => { + if (!this.#pending) this.deactivate(); + }, Number(this.element?.dataset?.tooltipDeactivationSpeed) ?? this.constructor.TOOLTIP_DEACTIVATION_MS); + } + + /* -------------------------------------------- */ + + /** + * Clear any existing deactivation workflow. + */ + #clearDeactivation() { + window.clearTimeout(this.#deactivationTimeout); + this.#pending = this.#deactivationTimeout = null; + } } diff --git a/src/helpers/caches.js b/src/helpers/caches.js index 5390edb7..4c8da6e9 100644 --- a/src/helpers/caches.js +++ b/src/helpers/caches.js @@ -3,42 +3,42 @@ import * as Utilities from "./utilities.js"; export function setupCaches() { - Hooks.on(CONSTANTS.HOOKS.PILE.DELETE, (doc) => { - const uuid = Utilities.getUuid(doc); - let actor = Utilities.getActor(doc); - if (actor instanceof Actor) { - actor = actor.toObject(); - } - deletedActorCache.set(uuid, actor); - }); + Hooks.on(CONSTANTS.HOOKS.PILE.DELETE, (doc) => { + const uuid = Utilities.getUuid(doc); + let actor = Utilities.getActor(doc); + if (actor instanceof Actor) { + actor = actor.toObject(); + } + deletedActorCache.set(uuid, actor); + }); } class DebouncedCache extends Map { - #debounceClear = {}; - #timeout = {}; - - constructor(timeout = 50) { - super(); - this.#timeout = timeout; - } - - set(key, value) { - this.#setDebounce(key) - return super.set(key, value); - } - - #setDebounce(key) { - if (!this.#debounceClear[key]) { - const self = this; - this.#debounceClear[key] = foundry.utils.debounce(() => { - delete self.#debounceClear[key]; - self.delete(key); - }, this.#timeout); - } - this.#debounceClear[key](); - } + #debounceClear = {}; + #timeout = {}; + + constructor(timeout = 50) { + super(); + this.#timeout = timeout; + } + + set(key, value) { + this.#setDebounce(key) + return super.set(key, value); + } + + #setDebounce(key) { + if (!this.#debounceClear[key]) { + const self = this; + this.#debounceClear[key] = foundry.utils.debounce(() => { + delete self.#debounceClear[key]; + self.delete(key); + }, this.#timeout); + } + this.#debounceClear[key](); + } } diff --git a/src/helpers/compendium-utilities.js b/src/helpers/compendium-utilities.js index 00b2cfe1..59322d4c 100644 --- a/src/helpers/compendium-utilities.js +++ b/src/helpers/compendium-utilities.js @@ -7,77 +7,77 @@ const COMPENDIUM_CACHE = {}; export async function initializeCompendiumCache() { - Hooks.on("updateItem", async (item) => { - if (!item?.pack || !item?.pack.startsWith(PACK_ID)) return; - COMPENDIUM_CACHE[item.uuid] = item.toObject(); - }); + Hooks.on("updateItem", async (item) => { + if (!item?.pack || !item?.pack.startsWith(PACK_ID)) return; + COMPENDIUM_CACHE[item.uuid] = item.toObject(); + }); - const pack = game.packs.get(PACK_ID); - if (pack) { - for (const index of pack.index) { - const item = await pack.getDocument(index._id); - COMPENDIUM_CACHE[item.uuid] = item.toObject(); - } - } + const pack = game.packs.get(PACK_ID); + if (pack) { + for (const index of pack.index) { + const item = await pack.getDocument(index._id); + COMPENDIUM_CACHE[item.uuid] = item.toObject(); + } + } - Hooks.on("updateCompendium", updateCache); + Hooks.on("updateCompendium", updateCache); - updateCache(); + updateCache(); } async function updateCache() { - const currencies = getSetting(SETTINGS.CURRENCIES); - const secondaryCurrencies = getSetting(SETTINGS.SECONDARY_CURRENCIES); - for (const currency of currencies.concat(secondaryCurrencies)) { - if (currency.type !== "item") continue; - if (currency.data.uuid) { - COMPENDIUM_CACHE[currency.data?.uuid] = (await fromUuid(currency.data.uuid)).toObject(); - } else if (currency.data.item) { - const item = await findOrCreateItemInCompendium(currency.data.item) - COMPENDIUM_CACHE[item.uuid] = item.toObject(); - } - } + const currencies = getSetting(SETTINGS.CURRENCIES); + const secondaryCurrencies = getSetting(SETTINGS.SECONDARY_CURRENCIES); + for (const currency of currencies.concat(secondaryCurrencies)) { + if (currency.type !== "item") continue; + if (currency.data.uuid) { + COMPENDIUM_CACHE[currency.data?.uuid] = (await fromUuid(currency.data.uuid)).toObject(); + } else if (currency.data.item) { + const item = await findOrCreateItemInCompendium(currency.data.item) + COMPENDIUM_CACHE[item.uuid] = item.toObject(); + } + } } export async function getItemCompendium() { - return game.packs.get(PACK_ID) || await CompendiumCollection.createCompendium({ - label: `Item Piles: Item Backup (DO NOT DELETE)`, - id: PACK_ID, - private: true, - type: "Item" - }); + return game.packs.get(PACK_ID) || await CompendiumCollection.createCompendium({ + label: `Item Piles: Item Backup (DO NOT DELETE)`, + id: PACK_ID, + private: true, + type: "Item" + }); } export async function addItemsToCompendium(items) { - return Item.createDocuments(items, { pack: PACK_ID }); + return Item.createDocuments(items, { pack: PACK_ID }); } export async function findSimilarItemInCompendium(itemToFind) { - const pack = await getItemCompendium(); - const item = game.packs.get(PACK_ID).index.find(compendiumItem => { - return compendiumItem.name === itemToFind.name - && compendiumItem.type === itemToFind.type; - }); - return (item?._id ? pack.getDocument(item._id) : false); + const pack = await getItemCompendium(); + const item = game.packs.get(PACK_ID).index.find(compendiumItem => { + return compendiumItem.name === itemToFind.name + && compendiumItem.type === itemToFind.type; + }); + return (item?._id ? pack.getDocument(item._id) : false); } export function getItemFromCache(uuid) { - return COMPENDIUM_CACHE[uuid] ?? false; + return COMPENDIUM_CACHE[uuid] ?? false; } export async function findOrCreateItemInCompendium(itemData) { - let compendiumItem = await findSimilarItemInCompendium(itemData); - if (!compendiumItem) { - compendiumItem = (await addItemsToCompendium([itemData]))[0]; - } - COMPENDIUM_CACHE[compendiumItem.uuid] = itemData; - return compendiumItem; + let compendiumItem = await findSimilarItemInCompendium(itemData); + if (!compendiumItem) { + compendiumItem = (await addItemsToCompendium([itemData]))[0]; + } + COMPENDIUM_CACHE[compendiumItem.uuid] = itemData; + return compendiumItem; } export function findSimilarItemInCompendiumSync(itemToFind) { - return Object.values(COMPENDIUM_CACHE).find(compendiumItem => { - return compendiumItem.name === itemToFind.name - && compendiumItem.type === itemToFind.type; - }) ?? false; + return Object.values(COMPENDIUM_CACHE).find(compendiumItem => { + return compendiumItem.name === itemToFind.name + && compendiumItem.type === itemToFind.type; + }) ?? false; } diff --git a/src/helpers/helpers.js b/src/helpers/helpers.js index a881e110..55ea4e07 100644 --- a/src/helpers/helpers.js +++ b/src/helpers/helpers.js @@ -6,50 +6,50 @@ import editors from "../applications/editors/index.js"; export const debounceManager = { - debounces: {}, - - setDebounce(id, method) { - if (this.debounces[id]) { - return this.debounces[id]; - } - this.debounces[id] = debounce(function (...args) { - delete debounceManager.debounces[id]; - return method(...args); - }, 250); - return this.debounces[id]; - } + debounces: {}, + + setDebounce(id, method) { + if (this.debounces[id]) { + return this.debounces[id]; + } + this.debounces[id] = debounce(function (...args) { + delete debounceManager.debounces[id]; + return method(...args); + }, 250); + return this.debounces[id]; + } }; export const hooks = { - run: true, - _hooks: {}, - - async runWithout(callback) { - await ItemPileSocket.executeForEveryone(ItemPileSocket.HANDLERS.TOGGLE_HOOKS, false); - await callback(); - await ItemPileSocket.executeForEveryone(ItemPileSocket.HANDLERS.TOGGLE_HOOKS, true); - }, - - call(hook, ...args) { - if (!this.run) return; - return Hooks.call(hook, ...args); - }, - - callAll(hook, ...args) { - if (!this.run) return; - return Hooks.callAll(hook, ...args); - }, - - on(hook, callback) { - Hooks.on(hook, (...args) => { - if (!this.run) return; - callback(...args); - }); - } + run: true, + _hooks: {}, + + async runWithout(callback) { + await ItemPileSocket.executeForEveryone(ItemPileSocket.HANDLERS.TOGGLE_HOOKS, false); + await callback(); + await ItemPileSocket.executeForEveryone(ItemPileSocket.HANDLERS.TOGGLE_HOOKS, true); + }, + + call(hook, ...args) { + if (!this.run) return; + return Hooks.call(hook, ...args); + }, + + callAll(hook, ...args) { + if (!this.run) return; + return Hooks.callAll(hook, ...args); + }, + + on(hook, callback) { + Hooks.on(hook, (...args) => { + if (!this.run) return; + callback(...args); + }); + } } export function getModuleVersion() { - return game.modules.get(CONSTANTS.MODULE_NAME).version; + return game.modules.get(CONSTANTS.MODULE_NAME).version; } /** @@ -59,15 +59,15 @@ export function getModuleVersion() { * @return {boolean} A boolean whether the function is actually a function */ export function isFunction(inFunc) { - return inFunc && ( - {}.toString.call(inFunc) === '[object Function]' - || - {}.toString.call(inFunc) === '[object AsyncFunction]' - ); + return inFunc && ( + {}.toString.call(inFunc) === '[object Function]' + || + {}.toString.call(inFunc) === '[object AsyncFunction]' + ); } export function wait(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => setTimeout(resolve, ms)); } /** @@ -75,88 +75,88 @@ export function wait(ms) { * @returns {*} */ export function getSetting(key) { - return game.settings.get(CONSTANTS.MODULE_NAME, key); + return game.settings.get(CONSTANTS.MODULE_NAME, key); } export function setSetting(key, value) { - if (value === undefined) { - throw new Error("setSetting | value must not be undefined!"); - } - return game.settings.set(CONSTANTS.MODULE_NAME, key, value); + if (value === undefined) { + throw new Error("setSetting | value must not be undefined!"); + } + return game.settings.set(CONSTANTS.MODULE_NAME, key, value); } export function debug(msg, args = "") { - if (game.settings.get(CONSTANTS.MODULE_NAME, "debug")) { - console.log(`DEBUG | Item Piles | ${msg}`, args) - } + if (game.settings.get(CONSTANTS.MODULE_NAME, "debug")) { + console.log(`DEBUG | Item Piles | ${msg}`, args) + } } export function custom_notify(message) { - message = `Item Piles | ${message}`; - ui.notifications.notify(message, { console: false }); - console.log(message.replace("
", "\n")); + message = `Item Piles | ${message}`; + ui.notifications.notify(message, { console: false }); + console.log(message.replace("
", "\n")); } export function custom_warning(warning, notify = false) { - warning = `Item Piles | ${warning}`; - if (notify) { - ui.notifications.warn(warning, { console: false }); - } - console.warn(warning.replace("
", "\n")); + warning = `Item Piles | ${warning}`; + if (notify) { + ui.notifications.warn(warning, { console: false }); + } + console.warn(warning.replace("
", "\n")); } export function custom_error(error, notify = true) { - error = `Item Piles | ${error}`; - if (notify) { - ui.notifications.error(error, { console: false }); - } - return new Error(error.replace("
", "\n")); + error = `Item Piles | ${error}`; + if (notify) { + ui.notifications.error(error, { console: false }); + } + return new Error(error.replace("
", "\n")); } export function capitalizeFirstLetter(str) { - return str.slice(0, 1).toUpperCase() + str.slice(1); + return str.slice(0, 1).toUpperCase() + str.slice(1); } export function isRealNumber(inNumber) { - return !isNaN(inNumber) - && typeof inNumber === "number" - && isFinite(inNumber); + return !isNaN(inNumber) + && typeof inNumber === "number" + && isFinite(inNumber); } export function isActiveGM(user) { - return user.active && user.isGM; + return user.active && user.isGM; } export function getActiveGMs() { - return game.users.filter(isActiveGM); + return game.users.filter(isActiveGM); } export function isResponsibleGM() { - if (!game.user.isGM) { - return false; - } - return !getActiveGMs().some(other => other.id < game.user.id); + if (!game.user.isGM) { + return false; + } + return !getActiveGMs().some(other => other.id < game.user.id); } export function isGMConnected() { - return !!Array.from(game.users).find(user => user.isGM && user.active); + return !!Array.from(game.users).find(user => user.isGM && user.active); } export function roundToDecimals(num, decimals) { - if (!decimals) return Math.floor(num); - return Number(Math.round(num + 'e' + decimals) + 'e-' + decimals); + if (!decimals) return Math.floor(num); + return Number(Math.round(num + 'e' + decimals) + 'e-' + decimals); } export function clamp(num, min, max) { - return Math.max(Math.min(num, max), min); + return Math.max(Math.min(num, max), min); } export function getActiveApps(id, single = false) { - const apps = Object.values(ui.windows).filter(app => app.id.startsWith(id) && app._state > Application.RENDER_STATES.CLOSED); - if (single) { - return apps?.[0] ?? false; - } - return apps; + const apps = Object.values(ui.windows).filter(app => app.id.startsWith(id) && app._state > Application.RENDER_STATES.CLOSED); + if (single) { + return apps?.[0] ?? false; + } + return apps; } /** @@ -168,24 +168,24 @@ export function getActiveApps(id, single = false) { */ export async function getFiles(inFile, { applyWildCard = false, softFail = false } = {}) { - let source = 'data'; - const browseOptions = { wildcard: applyWildCard }; - - if (/\.s3\./.test(inFile)) { - source = 's3' - const { bucket, keyPrefix } = FilePicker.parseS3URL(inFile); - if (bucket) { - browseOptions.bucket = bucket; - inFile = keyPrefix; - } - } - - try { - return (await FilePicker.browse(source, inFile, browseOptions)).files; - } catch (err) { - if (softFail) return false; - throw custom_error(`Could not get files! | ${err}`); - } + let source = 'data'; + const browseOptions = { wildcard: applyWildCard }; + + if (/\.s3\./.test(inFile)) { + source = 's3' + const { bucket, keyPrefix } = FilePicker.parseS3URL(inFile); + if (bucket) { + browseOptions.bucket = bucket; + inFile = keyPrefix; + } + } + + try { + return (await FilePicker.browse(source, inFile, browseOptions)).files; + } catch (err) { + if (softFail) return false; + throw custom_error(`Could not get files! | ${err}`); + } } /** @@ -196,10 +196,10 @@ export async function getFiles(inFile, { applyWildCard = false, softFail = false * @return {number} A random value between the range given */ export function random_float_between(min, max) { - const random = Math.random(); - const _max = Math.max(max, min); - const _min = Math.min(max, min); - return random * (_max - _min) + _min; + const random = Math.random(); + const _max = Math.max(max, min); + const _min = Math.min(max, min); + return random * (_max - _min) + _min; } /** @@ -210,7 +210,7 @@ export function random_float_between(min, max) { * @return {int} A random integer between the range given */ export function random_int_between(min, max) { - return Math.floor(random_float_between(min, max)); + return Math.floor(random_float_between(min, max)); } /** @@ -221,143 +221,143 @@ export function random_int_between(min, max) { * @return {object} A random element from the array */ export function random_array_element(inArray, { recurse = false } = {}) { - let choice = inArray[random_int_between(0, inArray.length)]; - if (recurse && Array.isArray(choice)) { - return random_array_element(choice, { recurse: true }); - } - return choice; + let choice = inArray[random_int_between(0, inArray.length)]; + if (recurse && Array.isArray(choice)) { + return random_array_element(choice, { recurse: true }); + } + return choice; } export function styleFromObject(obj, vars = false) { - return Object.entries(obj).map(entry => (vars ? "--" : "") + entry[0] + ': ' + entry[1] + ";").join(""); + return Object.entries(obj).map(entry => (vars ? "--" : "") + entry[0] + ': ' + entry[1] + ";").join(""); } export function abbreviateNumbers(number, decPlaces = 2) { - // 2 decimal places => 100, 3 => 1000, etc - decPlaces = Math.pow(10, decPlaces) + // 2 decimal places => 100, 3 => 1000, etc + decPlaces = Math.pow(10, decPlaces) - // Enumerate number abbreviations - let abbrev = ['k', 'm', 'b', 't'] + // Enumerate number abbreviations + let abbrev = ['k', 'm', 'b', 't'] - // Go through the array backwards, so we do the largest first - for (let i = abbrev.length - 1; i >= 0; i--) { + // Go through the array backwards, so we do the largest first + for (let i = abbrev.length - 1; i >= 0; i--) { - // Convert array index to "1000", "1000000", etc - let size = Math.pow(10, (i + 1) * 3) + // Convert array index to "1000", "1000000", etc + let size = Math.pow(10, (i + 1) * 3) - // If the number is bigger or equal do the abbreviation - if (size <= number) { - // Here, we multiply by decPlaces, round, and then divide by decPlaces. - // This gives us nice rounding to a particular decimal place. - number = Math.round((number * decPlaces) / size) / decPlaces + // If the number is bigger or equal do the abbreviation + if (size <= number) { + // Here, we multiply by decPlaces, round, and then divide by decPlaces. + // This gives us nice rounding to a particular decimal place. + number = Math.round((number * decPlaces) / size) / decPlaces - // Handle special case where we round up to the next abbreviation - if (number === 1000 && i < abbrev.length - 1) { - number = 1 - i++ - } + // Handle special case where we round up to the next abbreviation + if (number === 1000 && i < abbrev.length - 1) { + number = 1 + i++ + } - // Add the letter for the abbreviation - number += abbrev[i] + // Add the letter for the abbreviation + number += abbrev[i] - // We are done... stop - break; - } - } + // We are done... stop + break; + } + } - return number + return number } export function timeSince(date) { - const seconds = Math.floor((new Date() - date) / 1000); - const intervals = { - "d": 86400, - "h": 3600, - "m": 60 - } - - for (const [key, value] of Object.entries(intervals)) { - const interval = seconds / value; - if (interval > 1) { - return Math.floor(interval) + key; - } - } - - return Math.ceil(seconds) + "s"; + const seconds = Math.floor((new Date() - date) / 1000); + const intervals = { + "d": 86400, + "h": 3600, + "m": 60 + } + + for (const [key, value] of Object.entries(intervals)) { + const interval = seconds / value; + if (interval > 1) { + return Math.floor(interval) + key; + } + } + + return Math.ceil(seconds) + "s"; } export function getApplicationPositions(application_1, application_2 = false) { - let midPoint = (window.innerWidth / 2); - - if (!application_2) { - if (((midPoint - 200) - application_1.position.width - 25) > 0) { - midPoint -= 200 - } - return [ - { left: midPoint - application_1.position.width - 25 }, - { left: midPoint + 25 } - ] - } - - const application_1_position = { - left: application_1.position.left, - top: application_1.position.top, - width: application_1.position.width - }; - const application_2_position = { - left: application_2.position.left, - top: application_1.position.top, - width: application_2.position.width - }; - - application_2_position.left = application_1_position.left - application_2_position.width - 25; - if (application_2_position.left < 0) { - application_2_position.left = application_1_position.left + application_1_position.width + 25 - } - if ((application_2_position.left + application_2_position.width) > window.innerWidth) { - application_2_position.left = midPoint - application_2_position.width - 25; - application_1_position.left = midPoint + 25; - } - - return [ - application_1_position, - application_2_position - ] + let midPoint = (window.innerWidth / 2); + + if (!application_2) { + if (((midPoint - 200) - application_1.position.width - 25) > 0) { + midPoint -= 200 + } + return [ + { left: midPoint - application_1.position.width - 25 }, + { left: midPoint + 25 } + ] + } + + const application_1_position = { + left: application_1.position.left, + top: application_1.position.top, + width: application_1.position.width + }; + const application_2_position = { + left: application_2.position.left, + top: application_1.position.top, + width: application_2.position.width + }; + + application_2_position.left = application_1_position.left - application_2_position.width - 25; + if (application_2_position.left < 0) { + application_2_position.left = application_1_position.left + application_1_position.width + 25 + } + if ((application_2_position.left + application_2_position.width) > window.innerWidth) { + application_2_position.left = midPoint - application_2_position.width - 25; + application_1_position.left = midPoint + 25; + } + + return [ + application_1_position, + application_2_position + ] } export async function openEditor(key, data = false) { - const setting = SETTINGS.DEFAULTS()[key]; + const setting = SETTINGS.DEFAULTS()[key]; - const editor = editors[setting.application]; + const editor = editors[setting.application]; - if (!data) { - data = getSetting(key); - } + if (!data) { + data = getSetting(key); + } - const result = await editor.show(data, { ...setting.applicationOptions, onchange: setting.onchange } ?? {}); + const result = await editor.show(data, { ...setting.applicationOptions, onchange: setting.onchange } ?? {}); - if (setting.onchange && result) setting.onchange(result); + if (setting.onchange && result) setting.onchange(result); - return result; + return result; } export function isCoordinateWithinPosition(x, y, position) { - return x >= position.left && x < position.left + position.width - && y >= position.top && y < position.top + position.height; + return x >= position.left && x < position.left + position.width + && y >= position.top && y < position.top + position.height; } export function getCanvasMouse() { - return game.release.generation === 11 - ? canvas.app.renderer.plugins.interaction.pointer - : canvas.app.renderer.plugins.interaction.mouse; + return game.release.generation === 11 + ? canvas.app.renderer.plugins.interaction.pointer + : canvas.app.renderer.plugins.interaction.mouse; } diff --git a/src/helpers/pile-utilities.js b/src/helpers/pile-utilities.js index 60aec557..a7fc04b1 100644 --- a/src/helpers/pile-utilities.js +++ b/src/helpers/pile-utilities.js @@ -40,12 +40,17 @@ export function migrateFlagData(document, data = false) { export function canItemStack(item, targetActor) { const itemData = item instanceof Item ? item.toObject() : item; - const unstackableType = Utilities.getItemTypesThatCanStack().has(itemData.type); - if (!unstackableType || (targetActor && !isItemPileVault(targetActor))) return unstackableType; + if (!Utilities.isItemStackable(itemData)) return false; const itemFlagData = getItemFlagData(itemData); const actorFlagData = getActorFlagData(targetActor); + if (typeof actorFlagData.canStackItems === "boolean") { + actorFlagData.canStackItems = "yes"; + } + if (actorFlagData.canStackItems === "always" || actorFlagData.canStackItems === "alwaysno") { + return actorFlagData.canStackItems === "always"; + } return { - "default": actorFlagData.canStackItems, + "default": actorFlagData.canStackItems === "yes", "yes": true, "no": false }[itemFlagData?.canStack ?? "default"] && !(actorFlagData.type === CONSTANTS.PILE_TYPES.VAULT && itemFlagData.vaultExpander); @@ -55,6 +60,12 @@ export function getItemFlagData(item, data = false) { return getFlagData(Utilities.getDocument(item), CONSTANTS.FLAGS.ITEM, { ...CONSTANTS.ITEM_DEFAULTS }, data); } +/** + * + * @param target + * @param data + * @returns {Object} + */ export function getActorFlagData(target, data = false) { const defaults = foundry.utils.mergeObject( { ...CONSTANTS.PILE_DEFAULTS }, @@ -180,7 +191,18 @@ export function getItemPileTokens(filter = false) { }) ]).filter(([_, tokens]) => tokens.length); - return { allTokensOnScenes, validTokensOnScenes }; + const invalidTokensOnScenes = allTokensOnScenes.map(([scene, tokens]) => [ + scene, + tokens.filter(token => { + try { + return filter ? !filter(token) : false; + } catch (err) { + return true; + } + }) + ]).filter(([_, tokens]) => tokens.length); + + return { invalidTokensOnScenes, validTokensOnScenes }; } export function getActorItems(target, { itemFilters = false, getItemCurrencies = false } = {}) { @@ -226,7 +248,7 @@ export function getActorCurrencies(target, { return { ...currency, quantity: 0, - id: item?.id ?? item?._id ?? null, + id: item?.id ?? item?._id ?? itemData._id ?? null, item, index } @@ -716,15 +738,19 @@ function getExchangeRateDecimals(smallestExchangeRate) { export function getPriceArray(totalCost, currencies) { + if (!currencies) currencies = getCurrencyList() + const primaryCurrency = currencies.find(currency => currency.primary); if (currencies.length === 1) { return [{ ...primaryCurrency, cost: totalCost, + quantity: totalCost, baseCost: totalCost, maxCurrencyCost: totalCost, - string: primaryCurrency.abbreviation.replace('{#}', totalCost) + string: primaryCurrency.abbreviation.replace('{#}', totalCost), + secondary: false }] } @@ -745,9 +771,11 @@ export function getPriceArray(totalCost, currencies) { prices.push({ ...currency, cost: Math.round(numCurrency), + quantity: Math.round(numCurrency), baseCost: Math.round(numCurrency), maxCurrencyCost: Math.ceil(totalCost / currency.exchangeRate), - string: currency.abbreviation.replace("{#}", numCurrency) + string: currency.abbreviation.replace("{#}", numCurrency), + secondary: false }); } @@ -767,9 +795,11 @@ export function getPriceArray(totalCost, currencies) { prices.push({ ...primaryCurrency, cost: cost, + quantity: cost, baseCost: cost, maxCurrencyCost: totalCost, - string: primaryCurrency.abbreviation.replace('{#}', cost) + string: primaryCurrency.abbreviation.replace('{#}', cost), + secondary: false }); } @@ -784,9 +814,11 @@ export function getPriceArray(totalCost, currencies) { prices.push({ ...currency, cost: Math.round(numCurrency), + quantity: Math.round(numCurrency), baseCost: Math.round(numCurrency), maxCurrencyCost: Math.ceil(totalCost / currency.exchangeRate), - string: currency.abbreviation.replace("{#}", numCurrency) + string: currency.abbreviation.replace("{#}", numCurrency), + secondary: false }); } @@ -798,17 +830,19 @@ export function getPriceArray(totalCost, currencies) { export function getPriceFromString(str, currencyList = false) { if (!currencyList) { - currencyList = getCurrencyList().filter(currency => !currency.secondary); + currencyList = getCurrencyList() } const currencies = foundry.utils.duplicate(currencyList) .map(currency => { currency.quantity = 0 - currency.identifier = currency.abbreviation.toLowerCase().replace("{#}", "") + currency.identifier = currency.abbreviation.toLowerCase().replace("{#}", "").trim() return currency; }); - const splitBy = new RegExp("(.*?) *(" + currencies.map(currency => currency.identifier).join("|") + ")", "g"); + const sortedCurrencies = currencies.map(currency => `(${currency.identifier})`); + sortedCurrencies.sort((a, b) => b.length - a.length); + const splitBy = new RegExp("(.*?) *(" + sortedCurrencies.join("|") + ")", "g"); const parts = [...str.split(",").join("").split(" ").join("").trim().toLowerCase().matchAll(splitBy)]; @@ -824,14 +858,16 @@ export function getPriceFromString(str, currencyList = false) { if (roll.total !== Number(part[1])) { currency.roll = roll; } - overallCost += roll.total * currency.exchangeRate; + if (currency.exchangeRate) { + overallCost += roll.total * currency.exchangeRate; + } } catch (err) { } } } - if (overallCost === 0) { + if (!currencies.some(currency => currency.quantity)) { try { const roll = new Roll(str).evaluate({ async: false }); if (roll.total) { @@ -851,6 +887,29 @@ export function getPriceFromString(str, currencyList = false) { } +export function getCostOfItem(item, defaultCurrencies = false) { + + if (!defaultCurrencies) { + defaultCurrencies = getCurrencyList().filter(currency => !currency.secondary); + } + + let overallCost = 0; + let itemCost = Utilities.getItemCost(item); + if (SYSTEMS.DATA.ITEM_COST_TRANSFORMER) { + overallCost = SYSTEMS.DATA.ITEM_COST_TRANSFORMER(item, defaultCurrencies); + if (overallCost === false) { + Helpers.debug("failed to find price for item:", item) + } + } else if (typeof itemCost === "string" && isNaN(Number(itemCost))) { + overallCost = getPriceFromString(itemCost, defaultCurrencies).overallCost; + } else { + overallCost = Number(itemCost); + } + + return Math.max(0, overallCost); + +} + export function getPriceData({ cost = false, item = false, @@ -859,7 +918,8 @@ export function getPriceData({ sellerFlagData = false, buyerFlagData = false, itemFlagData = false, - quantity = 1 + quantity = 1, + secondaryPrices = false } = {}) { let priceData = []; @@ -917,7 +977,7 @@ export function getPriceData({ } const disableNormalCost = itemFlagData.disableNormalCost && !sellerFlagData.onlyAcceptBasePrice; - const hasOtherPrices = itemFlagData.prices.filter(priceGroup => priceGroup.length).length > 0; + const hasOtherPrices = secondaryPrices?.length > 0 || itemFlagData.prices.filter(priceGroup => priceGroup.length).length > 0; const currencyList = getCurrencyList(merchant); const currencies = getActorCurrencies(merchant, { currencyList, getAll: true }); @@ -928,18 +988,7 @@ export function getPriceData({ const smallestExchangeRate = getSmallestExchangeRate(defaultCurrencies); const decimals = getExchangeRateDecimals(smallestExchangeRate); - let overallCost; - let itemCost = Utilities.getItemCost(item); - if (SYSTEMS.DATA.ITEM_COST_TRANSFORMER) { - overallCost = SYSTEMS.DATA.ITEM_COST_TRANSFORMER(item, defaultCurrencies); - if (overallCost === false) { - Helpers.debug("failed to find price for item:", item) - } - } else if (typeof itemCost === "string" && isNaN(Number(itemCost))) { - overallCost = getPriceFromString(itemCost, defaultCurrencies).overallCost; - } else { - overallCost = Number(itemCost); - } + let overallCost = getCostOfItem(item, defaultCurrencies); if (itemFlagData?.free || (!disableNormalCost && (overallCost === 0 || overallCost < smallestExchangeRate) && !hasOtherPrices) || modifier <= 0) { priceData.push({ @@ -986,10 +1035,63 @@ export function getPriceData({ } } + // If the item has custom prices, we include them here + if (secondaryPrices) { + + if (!priceData.length) { + priceData.push({ + basePrices: [], + basePriceString: "", + prices: [], + priceString: "", + totalCost: 0, + baseCost: 0, + primary: true, + maxQuantity: 0, + quantity: quantity + }); + } + + for (const secondaryPrice of secondaryPrices) { + + const itemModifier = modifier; + const cost = Math.round(secondaryPrice.quantity * itemModifier * quantity); + const baseCost = Math.round(secondaryPrice.quantity * itemModifier); + secondaryPrice.name = game.i18n.localize(secondaryPrice.name); + if (!secondaryPrice.data?.item) { + secondaryPrice.data.item = CompendiumUtilities.getItemFromCache(secondaryPrice.data.uuid); + } + priceData[0].basePrices.push({ + ...secondaryPrice, + cost, + baseCost, + modifier: itemModifier, + string: secondaryPrice.abbreviation.replace("{#}", baseCost), + priceString: cost ? secondaryPrice.abbreviation.replace("{#}", cost) : "", + basePriceString: baseCost ? secondaryPrice.abbreviation.replace("{#}", baseCost) : "" + }); + priceData[0].prices.push({ + ...secondaryPrice, + cost, + baseCost, + modifier: itemModifier, + string: secondaryPrice.abbreviation.replace("{#}", cost), + priceString: cost ? secondaryPrice.abbreviation.replace("{#}", cost) : "", + basePriceString: baseCost ? secondaryPrice.abbreviation.replace("{#}", baseCost) : "" + }); + + priceData[0].basePriceString = priceData[0].basePrices.filter(price => price.cost).map(price => price.string).join(" "); + priceData[0].priceString = priceData[0].prices.filter(price => price.cost).map(price => price.string).join(" "); + + } + + } + // If the item has custom prices, we include them here if (itemFlagData.prices.length && !(merchant === buyer && buyerFlagData.onlyAcceptBasePrice)) { - priceData = priceData.concat(itemFlagData.prices.map(priceGroup => { + priceData = itemFlagData.prices.concat(otherPrices.map(priceGroup => { + if (!Array.isArray(priceGroup)) priceGroup = [priceGroup]; const prices = priceGroup.map(price => { const itemModifier = price.fixed ? 1 : modifier; const cost = Math.round(price.quantity * itemModifier * quantity); @@ -1004,7 +1106,8 @@ export function getPriceData({ baseCost, modifier: itemModifier, priceString: cost ? price.abbreviation.replace("{#}", cost) : "", - basePriceString: baseCost ? price.abbreviation.replace("{#}", baseCost) : "" + basePriceString: baseCost ? price.abbreviation.replace("{#}", baseCost) : "", + secondary: true }; }); @@ -1031,56 +1134,72 @@ export function getPriceData({ // For each price group, check for properties and items and make sure that the actor can afford it for (const priceGroup of priceData) { + + const primaryPrices = priceGroup.prices.filter(price => !price.secondary); + const secondaryPrices = priceGroup.prices.filter(price => price.secondary); priceGroup.maxQuantity = Infinity; - if (priceGroup.baseCost !== undefined) { + + if (primaryPrices.length) { priceGroup.prices.forEach(price => { price.maxQuantity = Infinity; }); - if (buyerInfiniteCurrencies) continue; - priceGroup.maxQuantity = Math.floor(totalCurrencies / priceGroup.baseCost); - priceGroup.prices.forEach(price => { - price.maxQuantity = priceGroup.maxQuantity; - }); - } else { - if (buyerInfiniteQuantity) continue; - for (const price of priceGroup.prices) { - if (price.type === "attribute") { - const attributeQuantity = Number(getProperty(buyer, price.data.path)); - price.buyerQuantity = attributeQuantity; - - if (price.percent) { - const percent = Math.min(1, price.baseCost / 100); - const percentQuantity = Math.max(0, Math.floor(attributeQuantity * percent)); - price.maxQuantity = Math.floor(attributeQuantity / percentQuantity); - price.baseCost = !buyer ? price.baseCost : percentQuantity; - price.cost = !buyer ? price.cost : percentQuantity * quantity; - price.quantity = !buyer ? price.quantity : percentQuantity; - } else { - price.maxQuantity = Math.floor(attributeQuantity / price.baseCost); - } + if (!buyerInfiniteCurrencies) { + priceGroup.maxQuantity = Math.floor(totalCurrencies / priceGroup.baseCost); + priceGroup.prices.forEach(price => { + price.maxQuantity = priceGroup.maxQuantity; + }); + } + } + + for (const price of secondaryPrices) { - priceGroup.maxQuantity = Math.min(priceGroup.maxQuantity, price.maxQuantity) + if (buyerInfiniteQuantity) { + price.maxQuantity = Infinity; + continue; + } + if (price.type === "attribute") { + const attributeQuantity = Number(getProperty(buyer, price.data.path)); + price.buyerQuantity = attributeQuantity; + + if (price.percent) { + const percent = Math.min(1, price.baseCost / 100); + const percentQuantity = Math.max(0, Math.floor(attributeQuantity * percent)); + price.maxQuantity = Math.floor(attributeQuantity / percentQuantity); + price.baseCost = !buyer ? price.baseCost : percentQuantity; + price.cost = !buyer ? price.cost : percentQuantity * quantity; + price.quantity = !buyer ? price.quantity : percentQuantity; } else { - const priceItem = CompendiumUtilities.getItemFromCache(price.data.uuid); - const foundItem = priceItem ? Utilities.findSimilarItem(buyer.items, priceItem) : false; - const itemQuantity = foundItem ? Utilities.getItemQuantity(foundItem) : 0; - price.buyerQuantity = itemQuantity; - - if (price.percent) { - const percent = Math.min(1, price.baseCost / 100); - const percentQuantity = Math.max(0, Math.floor(itemQuantity * percent)); - price.maxQuantity = Math.floor(itemQuantity / percentQuantity); - price.baseCost = !buyer ? price.baseCost : percentQuantity; - price.cost = !buyer ? price.cost : percentQuantity * quantity; - price.quantity = !buyer ? price.quantity : percentQuantity; - } else { - price.maxQuantity = Math.floor(itemQuantity / price.baseCost); - } + price.maxQuantity = Math.floor(attributeQuantity / price.baseCost); + } - priceGroup.maxQuantity = Math.min(priceGroup.maxQuantity, price.maxQuantity); + priceGroup.maxQuantity = Math.min(priceGroup.maxQuantity, price.maxQuantity) + + } else { + const priceItem = CompendiumUtilities.getItemFromCache(price.data.uuid); + const foundItem = priceItem ? Utilities.findSimilarItem(buyer.items, priceItem) : false; + const itemQuantity = foundItem ? Utilities.getItemQuantity(foundItem) : 0; + price.buyerQuantity = itemQuantity; + if (!itemQuantity) { + priceGroup.maxQuantity = 0; + priceGroup.quantity = 0; + continue; } + + if (price.percent) { + const percent = Math.min(1, price.baseCost / 100); + const percentQuantity = Math.max(0, Math.floor(itemQuantity * percent)); + price.maxQuantity = Math.floor(itemQuantity / percentQuantity); + price.baseCost = !buyer ? price.baseCost : percentQuantity; + price.cost = !buyer ? price.cost : percentQuantity * quantity; + price.quantity = !buyer ? price.quantity : percentQuantity; + } else { + price.maxQuantity = Math.floor(itemQuantity / price.baseCost); + } + + priceGroup.maxQuantity = Math.min(priceGroup.maxQuantity, price.maxQuantity); } + } } @@ -1119,6 +1238,7 @@ export function getPaymentData({ const prices = getPriceData({ cost: data.cost, item: data.item, + secondaryPrices: data.secondaryPrices, seller, buyer, sellerFlagData, @@ -1138,14 +1258,19 @@ export function getPaymentData({ return priceData; } - if (priceGroup.primary) { + const primaryPrices = priceGroup.prices.filter(price => !price.secondary && price.cost); + const secondaryPrices = priceGroup.prices.filter(price => price.secondary && price.cost); + + if (primaryPrices.length) { priceData.totalCurrencyCost = Helpers.roundToDecimals(priceData.totalCurrencyCost + priceGroup.totalCost, decimals); priceData.primary = true; - } else { + } - for (const price of priceGroup.prices) { + if (secondaryPrices.length) { + + for (const price of secondaryPrices) { let existingPrice = priceData.otherPrices.find(otherPrice => { return otherPrice.id === price.id || (otherPrice.name === price.name && otherPrice.img === price.img && otherPrice.type === price.type); @@ -1389,6 +1514,35 @@ export function getPaymentData({ } +export function isMerchantClosed(merchant, { pileData = false } = {}) { + + if (!pileData) pileData = getActorFlagData(merchant); + + const timestamp = window.SimpleCalendar.api.timestampToDate(window.SimpleCalendar.api.timestamp()); + + const openTimes = pileData.openTimes.open; + const closeTimes = pileData.openTimes.close; + + const openingTime = Number(openTimes.hour.toString() + "." + openTimes.minute.toString()); + const closingTime = Number(closeTimes.hour.toString() + "." + closeTimes.minute.toString()); + const currentTime = Number(timestamp.hour.toString() + "." + timestamp.minute.toString()); + + let isClosed = openingTime > closingTime + ? !(currentTime >= openingTime || currentTime <= closingTime) // Is the store open over midnight? + : !(currentTime >= openingTime && currentTime <= closingTime); // or is the store open during normal daylight hours? + + const currentWeekday = window.SimpleCalendar.api.getCurrentWeekday(); + + isClosed = isClosed || (pileData.closedDays ?? []).includes(currentWeekday.name); + + const currentDate = window.SimpleCalendar.api.currentDateTime(); + const notes = window.SimpleCalendar.api.getNotesForDay(currentDate.year, currentDate.month, currentDate.day); + const categories = new Set(notes.map(note => getProperty(note, "flags.foundryvtt-simple-calendar.noteData.categories") ?? []).deepFlatten()); + + return isClosed || categories.intersection(new Set(pileData.closedHolidays ?? [])).size > 0; + +} + export async function updateMerchantLog(itemPile, activityData = {}) { const vaultLog = getActorLog(itemPile); diff --git a/src/helpers/sharing-utilities.js b/src/helpers/sharing-utilities.js index e943a1b9..c01e0b54 100644 --- a/src/helpers/sharing-utilities.js +++ b/src/helpers/sharing-utilities.js @@ -3,7 +3,7 @@ import * as Utilities from "./utilities.js" import * as PileUtilities from "./pile-utilities.js" export function getActivePlayers(onlyActive = false) { - return Array.from(game.users).filter(u => (u.active || !onlyActive) && u.character); + return Array.from(game.users).filter(u => (u.active || !onlyActive) && u.character); } /** @@ -13,10 +13,10 @@ export function getActivePlayers(onlyActive = false) { * @returns {Array} */ export function getPlayersForItemPile(target) { - const targetActor = Utilities.getActor(target); - if (!PileUtilities.isValidItemPile(targetActor)) return []; - const pileData = PileUtilities.getActorFlagData(targetActor); - return getActivePlayers(pileData.activePlayers); + const targetActor = Utilities.getActor(target); + if (!PileUtilities.isValidItemPile(targetActor)) return []; + const pileData = PileUtilities.getActorFlagData(targetActor); + return getActivePlayers(pileData.activePlayers); } /** @@ -26,40 +26,40 @@ export function getPlayersForItemPile(target) { * @returns {boolean} */ export function canItemPileBeSplit(target) { - const pileData = PileUtilities.getActorFlagData(target); - const shareData = getItemPileSharingData(target); - const playerActors = getPlayersForItemPile(target).map(player => Utilities.getUserCharacter(player)); - const items = pileData.shareItemsEnabled ? PileUtilities.getActorItems(target) : []; - const currencies = pileData.shareCurrenciesEnabled || pileData.splitAllEnabled ? PileUtilities.getActorCurrencies(target) : []; - for (const item of items) { - if (playerActors.every(character => getItemSharesLeftForActor(target, item, character, { - shareData, - floor: true, - players: playerActors.length - }))) { - return true; - } - } - for (const currency of currencies) { - if (currency.type === "item") { - if (playerActors.every(character => getItemSharesLeftForActor(target, currency.item, character, { - shareData, - floor: true, - players: playerActors.length - }))) { - return true; - } - } else { - if (playerActors.every(character => getAttributeSharesLeftForActor(target, currency.path, character, { - shareData, - floor: true, - players: playerActors.length - }))) { - return true; - } - } - } - return false; + const pileData = PileUtilities.getActorFlagData(target); + const shareData = getItemPileSharingData(target); + const playerActors = getPlayersForItemPile(target).map(player => Utilities.getUserCharacter(player)); + const items = pileData.shareItemsEnabled ? PileUtilities.getActorItems(target) : []; + const currencies = pileData.shareCurrenciesEnabled || pileData.splitAllEnabled ? PileUtilities.getActorCurrencies(target) : []; + for (const item of items) { + if (playerActors.every(character => getItemSharesLeftForActor(target, item, character, { + shareData, + floor: true, + players: playerActors.length + }))) { + return true; + } + } + for (const currency of currencies) { + if (currency.type === "item") { + if (playerActors.every(character => getItemSharesLeftForActor(target, currency.item, character, { + shareData, + floor: true, + players: playerActors.length + }))) { + return true; + } + } else { + if (playerActors.every(character => getAttributeSharesLeftForActor(target, currency.path, character, { + shareData, + floor: true, + players: playerActors.length + }))) { + return true; + } + } + } + return false; } /** @@ -69,8 +69,8 @@ export function canItemPileBeSplit(target) { * @returns {Object} */ export function getItemPileSharingData(target) { - const targetActor = Utilities.getActor(target); - return foundry.utils.duplicate(getProperty(targetActor, CONSTANTS.FLAGS.SHARING) ?? {}); + const targetActor = Utilities.getActor(target); + return foundry.utils.duplicate(getProperty(targetActor, CONSTANTS.FLAGS.SHARING) ?? {}); } /** @@ -81,12 +81,12 @@ export function getItemPileSharingData(target) { * @returns {Promise} */ export function updateItemPileSharingData(target, incomingSharingData) { - const targetActor = Utilities.getActor(target); - const currentSharingData = getItemPileSharingData(targetActor); - const newSharingData = foundry.utils.mergeObject(currentSharingData, incomingSharingData); - return targetActor.update({ - [CONSTANTS.FLAGS.SHARING]: newSharingData - }); + const targetActor = Utilities.getActor(target); + const currentSharingData = getItemPileSharingData(targetActor); + const newSharingData = foundry.utils.mergeObject(currentSharingData, incomingSharingData); + return targetActor.update({ + [CONSTANTS.FLAGS.SHARING]: newSharingData + }); } /** @@ -96,10 +96,10 @@ export function updateItemPileSharingData(target, incomingSharingData) { * @returns {Promise} */ export function clearItemPileSharingData(target) { - const targetActor = Utilities.getActor(target); - return targetActor.update({ - [CONSTANTS.FLAGS.SHARING]: null - }); + const targetActor = Utilities.getActor(target); + return targetActor.update({ + [CONSTANTS.FLAGS.SHARING]: null + }); } /** @@ -112,231 +112,231 @@ export function clearItemPileSharingData(target) { */ export async function setItemPileSharingData(sourceUuid, targetUuid, { items = [], attributes = [] } = {}) { - const sourceActor = Utilities.getActor(sourceUuid); - const targetActor = Utilities.getActor(targetUuid); + const sourceActor = Utilities.getActor(sourceUuid); + const targetActor = Utilities.getActor(targetUuid); - const sourceIsItemPile = PileUtilities.isValidItemPile(sourceActor); - const targetIsItemPile = PileUtilities.isValidItemPile(targetActor); + const sourceIsItemPile = PileUtilities.isValidItemPile(sourceActor); + const targetIsItemPile = PileUtilities.isValidItemPile(targetActor); - // If both the source and target are item piles, we want to ignore this execution - if (sourceIsItemPile && targetIsItemPile) return; + // If both the source and target are item piles, we want to ignore this execution + if (sourceIsItemPile && targetIsItemPile) return; - if (items.length) { - items = items.map(itemData => { - Utilities.setItemQuantity(itemData.item, Math.abs(itemData.quantity)); - return itemData.item; - }) - } + if (items.length) { + items = items.map(itemData => { + Utilities.setItemQuantity(itemData.item, Math.abs(itemData.quantity)); + return itemData.item; + }) + } - if (!Array.isArray(attributes) && typeof attributes === "object") { - attributes = Object.entries(attributes).map(entry => { - return { - path: entry[0], - quantity: Math.abs(entry[1]) - } - }) - } + if (!Array.isArray(attributes) && typeof attributes === "object") { + attributes = Object.entries(attributes).map(entry => { + return { + path: entry[0], + quantity: Math.abs(entry[1]) + } + }) + } - if (sourceIsItemPile) { + if (sourceIsItemPile) { - if (PileUtilities.isItemPileEmpty(sourceIsItemPile)) { - return clearItemPileSharingData(sourceIsItemPile); - } + if (PileUtilities.isItemPileEmpty(sourceIsItemPile)) { + return clearItemPileSharingData(sourceIsItemPile); + } - const sharingData = addToItemPileSharingData(sourceActor, targetActor.uuid, { items, attributes }); - return updateItemPileSharingData(sourceActor, sharingData); + const sharingData = addToItemPileSharingData(sourceActor, targetActor.uuid, { items, attributes }); + return updateItemPileSharingData(sourceActor, sharingData); - } + } - const sharingData = removeFromItemPileSharingData(targetActor, sourceActor.uuid, { items, attributes }); - return updateItemPileSharingData(targetActor, sharingData); + const sharingData = removeFromItemPileSharingData(targetActor, sourceActor.uuid, { items, attributes }); + return updateItemPileSharingData(targetActor, sharingData); } export function addToItemPileSharingData(itemPile, actorUuid, { - sharingData = false, - items = [], - attributes = [] + sharingData = false, + items = [], + attributes = [] } = {}) { - const pileData = PileUtilities.getActorFlagData(itemPile); - - const pileCurrencies = PileUtilities.getActorCurrencies(itemPile, { getAll: true }); - - const filteredItems = items.filter(item => !pileCurrencies.some(currency => item.id !== currency.id)); - const currencies = items.filter(item => !pileCurrencies.some(currency => item.id === currency.id)); - - let pileSharingData = {}; - if (!sharingData && ((pileData.shareItemsEnabled && filteredItems.length) || (pileData.shareCurrenciesEnabled && (attributes.length || currencies.length)))) { - pileSharingData = getItemPileSharingData(itemPile); - } - - if ((pileData.shareItemsEnabled && filteredItems.length) || (pileData.shareCurrenciesEnabled && currencies.length)) { - - if (!pileSharingData.items) { - pileSharingData.items = []; - } - - for (const item of filteredItems.concat(currencies)) { - - let existingItem = Utilities.findSimilarItem(pileSharingData.items, item); - - if (!existingItem) { - let itemIndex = pileSharingData.items.push(Utilities.setSimilarityProperties({ - actors: [{ uuid: actorUuid, quantity: 0 }] - }, item)) - existingItem = pileSharingData.items[itemIndex - 1]; - } else if (!existingItem.actors) { - existingItem.actors = []; - existingItem._id = item.id; - } - - let actorData = existingItem.actors.find(data => data.uuid === actorUuid); - - const itemQuantity = Utilities.getItemQuantity(item); - if (!actorData) { - if (itemQuantity > 0) { - existingItem.actors.push({ uuid: actorUuid, quantity: itemQuantity }) - } - } else { - actorData.quantity += itemQuantity; - if (actorData.quantity <= 0) { - existingItem.actors.splice(existingItem.actors.indexOf(actorData), 1); - } - if (existingItem.actors.length === 0) { - pileSharingData.items.splice(pileSharingData.items.indexOf(existingItem), 1) - } - } - - } - - } - - if (pileData.shareCurrenciesEnabled && attributes.length) { - - if (!pileSharingData.attributes) { - pileSharingData.attributes = []; - } - - for (const attribute of attributes) { - - let existingCurrency = pileSharingData.attributes.find(sharingCurrency => sharingCurrency.path === attribute.path); - - if (!existingCurrency) { - let itemIndex = pileSharingData.attributes.push({ - path: attribute.path, - actors: [{ uuid: actorUuid, quantity: 0 }] - }) - existingCurrency = pileSharingData.attributes[itemIndex - 1]; - } else { - if (!existingCurrency.actors) { - existingCurrency.actors = []; - } - } - - let actorData = existingCurrency.actors.find(data => data.uuid === actorUuid); - - if (!actorData) { - if (attribute.quantity > 0) { - existingCurrency.actors.push({ uuid: actorUuid, quantity: attribute.quantity }) - } - } else { - actorData.quantity += attribute.quantity; - if (actorData.quantity <= 0) { - existingCurrency.actors.splice(existingCurrency.actors.indexOf(actorData), 1); - } - if (existingCurrency.actors.length === 0) { - pileSharingData.attributes.splice(pileSharingData.attributes.indexOf(existingCurrency), 1) - } - } - } - } - - - return pileSharingData; + const pileData = PileUtilities.getActorFlagData(itemPile); + + const pileCurrencies = PileUtilities.getActorCurrencies(itemPile, { getAll: true }); + + const filteredItems = items.filter(item => !pileCurrencies.some(currency => item.id !== currency.id)); + const currencies = items.filter(item => !pileCurrencies.some(currency => item.id === currency.id)); + + let pileSharingData = {}; + if (!sharingData && ((pileData.shareItemsEnabled && filteredItems.length) || (pileData.shareCurrenciesEnabled && (attributes.length || currencies.length)))) { + pileSharingData = getItemPileSharingData(itemPile); + } + + if ((pileData.shareItemsEnabled && filteredItems.length) || (pileData.shareCurrenciesEnabled && currencies.length)) { + + if (!pileSharingData.items) { + pileSharingData.items = []; + } + + for (const item of filteredItems.concat(currencies)) { + + let existingItem = Utilities.findSimilarItem(pileSharingData.items, item); + + if (!existingItem) { + let itemIndex = pileSharingData.items.push(Utilities.setSimilarityProperties({ + actors: [{ uuid: actorUuid, quantity: 0 }] + }, item)) + existingItem = pileSharingData.items[itemIndex - 1]; + } else if (!existingItem.actors) { + existingItem.actors = []; + existingItem._id = item.id; + } + + let actorData = existingItem.actors.find(data => data.uuid === actorUuid); + + const itemQuantity = Utilities.getItemQuantity(item); + if (!actorData) { + if (itemQuantity > 0) { + existingItem.actors.push({ uuid: actorUuid, quantity: itemQuantity }) + } + } else { + actorData.quantity += itemQuantity; + if (actorData.quantity <= 0) { + existingItem.actors.splice(existingItem.actors.indexOf(actorData), 1); + } + if (existingItem.actors.length === 0) { + pileSharingData.items.splice(pileSharingData.items.indexOf(existingItem), 1) + } + } + + } + + } + + if (pileData.shareCurrenciesEnabled && attributes.length) { + + if (!pileSharingData.attributes) { + pileSharingData.attributes = []; + } + + for (const attribute of attributes) { + + let existingCurrency = pileSharingData.attributes.find(sharingCurrency => sharingCurrency.path === attribute.path); + + if (!existingCurrency) { + let itemIndex = pileSharingData.attributes.push({ + path: attribute.path, + actors: [{ uuid: actorUuid, quantity: 0 }] + }) + existingCurrency = pileSharingData.attributes[itemIndex - 1]; + } else { + if (!existingCurrency.actors) { + existingCurrency.actors = []; + } + } + + let actorData = existingCurrency.actors.find(data => data.uuid === actorUuid); + + if (!actorData) { + if (attribute.quantity > 0) { + existingCurrency.actors.push({ uuid: actorUuid, quantity: attribute.quantity }) + } + } else { + actorData.quantity += attribute.quantity; + if (actorData.quantity <= 0) { + existingCurrency.actors.splice(existingCurrency.actors.indexOf(actorData), 1); + } + if (existingCurrency.actors.length === 0) { + pileSharingData.attributes.splice(pileSharingData.attributes.indexOf(existingCurrency), 1) + } + } + } + } + + + return pileSharingData; } export function removeFromItemPileSharingData(itemPile, actorUuid, { items = [], attributes = [] } = {}) { - items = items.map(item => { - Utilities.setItemQuantity(item, Utilities.getItemQuantity(item) * -1) - return item; - }); + items = items.map(item => { + Utilities.setItemQuantity(item, Utilities.getItemQuantity(item) * -1) + return item; + }); - attributes = attributes.map(attribute => { - attribute.quantity = attribute.quantity * -1; - return attribute; - }); + attributes = attributes.map(attribute => { + attribute.quantity = attribute.quantity * -1; + return attribute; + }); - return addToItemPileSharingData(itemPile, actorUuid, { items, attributes }); + return addToItemPileSharingData(itemPile, actorUuid, { items, attributes }); } export function getItemSharesLeftForActor(pile, item, recipient, { - currentQuantity = null, - floor = null, - players = null, - shareData = null + currentQuantity = null, + floor = null, + players = null, + shareData = null } = {}) { - if (item instanceof String) { - item = pile.items.get(item); - } - let previouslyTaken = 0; - let recipientUuid = Utilities.getUuid(recipient); - currentQuantity = currentQuantity ?? Math.abs(Utilities.getItemQuantity(item)); - let totalShares = currentQuantity; - - shareData = shareData ?? getItemPileSharingData(pile); - if (shareData?.items?.length) { - const foundItem = Utilities.findSimilarItem(shareData.items, item); - if (foundItem) { - totalShares = foundItem.actors.reduce((acc, actor) => acc + actor.quantity, currentQuantity); - const quantityTakenBefore = foundItem.actors.find(actor => actor.uuid === recipientUuid); - previouslyTaken = quantityTakenBefore ? quantityTakenBefore.quantity : 0; - } - } - - players = players ?? getPlayersForItemPile(pile).length; - let totalActorShare = totalShares / players; - if (!Number.isInteger(totalActorShare) && !floor) { - totalActorShare += 1; - } - - return Math.max(0, Math.min(currentQuantity, Math.floor(totalActorShare - previouslyTaken))); + if (item instanceof String) { + item = pile.items.get(item); + } + let previouslyTaken = 0; + let recipientUuid = Utilities.getUuid(recipient); + currentQuantity = currentQuantity ?? Math.abs(Utilities.getItemQuantity(item)); + let totalShares = currentQuantity; + + shareData = shareData ?? getItemPileSharingData(pile); + if (shareData?.items?.length) { + const foundItem = Utilities.findSimilarItem(shareData.items, item); + if (foundItem) { + totalShares = foundItem.actors.reduce((acc, actor) => acc + actor.quantity, currentQuantity); + const quantityTakenBefore = foundItem.actors.find(actor => actor.uuid === recipientUuid); + previouslyTaken = quantityTakenBefore ? quantityTakenBefore.quantity : 0; + } + } + + players = players ?? getPlayersForItemPile(pile).length; + let totalActorShare = totalShares / players; + if (!Number.isInteger(totalActorShare) && !floor) { + totalActorShare += 1; + } + + return Math.max(0, Math.min(currentQuantity, Math.floor(totalActorShare - previouslyTaken))); } export function getAttributeSharesLeftForActor(pile, path, recipient, { - currentQuantity = null, - floor = null, - players = null, - shareData = null + currentQuantity = null, + floor = null, + players = null, + shareData = null } = {}) { - let previouslyTaken = 0; - let recipientUuid = Utilities.getUuid(recipient); - currentQuantity = currentQuantity ?? Number(getProperty(pile, path) ?? 0); - let totalShares = currentQuantity; - - shareData = shareData ?? getItemPileSharingData(pile); - if (shareData?.attributes?.length) { - const existingCurrency = shareData.attributes.find(storedCurrency => storedCurrency.path === path); - if (existingCurrency) { - totalShares = existingCurrency.actors.reduce((acc, actor) => acc + actor.quantity, currentQuantity); - - const quantityTakenBefore = existingCurrency?.actors?.find(actor => actor.uuid === recipientUuid); - previouslyTaken = quantityTakenBefore ? quantityTakenBefore.quantity : 0; - } - } - - players = players ?? getPlayersForItemPile(pile).length; - let totalActorShare = totalShares / players; - if (!Number.isInteger(totalActorShare) && !floor) { - totalActorShare += 1; - } - - return Math.max(0, Math.min(currentQuantity, Math.floor(totalActorShare - previouslyTaken))); + let previouslyTaken = 0; + let recipientUuid = Utilities.getUuid(recipient); + currentQuantity = currentQuantity ?? Number(getProperty(pile, path) ?? 0); + let totalShares = currentQuantity; + + shareData = shareData ?? getItemPileSharingData(pile); + if (shareData?.attributes?.length) { + const existingCurrency = shareData.attributes.find(storedCurrency => storedCurrency.path === path); + if (existingCurrency) { + totalShares = existingCurrency.actors.reduce((acc, actor) => acc + actor.quantity, currentQuantity); + + const quantityTakenBefore = existingCurrency?.actors?.find(actor => actor.uuid === recipientUuid); + previouslyTaken = quantityTakenBefore ? quantityTakenBefore.quantity : 0; + } + } + + players = players ?? getPlayersForItemPile(pile).length; + let totalActorShare = totalShares / players; + if (!Number.isInteger(totalActorShare) && !floor) { + totalActorShare += 1; + } + + return Math.max(0, Math.min(currentQuantity, Math.floor(totalActorShare - previouslyTaken))); } diff --git a/src/helpers/transaction.js b/src/helpers/transaction.js index c9a0523d..37aa992c 100644 --- a/src/helpers/transaction.js +++ b/src/helpers/transaction.js @@ -8,222 +8,222 @@ import CONSTANTS from "../constants/constants.js"; export default class Transaction { - constructor(actor) { - this.actor = actor; - this.itemsToCreate = []; - this.itemsToUpdate = []; - this.itemsToDelete = []; - this.itemsToForceDelete = new Set(); - this.itemsToNotDelete = new Set(); - this.actorUpdates = {}; - this.attributeDeltas = new Map(); - this.attributeTypeMap = new Map(); - this.itemDeltas = new Map(); - this.itemTypeMap = new Map(); - this.itemFlagMap = new Map(); - this.preCommitted = false; - } - - async appendItemChanges(items, { - remove = false, - type = "item", - keepIfZero = false, - onlyDelta = false, - } = {}) { - - for (let data of items) { - - let item = data.item ?? data; - - type = PileUtilities.isItemCurrency(item, { target: this.actor }) - ? "currency" : type; - - let flags = data.flags ?? false; - let itemData = item instanceof Item ? item.toObject() : foundry.utils.duplicate(item); - if (SYSTEMS.DATA.ITEM_TRANSFORMER && !remove) { - itemData = await SYSTEMS.DATA.ITEM_TRANSFORMER(itemData); - } - const incomingQuantity = Math.abs(data.quantity ?? Utilities.getItemQuantity(itemData)) * (remove ? -1 : 1); - let itemId = itemData._id ?? itemData.id; - const actorHasItem = this.actor.items.get(itemId); - const actorExistingItem = actorHasItem || Utilities.findSimilarItem(this.actor.items, itemData, PileUtilities.getActorFlagData(this.actor), type === "currency"); - const canItemStack = PileUtilities.canItemStack(actorExistingItem || itemData, this.actor); - - if (!canItemStack) { - - if (remove && actorExistingItem) { - this.itemTypeMap.set(actorExistingItem.id, type); - if (!onlyDelta) { - this.itemsToForceDelete.add(actorExistingItem.id); - } - this.itemDeltas.set(actorExistingItem.id, -1); - } else { - if (!itemId) { - itemId = randomID(); - } - Utilities.setItemQuantity(itemData, incomingQuantity); - this.itemTypeMap.set(itemId, type) - this.itemsToCreate.push(itemData); - } - - } else if (actorExistingItem) { - - const existingItemUpdate = remove ? this.itemsToUpdate.find(item => item._id === itemId) : Utilities.findSimilarItem(this.itemsToUpdate, itemData); - if (keepIfZero || type === "currency") { - this.itemsToNotDelete.add(item.id); - } - if (!onlyDelta) { - if (existingItemUpdate) { - const newQuantity = Utilities.getItemQuantity(existingItemUpdate) + incomingQuantity; - Utilities.setItemQuantity(existingItemUpdate, newQuantity); - if (keepIfZero && type !== "currency") { - setProperty(existingItemUpdate, CONSTANTS.FLAGS.ITEM + ".notForSale", newQuantity === 0); - } - } else { - const newQuantity = Utilities.getItemQuantity(actorExistingItem) + incomingQuantity; - const update = Utilities.setItemQuantity(actorExistingItem.toObject(), newQuantity); - if (keepIfZero && type !== "currency") { - setProperty(update, CONSTANTS.FLAGS.ITEM + ".notForSale", newQuantity === 0); - } - this.itemTypeMap.set(actorExistingItem.id, type) - this.itemsToUpdate.push(update); - } - } - this.itemDeltas.set(actorExistingItem.id, - (this.itemDeltas.has(actorExistingItem.id) ? this.itemDeltas.get(actorExistingItem.id) : 0) + incomingQuantity - ); - } else { - if (!itemData._id) { - itemData._id = randomID(); - } - - const existingItemCreation = Utilities.findSimilarItem(this.itemsToCreate, itemData); - if (existingItemCreation) { - const newQuantity = Utilities.getItemQuantity(existingItemCreation) + incomingQuantity; - Utilities.setItemQuantity(existingItemCreation, newQuantity); - } else { - Utilities.setItemQuantity(itemData, incomingQuantity); - this.itemsToCreate.push(itemData); - this.itemTypeMap.set(itemData._id, type) - } - } - - if (flags) { - this.itemFlagMap.set(itemData._id, flags); - } - } - } - - async appendActorChanges(attributes, { set = false, remove = false, type = "attribute", onlyDelta = false } = {}) { - if (!Array.isArray(attributes)) { - attributes = Object.entries(attributes).map(entry => ({ path: entry[0], quantity: entry[1] })); - } - this.actorUpdates = attributes.reduce((acc, attribute) => { - const incomingQuantity = Math.abs(attribute.quantity) * (remove ? -1 : 1); - acc[attribute.path] = acc[attribute.path] ?? Number(getProperty(this.actor, attribute.path) ?? 0); - if (set) { - if (!onlyDelta) { - acc[attribute.path] = incomingQuantity - } - this.attributeDeltas.set(attribute.path, - (this.attributeDeltas.has(attribute.path) ? this.attributeDeltas.get(attribute.path) : acc[attribute.path]) + incomingQuantity - ); - } else { - if (!onlyDelta) { - acc[attribute.path] += incomingQuantity - } - this.attributeDeltas.set(attribute.path, - (this.attributeDeltas.has(attribute.path) ? this.attributeDeltas.get(attribute.path) : 0) + incomingQuantity - ); - } - this.attributeTypeMap.set(attribute.path, type) - return acc; - }, this.actorUpdates); - } - - prepare() { - - this.actorUpdates = Object.fromEntries(Object.entries(this.actorUpdates).filter(entry => { - if (this.attributeDeltas.get(entry[0]) === 0) { - this.attributeDeltas.delete(entry[0]); - } - return Number(getProperty(this.actor, entry[0])) !== entry[1]; - })) - this.itemsToCreate = this.itemsToCreate.filter(item => { - return !PileUtilities.canItemStack(item, this.actor) || Utilities.getItemQuantity(item) > 0 || this.itemTypeMap.get(item._id) === "currency" - }); - this.itemsToDelete = this.itemsToUpdate.filter(item => { - return Utilities.getItemQuantity(item) <= 0 && this.itemTypeMap.get(item._id) !== "currency"; - }).map(item => item._id).concat(Array.from(this.itemsToForceDelete)); - - for (const itemId of this.itemsToDelete) { - if (this.itemsToNotDelete.has(itemId)) { - this.itemsToDelete.splice(this.itemsToDelete.indexOf(itemId), 1); - } - } - - this.itemDeltas = Array.from(this.itemDeltas).map(([id, quantity]) => { - const item = this.actor.items.get(id).toObject(); - const existingFlags = getItemFlagData(item); - setProperty(item, CONSTANTS.FLAGS.ITEM, foundry.utils.mergeObject( - existingFlags, - this.itemFlagMap.get(id) ?? {} - )); - const type = this.itemTypeMap.get(id); - Utilities.setItemQuantity(item, quantity, true); - return { item, quantity, type }; - }).filter(delta => delta.quantity); - - this.itemsToUpdate = this.itemsToUpdate - .filter(item => Utilities.getItemQuantity(item) > 0 || this.itemsToNotDelete.has(item._id) || this.itemTypeMap.get(item._id) === "currency") - .filter(itemData => { - const item = this.actor.items.get(itemData._id) - return Utilities.getItemQuantity(item) !== Utilities.getItemQuantity(itemData); - }); - this.attributeDeltas = Object.fromEntries(this.attributeDeltas); - this.preCommitted = true; - return { - actorUpdates: this.actorUpdates, - itemsToCreate: this.itemsToCreate, - itemsToDelete: this.itemsToDelete, - itemsToUpdate: this.itemsToUpdate, - attributeDeltas: this.attributeDeltas, - itemDeltas: this.itemDeltas, - } - } - - async commit() { - - if (!this.preCommitted) { - this.prepare(); - } - - let itemsCreated; - const actorUuid = Utilities.getUuid(this.actor); - if (this.actor.isOwner) { - itemsCreated = await PrivateAPI._commitActorChanges(actorUuid, { - actorUpdates: this.actorUpdates, - itemsToUpdate: this.itemsToUpdate, - itemsToDelete: this.itemsToDelete, - itemsToCreate: this.itemsToCreate - }) - } else { - itemsCreated = await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.COMMIT_ACTOR_CHANGES, actorUuid, { - actorUpdates: this.actorUpdates, - itemsToUpdate: this.itemsToUpdate, - itemsToDelete: this.itemsToDelete, - itemsToCreate: this.itemsToCreate - }); - } - - return { - attributeDeltas: this.attributeDeltas, - itemDeltas: this.itemDeltas.concat(itemsCreated.map(item => { - return { - item, - quantity: PileUtilities.canItemStack(item) ? Utilities.getItemQuantity(item) : 1 - } - })) - } - } + constructor(actor) { + this.actor = actor; + this.itemsToCreate = []; + this.itemsToUpdate = []; + this.itemsToDelete = []; + this.itemsToForceDelete = new Set(); + this.itemsToNotDelete = new Set(); + this.actorUpdates = {}; + this.attributeDeltas = new Map(); + this.attributeTypeMap = new Map(); + this.itemDeltas = new Map(); + this.itemTypeMap = new Map(); + this.itemFlagMap = new Map(); + this.preCommitted = false; + } + + async appendItemChanges(items, { + remove = false, + type = "item", + keepIfZero = false, + onlyDelta = false, + } = {}) { + + for (let data of items) { + + let item = data.item ?? data; + + type = PileUtilities.isItemCurrency(item, { target: this.actor }) + ? "currency" : type; + + let flags = data.flags ?? false; + let itemData = item instanceof Item ? item.toObject() : foundry.utils.duplicate(item); + if (SYSTEMS.DATA.ITEM_TRANSFORMER && !remove) { + itemData = await SYSTEMS.DATA.ITEM_TRANSFORMER(itemData); + } + const incomingQuantity = Math.abs(data.quantity ?? Utilities.getItemQuantity(itemData)) * (remove ? -1 : 1); + let itemId = itemData._id ?? itemData.id; + const actorHasItem = this.actor.items.get(itemId); + const actorExistingItem = actorHasItem || Utilities.findSimilarItem(this.actor.items, itemData, PileUtilities.getActorFlagData(this.actor), type === "currency"); + const canItemStack = PileUtilities.canItemStack(actorExistingItem || itemData, this.actor); + + if (!canItemStack) { + + if (remove && actorExistingItem) { + this.itemTypeMap.set(actorExistingItem.id, type); + if (!onlyDelta) { + this.itemsToForceDelete.add(actorExistingItem.id); + } + this.itemDeltas.set(actorExistingItem.id, -1); + } else { + if (!itemId) { + itemId = randomID(); + } + Utilities.setItemQuantity(itemData, incomingQuantity); + this.itemTypeMap.set(itemId, type) + this.itemsToCreate.push(itemData); + } + + } else if (actorExistingItem) { + + const existingItemUpdate = remove ? this.itemsToUpdate.find(item => item._id === itemId) : Utilities.findSimilarItem(this.itemsToUpdate, itemData); + if (keepIfZero || type === "currency") { + this.itemsToNotDelete.add(item.id); + } + if (!onlyDelta) { + if (existingItemUpdate) { + const newQuantity = Utilities.getItemQuantity(existingItemUpdate) + incomingQuantity; + Utilities.setItemQuantity(existingItemUpdate, newQuantity); + if (keepIfZero && type !== "currency") { + setProperty(existingItemUpdate, CONSTANTS.FLAGS.ITEM + ".notForSale", newQuantity === 0); + } + } else { + const newQuantity = Utilities.getItemQuantity(actorExistingItem) + incomingQuantity; + const update = Utilities.setItemQuantity(actorExistingItem.toObject(), newQuantity); + if (keepIfZero && type !== "currency") { + setProperty(update, CONSTANTS.FLAGS.ITEM + ".notForSale", newQuantity === 0); + } + this.itemTypeMap.set(actorExistingItem.id, type) + this.itemsToUpdate.push(update); + } + } + this.itemDeltas.set(actorExistingItem.id, + (this.itemDeltas.has(actorExistingItem.id) ? this.itemDeltas.get(actorExistingItem.id) : 0) + incomingQuantity + ); + } else { + if (!itemData._id) { + itemData._id = randomID(); + } + + const existingItemCreation = Utilities.findSimilarItem(this.itemsToCreate, itemData); + if (existingItemCreation) { + const newQuantity = Utilities.getItemQuantity(existingItemCreation) + incomingQuantity; + Utilities.setItemQuantity(existingItemCreation, newQuantity); + } else { + Utilities.setItemQuantity(itemData, incomingQuantity); + this.itemsToCreate.push(itemData); + this.itemTypeMap.set(itemData._id, type) + } + } + + if (flags) { + this.itemFlagMap.set(itemData._id, flags); + } + } + } + + async appendActorChanges(attributes, { set = false, remove = false, type = "attribute", onlyDelta = false } = {}) { + if (!Array.isArray(attributes)) { + attributes = Object.entries(attributes).map(entry => ({ path: entry[0], quantity: entry[1] })); + } + this.actorUpdates = attributes.reduce((acc, attribute) => { + const incomingQuantity = Math.abs(attribute.quantity) * (remove ? -1 : 1); + acc[attribute.path] = acc[attribute.path] ?? Number(getProperty(this.actor, attribute.path) ?? 0); + if (set) { + if (!onlyDelta) { + acc[attribute.path] = incomingQuantity + } + this.attributeDeltas.set(attribute.path, + (this.attributeDeltas.has(attribute.path) ? this.attributeDeltas.get(attribute.path) : acc[attribute.path]) + incomingQuantity + ); + } else { + if (!onlyDelta) { + acc[attribute.path] += incomingQuantity + } + this.attributeDeltas.set(attribute.path, + (this.attributeDeltas.has(attribute.path) ? this.attributeDeltas.get(attribute.path) : 0) + incomingQuantity + ); + } + this.attributeTypeMap.set(attribute.path, type) + return acc; + }, this.actorUpdates); + } + + prepare() { + + this.actorUpdates = Object.fromEntries(Object.entries(this.actorUpdates).filter(entry => { + if (this.attributeDeltas.get(entry[0]) === 0) { + this.attributeDeltas.delete(entry[0]); + } + return Number(getProperty(this.actor, entry[0])) !== entry[1]; + })) + this.itemsToCreate = this.itemsToCreate.filter(item => { + return !PileUtilities.canItemStack(item, this.actor) || Utilities.getItemQuantity(item) > 0 || this.itemTypeMap.get(item._id) === "currency" + }); + this.itemsToDelete = this.itemsToUpdate.filter(item => { + return Utilities.getItemQuantity(item) <= 0 && this.itemTypeMap.get(item._id) !== "currency"; + }).map(item => item._id).concat(Array.from(this.itemsToForceDelete)); + + for (const itemId of this.itemsToDelete) { + if (this.itemsToNotDelete.has(itemId)) { + this.itemsToDelete.splice(this.itemsToDelete.indexOf(itemId), 1); + } + } + + this.itemDeltas = Array.from(this.itemDeltas).map(([id, quantity]) => { + const item = this.actor.items.get(id).toObject(); + const existingFlags = getItemFlagData(item); + setProperty(item, CONSTANTS.FLAGS.ITEM, foundry.utils.mergeObject( + existingFlags, + this.itemFlagMap.get(id) ?? {} + )); + const type = this.itemTypeMap.get(id); + Utilities.setItemQuantity(item, quantity, true); + return { item, quantity, type }; + }).filter(delta => delta.quantity); + + this.itemsToUpdate = this.itemsToUpdate + .filter(item => Utilities.getItemQuantity(item) > 0 || this.itemsToNotDelete.has(item._id) || this.itemTypeMap.get(item._id) === "currency") + .filter(itemData => { + const item = this.actor.items.get(itemData._id) + return Utilities.getItemQuantity(item) !== Utilities.getItemQuantity(itemData); + }); + this.attributeDeltas = Object.fromEntries(this.attributeDeltas); + this.preCommitted = true; + return { + actorUpdates: this.actorUpdates, + itemsToCreate: this.itemsToCreate, + itemsToDelete: this.itemsToDelete, + itemsToUpdate: this.itemsToUpdate, + attributeDeltas: this.attributeDeltas, + itemDeltas: this.itemDeltas, + } + } + + async commit() { + + if (!this.preCommitted) { + this.prepare(); + } + + let itemsCreated; + const actorUuid = Utilities.getUuid(this.actor); + if (this.actor.isOwner) { + itemsCreated = await PrivateAPI._commitActorChanges(actorUuid, { + actorUpdates: this.actorUpdates, + itemsToUpdate: this.itemsToUpdate, + itemsToDelete: this.itemsToDelete, + itemsToCreate: this.itemsToCreate + }) + } else { + itemsCreated = await ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.COMMIT_ACTOR_CHANGES, actorUuid, { + actorUpdates: this.actorUpdates, + itemsToUpdate: this.itemsToUpdate, + itemsToDelete: this.itemsToDelete, + itemsToCreate: this.itemsToCreate + }); + } + + return { + attributeDeltas: this.attributeDeltas, + itemDeltas: this.itemDeltas.concat(itemsCreated.map(item => { + return { + item, + quantity: PileUtilities.canItemStack(item) ? Utilities.getItemQuantity(item) : 1 + } + })) + } + } } diff --git a/src/helpers/utilities.js b/src/helpers/utilities.js index aa98c523..fca45918 100644 --- a/src/helpers/utilities.js +++ b/src/helpers/utilities.js @@ -5,16 +5,16 @@ import { getItemFlagData } from "./pile-utilities.js"; import { deletedActorCache } from "./caches.js"; export function getActor(target) { - if (target instanceof Actor) return target; - let targetDoc = target; - if (stringIsUuid(target)) { - targetDoc = fromUuidSync(target); - if (!targetDoc && deletedActorCache.has(target)) { - return deletedActorCache.get(target); - } - } - targetDoc = getDocument(targetDoc); - return targetDoc?.character ?? targetDoc?.actor ?? targetDoc; + if (target instanceof Actor) return target; + let targetDoc = target; + if (stringIsUuid(target)) { + targetDoc = fromUuidSync(target); + if (!targetDoc && deletedActorCache.has(target)) { + return deletedActorCache.get(target); + } + } + targetDoc = getDocument(targetDoc); + return targetDoc?.character ?? targetDoc?.actor ?? targetDoc; } /** @@ -22,28 +22,28 @@ export function getActor(target) { * @returns {PlaceableObject|foundry.abstract.Document} */ export function getToken(documentUuid) { - let doc = fromUuidSync(documentUuid); - doc = doc?.token ?? doc; - return doc instanceof TokenDocument ? doc?.object ?? doc : doc; + let doc = fromUuidSync(documentUuid); + doc = doc?.token ?? doc; + return doc instanceof TokenDocument ? doc?.object ?? doc : doc; } export function getDocument(target) { - if (stringIsUuid(target)) { - target = fromUuidSync(target); - } - return target?.document ?? target; + if (stringIsUuid(target)) { + target = fromUuidSync(target); + } + return target?.document ?? target; } export function stringIsUuid(inId) { - return typeof inId === "string" - && (inId.match(/\./g) || []).length - && !inId.endsWith("."); + return typeof inId === "string" + && (inId.match(/\./g) || []).length + && !inId.endsWith("."); } export function getUuid(target) { - if (stringIsUuid(target)) return target; - const document = getDocument(target); - return document?.uuid ?? false; + if (stringIsUuid(target)) return target; + const document = getDocument(target); + return document?.uuid ?? false; } /** @@ -57,137 +57,160 @@ export function getUuid(target) { */ export function findSimilarItem(items, findItem, actorFlagData = false, ignoreVault = false) { - const itemSimilarities = game.itempiles.API.ITEM_SIMILARITIES; - - let findItemData = findItem instanceof Item ? findItem.toObject() : findItem; - findItemData = findItemData?.item ?? findItemData; - const findItemId = findItemData?._id; - - let hasUniqueKey = false; - for (let prop of CONSTANTS.ITEM_FORCED_UNIQUE_KEYS) { - if (getProperty(findItemData, prop)) { - hasUniqueKey = true; - break; - } - } - - const actorIsVault = actorFlagData ? actorFlagData?.enabled && actorFlagData?.type === CONSTANTS.PILE_TYPES.VAULT : false; - - const filteredItems = items - .filter(item => { - for (let prop of CONSTANTS.ITEM_FORCED_UNIQUE_KEYS) { - if (getProperty(item?.item ?? item, prop)) { - return false; - } - } - return true; - }) - .filter(item => { - const itemId = item instanceof Item ? item.id : item?.item?._id ?? item?._id ?? item?.id; - if (itemId && findItemId && itemId === findItemId) { - return true; - } - - if (!itemSimilarities.some(path => hasProperty(findItem, path))) { - return false; - } - - if (hasUniqueKey) { - return false; - } - - let itemData = item instanceof Item ? item.toObject() : item; - itemData = itemData?.item ?? itemData; - if (areItemsDifferent(itemData, findItemData)) { - return false; - } - - return itemSimilarities.length > 0; - }) - - let sortedItems = filteredItems; - if (actorIsVault && !ignoreVault) { - - let distanceItems = filteredItems.map(item => { - const itemX = getProperty(item, CONSTANTS.FLAGS.ITEM + ".x") ?? Infinity; - const itemY = getProperty(item, CONSTANTS.FLAGS.ITEM + ".y") ?? Infinity; - const findX = getProperty(findItem, CONSTANTS.FLAGS.ITEM + ".x") ?? Infinity; - const findY = getProperty(findItem, CONSTANTS.FLAGS.ITEM + ".y") ?? Infinity; - const distance = new Ray({ x: itemX, y: itemY }, { x: findX, y: findY }).distance; - return { distance, item }; - }); - - distanceItems.sort((a, b) => a.distance - b.distance); - distanceItems = distanceItems.filter(item => { - return item.distance === 0 && { - "default": actorFlagData?.canStackItems ?? true, - "yes": true, - "no": false - }[getItemFlagData(item)?.canStack ?? "default"]; - }).map(item => item.item); - - sortedItems = distanceItems; - - } - - return sortedItems?.[0] ?? false; + const itemSimilarities = game.itempiles.API.ITEM_SIMILARITIES; + + let findItemData = findItem instanceof Item ? findItem.toObject() : findItem; + findItemData = findItemData?.item ?? findItemData; + const findItemId = findItemData?._id; + + let hasUniqueKey = false; + for (let prop of CONSTANTS.ITEM_FORCED_UNIQUE_KEYS) { + if (getProperty(findItemData, prop)) { + hasUniqueKey = true; + break; + } + } + + const actorIsVault = actorFlagData ? actorFlagData?.enabled && actorFlagData?.type === CONSTANTS.PILE_TYPES.VAULT : false; + + const filteredItems = items + .filter(item => { + for (let prop of CONSTANTS.ITEM_FORCED_UNIQUE_KEYS) { + if (getProperty(item?.item ?? item, prop)) { + return false; + } + } + return true; + }) + .filter(item => { + const itemId = item instanceof Item ? item.id : item?.item?._id ?? item?._id ?? item?.id; + if (itemId && findItemId && itemId === findItemId) { + return true; + } + + if (!itemSimilarities.some(path => hasProperty(findItem, path))) { + return false; + } + + if (hasUniqueKey) { + return false; + } + + let itemData = item instanceof Item ? item.toObject() : item; + itemData = itemData?.item ?? itemData; + if (areItemsDifferent(itemData, findItemData)) { + return false; + } + + return itemSimilarities.length > 0; + }) + + let sortedItems = filteredItems; + if (actorIsVault && !ignoreVault) { + + let distanceItems = filteredItems.map(item => { + const itemX = getProperty(item, CONSTANTS.FLAGS.ITEM + ".x") ?? Infinity; + const itemY = getProperty(item, CONSTANTS.FLAGS.ITEM + ".y") ?? Infinity; + const findX = getProperty(findItem, CONSTANTS.FLAGS.ITEM + ".x") ?? Infinity; + const findY = getProperty(findItem, CONSTANTS.FLAGS.ITEM + ".y") ?? Infinity; + const distance = new Ray({ x: itemX, y: itemY }, { x: findX, y: findY }).distance; + return { distance, item }; + }); + + distanceItems.sort((a, b) => a.distance - b.distance); + distanceItems = distanceItems.filter(item => { + return item.distance === 0 && { + "default": actorFlagData?.canStackItems ?? true, + "yes": true, + "no": false + }[getItemFlagData(item)?.canStack ?? "default"]; + }).map(item => item.item); + + sortedItems = distanceItems; + + } + + return sortedItems?.[0] ?? false; } export function areItemsDifferent(itemA, itemB) { - const itemSimilarities = game.itempiles.API.ITEM_SIMILARITIES; - for (const path of itemSimilarities) { - if (getProperty(itemA, path) !== getProperty(itemB, path) || (!hasProperty(itemA, path) ^ !hasProperty(itemB, path))) { - return true; - } - } - return false; + const itemSimilarities = game.itempiles.API.ITEM_SIMILARITIES; + for (const path of itemSimilarities) { + if (getProperty(itemA, path) !== getProperty(itemB, path) || (!hasProperty(itemA, path) ^ !hasProperty(itemB, path))) { + return true; + } + } + return false; } export function setSimilarityProperties(obj, item) { - const itemData = item instanceof Item ? item.toObject() : item; - setProperty(obj, "_id", itemData._id); - game.itempiles.API.ITEM_SIMILARITIES.forEach(prop => { - setProperty(obj, prop, getProperty(itemData, prop)); - }) - return obj; + const itemData = item instanceof Item ? item.toObject() : item; + setProperty(obj, "_id", itemData._id); + game.itempiles.API.ITEM_SIMILARITIES.forEach(prop => { + setProperty(obj, prop, getProperty(itemData, prop)); + }) + return obj; } let itemTypesWithQuantities = false; export function refreshItemTypesThatCanStack() { - itemTypesWithQuantities = false; - getItemTypesThatCanStack(); + itemTypesWithQuantities = false; + getItemTypesThatCanStack(); } export function getItemTypesThatCanStack() { - if (!itemTypesWithQuantities) { - const unstackableItemTypes = Helpers.getSetting(SETTINGS.UNSTACKABLE_ITEM_TYPES); - const types = new Set(Object.keys(CONFIG?.Item?.dataModels ?? {}).concat(game.system.template.Item.types)); - itemTypesWithQuantities = new Set(types.filter(type => { - let itemTemplate = {}; - if (CONFIG?.Item?.dataModels?.[type]?.defineSchema !== undefined) { - itemTemplate.system = Object.entries(CONFIG.Item.dataModels[type].defineSchema()) - .map(([key, schema]) => { - return [key, schema.fields ?? true] - }) - itemTemplate.system = Object.fromEntries(itemTemplate.system); - } else if (game.system?.template?.Item?.[type]) { - itemTemplate.system = foundry.utils.deepClone(game.system.template.Item[type]); - if (itemTemplate.system?.templates?.length) { - const templates = foundry.utils.duplicate(itemTemplate.system.templates); - for (let template of templates) { - itemTemplate.system = foundry.utils.mergeObject( - itemTemplate.system, - foundry.utils.duplicate(game.system.template.Item.templates[template]) - ); - } - } - } - return hasItemQuantity(itemTemplate); - })).filter(type => !unstackableItemTypes.includes(type)); - } - return itemTypesWithQuantities; + if (!itemTypesWithQuantities) { + + itemTypesWithQuantities = new Set(); + + if (game.system.id === "custom-system-builder") { + const itemTemplates = game.items + .filter(item => item?.templateSystem?.isTemplate) + .filter(item => item.templatesystem.getKeys().has(item.system.body.contents)); + for (const item of itemTemplates) { + itemTypesWithQuantities.add(item.name); + } + } + + const unstackableItemTypes = Helpers.getSetting(SETTINGS.UNSTACKABLE_ITEM_TYPES); + const types = new Set(Object.keys(CONFIG?.Item?.dataModels ?? {}).concat(game.system.template.Item.types)); + itemTypesWithQuantities = new Set([...itemTypesWithQuantities, ...types.filter(type => { + let itemTemplate = {}; + if (CONFIG?.Item?.dataModels?.[type]?.defineSchema !== undefined) { + itemTemplate.system = Object.entries(CONFIG.Item.dataModels[type].defineSchema()) + .map(([key, schema]) => { + return [key, schema.fields ?? true] + }) + itemTemplate.system = Object.fromEntries(itemTemplate.system); + } else if (game.system?.template?.Item?.[type]) { + itemTemplate.system = foundry.utils.deepClone(game.system.template.Item[type]); + if (itemTemplate.system?.templates?.length) { + const templates = foundry.utils.duplicate(itemTemplate.system.templates); + for (let template of templates) { + itemTemplate.system = foundry.utils.mergeObject( + itemTemplate.system, + foundry.utils.duplicate(game.system.template.Item.templates[template]) + ); + } + } + } + return hasItemQuantity(itemTemplate); + })].filter(type => !unstackableItemTypes.includes(type))); + } + return itemTypesWithQuantities; +} + +export function isItemStackable(itemData) { + getItemTypesThatCanStack(); + if (game.system.id === "custom-system-builder") { + const templateItem = game.items.get(itemData.system.template); + if (templateItem) { + return itemTypesWithQuantities.has(templateItem.name) + } + } + return itemTypesWithQuantities.has(itemData.type); } /** @@ -197,8 +220,8 @@ export function getItemTypesThatCanStack() { * @returns {number} */ export function getItemQuantity(item) { - const itemData = item instanceof Item ? item.toObject() : item; - return Number(getProperty(itemData, game.itempiles.API.ITEM_QUANTITY_ATTRIBUTE) ?? 0); + const itemData = item instanceof Item ? item.toObject() : item; + return Number(getProperty(itemData, game.itempiles.API.ITEM_QUANTITY_ATTRIBUTE) ?? 0); } @@ -206,11 +229,11 @@ export function getItemQuantity(item) { * Returns whether an item has the quantity property * * @param {Item/Object} item - * @returns {number} + * @returns {Boolean} */ export function hasItemQuantity(item) { - const itemData = item instanceof Item ? item.toObject() : item; - return hasProperty(itemData, game.itempiles.API.ITEM_QUANTITY_ATTRIBUTE); + const itemData = item instanceof Item ? item.toObject() : item; + return hasProperty(itemData, game.itempiles.API.ITEM_QUANTITY_ATTRIBUTE); } /** @@ -222,16 +245,16 @@ export function hasItemQuantity(item) { * @returns {Object} */ export function setItemQuantity(itemData, quantity, requiresExistingQuantity = false) { - if (!requiresExistingQuantity || getItemTypesThatCanStack().has(itemData.type)) { - setProperty(itemData, game.itempiles.API.ITEM_QUANTITY_ATTRIBUTE, quantity) - } - return itemData; + if (!requiresExistingQuantity || getItemTypesThatCanStack().has(itemData.type) || hasItemQuantity(itemData)) { + setProperty(itemData, game.itempiles.API.ITEM_QUANTITY_ATTRIBUTE, quantity); + } + return itemData; } export function getItemCost(item) { - const itemData = item instanceof Item ? item.toObject() : item; - return getProperty(itemData, game.itempiles.API.ITEM_PRICE_ATTRIBUTE) ?? 0; + const itemData = item instanceof Item ? item.toObject() : item; + return getProperty(itemData, game.itempiles.API.ITEM_PRICE_ATTRIBUTE) ?? 0; } /** @@ -241,157 +264,157 @@ export function getItemCost(item) { * @returns {Array} */ export function getTokensAtLocation(position) { - const tokens = [...canvas.tokens.placeables].filter(token => token?.mesh?.visible); - return tokens.filter(token => { - return position.x >= token.x && position.x < (token.x + (token.document.width * canvas.grid.size)) - && position.y >= token.y && position.y < (token.y + (token.document.height * canvas.grid.size)); - }); + const tokens = [...canvas.tokens.placeables].filter(token => token?.mesh?.visible); + return tokens.filter(token => { + return position.x >= token.x && position.x < (token.x + (token.document.width * canvas.grid.size)) + && position.y >= token.y && position.y < (token.y + (token.document.height * canvas.grid.size)); + }); } export function distance_between_rect(p1, p2) { - const x1 = p1.x; - const y1 = p1.y; - const x1b = p1.x + p1.w; - const y1b = p1.y + p1.h; - - const x2 = p2.x; - const y2 = p2.y; - const x2b = p2.x + p2.w; - const y2b = p2.y + p2.h; - - const left = x2b < x1; - const right = x1b < x2; - const bottom = y2b < y1; - const top = y1b < y2; - - if (top && left) { - return distance_between({ x: x1, y: y1b }, { x: x2b, y: y2 }); - } else if (left && bottom) { - return distance_between({ x: x1, y: y1 }, { x: x2b, y: y2b }); - } else if (bottom && right) { - return distance_between({ x: x1b, y: y1 }, { x: x2, y: y2b }); - } else if (right && top) { - return distance_between({ x: x1b, y: y1b }, { x: x2, y: y2 }); - } else if (left) { - return x1 - x2b; - } else if (right) { - return x2 - x1b; - } else if (bottom) { - return y1 - y2b; - } else if (top) { - return y2 - y1b; - } - - return 0; + const x1 = p1.x; + const y1 = p1.y; + const x1b = p1.x + p1.w; + const y1b = p1.y + p1.h; + + const x2 = p2.x; + const y2 = p2.y; + const x2b = p2.x + p2.w; + const y2b = p2.y + p2.h; + + const left = x2b < x1; + const right = x1b < x2; + const bottom = y2b < y1; + const top = y1b < y2; + + if (top && left) { + return distance_between({ x: x1, y: y1b }, { x: x2b, y: y2 }); + } else if (left && bottom) { + return distance_between({ x: x1, y: y1 }, { x: x2b, y: y2b }); + } else if (bottom && right) { + return distance_between({ x: x1b, y: y1 }, { x: x2, y: y2b }); + } else if (right && top) { + return distance_between({ x: x1b, y: y1b }, { x: x2, y: y2 }); + } else if (left) { + return x1 - x2b; + } else if (right) { + return x2 - x1b; + } else if (bottom) { + return y1 - y2b; + } else if (top) { + return y2 - y1b; + } + + return 0; } export function distance_between(a, b) { - return new Ray(a, b).distance; + return new Ray(a, b).distance; } export function grids_between_tokens(a, b) { - return Math.floor(distance_between_rect(a, b) / canvas.grid.size) + 1 + return Math.floor(distance_between_rect(a, b) / canvas.grid.size) + 1 } export function tokens_close_enough(a, b, maxDistance) { - const distance = grids_between_tokens(a, b); - return maxDistance >= distance; + const distance = grids_between_tokens(a, b); + return maxDistance >= distance; } export function refreshAppsWithDocument(doc, callback) { - const apps = Object.values(ui.windows).filter(app => app.id.endsWith(doc.id)); - for (const app of apps) { - if (app[callback]) { - app[callback](); - } - } + const apps = Object.values(ui.windows).filter(app => app.id.endsWith(doc.id)); + for (const app of apps) { + if (app[callback]) { + app[callback](); + } + } } export async function runMacro(macroId, macroData) { - // Credit to Otigon, Zhell, Gazkhan and MrVauxs for the code in this section - let macro; - if (macroId.startsWith("Compendium")) { - let packArray = macroId.split("."); - let compendium = game.packs.get(`${packArray[1]}.${packArray[2]}`); - if (!compendium) { - throw Helpers.custom_error(`Compendium ${packArray[1]}.${packArray[2]} was not found`); - } - let findMacro = (await compendium.getDocuments()).find(m => m.name === packArray[3] || m.id === packArray[3]) - if (!findMacro) { - throw Helpers.custom_error(`The "${packArray[3]}" macro was not found in Compendium ${packArray[1]}.${packArray[2]}`); - } - macro = new Macro(findMacro?.toObject()); - macro.ownership.default = CONST.DOCUMENT_PERMISSION_LEVELS.OWNER; - } else { - macro = game.macros.getName(macroId); - if (!macro) { - throw Helpers.custom_error(`Could not find macro with name "${macroId}"`); - } - } - - let result = false; - try { - result = await macro.execute(macroData); - } catch (err) { - Helpers.custom_warning(`Error when executing macro ${macroId}!\n${err}`, true); - } - - return result; + // Credit to Otigon, Zhell, Gazkhan and MrVauxs for the code in this section + let macro; + if (macroId.startsWith("Compendium")) { + let packArray = macroId.split("."); + let compendium = game.packs.get(`${packArray[1]}.${packArray[2]}`); + if (!compendium) { + throw Helpers.custom_error(`Compendium ${packArray[1]}.${packArray[2]} was not found`); + } + let findMacro = (await compendium.getDocuments()).find(m => m.name === packArray[3] || m.id === packArray[3]) + if (!findMacro) { + throw Helpers.custom_error(`The "${packArray[3]}" macro was not found in Compendium ${packArray[1]}.${packArray[2]}`); + } + macro = new Macro(findMacro?.toObject()); + macro.ownership.default = CONST.DOCUMENT_PERMISSION_LEVELS.OWNER; + } else { + macro = game.macros.getName(macroId); + if (!macro) { + throw Helpers.custom_error(`Could not find macro with name "${macroId}"`); + } + } + + let result = false; + try { + result = await macro.execute(macroData); + } catch (err) { + Helpers.custom_warning(`Error when executing macro ${macroId}!\n${err}`, true); + } + + return result; } export function getOwnedCharacters(user = false) { - user = user || game.user; - return game.actors.filter(actor => { - return actor.ownership?.[user.id] === CONST.DOCUMENT_PERMISSION_LEVELS.OWNER - && actor.prototypeToken.actorLink; - }) - .sort((a, b) => { - return b._stats.modifiedTime - a._stats.modifiedTime; - }); + user = user || game.user; + return game.actors.filter(actor => { + return actor.ownership?.[user.id] === CONST.DOCUMENT_PERMISSION_LEVELS.OWNER + && actor.prototypeToken.actorLink; + }) + .sort((a, b) => { + return b._stats.modifiedTime - a._stats.modifiedTime; + }); } export function getUserCharacter(user = false) { - user = user || game.user; - return user.character - || (user.isGM ? false : (getOwnedCharacters(user)?.[0] ?? false)); + user = user || game.user; + return user.character + || (user.isGM ? false : (getOwnedCharacters(user)?.[0] ?? false)); } export async function createFoldersFromNames(folders, type = "Actor") { - let lastFolder = false; - for (const folder of folders) { - let actualFolder = game.folders.getName(folder); - if (!actualFolder) { - const folderData = { name: folder, type, sorting: 'a' }; - if (lastFolder) { - folderData.parent = lastFolder.id; - } - actualFolder = await Folder.create(folderData); - } - lastFolder = actualFolder; - } - - if (lastFolder) { - return lastFolder; - } + let lastFolder = false; + for (const folder of folders) { + let actualFolder = game.folders.getName(folder); + if (!actualFolder) { + const folderData = { name: folder, type, sorting: 'a' }; + if (lastFolder) { + folderData.parent = lastFolder.id; + } + actualFolder = await Folder.create(folderData); + } + lastFolder = actualFolder; + } + + if (lastFolder) { + return lastFolder; + } } export function getSourceActorFromDropData(dropData) { - if (dropData.uuid) { - const doc = fromUuidSync(dropData.uuid); - return doc instanceof Actor ? doc : doc.parent; - } else if (dropData.tokenId) { - if (dropData.sceneId) { - const uuid = `Scene.${dropData.sceneId}.Token.${dropData.tokenId}`; - return fromUuidSync(uuid)?.actor; - } - return canvas.tokens.get(dropData.tokenId).actor; - } else if (dropData.actorId) { - return game.actors.get(dropData.actorId); - } - return false; + if (dropData.uuid) { + const doc = fromUuidSync(dropData.uuid); + return doc instanceof Actor ? doc : doc.parent; + } else if (dropData.tokenId) { + if (dropData.sceneId) { + const uuid = `Scene.${dropData.sceneId}.Token.${dropData.tokenId}`; + return fromUuidSync(uuid)?.actor; + } + return canvas.tokens.get(dropData.tokenId).actor; + } else if (dropData.actorId) { + return game.actors.get(dropData.actorId); + } + return false; } diff --git a/src/hotkeys.js b/src/hotkeys.js index 578233d5..f193d3c3 100644 --- a/src/hotkeys.js +++ b/src/hotkeys.js @@ -4,93 +4,93 @@ import PrivateAPI from "./API/private-api.js"; import * as Helpers from "./helpers/helpers.js"; const HOTKEYS = { - FORCE_DEFAULT_SHEET: "force-open-item-pile-inventory", - DROP: "force-drop-item", - DROP_ONE: "force-drop-one-item" + FORCE_DEFAULT_SHEET: "force-open-item-pile-inventory", + DROP: "force-drop-item", + DROP_ONE: "force-drop-one-item" } export const hotkeyActionState = { - get openPileInventory() { - const down = game.keybindings.get(CONSTANTS.MODULE_NAME, HOTKEYS.FORCE_DEFAULT_SHEET).some(keybind => { - return window.keyboard.downKeys.has(keybind?.key); - }); - return ( - (!down && !game.settings.get(CONSTANTS.MODULE_NAME, "invertSheetOpen")) - || - (down && game.settings.get(CONSTANTS.MODULE_NAME, "invertSheetOpen")) - ); - }, + get openPileInventory() { + const down = game.keybindings.get(CONSTANTS.MODULE_NAME, HOTKEYS.FORCE_DEFAULT_SHEET).some(keybind => { + return window.keyboard.downKeys.has(keybind?.key); + }); + return ( + (!down && !game.settings.get(CONSTANTS.MODULE_NAME, "invertSheetOpen")) + || + (down && game.settings.get(CONSTANTS.MODULE_NAME, "invertSheetOpen")) + ); + }, - get forceDropItem() { - return game.keybindings.get(CONSTANTS.MODULE_NAME, HOTKEYS.DROP).some(key => { - return window.keyboard.downKeys.has(key); - }); - }, + get forceDropItem() { + return game.keybindings.get(CONSTANTS.MODULE_NAME, HOTKEYS.DROP).some(key => { + return window.keyboard.downKeys.has(key); + }); + }, - get forceDropOneItem() { - return game.keybindings.get(CONSTANTS.MODULE_NAME, HOTKEYS.DROP).some(key => { - return window.keyboard.downKeys.has(key); - }); - } + get forceDropOneItem() { + return game.keybindings.get(CONSTANTS.MODULE_NAME, HOTKEYS.DROP).some(key => { + return window.keyboard.downKeys.has(key); + }); + } } export function registerHotkeysPre() { - game.keybindings.register(CONSTANTS.MODULE_NAME, HOTKEYS.FORCE_DEFAULT_SHEET, { - name: "Force open inventory modifier", - editable: [ - { key: "ControlLeft" }, - ] - }); + game.keybindings.register(CONSTANTS.MODULE_NAME, HOTKEYS.FORCE_DEFAULT_SHEET, { + name: "Force open inventory modifier", + editable: [ + { key: "ControlLeft" }, + ] + }); - game.keybindings.register(CONSTANTS.MODULE_NAME, HOTKEYS.DROP, { - name: "Force drop item (GM only) modifier", - editable: [ - { key: "ShiftLeft" }, - ] - }); + game.keybindings.register(CONSTANTS.MODULE_NAME, HOTKEYS.DROP, { + name: "Force drop item (GM only) modifier", + editable: [ + { key: "ShiftLeft" }, + ] + }); - game.keybindings.register(CONSTANTS.MODULE_NAME, HOTKEYS.DROP_ONE, { - name: "Force drop one item modifier", - editable: [ - { key: "AltLeft" }, - ] - }); + game.keybindings.register(CONSTANTS.MODULE_NAME, HOTKEYS.DROP_ONE, { + name: "Force drop one item modifier", + editable: [ + { key: "AltLeft" }, + ] + }); } export function registerHotkeysPost() { - if (!game.user.isGM) { - let clicked = false; - window.addEventListener("mousedown", (event) => { - if (!canvas.ready) return; - if (!(canvas.activeLayer instanceof TokenLayer)) return; - if (game.activeTool !== "select") return; - const hover = document.elementFromPoint(event.clientX, event.clientY); - if (!hover || (hover.id !== "board")) return; - if (event.button !== 0) return; + if (!game.user.isGM) { + let clicked = false; + window.addEventListener("mousedown", (event) => { + if (!canvas.ready) return; + if (!(canvas.activeLayer instanceof TokenLayer)) return; + if (game.activeTool !== "select") return; + const hover = document.elementFromPoint(event.clientX, event.clientY); + if (!hover || (hover.id !== "board")) return; + if (event.button !== 0) return; - const pos = Helpers.getCanvasMouse().getLocalPosition(canvas.app.stage); - const tokens = Utilities.getTokensAtLocation(pos) - .filter(token => { - const canView = token._canView(game.user); - const canSee = token.visible || game.user.isGM; - return !canView && canSee; - }); - if (!tokens.length) return; - tokens.sort((a, b) => b.zIndex - a.zIndex); - const token = Utilities.getDocument(tokens[0]); + const pos = Helpers.getCanvasMouse().getLocalPosition(canvas.app.stage); + const tokens = Utilities.getTokensAtLocation(pos) + .filter(token => { + const canView = token._canView(game.user); + const canSee = token.visible || game.user.isGM; + return !canView && canSee; + }); + if (!tokens.length) return; + tokens.sort((a, b) => b.zIndex - a.zIndex); + const token = Utilities.getDocument(tokens[0]); - if (clicked === token) { - clicked = false; - return PrivateAPI._itemPileClicked(token); - } + if (clicked === token) { + clicked = false; + return PrivateAPI._itemPileClicked(token); + } - clicked = token; - setTimeout(() => { - clicked = false; - }, 500); - }); - } + clicked = token; + setTimeout(() => { + clicked = false; + }, 500); + }); + } } diff --git a/src/libwrapper.js b/src/libwrapper.js index 39ca128d..bcdd0915 100644 --- a/src/libwrapper.js +++ b/src/libwrapper.js @@ -7,55 +7,55 @@ import { SYSTEMS } from "./systems.js"; export default function registerLibwrappers() { - libWrapper.register(CONSTANTS.MODULE_NAME, 'Token.prototype._onClickLeft2', function (wrapped, ...args) { - if (PileUtilities.isValidItemPile(this.document) && hotkeyActionState.openPileInventory) { - return PrivateAPI._itemPileClicked(this.document); - } - return wrapped(...args); - }, "MIXED"); - - const versionIsEleven = foundry.utils.isNewerVersion(game.version, "10.999"); - - const overrideMethod = versionIsEleven - ? `DocumentDirectory.prototype._onClickEntryName` - : `SidebarDirectory.prototype._onClickDocumentName`; - - libWrapper.register(CONSTANTS.MODULE_NAME, overrideMethod, function (wrapped, event) { - event.preventDefault(); - const element = event.currentTarget; - if (!(this instanceof Compendium)) { - const documentId = element.parentElement.dataset.documentId; - const document = this.constructor.collection.get(documentId); - if (PileUtilities.isValidItemPile(document)) { - const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_DIRECTORY_CLICK, document); - if (hookResult === false) return false; - } - } - return wrapped(event); - }, "MIXED"); - - Hooks.on(CONSTANTS.HOOKS.PRE_RENDER_SHEET, (doc, forced, options) => { - const renderItemPileInterface = forced && !options?.bypassItemPiles && PileUtilities.isValidItemPile(doc) && hotkeyActionState.openPileInventory; - if (!renderItemPileInterface) return; - game.itempiles.API.renderItemPileInterface(doc, { useDefaultCharacter: true }); - return false; - }) - - libWrapper.register(CONSTANTS.MODULE_NAME, `ActorSheet.prototype.render`, function (wrapped, forced, options, ...args) { - const renderItemPileInterface = Hooks.call(CONSTANTS.HOOKS.PRE_RENDER_SHEET, this.document, forced, options) === false; - if (this._state > Application.RENDER_STATES.NONE) { - if (renderItemPileInterface) { - wrapped(forced, options, ...args) - } else { - return wrapped(forced, options, ...args) - } - } - if (renderItemPileInterface) return; - return wrapped(forced, options, ...args); - }, "MIXED"); - - if (SYSTEMS.DATA.SHEET_OVERRIDES) { - SYSTEMS.DATA.SHEET_OVERRIDES(); - } + libWrapper.register(CONSTANTS.MODULE_NAME, 'Token.prototype._onClickLeft2', function (wrapped, ...args) { + if (PileUtilities.isValidItemPile(this.document) && hotkeyActionState.openPileInventory) { + return PrivateAPI._itemPileClicked(this.document); + } + return wrapped(...args); + }, "MIXED"); + + const versionIsEleven = foundry.utils.isNewerVersion(game.version, "10.999"); + + const overrideMethod = versionIsEleven + ? `DocumentDirectory.prototype._onClickEntryName` + : `SidebarDirectory.prototype._onClickDocumentName`; + + libWrapper.register(CONSTANTS.MODULE_NAME, overrideMethod, function (wrapped, event) { + event.preventDefault(); + const element = event.currentTarget; + if (!(this instanceof Compendium)) { + const documentId = element.parentElement.dataset.documentId; + const document = this.constructor.collection.get(documentId); + if (PileUtilities.isValidItemPile(document)) { + const hookResult = Helpers.hooks.call(CONSTANTS.HOOKS.PILE.PRE_DIRECTORY_CLICK, document); + if (hookResult === false) return false; + } + } + return wrapped(event); + }, "MIXED"); + + Hooks.on(CONSTANTS.HOOKS.PRE_RENDER_SHEET, (doc, forced, options) => { + const renderItemPileInterface = forced && !options?.bypassItemPiles && PileUtilities.isValidItemPile(doc) && hotkeyActionState.openPileInventory; + if (!renderItemPileInterface) return; + game.itempiles.API.renderItemPileInterface(doc, { useDefaultCharacter: true }); + return false; + }) + + libWrapper.register(CONSTANTS.MODULE_NAME, `ActorSheet.prototype.render`, function (wrapped, forced, options, ...args) { + const renderItemPileInterface = Hooks.call(CONSTANTS.HOOKS.PRE_RENDER_SHEET, this.document, forced, options) === false; + if (this._state > Application.RENDER_STATES.NONE) { + if (renderItemPileInterface) { + wrapped(forced, options, ...args) + } else { + return wrapped(forced, options, ...args) + } + } + if (renderItemPileInterface) return; + return wrapped(forced, options, ...args); + }, "MIXED"); + + if (SYSTEMS.DATA.SHEET_OVERRIDES) { + SYSTEMS.DATA.SHEET_OVERRIDES(); + } } diff --git a/src/migrations.js b/src/migrations.js index 30ab9250..d3b06bd5 100644 --- a/src/migrations.js +++ b/src/migrations.js @@ -6,368 +6,364 @@ import { findOrCreateItemInCompendium } from "./helpers/compendium-utilities.js" export default async function runMigrations() { - for (const version of Object.keys(migrations)) { - try { - await migrations[version](version); - } catch (err) { - console.error(err); - custom_warning(`Something went wrong when migrating to version ${version}. Please check the console for the error!`, true) - } - } + for (const version of Object.keys(migrations)) { + try { + await migrations[version](version); + } catch (err) { + console.error(err); + custom_warning(`Something went wrong when migrating to version ${version}. Please check the console for the error!`, true) + } + } } function getItemPileActorsOfLowerVersion(version) { - return PileUtilities.getItemPileActors((a) => { - const actorFlagVersion = getProperty(a, CONSTANTS.FLAGS.VERSION) || "1.0.0"; - return getProperty(a, CONSTANTS.FLAGS.PILE)?.enabled && isNewerVersion(version, actorFlagVersion); - }) + return PileUtilities.getItemPileActors((a) => { + const actorFlagVersion = getProperty(a, CONSTANTS.FLAGS.VERSION) || "1.0.0"; + return getProperty(a, CONSTANTS.FLAGS.PILE)?.enabled && isNewerVersion(version, actorFlagVersion); + }) } function getItemPileTokensOfLowerVersion(version) { - return PileUtilities.getItemPileTokens((token) => { - try { - const actorFlagVersion = getProperty(token, CONSTANTS.FLAGS.VERSION) || "1.0.0"; - return token.actor && isNewerVersion(version, actorFlagVersion); - } catch (err) { - return false; - } - }) + return PileUtilities.getItemPileTokens((token) => { + try { + const actorFlagVersion = getProperty(token, CONSTANTS.FLAGS.VERSION) || "1.0.0"; + return token.actor && isNewerVersion(version, actorFlagVersion); + } catch (err) { + return false; + } + }) } function filterValidItems(items, version) { - return items.filter(item => { - const itemFlagVersion = getProperty(item, CONSTANTS.FLAGS.VERSION); - return (itemFlagVersion && isNewerVersion(version, itemFlagVersion)) - || (!itemFlagVersion && hasProperty(item, CONSTANTS.FLAGS.ITEM)); - }); + return items.filter(item => { + const itemFlagVersion = getProperty(item, CONSTANTS.FLAGS.VERSION); + return (itemFlagVersion && isNewerVersion(version, itemFlagVersion)) || (!itemFlagVersion && hasProperty(item, CONSTANTS.FLAGS.ITEM)); + }); } function getActorValidItems(actor, version) { - return filterValidItems(actor.items, version); + return filterValidItems(actor.items, version); +} + +/** + * @param version + * @param {Function} callback + * @returns {Promise} + */ +async function updateActors(version, callback) { + + const actorUpdates = getItemPileActorsOfLowerVersion(version).map(actor => { + let flags = getProperty(actor, CONSTANTS.FLAGS.PILE); + const flagData = { + [CONSTANTS.FLAGS.PILE]: callback(flags, actor), [CONSTANTS.FLAGS.VERSION]: version + } + if (actor.actorLink) { + flagData["token"] = foundry.utils.deepClone(flagData); + } + return { + _id: actor.id, ...flagData + }; + }); + + if (actorUpdates.length) { + console.log(`Item Piles | Migrating ${actorUpdates.length} actors to version ${version}...`) + await Actor.updateDocuments(actorUpdates); + } + +} + +/** + * @param version + * @param {Function} callback + * @returns {Promise} + */ +async function updateTokens(version, callback) { + + const { validTokensOnScenes } = getItemPileTokensOfLowerVersion(version); + + for (const [sceneId, tokens] of validTokensOnScenes) { + const scene = game.scenes.get(sceneId) + const updates = tokens.map(token => ({ + _id: token.id, + [CONSTANTS.FLAGS.PILE]: callback(getProperty(token, CONSTANTS.FLAGS.PILE)), + [CONSTANTS.FLAGS.VERSION]: version, + })); + if (updates.length) { + console.log(`Item Piles | Migrating ${updates.length} tokens on scene "${sceneId}" to version ${version}...`); + await scene.updateEmbeddedDocuments("Token", updates); + } + } + +} + +/** + * @param version + * @param {Function} callback + * @returns {Promise} + */ +async function updateItems(version, callback) { + + const gameItems = filterValidItems(game.items, version); + + const gameItemUpdates = gameItems.map(item => { + const flags = getProperty(item, CONSTANTS.FLAGS.ITEM); + if (!flags) return false; + return PileUtilities.updateItemData(item, { + flags: callback(flags) + }, { version, returnUpdate: true }); + }).filter(Boolean); + + if (gameItemUpdates.length) { + console.log(`Item Piles | Migrating ${gameItemUpdates.length} items to version ${version}...`); + await Item.updateDocuments(gameItemUpdates); + } + + const actors = getItemPileActorsOfLowerVersion(version); + + const actorItemUpdates = actors.map(actor => { + + const itemPileItems = getActorValidItems(actor, version) + + return { + actor, update: { + _id: actor.id, [CONSTANTS.FLAGS.VERSION]: version + }, items: itemPileItems.map(item => { + const flags = getProperty(item, CONSTANTS.FLAGS.ITEM); + if (!flags) return false; + return PileUtilities.updateItemData(item, { + flags: callback(flags) + }, { version, returnUpdate: true }); + }).filter(Boolean) + } + + }); + + if (actorItemUpdates.length) { + console.log(`Item Piles | Migrating ${actorItemUpdates.length} item pile actors' items to version ${version}...`) + } + + await Actor.updateDocuments(actorItemUpdates.map(data => data.update)) + + for (const { actor, items } of actorItemUpdates) { + await actor.updateEmbeddedDocuments("Item", items); + } + + const { validTokensOnScenes } = getItemPileTokensOfLowerVersion(version); + + for (const [sceneId, tokens] of validTokensOnScenes) { + + const scene = game.scenes.get(sceneId); + + const updates = tokens.map(token => { + const itemPileItems = getActorValidItems(token.actor, version); + return { + token, update: { + _id: token.id, [CONSTANTS.FLAGS.VERSION]: version + }, items: itemPileItems.map(item => { + const flags = getProperty(item, CONSTANTS.FLAGS.ITEM); + if (!flags) return false; + return PileUtilities.updateItemData(item, { + flags: callback(flags) + }, { version, returnUpdate: true }); + }).filter(Boolean) + } + }); + + console.log(`Item Piles | Migrating ${updates.length} tokens on scene "${sceneId}" to version ${version}...`); + + await scene.updateEmbeddedDocuments("Token", updates.map(data => data.update)); + + for (const { token, itemUpdates } of updates) { + await token.actor.updateEmbeddedDocuments("Item", itemUpdates); + } + + } + } const migrations = { - "2.4.0": async (version) => { - - const actors = getItemPileActorsOfLowerVersion(version); - - const actorUpdates = actors.map(a => { - const flagData = { - [CONSTANTS.FLAGS.PILE]: PileUtilities.cleanFlagData(PileUtilities.migrateFlagData(a)), - [CONSTANTS.FLAGS.VERSION]: version - } - if (a.actorLink) { - flagData["token"] = foundry.utils.deepClone(flagData); - } - return { - _id: a.id, - ...flagData - }; - }); - - if (actorUpdates.length) { - console.log(`Item Piles | Migrating ${actorUpdates.length} actors to version ${version}...`) - } - - await Actor.updateDocuments(actorUpdates); - - const { allTokensOnScenes, validTokensOnScenes } = getItemPileTokensOfLowerVersion(version); - - for (const [sceneId, tokens] of validTokensOnScenes) { - const scene = game.scenes.get(sceneId) - const updates = []; - for (const token of tokens) { - const flagData = { - [CONSTANTS.FLAGS.PILE]: PileUtilities.cleanFlagData(PileUtilities.migrateFlagData(token.actor)), - [CONSTANTS.FLAGS.VERSION]: version, - } - updates.push({ - _id: token.id, - ...flagData - }); - } - console.log(`Item Piles | Migrating ${updates.length} tokens on scene "${sceneId}" to version ${version}...`); - await scene.updateEmbeddedDocuments("Token", updates); - } - - const invalidTokensOnScenes = allTokensOnScenes.map(([scene, tokens]) => [ - scene, - tokens.filter(token => { - try { - const actorFlagVersion = getProperty(token, CONSTANTS.FLAGS.VERSION) || "1.0.0"; - return !token.actor || isNewerVersion(version, actorFlagVersion); - } catch (err) { - return true; - } - }) - ]).filter(([_, tokens]) => tokens.length); - - for (const [sceneId, tokens] of invalidTokensOnScenes) { - - const scene = game.scenes.get(sceneId); - - let deletions = []; - let updates = []; - for (const token of tokens) { - - const flagData = { - [CONSTANTS.FLAGS.PILE]: PileUtilities.cleanFlagData(PileUtilities.migrateFlagData(token)), - [CONSTANTS.FLAGS.VERSION]: version, - } - - let tokenActor = game.actors.get(token.actorId); - if (!tokenActor) { - tokenActor = game.actors.get(getSetting(SETTINGS.DEFAULT_ITEM_PILE_ACTOR_ID)); - } - if (!tokenActor) { - deletions.push(token.id); - continue; - } - - const update = { - _id: token.id, - actorLink: false, - actorId: tokenActor.id, - [CONSTANTS.ACTOR_DELTA_PROPERTY]: { - items: [] - }, - ...flagData - } - - for (let itemData of token[CONSTANTS.ACTOR_DELTA_PROPERTY]?.items ?? []) { - const item = await Item.implementation.create(itemData, { temporary: true }); - update[CONSTANTS.ACTOR_DELTA_PROPERTY].items.push(item.toObject()); - } - - updates.push(update); - - await token.update({ - actorLink: true - }); - } - - await scene.updateEmbeddedDocuments("Token", updates); - await scene.deleteEmbeddedDocuments("Token", deletions); - - console.log(`Item Piles | Fixing ${updates.length} tokens on scene "${sceneId}" to version ${version}...`); - - } - - if (invalidTokensOnScenes.length && invalidTokensOnScenes.some(([sceneId]) => sceneId === game.user.viewedScene)) { - ui.notifications.notify("Item Piles | Attempted to fix some broken tokens on various scenes. If the current scene fails to load, please refresh.") - } - }, - - "2.4.17": async (version) => { - - const items = filterValidItems(game.items, version); - - const itemUpdates = items.map(item => { - const flags = getProperty(item, CONSTANTS.FLAGS.ITEM); - flags.infiniteQuantity = "default"; - return PileUtilities.updateItemData(item, { flags }, { version, returnUpdate: true }); - }); - - if (itemUpdates.length) { - console.log(`Item Piles | Migrating ${itemUpdates.length} items to version ${version}...`); - await Item.updateDocuments(itemUpdates); - } - - const actors = getItemPileActorsOfLowerVersion(version); - - const actorItemUpdates = actors.map(actor => { - - const itemPileItems = getActorValidItems(actor, version) - - return { - actor, - items: itemPileItems.map(item => { - const flags = getProperty(item, CONSTANTS.FLAGS.ITEM); - if (!flags) return false; - flags.infiniteQuantity = "default"; - return PileUtilities.updateItemData(item, { flags }, { version, returnUpdate: true }); - }).filter(Boolean) - } - - }).filter(update => update.items.length); - - if (actorItemUpdates.length) { - console.log(`Item Piles | Migrating ${actorItemUpdates.length} item pile actors' items to version ${version}...`) - } - - for (const { actor, items } of actorItemUpdates) { - await actor.updateEmbeddedDocuments("Item", items); - } - - const { validTokensOnScenes } = getItemPileTokensOfLowerVersion(version); - - for (const [sceneId, tokens] of validTokensOnScenes) { - - const updates = tokens.map(token => { - const itemPileItems = getActorValidItems(token.actor, version); - return { - token, - update: { - [CONSTANTS.FLAGS.VERSION]: version - }, - items: itemPileItems.map(item => { - const flags = PileUtilities.getItemFlagData(item); - flags.infiniteQuantity = "default"; - return PileUtilities.updateItemData(item, { flags }, { version, returnUpdate: true }); - }) - } - }); - - console.log(`Item Piles | Migrating ${updates.length} tokens on scene "${sceneId}" to version ${version}...`); - - for (const { token, update, items } of updates) { - await token.update(update); - await token.actor.updateEmbeddedDocuments("Item", items) - } - - } - - }, - - "2.6.1": async (version) => { - - const actors = getItemPileActorsOfLowerVersion(version); - - const actorUpdates = actors.map(a => { - const flags = getProperty(a, CONSTANTS.FLAGS.PILE); - if (flags?.itemTypePriceModifiers) { - flags.itemTypePriceModifiers = flags.itemTypePriceModifiers.map(priceModifier => { - const custom = Object.keys(CONFIG.Item.typeLabels).indexOf(priceModifier.type) === -1; - priceModifier.category = custom ? priceModifier.type : ""; - priceModifier.type = custom ? "custom" : priceModifier.type; - return priceModifier; - }) - } - const flagData = { - [CONSTANTS.FLAGS.PILE]: flags, - [CONSTANTS.FLAGS.VERSION]: version - } - if (a.actorLink) { - flagData["token"] = foundry.utils.deepClone(flagData); - } - return { - _id: a.id, - ...flagData - }; - }); - - if (actorUpdates.length) { - console.log(`Item Piles | Migrating ${actorUpdates.length} actors to version ${version}...`) - } - - await Actor.updateDocuments(actorUpdates); - - const { validTokensOnScenes } = getItemPileTokensOfLowerVersion(version); - - for (const [sceneId, tokens] of validTokensOnScenes) { - const scene = game.scenes.get(sceneId) - const updates = []; - for (const token of tokens) { - const flags = getProperty(token, CONSTANTS.FLAGS.PILE); - if (flags?.itemTypePriceModifiers) { - flags.itemTypePriceModifiers = flags.itemTypePriceModifiers.map(priceModifier => { - const custom = Object.keys(CONFIG.Item.typeLabels).indexOf(priceModifier.type) === -1; - priceModifier.category = custom ? priceModifier.type : ""; - priceModifier.type = custom ? "custom" : priceModifier.type; - return priceModifier; - }) - } - const flagData = { - [CONSTANTS.FLAGS.PILE]: flags, - [CONSTANTS.FLAGS.VERSION]: version, - } - updates.push({ - _id: token.id, - ...flagData - }); - } - console.log(`Item Piles | Migrating ${updates.length} tokens on scene "${sceneId}" to version ${version}...`); - await scene.updateEmbeddedDocuments("Token", updates); - } - - }, - - "2.7.18": async (version) => { - - const actors = getItemPileActorsOfLowerVersion(version); - - const recursivelyAddItemsToCompendium = async (itemData) => { - const flagData = PileUtilities.getItemFlagData(itemData); - for (const priceGroup of flagData?.prices ?? []) { - for (const price of priceGroup) { - if (price.type !== "item" || !price.data.item) continue; - const compendiumItemUuid = (await recursivelyAddItemsToCompendium(price.data.item).uuid); - price.data = { uuid: compendiumItemUuid }; - } - } - setProperty(itemData, CONSTANTS.FLAGS.ITEM, PileUtilities.cleanItemFlagData(flagData, { addRemoveFlag: true })); - return findOrCreateItemInCompendium(itemData); - } - - const getActorItemUpdates = async (actorItems) => { - const items = actorItems.filter(item => PileUtilities.getItemFlagData(item).prices.length); - const updates = []; - for (const item of items) { - const flagData = PileUtilities.getItemFlagData(item); - let update = false; - if (!flagData.prices.length) continue; - for (const priceGroup of flagData.prices) { - for (const price of priceGroup) { - if (price.type !== "item" || !price.data.item) continue; - const compendiumItem = await recursivelyAddItemsToCompendium(price.data.item); - price.data = { uuid: compendiumItem.uuid }; - update = true; - } - } - if (update) { - updates.push({ - _id: item.id, - [CONSTANTS.FLAGS.VERSION]: version, - [CONSTANTS.FLAGS.ITEM]: PileUtilities.cleanItemFlagData(flagData) - }) - } - } - return updates; - } - - const updates = await getActorItemUpdates(filterValidItems(game.items, version)); - if (updates.length) { - console.log(`Item Piles | Migrating ${updates.length} items to version ${version}...`); - await Item.updateDocuments(updates); - } - - let updatedActors = 0; - for (const actor of actors) { - const items = getActorValidItems(actor, version); - const updates = await getActorItemUpdates(items); - if (updates.length) { - await actor.updateEmbeddedDocuments("Item", updates); - updatedActors++; - } - } - if (updatedActors) { - console.log(`Item Piles | Migrating ${updatedActors} actors with out of date items to version ${version}...`); - } - - const { validTokensOnScenes } = getItemPileTokensOfLowerVersion(version); - - for (const [sceneId, tokens] of validTokensOnScenes) { - let updatedTokens = 0; - for (const token of tokens) { - const items = getActorValidItems(token.actor, version); - const updates = await getActorItemUpdates(items); - if (updates.length) { - updatedTokens++; - await token.actor.updateEmbeddedDocuments("Item", updates); - } - } - console.log(`Item Piles | Migrating ${updatedTokens} tokens on scene "${sceneId}" to version ${version}...`); - } - } + "2.4.0": async (version) => { + + await updateActors(version, (flags) => { + return PileUtilities.cleanFlagData(flags); + }); + + await updateTokens(version, (flags) => { + return PileUtilities.cleanFlagData(flags); + }); + + const { invalidTokensOnScenes } = getItemPileTokensOfLowerVersion(version); + + for (const [sceneId, tokens] of invalidTokensOnScenes) { + + const scene = game.scenes.get(sceneId); + + let deletions = []; + let updates = []; + for (const token of tokens) { + + const flagData = { + [CONSTANTS.FLAGS.PILE]: PileUtilities.cleanFlagData(PileUtilities.migrateFlagData(token)), + [CONSTANTS.FLAGS.VERSION]: version, + } + + let tokenActor = game.actors.get(token.actorId); + if (!tokenActor) { + tokenActor = game.actors.get(getSetting(SETTINGS.DEFAULT_ITEM_PILE_ACTOR_ID)); + } + if (!tokenActor) { + deletions.push(token.id); + continue; + } + + const update = { + _id: token.id, actorLink: false, actorId: tokenActor.id, [CONSTANTS.ACTOR_DELTA_PROPERTY]: { + items: [] + }, ...flagData + } + + for (let itemData of token[CONSTANTS.ACTOR_DELTA_PROPERTY]?.items ?? []) { + const item = await Item.implementation.create(itemData, { temporary: true }); + update[CONSTANTS.ACTOR_DELTA_PROPERTY].items.push(item.toObject()); + } + + updates.push(update); + + await token.update({ + actorLink: true + }); + } + + await scene.updateEmbeddedDocuments("Token", updates); + await scene.deleteEmbeddedDocuments("Token", deletions); + + console.log(`Item Piles | Fixing ${updates.length} tokens on scene "${sceneId}" to version ${version}...`); + + } + + if (invalidTokensOnScenes.length && invalidTokensOnScenes.some(([sceneId]) => sceneId === game.user.viewedScene)) { + ui.notifications.notify("Item Piles | Attempted to fix some broken tokens on various scenes. If the current scene fails to load, please refresh.") + } + }, + + "2.4.17": async (version) => { + + await updateItems(version, (flags) => { + flags.infiniteQuantity = "default"; + return flags; + }); + + }, + + "2.6.1": async (version) => { + + const flagUpdateCallback = (flags) => { + if (flags?.itemTypePriceModifiers) { + flags.itemTypePriceModifiers = flags.itemTypePriceModifiers.map(priceModifier => { + const custom = Object.keys(CONFIG.Item.typeLabels).indexOf(priceModifier.type) === -1; + priceModifier.category = custom ? priceModifier.type : ""; + priceModifier.type = custom ? "custom" : priceModifier.type; + return priceModifier; + }) + } + return flags; + } + + await updateActors(version, flagUpdateCallback); + await updateTokens(version, flagUpdateCallback); + + }, + + "2.7.18": async (version) => { + + const actors = getItemPileActorsOfLowerVersion(version); + + const recursivelyAddItemsToCompendium = async (itemData) => { + const flagData = PileUtilities.getItemFlagData(itemData); + for (const priceGroup of flagData?.prices ?? []) { + for (const price of priceGroup) { + if (price.type !== "item" || !price.data.item) continue; + const compendiumItemUuid = (await recursivelyAddItemsToCompendium(price.data.item).uuid); + price.data = { uuid: compendiumItemUuid }; + } + } + setProperty(itemData, CONSTANTS.FLAGS.ITEM, PileUtilities.cleanItemFlagData(flagData, { addRemoveFlag: true })); + return findOrCreateItemInCompendium(itemData); + } + + const getActorItemUpdates = async (actorItems) => { + const items = actorItems.filter(item => PileUtilities.getItemFlagData(item).prices.length); + const updates = []; + for (const item of items) { + const flagData = PileUtilities.getItemFlagData(item); + let update = false; + if (!flagData.prices.length) continue; + for (const priceGroup of flagData.prices) { + for (const price of priceGroup) { + if (price.type !== "item" || !price.data.item) continue; + const compendiumItem = await recursivelyAddItemsToCompendium(price.data.item); + price.data = { uuid: compendiumItem.uuid }; + update = true; + } + } + if (update) { + updates.push({ + _id: item.id, + [CONSTANTS.FLAGS.VERSION]: version, + [CONSTANTS.FLAGS.ITEM]: PileUtilities.cleanItemFlagData(flagData) + }) + } + } + return updates; + } + + const updates = await getActorItemUpdates(filterValidItems(game.items, version)); + if (updates.length) { + console.log(`Item Piles | Migrating ${updates.length} items to version ${version}...`); + await Item.updateDocuments(updates); + } + + let updatedActors = 0; + for (const actor of actors) { + const items = getActorValidItems(actor, version); + const updates = await getActorItemUpdates(items); + if (updates.length) { + await actor.updateEmbeddedDocuments("Item", updates); + updatedActors++; + } + } + if (updatedActors) { + console.log(`Item Piles | Migrating ${updatedActors} actors with out of date items to version ${version}...`); + } + + const { validTokensOnScenes } = getItemPileTokensOfLowerVersion(version); + + for (const [sceneId, tokens] of validTokensOnScenes) { + let updatedTokens = 0; + for (const token of tokens) { + const items = getActorValidItems(token.actor, version); + const updates = await getActorItemUpdates(items); + if (updates.length) { + updatedTokens++; + await token.actor.updateEmbeddedDocuments("Item", updates); + } + } + console.log(`Item Piles | Migrating ${updatedTokens} tokens on scene "${sceneId}" to version ${version}...`); + } + }, + + "2.8.2": async (version) => { + + const flagUpdateCallback = (flags) => { + if (flags?.canStackItems !== undefined) { + flags.canStackItems = flags.canStackItems ? "yes" : "no"; + } + return flags; + } + + await updateActors(version, flagUpdateCallback); + await updateTokens(version, flagUpdateCallback); + + } }; diff --git a/src/module.js b/src/module.js index a2593d97..6fac9706 100644 --- a/src/module.js +++ b/src/module.js @@ -34,28 +34,29 @@ Hooks.once("init", async () => { setupCaches(); applyShims(); setupPlugins("init"); + + game.itempiles = { + API, + hooks: CONSTANTS.HOOKS, + flags: CONSTANTS.FLAGS, + pile_types: CONSTANTS.PILE_TYPES, + pile_flag_defaults: CONSTANTS.PILE_DEFAULTS, + item_flag_defaults: CONSTANTS.ITEM_DEFAULTS, + apps: { + ItemPileConfig, + ItemEditor + } + }; + window.ItemPiles = { + API: API + }; + }); Hooks.once("ready", () => { setTimeout(() => { - game.itempiles = { - API, - hooks: CONSTANTS.HOOKS, - flags: CONSTANTS.FLAGS, - pile_types: CONSTANTS.PILE_TYPES, - pile_flag_defaults: CONSTANTS.PILE_DEFAULTS, - item_flag_defaults: CONSTANTS.ITEM_DEFAULTS, - apps: { - ItemPileConfig, - ItemEditor - } - }; - window.ItemPiles = { - API: API - }; - if (game.user.isGM) { if (!game.modules.get('lib-wrapper')?.active) { let word = "install and activate"; diff --git a/src/plugins/base-plugin.js b/src/plugins/base-plugin.js index 78a08759..fe446397 100644 --- a/src/plugins/base-plugin.js +++ b/src/plugins/base-plugin.js @@ -2,45 +2,45 @@ import * as Helpers from "../helpers/helpers.js"; export default class BasePlugin { - invalidVersionError = "" - minVersionError = "" + invalidVersionError = "" + minVersionError = "" - constructor(pluginName, minVersion, invalidVersion) { - this.pluginName = pluginName; - this.minVersion = minVersion; - this.invalidVersion = invalidVersion; - this.initialized = false; - this.initialize(); - } + constructor(pluginName, minVersion, invalidVersion) { + this.pluginName = pluginName; + this.minVersion = minVersion; + this.invalidVersion = invalidVersion; + this.initialized = false; + this.initialize(); + } - initialize() { + initialize() { - if (!game.modules.get(this.pluginName)?.active) { - return; - } + if (!game.modules.get(this.pluginName)?.active) { + return; + } - if (game.modules.get(this.pluginName).version === this.invalidVersion) { - if (this.invalidVersionError) { - throw Helpers.custom_error(this.invalidVersionError); - } - return; - } + if (game.modules.get(this.pluginName).version === this.invalidVersion) { + if (this.invalidVersionError) { + throw Helpers.custom_error(this.invalidVersionError); + } + return; + } - if (isNewerVersion(this.minVersion, game.modules.get(this.pluginName).version)) { - if (this.minVersionError) { - throw Helpers.custom_error(this.minVersionError); - } - return; - } + if (isNewerVersion(this.minVersion, game.modules.get(this.pluginName).version)) { + if (this.minVersionError) { + throw Helpers.custom_error(this.minVersionError); + } + return; + } - this.registerHooks(); + this.registerHooks(); - this.initialized = true; + this.initialized = true; - } + } - registerHooks() { + registerHooks() { - } + } } diff --git a/src/plugins/levels-3d-preview.js b/src/plugins/levels-3d-preview.js index c14f9405..80539737 100644 --- a/src/plugins/levels-3d-preview.js +++ b/src/plugins/levels-3d-preview.js @@ -3,15 +3,15 @@ import PrivateAPI from "../API/private-api.js"; export default class Levels3dPreview extends BasePlugin { - registerHooks() { + registerHooks() { - Hooks.on("3DCanvasConfig", (config) => { - config.INTERACTIONS.dropFunctions.Item = async function (event, data) { - canvas.tokens.activate(); - return PrivateAPI._dropData(canvas, data); - } - }); + Hooks.on("3DCanvasConfig", (config) => { + config.INTERACTIONS.dropFunctions.Item = async function (event, data) { + canvas.tokens.activate(); + return PrivateAPI._dropData(canvas, data); + } + }); - } + } } diff --git a/src/plugins/main.js b/src/plugins/main.js index 29644233..2524d407 100644 --- a/src/plugins/main.js +++ b/src/plugins/main.js @@ -3,32 +3,32 @@ import Levels3dPreview from "./levels-3d-preview.js"; import RarityColors from "./rarity-colors.js"; export const Plugins = { - "foundryvtt-simple-calendar": { - on: "ready", - data: null, - class: SimpleCalendarPlugin, - minVersion: "2.0.0", - invalidVersion: "v1.3.75" - }, - "levels-3d-preview": { - on: "init", - data: null, - class: Levels3dPreview, - minVersion: "4.9.6" - }, - "rarity-colors": { - on: "init", - data: null, - class: RarityColors, - minVersion: "0.3.6" - } + "foundryvtt-simple-calendar": { + on: "ready", + data: null, + class: SimpleCalendarPlugin, + minVersion: "2.0.0", + invalidVersion: "v1.3.75" + }, + "levels-3d-preview": { + on: "init", + data: null, + class: Levels3dPreview, + minVersion: "4.9.6" + }, + "rarity-colors": { + on: "init", + data: null, + class: RarityColors, + minVersion: "0.3.6" + } } export function setupPlugins(hook) { - for (const [plugin, pluginData] of Object.entries(Plugins).filter(e => e[1].on === hook)) { - if (!game.modules.get(plugin)?.active) { - continue; - } - pluginData.data = new pluginData.class(plugin, pluginData.minVersion, pluginData?.invalidVersion); - } + for (const [plugin, pluginData] of Object.entries(Plugins).filter(e => e[1].on === hook)) { + if (!game.modules.get(plugin)?.active) { + continue; + } + pluginData.data = new pluginData.class(plugin, pluginData.minVersion, pluginData?.invalidVersion); + } } diff --git a/src/plugins/rarity-colors.js b/src/plugins/rarity-colors.js index 9a95728e..e1e68c2c 100644 --- a/src/plugins/rarity-colors.js +++ b/src/plugins/rarity-colors.js @@ -2,20 +2,20 @@ import BasePlugin from "./base-plugin.js"; export default class RarityColors extends BasePlugin { - minVersionError = "Rarity Colors is out of date to be compatible with Item Piles, please update to 0.3.6 soon as possible."; + minVersionError = "Rarity Colors is out of date to be compatible with Item Piles, please update to 0.3.6 soon as possible."; - getItemColor(item) { + getItemColor(item) { - if (game.system.id !== "dnd5e") return false; + if (game.system.id !== "dnd5e") return false; - if (!game.modules.get(this.pluginName)?.api?.getColorFromItem) return false; + if (!game.modules.get(this.pluginName)?.api?.getColorFromItem) return false; - try { - return game.modules.get(this.pluginName)?.api?.getColorFromItem(item); - } catch (err) { - return false; - } + try { + return game.modules.get(this.pluginName)?.api?.getColorFromItem(item); + } catch (err) { + return false; + } - } + } } diff --git a/src/plugins/simple-calendar.js b/src/plugins/simple-calendar.js index 2727d3fd..45f75d17 100644 --- a/src/plugins/simple-calendar.js +++ b/src/plugins/simple-calendar.js @@ -8,182 +8,252 @@ let previousState; export default class SimpleCalendarPlugin extends BasePlugin { - invalidVersionError = "Simple Calendar version 1.3.75 is installed, but Item Piles requires version 2.0.0 or above. The author made a mistake, and you will need to reinstall the Simple Calendar module."; - minVersionError = "Simple Calendar is out of date to be compatible with Item Piles, please update as soon as possible."; - - registerHooks() { - previousState = { - dateTime: window.SimpleCalendar.api.currentDateTime(), - weekday: window.SimpleCalendar.api.getCurrentWeekday(), - timestamp: window.SimpleCalendar.api.dateToTimestamp({}) - } - Hooks.on(window.SimpleCalendar.Hooks.DateTimeChange, () => { - ItemPileStore.notifyAllOfChanges("updateOpenCloseStatus"); - this.handleTimePassed(); - }); - } - - async handleTimePassed() { - - const newState = { - dateTime: window.SimpleCalendar.api.currentDateTime(), - weekday: window.SimpleCalendar.api.getCurrentWeekday(), - timestamp: window.SimpleCalendar.api.dateToTimestamp({}) - } - - const currentCalendar = window.SimpleCalendar.api.getCurrentCalendar(); - const numWeekdays = currentCalendar.weekdays.length; - - const notes = window.SimpleCalendar.api.getNotes() - .filter(note => getProperty(note, "flags.foundryvtt-simple-calendar.noteData.categories")?.length) - .map(note => { - const flags = getProperty(note, "flags.foundryvtt-simple-calendar.noteData"); - let timestampData = { - year: flags.startDate.year, - month: flags.startDate.month, - day: flags.startDate.day, - hour: flags.allDay ? 0 : flags.startDate.hour, - minute: flags.allDay ? 0 : flags.startDate.minute, - seconds: flags.allDay ? 0 : flags.startDate.seconds, - }; - switch (flags?.repeats) { - case window.SimpleCalendar.api.NoteRepeat.Weekly: - const noteWeekDay = window.SimpleCalendar.api.timestampToDate(window.SimpleCalendar.api.dateToTimestamp(timestampData)).dayOfTheWeek - 1; - const currentWeekDay = window.SimpleCalendar.api.timestampToDate(newState.timestamp).dayOfTheWeek - 1; - let weekdayCountDifference = currentWeekDay - noteWeekDay; - if (weekdayCountDifference < 0) { - weekdayCountDifference += numWeekdays - } - timestampData.year = newState.dateTime.year; - timestampData.month = newState.dateTime.month; - timestampData.day = newState.dateTime.day; - const weekInSeconds = SimpleCalendar.api.timestampPlusInterval(0, { day: 1 }) * weekdayCountDifference; - const timestamp = window.SimpleCalendar.api.dateToTimestamp(timestampData) - weekInSeconds; - timestampData.day = window.SimpleCalendar.api.timestampToDate(timestamp).day; - break; - - case window.SimpleCalendar.api.NoteRepeat.Monthly: - timestampData.year = newState.dateTime.year; - timestampData.month = newState.dateTime.month; - break; - - case window.SimpleCalendar.api.NoteRepeat.Yearly: - timestampData.year = newState.dateTime.year; - break; - } - return { - document: note, - flags, - dateTime: timestampData, - timestamp: window.SimpleCalendar.api.dateToTimestamp(timestampData) - } - }) - .filter(note => { - return note.timestamp > previousState.timestamp && note.timestamp <= newState.timestamp; - }); - - const categories = new Set(notes.map(note => note.flags?.categories ?? []).deepFlatten()); - - const actors = PileUtilities.getItemPileActors((actor) => { - const flags = PileUtilities.getActorFlagData(actor, getProperty(actor, CONSTANTS.FLAGS.PILE)); - if (flags.type !== CONSTANTS.PILE_TYPES.MERCHANT) return false; - return merchantRefreshFilter(flags, newState, previousState, categories); - }); - - const { validTokensOnScenes } = PileUtilities.getItemPileTokens((token) => { - const flags = PileUtilities.getActorFlagData(token, getProperty(token, CONSTANTS.FLAGS.PILE)); - if (flags.type !== CONSTANTS.PILE_TYPES.MERCHANT) return false; - return merchantRefreshFilter(flags, newState, previousState, categories); - }); - - previousState = newState; - - for (const actor of actors) { - await this.refreshActorItems(actor, notes); - } - - for (const [_, tokens] of validTokensOnScenes) { - for (const token of tokens) { - await this.refreshActorItems(token.actor, notes); - } - } - } - - async refreshActorItems(actor, notes) { - - const actorTransaction = new Transaction(actor); - - const actorItems = game.itempiles.API.getActorItems(actor); - const newActorItems = await PileUtilities.rollMerchantTables({ actor }); - - await actorTransaction.appendItemChanges(actorItems.filter(item => { - const itemFlags = PileUtilities.getItemFlagData(item); - return !itemFlags.keepOnMerchant && !itemFlags.keepIfZero; - }), { remove: true }); - - await actorTransaction.appendItemChanges(actorItems.filter(item => { - const itemFlags = PileUtilities.getItemFlagData(item); - return !itemFlags.keepOnMerchant && itemFlags.keepIfZero; - }), { remove: true, keepIfZero: true }); - - await actorTransaction.appendItemChanges(newActorItems.map(entry => ({ - item: entry.item, quantity: entry.quantity, flags: entry.flags - }))); - - const commit = actorTransaction.prepare(); - - const result = Hooks.call(CONSTANTS.HOOKS.PILE.PRE_REFRESH_INVENTORY, actor, commit, notes) - if (result === false) return; - - await actorTransaction.commit(); - - } + invalidVersionError = "Simple Calendar version 1.3.75 is installed, but Item Piles requires version 2.0.0 or above. The author made a mistake, and you will need to reinstall the Simple Calendar module."; + minVersionError = "Simple Calendar is out of date to be compatible with Item Piles, please update as soon as possible."; + + registerHooks() { + previousState = { + dateTime: window.SimpleCalendar.api.currentDateTime(), + weekday: window.SimpleCalendar.api.getCurrentWeekday(), + timestamp: window.SimpleCalendar.api.dateToTimestamp({}) + } + Hooks.on(window.SimpleCalendar.Hooks.DateTimeChange, () => { + ItemPileStore.notifyAllOfChanges("updateOpenCloseStatus"); + this.handleTimePassed(); + }); + } + + async handleTimePassed() { + + const newState = { + dateTime: window.SimpleCalendar.api.currentDateTime(), + weekday: window.SimpleCalendar.api.getCurrentWeekday(), + timestamp: window.SimpleCalendar.api.dateToTimestamp({}) + } + + const currentCalendar = window.SimpleCalendar.api.getCurrentCalendar(); + const numWeekdays = currentCalendar.weekdays.length; + + const notes = window.SimpleCalendar.api.getNotes() + .filter(note => getProperty(note, "flags.foundryvtt-simple-calendar.noteData.categories")?.length) + .map(note => { + const flags = getProperty(note, "flags.foundryvtt-simple-calendar.noteData"); + let timestampData = { + year: flags.startDate.year, + month: flags.startDate.month, + day: flags.startDate.day, + hour: flags.allDay ? 0 : flags.startDate.hour, + minute: flags.allDay ? 0 : flags.startDate.minute, + seconds: flags.allDay ? 0 : flags.startDate.seconds, + }; + switch (flags?.repeats) { + case window.SimpleCalendar.api.NoteRepeat.Weekly: + const noteWeekDay = window.SimpleCalendar.api.timestampToDate(window.SimpleCalendar.api.dateToTimestamp(timestampData)).dayOfTheWeek - 1; + const currentWeekDay = window.SimpleCalendar.api.timestampToDate(newState.timestamp).dayOfTheWeek - 1; + let weekdayCountDifference = currentWeekDay - noteWeekDay; + if (weekdayCountDifference < 0) { + weekdayCountDifference += numWeekdays + } + timestampData.year = newState.dateTime.year; + timestampData.month = newState.dateTime.month; + timestampData.day = newState.dateTime.day; + const weekInSeconds = SimpleCalendar.api.timestampPlusInterval(0, { day: 1 }) * weekdayCountDifference; + const timestamp = window.SimpleCalendar.api.dateToTimestamp(timestampData) - weekInSeconds; + timestampData.day = window.SimpleCalendar.api.timestampToDate(timestamp).day; + break; + + case window.SimpleCalendar.api.NoteRepeat.Monthly: + timestampData.year = newState.dateTime.year; + timestampData.month = newState.dateTime.month; + break; + + case window.SimpleCalendar.api.NoteRepeat.Yearly: + timestampData.year = newState.dateTime.year; + break; + } + return { + document: note, + flags, + dateTime: timestampData, + timestamp: window.SimpleCalendar.api.dateToTimestamp(timestampData) + } + }) + .filter(note => { + return note.timestamp > previousState.timestamp && note.timestamp <= newState.timestamp; + }); + + const categories = new Set(notes.map(note => note.flags?.categories ?? []).deepFlatten()); + + await this.hideMerchantTokens(); + await this.refreshMerchantInventories(newState, previousState, categories); + + } + + async hideMerchantTokens() { + + const actors = PileUtilities.getItemPileActors((actor) => { + if (!PileUtilities.isItemPileMerchant(actor)) return false; + const flags = PileUtilities.getActorFlagData(actor); + return flags.hideTokenWhenClosed && PileUtilities.isMerchantClosed(actor); + }); + + const actorTokens = actors.map(actor => actor.getActiveTokens()) + .deepFlatten() + .reduce((acc, token) => { + if (!acc[token.parent.id]) acc[token.parent.id] = []; + acc[token.parent.id].push(token); + return acc; + }, {}); + + const { validTokensOnScenes } = PileUtilities.getItemPileTokens((token) => { + if (!PileUtilities.isItemPileMerchant(token)) return false; + const flags = PileUtilities.getActorFlagData(token); + return flags.hideTokenWhenClosed && PileUtilities.isMerchantClosed(token); + }); + + await Actor.updateDocuments(actors.map(actor => { + const flags = PileUtilities.getActorFlagData(actor); + const closed = PileUtilities.isMerchantClosed(actor); + if (flags.openTimes.status !== "auto") { + flags.openTimes.status = closed ? "closed" : "open"; + } + const cleanFlags = PileUtilities.cleanFlagData(flags); + return { + _id: actor.id, + [CONSTANTS.FLAGS.PILE]: cleanFlags, + hidden: closed + } + })); + + for (const [sceneId, tokens] of validTokensOnScenes) { + const scene = game.scenes.get(sceneId); + + const updates = tokens.map(token => { + const flags = PileUtilities.getActorFlagData(token); + const closed = PileUtilities.isMerchantClosed(token); + if (flags.openTimes.status !== "auto") { + flags.openTimes.status = closed ? "closed" : "open"; + } + const cleanFlags = PileUtilities.cleanFlagData(flags); + return { + _id: token.id, + [CONSTANTS.FLAGS.PILE]: cleanFlags, + hidden: closed + } + }).concat(actorTokens.map(token => { + const closed = PileUtilities.isMerchantClosed(token); + return { + _id: token.id, + hidden: closed + } + })) + + await scene.updateEmbeddedDocuments("Token", updates); + } + } + + async refreshMerchantInventories(newState, previousState, categories) { + + const actors = PileUtilities.getItemPileActors((actor) => { + if (!PileUtilities.isItemPileMerchant(actor)) return false; + const flags = PileUtilities.getActorFlagData(actor); + return merchantRefreshFilter(flags, newState, previousState, categories); + }); + + const { validTokensOnScenes } = PileUtilities.getItemPileTokens((token) => { + if (!PileUtilities.isItemPileMerchant(token)) return false; + const flags = PileUtilities.getActorFlagData(token); + return merchantRefreshFilter(flags, newState, previousState, categories); + }); + + previousState = newState; + + for (const actor of actors) { + await this.refreshActorItems(actor, notes); + } + + for (const [_, tokens] of validTokensOnScenes) { + for (const token of tokens) { + await this.refreshActorItems(token.actor, notes); + } + } + } + + async refreshActorItems(actor, notes) { + + const actorTransaction = new Transaction(actor); + + const actorItems = game.itempiles.API.getActorItems(actor); + const newActorItems = await PileUtilities.rollMerchantTables({ actor }); + + await actorTransaction.appendItemChanges(actorItems.filter(item => { + const itemFlags = PileUtilities.getItemFlagData(item); + return !itemFlags.keepOnMerchant && !itemFlags.keepIfZero; + }), { remove: true }); + + await actorTransaction.appendItemChanges(actorItems.filter(item => { + const itemFlags = PileUtilities.getItemFlagData(item); + return !itemFlags.keepOnMerchant && itemFlags.keepIfZero; + }), { remove: true, keepIfZero: true }); + + await actorTransaction.appendItemChanges(newActorItems.map(entry => ({ + item: entry.item, quantity: entry.quantity, flags: entry.flags + }))); + + const commit = actorTransaction.prepare(); + + const result = Hooks.call(CONSTANTS.HOOKS.PILE.PRE_REFRESH_INVENTORY, actor, commit, notes) + if (result === false) return; + + await actorTransaction.commit(); + + } } function merchantRefreshFilter(flags, newState, previousState, categories) { - const openTimesEnabled = flags.openTimes.enabled; + const openTimesEnabled = flags.openTimes.enabled; - if (!openTimesEnabled) return false; + if (!openTimesEnabled) return false; - const openTimes = flags.openTimes.open; - const closeTimes = flags.openTimes.close; + const openTimes = flags.openTimes.open; + const closeTimes = flags.openTimes.close; - const openHour = openTimesEnabled ? openTimes.hour : 0; - const openMinute = openTimesEnabled ? openTimes.minute : 0; - const closeHour = openTimesEnabled ? closeTimes.hour : 0; - const closeMinute = openTimesEnabled ? closeTimes.minute : 0; + const openHour = openTimesEnabled ? openTimes.hour : 0; + const openMinute = openTimesEnabled ? openTimes.minute : 0; + const closeHour = openTimesEnabled ? closeTimes.hour : 0; + const closeMinute = openTimesEnabled ? closeTimes.minute : 0; - const openingTime = Number(openHour.toString() + "." + openMinute.toString()); - const closingTime = Number(closeHour.toString() + "." + closeMinute.toString()); - const previousTime = Number(previousState.dateTime.hour.toString() + "." + previousState.dateTime.minute.toString()); - const currentTime = Number(newState.dateTime.hour.toString() + "." + newState.dateTime.minute.toString()); + const openingTime = Number(openHour.toString() + "." + openMinute.toString()); + const closingTime = Number(closeHour.toString() + "." + closeMinute.toString()); + const previousTime = Number(previousState.dateTime.hour.toString() + "." + previousState.dateTime.minute.toString()); + const currentTime = Number(newState.dateTime.hour.toString() + "." + newState.dateTime.minute.toString()); - const wasOpen = openingTime > closingTime - ? (previousTime >= openingTime || previousTime <= closingTime) - : (previousTime >= openingTime && previousTime <= closingTime); + const wasOpen = openingTime > closingTime + ? (previousTime >= openingTime || previousTime <= closingTime) + : (previousTime >= openingTime && previousTime <= closingTime); - const isOpen = openingTime > closingTime - ? (currentTime >= openingTime || currentTime <= closingTime) - : (currentTime >= openingTime && currentTime <= closingTime); + const isOpen = openingTime > closingTime + ? (currentTime >= openingTime || currentTime <= closingTime) + : (currentTime >= openingTime && currentTime <= closingTime); - const allWeekdays = window.SimpleCalendar.api.getAllWeekdays(); - const dayLength = SimpleCalendar.api.timestampPlusInterval(0, { day: 1 }); + const allWeekdays = window.SimpleCalendar.api.getAllWeekdays(); + const dayLength = SimpleCalendar.api.timestampPlusInterval(0, { day: 1 }); - const daysPassed = Math.floor((newState.timestamp - previousState.timestamp) / dayLength); + const daysPassed = Math.floor((newState.timestamp - previousState.timestamp) / dayLength); - const currentWeekday = newState.weekday; + const currentWeekday = newState.weekday; - const shouldRefreshOnCurrentWeekday = flags.refreshItemsDays.includes(currentWeekday.name); - const shouldRefreshPastWeekday = flags.refreshItemsDays.length > 0 && daysPassed >= allWeekdays.length; + const shouldRefreshOnCurrentWeekday = flags.refreshItemsDays.includes(currentWeekday.name); + const shouldRefreshPastWeekday = flags.refreshItemsDays.length > 0 && daysPassed >= allWeekdays.length; - const shouldRefresh = ( - flags.refreshItemsOnOpen || - shouldRefreshOnCurrentWeekday || - shouldRefreshPastWeekday || - categories.intersection(new Set(flags.refreshItemsHolidays)).size > 0 - ); + const shouldRefresh = ( + flags.refreshItemsOnOpen || + shouldRefreshOnCurrentWeekday || + shouldRefreshPastWeekday || + categories.intersection(new Set(flags.refreshItemsHolidays)).size > 0 + ); - return (!wasOpen && isOpen) && shouldRefresh; + return (!wasOpen && isOpen) && shouldRefresh; } diff --git a/src/settings.js b/src/settings.js index 799c4034..55391691 100644 --- a/src/settings.js +++ b/src/settings.js @@ -32,6 +32,16 @@ export async function applyDefaultSettings() { await patchCurrencySettings(); } +export async function applySoftMigration(migrationKey) { + const migrationSettings = SYSTEMS.DATA.SOFT_MIGRATIONS[migrationKey]; + for (const [key, values] of Object.entries(migrationSettings)) { + const settingsKey = SETTINGS[key]; + const currentSettingValue = Helpers.getSetting(settingsKey); + await Helpers.setSetting(settingsKey, foundry.utils.mergeObject(currentSettingValue, values)); + } + await Helpers.setSetting(SETTINGS.SYSTEM_VERSION, SYSTEMS.DATA.VERSION); +} + export async function patchCurrencySettings() { const currencies = Helpers.getSetting(SETTINGS.CURRENCIES); for (let currency of currencies) { @@ -88,7 +98,10 @@ export async function checkSystem() { const currentVersion = Helpers.getSetting(SETTINGS.SYSTEM_VERSION); const newVersion = SYSTEMS.DATA.VERSION; Helpers.debug(`Comparing system version - Current: ${currentVersion} - New: ${newVersion}`) - if (isNewerVersion(newVersion, currentVersion)) { + if (SYSTEMS.DATA.SOFT_MIGRATIONS[currentVersion + "-" + newVersion]) { + Helpers.debug(`Applying soft migration for ${game.system.title}`); + await applySoftMigration(currentVersion + "-" + newVersion); + } else if (isNewerVersion(newVersion, currentVersion)) { Helpers.debug(`Applying system settings for ${game.system.title}`) await applyDefaultSettings(); } diff --git a/src/stores/item-pile-store.js b/src/stores/item-pile-store.js index 4d1b3f15..3bb79703 100644 --- a/src/stores/item-pile-store.js +++ b/src/stores/item-pile-store.js @@ -13,483 +13,483 @@ const __STORES__ = new Map(); export default class ItemPileStore { - constructor(application, source, recipient = false, { recipientPileData = false } = {}) { + constructor(application, source, recipient = false, { recipientPileData = false } = {}) { - this.subscriptions = []; + this.subscriptions = []; - this.interactionId = randomID(); - this.application = application; + this.interactionId = randomID(); + this.application = application; - this.uuid = Utilities.getUuid(source); - this.actor = Utilities.getActor(source); - this.document = new TJSDocument(this.actor); + this.uuid = Utilities.getUuid(source); + this.actor = Utilities.getActor(source); + this.document = new TJSDocument(this.actor); - this.recipient = recipient ? Utilities.getActor(recipient) : false; - this.recipientDocument = recipient ? new TJSDocument(this.recipient) : new TJSDocument(); - this.recipientPileData = writable(recipientPileData); + this.recipient = recipient ? Utilities.getActor(recipient) : false; + this.recipientDocument = recipient ? new TJSDocument(this.recipient) : new TJSDocument(); + this.recipientPileData = writable(recipientPileData); - this.pileData = writable({}); - this.shareData = writable({}); + this.pileData = writable({}); + this.shareData = writable({}); - this.recipientPileData = writable({}) - this.recipientShareData = writable({}); + this.recipientPileData = writable({}) + this.recipientShareData = writable({}); - this.deleted = writable(false); + this.deleted = writable(false); - this.search = writable(""); - this.editQuantities = writable(true); + this.search = writable(""); + this.editQuantities = writable(true); - this.allItems = writable([]); - this.attributes = writable([]); + this.allItems = writable([]); + this.attributes = writable([]); - this.items = writable([]); - this.visibleItems = writable([]); + this.items = writable([]); + this.visibleItems = writable([]); - this.pileCurrencies = writable([]); - this.recipientCurrencies = writable([]); + this.pileCurrencies = writable([]); + this.recipientCurrencies = writable([]); - this.currencies = writable([]); - this.allCurrencies = writable([]); + this.currencies = writable([]); + this.allCurrencies = writable([]); - this.itemsPerCategory = writable({}); - this.categories = writable([]); - this.itemCategories = writable([]); + this.itemsPerCategory = writable({}); + this.categories = writable([]); + this.itemCategories = writable([]); - this.numItems = writable(0); - this.numCurrencies = writable(0); + this.numItems = writable(0); + this.numCurrencies = writable(0); - this.name = writable(""); - this.img = writable(""); + this.name = writable(""); + this.img = writable(""); - __STORES__.set(this.uuid, this); + __STORES__.set(this.uuid, this); - } + } - get ItemClass() { - return PileItem; - }; + get ItemClass() { + return PileItem; + }; - get AttributeClass() { - return PileAttribute; - }; + get AttributeClass() { + return PileAttribute; + }; - get searchDelay() { - return 200; - } + get searchDelay() { + return 200; + } - static make(...args) { - const store = new this(...args) - store.setupStores(); - store.setupSubscriptions(); - return store; - } + static make(...args) { + const store = new this(...args) + store.setupStores(); + store.setupSubscriptions(); + return store; + } - static getStore(actor) { - const uuid = Utilities.getUuid(actor); - return __STORES__.get(uuid); - } + static getStore(actor) { + const uuid = Utilities.getUuid(actor); + return __STORES__.get(uuid); + } - static notifyChanges(event, actor, ...args) { - const store = this.getStore(actor); - if (store) { - store[event](...args); - } - } + static notifyChanges(event, actor, ...args) { + const store = this.getStore(actor); + if (store) { + store[event](...args); + } + } - static notifyAllOfChanges(event, ...args) { - for (const store of __STORES__.values()) { - if (store[event]) { - store[event](...args); - } - } - } + static notifyAllOfChanges(event, ...args) { + for (const store of __STORES__.values()) { + if (store[event]) { + store[event](...args); + } + } + } - setupStores() { + setupStores() { - this.pileData.set(PileUtilities.getActorFlagData(this.actor)); - this.shareData.set(SharingUtilities.getItemPileSharingData(this.actor)); - - this.recipientPileData.set(this.recipient ? PileUtilities.getActorFlagData(this.recipient) : {}); - this.recipientShareData.set(this.recipient ? SharingUtilities.getItemPileSharingData(this.recipient) : {}); + this.pileData.set(PileUtilities.getActorFlagData(this.actor)); + this.shareData.set(SharingUtilities.getItemPileSharingData(this.actor)); + + this.recipientPileData.set(this.recipient ? PileUtilities.getActorFlagData(this.recipient) : {}); + this.recipientShareData.set(this.recipient ? SharingUtilities.getItemPileSharingData(this.recipient) : {}); - this.deleted.set(false); + this.deleted.set(false); - this.search.set(""); - this.editQuantities.set(!this.recipient); - - this.allItems.set([]); - this.attributes.set([]); - - this.items.set([]); - this.visibleItems.set([]); - - this.pileCurrencies.set(PileUtilities.getActorCurrencies(this.actor, { getAll: true })); - this.recipientCurrencies.set(this.recipient ? PileUtilities.getActorCurrencies(this.recipient, { getAll: true }) : []); - - this.currencies.set([]); - this.allCurrencies.set([]); - - this.itemsPerCategory.set({}); - this.categories.set([]); - this.itemCategories.set([]); - - this.numItems.set(0); - this.numCurrencies.set(0); - - this.name.set(""); - this.img.set(""); - - } - - getActorImage() { - return this.actor.img; - } - - setupSubscriptions() { - - this.subscribeTo(this.document, () => { - const { data } = this.document.updateOptions; - if (hasProperty(data, CONSTANTS.FLAGS.SHARING)) { - this.shareData.set(SharingUtilities.getItemPileSharingData(this.actor)); - this.refreshItems(); - } - if (hasProperty(data, CONSTANTS.FLAGS.PILE)) { - this.pileData.set(PileUtilities.getActorFlagData(this.actor)); - this.pileCurrencies.set(PileUtilities.getActorCurrencies(this.actor, { getAll: true })); - this.refreshItems(); - } - this.name.set(this.actor.name); - this.img.set(this.getActorImage()); - }); - - if (this.recipientDocument) { - this.subscribeTo(this.recipientDocument, () => { - const { data } = this.document.updateOptions; - if (hasProperty(data, CONSTANTS.FLAGS.SHARING)) { - this.recipientShareData.set(SharingUtilities.getItemPileSharingData(this.recipient)); - this.refreshItems(); - } - if (hasProperty(data, CONSTANTS.FLAGS.PILE)) { - this.recipientPileData.set(PileUtilities.getActorFlagData(this.recipient)); - this.recipientCurrencies.set(PileUtilities.getActorCurrencies(this.recipient, { getAll: true })); - this.refreshItems(); - } - }); - } - - const items = []; - const attributes = []; - - const pileData = PileUtilities.isValidItemPile(this.actor) || !this.recipient ? get(this.pileData) : get(this.recipientPileData); - - PileUtilities.getActorItems(this.actor, { itemFilters: pileData.overrideItemFilters }).map(item => { - items.push(new this.ItemClass(this, item)); - }); - - PileUtilities.getActorCurrencies(this.actor, { forActor: this.recipient, getAll: true }).forEach(currency => { - if (currency.type === "item") { - if (!currency.item) return - items.push(new this.ItemClass(this, currency.item, true, !!currency?.secondary)); - } else { - attributes.push(new this.AttributeClass(this, currency, true, !!currency?.secondary)); - } - }); - - this.allItems.set(items); - this.attributes.set(attributes); - - this.subscribeTo(this.allItems, () => { - this.refreshItems(); - }); - this.subscribeTo(this.attributes, () => { - this.refreshItems(); - }); - - const filterDebounce = foundry.utils.debounce(() => { - this.refreshItems(); - }, this.searchDelay); - this.subscribeTo(this.search, (val) => { - filterDebounce() - }); - - } - - updateSource(newSource) { - this.uuid = Utilities.getUuid(newSource); - this.actor = Utilities.getActor(newSource); - this.document.set(this.actor); - __STORES__.set(this.uuid, this); - this.unsubscribe(); - this.setupStores(); - this.setupSubscriptions(); - } - - updateRecipient(newRecipient) { - this.recipient = newRecipient; - this.recipientDocument.set(this.recipient); - this.unsubscribe(); - this.setupStores(); - this.setupSubscriptions(); - } - - visibleItemFilterFunction(entry, actorIsMerchant, pileData, recipientPileData) { - const itemFlagData = entry.itemFlagData ? get(entry.itemFlagData) : {}; - return !entry.isCurrency - && (this.actor.isOwner || !actorIsMerchant || !itemFlagData?.hidden); - } - - itemSortFunction(a, b, inverse) { - return (b.item.name > a.item.name ? -1 : 1) * (inverse ? -1 : 1); - } - - refreshItems() { - const allItems = get(this.allItems); - const pileData = get(this.pileData); - const recipientPileData = this.recipient ? PileUtilities.getActorFlagData(this.recipient) : {} - const actorIsMerchant = PileUtilities.isItemPileMerchant(this.actor, pileData); - - const visibleItems = allItems.filter(entry => this.visibleItemFilterFunction(entry, actorIsMerchant, pileData, recipientPileData)); - const itemCurrencies = allItems.filter(entry => entry.isCurrency && !entry.isSecondaryCurrency); - const secondaryItemCurrencies = allItems.filter(entry => entry.isSecondaryCurrency); - - this.visibleItems.set(visibleItems); - - const items = visibleItems.filter(entry => !get(entry.filtered)); - - this.numItems.set(items.filter(entry => get(entry.quantity) > 0).length); - this.items.set(items.sort((a, b) => this.itemSortFunction(a, b))); - - const currencies = get(this.attributes).filter(entry => !entry.isSecondaryCurrency).concat(itemCurrencies); - const secondaryCurrencies = get(this.attributes).filter(entry => entry.isSecondaryCurrency).concat(secondaryItemCurrencies); - - this.numCurrencies.set(currencies.concat(secondaryCurrencies).filter(entry => get(entry.quantity) > 0).length); - - this.currencies.set(currencies.concat(secondaryCurrencies).filter(entry => !get(entry.filtered))); - this.allCurrencies.set(currencies.concat(secondaryCurrencies)); - - this.itemCategories.set(Object.values(visibleItems.reduce((acc, item) => { - const category = get(item.category); - if (!acc[category.type]) { - acc[category.type] = { ...category }; - } - return acc; - }, {})).sort((a, b) => a.label < b.label ? -1 : 1)); - - const itemsPerCategory = items - .reduce((acc, item) => { - const category = get(item.category); - if (!acc[category.type]) { - acc[category.type] = { - service: category.service, - type: category.type, - label: category.label, - items: [] - }; - } - acc[category.type].items.push(item); - return acc; - }, {}) - - Object.values(itemsPerCategory).forEach(category => category.items.sort((a, b) => { - return a.item.name < b.item.name ? -1 : 1; - })); - - this.itemsPerCategory.set(itemsPerCategory); - - this.categories.set(Object.values(itemsPerCategory).map(category => { - return { - service: category.service, - label: category.label, - type: category.type - } - }).sort((a, b) => a.label < b.label ? -1 : 1)); - } - - createItem(item) { - if (PileUtilities.isItemInvalid(this.actor, item)) return; - const items = get(this.allItems); - const deletedItems = items - .filter(item => item.id === null) - .map(item => ({ - pileItem: item, - ...item.similarities - })); - const previouslyDeletedItem = Utilities.findSimilarItem(deletedItems, item); - if (previouslyDeletedItem) { - previouslyDeletedItem.pileItem.setup(item); - } else { - items.push(new this.ItemClass(this, item)); - } - this.allItems.set(items); - } - - deleteItem(item) { - if (PileUtilities.isItemInvalid(this.actor, item)) return; - const items = get(this.allItems); - const pileItem = items.find(pileItem => pileItem.id === item.id); - if (!pileItem) return; - if (get(this.editQuantities) || !InterfaceTracker.isOpened(this.application.id)) { - items.splice(items.indexOf(pileItem), 1); - this.allItems.set(items); - } else { - pileItem.id = null; - pileItem.quantity.set(0); - pileItem.quantityLeft.set(0); - } - pileItem.unsubscribe(); - } - - hasSimilarItem(item) { - const items = get(this.allItems).map(item => item.item); - return !!Utilities.findSimilarItem(items, item, get(this.pileData)); - } - - delete() { - this.deleted.set(true); - } - - async update() { - - const itemsToUpdate = []; - const itemsToDelete = []; - const attributesToUpdate = {}; - - const items = get(this.allItems).filter(item => item.id); - for (let item of items) { - const itemQuantity = get(item.quantity); - if (itemQuantity === 0) { - itemsToDelete.push(item.id); - } else { - if (PileUtilities.canItemStack(item.item, this.actor)) { - itemsToUpdate.push(Utilities.setItemQuantity({ _id: item.id }, itemQuantity)); - } - } - } - - const attributes = get(this.attributes); - for (let attribute of attributes) { - attributesToUpdate[attribute.path] = get(attribute.quantity); - } - - const pileSharingData = SharingUtilities.getItemPileSharingData(this.actor); - - await this.actor.update(attributesToUpdate); - if (pileSharingData?.currencies) { - pileSharingData.currencies = pileSharingData.currencies.map(currency => { - if (attributesToUpdate[currency.path] !== undefined) { - currency.actors = currency.actors.map(actor => { - actor.quantity = Math.max(0, Math.min(actor.quantity, attributesToUpdate[currency.path])); - return actor; - }) - } - return currency; - }) - } - - await this.actor.updateEmbeddedDocuments("Item", itemsToUpdate); - await this.actor.deleteEmbeddedDocuments("Item", itemsToDelete); - if (pileSharingData?.items) { - pileSharingData.items = pileSharingData.items.map(item => { - const sharingItem = itemsToUpdate.find(item => item._id === item.id); - if (sharingItem) { - item.actors = item.actors.map(actor => { - actor.quantity = Math.max(0, Math.min(actor.quantity, sharingItem.quantity)); - return actor; - }) - } - return item; - }) - } - - await SharingUtilities.updateItemPileSharingData(this.actor, pileSharingData); - - this.refreshItems(); - - Helpers.custom_notify(game.i18n.localize("ITEM-PILES.Notifications.UpdateItemPileSuccess")); - - } - - async depositCurrency() { - const result = await DropCurrencyDialog.show(this.recipient, this.actor, { localization: "DepositCurrencies" }); - return this._addCurrency(result, this.recipient, this.actor); - } - - async withdrawCurrency() { - const result = await DropCurrencyDialog.show(this.actor, this.recipient, { localization: "WithdrawCurrencies" }); - return this._addCurrency(result, this.actor, this.recipient); - } - - async addCurrency(recipient = false) { - const source = recipient || this.actor; - const target = recipient ? this.actor : false; - const result = await DropCurrencyDialog.show(source, target, { - localization: !target ? "EditCurrencies" : false, - unlimitedCurrencies: !target && game.user.isGM, - existingCurrencies: PileUtilities.getActorCurrencies(source, { combine: true }), - getUpdates: !target - }); - return this._addCurrency(result, source, target); - } - - async _addCurrency(currencies, source, target = false) { - - if (!currencies) return; - - if (!target) { - - if (!game.user.isGM) return; - - if (!foundry.utils.isEmpty(currencies.attributes)) { - await game.itempiles.API.setAttributes(source, currencies.attributes, { interactionId: this.interactionId }) - } - if (currencies.items.length) { - const itemsToAdd = currencies.items.filter(currency => currency.quantity > 0); - const itemsToRemove = currencies.items.filter(currency => currency.quantity < 0); - await game.itempiles.API.addItems(source, itemsToAdd, { interactionId: this.interactionId }) - await game.itempiles.API.removeItems(source, itemsToRemove, { interactionId: this.interactionId }) - } - - } else { - - if (!foundry.utils.isEmpty(currencies.attributes)) { - await game.itempiles.API.transferAttributes(source, target, currencies.attributes, { interactionId: this.interactionId }) - } - if (currencies.items.length) { - await game.itempiles.API.transferItems(source, target, currencies.items, { interactionId: this.interactionId }) - } - } - - } - - takeAll() { - game.itempiles.API.transferEverything( - this.actor, - this.recipient, - { interactionId: this.interactionId } - ); - } - - splitAll() { - return game.itempiles.API.splitItemPileContents(this.actor, { instigator: this.recipient }); - } - - closeContainer() { - if (!InterfaceTracker.isOpened(this.application.id)) { - return game.itempiles.API.closeItemPile(this.actor, this.recipient); - } - } - - subscribeTo(target, callback) { - this.subscriptions.push(target.subscribe(callback)); - } - - unsubscribe() { - this.subscriptions.forEach(unsubscribe => unsubscribe()); - this.subscriptions = []; - } - - onDestroy() { - this.unsubscribe(); - __STORES__.delete(this.uuid); - } + this.search.set(""); + this.editQuantities.set(!this.recipient); + + this.allItems.set([]); + this.attributes.set([]); + + this.items.set([]); + this.visibleItems.set([]); + + this.pileCurrencies.set(PileUtilities.getActorCurrencies(this.actor, { getAll: true })); + this.recipientCurrencies.set(this.recipient ? PileUtilities.getActorCurrencies(this.recipient, { getAll: true }) : []); + + this.currencies.set([]); + this.allCurrencies.set([]); + + this.itemsPerCategory.set({}); + this.categories.set([]); + this.itemCategories.set([]); + + this.numItems.set(0); + this.numCurrencies.set(0); + + this.name.set(""); + this.img.set(""); + + } + + getActorImage() { + return this.actor.img; + } + + setupSubscriptions() { + + this.subscribeTo(this.document, () => { + const { data } = this.document.updateOptions; + if (hasProperty(data, CONSTANTS.FLAGS.SHARING)) { + this.shareData.set(SharingUtilities.getItemPileSharingData(this.actor)); + this.refreshItems(); + } + if (hasProperty(data, CONSTANTS.FLAGS.PILE)) { + this.pileData.set(PileUtilities.getActorFlagData(this.actor)); + this.pileCurrencies.set(PileUtilities.getActorCurrencies(this.actor, { getAll: true })); + this.refreshItems(); + } + this.name.set(this.actor.name); + this.img.set(this.getActorImage()); + }); + + if (this.recipientDocument) { + this.subscribeTo(this.recipientDocument, () => { + const { data } = this.document.updateOptions; + if (hasProperty(data, CONSTANTS.FLAGS.SHARING)) { + this.recipientShareData.set(SharingUtilities.getItemPileSharingData(this.recipient)); + this.refreshItems(); + } + if (hasProperty(data, CONSTANTS.FLAGS.PILE)) { + this.recipientPileData.set(PileUtilities.getActorFlagData(this.recipient)); + this.recipientCurrencies.set(PileUtilities.getActorCurrencies(this.recipient, { getAll: true })); + this.refreshItems(); + } + }); + } + + const items = []; + const attributes = []; + + const pileData = PileUtilities.isValidItemPile(this.actor) || !this.recipient ? get(this.pileData) : get(this.recipientPileData); + + PileUtilities.getActorItems(this.actor, { itemFilters: pileData.overrideItemFilters }).map(item => { + items.push(new this.ItemClass(this, item)); + }); + + PileUtilities.getActorCurrencies(this.actor, { forActor: this.recipient, getAll: true }).forEach(currency => { + if (currency.type === "item") { + if (!currency.item) return + items.push(new this.ItemClass(this, currency.item, true, !!currency?.secondary)); + } else { + attributes.push(new this.AttributeClass(this, currency, true, !!currency?.secondary)); + } + }); + + this.allItems.set(items); + this.attributes.set(attributes); + + this.subscribeTo(this.allItems, () => { + this.refreshItems(); + }); + this.subscribeTo(this.attributes, () => { + this.refreshItems(); + }); + + const filterDebounce = foundry.utils.debounce(() => { + this.refreshItems(); + }, this.searchDelay); + this.subscribeTo(this.search, (val) => { + filterDebounce() + }); + + } + + updateSource(newSource) { + this.uuid = Utilities.getUuid(newSource); + this.actor = Utilities.getActor(newSource); + this.document.set(this.actor); + __STORES__.set(this.uuid, this); + this.unsubscribe(); + this.setupStores(); + this.setupSubscriptions(); + } + + updateRecipient(newRecipient) { + this.recipient = newRecipient; + this.recipientDocument.set(this.recipient); + this.unsubscribe(); + this.setupStores(); + this.setupSubscriptions(); + } + + visibleItemFilterFunction(entry, actorIsMerchant, pileData, recipientPileData) { + const itemFlagData = entry.itemFlagData ? get(entry.itemFlagData) : {}; + return !entry.isCurrency + && (this.actor.isOwner || !actorIsMerchant || !itemFlagData?.hidden); + } + + itemSortFunction(a, b, inverse) { + return (b.item.name > a.item.name ? -1 : 1) * (inverse ? -1 : 1); + } + + refreshItems() { + const allItems = get(this.allItems); + const pileData = get(this.pileData); + const recipientPileData = this.recipient ? PileUtilities.getActorFlagData(this.recipient) : {} + const actorIsMerchant = PileUtilities.isItemPileMerchant(this.actor, pileData); + + const visibleItems = allItems.filter(entry => this.visibleItemFilterFunction(entry, actorIsMerchant, pileData, recipientPileData)); + const itemCurrencies = allItems.filter(entry => entry.isCurrency && !entry.isSecondaryCurrency); + const secondaryItemCurrencies = allItems.filter(entry => entry.isSecondaryCurrency); + + this.visibleItems.set(visibleItems); + + const items = visibleItems.filter(entry => !get(entry.filtered)); + + this.numItems.set(items.filter(entry => get(entry.quantity) > 0).length); + this.items.set(items.sort((a, b) => this.itemSortFunction(a, b))); + + const currencies = get(this.attributes).filter(entry => !entry.isSecondaryCurrency).concat(itemCurrencies); + const secondaryCurrencies = get(this.attributes).filter(entry => entry.isSecondaryCurrency).concat(secondaryItemCurrencies); + + this.numCurrencies.set(currencies.concat(secondaryCurrencies).filter(entry => get(entry.quantity) > 0).length); + + this.currencies.set(currencies.concat(secondaryCurrencies).filter(entry => !get(entry.filtered))); + this.allCurrencies.set(currencies.concat(secondaryCurrencies)); + + this.itemCategories.set(Object.values(visibleItems.reduce((acc, item) => { + const category = get(item.category); + if (!acc[category.type]) { + acc[category.type] = { ...category }; + } + return acc; + }, {})).sort((a, b) => a.label < b.label ? -1 : 1)); + + const itemsPerCategory = items + .reduce((acc, item) => { + const category = get(item.category); + if (!acc[category.type]) { + acc[category.type] = { + service: category.service, + type: category.type, + label: category.label, + items: [] + }; + } + acc[category.type].items.push(item); + return acc; + }, {}) + + Object.values(itemsPerCategory).forEach(category => category.items.sort((a, b) => { + return a.item.name < b.item.name ? -1 : 1; + })); + + this.itemsPerCategory.set(itemsPerCategory); + + this.categories.set(Object.values(itemsPerCategory).map(category => { + return { + service: category.service, + label: category.label, + type: category.type + } + }).sort((a, b) => a.label < b.label ? -1 : 1)); + } + + createItem(item) { + if (PileUtilities.isItemInvalid(this.actor, item)) return; + const items = get(this.allItems); + const deletedItems = items + .filter(item => item.id === null) + .map(item => ({ + pileItem: item, + ...item.similarities + })); + const previouslyDeletedItem = Utilities.findSimilarItem(deletedItems, item); + if (previouslyDeletedItem) { + previouslyDeletedItem.pileItem.setup(item); + } else { + items.push(new this.ItemClass(this, item)); + } + this.allItems.set(items); + } + + deleteItem(item) { + if (PileUtilities.isItemInvalid(this.actor, item)) return; + const items = get(this.allItems); + const pileItem = items.find(pileItem => pileItem.id === item.id); + if (!pileItem) return; + if (get(this.editQuantities) || !InterfaceTracker.isOpened(this.application.id)) { + items.splice(items.indexOf(pileItem), 1); + this.allItems.set(items); + } else { + pileItem.id = null; + pileItem.quantity.set(0); + pileItem.quantityLeft.set(0); + } + pileItem.unsubscribe(); + } + + hasSimilarItem(item) { + const items = get(this.allItems).map(item => item.item); + return !!Utilities.findSimilarItem(items, item, get(this.pileData)); + } + + delete() { + this.deleted.set(true); + } + + async update() { + + const itemsToUpdate = []; + const itemsToDelete = []; + const attributesToUpdate = {}; + + const items = get(this.allItems).filter(item => item.id); + for (let item of items) { + const itemQuantity = get(item.quantity); + if (itemQuantity === 0) { + itemsToDelete.push(item.id); + } else { + if (PileUtilities.canItemStack(item.item, this.actor)) { + itemsToUpdate.push(Utilities.setItemQuantity({ _id: item.id }, itemQuantity)); + } + } + } + + const attributes = get(this.attributes); + for (let attribute of attributes) { + attributesToUpdate[attribute.path] = get(attribute.quantity); + } + + const pileSharingData = SharingUtilities.getItemPileSharingData(this.actor); + + await this.actor.update(attributesToUpdate); + if (pileSharingData?.currencies) { + pileSharingData.currencies = pileSharingData.currencies.map(currency => { + if (attributesToUpdate[currency.path] !== undefined) { + currency.actors = currency.actors.map(actor => { + actor.quantity = Math.max(0, Math.min(actor.quantity, attributesToUpdate[currency.path])); + return actor; + }) + } + return currency; + }) + } + + await this.actor.updateEmbeddedDocuments("Item", itemsToUpdate); + await this.actor.deleteEmbeddedDocuments("Item", itemsToDelete); + if (pileSharingData?.items) { + pileSharingData.items = pileSharingData.items.map(item => { + const sharingItem = itemsToUpdate.find(item => item._id === item.id); + if (sharingItem) { + item.actors = item.actors.map(actor => { + actor.quantity = Math.max(0, Math.min(actor.quantity, sharingItem.quantity)); + return actor; + }) + } + return item; + }) + } + + await SharingUtilities.updateItemPileSharingData(this.actor, pileSharingData); + + this.refreshItems(); + + Helpers.custom_notify(game.i18n.localize("ITEM-PILES.Notifications.UpdateItemPileSuccess")); + + } + + async depositCurrency() { + const result = await DropCurrencyDialog.show(this.recipient, this.actor, { localization: "DepositCurrencies" }); + return this._addCurrency(result, this.recipient, this.actor); + } + + async withdrawCurrency() { + const result = await DropCurrencyDialog.show(this.actor, this.recipient, { localization: "WithdrawCurrencies" }); + return this._addCurrency(result, this.actor, this.recipient); + } + + async addCurrency(recipient = false) { + const source = recipient || this.actor; + const target = recipient ? this.actor : false; + const result = await DropCurrencyDialog.show(source, target, { + localization: !target ? "EditCurrencies" : false, + unlimitedCurrencies: !target && game.user.isGM, + existingCurrencies: PileUtilities.getActorCurrencies(source, { combine: true }), + getUpdates: !target + }); + return this._addCurrency(result, source, target); + } + + async _addCurrency(currencies, source, target = false) { + + if (!currencies) return; + + if (!target) { + + if (!game.user.isGM) return; + + if (!foundry.utils.isEmpty(currencies.attributes)) { + await game.itempiles.API.setAttributes(source, currencies.attributes, { interactionId: this.interactionId }) + } + if (currencies.items.length) { + const itemsToAdd = currencies.items.filter(currency => currency.quantity > 0); + const itemsToRemove = currencies.items.filter(currency => currency.quantity < 0); + await game.itempiles.API.addItems(source, itemsToAdd, { interactionId: this.interactionId }) + await game.itempiles.API.removeItems(source, itemsToRemove, { interactionId: this.interactionId }) + } + + } else { + + if (!foundry.utils.isEmpty(currencies.attributes)) { + await game.itempiles.API.transferAttributes(source, target, currencies.attributes, { interactionId: this.interactionId }) + } + if (currencies.items.length) { + await game.itempiles.API.transferItems(source, target, currencies.items, { interactionId: this.interactionId }) + } + } + + } + + takeAll() { + game.itempiles.API.transferEverything( + this.actor, + this.recipient, + { interactionId: this.interactionId } + ); + } + + splitAll() { + return game.itempiles.API.splitItemPileContents(this.actor, { instigator: this.recipient }); + } + + closeContainer() { + if (!InterfaceTracker.isOpened(this.application.id)) { + return game.itempiles.API.closeItemPile(this.actor, this.recipient); + } + } + + subscribeTo(target, callback) { + this.subscriptions.push(target.subscribe(callback)); + } + + unsubscribe() { + this.subscriptions.forEach(unsubscribe => unsubscribe()); + this.subscriptions = []; + } + + onDestroy() { + this.unsubscribe(); + __STORES__.delete(this.uuid); + } } diff --git a/src/stores/merchant-store.js b/src/stores/merchant-store.js index 79aafd18..1391da8e 100644 --- a/src/stores/merchant-store.js +++ b/src/stores/merchant-store.js @@ -16,573 +16,554 @@ import EntryButtons from "../applications/merchant-app/components/EntryButtons.s export default class MerchantStore extends ItemPileStore { - constructor(...args) { - super(...args); - this.services = writable({}); - this.editPrices = writable(false); - this.typeFilter = writable("all"); - this.sortType = writable(0); - this.priceModifiersPerType = writable({}); - this.priceModifiersForActor = writable({}); - this.priceSelector = writable(""); - this.closed = writable(false); - this.itemColumns = writable([]); - this.sortTypes = writable([]); - this.inverseSort = writable(false); - this.isMerchant = false; - this.log = writable([]); - this.visibleLogItems = writable(20); - this.logSearch = writable(""); - } - - get ItemClass() { - return PileMerchantItem; - } - - setupStores() { - super.setupStores(); - this.services.set({}); - this.editPrices.set(false); - this.typeFilter.set("all"); - this.sortType.set(0); - this.priceModifiersPerType.set({}); - this.priceModifiersForActor.set({}); - this.priceSelector.set(""); - this.closed.set(false); - this.itemColumns.set([]); - this.sortTypes.set([]); - this.inverseSort.set(false); - this.isMerchant = false; - this.log.set([]); - this.visibleLogItems.set(20); - this.logSearch.set(""); - } - - getActorImage() { - const pileData = get(this.pileData); - return pileData?.merchantImage || this.actor.img; - } - - setupSubscriptions() { - - let setup = false; - super.setupSubscriptions(); - this.subscribeTo(this.document, () => { - if (!setup) return; - this.processLogEntries(); - }); - this.subscribeTo(this.pileData, (pileData) => { - this.isMerchant = PileUtilities.isItemPileMerchant(this.actor, pileData); - this.setupColumns(pileData); - if (!setup) return; - this.updatePriceModifiers(); - this.updateOpenCloseStatus(); - }); - if (this.recipientDocument) { - this.subscribeTo(this.recipientPileData, (pileData) => { - if (PileUtilities.isItemPileMerchant(this.recipient, pileData)) { - this.setupColumns(pileData); - } - if (!setup) return; - this.updatePriceModifiers(); - }); - this.subscribeTo(this.recipientDocument, () => { - if (!setup) return; - this.refreshItemPrices(); - }) - } - this.subscribeTo(this.typeFilter, (val) => { - if (!setup) return; - this.refreshItems() - }); - this.subscribeTo(this.sortType, (val) => { - if (!setup) return; - this.refreshItems(); - }) - this.subscribeTo(this.inverseSort, (val) => { - if (!setup) return; - this.refreshItems(); - }) - this.subscribeTo(this.logSearch, () => { - if (!setup) return; - this.filterLogEntries(); - }) - setup = true; - this.updatePriceModifiers(); - this.updateOpenCloseStatus(); - this.refreshItems(); - this.processLogEntries(); - } - - setupColumns(pileData) { - - const customColumns = foundry.utils.deepClone(pileData.merchantColumns ?? []) - .filter(column => { - return this.isMerchant ? (column?.buying ?? true) : (column?.selling ?? true); - }) - .map(column => ({ - label: localize(column.label), - component: CustomColumn, - data: column, - sortMethod: (a, b, inverse) => { - const path = column.path; - const AProp = getProperty(b.item, path); - const BProp = getProperty(a.item, path); - if (!column?.mapping?.[AProp] || !column?.mapping?.[BProp]) { - return (AProp > BProp ? 1 : -1) * (inverse ? -1 : 1); - } - const keys = Object.keys(column.mapping); - return (keys.indexOf(AProp) - keys.indexOf(BProp)) * (inverse ? -1 : 1); - } - })); - - const columns = []; - - columns.push({ - label: "Type", - component: ItemEntry - }); - - if (pileData.displayQuantity !== "alwaysno") { - columns.push({ - label: "Quantity", - component: QuantityColumn, - sortMethod: (a, b, inverse) => { - return (get(b.quantity) - get(a.quantity)) * (inverse ? -1 : 1); - } - }) - } - - columns.push(...customColumns) - columns.push({ - label: "Price", - component: PriceSelector, - sortMethod: (a, b, inverse) => { - const APrice = get(a.prices).find(price => price.primary); - const BPrice = get(b.prices).find(price => price.primary); - if (!APrice) return 1; - if (!BPrice) return -1; - return (BPrice.totalCost - APrice.totalCost) * (inverse ? -1 : 1); - } - }) - columns.push({ - label: false, - component: EntryButtons - }); - - this.itemColumns.set(columns); - - const sortTypes = columns.filter(col => col.label); - - sortTypes.splice(1, 0, { label: "Name" }); - - this.sortTypes.set(sortTypes) - - } - - refreshItemPrices() { - const pileData = get(this.pileData); - const recipientPileData = get(this.recipientPileData); - get(this.allItems).forEach(item => { - item.refreshPriceData(pileData, recipientPileData); - }); - } - - visibleItemFilterFunction(entry, actorIsMerchant, pileData, recipientPileData) { - const itemIsFree = !!get(entry.prices).find(price => price.free); - return super.visibleItemFilterFunction(entry, actorIsMerchant, pileData, recipientPileData) - && ( - actorIsMerchant - ? !(pileData?.hideItemsWithZeroCost && itemIsFree) - : !(recipientPileData?.hideItemsWithZeroCost && itemIsFree) - ); - } - - itemSortFunction(a, b) { - const sortType = get(this.sortType); - const inverse = get(this.inverseSort); - if (sortType <= 1) { - return super.itemSortFunction(a, b, inverse); - } - const selectedSortType = get(this.sortTypes)[sortType]; - return selectedSortType?.sortMethod(a, b, inverse, selectedSortType); - } - - createItem(item) { - if (PileUtilities.isItemInvalid(this.actor, item)) return; - const items = get(this.allItems); - const itemClass = new this.ItemClass(this, item); - itemClass.refreshPriceData(); - items.push(itemClass); - this.allItems.set(items); - this.refreshItems(); - } - - deleteItem(item) { - if (PileUtilities.isItemInvalid(this.actor, item)) return; - const items = get(this.allItems); - const pileItem = items.find(pileItem => pileItem.id === item.id); - if (!pileItem) return; - pileItem.unsubscribe(); - items.splice(items.indexOf(pileItem), 1); - this.allItems.set(items); - this.refreshItems(); - } - - updatePriceModifiers() { - let pileData = get(this.pileData); - let change = false; - if (pileData.itemTypePriceModifiers && typeof pileData.itemTypePriceModifiers === "object") { - change = true; - this.priceModifiersPerType.set((pileData.itemTypePriceModifiers ?? {}).reduce((acc, priceData) => { - acc[priceData.category.toLowerCase() || priceData.type] = priceData; - return acc; - }, {})); - } - if (this.recipient && pileData.actorPriceModifiers && Array.isArray(pileData.actorPriceModifiers)) { - change = true; - const recipientUuid = Utilities.getUuid(this.recipient); - const actorSpecificModifiers = pileData.actorPriceModifiers?.find(data => data.actorUuid === recipientUuid); - if (actorSpecificModifiers) { - this.priceModifiersForActor.set(actorSpecificModifiers); - } - } - if (change) { - this.refreshItemPrices(); - } - } - - addOverrideTypePrice(type) { - const pileData = get(this.pileData); - const custom = Object.keys(CONFIG.Item.typeLabels).indexOf(type) === -1; - pileData.itemTypePriceModifiers.push({ - category: custom ? type : "", - type: custom ? "custom" : type, - override: false, - buyPriceModifier: 1, - sellPriceModifier: 1 - }) - this.pileData.set(pileData); - } - - removeOverrideTypePrice(type) { - const pileData = get(this.pileData); - const priceMods = pileData.itemTypePriceModifiers; - const typeEntry = priceMods.find(entry => entry.type === type); - priceMods.splice(priceMods.indexOf(typeEntry), 1); - this.pileData.set(pileData); - } - - async update() { - const pileData = get(this.pileData); - const priceModPerType = get(this.priceModifiersPerType); - pileData.itemTypePriceModifiers = Object.values(priceModPerType); - await PileUtilities.updateItemPileData(this.actor, pileData); - Helpers.custom_notify(localize("ITEM-PILES.Notifications.UpdateMerchantSuccess")); - } - - tradeItem(pileItem, selling) { - if (get(pileItem.itemFlagData).notForSale && !game.user.isGM) return; - TradeMerchantItemDialog.show( - pileItem, - this.actor, - this.recipient, - { selling } - ); - } - - async updateOpenCloseStatus() { - const pileData = get(this.pileData); - if (pileData.openTimes.status === "auto") { - if (game.modules.get('foundryvtt-simple-calendar')?.active && pileData.openTimes.enabled) { - const openTimes = pileData.openTimes.open; - const closeTimes = pileData.openTimes.close; - const timestamp = window.SimpleCalendar.api.timestampToDate(window.SimpleCalendar.api.timestamp()); - - const openingTime = Number(openTimes.hour.toString() + "." + openTimes.minute.toString()); - const closingTime = Number(closeTimes.hour.toString() + "." + closeTimes.minute.toString()); - const currentTime = Number(timestamp.hour.toString() + "." + timestamp.minute.toString()); - - let isClosed = openingTime > closingTime - ? !(currentTime >= openingTime || currentTime <= closingTime) // Is the store open over midnight? - : !(currentTime >= openingTime && currentTime <= closingTime); // or is the store open during normal daylight hours? - - const currentWeekday = window.SimpleCalendar.api.getCurrentWeekday(); - - isClosed = isClosed || (pileData.closedDays ?? []).includes(currentWeekday.name); - - const currentDate = window.SimpleCalendar.api.currentDateTime(); - const notes = window.SimpleCalendar.api.getNotesForDay(currentDate.year, currentDate.month, currentDate.day); - const categories = new Set(notes.map(note => getProperty(note, "flags.foundryvtt-simple-calendar.noteData.categories") ?? []).deepFlatten()); - - isClosed = isClosed || categories.intersection(new Set(pileData.closedHolidays ?? [])).size > 0; - - this.closed.set(isClosed); - - } else if (isResponsibleGM()) { - pileData.openTimes.status = "open"; - await PileUtilities.updateItemPileData(this.actor, pileData); - } - - } else if (!pileData.openTimes.status.startsWith("auto")) { - - this.closed.set(pileData.openTimes.status === "closed"); - - } - } - - async setOpenStatus(status) { - const pileData = get(this.pileData); - pileData.openTimes.status = status; - await PileUtilities.updateItemPileData(this.actor, pileData); - } - - processLogEntries() { - - //const pileData = get(this.pileData); - const logEntries = PileUtilities.getActorLog(this.actor); + constructor(...args) { + super(...args); + this.services = writable({}); + this.editPrices = writable(false); + this.typeFilter = writable("all"); + this.sortType = writable(0); + this.priceModifiersPerType = writable({}); + this.priceModifiersForActor = writable({}); + this.priceSelector = writable(""); + this.closed = writable(false); + this.itemColumns = writable([]); + this.sortTypes = writable([]); + this.inverseSort = writable(false); + this.isMerchant = false; + this.log = writable([]); + this.visibleLogItems = writable(20); + this.logSearch = writable(""); + } + + get ItemClass() { + return PileMerchantItem; + } + + setupStores() { + super.setupStores(); + this.services.set({}); + this.editPrices.set(false); + this.typeFilter.set("all"); + this.sortType.set(0); + this.priceModifiersPerType.set({}); + this.priceModifiersForActor.set({}); + this.priceSelector.set(""); + this.closed.set(false); + this.itemColumns.set([]); + this.sortTypes.set([]); + this.inverseSort.set(false); + this.isMerchant = false; + this.log.set([]); + this.visibleLogItems.set(20); + this.logSearch.set(""); + } + + getActorImage() { + const pileData = get(this.pileData); + return pileData?.merchantImage || this.actor.img; + } + + setupSubscriptions() { + + let setup = false; + super.setupSubscriptions(); + this.subscribeTo(this.document, () => { + if (!setup) return; + this.processLogEntries(); + }); + this.subscribeTo(this.pileData, (pileData) => { + this.isMerchant = PileUtilities.isItemPileMerchant(this.actor, pileData); + this.setupColumns(pileData); + if (!setup) return; + this.updatePriceModifiers(); + this.updateOpenCloseStatus(); + }); + if (this.recipientDocument) { + this.subscribeTo(this.recipientPileData, (pileData) => { + if (PileUtilities.isItemPileMerchant(this.recipient, pileData)) { + this.setupColumns(pileData); + } + if (!setup) return; + this.updatePriceModifiers(); + }); + this.subscribeTo(this.recipientDocument, () => { + if (!setup) return; + this.refreshItemPrices(); + }) + } + this.subscribeTo(this.typeFilter, (val) => { + if (!setup) return; + this.refreshItems() + }); + this.subscribeTo(this.sortType, (val) => { + if (!setup) return; + this.refreshItems(); + }) + this.subscribeTo(this.inverseSort, (val) => { + if (!setup) return; + this.refreshItems(); + }) + this.subscribeTo(this.logSearch, () => { + if (!setup) return; + this.filterLogEntries(); + }) + setup = true; + this.updatePriceModifiers(); + this.updateOpenCloseStatus(); + this.refreshItems(); + this.processLogEntries(); + } + + setupColumns(pileData) { + + const customColumns = foundry.utils.deepClone(pileData.merchantColumns ?? []) + .filter(column => { + return this.isMerchant ? (column?.buying ?? true) : (column?.selling ?? true); + }) + .map(column => ({ + label: localize(column.label), + component: CustomColumn, + data: column, + sortMethod: (a, b, inverse) => { + const path = column.path; + const AProp = getProperty(b.item, path); + const BProp = getProperty(a.item, path); + if (!column?.mapping?.[AProp] || !column?.mapping?.[BProp]) { + return (AProp > BProp ? 1 : -1) * (inverse ? -1 : 1); + } + const keys = Object.keys(column.mapping); + return (keys.indexOf(AProp) - keys.indexOf(BProp)) * (inverse ? -1 : 1); + } + })); + + const columns = []; + + columns.push({ + label: "Type", + component: ItemEntry + }); + + if (pileData.displayQuantity !== "alwaysno") { + columns.push({ + label: "Quantity", + component: QuantityColumn, + sortMethod: (a, b, inverse) => { + return (get(b.quantity) - get(a.quantity)) * (inverse ? -1 : 1); + } + }) + } + + columns.push(...customColumns) + columns.push({ + label: "Price", + component: PriceSelector, + sortMethod: (a, b, inverse) => { + const APrice = get(a.prices).find(price => price.primary); + const BPrice = get(b.prices).find(price => price.primary); + if (!APrice) return 1; + if (!BPrice) return -1; + return (BPrice.totalCost - APrice.totalCost) * (inverse ? -1 : 1); + } + }) + columns.push({ + label: false, + component: EntryButtons + }); + + this.itemColumns.set(columns); + + const sortTypes = columns.filter(col => col.label); + + sortTypes.splice(1, 0, { label: "Name" }); + + this.sortTypes.set(sortTypes) + + } + + refreshItemPrices() { + const pileData = get(this.pileData); + const recipientPileData = get(this.recipientPileData); + get(this.allItems).forEach(item => { + item.refreshPriceData(pileData, recipientPileData); + }); + } + + visibleItemFilterFunction(entry, actorIsMerchant, pileData, recipientPileData) { + const itemIsFree = !!get(entry.prices).find(price => price.free); + return super.visibleItemFilterFunction(entry, actorIsMerchant, pileData, recipientPileData) + && ( + actorIsMerchant + ? !(pileData?.hideItemsWithZeroCost && itemIsFree) + : !(recipientPileData?.hideItemsWithZeroCost && itemIsFree) + ); + } + + itemSortFunction(a, b) { + const sortType = get(this.sortType); + const inverse = get(this.inverseSort); + if (sortType <= 1) { + return super.itemSortFunction(a, b, inverse); + } + const selectedSortType = get(this.sortTypes)[sortType]; + return selectedSortType?.sortMethod(a, b, inverse, selectedSortType); + } + + createItem(item) { + if (PileUtilities.isItemInvalid(this.actor, item)) return; + const items = get(this.allItems); + const itemClass = new this.ItemClass(this, item); + itemClass.refreshPriceData(); + items.push(itemClass); + this.allItems.set(items); + this.refreshItems(); + } + + deleteItem(item) { + if (PileUtilities.isItemInvalid(this.actor, item)) return; + const items = get(this.allItems); + const pileItem = items.find(pileItem => pileItem.id === item.id); + if (!pileItem) return; + pileItem.unsubscribe(); + items.splice(items.indexOf(pileItem), 1); + this.allItems.set(items); + this.refreshItems(); + } + + updatePriceModifiers() { + let pileData = get(this.pileData); + let change = false; + if (pileData.itemTypePriceModifiers && typeof pileData.itemTypePriceModifiers === "object") { + change = true; + this.priceModifiersPerType.set((pileData.itemTypePriceModifiers ?? {}).reduce((acc, priceData) => { + acc[priceData.category.toLowerCase() || priceData.type] = priceData; + return acc; + }, {})); + } + if (this.recipient && pileData.actorPriceModifiers && Array.isArray(pileData.actorPriceModifiers)) { + change = true; + const recipientUuid = Utilities.getUuid(this.recipient); + const actorSpecificModifiers = pileData.actorPriceModifiers?.find(data => data.actorUuid === recipientUuid); + if (actorSpecificModifiers) { + this.priceModifiersForActor.set(actorSpecificModifiers); + } + } + if (change) { + this.refreshItemPrices(); + } + } + + addOverrideTypePrice(type) { + const pileData = get(this.pileData); + const custom = Object.keys(CONFIG.Item.typeLabels).indexOf(type) === -1; + pileData.itemTypePriceModifiers.push({ + category: custom ? type : "", + type: custom ? "custom" : type, + override: false, + buyPriceModifier: 1, + sellPriceModifier: 1 + }) + this.pileData.set(pileData); + } + + removeOverrideTypePrice(type) { + const pileData = get(this.pileData); + const priceMods = pileData.itemTypePriceModifiers; + const typeEntry = priceMods.find(entry => entry.type === type); + priceMods.splice(priceMods.indexOf(typeEntry), 1); + this.pileData.set(pileData); + } + + async update() { + const pileData = get(this.pileData); + const priceModPerType = get(this.priceModifiersPerType); + pileData.itemTypePriceModifiers = Object.values(priceModPerType); + await PileUtilities.updateItemPileData(this.actor, pileData); + Helpers.custom_notify(localize("ITEM-PILES.Notifications.UpdateMerchantSuccess")); + } + + tradeItem(pileItem, selling) { + if (get(pileItem.itemFlagData).notForSale && !game.user.isGM) return; + TradeMerchantItemDialog.show( + pileItem, + this.actor, + this.recipient, + { selling } + ); + } + + async updateOpenCloseStatus() { + const pileData = get(this.pileData); + if (pileData.openTimes.status === "auto") { + if (game.modules.get('foundryvtt-simple-calendar')?.active && pileData.openTimes.enabled) { + + const isClosed = PileUtilities.isMerchantClosed(this.actor, { pileData }); + + this.closed.set(isClosed); + + } else if (isResponsibleGM()) { + pileData.openTimes.status = "open"; + await PileUtilities.updateItemPileData(this.actor, pileData); + } + + } else if (!pileData.openTimes.status.startsWith("auto")) { + + this.closed.set(pileData.openTimes.status === "closed"); + + } + } + + async setOpenStatus(status) { + const pileData = get(this.pileData); + pileData.openTimes.status = status; + await PileUtilities.updateItemPileData(this.actor, pileData); + } + + processLogEntries() { + + //const pileData = get(this.pileData); + const logEntries = PileUtilities.getActorLog(this.actor); + + logEntries.sort((a, b) => b.date - a.date); + + logEntries.forEach(log => { + + let instigator; + if (log.actor !== undefined) { + instigator = game.i18n.format("ITEM-PILES.Merchant.LogUserActor", { + actor_name: log.actor || "Unknown character", + user_name: game.users.get(log.user)?.name ?? "unknown user", + }) + } else { + instigator = game.users.get(log.user)?.name ?? "unknown user"; + } + + if (log.property) { + + const properties = { + "notForSale": ["ITEM-PILES.Merchant.LogSetForSale", "ITEM-PILES.Merchant.LogSetNotForSale"], + "hidden": ["ITEM-PILES.Merchant.LogSetVisible", "ITEM-PILES.Merchant.LogSetHidden"] + } - logEntries.sort((a, b) => b.date - a.date); + log.text = localize(properties[log.property][Number(log.value)], { instigator, item: log.item }); - logEntries.forEach(log => { + log.class = log.value ? "item-piles-log-positive" : "item-piles-log-negative"; - let instigator; - if (log.actor !== undefined) { - instigator = game.i18n.format("ITEM-PILES.Merchant.LogUserActor", { - actor_name: log.actor || "Unknown character", - user_name: game.users.get(log.user)?.name ?? "unknown user", - }) - } else { - instigator = game.users.get(log.user)?.name ?? "unknown user"; - } + } else if (log.sold !== undefined) { - if (log.property) { + const quantity = Math.abs(log.qty) > 1 + ? game.i18n.format("ITEM-PILES.Merchant.LogQuantity", { quantity: Math.abs(log.qty) }) + : ""; - const properties = { - "notForSale": ["ITEM-PILES.Merchant.LogSetForSale", "ITEM-PILES.Merchant.LogSetNotForSale"], - "hidden": ["ITEM-PILES.Merchant.LogSetVisible", "ITEM-PILES.Merchant.LogSetHidden"] - } + const action = localize("ITEM-PILES.Merchant." + (log.sold ? "LogSold" : "LogBought")); - log.text = localize(properties[log.property][Number(log.value)], { instigator, item: log.item }); + log.text = localize("ITEM-PILES.Merchant.LogTransaction", { + instigator, quantity, item: `${log.item}`, action: `${action}`, price: log.price + }); - log.class = log.value ? "item-piles-log-positive" : "item-piles-log-negative"; + log.class = log.sold ? "item-piles-log-sold" : "item-piles-log-bought"; - } else if (log.sold !== undefined) { + } else { - const quantity = Math.abs(log.qty) > 1 - ? game.i18n.format("ITEM-PILES.Merchant.LogQuantity", { quantity: Math.abs(log.qty) }) - : ""; + log.text = localize("ITEM-PILES.Merchant.LogSetQuantity", { + instigator, quantity: Math.abs(log.qty), item: `${log.item}` + }); - const action = localize("ITEM-PILES.Merchant." + (log.sold ? "LogSold" : "LogBought")); + log.class = "item-piles-log-other"; - log.text = localize("ITEM-PILES.Merchant.LogTransaction", { - instigator, quantity, item: `${log.item}`, action: `${action}`, price: log.price - }); + } - log.class = log.sold ? "item-piles-log-sold" : "item-piles-log-bought"; + log.visible = true; - } else { + }); - log.text = localize("ITEM-PILES.Merchant.LogSetQuantity", { - instigator, quantity: Math.abs(log.qty), item: `${log.item}` - }); + this.log.set(logEntries); - log.class = "item-piles-log-other"; + this.filterLogEntries() - } + } - log.visible = true; - - }); - - this.log.set(logEntries); - - this.filterLogEntries() - - } - - filterLogEntries() { - const search = get(this.logSearch).toLowerCase(); - const regex = new RegExp(search, "g"); - this.log.update((logs) => { - for (let log of logs) { - log.visible = log.text.toLowerCase().search(regex) !== -1; - } - return logs; - }) - } + filterLogEntries() { + const search = get(this.logSearch).toLowerCase(); + const regex = new RegExp(search, "g"); + this.log.update((logs) => { + for (let log of logs) { + log.visible = log.text.toLowerCase().search(regex) !== -1; + } + return logs; + }) + } } class PileMerchantItem extends PileItem { - setupStores(...args) { - super.setupStores(...args); - this.prices = writable([]); - this.displayQuantity = writable(false); - this.selectedPriceGroup = writable(-1); - this.quantityToBuy = writable(1); - this.quantityForPrice = writable(1); - this.infiniteQuantity = writable(false); - this.isService = false; - } - - setupSubscriptions() { - let setup = false; - super.setupSubscriptions(); - this.subscribeTo(this.store.pileData, () => { - if (!setup) return; - this.refreshPriceData(); - this.refreshDisplayQuantity(); - }); - if (this.store.recipient) { - this.subscribeTo(this.store.recipientPileData, () => { - if (!setup) return - this.refreshPriceData(); - this.refreshDisplayQuantity(); - }); - } - this.subscribeTo(this.quantityToBuy, () => { - if (!setup) return; - this.refreshPriceData(); - }); - this.subscribeTo(this.itemDocument, () => { - if (!setup) return; - this.refreshPriceData(); - this.store.refreshItems(); - }); - this.subscribeTo(this.store.typeFilter, () => { - if (!setup) return; - this.filter() - }); - this.subscribeTo(this.itemFlagData, (flagData) => { - this.isService = flagData.isService; - if (!setup) return; - this.refreshPriceData(); - this.refreshDisplayQuantity(); - }); - this.refreshDisplayQuantity(); - setup = true; - } - - refreshDisplayQuantity() { - - const pileData = get(this.store.pileData); - const itemFlagData = get(this.itemFlagData); - const isMerchant = PileUtilities.isItemPileMerchant(this.store.actor, pileData) - - const merchantDisplayQuantity = pileData.displayQuantity; - const itemFlagDataQuantity = itemFlagData.displayQuantity; - - const itemInfiniteQuantity = { - "default": pileData.infiniteQuantity, - "yes": true, - "no": false - }[isMerchant ? (itemFlagData.infiniteQuantity ?? "default") : "no"]; - - this.infiniteQuantity.set(itemInfiniteQuantity) - - if (itemFlagDataQuantity === "always") { - return this.displayQuantity.set(true); - } - - const itemDisplayQuantity = { - "default": merchantDisplayQuantity === "yes", - "yes": true, - "no": false - }[itemFlagDataQuantity ?? "default"]; - - if (merchantDisplayQuantity.startsWith("always")) { - return this.displayQuantity.set(merchantDisplayQuantity.endsWith("yes")); - } - - this.displayQuantity.set(itemDisplayQuantity); - - } - - refreshPriceData(sellerFlagData, buyerFlagData) { - - const quantityToBuy = get(this.quantityToBuy); - const itemFlagData = get(this.itemFlagData); - sellerFlagData = sellerFlagData ?? get(this.store.pileData); - buyerFlagData = buyerFlagData ?? get(this.store.recipientPileData); - const priceData = PileUtilities.getPriceData({ - item: this.item, - seller: this.store.actor, - buyer: this.store.recipient, - sellerFlagData, - buyerFlagData, - itemFlagData, - quantity: quantityToBuy - }); - - let selectedPriceGroup = get(this.selectedPriceGroup); - if (selectedPriceGroup === -1) { - selectedPriceGroup = Math.max(0, priceData.findIndex(price => price.maxQuantity)); - this.selectedPriceGroup.set(selectedPriceGroup) - } - - this.prices.set(priceData); - - this.quantityForPrice.set( - game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE - ? getProperty(this.item, game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE) ?? 1 - : 1 - ); - - } - - filter() { - const name = get(this.name).trim(); - const type = get(this.category).type; - const search = get(this.store.search).trim(); - const typeFilter = get(this.store.typeFilter); - const searchFiltered = !name.toLowerCase().includes(search.toLowerCase()); - const typeFiltered = typeFilter !== "all" && typeFilter.toLowerCase() !== type.toLowerCase(); - this.filtered.set(searchFiltered || typeFiltered); - } - - async toggleProperty(property) { - - this.itemFlagData.update((data) => { - data[property] = !data[property] - return data; - }); - - const itemFlagData = get(this.itemFlagData); - - await PileUtilities.updateItemData(this.item, { flags: itemFlagData }); - - const pileFlagData = get(this.store.pileData); - if (pileFlagData.logMerchantActivity) { - await PileUtilities.updateMerchantLog(this.store.actor, { - type: "event", - user: game.user.id, - item: this.item.name, - property, - value: !itemFlagData[property] - }); - } - } - - updateQuantity(quantity) { - const pileFlagData = get(this.store.pileData); - const itemFlagData = get(this.itemFlagData); - const roll = new Roll(quantity).evaluate({ async: false }); - this.quantity.set(roll.total); - const baseData = {}; - if (itemFlagData.isService || pileFlagData.keepZeroQuantity || itemFlagData.keepZeroQuantity) { - baseData[CONSTANTS.FLAGS.ITEM + ".notForSale"] = roll.total <= 0; - } - if (pileFlagData.logMerchantActivity) { - PileUtilities.updateMerchantLog(this.store.actor, { - type: "event", - user: game.user.id, - item: this.item.name, - qty: roll.total - }); - } - return this.item.update(Utilities.setItemQuantity(baseData, roll.total)); - } + setupStores(...args) { + super.setupStores(...args); + this.prices = writable([]); + this.displayQuantity = writable(false); + this.selectedPriceGroup = writable(-1); + this.quantityToBuy = writable(1); + this.quantityForPrice = writable(1); + this.infiniteQuantity = writable(false); + this.isService = false; + } + + setupSubscriptions() { + let setup = false; + super.setupSubscriptions(); + this.subscribeTo(this.store.pileData, () => { + if (!setup) return; + this.refreshPriceData(); + this.refreshDisplayQuantity(); + }); + if (this.store.recipient) { + this.subscribeTo(this.store.recipientPileData, () => { + if (!setup) return + this.refreshPriceData(); + this.refreshDisplayQuantity(); + }); + } + this.subscribeTo(this.quantityToBuy, () => { + if (!setup) return; + this.refreshPriceData(); + }); + this.subscribeTo(this.itemDocument, () => { + if (!setup) return; + this.refreshPriceData(); + this.store.refreshItems(); + }); + this.subscribeTo(this.store.typeFilter, () => { + if (!setup) return; + this.filter() + }); + this.subscribeTo(this.itemFlagData, (flagData) => { + this.isService = flagData.isService; + if (!setup) return; + this.refreshPriceData(); + this.refreshDisplayQuantity(); + }); + this.refreshDisplayQuantity(); + setup = true; + } + + refreshDisplayQuantity() { + + const pileData = get(this.store.pileData); + const itemFlagData = get(this.itemFlagData); + const isMerchant = PileUtilities.isItemPileMerchant(this.store.actor, pileData) + + const merchantDisplayQuantity = pileData.displayQuantity; + const itemFlagDataQuantity = itemFlagData.displayQuantity; + + const itemInfiniteQuantity = { + "default": pileData.infiniteQuantity, + "yes": true, + "no": false + }[isMerchant ? (itemFlagData.infiniteQuantity ?? "default") : "no"]; + + this.infiniteQuantity.set(itemInfiniteQuantity) + + if (itemFlagDataQuantity === "always") { + return this.displayQuantity.set(true); + } + + const itemDisplayQuantity = { + "default": merchantDisplayQuantity === "yes", + "yes": true, + "no": false + }[itemFlagDataQuantity ?? "default"]; + + if (merchantDisplayQuantity.startsWith("always")) { + return this.displayQuantity.set(merchantDisplayQuantity.endsWith("yes")); + } + + this.displayQuantity.set(itemDisplayQuantity); + + } + + refreshPriceData(sellerFlagData, buyerFlagData) { + + const quantityToBuy = get(this.quantityToBuy); + const itemFlagData = get(this.itemFlagData); + sellerFlagData = sellerFlagData ?? get(this.store.pileData); + buyerFlagData = buyerFlagData ?? get(this.store.recipientPileData); + const priceData = PileUtilities.getPriceData({ + item: this.item, + seller: this.store.actor, + buyer: this.store.recipient, + sellerFlagData, + buyerFlagData, + itemFlagData, + quantity: quantityToBuy + }); + + let selectedPriceGroup = get(this.selectedPriceGroup); + if (selectedPriceGroup === -1) { + selectedPriceGroup = Math.max(0, priceData.findIndex(price => price.maxQuantity)); + this.selectedPriceGroup.set(selectedPriceGroup) + } + + this.prices.set(priceData); + + this.quantityForPrice.set( + game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE + ? getProperty(this.item, game.itempiles.API.QUANTITY_FOR_PRICE_ATTRIBUTE) ?? 1 + : 1 + ); + + } + + filter() { + const name = get(this.name).trim(); + const type = get(this.category).type; + const search = get(this.store.search).trim(); + const typeFilter = get(this.store.typeFilter); + const searchFiltered = !name.toLowerCase().includes(search.toLowerCase()); + const typeFiltered = typeFilter !== "all" && typeFilter.toLowerCase() !== type.toLowerCase(); + this.filtered.set(searchFiltered || typeFiltered); + } + + async toggleProperty(property) { + + this.itemFlagData.update((data) => { + data[property] = !data[property] + return data; + }); + + const itemFlagData = get(this.itemFlagData); + + await PileUtilities.updateItemData(this.item, { flags: itemFlagData }); + + const pileFlagData = get(this.store.pileData); + if (pileFlagData.logMerchantActivity) { + await PileUtilities.updateMerchantLog(this.store.actor, { + type: "event", + user: game.user.id, + item: this.item.name, + property, + value: !itemFlagData[property] + }); + } + } + + updateQuantity(quantity) { + const pileFlagData = get(this.store.pileData); + const itemFlagData = get(this.itemFlagData); + const roll = new Roll(quantity).evaluate({ async: false }); + this.quantity.set(roll.total); + const baseData = {}; + if (itemFlagData.isService || pileFlagData.keepZeroQuantity || itemFlagData.keepZeroQuantity) { + baseData[CONSTANTS.FLAGS.ITEM + ".notForSale"] = roll.total <= 0; + } + if (pileFlagData.logMerchantActivity) { + PileUtilities.updateMerchantLog(this.store.actor, { + type: "event", + user: game.user.id, + item: this.item.name, + qty: roll.total + }); + } + return this.item.update(Utilities.setItemQuantity(baseData, roll.total)); + } } diff --git a/src/stores/pile-item.js b/src/stores/pile-item.js index ffcc2cbe..dff6c76b 100644 --- a/src/stores/pile-item.js +++ b/src/stores/pile-item.js @@ -8,301 +8,300 @@ import * as Helpers from "../helpers/helpers.js"; import { Plugins } from "../plugins/main.js"; import { SYSTEMS } from "../systems.js"; import * as CompendiumUtilities from "../helpers/compendium-utilities.js"; -import { findSimilarItemInCompendiumSync } from "../helpers/compendium-utilities.js"; class PileBaseItem { - constructor(store, data, isCurrency = false, isSecondaryCurrency = false) { - this.store = store; - this.subscriptions = []; - this.isCurrency = isCurrency; - this.isSecondaryCurrency = isSecondaryCurrency; - this.setup(data); - } - - setupStores() { - this.category = writable({ service: false, type: "", label: "" }); - this.quantity = writable(1); - this.currentQuantity = writable(1); - this.quantityLeft = writable(1); - this.filtered = writable(true); - this.presentFromTheStart = writable(false); - this.rarityColor = writable(false); - } - - setupSubscriptions() { - // Higher order implementation - } - - setup(data) { - this.unsubscribe(); - this.setupStores(data); - this.setupSubscriptions(data); - } - - subscribeTo(target, callback) { - this.subscriptions.push(target.subscribe(callback)); - } - - unsubscribe() { - this.subscriptions.forEach(unsubscribe => unsubscribe()); - this.subscriptions = []; - } - - preview() { - } + constructor(store, data, isCurrency = false, isSecondaryCurrency = false) { + this.store = store; + this.subscriptions = []; + this.isCurrency = isCurrency; + this.isSecondaryCurrency = isSecondaryCurrency; + this.setup(data); + } + + setupStores() { + this.category = writable({ service: false, type: "", label: "" }); + this.quantity = writable(1); + this.currentQuantity = writable(1); + this.quantityLeft = writable(1); + this.filtered = writable(true); + this.presentFromTheStart = writable(false); + this.rarityColor = writable(false); + } + + setupSubscriptions() { + // Higher order implementation + } + + setup(data) { + this.unsubscribe(); + this.setupStores(data); + this.setupSubscriptions(data); + } + + subscribeTo(target, callback) { + this.subscriptions.push(target.subscribe(callback)); + } + + unsubscribe() { + this.subscriptions.forEach(unsubscribe => unsubscribe()); + this.subscriptions = []; + } + + preview() { + } } export class PileItem extends PileBaseItem { - setupStores(item) { - super.setupStores(); - this.item = item; - this.itemDocument = new TJSDocument(this.item); - this.canStack = PileUtilities.canItemStack(this.item, this.actor); - this.presentFromTheStart.set(Utilities.getItemQuantity(this.item) > 0 || !this.canStack); - this.quantity.set(this.canStack ? Utilities.getItemQuantity(this.item) : 1); - this.currentQuantity.set(Math.min(get(this.currentQuantity), get(this.quantityLeft), get(this.quantity))); - this.id = this.item.id; - this.type = this.item.type; - const itemData = CompendiumUtilities.findSimilarItemInCompendiumSync(this.item); - this.name = writable(itemData?.name ?? this.item.name); - this.img = writable(itemData?.img ?? this.item.img); - this.abbreviation = writable(""); - this.identifier = randomID(); - this.itemFlagData = writable(PileUtilities.getItemFlagData(this.item)); - } - - setupSubscriptions() { - super.setupSubscriptions() - - this.subscribeTo(this.store.pileData, () => { - this.setupProperties(); - }); - this.subscribeTo(this.store.pileCurrencies, () => { - this.setupProperties(); - }); - - this.subscribeTo(this.store.shareData, () => { - if (!this.toShare) { - this.quantityLeft.set(get(this.quantity)); - return; - } - const quantityLeft = SharingUtilities.getItemSharesLeftForActor(this.store.actor, this.item, this.store.recipient); - this.quantityLeft.set(quantityLeft); - }); - - this.subscribeTo(this.itemDocument, () => { - const { data } = this.itemDocument.updateOptions; - const itemData = CompendiumUtilities.findSimilarItemInCompendiumSync(this.item); - this.name = writable(itemData?.name ?? this.item.name); - this.img = writable(itemData?.img ?? this.item.img); - this.similarities = Utilities.setSimilarityProperties({}, this.item); - if (PileUtilities.canItemStack(this.item, this.store.actor) && Utilities.hasItemQuantity(data)) { - this.quantity.set(Utilities.getItemQuantity(data)); - const quantity = Math.min(get(this.currentQuantity), get(this.quantityLeft), get(this.quantity)); - this.currentQuantity.set(quantity); - } - if (hasProperty(data, CONSTANTS.FLAGS.ITEM)) { - this.itemFlagData.set(PileUtilities.getItemFlagData(this.item)); - this.updateCategory(); - this.store.refreshItems(); - } - if (Plugins["rarity-colors"].data) { - this.rarityColor.set(Plugins["rarity-colors"].data.getItemColor(this.item)); - } - }); - - this.updateCategory(); - - this.subscribeTo(this.quantity, this.filter.bind(this)); - this.subscribeTo(this.store.search, this.filter.bind(this)); - this.subscribeTo(this.category, this.filter.bind(this)); - } - - setupProperties() { - const actorIsItemPile = PileUtilities.isValidItemPile(this.store.actor, get(this.store.pileData)); - const pileActor = actorIsItemPile ? this.store.actor : this.store.recipient; - const pileActorData = actorIsItemPile ? this.store.pileData : this.store.recipientPileData; - const pileCurrencies = actorIsItemPile ? get(this.store.pileCurrencies) : get(this.store.recipientCurrencies); - this.isCurrency = PileUtilities.isItemCurrency(this.item, { - target: pileActor, - actorCurrencies: pileCurrencies - }); - const currency = this.isCurrency ? PileUtilities.getItemCurrencyData(this.item, { - target: pileActor, - actorCurrencies: pileCurrencies - }) : {}; - this.isSecondaryCurrency = !!currency?.secondary; - this.abbreviation.set(currency?.abbreviation ?? ""); - this.similarities = Utilities.setSimilarityProperties({}, this.item); - this.name.set(this.isCurrency ? currency.name : this.item.name); - this.img.set(this.isCurrency ? currency.img : this.item.img); - this.toShare = this.isCurrency - ? get(pileActorData).shareCurrenciesEnabled && !!this.store.recipient - : get(pileActorData).shareItemsEnabled && !!this.store.recipient; - } - - updateCategory() { - const pileData = get(this.store.pileData); - const itemFlagData = get(this.itemFlagData); - this.category.update(cat => { - cat.service = itemFlagData?.isService; - if (itemFlagData.customCategory) { - cat.type = itemFlagData.customCategory.toLowerCase(); - cat.label = itemFlagData.customCategory; - } else if (cat.service && pileData.enabled && pileData.type === CONSTANTS.PILE_TYPES.MERCHANT) { - cat.type = "item-piles-service"; - cat.label = "ITEM-PILES.Merchant.Service"; - } else { - cat.type = this.type; - cat.label = CONFIG.Item.typeLabels[this.type]; - } - return cat; - }); - } - - filter() { - const name = get(this.name).trim(); - const search = get(this.store.search).trim(); - const presentFromTheStart = get(this.presentFromTheStart); - const quantity = get(this.quantity); - if (quantity === 0 && !presentFromTheStart) { - this.filtered.set(true); - } else if (search) { - this.filtered.set(!name.toLowerCase().includes(search.toLowerCase())); - } else { - this.filtered.set(!presentFromTheStart && quantity === 0); - } - } - - take() { - const quantity = Math.min(get(this.currentQuantity), get(this.quantityLeft)); - if (!quantity) return; - return game.itempiles.API.transferItems( - this.store.actor, - this.store.recipient, - [{ _id: this.id, quantity }], - { interactionId: this.store.interactionId } - ); - } - - async remove() { - return game.itempiles.API.removeItems(this.store.actor, [this.id]); - } - - updateQuantity(quantity, add = false) { - let total = typeof quantity === "string" ? (new Roll(quantity).evaluate({ async: false })).total : quantity; - if (add) { - total += get(this.quantity); - } - this.quantity.set(total); - return this.item.update(Utilities.setItemQuantity({}, total)); - } - - async updateFlags() { - await this.item.update({ - [CONSTANTS.FLAGS.ITEM]: get(this.itemFlagData), - [CONSTANTS.FLAGS.VERSION]: Helpers.getModuleVersion() - }) - } - - preview() { - const pileData = get(this.store.pileData); - if (!pileData.canInspectItems && !game.user.isGM) return; - if (SYSTEMS.DATA?.PREVIEW_ITEM_TRANSFORMER) { - if (!SYSTEMS.DATA?.PREVIEW_ITEM_TRANSFORMER(this.item)) { - return; - } - } - if (game.user.isGM || this.item.permission[game.user.id] === 3) { - return this.item.sheet.render(true); - } - const cls = this.item._getSheetClass(); - const sheet = new cls(this.item, { editable: false }); - return sheet._render(true); - } + setupStores(item) { + super.setupStores(); + this.item = item; + this.itemDocument = new TJSDocument(this.item); + this.canStack = PileUtilities.canItemStack(this.item, this.actor); + this.presentFromTheStart.set(Utilities.getItemQuantity(this.item) > 0 || !this.canStack); + this.quantity.set(this.canStack ? Utilities.getItemQuantity(this.item) : 1); + this.currentQuantity.set(Math.min(get(this.currentQuantity), get(this.quantityLeft), get(this.quantity))); + this.id = this.item.id; + this.type = this.item.type; + const itemData = CompendiumUtilities.findSimilarItemInCompendiumSync(this.item); + this.name = writable(itemData?.name ?? this.item.name); + this.img = writable(itemData?.img ?? this.item.img); + this.abbreviation = writable(""); + this.identifier = randomID(); + this.itemFlagData = writable(PileUtilities.getItemFlagData(this.item)); + } + + setupSubscriptions() { + super.setupSubscriptions() + + this.subscribeTo(this.store.pileData, () => { + this.setupProperties(); + }); + this.subscribeTo(this.store.pileCurrencies, () => { + this.setupProperties(); + }); + + this.subscribeTo(this.store.shareData, () => { + if (!this.toShare) { + this.quantityLeft.set(get(this.quantity)); + return; + } + const quantityLeft = SharingUtilities.getItemSharesLeftForActor(this.store.actor, this.item, this.store.recipient); + this.quantityLeft.set(quantityLeft); + }); + + this.subscribeTo(this.itemDocument, () => { + const { data } = this.itemDocument.updateOptions; + const itemData = CompendiumUtilities.findSimilarItemInCompendiumSync(this.item); + this.name = writable(itemData?.name ?? this.item.name); + this.img = writable(itemData?.img ?? this.item.img); + this.similarities = Utilities.setSimilarityProperties({}, this.item); + if (PileUtilities.canItemStack(this.item, this.store.actor) && Utilities.hasItemQuantity(data)) { + this.quantity.set(Utilities.getItemQuantity(data)); + const quantity = Math.min(get(this.currentQuantity), get(this.quantityLeft), get(this.quantity)); + this.currentQuantity.set(quantity); + } + if (hasProperty(data, CONSTANTS.FLAGS.ITEM)) { + this.itemFlagData.set(PileUtilities.getItemFlagData(this.item)); + this.updateCategory(); + this.store.refreshItems(); + } + if (Plugins["rarity-colors"].data) { + this.rarityColor.set(Plugins["rarity-colors"].data.getItemColor(this.item)); + } + }); + + this.updateCategory(); + + this.subscribeTo(this.quantity, this.filter.bind(this)); + this.subscribeTo(this.store.search, this.filter.bind(this)); + this.subscribeTo(this.category, this.filter.bind(this)); + } + + setupProperties() { + const actorIsItemPile = PileUtilities.isValidItemPile(this.store.actor, get(this.store.pileData)); + const pileActor = actorIsItemPile ? this.store.actor : this.store.recipient; + const pileActorData = actorIsItemPile ? this.store.pileData : this.store.recipientPileData; + const pileCurrencies = actorIsItemPile ? get(this.store.pileCurrencies) : get(this.store.recipientCurrencies); + this.isCurrency = PileUtilities.isItemCurrency(this.item, { + target: pileActor, + actorCurrencies: pileCurrencies + }); + const currency = this.isCurrency ? PileUtilities.getItemCurrencyData(this.item, { + target: pileActor, + actorCurrencies: pileCurrencies + }) : {}; + this.isSecondaryCurrency = !!currency?.secondary; + this.abbreviation.set(currency?.abbreviation ?? ""); + this.similarities = Utilities.setSimilarityProperties({}, this.item); + this.name.set(this.isCurrency ? currency.name : this.item.name); + this.img.set(this.isCurrency ? currency.img : this.item.img); + this.toShare = this.isCurrency + ? get(pileActorData).shareCurrenciesEnabled && !!this.store.recipient + : get(pileActorData).shareItemsEnabled && !!this.store.recipient; + } + + updateCategory() { + const pileData = get(this.store.pileData); + const itemFlagData = get(this.itemFlagData); + this.category.update(cat => { + cat.service = itemFlagData?.isService; + if (itemFlagData.customCategory) { + cat.type = itemFlagData.customCategory.toLowerCase(); + cat.label = itemFlagData.customCategory; + } else if (cat.service && pileData.enabled && pileData.type === CONSTANTS.PILE_TYPES.MERCHANT) { + cat.type = "item-piles-service"; + cat.label = "ITEM-PILES.Merchant.Service"; + } else { + cat.type = this.type; + cat.label = CONFIG.Item.typeLabels[this.type]; + } + return cat; + }); + } + + filter() { + const name = get(this.name).trim(); + const search = get(this.store.search).trim(); + const presentFromTheStart = get(this.presentFromTheStart); + const quantity = get(this.quantity); + if (quantity === 0 && !presentFromTheStart) { + this.filtered.set(true); + } else if (search) { + this.filtered.set(!name.toLowerCase().includes(search.toLowerCase())); + } else { + this.filtered.set(!presentFromTheStart && quantity === 0); + } + } + + take() { + const quantity = Math.min(get(this.currentQuantity), get(this.quantityLeft)); + if (!quantity) return; + return game.itempiles.API.transferItems( + this.store.actor, + this.store.recipient, + [{ _id: this.id, quantity }], + { interactionId: this.store.interactionId } + ); + } + + async remove() { + return game.itempiles.API.removeItems(this.store.actor, [this.id]); + } + + updateQuantity(quantity, add = false) { + let total = typeof quantity === "string" ? (new Roll(quantity).evaluate({ async: false })).total : quantity; + if (add) { + total += get(this.quantity); + } + this.quantity.set(total); + return this.item.update(Utilities.setItemQuantity({}, total)); + } + + async updateFlags() { + await this.item.update({ + [CONSTANTS.FLAGS.ITEM]: get(this.itemFlagData), + [CONSTANTS.FLAGS.VERSION]: Helpers.getModuleVersion() + }) + } + + preview() { + const pileData = get(this.store.pileData); + if (!pileData.canInspectItems && !game.user.isGM) return; + if (SYSTEMS.DATA?.PREVIEW_ITEM_TRANSFORMER) { + if (!SYSTEMS.DATA?.PREVIEW_ITEM_TRANSFORMER(this.item)) { + return; + } + } + if (game.user.isGM || this.item.permission[game.user.id] === 3) { + return this.item.sheet.render(true); + } + const cls = this.item._getSheetClass(); + const sheet = new cls(this.item, { editable: false }); + return sheet._render(true); + } } export class PileAttribute extends PileBaseItem { - setupStores(attribute) { - super.setupStores(); - this.attribute = attribute; - this.path = this.attribute.path; - this.name = writable(this.attribute.name); - this.img = writable(this.attribute.img); - this.abbreviation = writable(this.attribute.abbreviation); - this.identifier = randomID() - const startingQuantity = Number(getProperty(this.store.actor, this.path) ?? 0); - this.presentFromTheStart.set(startingQuantity > 0); - this.quantity.set(startingQuantity); - this.currentQuantity.set(Math.min(get(this.currentQuantity), get(this.quantityLeft), get(this.quantity))); - this.category.set({ type: "currency", label: "ITEM-PILES.Currency" }); - } - - setupSubscriptions() { - super.setupSubscriptions(); - - this.subscribeTo(this.store.pileData, this.setupProperties.bind(this)); - - this.subscribeTo(this.store.shareData, (val) => { - if (!this.toShare) { - this.quantityLeft.set(get(this.quantity)); - return; - } - const quantityLeft = SharingUtilities.getAttributeSharesLeftForActor(this.store.actor, this.path, this.store.recipient); - this.quantityLeft.set(quantityLeft); - }); - - this.subscribeTo(this.store.document, () => { - const { data } = this.store.document.updateOptions; - this.path = this.attribute.path; - this.name.set(this.attribute.name); - this.img.set(this.attribute.img); - if (hasProperty(data, this.path)) { - this.quantity.set(Number(getProperty(data, this.path) ?? 0)); - this.currentQuantity.set(Math.min(get(this.currentQuantity), get(this.quantityLeft), get(this.quantity))); - this.store.refreshItems(); - } - }); - - this.subscribeTo(this.quantity, this.filter.bind(this)); - this.subscribeTo(this.store.search, this.filter.bind(this)); - } - - setupProperties() { - this.toShare = get(this.store.pileData).shareCurrenciesEnabled && !!this.store.recipient; - } - - filter() { - const name = get(this.name); - const search = get(this.store.search); - const presentFromTheStart = get(this.presentFromTheStart); - const quantity = get(this.quantity); - if (quantity === 0 && !presentFromTheStart) { - this.filtered.set(true); - } else if (search) { - this.filtered.set(!name.toLowerCase().includes(search.toLowerCase())); - } else { - this.filtered.set(!presentFromTheStart && quantity === 0); - } - } - - take() { - const quantity = Math.min(get(this.currentQuantity), get(this.quantityLeft)); - return game.itempiles.API.transferAttributes( - this.store.actor, - this.store.recipient, - { [this.path]: quantity }, - { interactionId: this.store.interactionId } - ); - } - - updateQuantity() { - return this.store.actor.update({ - [this.path]: get(this.quantity) - }); - } + setupStores(attribute) { + super.setupStores(); + this.attribute = attribute; + this.path = this.attribute.path; + this.name = writable(this.attribute.name); + this.img = writable(this.attribute.img); + this.abbreviation = writable(this.attribute.abbreviation); + this.identifier = randomID() + const startingQuantity = Number(getProperty(this.store.actor, this.path) ?? 0); + this.presentFromTheStart.set(startingQuantity > 0); + this.quantity.set(startingQuantity); + this.currentQuantity.set(Math.min(get(this.currentQuantity), get(this.quantityLeft), get(this.quantity))); + this.category.set({ type: "currency", label: "ITEM-PILES.Currency" }); + } + + setupSubscriptions() { + super.setupSubscriptions(); + + this.subscribeTo(this.store.pileData, this.setupProperties.bind(this)); + + this.subscribeTo(this.store.shareData, (val) => { + if (!this.toShare) { + this.quantityLeft.set(get(this.quantity)); + return; + } + const quantityLeft = SharingUtilities.getAttributeSharesLeftForActor(this.store.actor, this.path, this.store.recipient); + this.quantityLeft.set(quantityLeft); + }); + + this.subscribeTo(this.store.document, () => { + const { data } = this.store.document.updateOptions; + this.path = this.attribute.path; + this.name.set(this.attribute.name); + this.img.set(this.attribute.img); + if (hasProperty(data, this.path)) { + this.quantity.set(Number(getProperty(data, this.path) ?? 0)); + this.currentQuantity.set(Math.min(get(this.currentQuantity), get(this.quantityLeft), get(this.quantity))); + this.store.refreshItems(); + } + }); + + this.subscribeTo(this.quantity, this.filter.bind(this)); + this.subscribeTo(this.store.search, this.filter.bind(this)); + } + + setupProperties() { + this.toShare = get(this.store.pileData).shareCurrenciesEnabled && !!this.store.recipient; + } + + filter() { + const name = get(this.name); + const search = get(this.store.search); + const presentFromTheStart = get(this.presentFromTheStart); + const quantity = get(this.quantity); + if (quantity === 0 && !presentFromTheStart) { + this.filtered.set(true); + } else if (search) { + this.filtered.set(!name.toLowerCase().includes(search.toLowerCase())); + } else { + this.filtered.set(!presentFromTheStart && quantity === 0); + } + } + + take() { + const quantity = Math.min(get(this.currentQuantity), get(this.quantityLeft)); + return game.itempiles.API.transferAttributes( + this.store.actor, + this.store.recipient, + { [this.path]: quantity }, + { interactionId: this.store.interactionId } + ); + } + + updateQuantity() { + return this.store.actor.update({ + [this.path]: get(this.quantity) + }); + } } diff --git a/src/stores/vault-store.js b/src/stores/vault-store.js index f3fe371e..29aea572 100644 --- a/src/stores/vault-store.js +++ b/src/stores/vault-store.js @@ -15,498 +15,498 @@ import { SYSTEMS } from "../systems.js"; export class VaultStore extends ItemPileStore { - constructor(...args) { - super(...args); - this.gridData = writable({}); - this.gridItems = writable([]); - this.validGridItems = writable([]); - this.canDepositCurrency = writable(false); - this.canWithdrawCurrency = writable(false); - this.logSearch = writable(""); - this.vaultLog = writable([]); - this.visibleLogItems = writable(18); - this.highlightedGridItems = writable([]); - this.vaultExpanderItems = writable([]); - this.dragPosition = writable({ x: 0, y: 0, w: 1, h: 1, active: false, }); - this.mainContainer = false; - } - - get searchDelay() { - return 50; - } - - get ItemClass() { - return VaultItem; - } - - setupStores(reset = false) { - super.setupStores(reset); - this.gridData.set({}); - this.gridItems.set([]); - this.validGridItems.set([]); - this.logSearch.set(""); - this.vaultLog.set([]); - this.visibleLogItems.set(18); - this.highlightedGridItems.set([]); - this.vaultExpanderItems.set([]); - this.dragPosition.set({ x: 0, y: 0, w: 1, h: 1, active: false, }); - - this.refreshGridDebounce = foundry.utils.debounce(() => { - this.refreshGrid(); - }, this.searchDelay); - } - - setupSubscriptions() { - super.setupSubscriptions(); - this.subscribeTo(this.pileData, () => { - this.refreshGridDebounce(); - this.processLogEntries(); - }); - - this.subscribeTo(this.document, () => { - const { data } = this.document.updateOptions; - if (hasProperty(data, CONSTANTS.FLAGS.LOG)) { - this.processLogEntries(); - } - }); - - this.subscribeTo(this.logSearch, this.filterLogEntries.bind(this)); - - this.refreshGrid(); - this.processLogEntries(); - } - - processLogEntries() { - - const pileData = get(this.pileData); - const logEntries = PileUtilities.getActorLog(this.actor); - - logEntries.map(log => { - - let instigator = log.actor || "Unknown character"; - if (pileData.vaultLogType === "user_actor") { - instigator = game.i18n.format("ITEM-PILES.Vault.LogUserActor", { - actor_name: log.actor || "Unknown character", - user_name: game.users.get(log.user)?.name ?? "unknown user", - }) - } else if (pileData.vaultLogType === "user") { - instigator = game.users.get(log.user)?.name ?? "unknown user"; - } - - const quantity = Math.abs(log.qty) > 1 - ? game.i18n.format("ITEM-PILES.Vault.LogQuantity", { quantity: Math.abs(log.qty) }) - : ""; - - if (!log.action) { - log.action = log.qty > 0 ? "deposited" : "withdrew"; - } - - const action = log.action === "withdrew" || log.action === "deposited" - ? game.i18n.localize("ITEM-PILES.Vault." + (log.action.slice(0, 1).toUpperCase() + log.action.slice(1))) - : log.action; - - log.text = game.i18n.format("ITEM-PILES.Vault.LogEntry", { - instigator, - action: `${action}`, - quantity: quantity, - item_name: `${log.name}`, - }) - log.visible = true; - - }); - - this.vaultLog.set(logEntries); - - this.filterLogEntries() - - } - - filterLogEntries() { - const search = get(this.logSearch).toLowerCase(); - const regex = new RegExp(search, "g"); - this.vaultLog.update((logs) => { - for (let log of logs) { - log.visible = log.text.toLowerCase().search(regex) !== -1; - } - return logs; - }) - } - - refreshFreeSpaces() { - const pileData = get(this.pileData); - this.gridData.update(() => { - - const access = PileUtilities.getVaultAccess(this.actor, { - flagData: pileData, - hasRecipient: !!this.recipient - }); - - return { - ...PileUtilities.getVaultGridData(this.actor, pileData), - ...access, - canEditCurrencies: game.user.isGM, - fullAccess: game.user.isGM || Object.values(access).every(Boolean), - gridSize: 40, - gap: 4 - } - }) - } - - canItemsFitWithout(itemToCompare) { - - const pileData = get(this.pileData); - const items = get(this.validGridItems); - const vaultExpanders = get(this.vaultExpanderItems); - - const expansions = vaultExpanders.reduce((acc, item) => { - if (item === itemToCompare) return acc; - acc.cols += get(item.itemFlagData).addsCols * get(item.quantity); - acc.rows += get(item.itemFlagData).addsRows * get(item.quantity); - return acc; - }, { - cols: pileData.baseExpansionCols ?? 0, - rows: pileData.baseExpansionRows ?? 0 - }); - - const enabledCols = Math.min(pileData.cols, expansions.cols); - const enabledRows = Math.min(pileData.rows, expansions.rows); - - const enabledSpaces = enabledCols * enabledRows; - - return enabledSpaces - items.length; - - } - - updateGrid(items) { - - if (!items.length) return; - - const itemsToUpdate = items.map(item => { - const transform = get(item.transform); - return { - _id: item.id, - [CONSTANTS.FLAGS.ITEM + ".x"]: transform.x, - [CONSTANTS.FLAGS.ITEM + ".y"]: transform.y - } - }); - - helpers.debug("itemsToUpdate", itemsToUpdate); - - const actorUuid = Utilities.getUuid(this.actor); - if (this.actor.isOwner) { - return PrivateAPI._commitActorChanges(actorUuid, { - itemsToUpdate, - }); - } - - return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.COMMIT_ACTOR_CHANGES, actorUuid, { - itemsToUpdate, - }); - } - - refreshItems() { - super.refreshItems(); - const pileData = get(this.pileData); - this.validGridItems.set(get(this.allItems).filter(entry => { - const itemFlagData = get(entry.itemFlagData); - return (!pileData.vaultExpansion || !itemFlagData.vaultExpander) && !entry.isCurrency; - })); - this.highlightedGridItems.set(get(this.items).filter(entry => { - const itemFlagData = get(entry.itemFlagData); - return !pileData.vaultExpansion || !itemFlagData.vaultExpander; - }).map(item => item.id)); - this.vaultExpanderItems.set(get(this.allItems).filter(entry => { - const itemFlagData = get(entry.itemFlagData); - return !pileData.vaultExpansion || itemFlagData.vaultExpander; - })); - this.refreshGridDebounce(); - } - - createItem(...args) { - super.createItem(...args); - this.refreshGrid(); - } - - deleteItem(...args) { - super.deleteItem(...args); - this.refreshGrid(); - } - - refreshGrid() { - this.refreshFreeSpaces(); - this.gridItems.set(this.placeItemsOnGrid()); - } - - placeItemsOnGrid() { - const search = get(this.search) - const highlightedItems = get(this.highlightedGridItems); - const gridData = get(this.gridData); - const allItems = [...get(this.validGridItems)]; - const existingItems = []; - - const grid = Array.from(Array(gridData.enabledCols).keys()).map((_, x) => { - return Array.from(Array(gridData.enabledRows).keys()).map((_, y) => { - const item = allItems.find(item => { - return item.x === x && item.y === y - }); - if (item) { - allItems.splice(allItems.indexOf(item), 1); - existingItems.push({ - id: item.id, - transform: item.transform, - highlight: search && highlightedItems.includes(item.id), - item, - }); - } - return item?.id ?? null; - }); - }); - - helpers.debug("grid", grid); - - helpers.debug("existingItems", existingItems); - - helpers.debug("allItems", allItems); - - const itemsToUpdate = allItems - .map(item => { - for (let x = 0; x < gridData.enabledCols; x++) { - for (let y = 0; y < gridData.enabledRows; y++) { - if (!grid[x][y]) { - grid[x][y] = item.id; - item.transform.update(trans => { - trans.x = x; - trans.y = y; - return trans; - }); - return { - id: item.id, - transform: item.transform, - highlight: search && highlightedItems.includes(item.id), - item - }; - } - } - } - }) - .filter(Boolean) - - this.updateGrid(itemsToUpdate) - - return itemsToUpdate.concat(existingItems); - - } - - async onDropData(data, event, isExpander) { - - const dragPosition = get(this.dragPosition); - const { x, y } = dragPosition; - this.dragPosition.set({ x: 0, y: 0, w: 1, h: 1, active: false, }); - - if (data.type === "Actor" && game.user.isGM) { - const oldHeight = this.mainContainer.getBoundingClientRect().height; - const newRecipient = data.uuid ? (await fromUuid(data.uuid)) : game.actors.get(data.id); - if (!this.recipient) { - setTimeout(() => { - const newHeight = this.mainContainer.getBoundingClientRect().height - oldHeight; - this.application.position.stores.height.set(get(this.application.position.stores.height) + newHeight); - }); - } - this.updateRecipient(newRecipient); - this.refreshFreeSpaces(); - return; - } - - if (data.type !== "Item") { - Helpers.custom_warning(`You can't drop documents of type "${data.type}" into this Item Piles vault!`, true) - return false; - } - - const item = await Item.implementation.fromDropData(data); - - const itemData = item.toObject(); - - if (!itemData) { - console.error(data); - throw Helpers.custom_error("Something went wrong when dropping this item!") - } - - const source = (data.uuid ? fromUuidSync(data.uuid) : false)?.parent ?? false; - const target = this.actor; - - if (source === target) { - Helpers.custom_warning(`You can't drop items into the vault that originate from the vault!`, true) - return false; - } - - if (!source && !game.user.isGM) { - Helpers.custom_warning(`Only GMs can drop items from the sidebar!`, true) - return false; - } - - const vaultExpander = getProperty(itemData, CONSTANTS.FLAGS.ITEM + ".vaultExpander"); - - if (isExpander && !vaultExpander) { - Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Warnings.VaultItemNotExpander"), true) - return false; - } - - const gridData = get(this.gridData); - - let flagData = PileUtilities.getItemFlagData(itemData); - setProperty(flagData, "x", x); - setProperty(flagData, "y", y); - setProperty(itemData, CONSTANTS.FLAGS.ITEM, flagData); - - if (!this.hasSimilarItem(itemData) && !vaultExpander && !gridData?.freeSpaces) { - Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Warnings.VaultFull"), true) - return false; - } - - return PrivateAPI._depositWithdrawItem({ - source, - target, - itemData: { - item: itemData, - quantity: 1 - }, - gridPosition: { x, y } - }); - - } + constructor(...args) { + super(...args); + this.gridData = writable({}); + this.gridItems = writable([]); + this.validGridItems = writable([]); + this.canDepositCurrency = writable(false); + this.canWithdrawCurrency = writable(false); + this.logSearch = writable(""); + this.vaultLog = writable([]); + this.visibleLogItems = writable(18); + this.highlightedGridItems = writable([]); + this.vaultExpanderItems = writable([]); + this.dragPosition = writable({ x: 0, y: 0, w: 1, h: 1, active: false, }); + this.mainContainer = false; + } + + get searchDelay() { + return 50; + } + + get ItemClass() { + return VaultItem; + } + + setupStores(reset = false) { + super.setupStores(reset); + this.gridData.set({}); + this.gridItems.set([]); + this.validGridItems.set([]); + this.logSearch.set(""); + this.vaultLog.set([]); + this.visibleLogItems.set(18); + this.highlightedGridItems.set([]); + this.vaultExpanderItems.set([]); + this.dragPosition.set({ x: 0, y: 0, w: 1, h: 1, active: false, }); + + this.refreshGridDebounce = foundry.utils.debounce(() => { + this.refreshGrid(); + }, this.searchDelay); + } + + setupSubscriptions() { + super.setupSubscriptions(); + this.subscribeTo(this.pileData, () => { + this.refreshGridDebounce(); + this.processLogEntries(); + }); + + this.subscribeTo(this.document, () => { + const { data } = this.document.updateOptions; + if (hasProperty(data, CONSTANTS.FLAGS.LOG)) { + this.processLogEntries(); + } + }); + + this.subscribeTo(this.logSearch, this.filterLogEntries.bind(this)); + + this.refreshGrid(); + this.processLogEntries(); + } + + processLogEntries() { + + const pileData = get(this.pileData); + const logEntries = PileUtilities.getActorLog(this.actor); + + logEntries.map(log => { + + let instigator = log.actor || "Unknown character"; + if (pileData.vaultLogType === "user_actor") { + instigator = game.i18n.format("ITEM-PILES.Vault.LogUserActor", { + actor_name: log.actor || "Unknown character", + user_name: game.users.get(log.user)?.name ?? "unknown user", + }) + } else if (pileData.vaultLogType === "user") { + instigator = game.users.get(log.user)?.name ?? "unknown user"; + } + + const quantity = Math.abs(log.qty) > 1 + ? game.i18n.format("ITEM-PILES.Vault.LogQuantity", { quantity: Math.abs(log.qty) }) + : ""; + + if (!log.action) { + log.action = log.qty > 0 ? "deposited" : "withdrew"; + } + + const action = log.action === "withdrew" || log.action === "deposited" + ? game.i18n.localize("ITEM-PILES.Vault." + (log.action.slice(0, 1).toUpperCase() + log.action.slice(1))) + : log.action; + + log.text = game.i18n.format("ITEM-PILES.Vault.LogEntry", { + instigator, + action: `${action}`, + quantity: quantity, + item_name: `${log.name}`, + }) + log.visible = true; + + }); + + this.vaultLog.set(logEntries); + + this.filterLogEntries() + + } + + filterLogEntries() { + const search = get(this.logSearch).toLowerCase(); + const regex = new RegExp(search, "g"); + this.vaultLog.update((logs) => { + for (let log of logs) { + log.visible = log.text.toLowerCase().search(regex) !== -1; + } + return logs; + }) + } + + refreshFreeSpaces() { + const pileData = get(this.pileData); + this.gridData.update(() => { + + const access = PileUtilities.getVaultAccess(this.actor, { + flagData: pileData, + hasRecipient: !!this.recipient + }); + + return { + ...PileUtilities.getVaultGridData(this.actor, pileData), + ...access, + canEditCurrencies: game.user.isGM, + fullAccess: game.user.isGM || Object.values(access).every(Boolean), + gridSize: 40, + gap: 4 + } + }) + } + + canItemsFitWithout(itemToCompare) { + + const pileData = get(this.pileData); + const items = get(this.validGridItems); + const vaultExpanders = get(this.vaultExpanderItems); + + const expansions = vaultExpanders.reduce((acc, item) => { + if (item === itemToCompare) return acc; + acc.cols += get(item.itemFlagData).addsCols * get(item.quantity); + acc.rows += get(item.itemFlagData).addsRows * get(item.quantity); + return acc; + }, { + cols: pileData.baseExpansionCols ?? 0, + rows: pileData.baseExpansionRows ?? 0 + }); + + const enabledCols = Math.min(pileData.cols, expansions.cols); + const enabledRows = Math.min(pileData.rows, expansions.rows); + + const enabledSpaces = enabledCols * enabledRows; + + return enabledSpaces - items.length; + + } + + updateGrid(items) { + + if (!items.length) return; + + const itemsToUpdate = items.map(item => { + const transform = get(item.transform); + return { + _id: item.id, + [CONSTANTS.FLAGS.ITEM + ".x"]: transform.x, + [CONSTANTS.FLAGS.ITEM + ".y"]: transform.y + } + }); + + helpers.debug("itemsToUpdate", itemsToUpdate); + + const actorUuid = Utilities.getUuid(this.actor); + if (this.actor.isOwner) { + return PrivateAPI._commitActorChanges(actorUuid, { + itemsToUpdate, + }); + } + + return ItemPileSocket.executeAsGM(ItemPileSocket.HANDLERS.COMMIT_ACTOR_CHANGES, actorUuid, { + itemsToUpdate, + }); + } + + refreshItems() { + super.refreshItems(); + const pileData = get(this.pileData); + this.validGridItems.set(get(this.allItems).filter(entry => { + const itemFlagData = get(entry.itemFlagData); + return (!pileData.vaultExpansion || !itemFlagData.vaultExpander) && !entry.isCurrency; + })); + this.highlightedGridItems.set(get(this.items).filter(entry => { + const itemFlagData = get(entry.itemFlagData); + return !pileData.vaultExpansion || !itemFlagData.vaultExpander; + }).map(item => item.id)); + this.vaultExpanderItems.set(get(this.allItems).filter(entry => { + const itemFlagData = get(entry.itemFlagData); + return !pileData.vaultExpansion || itemFlagData.vaultExpander; + })); + this.refreshGridDebounce(); + } + + createItem(...args) { + super.createItem(...args); + this.refreshGrid(); + } + + deleteItem(...args) { + super.deleteItem(...args); + this.refreshGrid(); + } + + refreshGrid() { + this.refreshFreeSpaces(); + this.gridItems.set(this.placeItemsOnGrid()); + } + + placeItemsOnGrid() { + const search = get(this.search) + const highlightedItems = get(this.highlightedGridItems); + const gridData = get(this.gridData); + const allItems = [...get(this.validGridItems)]; + const existingItems = []; + + const grid = Array.from(Array(gridData.enabledCols).keys()).map((_, x) => { + return Array.from(Array(gridData.enabledRows).keys()).map((_, y) => { + const item = allItems.find(item => { + return item.x === x && item.y === y + }); + if (item) { + allItems.splice(allItems.indexOf(item), 1); + existingItems.push({ + id: item.id, + transform: item.transform, + highlight: search && highlightedItems.includes(item.id), + item, + }); + } + return item?.id ?? null; + }); + }); + + helpers.debug("grid", grid); + + helpers.debug("existingItems", existingItems); + + helpers.debug("allItems", allItems); + + const itemsToUpdate = allItems + .map(item => { + for (let x = 0; x < gridData.enabledCols; x++) { + for (let y = 0; y < gridData.enabledRows; y++) { + if (!grid[x][y]) { + grid[x][y] = item.id; + item.transform.update(trans => { + trans.x = x; + trans.y = y; + return trans; + }); + return { + id: item.id, + transform: item.transform, + highlight: search && highlightedItems.includes(item.id), + item + }; + } + } + } + }) + .filter(Boolean) + + this.updateGrid(itemsToUpdate) + + return itemsToUpdate.concat(existingItems); + + } + + async onDropData(data, event, isExpander) { + + const dragPosition = get(this.dragPosition); + const { x, y } = dragPosition; + this.dragPosition.set({ x: 0, y: 0, w: 1, h: 1, active: false, }); + + if (data.type === "Actor" && game.user.isGM) { + const oldHeight = this.mainContainer.getBoundingClientRect().height; + const newRecipient = data.uuid ? (await fromUuid(data.uuid)) : game.actors.get(data.id); + if (!this.recipient) { + setTimeout(() => { + const newHeight = this.mainContainer.getBoundingClientRect().height - oldHeight; + this.application.position.stores.height.set(get(this.application.position.stores.height) + newHeight); + }); + } + this.updateRecipient(newRecipient); + this.refreshFreeSpaces(); + return; + } + + if (data.type !== "Item") { + Helpers.custom_warning(`You can't drop documents of type "${data.type}" into this Item Piles vault!`, true) + return false; + } + + const item = await Item.implementation.fromDropData(data); + + const itemData = item.toObject(); + + if (!itemData) { + console.error(data); + throw Helpers.custom_error("Something went wrong when dropping this item!") + } + + const source = (data.uuid ? fromUuidSync(data.uuid) : false)?.parent ?? false; + const target = this.actor; + + if (source === target) { + Helpers.custom_warning(`You can't drop items into the vault that originate from the vault!`, true) + return false; + } + + if (!source && !game.user.isGM) { + Helpers.custom_warning(`Only GMs can drop items from the sidebar!`, true) + return false; + } + + const vaultExpander = getProperty(itemData, CONSTANTS.FLAGS.ITEM + ".vaultExpander"); + + if (isExpander && !vaultExpander) { + Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Warnings.VaultItemNotExpander"), true) + return false; + } + + const gridData = get(this.gridData); + + let flagData = PileUtilities.getItemFlagData(itemData); + setProperty(flagData, "x", x); + setProperty(flagData, "y", y); + setProperty(itemData, CONSTANTS.FLAGS.ITEM, flagData); + + if (!this.hasSimilarItem(itemData) && !vaultExpander && !gridData?.freeSpaces) { + Helpers.custom_warning(game.i18n.localize("ITEM-PILES.Warnings.VaultFull"), true) + return false; + } + + return PrivateAPI._depositWithdrawItem({ + source, + target, + itemData: { + item: itemData, + quantity: 1 + }, + gridPosition: { x, y } + }); + + } } export class VaultItem extends PileItem { - setupStores(...args) { - super.setupStores(...args); - this.transform = writable({ - x: 0, y: 0, w: 1, h: 1 - }); - this.x = 0; - this.y = 0; - this.style = writable({}); - } - - setupSubscriptions() { - super.setupSubscriptions(); - let setup = false; - this.subscribeTo(this.itemDocument, () => { - let rarityColor = get(this.rarityColor); - if (rarityColor) { - this.style.set({ "box-shadow": `inset 0px 0px 7px 0px ${rarityColor}` }); - } else { - this.style.set(SYSTEMS.DATA?.VAULT_STYLES ? SYSTEMS.DATA?.VAULT_STYLES.filter(style => { - return getProperty(this.item, style.path) === style.value; - }).reduce((acc, style) => { - return foundry.utils.mergeObject(acc, style.styling); - }, {}) : {}); - } - }); - this.subscribeTo(this.itemFlagData, (data) => { - if (setup) { - helpers.debug("itemFlagData", data); - } - this.transform.set({ - x: data.x, y: data.y, w: data.width ?? 1, h: data.height ?? 1 - }); - }); - this.subscribeTo(this.transform, (transform) => { - if (setup) { - helpers.debug("transform", transform); - } - this.x = transform.x; - this.y = transform.y; - }); - this.subscribeTo(this.quantity, () => { - const itemFlagData = get(this.itemFlagData); - if (!itemFlagData.vaultExpander) return; - this.store.refreshFreeSpaces(); - this.store.refreshGridDebounce(); - }) - setup = true; - } - - async take() { - - const pileData = get(this.store.pileData); - const itemFlagData = get(this.itemFlagData); - - if (pileData.vaultExpansion && itemFlagData.vaultExpander) { - const slotsLeft = this.store.canItemsFitWithout(this); - if (slotsLeft < 0) { - return TJSDialog.prompt({ - title: "Item Piles", content: { - class: CustomDialog, props: { - header: game.i18n.localize("ITEM-PILES.Dialogs.CantRemoveVaultExpander.Title"), - content: game.i18n.format("ITEM-PILES.Dialogs.CantRemoveVaultExpander.Content", { - num_items: Math.abs(slotsLeft) - }) - } - }, - modal: true - }); - } - } - - let quantity = get(this.quantity); - if (quantity > 1) { - quantity = await DropItemDialog.show(this.item, this.store.actor, { - localizationTitle: "WithdrawItem" - }); - } - return game.itempiles.API.transferItems(this.store.actor, this.store.recipient, [{ - _id: this.id, - quantity - }], { interactionId: this.store.interactionId }); - } - - async split(x, y) { - - let quantity = await DropItemDialog.show(this.item, this.store.actor, { - localizationTitle: "SplitItem", - quantityAdjustment: -1 - }); - - await game.itempiles.API.removeItems(this.store.actor, [{ - _id: this.id, - quantity - }], { interactionId: this.store.interactionId }); - - const itemData = this.item.toObject(); - - const flagData = PileUtilities.getItemFlagData(this.item); - setProperty(flagData, "x", x); - setProperty(flagData, "y", y); - setProperty(itemData, CONSTANTS.FLAGS.ITEM, flagData); - - await game.itempiles.API.addItems(this.store.actor, [{ - item: itemData, - quantity - }], { interactionId: this.store.interactionId }); - - } - - async merge(itemToMerge) { - - const itemDelta = await game.itempiles.API.removeItems(this.store.actor, [{ - _id: itemToMerge.id - }], { - interactionId: this.store.interactionId, - skipVaultLogging: true - }); - - return game.itempiles.API.addItems(this.store.actor, [{ id: this.id, quantity: Math.abs(itemDelta[0].quantity) }], { - interactionId: this.store.interactionId, - skipVaultLogging: true - }) - - } - - areItemsSimilar(itemToCompare) { - return !Utilities.areItemsDifferent(this.item, itemToCompare.item) - && PileUtilities.canItemStack(itemToCompare.item, this.store.actor); - } + setupStores(...args) { + super.setupStores(...args); + this.transform = writable({ + x: 0, y: 0, w: 1, h: 1 + }); + this.x = 0; + this.y = 0; + this.style = writable({}); + } + + setupSubscriptions() { + super.setupSubscriptions(); + let setup = false; + this.subscribeTo(this.itemDocument, () => { + let rarityColor = get(this.rarityColor); + if (rarityColor) { + this.style.set({ "box-shadow": `inset 0px 0px 7px 0px ${rarityColor}` }); + } else { + this.style.set(SYSTEMS.DATA?.VAULT_STYLES ? SYSTEMS.DATA?.VAULT_STYLES.filter(style => { + return getProperty(this.item, style.path) === style.value; + }).reduce((acc, style) => { + return foundry.utils.mergeObject(acc, style.styling); + }, {}) : {}); + } + }); + this.subscribeTo(this.itemFlagData, (data) => { + if (setup) { + helpers.debug("itemFlagData", data); + } + this.transform.set({ + x: data.x, y: data.y, w: data.width ?? 1, h: data.height ?? 1 + }); + }); + this.subscribeTo(this.transform, (transform) => { + if (setup) { + helpers.debug("transform", transform); + } + this.x = transform.x; + this.y = transform.y; + }); + this.subscribeTo(this.quantity, () => { + const itemFlagData = get(this.itemFlagData); + if (!itemFlagData.vaultExpander) return; + this.store.refreshFreeSpaces(); + this.store.refreshGridDebounce(); + }) + setup = true; + } + + async take() { + + const pileData = get(this.store.pileData); + const itemFlagData = get(this.itemFlagData); + + if (pileData.vaultExpansion && itemFlagData.vaultExpander) { + const slotsLeft = this.store.canItemsFitWithout(this); + if (slotsLeft < 0) { + return TJSDialog.prompt({ + title: "Item Piles", content: { + class: CustomDialog, props: { + header: game.i18n.localize("ITEM-PILES.Dialogs.CantRemoveVaultExpander.Title"), + content: game.i18n.format("ITEM-PILES.Dialogs.CantRemoveVaultExpander.Content", { + num_items: Math.abs(slotsLeft) + }) + } + }, + modal: true + }); + } + } + + let quantity = get(this.quantity); + if (quantity > 1) { + quantity = await DropItemDialog.show(this.item, this.store.actor, { + localizationTitle: "WithdrawItem" + }); + } + return game.itempiles.API.transferItems(this.store.actor, this.store.recipient, [{ + _id: this.id, + quantity + }], { interactionId: this.store.interactionId }); + } + + async split(x, y) { + + let quantity = await DropItemDialog.show(this.item, this.store.actor, { + localizationTitle: "SplitItem", + quantityAdjustment: -1 + }); + + await game.itempiles.API.removeItems(this.store.actor, [{ + _id: this.id, + quantity + }], { interactionId: this.store.interactionId }); + + const itemData = this.item.toObject(); + + const flagData = PileUtilities.getItemFlagData(this.item); + setProperty(flagData, "x", x); + setProperty(flagData, "y", y); + setProperty(itemData, CONSTANTS.FLAGS.ITEM, flagData); + + await game.itempiles.API.addItems(this.store.actor, [{ + item: itemData, + quantity + }], { interactionId: this.store.interactionId }); + + } + + async merge(itemToMerge) { + + const itemDelta = await game.itempiles.API.removeItems(this.store.actor, [{ + _id: itemToMerge.id + }], { + interactionId: this.store.interactionId, + skipVaultLogging: true + }); + + return game.itempiles.API.addItems(this.store.actor, [{ id: this.id, quantity: Math.abs(itemDelta[0].quantity) }], { + interactionId: this.store.interactionId, + skipVaultLogging: true + }) + + } + + areItemsSimilar(itemToCompare) { + return !Utilities.areItemsDifferent(this.item, itemToCompare.item) + && PileUtilities.canItemStack(itemToCompare.item, this.store.actor); + } } diff --git a/src/systems.js b/src/systems.js index 0a7d24f9..b75ccca8 100644 --- a/src/systems.js +++ b/src/systems.js @@ -166,7 +166,8 @@ export const SYSTEMS = { SECONDARY_CURRENCIES: [], PILE_DEFAULTS: {}, TOKEN_FLAG_DEFAULTS: {}, - CURRENCY_DECIMAL_DIGITS: 0.00001 + CURRENCY_DECIMAL_DIGITS: 0.00001, + SOFT_MIGRATIONS: {} }, get HAS_SYSTEM_SUPPORT() { diff --git a/src/systems/a5e.js b/src/systems/a5e.js index 5cffb93a..79a375b1 100644 --- a/src/systems/a5e.js +++ b/src/systems/a5e.js @@ -2,103 +2,103 @@ import CONSTANTS from "../constants/constants.js"; export default { - "VERSION": "1.0.1", + "VERSION": "1.0.1", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "feature, maneuver, spell, background, culture, heritage, destiny" - } - ], + // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "feature, maneuver, spell, background, culture, heritage, destiny" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "Platinum Pieces", - img: "icons/commodities/currency/coin-inset-snail-silver.webp", - abbreviation: "{#}PP", - data: { - path: "system.currency.pp" - }, - primary: false, - exchangeRate: 10 - }, - { - type: "attribute", - name: "Gold Pieces", - img: "icons/commodities/currency/coin-embossed-crown-gold.webp", - abbreviation: "{#}GP", - data: { - path: "system.currency.gp", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "Electrum Pieces", - img: "icons/commodities/currency/coin-inset-copper-axe.webp", - abbreviation: "{#}EP", - data: { - path: "system.currency.ep", - }, - primary: false, - exchangeRate: 0.5 - }, - { - type: "attribute", - name: "Silver Pieces", - img: "icons/commodities/currency/coin-engraved-moon-silver.webp", - abbreviation: "{#}SP", - data: { - path: "system.currency.sp", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "Copper Pieces", - img: "icons/commodities/currency/coin-engraved-waves-copper.webp", - abbreviation: "{#}CP", - data: { - path: "system.currency.cp", - }, - primary: false, - exchangeRate: 0.01 - } - ], + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "Platinum Pieces", + img: "icons/commodities/currency/coin-inset-snail-silver.webp", + abbreviation: "{#}PP", + data: { + path: "system.currency.pp" + }, + primary: false, + exchangeRate: 10 + }, + { + type: "attribute", + name: "Gold Pieces", + img: "icons/commodities/currency/coin-embossed-crown-gold.webp", + abbreviation: "{#}GP", + data: { + path: "system.currency.gp", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "Electrum Pieces", + img: "icons/commodities/currency/coin-inset-copper-axe.webp", + abbreviation: "{#}EP", + data: { + path: "system.currency.ep", + }, + primary: false, + exchangeRate: 0.5 + }, + { + type: "attribute", + name: "Silver Pieces", + img: "icons/commodities/currency/coin-engraved-moon-silver.webp", + abbreviation: "{#}SP", + data: { + path: "system.currency.sp", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "Copper Pieces", + img: "icons/commodities/currency/coin-engraved-waves-copper.webp", + abbreviation: "{#}CP", + data: { + path: "system.currency.cp", + }, + primary: false, + exchangeRate: 0.01 + } + ], - "SHEET_OVERRIDES": () => { + "SHEET_OVERRIDES": () => { - libWrapper.register(CONSTANTS.MODULE_NAME, `game.a5e.applications.ActorSheetA5e.prototype.render`, function (wrapped, forced, options, ...args) { - const renderItemPileInterface = Hooks.call(CONSTANTS.HOOKS.PRE_RENDER_SHEET, this.actor, forced, options) === false; - if (this._state > Application.RENDER_STATES.NONE) { - if (renderItemPileInterface) { - wrapped(forced, options, ...args) - } else { - return wrapped(forced, options, ...args) - } - } - if (renderItemPileInterface) return; - return wrapped(forced, options, ...args); - }, "MIXED"); + libWrapper.register(CONSTANTS.MODULE_NAME, `game.a5e.applications.ActorSheetA5e.prototype.render`, function (wrapped, forced, options, ...args) { + const renderItemPileInterface = Hooks.call(CONSTANTS.HOOKS.PRE_RENDER_SHEET, this.actor, forced, options) === false; + if (this._state > Application.RENDER_STATES.NONE) { + if (renderItemPileInterface) { + wrapped(forced, options, ...args) + } else { + return wrapped(forced, options, ...args) + } + } + if (renderItemPileInterface) return; + return wrapped(forced, options, ...args); + }, "MIXED"); - } + } }; diff --git a/src/systems/alienrpg.js b/src/systems/alienrpg.js index 28bde57c..b169196d 100644 --- a/src/systems/alienrpg.js +++ b/src/systems/alienrpg.js @@ -1,32 +1,32 @@ export default { - "VERSION": "1.0.0", - "CURRENCIES": [ - { - "type": "attribute", - "name": "Dollar", - "img": "", - "abbreviation": "${#}", - "data": { - "path": "system.attributes.cash.value" - }, - "primary": true, - "exchangeRate": 1 - } - ], - "CURRENCY_DECIMAL_DIGITS": 0.01, - "ITEM_FILTERS": [{ - "path": "type", - "filters": "talent,planet-system,skill-stunts,agenda,specialty,critical-injury" - }], - "ITEM_COST_TRANSFORMER": (item) => { - let overallCost = getProperty(item, "system.attributes.cost.value"); - if (overallCost) { - overallCost = overallCost.replace("$", "").replace(",", ""); - } - return Number(overallCost) ?? 0; - }, - "ITEM_SIMILARITIES": ["name", "type"], - "ACTOR_CLASS_TYPE": "character", - "ITEM_QUANTITY_ATTRIBUTE": "system.attributes.quantity.value", - "ITEM_PRICE_ATTRIBUTE": "system.attributes.cost.value" + "VERSION": "1.0.1", + "CURRENCIES": [ + { + "type": "attribute", + "name": "Dollar", + "img": "", + "abbreviation": "${#}", + "data": { + "path": "system.general.cash.value" + }, + "primary": true, + "exchangeRate": 1 + } + ], + "CURRENCY_DECIMAL_DIGITS": 0.01, + "ITEM_FILTERS": [{ + "path": "type", + "filters": "talent,planet-system,skill-stunts,agenda,specialty,critical-injury" + }], + "ITEM_COST_TRANSFORMER": (item) => { + let overallCost = getProperty(item, "system.attributes.cost.value"); + if (overallCost) { + overallCost = overallCost.replace("$", "").replace(",", ""); + } + return Number(overallCost) ?? 0; + }, + "ITEM_SIMILARITIES": ["name", "type"], + "ACTOR_CLASS_TYPE": "character", + "ITEM_QUANTITY_ATTRIBUTE": "system.attributes.quantity.value", + "ITEM_PRICE_ATTRIBUTE": "system.attributes.cost.value" } diff --git a/src/systems/blade-runner.js b/src/systems/blade-runner.js index b0c6b1da..9e1ca7cc 100644 --- a/src/systems/blade-runner.js +++ b/src/systems/blade-runner.js @@ -1,110 +1,110 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "loot", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "loot", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.qty", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.qty", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.cost", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.cost", - // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "upgrade,specialty,injury" - } - ], + // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "upgrade,specialty,injury" + } + ], - "UNSTACKABLE_ITEM_TYPES": ["weapon", "armor"], + "UNSTACKABLE_ITEM_TYPES": ["weapon", "armor"], - "PILE_DEFAULTS": { - merchantColumns: [{ - label: "FLBR.ItemAvailability", - path: "system.availability", - formatting: "{#}", - mapping: { - 5: 'FLBR.ITEM_AVAILABILITY.Incidental', - 4: 'FLBR.ITEM_AVAILABILITY.Standard', - 3: 'FLBR.ITEM_AVAILABILITY.Premium', - 2: 'FLBR.ITEM_AVAILABILITY.Rare', - 1: 'FLBR.ITEM_AVAILABILITY.Luxury', - } - }] - }, + "PILE_DEFAULTS": { + merchantColumns: [{ + label: "FLBR.ItemAvailability", + path: "system.availability", + formatting: "{#}", + mapping: { + 5: 'FLBR.ITEM_AVAILABILITY.Incidental', + 4: 'FLBR.ITEM_AVAILABILITY.Standard', + 3: 'FLBR.ITEM_AVAILABILITY.Premium', + 2: 'FLBR.ITEM_AVAILABILITY.Rare', + 1: 'FLBR.ITEM_AVAILABILITY.Luxury', + } + }] + }, - // This function is an optional system handler that specifically transforms an item when it is added to actors - "ITEM_TRANSFORMER": async (itemData) => { - if (itemData?.system?.mounted) itemData.system.mounted = false; - return itemData; - }, + // This function is an optional system handler that specifically transforms an item when it is added to actors + "ITEM_TRANSFORMER": async (itemData) => { + if (itemData?.system?.mounted) itemData.system.mounted = false; + return itemData; + }, - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "FLBR.HEADER.ChinyenPoints", - img: "icons/commodities/currency/coins-plain-stack-silver.webp", - abbreviation: "{#}C¥", - data: { - path: "system.metaCurrencies.chinyen", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "FLBR.HEADER.PromotionPoints", - img: "icons/commodities/treasure/medal-ribbon-gold-blue.webp", - abbreviation: "{#}PP", - data: { - path: "system.metaCurrencies.promotion", - }, - primary: false, - exchangeRate: 1 - }, - { - type: "attribute", - name: "FLBR.HEADER.HumanityPoints", - img: "icons/sundries/gaming/chess-knight-white.webp", - abbreviation: "{#}HP", - data: { - path: "system.metaCurrencies.humanity", - }, - primary: false, - exchangeRate: 1 - } - ], + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "FLBR.HEADER.ChinyenPoints", + img: "icons/commodities/currency/coins-plain-stack-silver.webp", + abbreviation: "{#}C¥", + data: { + path: "system.metaCurrencies.chinyen", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "FLBR.HEADER.PromotionPoints", + img: "icons/commodities/treasure/medal-ribbon-gold-blue.webp", + abbreviation: "{#}PP", + data: { + path: "system.metaCurrencies.promotion", + }, + primary: false, + exchangeRate: 1 + }, + { + type: "attribute", + name: "FLBR.HEADER.HumanityPoints", + img: "icons/sundries/gaming/chess-knight-white.webp", + abbreviation: "{#}HP", + data: { + path: "system.metaCurrencies.humanity", + }, + primary: false, + exchangeRate: 1 + } + ], - "VAULT_STYLES": [ - { - path: "system.availability", - value: 1, - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(255,119,0,1)" - } - }, - { - path: "system.availability", - value: 2, - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(255,0,247,1)" - } - }, - { - path: "system.availability", - value: 3, - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(0,136,255,1)" - } - } - ] + "VAULT_STYLES": [ + { + path: "system.availability", + value: 1, + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(255,119,0,1)" + } + }, + { + path: "system.availability", + value: 2, + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(255,0,247,1)" + } + }, + { + path: "system.availability", + value: 3, + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(0,136,255,1)" + } + } + ] } diff --git a/src/systems/cyberpunk-red-core.js b/src/systems/cyberpunk-red-core.js index 83a302d5..cf08ab04 100644 --- a/src/systems/cyberpunk-red-core.js +++ b/src/systems/cyberpunk-red-core.js @@ -1,47 +1,47 @@ export default { - "VERSION": "1.0.3", - - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", - - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.amount", - - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price.market", - - // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "role, skill, criticalInjury" - }, - { - "path": "system.isInstalled", - "filters": [true] - } - ], - - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], - - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "CPR.characterSheet.rightPane.eurobucks.eurobucks", - img: "icons/svg/coins.svg", - abbreviation: "{#}€$", - data: { - path: "system.wealth.value" - }, - primary: true, - exchangeRate: 1 - } - ], - - "CURRENCY_DECIMAL_DIGITS": 0.01 + "VERSION": "1.0.3", + + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", + + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.amount", + + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price.market", + + // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "role, skill, criticalInjury" + }, + { + "path": "system.isInstalled", + "filters": [true] + } + ], + + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], + + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "CPR.characterSheet.rightPane.eurobucks.eurobucks", + img: "icons/svg/coins.svg", + abbreviation: "{#}€$", + data: { + path: "system.wealth.value" + }, + primary: true, + exchangeRate: 1 + } + ], + + "CURRENCY_DECIMAL_DIGITS": 0.01 } diff --git a/src/systems/cyphersystem.js b/src/systems/cyphersystem.js index 88993d5e..a183d00b 100644 --- a/src/systems/cyphersystem.js +++ b/src/systems/cyphersystem.js @@ -1,96 +1,96 @@ export default { - "VERSION": "1.0.1", + "VERSION": "1.0.1", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "pc", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "pc", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.basic.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.basic.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "flags.item-piles.system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "flags.item-piles.system.price", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "ability, lasting-damage, power-shift, recursion, skill, tag" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "ability, lasting-damage, power-shift, recursion, skill, tag" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "Adamantine pieces", - img: "icons/commodities/currency/coin-embossed-ruby-gold.webp", - abbreviation: "{#} ap", - data: { - path: "system.settings.equipment.currency.quantity6" - }, - primary: false, - exchangeRate: 1000 - }, - { - type: "attribute", - name: "Mithral pieces", - img: "icons/commodities/currency/coin-embossed-unicorn-silver.webp", - abbreviation: "{#} mp", - data: { - path: "system.settings.equipment.currency.quantity5" - }, - primary: false, - exchangeRate: 100 - }, - { - type: "attribute", - name: "Platinum pieces", - img: "icons/commodities/currency/coin-engraved-moon-silver.webp", - abbreviation: "{#} pp", - data: { - path: "system.settings.equipment.currency.quantity4" - }, - primary: false, - exchangeRate: 10 - }, - { - type: "attribute", - name: "Gold pieces", - img: "icons/commodities/currency/coins-plain-gold.webp", - abbreviation: "{#} gp", - data: { - path: "system.settings.equipment.currency.quantity3" - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "Silver pieces", - img: "icons/commodities/currency/coins-engraved-face-silver.webp", - abbreviation: "{#} sp", - data: { - path: "system.settings.equipment.currency.quantity2" - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "Copper pieces", - img: "icons/commodities/currency/coins-engraved-copper.webp", - abbreviation: "{#} cp", - data: { - path: "system.settings.equipment.currency.quantity1" - }, - primary: false, - exchangeRate: 0.01 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "Adamantine pieces", + img: "icons/commodities/currency/coin-embossed-ruby-gold.webp", + abbreviation: "{#} ap", + data: { + path: "system.settings.equipment.currency.quantity6" + }, + primary: false, + exchangeRate: 1000 + }, + { + type: "attribute", + name: "Mithral pieces", + img: "icons/commodities/currency/coin-embossed-unicorn-silver.webp", + abbreviation: "{#} mp", + data: { + path: "system.settings.equipment.currency.quantity5" + }, + primary: false, + exchangeRate: 100 + }, + { + type: "attribute", + name: "Platinum pieces", + img: "icons/commodities/currency/coin-engraved-moon-silver.webp", + abbreviation: "{#} pp", + data: { + path: "system.settings.equipment.currency.quantity4" + }, + primary: false, + exchangeRate: 10 + }, + { + type: "attribute", + name: "Gold pieces", + img: "icons/commodities/currency/coins-plain-gold.webp", + abbreviation: "{#} gp", + data: { + path: "system.settings.equipment.currency.quantity3" + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "Silver pieces", + img: "icons/commodities/currency/coins-engraved-face-silver.webp", + abbreviation: "{#} sp", + data: { + path: "system.settings.equipment.currency.quantity2" + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "Copper pieces", + img: "icons/commodities/currency/coins-engraved-copper.webp", + abbreviation: "{#} cp", + data: { + path: "system.settings.equipment.currency.quantity1" + }, + primary: false, + exchangeRate: 0.01 + } + ] } diff --git a/src/systems/d35e.js b/src/systems/d35e.js index a040ce6c..91a5590b 100644 --- a/src/systems/d35e.js +++ b/src/systems/d35e.js @@ -1,74 +1,74 @@ export default { - "VERSION": "1.0.2", + "VERSION": "1.0.2", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "npc", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "npc", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "spell,feat,class,race,attack,full-attack,buff,aura,alignment,enhancement,damage-type,material" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "spell,feat,class,race,attack,full-attack,buff,aura,alignment,enhancement,damage-type,material" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "DND5E.CurrencyPP", - img: "icons/commodities/currency/coin-inset-snail-silver.webp", - abbreviation: "{#}PP", - data: { - path: "system.currency.pp" - }, - primary: false, - exchangeRate: 10 - }, - { - type: "attribute", - name: "DND5E.CurrencyGP", - img: "icons/commodities/currency/coin-embossed-crown-gold.webp", - abbreviation: "{#}GP", - data: { - path: "system.currency.gp", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "DND5E.CurrencySP", - img: "icons/commodities/currency/coin-engraved-moon-silver.webp", - abbreviation: "{#}SP", - data: { - path: "system.currency.sp", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "DND5E.CurrencyCP", - img: "icons/commodities/currency/coin-engraved-waves-copper.webp", - abbreviation: "{#}CP", - data: { - path: "system.currency.cp", - }, - primary: false, - exchangeRate: 0.01 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "DND5E.CurrencyPP", + img: "icons/commodities/currency/coin-inset-snail-silver.webp", + abbreviation: "{#}PP", + data: { + path: "system.currency.pp" + }, + primary: false, + exchangeRate: 10 + }, + { + type: "attribute", + name: "DND5E.CurrencyGP", + img: "icons/commodities/currency/coin-embossed-crown-gold.webp", + abbreviation: "{#}GP", + data: { + path: "system.currency.gp", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "DND5E.CurrencySP", + img: "icons/commodities/currency/coin-engraved-moon-silver.webp", + abbreviation: "{#}SP", + data: { + path: "system.currency.sp", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "DND5E.CurrencyCP", + img: "icons/commodities/currency/coin-engraved-waves-copper.webp", + abbreviation: "{#}CP", + data: { + path: "system.currency.cp", + }, + primary: false, + exchangeRate: 0.01 + } + ] } diff --git a/src/systems/dark-heresy.js b/src/systems/dark-heresy.js index ad85bbbe..470d139c 100644 --- a/src/systems/dark-heresy.js +++ b/src/systems/dark-heresy.js @@ -1,36 +1,35 @@ export default { - "VERSION": "1.0.0", - - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "acolyte", - - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "", - - // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "specialAbility, aptitude, talent, psychicPower, trait" - }, - { - "path": "system.installed", - "filters": "installed" - } - ], - - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": [], - - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [], - - "CURRENCY_DECIMAL_DIGITS": 0.00001 + "VERSION": "1.0.0", + + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "acolyte", + + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "", + + // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "specialAbility, aptitude, talent, psychicPower, trait" + }, + { + "path": "system.installed", + "filters": "installed" + } + ], + + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": [], + + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [], + + "CURRENCY_DECIMAL_DIGITS": 0.00001 } - \ No newline at end of file diff --git a/src/systems/dcc.js b/src/systems/dcc.js index 71a61d42..67746fe2 100644 --- a/src/systems/dcc.js +++ b/src/systems/dcc.js @@ -1,112 +1,112 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "Player", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "Player", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.value", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.value", - // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "spell,skill" - }, - ], + // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "spell,skill" + }, + ], - // This function is an optional system handler that specifically transforms an item when it is added to actors - "ITEM_TRANSFORMER": async (itemData) => { - // Transferred items should become unequipped - if (itemData?.system?.equipped) { - itemData.system.equipped = false; - } - // Transferring spells doesn't make a lot of sense, but if it happens the lost flag and mercurial effect should be cleared - ["lost", "mercurialEffect"].forEach(key => { - if (itemData?.system?.[key] !== undefined) { - delete itemData.system[key]; - } - }); - return itemData; - }, + // This function is an optional system handler that specifically transforms an item when it is added to actors + "ITEM_TRANSFORMER": async (itemData) => { + // Transferred items should become unequipped + if (itemData?.system?.equipped) { + itemData.system.equipped = false; + } + // Transferring spells doesn't make a lot of sense, but if it happens the lost flag and mercurial effect should be cleared + ["lost", "mercurialEffect"].forEach(key => { + if (itemData?.system?.[key] !== undefined) { + delete itemData.system[key]; + } + }); + return itemData; + }, - // This function is an optional system handler that specifically transforms an item's price into a more unified numeric format - "ITEM_COST_TRANSFORMER": (item, currencies) => { - let overallCost = 0; - currencies.forEach((currency, index) => { - let denominationCost = Number(getProperty(item, currency.data.path.replace("system.currency.", "system.value."))); - if (!isNaN(denominationCost)) { - overallCost += denominationCost * currency.exchangeRate; - } - }) - return overallCost ?? 0; - }, + // This function is an optional system handler that specifically transforms an item's price into a more unified numeric format + "ITEM_COST_TRANSFORMER": (item, currencies) => { + let overallCost = 0; + currencies.forEach((currency, index) => { + let denominationCost = Number(getProperty(item, currency.data.path.replace("system.currency.", "system.value."))); + if (!isNaN(denominationCost)) { + overallCost += denominationCost * currency.exchangeRate; + } + }) + return overallCost ?? 0; + }, - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "DCC.CurrencyPP", - img: "icons/commodities/currency/coin-inset-snail-silver.webp", - abbreviation: "{#}Pp", - data: { - path: "system.currency.pp", - }, - primary: false, - exchangeRate: 100 - }, - { - type: "attribute", - name: "DCC.CurrencyEP", - img: "icons/commodities/currency/coin-inset-copper-axe.webp", - abbreviation: "{#} Ep", - data: { - path: "system.currency.ep", - }, - primary: false, - exchangeRate: 10 - }, - { - type: "attribute", - name: "DCC.CurrencyGP", - img: "icons/commodities/currency/coin-embossed-crown-gold.webp", - abbreviation: "{#} Gp", - data: { - path: "system.currency.gp", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "DCC.CurrencySP", - img: "icons/commodities/currency/coin-engraved-moon-silver.webp", - abbreviation: "{#} Sp", - data: { - path: "system.currency.sp", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "DCC.CurrencyCP", - img: "icons/commodities/currency/coin-engraved-waves-copper.webp", - abbreviation: "{#} Cp", - data: { - path: "system.currency.cp", - }, - primary: false, - exchangeRate: 0.01 - } - ], + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "DCC.CurrencyPP", + img: "icons/commodities/currency/coin-inset-snail-silver.webp", + abbreviation: "{#}Pp", + data: { + path: "system.currency.pp", + }, + primary: false, + exchangeRate: 100 + }, + { + type: "attribute", + name: "DCC.CurrencyEP", + img: "icons/commodities/currency/coin-inset-copper-axe.webp", + abbreviation: "{#} Ep", + data: { + path: "system.currency.ep", + }, + primary: false, + exchangeRate: 10 + }, + { + type: "attribute", + name: "DCC.CurrencyGP", + img: "icons/commodities/currency/coin-embossed-crown-gold.webp", + abbreviation: "{#} Gp", + data: { + path: "system.currency.gp", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "DCC.CurrencySP", + img: "icons/commodities/currency/coin-engraved-moon-silver.webp", + abbreviation: "{#} Sp", + data: { + path: "system.currency.sp", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "DCC.CurrencyCP", + img: "icons/commodities/currency/coin-engraved-waves-copper.webp", + abbreviation: "{#} Cp", + data: { + path: "system.currency.cp", + }, + primary: false, + exchangeRate: 0.01 + } + ], } diff --git a/src/systems/dnd4e.js b/src/systems/dnd4e.js index fd2b2dff..8b36a174 100644 --- a/src/systems/dnd4e.js +++ b/src/systems/dnd4e.js @@ -1,151 +1,151 @@ export default { - "VERSION": "1.0.4", + "VERSION": "1.0.4", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "Player Character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "Player Character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "classFeats,feat,raceFeats,pathFeats,destinyFeats,ritual,power" - } - ], + // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "classFeats,feat,raceFeats,pathFeats,destinyFeats,ritual,power" + } + ], - // This function is an optional system handler that specifically transforms an item when it is added to actors - "ITEM_TRANSFORMER": async (itemData) => { - ["equipped", "proficient", "prepared"].forEach(key => { - if (itemData?.system?.[key] !== undefined) { - delete itemData.system[key]; - } - }); - setProperty(itemData, "system.attunement", false); - return itemData; - }, + // This function is an optional system handler that specifically transforms an item when it is added to actors + "ITEM_TRANSFORMER": async (itemData) => { + ["equipped", "proficient", "prepared"].forEach(key => { + if (itemData?.system?.[key] !== undefined) { + delete itemData.system[key]; + } + }); + setProperty(itemData, "system.attunement", false); + return itemData; + }, - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "DND4EBETA.CurrencyAD", - img: "icons/commodities/gems/gem-faceted-round-white.webp", - abbreviation: "{#}AD", - data: { - path: "system.currency.ad" - }, - primary: false, - exchangeRate: 10000 - }, - { - type: "attribute", - name: "DND4EBETA.CurrencyPP", - img: "icons/commodities/currency/coin-inset-snail-silver.webp", - abbreviation: "{#}PP", - data: { - path: "system.currency.pp" - }, - primary: false, - exchangeRate: 100 - }, - { - type: "attribute", - name: "DND4EBETA.CurrencyGP", - img: "icons/commodities/currency/coin-embossed-crown-gold.webp", - abbreviation: "{#}GP", - data: { - path: "system.currency.gp", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "DND4EBETA.CurrencySP", - img: "icons/commodities/currency/coin-engraved-moon-silver.webp", - abbreviation: "{#}SP", - data: { - path: "system.currency.sp", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "DND4EBETA.CurrencyCP", - img: "icons/commodities/currency/coin-engraved-waves-copper.webp", - abbreviation: "{#}CP", - data: { - path: "system.currency.cp", - }, - primary: false, - exchangeRate: 0.01 - }, - { - type: "attribute", - name: "DND4EBETA.RitualCompAR", - img: "icons/commodities/materials/bowl-powder-teal.webp", - abbreviation: "{#}AR", - data: { - path: "system.ritualcomp.ar", - }, - primary: false, - exchangeRate: 1 - }, - { - type: "attribute", - name: "DND4EBETA.RitualCompMS", - img: "icons/commodities/materials/bowl-liquid-white.webp", - abbreviation: "{#}MS", - data: { - path: "system.ritualcomp.ms", - }, - primary: false, - exchangeRate: 1 - }, - { - type: "attribute", - name: "DND4EBETA.RitualCompRH", - img: "icons/commodities/materials/plant-sprout-seed-brown-green.webp", - abbreviation: "{#}RH", - data: { - path: "system.ritualcomp.rh", - }, - primary: false, - exchangeRate: 1 - }, - { - type: "attribute", - name: "DND4EBETA.RitualCompSI", - img: "icons/commodities/materials/bowl-liquid-red.webp", - abbreviation: "{#}SI", - data: { - path: "system.ritualcomp.si", - }, - primary: false, - exchangeRate: 1 - }, - { - type: "attribute", - name: "DND4EBETA.RitualCompRS", - img: "icons/commodities/gems/gem-faceted-cushion-teal.webp", - abbreviation: "{#}RS", - data: { - path: "system.ritualcomp.rs", - }, - primary: false, - exchangeRate: 1 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "DND4EBETA.CurrencyAD", + img: "icons/commodities/gems/gem-faceted-round-white.webp", + abbreviation: "{#}AD", + data: { + path: "system.currency.ad" + }, + primary: false, + exchangeRate: 10000 + }, + { + type: "attribute", + name: "DND4EBETA.CurrencyPP", + img: "icons/commodities/currency/coin-inset-snail-silver.webp", + abbreviation: "{#}PP", + data: { + path: "system.currency.pp" + }, + primary: false, + exchangeRate: 100 + }, + { + type: "attribute", + name: "DND4EBETA.CurrencyGP", + img: "icons/commodities/currency/coin-embossed-crown-gold.webp", + abbreviation: "{#}GP", + data: { + path: "system.currency.gp", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "DND4EBETA.CurrencySP", + img: "icons/commodities/currency/coin-engraved-moon-silver.webp", + abbreviation: "{#}SP", + data: { + path: "system.currency.sp", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "DND4EBETA.CurrencyCP", + img: "icons/commodities/currency/coin-engraved-waves-copper.webp", + abbreviation: "{#}CP", + data: { + path: "system.currency.cp", + }, + primary: false, + exchangeRate: 0.01 + }, + { + type: "attribute", + name: "DND4EBETA.RitualCompAR", + img: "icons/commodities/materials/bowl-powder-teal.webp", + abbreviation: "{#}AR", + data: { + path: "system.ritualcomp.ar", + }, + primary: false, + exchangeRate: 1 + }, + { + type: "attribute", + name: "DND4EBETA.RitualCompMS", + img: "icons/commodities/materials/bowl-liquid-white.webp", + abbreviation: "{#}MS", + data: { + path: "system.ritualcomp.ms", + }, + primary: false, + exchangeRate: 1 + }, + { + type: "attribute", + name: "DND4EBETA.RitualCompRH", + img: "icons/commodities/materials/plant-sprout-seed-brown-green.webp", + abbreviation: "{#}RH", + data: { + path: "system.ritualcomp.rh", + }, + primary: false, + exchangeRate: 1 + }, + { + type: "attribute", + name: "DND4EBETA.RitualCompSI", + img: "icons/commodities/materials/bowl-liquid-red.webp", + abbreviation: "{#}SI", + data: { + path: "system.ritualcomp.si", + }, + primary: false, + exchangeRate: 1 + }, + { + type: "attribute", + name: "DND4EBETA.RitualCompRS", + img: "icons/commodities/gems/gem-faceted-cushion-teal.webp", + abbreviation: "{#}RS", + data: { + path: "system.ritualcomp.rs", + }, + primary: false, + exchangeRate: 1 + } + ] } diff --git a/src/systems/dnd5e-2.0.3.js b/src/systems/dnd5e-2.0.3.js index 58b217cb..3b1bf2b6 100644 --- a/src/systems/dnd5e-2.0.3.js +++ b/src/systems/dnd5e-2.0.3.js @@ -1,145 +1,145 @@ export default { - "VERSION": "1.0.3", + "VERSION": "1.0.3", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "spell,feat,class,subclass,background" - }, - { - "path": "system.weaponType", - "filters": "natural" - } - ], + // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "spell,feat,class,subclass,background" + }, + { + "path": "system.weaponType", + "filters": "natural" + } + ], - // This function is an optional system handler that specifically transforms an item when it is added to actors - "ITEM_TRANSFORMER": async (itemData) => { - ["equipped", "proficient", "prepared"].forEach(key => { - if (itemData?.system?.[key] !== undefined) { - delete itemData.system[key]; - } - }); - setProperty(itemData, "system.attunement", Math.min(CONFIG.DND5E.attunementTypes.REQUIRED, itemData?.system?.attunement ?? 0)); - if (itemData.type === "spell") { - try { - const scroll = await Item.implementation.createScrollFromSpell(itemData); - itemData = scroll.toObject(); - } catch (err) { - } - } - return itemData; - }, + // This function is an optional system handler that specifically transforms an item when it is added to actors + "ITEM_TRANSFORMER": async (itemData) => { + ["equipped", "proficient", "prepared"].forEach(key => { + if (itemData?.system?.[key] !== undefined) { + delete itemData.system[key]; + } + }); + setProperty(itemData, "system.attunement", Math.min(CONFIG.DND5E.attunementTypes.REQUIRED, itemData?.system?.attunement ?? 0)); + if (itemData.type === "spell") { + try { + const scroll = await Item.implementation.createScrollFromSpell(itemData); + itemData = scroll.toObject(); + } catch (err) { + } + } + return itemData; + }, - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "DND5E.CurrencyPP", - img: "icons/commodities/currency/coin-inset-snail-silver.webp", - abbreviation: "{#}PP", - data: { - path: "system.currency.pp" - }, - primary: false, - exchangeRate: 10 - }, - { - type: "attribute", - name: "DND5E.CurrencyGP", - img: "icons/commodities/currency/coin-embossed-crown-gold.webp", - abbreviation: "{#}GP", - data: { - path: "system.currency.gp", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "DND5E.CurrencyEP", - img: "icons/commodities/currency/coin-inset-copper-axe.webp", - abbreviation: "{#}EP", - data: { - path: "system.currency.ep", - }, - primary: false, - exchangeRate: 0.5 - }, - { - type: "attribute", - name: "DND5E.CurrencySP", - img: "icons/commodities/currency/coin-engraved-moon-silver.webp", - abbreviation: "{#}SP", - data: { - path: "system.currency.sp", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "DND5E.CurrencyCP", - img: "icons/commodities/currency/coin-engraved-waves-copper.webp", - abbreviation: "{#}CP", - data: { - path: "system.currency.cp", - }, - primary: false, - exchangeRate: 0.01 - } - ], + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "DND5E.CurrencyPP", + img: "icons/commodities/currency/coin-inset-snail-silver.webp", + abbreviation: "{#}PP", + data: { + path: "system.currency.pp" + }, + primary: false, + exchangeRate: 10 + }, + { + type: "attribute", + name: "DND5E.CurrencyGP", + img: "icons/commodities/currency/coin-embossed-crown-gold.webp", + abbreviation: "{#}GP", + data: { + path: "system.currency.gp", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "DND5E.CurrencyEP", + img: "icons/commodities/currency/coin-inset-copper-axe.webp", + abbreviation: "{#}EP", + data: { + path: "system.currency.ep", + }, + primary: false, + exchangeRate: 0.5 + }, + { + type: "attribute", + name: "DND5E.CurrencySP", + img: "icons/commodities/currency/coin-engraved-moon-silver.webp", + abbreviation: "{#}SP", + data: { + path: "system.currency.sp", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "DND5E.CurrencyCP", + img: "icons/commodities/currency/coin-engraved-waves-copper.webp", + abbreviation: "{#}CP", + data: { + path: "system.currency.cp", + }, + primary: false, + exchangeRate: 0.01 + } + ], - VAULT_STYLES: [ - { - path: "system.rarity", - value: "artifact", - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(255,191,0,1)" - } - }, - { - path: "system.rarity", - value: "legendary", - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(255,119,0,1)" - } - }, - { - path: "system.rarity", - value: "veryRare", - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(255,0,247,1)" - } - }, - { - path: "system.rarity", - value: "rare", - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(0,136,255,1)" - } - }, - { - path: "system.rarity", - value: "uncommon", - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(0,255,9,1)" - } - } - ] + VAULT_STYLES: [ + { + path: "system.rarity", + value: "artifact", + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(255,191,0,1)" + } + }, + { + path: "system.rarity", + value: "legendary", + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(255,119,0,1)" + } + }, + { + path: "system.rarity", + value: "veryRare", + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(255,0,247,1)" + } + }, + { + path: "system.rarity", + value: "rare", + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(0,136,255,1)" + } + }, + { + path: "system.rarity", + value: "uncommon", + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(0,255,9,1)" + } + } + ] } diff --git a/src/systems/dnd5e.js b/src/systems/dnd5e.js index 84b27663..988193d4 100644 --- a/src/systems/dnd5e.js +++ b/src/systems/dnd5e.js @@ -1,256 +1,261 @@ -import { TJSDialog } from "@typhonjs-fvtt/runtime/svelte/application"; -import GiveItemsShell from "../applications/dialogs/give-items-dialog/give-items-shell.svelte"; -import * as PileUtilities from "../helpers/pile-utilities.js"; -import { hotkeyActionState } from "../hotkeys.js"; -import * as Utilities from "../helpers/utilities.js"; -import DropItemDialog from "../applications/dialogs/drop-item-dialog/drop-item-dialog.js"; import GiveItems from "../applications/dialogs/give-items-dialog/give-items-dialog.js"; import PrivateAPI from "../API/private-api.js"; export default { - "VERSION": "1.0.6", + "VERSION": "1.0.7", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price.value", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price.value", - // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "spell,feat,class,subclass,background" - }, - { - "path": "system.weaponType", - "filters": "natural" - } - ], + // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "spell,feat,class,subclass,background" + }, + { + "path": "system.weaponType", + "filters": "natural" + } + ], - // This function is an optional system handler that specifically transforms an item when it is added to actors - "ITEM_TRANSFORMER": async (itemData) => { - ["equipped", "proficient", "prepared"].forEach(key => { - if (itemData?.system?.[key] !== undefined) { - delete itemData.system[key]; - } - }); - setProperty(itemData, "system.attunement", Math.min(CONFIG.DND5E.attunementTypes.REQUIRED, itemData?.system?.attunement ?? 0)); - if (itemData.type === "spell") { - try { - const scroll = await Item.implementation.createScrollFromSpell(itemData); - itemData = scroll.toObject(); - } catch (err) { - } - } - return itemData; - }, + // This function is an optional system handler that specifically transforms an item when it is added to actors + "ITEM_TRANSFORMER": async (itemData) => { + ["equipped", "proficient", "prepared"].forEach(key => { + if (itemData?.system?.[key] !== undefined) { + delete itemData.system[key]; + } + }); + setProperty(itemData, "system.attunement", Math.min(CONFIG.DND5E.attunementTypes.REQUIRED, itemData?.system?.attunement ?? 0)); + if (itemData.type === "spell") { + try { + const scroll = await Item.implementation.createScrollFromSpell(itemData); + itemData = scroll.toObject(); + } catch (err) { + } + } + return itemData; + }, - // This function is an optional system handler that specifically transforms an item's price into a more unified numeric format - "ITEM_COST_TRANSFORMER": (item, currencies) => { - const overallCost = Number(getProperty(item, "system.price.value")) ?? 0; - const priceDenomination = getProperty(item, "system.price.denomination"); - if (priceDenomination) { - const currencyDenomination = currencies - .filter(currency => currency.type === "attribute") - .find(currency => { - return currency.data.path.toLowerCase().endsWith(priceDenomination); - }); - if (currencyDenomination) { - return overallCost * currencyDenomination.exchangeRate; - } - } - return overallCost ?? 0; - }, + // This function is an optional system handler that specifically transforms an item's price into a more unified numeric format + "ITEM_COST_TRANSFORMER": (item, currencies) => { + const overallCost = Number(getProperty(item, "system.price.value")) ?? 0; + const priceDenomination = getProperty(item, "system.price.denomination"); + if (priceDenomination) { + const currencyDenomination = currencies + .filter(currency => currency.type === "attribute") + .find(currency => { + return currency.data.path.toLowerCase().endsWith(priceDenomination); + }); + if (currencyDenomination) { + return overallCost * currencyDenomination.exchangeRate; + } + } + return overallCost ?? 0; + }, - "PRICE_MODIFIER_TRANSFORMER": ({ - buyPriceModifier, - sellPriceModifier, - actor = false, - actorPriceModifiers = [] - } = {}) => { + "PRICE_MODIFIER_TRANSFORMER": ({ + buyPriceModifier, + sellPriceModifier, + actor = false, + actorPriceModifiers = [] + } = {}) => { - const modifiers = { - buyPriceModifier, - sellPriceModifier - }; + const modifiers = { + buyPriceModifier, + sellPriceModifier + }; - if (!actor) return modifiers; + if (!actor) return modifiers; - const groupModifiers = actorPriceModifiers - .map(data => ({ ...data, actor: fromUuidSync(data.actorUuid) })) - .filter(data => { - return data.actor && data.actor.type === "group" && data.actor.system.members.some(member => member === actor) - }); + const groupModifiers = actorPriceModifiers + .map(data => ({ ...data, actor: fromUuidSync(data.actorUuid) })) + .filter(data => { + return data.actor && data.actor.type === "group" && data.actor.system.members.some(member => member === actor) + }); - modifiers.buyPriceModifier = groupModifiers.reduce((acc, data) => { - return data.override ? data.buyPriceModifier ?? acc : acc * data.buyPriceModifier; - }, buyPriceModifier); + modifiers.buyPriceModifier = groupModifiers.reduce((acc, data) => { + return data.override ? data.buyPriceModifier ?? acc : acc * data.buyPriceModifier; + }, buyPriceModifier); - modifiers.sellPriceModifier = groupModifiers.reduce((acc, data) => { - return data.override ? data.sellPriceModifier ?? acc : acc * data.sellPriceModifier; - }, sellPriceModifier); + modifiers.sellPriceModifier = groupModifiers.reduce((acc, data) => { + return data.override ? data.sellPriceModifier ?? acc : acc * data.sellPriceModifier; + }, sellPriceModifier); - return modifiers; + return modifiers; - }, + }, - "PILE_DEFAULTS": { - merchantColumns: [{ - label: "", - path: "system.equipped", - formatting: "{#}", - buying: false, - selling: true, - mapping: { - "true": "✔", - "false": "" - } - }, { - label: "Rarity", - path: "system.rarity", - formatting: "{#}", - buying: true, - selling: true, - mapping: { - "common": "DND5E.ItemRarityCommon", - "uncommon": "DND5E.ItemRarityUncommon", - "rare": "DND5E.ItemRarityRare", - "veryRare": "DND5E.ItemRarityVeryRare", - "legendary": "DND5E.ItemRarityLegendary", - "artifact": "DND5E.ItemRarityArtifact" - } - }] - }, + "SOFT_MIGRATIONS": { + "1.0.6-1.0.7": { + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "spell,feat,class,subclass,background,race" + } + ] + } + }, - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + "PILE_DEFAULTS": { + merchantColumns: [{ + label: "", + path: "system.equipped", + formatting: "{#}", + buying: false, + selling: true, + mapping: { + "true": "✔", + "false": "" + } + }, { + label: "Rarity", + path: "system.rarity", + formatting: "{#}", + buying: true, + selling: true, + mapping: { + "common": "DND5E.ItemRarityCommon", + "uncommon": "DND5E.ItemRarityUncommon", + "rare": "DND5E.ItemRarityRare", + "veryRare": "DND5E.ItemRarityVeryRare", + "legendary": "DND5E.ItemRarityLegendary", + "artifact": "DND5E.ItemRarityArtifact" + } + }] + }, - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "DND5E.CurrencyPP", - img: "icons/commodities/currency/coin-inset-snail-silver.webp", - abbreviation: "{#}PP", - data: { - path: "system.currency.pp" - }, - primary: false, - exchangeRate: 10 - }, - { - type: "attribute", - name: "DND5E.CurrencyGP", - img: "icons/commodities/currency/coin-embossed-crown-gold.webp", - abbreviation: "{#}GP", - data: { - path: "system.currency.gp", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "DND5E.CurrencyEP", - img: "icons/commodities/currency/coin-inset-copper-axe.webp", - abbreviation: "{#}EP", - data: { - path: "system.currency.ep", - }, - primary: false, - exchangeRate: 0.5 - }, - { - type: "attribute", - name: "DND5E.CurrencySP", - img: "icons/commodities/currency/coin-engraved-moon-silver.webp", - abbreviation: "{#}SP", - data: { - path: "system.currency.sp", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "DND5E.CurrencyCP", - img: "icons/commodities/currency/coin-engraved-waves-copper.webp", - abbreviation: "{#}CP", - data: { - path: "system.currency.cp", - }, - primary: false, - exchangeRate: 0.01 - } - ], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - "VAULT_STYLES": [ - { - path: "system.rarity", - value: "artifact", - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(255,191,0,1)" - } - }, - { - path: "system.rarity", - value: "legendary", - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(255,119,0,1)" - } - }, - { - path: "system.rarity", - value: "veryRare", - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(255,0,247,1)" - } - }, - { - path: "system.rarity", - value: "rare", - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(0,136,255,1)" - } - }, - { - path: "system.rarity", - value: "uncommon", - styling: { - "box-shadow": "inset 0px 0px 7px 0px rgba(0,255,9,1)" - } - } - ], + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "DND5E.CurrencyPP", + img: "icons/commodities/currency/coin-inset-snail-silver.webp", + abbreviation: "{#}PP", + data: { + path: "system.currency.pp" + }, + primary: false, + exchangeRate: 10 + }, + { + type: "attribute", + name: "DND5E.CurrencyGP", + img: "icons/commodities/currency/coin-embossed-crown-gold.webp", + abbreviation: "{#}GP", + data: { + path: "system.currency.gp", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "DND5E.CurrencyEP", + img: "icons/commodities/currency/coin-inset-copper-axe.webp", + abbreviation: "{#}EP", + data: { + path: "system.currency.ep", + }, + primary: false, + exchangeRate: 0.5 + }, + { + type: "attribute", + name: "DND5E.CurrencySP", + img: "icons/commodities/currency/coin-engraved-moon-silver.webp", + abbreviation: "{#}SP", + data: { + path: "system.currency.sp", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "DND5E.CurrencyCP", + img: "icons/commodities/currency/coin-engraved-waves-copper.webp", + abbreviation: "{#}CP", + data: { + path: "system.currency.cp", + }, + primary: false, + exchangeRate: 0.01 + } + ], - "SYSTEM_HOOKS": () => { + "VAULT_STYLES": [ + { + path: "system.rarity", + value: "artifact", + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(255,191,0,1)" + } + }, + { + path: "system.rarity", + value: "legendary", + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(255,119,0,1)" + } + }, + { + path: "system.rarity", + value: "veryRare", + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(255,0,247,1)" + } + }, + { + path: "system.rarity", + value: "rare", + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(0,136,255,1)" + } + }, + { + path: "system.rarity", + value: "uncommon", + styling: { + "box-shadow": "inset 0px 0px 7px 0px rgba(0,255,9,1)" + } + } + ], - Hooks.on("dnd5e.getItemContextOptions", (item, options) => { - console.log(game.itempiles.API.isItemInvalid(item)) - options.push({ - name: "Give to character", - icon: "", - callback: async () => { - const result = await GiveItems.show(item); - if (!result) return; - PrivateAPI._giveItem({ - itemData: { - item: item.toObject(), - quantity: result.quantity - }, - source: item.parent.uuid, - target: result.target, - }, { skipQuantityDialog: true }) - }, - condition: !game.itempiles.API.isItemInvalid(item) - }) - }); + "SYSTEM_HOOKS": () => { - } + Hooks.on("dnd5e.getItemContextOptions", (item, options) => { + console.log(game.itempiles.API.isItemInvalid(item)) + options.push({ + name: "Give to character", + icon: "", + callback: async () => { + const result = await GiveItems.show(item); + if (!result) return; + PrivateAPI._giveItem({ + itemData: { + item: item.toObject(), + quantity: result.quantity + }, + source: item.parent.uuid, + target: result.target, + }, { skipQuantityDialog: true }) + }, + condition: !game.itempiles.API.isItemInvalid(item) + }) + }); + + } } diff --git a/src/systems/ds4.js b/src/systems/ds4.js index 09c72bc1..13e0ced7 100644 --- a/src/systems/ds4.js +++ b/src/systems/ds4.js @@ -2,64 +2,64 @@ export default { - "VERSION": "1.0.2", + "VERSION": "1.0.2", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "spell,talent,racialAbility,language,alphabet,specialCreatureAbility" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "spell,talent,racialAbility,language,alphabet,specialCreatureAbility" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "DS4.CharacterCurrencyGold", - img: "icons/commodities/currency/coin-embossed-crown-gold.webp", - abbreviation: "{#}G", - data: { - path: "system.currency.gold", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "DS4.CharacterCurrencySilver", - img: "icons/commodities/currency/coin-inset-snail-silver.webp", - abbreviation: "{#}S", - data: { - path: "system.currency.silver", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "DS4.CharacterCurrencyCopper", - img: "icons/commodities/currency/coin-engraved-waves-copper.webp", - abbreviation: "{#}C", - data: { - path: "system.currency.copper", - }, - primary: false, - exchangeRate: 0.01 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "DS4.CharacterCurrencyGold", + img: "icons/commodities/currency/coin-embossed-crown-gold.webp", + abbreviation: "{#}G", + data: { + path: "system.currency.gold", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "DS4.CharacterCurrencySilver", + img: "icons/commodities/currency/coin-inset-snail-silver.webp", + abbreviation: "{#}S", + data: { + path: "system.currency.silver", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "DS4.CharacterCurrencyCopper", + img: "icons/commodities/currency/coin-engraved-waves-copper.webp", + abbreviation: "{#}C", + data: { + path: "system.currency.copper", + }, + primary: false, + exchangeRate: 0.01 + } + ] } diff --git a/src/systems/fallout.js b/src/systems/fallout.js index 3abf2492..70bcadf0 100644 --- a/src/systems/fallout.js +++ b/src/systems/fallout.js @@ -1,43 +1,43 @@ export default { - "VERSION": "1.0.3", - - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", - - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.cost", - - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "skill,perk,special_ability" - } - ], - - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], - - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "FALLOUT.UI.CAPS", - img: "icons/commodities/currency/coins-engraved-copper.webp", - abbreviation: "{#}C", - data: { - path: "system.currency.caps", - }, - primary: true, - exchangeRate: 1 - } - ], - - "CURRENCY_DECIMAL_DIGITS": 0.01 + "VERSION": "1.0.3", + + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", + + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.cost", + + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "skill,perk,special_ability" + } + ], + + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], + + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "FALLOUT.UI.CAPS", + img: "icons/commodities/currency/coins-engraved-copper.webp", + abbreviation: "{#}C", + data: { + path: "system.currency.caps", + }, + primary: true, + exchangeRate: 1 + } + ], + + "CURRENCY_DECIMAL_DIGITS": 0.01 } diff --git a/src/systems/forbidden-lands.js b/src/systems/forbidden-lands.js index 050c6cd2..493d0080 100644 --- a/src/systems/forbidden-lands.js +++ b/src/systems/forbidden-lands.js @@ -1,63 +1,63 @@ export default { - "VERSION": "1.0.1", + "VERSION": "1.0.2", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.cost", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.cost", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "criticalInjury,monsterAttack,monsterTalent" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "criticalInjury,monsterAttack,monsterTalent" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "Gold", - img: "icons/commodities/currency/coin-embossed-crown-gold.webp", - abbreviation: "{#}GP", - data: { - path: "system.currency.gold", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "Silver", - img: "icons/commodities/currency/coin-engraved-moon-silver.webp", - abbreviation: "{#}SP", - data: { - path: "system.currency.silver", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "Copper", - img: "icons/commodities/currency/coin-engraved-waves-copper.webp", - abbreviation: "{#}CP", - data: { - path: "system.currency.copper", - }, - primary: false, - exchangeRate: 0.01 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "Gold", + img: "icons/commodities/currency/coin-embossed-crown-gold.webp", + abbreviation: "{#}GP", + data: { + path: "system.currency.gold.value", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "Silver", + img: "icons/commodities/currency/coin-engraved-moon-silver.webp", + abbreviation: "{#}SP", + data: { + path: "system.currency.silver.value", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "Copper", + img: "icons/commodities/currency/coin-engraved-waves-copper.webp", + abbreviation: "{#}CP", + data: { + path: "system.currency.copper.value", + }, + primary: false, + exchangeRate: 0.01 + } + ] } diff --git a/src/systems/icrpg.js b/src/systems/icrpg.js index 05bddb81..2776883f 100644 --- a/src/systems/icrpg.js +++ b/src/systems/icrpg.js @@ -1,43 +1,43 @@ export default { - "VERSION": "1.0.3", - - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", - - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", - - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "ability" - } - ], - - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], - - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "Coin", - img: "icons/svg/coins.svg", - abbreviation: "{#}C", - data: { - path: "system.coin.value", - }, - primary: true, - exchangeRate: 1 - } - ], - - "CURRENCY_DECIMAL_DIGITS": 0.01 + "VERSION": "1.0.3", + + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", + + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", + + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "ability" + } + ], + + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], + + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "Coin", + img: "icons/svg/coins.svg", + abbreviation: "{#}C", + data: { + path: "system.coin.value", + }, + primary: true, + exchangeRate: 1 + } + ], + + "CURRENCY_DECIMAL_DIGITS": 0.01 } diff --git a/src/systems/icrpgme.js b/src/systems/icrpgme.js index 24c61c1a..f893faba 100644 --- a/src/systems/icrpgme.js +++ b/src/systems/icrpgme.js @@ -1,32 +1,32 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "flags.item-piles.system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "flags.item-piles.system.price", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "ability,power,augment,spell,part,property" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "ability,power,augment,spell,part,property" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [], + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [], - "CURRENCY_DECIMAL_DIGITS": 0.01 + "CURRENCY_DECIMAL_DIGITS": 0.01 } diff --git a/src/systems/kamigakari.js b/src/systems/kamigakari.js index 900ad0af..19a83638 100644 --- a/src/systems/kamigakari.js +++ b/src/systems/kamigakari.js @@ -1,40 +1,40 @@ export default { - "VERSION": "1.0.0", - - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", - - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", - - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "talent,attackOption,race,style,facade,bond" - } - ], - - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], - - "CURRENCIES": [ - { - type: "attribute", - name: "KG.Money", - img: "icons/commodities/currency/coin-embossed-crown-gold.webp", - abbreviation: "{#}G", - data: { - path: "system.attributes.money", - }, - primary: true, - exchangeRate: 1 - } - ], - - "CURRENCY_DECIMAL_DIGITS": 0.01 + "VERSION": "1.0.0", + + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", + + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", + + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "talent,attackOption,race,style,facade,bond" + } + ], + + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], + + "CURRENCIES": [ + { + type: "attribute", + name: "KG.Money", + img: "icons/commodities/currency/coin-embossed-crown-gold.webp", + abbreviation: "{#}G", + data: { + path: "system.attributes.money", + }, + primary: true, + exchangeRate: 1 + } + ], + + "CURRENCY_DECIMAL_DIGITS": 0.01 } diff --git a/src/systems/knave.js b/src/systems/knave.js index 9854de88..a3f9521f 100644 --- a/src/systems/knave.js +++ b/src/systems/knave.js @@ -1,29 +1,29 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.coppers", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.coppers", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "spell" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "spell" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [] } diff --git a/src/systems/naheulbeuk.js b/src/systems/naheulbeuk.js index 86095bad..11294f16 100644 --- a/src/systems/naheulbeuk.js +++ b/src/systems/naheulbeuk.js @@ -1,84 +1,84 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.prix", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.prix", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "ape,attaque,competence,coup,etat,metier,origine,region,sort,trait" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "ape,attaque,competence,coup,etat,metier,origine,region,sort,trait" + } + ], - // This function is an optional system handler that specifically transforms an item when it is added to actors - "ITEM_TRANSFORMER": async (itemData) => { - ["equipe"].forEach(key => { - if (itemData?.system?.[key] !== undefined) { - delete itemData.system[key]; - } - }); - return itemData; - }, + // This function is an optional system handler that specifically transforms an item when it is added to actors + "ITEM_TRANSFORMER": async (itemData) => { + ["equipe"].forEach(key => { + if (itemData?.system?.[key] !== undefined) { + delete itemData.system[key]; + } + }); + return itemData; + }, - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "item", - name: "Pièce d'argent", - img: "systems/naheulbeuk/assets/from-rexard-icons/Tresors/tresor%20(101).webp", - abbreviation: "{#}PA", - data: { - uuid: "Compendium.naheulbeuk.trucs.BTUFKc6sEbJLmlas" - }, - primary: false, - exchangeRate: 10 - }, - { - type: "item", - name: "Pièce d'or", - img: "systems/naheulbeuk/assets/from-rexard-icons/Tresors/tresor%20(52).webp", - abbreviation: "{#}PO", - data: { - uuid: "Compendium.naheulbeuk.trucs.AKuErwzQ6wDxtzyp" - }, - primary: true, - exchangeRate: 1 - }, - { - type: "item", - name: "Lingot de Thrytil", - img: "systems/naheulbeuk/assets/from-rexard-icons/Objets/Materiaux/objet%20(291).webp", - abbreviation: "{#}LT", - data: { - uuid: "Compendium.naheulbeuk.trucs.tOTNc2WYpkyf2Yyl" - }, - primary: false, - exchangeRate: 0.01 - }, - { - type: "item", - name: "Lingot de Berylium", - img: "systems/naheulbeuk/assets/from-rexard-icons/Objets/Materiaux/objet%20(252).webp", - abbreviation: "{#}LB", - data: { - uuid: "Compendium.naheulbeuk.trucs.r4qLXqXaIIdyKzOf" - }, - primary: false, - exchangeRate: 0.002 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "item", + name: "Pièce d'argent", + img: "systems/naheulbeuk/assets/from-rexard-icons/Tresors/tresor%20(101).webp", + abbreviation: "{#}PA", + data: { + uuid: "Compendium.naheulbeuk.trucs.BTUFKc6sEbJLmlas" + }, + primary: false, + exchangeRate: 10 + }, + { + type: "item", + name: "Pièce d'or", + img: "systems/naheulbeuk/assets/from-rexard-icons/Tresors/tresor%20(52).webp", + abbreviation: "{#}PO", + data: { + uuid: "Compendium.naheulbeuk.trucs.AKuErwzQ6wDxtzyp" + }, + primary: true, + exchangeRate: 1 + }, + { + type: "item", + name: "Lingot de Thrytil", + img: "systems/naheulbeuk/assets/from-rexard-icons/Objets/Materiaux/objet%20(291).webp", + abbreviation: "{#}LT", + data: { + uuid: "Compendium.naheulbeuk.trucs.tOTNc2WYpkyf2Yyl" + }, + primary: false, + exchangeRate: 0.01 + }, + { + type: "item", + name: "Lingot de Berylium", + img: "systems/naheulbeuk/assets/from-rexard-icons/Objets/Materiaux/objet%20(252).webp", + abbreviation: "{#}LB", + data: { + uuid: "Compendium.naheulbeuk.trucs.r4qLXqXaIIdyKzOf" + }, + primary: false, + exchangeRate: 0.002 + } + ] } diff --git a/src/systems/ose.js b/src/systems/ose.js index c352812f..01038e36 100644 --- a/src/systems/ose.js +++ b/src/systems/ose.js @@ -1,53 +1,53 @@ export default { - "VERSION": "1.0.0", - - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", - - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity.value", - - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.cost", - - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "spell,ability" - } - ], - - "UNSTACKABLE_ITEM_TYPES": ["weapon", "armor", "container"], - - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type", "system.treasure"], - - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "item", - name: "OSE.items.gp.long", - img: "systems/ose/assets/gold.png", - abbreviation: "{#}GP", - data: { - item: { - "name": "Cold Coins", - "type": "item", - "img": "systems/ose/assets/gold.png", - "system": { - "quantity": { "value": 1, "max": null }, - "weight": 0.1, - "cost": 1, - "treasure": true, - } - } - }, - primary: true, - exchangeRate: 1 - } - ] + "VERSION": "1.0.0", + + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", + + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity.value", + + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.cost", + + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "spell,ability" + } + ], + + "UNSTACKABLE_ITEM_TYPES": ["weapon", "armor", "container"], + + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type", "system.treasure"], + + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "item", + name: "OSE.items.gp.long", + img: "systems/ose/assets/gold.png", + abbreviation: "{#}GP", + data: { + item: { + "name": "Cold Coins", + "type": "item", + "img": "systems/ose/assets/gold.png", + "system": { + "quantity": { "value": 1, "max": null }, + "weight": 0.1, + "cost": 1, + "treasure": true, + } + } + }, + primary: true, + exchangeRate: 1 + } + ] } diff --git a/src/systems/pf1.js b/src/systems/pf1.js index 9eb1b37e..d3f00216 100644 --- a/src/systems/pf1.js +++ b/src/systems/pf1.js @@ -1,87 +1,87 @@ export default { - "VERSION": "1.0.5", + "VERSION": "1.0.5", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "npc", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "npc", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // This function is an optional system handler that specifically transforms an item's price into a more unified numeric format - "ITEM_COST_TRANSFORMER": (item, currencies) => { - // Account for wand charges, broken condition, and other traits that are not reflected in base price. - // Spoof quantity to 1 temporarily - const origQuantity = item.system.quantity; - item.system.quantity = 1; - // Get actual value - const value = item.getValue({ sellValue: 1.0 }); - // Restore quantity - item.system.quantity = origQuantity; - return value; - }, + // This function is an optional system handler that specifically transforms an item's price into a more unified numeric format + "ITEM_COST_TRANSFORMER": (item, currencies) => { + // Account for wand charges, broken condition, and other traits that are not reflected in base price. + // Spoof quantity to 1 temporarily + const origQuantity = item.system.quantity; + item.system.quantity = 1; + // Get actual value + const value = item.getValue({ sellValue: 1.0 }); + // Restore quantity + item.system.quantity = origQuantity; + return value; + }, - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "attack,buff,class,feat,race,spell" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "attack,buff,class,feat,race,spell" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "PF1.CurrencyPlatinumP", - img: "systems/pf1/icons/items/inventory/coins-silver.jpg", - abbreviation: "{#}P", - data: { - path: "system.currency.pp", - }, - primary: false, - exchangeRate: 10 - }, - { - type: "attribute", - name: "PF1.CurrencyGoldP", - img: "systems/pf1/icons/items/inventory/coin-gold.jpg", - abbreviation: "{#}G", - data: { - path: "system.currency.gp", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "PF1.CurrencySilverP", - img: "systems/pf1/icons/items/inventory/coin-silver.jpg", - abbreviation: "{#}S", - data: { - path: "system.currency.sp", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "PF1.CurrencyCopperP", - img: "systems/pf1/icons/items/inventory/coin-copper.jpg", - abbreviation: "{#}C", - data: { - path: "system.currency.cp", - }, - primary: false, - exchangeRate: 0.01 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "PF1.CurrencyPlatinumP", + img: "systems/pf1/icons/items/inventory/coins-silver.jpg", + abbreviation: "{#}P", + data: { + path: "system.currency.pp", + }, + primary: false, + exchangeRate: 10 + }, + { + type: "attribute", + name: "PF1.CurrencyGoldP", + img: "systems/pf1/icons/items/inventory/coin-gold.jpg", + abbreviation: "{#}G", + data: { + path: "system.currency.gp", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "PF1.CurrencySilverP", + img: "systems/pf1/icons/items/inventory/coin-silver.jpg", + abbreviation: "{#}S", + data: { + path: "system.currency.sp", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "PF1.CurrencyCopperP", + img: "systems/pf1/icons/items/inventory/coin-copper.jpg", + abbreviation: "{#}C", + data: { + path: "system.currency.cp", + }, + primary: false, + exchangeRate: 0.01 + } + ] }; diff --git a/src/systems/pf2e.js b/src/systems/pf2e.js index 27cab755..24415272 100644 --- a/src/systems/pf2e.js +++ b/src/systems/pf2e.js @@ -3,129 +3,129 @@ import BasicItemDialog from "../applications/components/BasicItemDialog.svelte"; export default { - "VERSION": "1.0.4", + "VERSION": "1.0.4", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "loot", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "loot", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // The quantity for price attribute is the path to the attribute on each item that determine how many you get for its price - "QUANTITY_FOR_PRICE_ATTRIBUTE": "system.price.per", + // The quantity for price attribute is the path to the attribute on each item that determine how many you get for its price + "QUANTITY_FOR_PRICE_ATTRIBUTE": "system.price.per", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [{ - "path": "type", - "filters": 'action,ancestry,background,class,condition,deity,effect,feat,heritage,lore,melee,spell,spellcastingEntry' - }], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [{ + "path": "type", + "filters": 'action,ancestry,background,class,condition,deity,effect,feat,heritage,lore,melee,spell,spellcastingEntry' + }], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type", "system.temporary.value"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type", "system.temporary.value"], - // This function is an optional system handler that specifically transforms an item's price into a more unified numeric format - "ITEM_COST_TRANSFORMER": (item) => { - const itemCost = getProperty(item, "system.price"); - const { copperValue } = new game.pf2e.Coins(itemCost?.value ?? {}); - return copperValue / 100; - }, + // This function is an optional system handler that specifically transforms an item's price into a more unified numeric format + "ITEM_COST_TRANSFORMER": (item) => { + const itemCost = getProperty(item, "system.price"); + const { copperValue } = new game.pf2e.Coins(itemCost?.value ?? {}); + return copperValue / 100; + }, - "PREVIEW_ITEM_TRANSFORMER": (item) => { - if (game.user.isGM || item?.identificationStatus !== "unidentified") return item; - new TJSDialog({ - title: item.name, - content: { - class: BasicItemDialog, - props: { - item - } - } - }, { - classes: ["pf2e item sheet dorako-ui"], - resizable: false, - height: "auto", - width: "auto" - }).render(true); - return false; - }, + "PREVIEW_ITEM_TRANSFORMER": (item) => { + if (game.user.isGM || item?.identificationStatus !== "unidentified") return item; + new TJSDialog({ + title: item.name, + content: { + class: BasicItemDialog, + props: { + item + } + } + }, { + classes: ["pf2e item sheet dorako-ui"], + resizable: false, + height: "auto", + width: "auto" + }).render(true); + return false; + }, - "PILE_DEFAULTS": { - merchantColumns: [{ - "label": "Rarity", - "path": "system.traits.rarity", - "formatting": "{#}", - "buying": true, - "selling": true, - "mapping": { - "common": "PF2E.TraitCommon", - "uncommon": "PF2E.TraitUncommon", - "rare": "PF2E.TraitRare", - "unique": "PF2E.TraitUnique" - } - }, { - "label": "Bulk", - "path": "system.bulk.value", - "formatting": "{#}", - "buying": true, - "selling": true, - "mapping": { "0": "" } - }] - }, + "PILE_DEFAULTS": { + merchantColumns: [{ + "label": "Rarity", + "path": "system.traits.rarity", + "formatting": "{#}", + "buying": true, + "selling": true, + "mapping": { + "common": "PF2E.TraitCommon", + "uncommon": "PF2E.TraitUncommon", + "rare": "PF2E.TraitRare", + "unique": "PF2E.TraitUnique" + } + }, { + "label": "Bulk", + "path": "system.bulk.value", + "formatting": "{#}", + "buying": true, + "selling": true, + "mapping": { "0": "" } + }] + }, - "TOKEN_FLAG_DEFAULTS": { - flags: { - pf2e: { - linkToActorSize: false, - autoscale: false - } - } - }, + "TOKEN_FLAG_DEFAULTS": { + flags: { + pf2e: { + linkToActorSize: false, + autoscale: false + } + } + }, - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()`, put it into `data.item`, and strip out any module data - "CURRENCIES": [{ - type: "item", - name: "Platinum Pieces", - img: "systems/pf2e/icons/equipment/treasure/currency/platinum-pieces.webp", - abbreviation: "{#}PP", - data: { - uuid: "Compendium.pf2e.equipment-srd.JuNPeK5Qm1w6wpb4" - }, - primary: false, - exchangeRate: 10 - }, { - type: "item", - name: "Gold Pieces", - img: "systems/pf2e/icons/equipment/treasure/currency/gold-pieces.webp", - abbreviation: "{#}GP", - data: { - uuid: "Compendium.pf2e.equipment-srd.B6B7tBWJSqOBz5zz" - }, - primary: true, - exchangeRate: 1 - }, { - type: "item", - name: "Silver Pieces", - img: "systems/pf2e/icons/equipment/treasure/currency/silver-pieces.webp", - abbreviation: "{#}SP", - data: { - uuid: "Compendium.pf2e.equipment-srd.5Ew82vBF9YfaiY9f" - }, - primary: false, - exchangeRate: 0.1 - }, { - type: "item", - name: "Copper Pieces", - img: "systems/pf2e/icons/equipment/treasure/currency/copper-pieces.webp", - abbreviation: "{#}CP", - data: { - uuid: "Compendium.pf2e.equipment-srd.lzJ8AVhRcbFul5fh" - }, - primary: false, - exchangeRate: 0.01 - }] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()`, put it into `data.item`, and strip out any module data + "CURRENCIES": [{ + type: "item", + name: "Platinum Pieces", + img: "systems/pf2e/icons/equipment/treasure/currency/platinum-pieces.webp", + abbreviation: "{#}PP", + data: { + uuid: "Compendium.pf2e.equipment-srd.JuNPeK5Qm1w6wpb4" + }, + primary: false, + exchangeRate: 10 + }, { + type: "item", + name: "Gold Pieces", + img: "systems/pf2e/icons/equipment/treasure/currency/gold-pieces.webp", + abbreviation: "{#}GP", + data: { + uuid: "Compendium.pf2e.equipment-srd.B6B7tBWJSqOBz5zz" + }, + primary: true, + exchangeRate: 1 + }, { + type: "item", + name: "Silver Pieces", + img: "systems/pf2e/icons/equipment/treasure/currency/silver-pieces.webp", + abbreviation: "{#}SP", + data: { + uuid: "Compendium.pf2e.equipment-srd.5Ew82vBF9YfaiY9f" + }, + primary: false, + exchangeRate: 0.1 + }, { + type: "item", + name: "Copper Pieces", + img: "systems/pf2e/icons/equipment/treasure/currency/copper-pieces.webp", + abbreviation: "{#}CP", + data: { + uuid: "Compendium.pf2e.equipment-srd.lzJ8AVhRcbFul5fh" + }, + primary: false, + exchangeRate: 0.01 + }] } diff --git a/src/systems/pirateborg.js b/src/systems/pirateborg.js index 11f3ec0a..1f796be8 100644 --- a/src/systems/pirateborg.js +++ b/src/systems/pirateborg.js @@ -1,41 +1,41 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "container", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "container", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "feature,class,subclass,background" - } - ], + // Item filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "feature,class,subclass,background" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "PB.Silver", - img: "systems/pirateborg/icons/misc/thing-of-importance.png", - abbreviation: "{#}SP", - data: { - path: "system.silver", - }, - primary: true, - exchangeRate: 1 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "PB.Silver", + img: "systems/pirateborg/icons/misc/thing-of-importance.png", + abbreviation: "{#}SP", + data: { + path: "system.silver", + }, + primary: true, + exchangeRate: 1 + } + ] } diff --git a/src/systems/ptu.js b/src/systems/ptu.js index 6aaa4af4..a2121f73 100644 --- a/src/systems/ptu.js +++ b/src/systems/ptu.js @@ -1,41 +1,41 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.cost", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.cost", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "feat,edge,ability,move,capability,pokeedge,dexentry" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "feat,edge,ability,move,capability,pokeedge,dexentry" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "PTU.Money", - img: "icons/commodities/currency/coin-inset-snail-silver.webp", - abbreviation: "{#}₱", - data: { - path: "system.money" - }, - primary: true, - exchangeRate: 1 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "PTU.Money", + img: "icons/commodities/currency/coin-inset-snail-silver.webp", + abbreviation: "{#}₱", + data: { + path: "system.money" + }, + primary: true, + exchangeRate: 1 + } + ] } diff --git a/src/systems/sfrpg.js b/src/systems/sfrpg.js index db179f46..c10ab9c6 100644 --- a/src/systems/sfrpg.js +++ b/src/systems/sfrpg.js @@ -1,51 +1,51 @@ export default { - "VERSION": "1.0.2", - - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "npc2", - - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", - - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "attack,buff,class,feat,race,spell" - } - ], - - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], - - "CURRENCIES": [ - { - type: "attribute", - name: "SFRPG.Currencies.Credits", - img: "systems/sfrpg/icons/equipment/goods/credstick.webp", - abbreviation: "{#}C", - data: { - path: "system.currency.credit", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "SFRPG.Currencies.UPBs", - img: "systems/sfrpg/icons/equipment/goods/upb.webp", - abbreviation: "{#} UBP", - data: { - path: "system.currency.upb", - }, - primary: false, - exchangeRate: 1 - } - ], - - "CURRENCY_DECIMAL_DIGITS": 0.01 + "VERSION": "1.0.2", + + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "npc2", + + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", + + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "attack,buff,class,feat,race,spell" + } + ], + + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], + + "CURRENCIES": [ + { + type: "attribute", + name: "SFRPG.Currencies.Credits", + img: "systems/sfrpg/icons/equipment/goods/credstick.webp", + abbreviation: "{#}C", + data: { + path: "system.currency.credit", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "SFRPG.Currencies.UPBs", + img: "systems/sfrpg/icons/equipment/goods/upb.webp", + abbreviation: "{#} UBP", + data: { + path: "system.currency.upb", + }, + primary: false, + exchangeRate: 1 + } + ], + + "CURRENCY_DECIMAL_DIGITS": 0.01 } diff --git a/src/systems/splittermond.js b/src/systems/splittermond.js index ab517fd9..cbbde216 100644 --- a/src/systems/splittermond.js +++ b/src/systems/splittermond.js @@ -1,63 +1,63 @@ export default { - "VERSION": "1.0.4", + "VERSION": "1.0.4", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "npc", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "npc", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "spell,strength,weakness,mastery,species,culture,ancestry,education,resource,npcfeature,moonsign,language,culturelore,statuseffect,spelleffect,npcattack" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "spell,strength,weakness,mastery,species,culture,ancestry,education,resource,npcfeature,moonsign,language,culturelore,statuseffect,spelleffect,npcattack" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type", "system.sufferedDamage", "system.quality"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type", "system.sufferedDamage", "system.quality"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "Solare", - img: "icons/commodities/currency/coins-assorted-mix-copper.webp", - abbreviation: "{#}S", - data: { - path: "system.currency.S", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "Lunare", - img: "icons/commodities/currency/coins-assorted-mix-silver.webp", - abbreviation: "{#}L", - data: { - path: "system.currency.L", - }, - primary: false, - exchangeRate: 0.01 - }, - { - type: "attribute", - name: "Telare", - img: "icons/commodities/currency/coins-assorted-mix-platinum.webp", - abbreviation: "{#}T", - data: { - path: "system.currency.T", - }, - primary: false, - exchangeRate: 0.0001 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "Solare", + img: "icons/commodities/currency/coins-assorted-mix-copper.webp", + abbreviation: "{#}S", + data: { + path: "system.currency.S", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "Lunare", + img: "icons/commodities/currency/coins-assorted-mix-silver.webp", + abbreviation: "{#}L", + data: { + path: "system.currency.L", + }, + primary: false, + exchangeRate: 0.01 + }, + { + type: "attribute", + name: "Telare", + img: "icons/commodities/currency/coins-assorted-mix-platinum.webp", + abbreviation: "{#}T", + data: { + path: "system.currency.T", + }, + primary: false, + exchangeRate: 0.0001 + } + ] } diff --git a/src/systems/starwarsffg.js b/src/systems/starwarsffg.js index 2bae7b16..7ad3820a 100644 --- a/src/systems/starwarsffg.js +++ b/src/systems/starwarsffg.js @@ -1,43 +1,43 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity.value", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity.value", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price.value", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price.value", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "species,career,specialization,ability,criticaldamage,criticalinjury,talent,homesteadupgrade,signatureability,forcepower" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "species,career,specialization,ability,criticaldamage,criticalinjury,talent,homesteadupgrade,signatureability,forcepower" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - "type": "attribute", - "name": "Credits", - "img": "systems/starwarsffg/images/mod-all.png", - "abbreviation": "{#}cr", - "data": { - "path": "system.stats.credits.value" - }, - "primary": true, - "exchangeRate": 1, - "index": 0, - "id": "system.stats.credits.value" - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + "type": "attribute", + "name": "Credits", + "img": "systems/starwarsffg/images/mod-all.png", + "abbreviation": "{#}cr", + "data": { + "path": "system.stats.credits.value" + }, + "primary": true, + "exchangeRate": 1, + "index": 0, + "id": "system.stats.credits.value" + } + ] } diff --git a/src/systems/sw5e-2.0.3.2.3.8.js b/src/systems/sw5e-2.0.3.2.3.8.js index 24fa9926..e171af1e 100644 --- a/src/systems/sw5e-2.0.3.2.3.8.js +++ b/src/systems/sw5e-2.0.3.2.3.8.js @@ -1,53 +1,53 @@ export default { - VERSION: "1.0.1", + VERSION: "1.0.1", - ACTOR_CLASS_TYPE: "character", + ACTOR_CLASS_TYPE: "character", - ITEM_QUANTITY_ATTRIBUTE: "system.quantity", + ITEM_QUANTITY_ATTRIBUTE: "system.quantity", - ITEM_PRICE_ATTRIBUTE: "system.price", + ITEM_PRICE_ATTRIBUTE: "system.price", - ITEM_FILTERS: [ - { - path: "type", - filters: "power,feat,class,archetype,background", - }, - { - path: "system.weaponType", - filters: "natural", - }, - ], + ITEM_FILTERS: [ + { + path: "type", + filters: "power,feat,class,archetype,background", + }, + { + path: "system.weaponType", + filters: "natural", + }, + ], - ITEM_TRANSFORMER: async (itemData, actor = false) => { - ["equipped", "proficient", "prepared"].forEach((key) => { - if (itemData?.system?.[key] !== void 0) { - delete itemData.system[key]; - } - }); - setProperty( - itemData, - "system.attunement", - Math.min( - CONFIG.SW5E.attunementTypes.REQUIRED, - itemData?.system?.attunement ?? 0 - ) - ); - return itemData; - }, + ITEM_TRANSFORMER: async (itemData, actor = false) => { + ["equipped", "proficient", "prepared"].forEach((key) => { + if (itemData?.system?.[key] !== void 0) { + delete itemData.system[key]; + } + }); + setProperty( + itemData, + "system.attunement", + Math.min( + CONFIG.SW5E.attunementTypes.REQUIRED, + itemData?.system?.attunement ?? 0 + ) + ); + return itemData; + }, - ITEM_SIMILARITIES: ["name", "type"], + ITEM_SIMILARITIES: ["name", "type"], - CURRENCIES: [ - { - type: "attribute", - name: "SW5E.CurrencyGC", - img: "systems/sw5e/packs/Icons/Data Recording and Storage/CreditChip.webp", - abbreviation: "{#}GC", - data: { - path: "system.currency.gc", - }, - primary: true, - exchangeRate: 1, - }, - ], + CURRENCIES: [ + { + type: "attribute", + name: "SW5E.CurrencyGC", + img: "systems/sw5e/packs/Icons/Data Recording and Storage/CreditChip.webp", + abbreviation: "{#}GC", + data: { + path: "system.currency.gc", + }, + primary: true, + exchangeRate: 1, + }, + ], }; diff --git a/src/systems/sw5e.js b/src/systems/sw5e.js index 687a92ca..eac1cd1d 100644 --- a/src/systems/sw5e.js +++ b/src/systems/sw5e.js @@ -1,53 +1,53 @@ export default { - VERSION: "1.0.1", + VERSION: "1.0.1", - ACTOR_CLASS_TYPE: "character", + ACTOR_CLASS_TYPE: "character", - ITEM_QUANTITY_ATTRIBUTE: "system.quantity", + ITEM_QUANTITY_ATTRIBUTE: "system.quantity", - ITEM_PRICE_ATTRIBUTE: "system.price.value", + ITEM_PRICE_ATTRIBUTE: "system.price.value", - ITEM_FILTERS: [ - { - path: "type", - filters: "power,feat,class,archetype,background", - }, - { - path: "system.weaponType", - filters: "natural", - }, - ], + ITEM_FILTERS: [ + { + path: "type", + filters: "power,feat,class,archetype,background", + }, + { + path: "system.weaponType", + filters: "natural", + }, + ], - ITEM_TRANSFORMER: async (itemData, actor = false) => { - ["equipped", "proficient", "prepared"].forEach((key) => { - if (itemData?.system?.[key] !== void 0) { - delete itemData.system[key]; - } - }); - setProperty( - itemData, - "system.attunement", - Math.min( - CONFIG.SW5E.attunementTypes.REQUIRED, - itemData?.system?.attunement ?? 0 - ) - ); - return itemData; - }, + ITEM_TRANSFORMER: async (itemData, actor = false) => { + ["equipped", "proficient", "prepared"].forEach((key) => { + if (itemData?.system?.[key] !== void 0) { + delete itemData.system[key]; + } + }); + setProperty( + itemData, + "system.attunement", + Math.min( + CONFIG.SW5E.attunementTypes.REQUIRED, + itemData?.system?.attunement ?? 0 + ) + ); + return itemData; + }, - ITEM_SIMILARITIES: ["name", "type"], + ITEM_SIMILARITIES: ["name", "type"], - CURRENCIES: [ - { - type: "attribute", - name: "SW5E.CurrencyGC", - img: "systems/sw5e/packs/Icons/Data Recording and Storage/CreditChip.webp", - abbreviation: "{#}GC", - data: { - path: "system.currency.gc", - }, - primary: true, - exchangeRate: 1, - }, - ], + CURRENCIES: [ + { + type: "attribute", + name: "SW5E.CurrencyGC", + img: "systems/sw5e/packs/Icons/Data Recording and Storage/CreditChip.webp", + abbreviation: "{#}GC", + data: { + path: "system.currency.gc", + }, + primary: true, + exchangeRate: 1, + }, + ], }; diff --git a/src/systems/swade.js b/src/systems/swade.js index 5c7f3232..c6c5cbd0 100644 --- a/src/systems/swade.js +++ b/src/systems/swade.js @@ -1,44 +1,44 @@ export default { - "VERSION": "1.0.4", - - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", - - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", - - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "edge,hindrance,skill,power,ability" - } - ], - - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], - - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "SWADE.Currency", - img: "icons/svg/coins.svg", - abbreviation: "{#}T", - data: { - path: "system.details.currency", - }, - primary: true, - exchangeRate: 1 - } - ], - - "CURRENCY_DECIMAL_DIGITS": 0.01 + "VERSION": "1.0.4", + + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", + + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", + + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "edge,hindrance,skill,power,ability" + } + ], + + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], + + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "SWADE.Currency", + img: "icons/svg/coins.svg", + abbreviation: "{#}T", + data: { + path: "system.details.currency", + }, + primary: true, + exchangeRate: 1 + } + ], + + "CURRENCY_DECIMAL_DIGITS": 0.01 } diff --git a/src/systems/swse.js b/src/systems/swse.js index 04b09f2d..ac78de1c 100644 --- a/src/systems/swse.js +++ b/src/systems/swse.js @@ -1,39 +1,39 @@ export default { - "VERSION": "1.0.3", - - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", - - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "affiliation,background,class,beast quality,destiny,feat,forcePower,forceRegimen,forceSecret,forceTechnique,species,talent,template,trait,vehicleSystem" - } - ], - - "ITEM_PRICE_ATTRIBUTE": "system.cost", - - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["type", "name", "strippable", "hasPrerequisites", "modifiable", "hasLevels"], - - "CURRENCIES": [ - { - type: "attribute", - name: "Credits", - img: "icons/svg/coins.svg", - abbreviation: "{#}C", - data: { - path: "system.credits", - }, - primary: true, - exchangeRate: 1 - } - ], - - "CURRENCY_DECIMAL_DIGITS": 0.01 + "VERSION": "1.0.3", + + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", + + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "affiliation,background,class,beast quality,destiny,feat,forcePower,forceRegimen,forceSecret,forceTechnique,species,talent,template,trait,vehicleSystem" + } + ], + + "ITEM_PRICE_ATTRIBUTE": "system.cost", + + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["type", "name", "strippable", "hasPrerequisites", "modifiable", "hasLevels"], + + "CURRENCIES": [ + { + type: "attribute", + name: "Credits", + img: "icons/svg/coins.svg", + abbreviation: "{#}C", + data: { + path: "system.credits", + }, + primary: true, + exchangeRate: 1 + } + ], + + "CURRENCY_DECIMAL_DIGITS": 0.01 } diff --git a/src/systems/symbaroum.js b/src/systems/symbaroum.js index 6a6a64dc..805840b3 100644 --- a/src/systems/symbaroum.js +++ b/src/systems/symbaroum.js @@ -1,63 +1,63 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "monster", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "monster", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.number", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.number", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.cost", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.cost", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "ability,boon,burden,mysticalPower,ritual,trait" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "ability,boon,burden,mysticalPower,ritual,trait" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "Thaler", - img: "icons/commodities/currency/coins-assorted-mix-copper.webp", - abbreviation: "{#}T", - data: { - path: "system.money.thaler", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "Shilling", - img: "icons/commodities/currency/coins-assorted-mix-silver.webp", - abbreviation: "{#}S", - data: { - path: "system.money.shilling", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "Orteg", - img: "icons/commodities/currency/coins-assorted-mix-platinum.webp", - abbreviation: "{#}O", - data: { - path: "system.money.orteg", - }, - primary: false, - exchangeRate: 0.01 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "Thaler", + img: "icons/commodities/currency/coins-assorted-mix-copper.webp", + abbreviation: "{#}T", + data: { + path: "system.money.thaler", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "Shilling", + img: "icons/commodities/currency/coins-assorted-mix-silver.webp", + abbreviation: "{#}S", + data: { + path: "system.money.shilling", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "Orteg", + img: "icons/commodities/currency/coins-assorted-mix-platinum.webp", + abbreviation: "{#}O", + data: { + path: "system.money.orteg", + }, + primary: false, + exchangeRate: 0.01 + } + ] } diff --git a/src/systems/t2k4e.js b/src/systems/t2k4e.js index 213f37b1..d18a9719 100644 --- a/src/systems/t2k4e.js +++ b/src/systems/t2k4e.js @@ -1,29 +1,29 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.qty", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.qty", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "specialty,injury" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "specialty,injury" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [] } diff --git a/src/systems/tormenta20.js b/src/systems/tormenta20.js index 1cfcc09c..abc942a3 100644 --- a/src/systems/tormenta20.js +++ b/src/systems/tormenta20.js @@ -1,67 +1,67 @@ export default { - "VERSION": "1.0.2", + "VERSION": "1.0.2", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.qtd", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.qtd", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price.gc", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price.gc", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "magia,poder,classe" - }, - { - "path": "system.tipoUso", - "filters": "nat" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "magia,poder,classe" + }, + { + "path": "system.tipoUso", + "filters": "nat" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "Ouro", - img: "icons/commodities/currency/coin-embossed-insect-gold.webp", - abbreviation: "{#}O", - data: { - path: "system.dinheiro.to", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "Prata", - img: "icons/commodities/currency/coin-embossed-unicorn-silver.webp", - abbreviation: "{#}P", - data: { - path: "system.dinheiro.tp", - }, - primary: false, - exchangeRate: 0.1 - }, - { - type: "attribute", - name: "Cobre", - img: "icons/commodities/currency/coin-engraved-waves-copper.webp", - abbreviation: "{#}C", - data: { - path: "system.dinheiro.tc", - }, - primary: false, - exchangeRate: 0.01 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "Ouro", + img: "icons/commodities/currency/coin-embossed-insect-gold.webp", + abbreviation: "{#}O", + data: { + path: "system.dinheiro.to", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "Prata", + img: "icons/commodities/currency/coin-embossed-unicorn-silver.webp", + abbreviation: "{#}P", + data: { + path: "system.dinheiro.tp", + }, + primary: false, + exchangeRate: 0.1 + }, + { + type: "attribute", + name: "Cobre", + img: "icons/commodities/currency/coin-engraved-waves-copper.webp", + abbreviation: "{#}C", + data: { + path: "system.dinheiro.tc", + }, + primary: false, + exchangeRate: 0.01 + } + ] } diff --git a/src/systems/wfrp4e.js b/src/systems/wfrp4e.js index 49b30138..38784fb1 100644 --- a/src/systems/wfrp4e.js +++ b/src/systems/wfrp4e.js @@ -2,113 +2,113 @@ import CONSTANTS from "../constants/constants.js"; export default { - "VERSION": "1.0.7", + "VERSION": "1.0.7", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity.value", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity.value", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "career,container,critical,disease,injury,mutation,prayer,psychology,talent,skill,spell,trait,extendedTest,vehicleMod,cargo" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "career,container,critical,disease,injury,mutation,prayer,psychology,talent,skill,spell,trait,extendedTest,vehicleMod,cargo" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // This function is an optional system handler that specifically transforms an item's price into a more unified numeric format - "ITEM_COST_TRANSFORMER": (item) => { - let overallCost = 0; - const prices = getProperty(item, "system.price"); - overallCost += (Number(prices?.["gc"]) ?? 0) * 240; - overallCost += (Number(prices?.["ss"]) ?? 0) * 12; - overallCost += (Number(prices?.["bp"]) ?? 0) * 1; - return overallCost; - }, + // This function is an optional system handler that specifically transforms an item's price into a more unified numeric format + "ITEM_COST_TRANSFORMER": (item) => { + let overallCost = 0; + const prices = getProperty(item, "system.price"); + overallCost += (Number(prices?.["gc"]) ?? 0) * 240; + overallCost += (Number(prices?.["ss"]) ?? 0) * 12; + overallCost += (Number(prices?.["bp"]) ?? 0) * 1; + return overallCost; + }, - "PILE_TYPE_DEFAULTS": { - [CONSTANTS.PILE_TYPES.MERCHANT]: { - merchantColumns: [{ - label: "Availability", - path: "system.availability.value" - }], - } - }, + "PILE_TYPE_DEFAULTS": { + [CONSTANTS.PILE_TYPES.MERCHANT]: { + merchantColumns: [{ + label: "Availability", + path: "system.availability.value" + }], + } + }, - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "item", - name: "Gold Crown", - img: "modules/wfrp4e-core/icons/currency/goldcrown.png", - abbreviation: "{#}GC", - data: { - item: { - "name": "Gold Crown", - "type": "money", - "img": "modules/wfrp4e-core/icons/currency/goldcrown.png", - "system": { - "quantity": { "type": "Number", "label": "Quantity", "value": 1 }, - "encumbrance": { "type": "Number", "label": "Encumbrance", "value": 0.005 }, - "coinValue": { "label": "Value (in d)", "type": "Number", "value": 240 }, - "source": { "type": "String", "label": "Source" } - } - } - }, - primary: false, - exchangeRate: 240 - }, - { - type: "item", - name: "Silver Shilling", - img: "modules/wfrp4e-core/icons/currency/silvershilling.png", - abbreviation: "{#}SS", - data: { - item: { - "name": "Silver Shilling", - "type": "money", - "img": "modules/wfrp4e-core/icons/currency/silvershilling.png", - "system": { - "quantity": { "type": "Number", "label": "Quantity", "value": 1 }, - "encumbrance": { "type": "Number", "label": "Encumbrance", "value": 0.01 }, - "coinValue": { "label": "Value (in d)", "type": "Number", "value": 12 }, - "source": { "type": "String", "label": "Source" } - } - } - }, - primary: false, - exchangeRate: 12 - }, - { - type: "item", - name: "Brass Penny", - img: "modules/wfrp4e-core/icons/currency/brasspenny.png", - abbreviation: "{#}BP", - data: { - item: { - "name": "Brass Penny", - "type": "money", - "img": "modules/wfrp4e-core/icons/currency/brasspenny.png", - "system": { - "quantity": { "type": "Number", "label": "Quantity", "value": 1 }, - "encumbrance": { "type": "Number", "label": "Encumbrance", "value": 0.01 }, - "coinValue": { "label": "Value (in d)", "type": "Number", "value": 1 }, - "source": { "type": "String", "label": "Source" } - } - } - }, - primary: true, - exchangeRate: 1 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "item", + name: "Gold Crown", + img: "modules/wfrp4e-core/icons/currency/goldcrown.png", + abbreviation: "{#}GC", + data: { + item: { + "name": "Gold Crown", + "type": "money", + "img": "modules/wfrp4e-core/icons/currency/goldcrown.png", + "system": { + "quantity": { "type": "Number", "label": "Quantity", "value": 1 }, + "encumbrance": { "type": "Number", "label": "Encumbrance", "value": 0.005 }, + "coinValue": { "label": "Value (in d)", "type": "Number", "value": 240 }, + "source": { "type": "String", "label": "Source" } + } + } + }, + primary: false, + exchangeRate: 240 + }, + { + type: "item", + name: "Silver Shilling", + img: "modules/wfrp4e-core/icons/currency/silvershilling.png", + abbreviation: "{#}SS", + data: { + item: { + "name": "Silver Shilling", + "type": "money", + "img": "modules/wfrp4e-core/icons/currency/silvershilling.png", + "system": { + "quantity": { "type": "Number", "label": "Quantity", "value": 1 }, + "encumbrance": { "type": "Number", "label": "Encumbrance", "value": 0.01 }, + "coinValue": { "label": "Value (in d)", "type": "Number", "value": 12 }, + "source": { "type": "String", "label": "Source" } + } + } + }, + primary: false, + exchangeRate: 12 + }, + { + type: "item", + name: "Brass Penny", + img: "modules/wfrp4e-core/icons/currency/brasspenny.png", + abbreviation: "{#}BP", + data: { + item: { + "name": "Brass Penny", + "type": "money", + "img": "modules/wfrp4e-core/icons/currency/brasspenny.png", + "system": { + "quantity": { "type": "Number", "label": "Quantity", "value": 1 }, + "encumbrance": { "type": "Number", "label": "Encumbrance", "value": 0.01 }, + "coinValue": { "label": "Value (in d)", "type": "Number", "value": 1 }, + "source": { "type": "String", "label": "Source" } + } + } + }, + primary: true, + exchangeRate: 1 + } + ] } diff --git a/src/systems/wwn.js b/src/systems/wwn.js index 33d127f0..90948c47 100644 --- a/src/systems/wwn.js +++ b/src/systems/wwn.js @@ -1,85 +1,85 @@ export default { - "VERSION": "1.0.0", + "VERSION": "1.0.0", - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "character", + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "character", - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.price", + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.price", - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "ability, art, asset, focus, skill, spell" - } - ], + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "ability, art, asset, focus, skill, spell" + } + ], - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "WWN.items.PP.long", - img: "icons/commodities/currency/coin-inset-snail-silver.webp", - abbreviation: "{#}PP", - data: { - path: "system.currency.pp" - }, - primary: false, - exchangeRate: 50 - }, - { - type: "attribute", - name: "WWN.items.GP.long", - img: "icons/commodities/currency/coin-embossed-crown-gold.webp", - abbreviation: "{#}GP", - data: { - path: "system.currency.gp", - }, - primary: false, - exchangeRate: 10 - }, - { - type: "attribute", - name: "WWN.items.EP.long", - img: "icons/commodities/currency/coin-inset-copper-axe.webp", - abbreviation: "{#}EP", - data: { - path: "system.currency.ep", - }, - primary: false, - exchangeRate: 5 - }, - { - type: "attribute", - name: "WWN.items.SP.long", - img: "icons/commodities/currency/coin-engraved-moon-silver.webp", - abbreviation: "{#}SP", - data: { - path: "system.currency.sp", - }, - primary: true, - exchangeRate: 1 - }, - { - type: "attribute", - name: "WWN.items.CP.long", - img: "icons/commodities/currency/coin-engraved-waves-copper.webp", - abbreviation: "{#}CP", - data: { - path: "system.currency.cp", - }, - primary: false, - exchangeRate: 0.01 - } - ] + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "WWN.items.PP.long", + img: "icons/commodities/currency/coin-inset-snail-silver.webp", + abbreviation: "{#}PP", + data: { + path: "system.currency.pp" + }, + primary: false, + exchangeRate: 50 + }, + { + type: "attribute", + name: "WWN.items.GP.long", + img: "icons/commodities/currency/coin-embossed-crown-gold.webp", + abbreviation: "{#}GP", + data: { + path: "system.currency.gp", + }, + primary: false, + exchangeRate: 10 + }, + { + type: "attribute", + name: "WWN.items.EP.long", + img: "icons/commodities/currency/coin-inset-copper-axe.webp", + abbreviation: "{#}EP", + data: { + path: "system.currency.ep", + }, + primary: false, + exchangeRate: 5 + }, + { + type: "attribute", + name: "WWN.items.SP.long", + img: "icons/commodities/currency/coin-engraved-moon-silver.webp", + abbreviation: "{#}SP", + data: { + path: "system.currency.sp", + }, + primary: true, + exchangeRate: 1 + }, + { + type: "attribute", + name: "WWN.items.CP.long", + img: "icons/commodities/currency/coin-engraved-waves-copper.webp", + abbreviation: "{#}CP", + data: { + path: "system.currency.cp", + }, + primary: false, + exchangeRate: 0.01 + } + ] } diff --git a/src/systems/yzecoriolis.js b/src/systems/yzecoriolis.js index 1f10d740..7544ac72 100644 --- a/src/systems/yzecoriolis.js +++ b/src/systems/yzecoriolis.js @@ -1,43 +1,43 @@ export default { - "VERSION": "1.0.0", - - // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. - "ACTOR_CLASS_TYPE": "npc", - - // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists - "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", - - // The item price attribute is the path to the attribute on each item that determine how much it costs - "ITEM_PRICE_ATTRIBUTE": "system.cost", - - // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes - "ITEM_FILTERS": [ - { - "path": "type", - "filters": "injury, shipProblem, shipCriticalDamage, shipLogbook" - } - ], - - // Item similarities determines how item piles detect similarities and differences in the system - "ITEM_SIMILARITIES": ["name", "type"], - - // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) - // In the case of attributes, the path is relative to the "actor.system" - // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data - "CURRENCIES": [ - { - type: "attribute", - name: "YZECORIOLIS.Birr", - img: "icons/commodities/currency/coin-inset-insect-gold.webp", - abbreviation: "{#}B", - data: { - path: "system.birr", - }, - primary: true, - exchangeRate: 1 - } - ], - - "CURRENCY_DECIMAL_DIGITS": 0.01 + "VERSION": "1.0.0", + + // The actor class type is the type of actor that will be used for the default item pile actor that is created on first item drop. + "ACTOR_CLASS_TYPE": "npc", + + // The item quantity attribute is the path to the attribute on items that denote how many of that item that exists + "ITEM_QUANTITY_ATTRIBUTE": "system.quantity", + + // The item price attribute is the path to the attribute on each item that determine how much it costs + "ITEM_PRICE_ATTRIBUTE": "system.cost", + + // Item types and the filters actively remove items from the item pile inventory UI that users cannot loot, such as spells, feats, and classes + "ITEM_FILTERS": [ + { + "path": "type", + "filters": "injury, shipProblem, shipCriticalDamage, shipLogbook" + } + ], + + // Item similarities determines how item piles detect similarities and differences in the system + "ITEM_SIMILARITIES": ["name", "type"], + + // Currencies in item piles is a versatile system that can accept actor attributes (a number field on the actor's sheet) or items (actual items in their inventory) + // In the case of attributes, the path is relative to the "actor.system" + // In the case of items, it is recommended you export the item with `.toObject()` and strip out any module data + "CURRENCIES": [ + { + type: "attribute", + name: "YZECORIOLIS.Birr", + img: "icons/commodities/currency/coin-inset-insect-gold.webp", + abbreviation: "{#}B", + data: { + path: "system.birr", + }, + primary: true, + exchangeRate: 1 + } + ], + + "CURRENCY_DECIMAL_DIGITS": 0.01 }