Skip to content

Commit

Permalink
Merge pull request #8 from foundryvtt/containers
Browse files Browse the repository at this point in the history
[#729] Add container support to sidebar & compendiums
  • Loading branch information
arbron authored Nov 27, 2023
2 parents 6569c53 + b711795 commit a21e03a
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 27 deletions.
7 changes: 7 additions & 0 deletions dnd5e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Hooks.once("init", function() {
CONFIG.DND5E = DND5E;
CONFIG.ActiveEffect.documentClass = documents.ActiveEffect5e;
CONFIG.Actor.documentClass = documents.Actor5e;
CONFIG.Item.collection = dataModels.collection.Items5e;
CONFIG.Item.compendiumIndexFields.push("system.container");
CONFIG.Item.documentClass = documents.Item5e;
CONFIG.Token.documentClass = documents.TokenDocument5e;
CONFIG.Token.objectClass = canvas.Token5e;
Expand All @@ -60,6 +62,7 @@ Hooks.once("init", function() {
CONFIG.MeasuredTemplate.defaults.angle = 53.13; // 5e cone RAW should be 53.13 degrees
CONFIG.Note.objectClass = canvas.Note5e;
CONFIG.ui.combat = applications.combat.CombatTracker5e;
CONFIG.ui.items = dnd5e.applications.item.ItemDirectory5e;

// Register System Settings
registerSystemSettings();
Expand Down Expand Up @@ -221,6 +224,10 @@ Hooks.once("setup", function() {
// Apply custom compendium styles to the SRD rules compendium.
const rules = game.packs.get("dnd5e.rules");
rules.applicationClass = applications.journal.SRDCompendium;

// Apply custom item compendium
game.packs.filter(p => p.metadata.type === "Item")
.forEach(p => p.applicationClass = applications.item.ItemCompendium5e);
});

/* --------------------------------------------- */
Expand Down
1 change: 1 addition & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@
"DND5E.Container": "Container",
"DND5E.ContainerDeleteMessage": "This container will be permanently deleted and cannot be recovered and the {count} items contained within will be moved out.",
"DND5E.ContainerDeleteContents": "Also delete all items within the container.",
"DND5E.ContainerMaxDepth": "Containers cannot be nested more than {depth} levels deep.",
"DND5E.ContainerRecursiveError": "Containers cannot contain themselves.",
"DND5E.Contents": "Contents",
"DND5E.ContextMenuActionEdit": "Edit",
Expand Down
13 changes: 4 additions & 9 deletions module/applications/actor/base-sheet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export default class ActorSheet5e extends ActorSheetMixin(ActorSheet) {

// Remove items in containers & sort remaining
context.items = context.items
.filter(i => !i.system.container)
.filter(i => !this.actor.items.has(i.system.container))
.sort((a, b) => (a.sort || 0) - (b.sort || 0));

// Temporary HP
Expand Down Expand Up @@ -968,14 +968,9 @@ export default class ActorSheet5e extends ActorSheetMixin(ActorSheet) {
items = itemsWithoutAdvancement;
}

const toCreate = [];
for ( const item of items ) {
const result = await this._onDropSingleItem(item);
if ( result ) toCreate.push(result);
}

// Create the owned items as normal
return this.actor.createEmbeddedDocuments("Item", toCreate);
// Create the owned items & contents as normal
const toCreate = await Item5e.createWithContents(items, {transformFirst: this._onDropSingleItem.bind(this)});
return Item5e.createDocuments(toCreate, {pack: this.actor.pack, parent: this.actor, keepId: true});
}

/* -------------------------------------------- */
Expand Down
2 changes: 2 additions & 0 deletions module/applications/item/_module.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export {default as ContainerSheet} from "./container-sheet.mjs";
export {default as ItemCompendium5e} from "./item-compendium.mjs";
export {default as ItemDirectory5e} from "./item-directory.mjs";
export {default as ItemSheet5e} from "./item-sheet.mjs";

export {default as AbilityUseDialog} from "./ability-use-dialog.mjs";
18 changes: 8 additions & 10 deletions module/applications/item/container-sheet.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Item5e from "../../documents/item.mjs";
import ItemSheet5e from "./item-sheet.mjs";

export default class ContainerSheet extends ItemSheet5e {
Expand Down Expand Up @@ -124,14 +125,14 @@ export default class ContainerSheet extends ItemSheet5e {
const summary = $(li.querySelector(".item-summary"));
summary.slideUp(200, () => summary.remove());
this._expanded.delete(item.id);
li.classList.remove("expanded");
} else {
const chatData = await item.getChatData({secrets: this.item.actor?.isOwner ?? false});
const summary = $(await renderTemplate("systems/dnd5e/templates/items/parts/item-summary.hbs", chatData));
$(li).append(summary.hide());
summary.slideDown(200);
this._expanded.add(item.id);
}
return;
case "use":
return item.use({}, { event });
}
Expand Down Expand Up @@ -172,9 +173,9 @@ export default class ContainerSheet extends ItemSheet5e {

/**
* Handle the dropping of Item data onto an Item Sheet.
* @param {DragEvent} event The concluding DragEvent which contains the drop data.
* @param {object} data The data transfer extracted from the event.
* @returns {Promise<Item|boolean>} The created Item object or `false` if it couldn't be created.
* @param {DragEvent} event The concluding DragEvent which contains the drop data.
* @param {object} data The data transfer extracted from the event.
* @returns {Promise<Item5e[]|boolean>} The created Item object or `false` if it couldn't be created.
* @protected
*/
async _onDropItem(event, data) {
Expand All @@ -198,12 +199,9 @@ export default class ContainerSheet extends ItemSheet5e {
return item.update({"system.container": this.item.id});
}

// Otherwise, create a new item in this context
const context = {pack: this.item.pack, parent: this.item.actor};
const itemData = foundry.utils.mergeObject(item.toObject(), {"system.container": this.item.id});
const newItem = await Item.create(itemData, context);

return newItem;
// Otherwise, create a new item & contents in this context
const toCreate = await Item5e.createWithContents([item], {container: this.item});
return Item5e.createDocuments(toCreate, {pack: this.item.pack, parent: this.item.actor, keepId: true});
}

/* -------------------------------------------- */
Expand Down
45 changes: 45 additions & 0 deletions module/applications/item/item-compendium.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Item5e from "../../documents/item.mjs";

/**
* Compendium with added support for item containers.
*/
export default class ItemCompendium5e extends Compendium {

/** @inheritdoc */
async _render(...args) {
await super._render(...args);

if ( this.collection.index ) {
if ( !this.collection._reindexing ) this.collection._reindexing = this.collection.getIndex();
await this.collection._reindexing;
items = this.collection.index;
}
for ( const item of items ) {
if ( items.has(item.system.container) ) this._element[0].querySelector(`[data-entry-id="${item._id}"]`)?.remove();
}
}

/* -------------------------------------------- */

/** @inheritdoc */
async _handleDroppedEntry(target, data) {
// Obtain the dropped Document
let item = await Item.fromDropData(data);
if ( !item ) return;

// Create item and its contents if it doesn't already exist here
if ( !this._entryAlreadyExists(item) ) {
const contents = await item.system.contents;
if ( contents?.size ) {
const toCreate = await Item5e.createWithContents([item], {transformAll: item => item.toCompendium(item)});
[item] = await Item5e.createDocuments(toCreate, {pack: this.collection.collection, keepId: true});
}
}

// Otherwise, if it is within a container, take it out
else if ( item.system.container ) await item.update({"system.container": null});

// Let parent method perform sorting
super._handleDroppedEntry(target, item.toDragData());
}
}
25 changes: 25 additions & 0 deletions module/applications/item/item-directory.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Item5e from "../../documents/item.mjs";

/**
* Items sidebar with added support for item containers.
*/
export default class ItemDirectory5e extends ItemDirectory {
/** @inheritdoc */
async _handleDroppedEntry(target, data) {
// Obtain the dropped Document
let item = await this._getDroppedEntryFromData(data);
if ( !item ) return;

// Create item and its contents if it doesn't already exist here
if ( !this._entryAlreadyExists(item) ) {
const toCreate = await Item5e.createWithContents([item]);
[item] = await Item5e.createDocuments(toCreate, {keepId: true});
}

// Otherwise, if it is within a container, take it out
else if ( item.system.container ) await item.update({"system.container": null});

// Let parent method perform sorting
super._handleDroppedEntry(target, item.toDragData());
}
}
1 change: 1 addition & 0 deletions module/data/_module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * as fields from "./fields.mjs";

export * as actor from "./actor/_module.mjs";
export * as advancement from "./advancement/_module.mjs";
export * as collection from "./collection/_module.mjs";
export * as item from "./item/_module.mjs";
export * as journal from "./journal/_module.mjs";
export * as shared from "./shared/_module.mjs";
1 change: 1 addition & 0 deletions module/data/collection/_module.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {default as Items5e} from "./items-collection.mjs";
29 changes: 29 additions & 0 deletions module/data/collection/items-collection.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Item5e from "../../documents/item.mjs";

/**
* Custom items collection to hide items in containers automatically.
*/
export default class Items5e extends Items {
/** @override */
_getVisibleTreeContents(entry) {
return this.contents.filter(c => c.visible && !this.has(c.system?.container));
}

/* -------------------------------------------- */

/** @inheritdoc */
async importFromCompendium(pack, id, updateData={}, options={}) {
const created = await super.importFromCompendium(pack, id, updateData, options);

const item = await pack.getDocument(id);
const contents = await item.system.contents;
if ( contents ) {
const toCreate = await Item5e.createWithContents(contents, {
container: created, keepId: options.keepId, transformAll: item => this.fromCompendium(item, options)
});
await Item5e.createDocuments(toCreate, {fromCompendium: true, keepId: true});
}

return created;
}
}
6 changes: 3 additions & 3 deletions module/data/item/container.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default class ContainerData extends SystemDataModel.mixin(
* @type {Collection<Item5e>|Promise<Collection<Item5e>>}
*/
get allContainedItems() {
if ( !this.parent || !this.contents.size ) return new foundry.utils.Collection();
if ( !this.parent ) return new foundry.utils.Collection();
if ( this.parent.pack ) return this.#allContainedItems();

return this.contents.reduce((collection, item) => {
Expand All @@ -107,8 +107,8 @@ export default class ContainerData extends SystemDataModel.mixin(
async #allContainedItems() {
return (await this.contents).reduce(async (promise, item) => {
const collection = await promise;
collection.add(item);
if ( item.type === "backpack" ) (await item.system.allContainedItems).forEach(i => collection.set(id.id, i));
collection.set(item.id, item);
if ( item.type === "backpack" ) (await item.system.allContainedItems).forEach(i => collection.set(i.id, i));
return collection;
}, new foundry.utils.Collection());
}
Expand Down
53 changes: 48 additions & 5 deletions module/documents/item.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ClassData from "../data/item/class.mjs";
import PhysicalItemTemplate from "../data/item/templates/physical-item.mjs";
import {d20Roll, damageRoll} from "../dice/dice.mjs";
import simplifyRollFormula from "../dice/simplify-roll-formula.mjs";
import Advancement from "./advancement/advancement.mjs";
Expand Down Expand Up @@ -2097,11 +2098,9 @@ export default class Item5e extends SystemDocumentMixin(Item) {
if ( userId !== game.user.id ) return;

// Delete a container's contents when it is deleted
const contents = await this.system.contents;
if ( contents && options.deleteContents ) {
await Item.deleteDocuments(Array.from(contents.map(i => i.id)), {
pack: this.pack, parent: this.parent, deleteContents: true
});
const contents = await this.system.allContainedItems;
if ( contents?.size && options.deleteContents ) {
await Item.deleteDocuments(Array.from(contents.map(i => i.id)), { pack: this.pack, parent: this.parent });
}

// Assign a new original class
Expand Down Expand Up @@ -2208,6 +2207,50 @@ export default class Item5e extends SystemDocumentMixin(Item) {
/* Factory Methods */
/* -------------------------------------------- */

/**
* Prepare creation data for the provided items and any items contained within them. The data created by this method
* can be passed to `createDocuments` with `keepId` always set to true to maintain links to container contents.
* @param {Item5e[]} items Items to create.
* @param {object} [context={}] Context for the item's creation.
* @param {Item5e} [context.container] Container in which to create the item.
* @param {boolean} [context.keepId=false] Should IDs be maintained?
* @param {Function} [context.transformAll] Method called on provided items and their contents.
* @param {Function} [context.transformFirst] Method called only on provided items.
* @returns {object[]} Data for items to be created.
*/
static async createWithContents(items, { container, keepId=false, transformAll, transformFirst }={}) {
let depth = 0;
if ( container ) {
depth = 1 + (await container.system.allContainers()).length;
if ( depth > PhysicalItemTemplate.MAX_DEPTH ) {
ui.notifications.warn(game.i18n.format("DND5E.ContainerMaxDepth", { depth: PhysicalItemTemplate.MAX_DEPTH }));
return;
}
}

const createItemData = async (item, containerId, depth) => {
let newItemData = transformAll ? await transformAll(item) : item;
if ( transformFirst && (depth === 0) ) newItemData = await transformFirst(newItemData);
if ( newItemData instanceof foundry.abstract.Document ) newItemData = newItemData.toObject();
else if ( !newItemData ) return;
foundry.utils.mergeObject(newItemData, {"system.container": containerId} );
if ( !keepId ) newItemData._id = foundry.utils.randomID();

const contents = await item.system.contents;
if ( contents && (depth < PhysicalItemTemplate.MAX_DEPTH) ) {
for ( const doc of contents ) await createItemData(doc, newItemData._id, depth + 1);
}

created.push(newItemData);
};

const created = [];
for ( const item of items ) await createItemData(item, container?.id, depth);
return created;
}

/* -------------------------------------------- */

/**
* Create a consumable spell scroll Item from a spell Item.
* @param {Item5e|object} spell The spell or item data to be made into a scroll
Expand Down

0 comments on commit a21e03a

Please sign in to comment.