diff --git a/README.md b/README.md index 2e9cca39..3d7961da 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Like what we've done? Buy us a coffee! --- +![Item Piles in a nutshell](./docs/images/intro.jpg) + ## What is Item Piles? This module enables dropping items onto the canvas, which then get represented as a pile of items. In order to work in all systems without breaking or messing too much with the core functionality of Foundry, this **creates an unlinked token & actor** to hold these items. When a player double-clicks on an item pile token, it opens a custom UI to show what the pile contains and players can then take items from it. @@ -69,6 +71,7 @@ This module leverages the [Advanced Macros](https://github.com/League-of-Foundry - [Pathfinder 1e](https://foundryvtt.com/packages/pf1) - [Dungeon Slayers 4](https://foundryvtt.com/packages/ds4) - [D&D 3.5e SRD](https://foundryvtt.com/packages/D35E) +- [Savage Worlds Adventure Edition](https://foundryvtt.com/packages/swade) ## Not Supported Systems @@ -91,7 +94,13 @@ Installing this module allows anyone to drag & drop items to the canvas, which c As a GM, you can duplicate the default item pile to create new versions of that pile, or turn existing actors into new item piles through the `Item Pile` button on the actor sheets' header bar. This UI has a wide range of customization, which allows you to control exactly how your players interact with the item pile. -![Item Pile Token Configuration](https://raw.githubusercontent.com/fantasycalendar/FoundryVTT-ItemPiles/master/docs/images/configs.png) +![Item Pile Token Configuration](./docs/images/configs.png) + +### Warning! + +Be sure to set any new Item Piles as **unlinked from its actor data**! Unless you know what you're doing, keeping this enabled will be confusing as **all tokens of this pile will share the same images and inventory**. + +![Item Pile Token Linked](./docs/images/disable-link.jpg) ### Interaction @@ -99,13 +108,13 @@ Any player or GM can drag & drop items - if you drag & drop an item from an acto When players double-click on the item pile (or Left Control + double-click for GMs to inspect, or to inspect as someone else select two tokens, and repeat), they will get a custom UI where they can choose what they want to take from the item pile, or all of it (if they're loot goblins). -![Item Pile Inventory UI](https://raw.githubusercontent.com/fantasycalendar/FoundryVTT-ItemPiles/master/docs/images/inventory-ui.png) +![Item Pile Inventory UI](./docs/images/inventory-ui.png) ### Extra UI and settings In addition, item piles have a few extra buttons on the right click Token HUD to open, close, lock, and unlock containers, and a quick way to access the configuration of that token. -![Item Pile Token HUD buttons](https://raw.githubusercontent.com/fantasycalendar/FoundryVTT-ItemPiles/master/docs/images/token-buttons.png) +![Item Pile Token HUD buttons](./docs/images/token-buttons.png) In the module settings, you can configure all sorts of things, such as whether empty piles auto-delete once they're empty, and which item types are allowed to be picked up. These are by default configured to the D&D5e system, so adjust them accordingly for your own system. @@ -113,7 +122,7 @@ In the module settings, you can configure all sorts of things, such as whether e For most systems, currencies or other physical things aren't considered "items" in Foundry, but rather just numbers on the sheet, so dragging and dropping them is hard. However, Item Piles still allow you to pick up such items from piles using its flexible "Dynamic Attributes" feature. With this feature, you can configure what types of these "numbers only" things that can still be picked from piles up by players. -![Item Pile Dynamic Attribute Settings](https://raw.githubusercontent.com/fantasycalendar/FoundryVTT-ItemPiles/master/docs/images/attributes.png) +![Item Pile Dynamic Attribute Settings](./docs/images/attributes.png) Each row represents a field on a character sheet that may be picked up. As you can see, the D&D5e system denotes the number of gold coins a character has with the `actor.data.data.currency.gp` field, so by putting `data.currency.gp` as a valid field in this UI, Item Piles figures out that if an actor has more than 0 in this field, it can be "picked up" and transferred to the character who picked it up. diff --git a/changelog.md b/changelog.md index 4c155fa7..84792320 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,10 @@ # Item Piles Changelog +## Version 1.1.1 +- Added support for the Savage Worlds Adventure Edition: +- Fixed linked token actors not acting like they are linked - now all tokens on the canvas with the same linked actor share the same state and image +- Fixed dynamic attributes not being treated as numbers, which caused problems in some systems (such as SWADE) if they were stored as strings + ## Version 1.1.0 - Added support for the D&D 3.5 system: - Added the ability to click on item names to inspect the items - this can be disabled in the item pile's settings diff --git a/docs/images/disable-link.jpg b/docs/images/disable-link.jpg new file mode 100644 index 00000000..a1eddde9 Binary files /dev/null and b/docs/images/disable-link.jpg differ diff --git a/docs/images/intro.jpg b/docs/images/intro.jpg new file mode 100644 index 00000000..3e53155d Binary files /dev/null and b/docs/images/intro.jpg differ diff --git a/scripts/api.js b/scripts/api.js index 855f84f3..77295ad8 100644 --- a/scripts/api.js +++ b/scripts/api.js @@ -891,12 +891,12 @@ export default class API { const item = lib.getSimilarItem(targetActorItems, { itemId: itemData._id, itemName: itemData.name, itemType: itemData.type }); - const incomingQuantity = getProperty(itemData, API.ITEM_QUANTITY_ATTRIBUTE) ?? 1; + const incomingQuantity = Number(getProperty(itemData, API.ITEM_QUANTITY_ATTRIBUTE) ?? 1); const itemAdded = item ? item.toObject() : foundry.utils.duplicate(itemData); if (item) { - const currentQuantity = getProperty(item.data, API.ITEM_QUANTITY_ATTRIBUTE); + const currentQuantity = Number(getProperty(item.data, API.ITEM_QUANTITY_ATTRIBUTE)); const newQuantity = currentQuantity + incomingQuantity; itemsToUpdate.push({ "_id": item.id, @@ -998,9 +998,9 @@ export default class API { const actorItem = targetActor.items.get(itemId); const removedItem = actorItem.toObject(); - const currentQuantity = getProperty(actorItem.data, API.ITEM_QUANTITY_ATTRIBUTE); + const currentQuantity = Number(getProperty(actorItem.data, API.ITEM_QUANTITY_ATTRIBUTE)); - const quantityToRemove = getProperty(item, API.ITEM_QUANTITY_ATTRIBUTE) ?? item.quantity ?? currentQuantity; + const quantityToRemove = Number(getProperty(item, API.ITEM_QUANTITY_ATTRIBUTE) ?? item.quantity ?? currentQuantity); const newQuantity = Math.max(0, currentQuantity - quantityToRemove); @@ -1249,7 +1249,7 @@ export default class API { for (const [attribute, quantityToAdd] of Object.entries(attributes)) { - const currentQuantity = getProperty(targetActor.data, attribute); + const currentQuantity = Number(getProperty(targetActor.data, attribute)); updates[attribute] = currentQuantity + quantityToAdd; attributesAdded[attribute] = currentQuantity + quantityToAdd; @@ -1336,13 +1336,13 @@ export default class API { if (Array.isArray(attributes)) { attributes = Object.fromEntries(attributes.map(attribute => { - return [attribute, getProperty(targetActor.data, attribute)]; + return [attribute, Number(getProperty(targetActor.data, attribute))]; })) } for (const [attribute, quantityToRemove] of Object.entries(attributes)) { - const currentQuantity = getProperty(targetActor.data, attribute); + const currentQuantity = Number(getProperty(targetActor.data, attribute)); const newQuantity = Math.max(0, currentQuantity - quantityToRemove); updates[attribute] = newQuantity; @@ -1515,7 +1515,7 @@ export default class API { const attributesToTransfer = sourceAttributes.filter(attribute => { return hasProperty(sourceActor.data, attribute.path) - && getProperty(sourceActor.data, attribute.path) > 0 + && Number(getProperty(sourceActor.data, attribute.path)) > 0 && hasProperty(targetActor.data, attribute.path); }).map(attribute => attribute.path); diff --git a/scripts/formapplications/dropDialog.js b/scripts/formapplications/dropDialog.js index 0391083a..df556d91 100644 --- a/scripts/formapplications/dropDialog.js +++ b/scripts/formapplications/dropDialog.js @@ -35,7 +35,7 @@ export default class DropDialog extends FormApplication { data.dropObjects = this.dropObjects; data.itemPileAtLocation = this.dropObjects.length > 0; data.droppedItem = this.droppedItem; - data.itemQuantity = getProperty(this.droppedItem, API.ITEM_QUANTITY_ATTRIBUTE) ?? 1; + data.itemQuantity = Number(getProperty(this.droppedItem, API.ITEM_QUANTITY_ATTRIBUTE) ?? 1); data.itemQuantityMoreThanOne = data.itemQuantity > 1; return data; diff --git a/scripts/formapplications/itemPileInventory.js b/scripts/formapplications/itemPileInventory.js index 70ce0470..aba0ff4c 100644 --- a/scripts/formapplications/itemPileInventory.js +++ b/scripts/formapplications/itemPileInventory.js @@ -91,7 +91,7 @@ export class ItemPileInventory extends FormApplication { name = foundItem.name; img = foundItem.data.img; type = getProperty(foundItem.data, API.ITEM_TYPE_ATTRIBUTE); - maxQuantity = getProperty(foundItem.data, API.ITEM_QUANTITY_ATTRIBUTE); + maxQuantity = Number(getProperty(foundItem.data, API.ITEM_QUANTITY_ATTRIBUTE)); }else{ itemQuantity = 0; maxQuantity = 0; @@ -118,7 +118,7 @@ export class ItemPileInventory extends FormApplication { type: item.type, img: item.data?.img ?? "", currentQuantity: 1, - maxQuantity: getProperty(item.data, API.ITEM_QUANTITY_ATTRIBUTE) ?? 1 + maxQuantity: Number(getProperty(item.data, API.ITEM_QUANTITY_ATTRIBUTE) ?? 1) }; }) } @@ -135,7 +135,7 @@ export class ItemPileInventory extends FormApplication { const img = $(this).find('.item-piles-img').attr('src'); const itemQuantity = $(this).find('input').val(); - const maxQuantity = getProperty(self.pile.actor.data, path) ?? 0; + const maxQuantity = Number(getProperty(self.pile.actor.data, path)) ?? 0; const currentQuantity = Math.min(maxQuantity, Math.max(itemQuantity, 0)); @@ -163,7 +163,7 @@ export class ItemPileInventory extends FormApplication { path: attribute.path, img: attribute.img, currentQuantity: 1, - maxQuantity: getProperty(this.pile.actor.data, attribute.path) ?? 1 + maxQuantity: Number(getProperty(this.pile.actor.data, attribute.path) ?? 1) } }).filter(Boolean); } @@ -277,7 +277,7 @@ export class ItemPileInventory extends FormApplication { this.saveItems(); this.saveAttributes(); const item = this.pile.actor.items.get(itemId); - const maxQuantity = getProperty(item.data, API.ITEM_QUANTITY_ATTRIBUTE) ?? 1; + const maxQuantity = Number(getProperty(item.data, API.ITEM_QUANTITY_ATTRIBUTE) ?? 1); const quantity = Math.min(inputQuantity, maxQuantity); await API.transferItems(this.pile, this.recipient, [{ _id: itemId, quantity }]); } @@ -285,7 +285,7 @@ export class ItemPileInventory extends FormApplication { async takeAttribute(attribute, inputQuantity) { this.saveItems(); this.saveAttributes(); - const maxQuantity = getProperty(this.pile.actor.data, attribute) ?? 0; + const maxQuantity = Number(getProperty(this.pile.actor.data, attribute) ?? 0); const quantity = Math.min(inputQuantity, maxQuantity); await API.transferAttributes(this.pile, this.recipient, { [attribute]: quantity }); } diff --git a/scripts/lib/lib.js b/scripts/lib/lib.js index d14b9aa1..eaf02d3d 100644 --- a/scripts/lib/lib.js +++ b/scripts/lib/lib.js @@ -124,7 +124,7 @@ export function hasNonzeroAttribute(target, attribute){ const actor = target instanceof TokenDocument ? target.actor : target; - const attributeValue = getProperty(actor.data, attribute) ?? 0; + const attributeValue = Number(getProperty(actor.data, attribute) ?? 0); return hasProperty(actor.data, attribute) && attributeValue > 0; } diff --git a/scripts/systems.js b/scripts/systems.js index 8d7daca7..77ecfdfe 100644 --- a/scripts/systems.js +++ b/scripts/systems.js @@ -4,6 +4,7 @@ import dnd5e from "./systems/dnd5e.js"; import pf1 from "./systems/pf1.js"; import ds4 from "./systems/ds4.js"; import d35e from "./systems/ds4.js"; +import swade from "./systems/swade.js"; // ↑ IMPORT SYSTEMS HERE ↑ /** @@ -18,6 +19,7 @@ export const SYSTEMS = { pf1, ds4, d35e, + swade, // ↑ ADD SYSTEMS HERE ↑ }?.[game.system.id]; } diff --git a/scripts/systems/swade.js b/scripts/systems/swade.js new file mode 100644 index 00000000..924048c4 --- /dev/null +++ b/scripts/systems/swade.js @@ -0,0 +1,20 @@ +export default { + // 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": "data.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_TYPE_ATTRIBUTE": "type", + "ITEM_TYPE_FILTERS": "edge,hindrance,skill,power,ability", + + // Dynamic attributes are things like currencies or transferable powers that exist as editable number fields on character sheets + "DYNAMIC_ATTRIBUTES": [ + { + name: "SWADE.Currency", + path: "data.details.currency", + img: "icons/svg/coins.svg" + } + ] +} \ No newline at end of file