//-----------------------------------------------------------------------------
// Inventory
//-----------------------------------------------------------------------------
/**
* @class RezInventory
* @extends RezBasicObject
* @category Elements
* @description Manages a collection of slots that can hold items.
*
* An inventory is a container system that organizes items into typed slots.
* Each slot can accept items of a specific type and may have capacity limits.
* Inventories can be owned by actors, enabling equipment systems with effects.
*
* Key features:
* - **Typed Slots**: Each slot accepts only items of a matching type
* - **Capacity**: Slots can have size limits based on item sizes
* - **Effects**: Items can apply effects to the inventory's owner when inserted
* - **Events**: Triggers events on insert/remove for items, slots, and inventory
*
* Slots are defined as references to `@slot` elements. Each slot has an `accessor`
* attribute that determines the attribute name used to store its contents
* (e.g., a slot with accessor "weapon" stores items in `weapon_contents`).
*
* **Define in Rez:**
* <pre><code>
* @inventory player_inv {
* slots: [#slot_weapon, #slot_armor]
* initial_weapon: [#item_sword]
* }
* </code></pre>
*
* @example <caption>Add an item at runtime</caption>
* const inv = $("player_inv");
* if(inv.canAddItemForSlot("slot_weapon", "item_axe").result) {
* inv.addItemToSlot("slot_weapon", "item_axe");
* }
*/
class RezInventory extends RezBasicObject {
constructor(id, attributes) {
super("inventory", id, attributes);
}
/**
* @function elementInitializer
* @memberof RezInventory
* @description called as part of the init process this creates the inital inventory slots
*/
elementInitializer() {
this.addInitialContents();
}
addInitialContents() {
const slots = this.getAttributeValue("slots");
for(const slotId of slots) {
const slot = $t(slotId, "slot", true);
const accessor = slot.getAttributeValue("accessor");
const slotInitialContentsAttrName = `initial_${accessor}`;
const initialContents = this.getAttributeValue(slotInitialContentsAttrName, []);
for(const contentId of initialContents) {
this.addItemToSlot(slotId, contentId);
}
}
}
/**
* @function addItemHolderForSlot
* @memberof RezInventory
* @param {string} slot_id
* @description add a new slot to the inventory
*/
addSlot(slotId) {
const slot = $t(slotId, "slot", true);
const attrName = `${slot.accessor}_contents`;
if(!this.hasAttribute(attrName)) {
this.setAttribute(attrName, []);
this.createStaticProperty(attrName);
}
}
/**
* @function getFirstItemForSlot
* @memberof RezInventory
* @param {string} slot_id
* @returns {string} id of first item in the slot
*/
getFirstItemForSlot(slotId) {
return this.getItemsForSlot(slotId)[0];
}
/**
* @function getItemsForSlot
* @memberof RezInventory
* @param {string} slot_id
* @returns {array} contents of the specified slot
*/
getItemsForSlot(slotId) {
const slot = this.getSlot(slotId);
return this.getAttribute(`${slot.accessor}_contents`);
}
/**
* @function slotIsOccupied
* @memberof RezInventory
* @param {string} slot_id
* @returns {boolean} true if there is at least one item in the slot
*/
slotIsOccupied(slotId) {
return this.countItemsForSlot(slotId) > 0;
}
/**
* @function getSlot
* @memberof RezInventory
* @param {string} slot_id
* @returns {object} reference to the slot with the given id or throws an Error
* if the slot is either not defined, or not part of this inventory.
*/
getSlot(slotId) {
if(!this.getAttribute("slots").has(slotId)) {
throw new Error(`Inventory |${this.id}| does not have slot |${slotId}|!`);
} else {
return $t(slotId, "slot", true);
}
}
/**
* @function setSlot
* @memberof RezInventory
* @param {string} slot_id
* @param {array} items array of item id's
*/
setSlot(slotId, itemIds) {
const slot = this.getSlot(slotId);
const attrName = `${slot.accessor}_contents`;
this.setAttribute(attrName, itemIds);
}
/**
* @function appendItemToSlot
* @memberof RezInventory
* @param {string} slot_id
* @param {string} item_id
* @description appends the given item to the given slot
*/
appendItemToSlot(slotId, itemId) {
this.getItemsForSlot(slotId).push(itemId);
}
/**
* @function appendItemsToSlot
* @memberof RezInventory
* @param {string|array} item_or_items either an item_id or array of item_id's to append to the slot
* @description add either a single item_id or an array of item_ids to the slot
*/
appendToSlot(slotId, itemOrItems) {
if(Array.isArray(itemOrItems)) {
itemOrItems.forEach((itemid) => {
this.appendItemToSlot(slotId, itemid);
});
} else {
this.appendItemToSlot(slotId, itemOrItems);
}
}
/**
* @function setItemForSlot
* @memberof RezInventory
* @param {string} item_id
* @description replaces any existing item content for the slot with this item
*/
setItemForSlot(slotId, itemId) {
this.setSlot(slotId, [itemId]);
}
/**
* @function setItemsForSlot
* @memberof RezInventory
* @param {array} items array of item ids
* @description replaces any existing item content for the slot with these items
*/
setItemsForSlot(slotId, items) {
this.setSlot(slotId, items);
}
/**
* @function countItemsInSlot
* @memberof RezInventory
* @param {string} slot_id
* @returns {integer} number of items in the given slot
*/
countItemsInSlot(slotId) {
return this.getItemsForSlot(slotId).length;
}
/**
* @function slotContainsItem
* @memberof RezInventory
* @param {string} slot_id
* @param {string} item_id
* @returns {boolean} true if the item_id is in the slot
*/
slotContainsItem(slotId, itemId) {
return this.getItemsForSlot(slotId).some((anItemId) => itemId === anItemId);
}
/**
* @function containsItem
* @memberof RezInventory
* @param {string} item_id
* @returns {string|null} slot_id of the slot containing the item, or null if no slot contains it
*/
containsItem(itemId) {
for(const slotId of this.getAttribute("slots")) {
if(this.slotContainsItem(slotId, itemId)) {
return slotId;
}
}
return undefined;
}
/**
* @function itemFitsInSlot
* @memberof RezInventory
* @param {string} slot_id
* @param {string} item_id
* @returns {boolean} true if the item will fit with any other contents of the slot
*/
itemFitsInSlot(slotId, itemId) {
// TODO: this code looks like shit
const itemToBeAdded = $(itemId);
const itemSize = itemToBeAdded.getAttributeValue("size", 0);
if(itemSize === 0) {
return true;
}
const slot = $(slotId);
if(slot.has_capacity) {
const used_capacity = this.getItemsForSlot(slotId).reduce((amount, itemId) => {
const item = $(itemId);
return amount + item.size;
}, 0);
return used_capacity + itemSize.size <= slot.capacity;
} else {
return true;
}
}
/**
* @function slotAcceptsItem
* @memberof RezInventory
* @param {string} slot_id
* @param {string} item_id
* @returns {boolean} true if the given item has a type that this slot accepts
*/
slotAcceptsItem(slotId, itemId) {
const slot = this.getSlot(slotId);
const accepts = slot.getAttributeValue("accepts");
const item = $(itemId);
const type = item.getAttributeValue("type");
return type === accepts;
}
/**
* @function canAddItemForSlot
* @memberof RezInventory
* @param {string} slot_id
* @param {string} item_id
* @returns {boolean} true if the slot accepts the item
*/
canAddItemForSlot(slotId, itemId) {
const decision = new RezDecision("canItemForSlot");
if(!this.slotAcceptsItem(slotId, itemId)) {
decision
.no("slot doesn't take this kind of item")
.setData("failed_on", "accepts");
} else if(!this.itemFitsInSlot(slotId, itemId)) {
decision.no("does not fit").setData("failed_on", "capacity");
} else if(this.owner != null) {
const actorDecision = this.owner.checkItem(this.id, slotId, itemId);
if(actorDecision.result) {
decision.yes();
} else {
decision.no(actorDecision.reason()).setData("failed_on", "actor");
}
} else {
decision.yes();
}
return decision;
}
/**
* @function canRemoveItemFromSlot
* @memberof RezInventory
* @param {string} slot_id
* @param {string} item_id
* @returns {object} RezDecision containing the result whether the item can be removed from the slot
*/
canRemoveItemFromSlot(slotId, itemId) {
// TODO: this code looks like shit
const decision = new RezDecision("canRemoveItemFromSlot");
decision.defaultYes();
const item = $(itemId);
decision.setData("inventory_id", this.id);
decision.setData("slot_id", slotId);
item.canBeRemoved(decision);
if(!decision.result) {
return decision;
}
if(this.owner == null) {
return decision;
}
this.owner.canRemoveItem(decision);
return decision;
}
/**
* @function addItemToSlot
* @memberof RezInventory
* @param {string} slot_id
* @param {string} item_id
* @description adds the given item to the given slot, notifying inventory, slot & item and applying effects
*/
addItemToSlot(slotId, itemId) {
const item = $(itemId);
// Anything can be added to an inventory provided it has an `type`
// attribute to work with the slot accepts
if(!item.hasAttribute("type")) {
throw new Error(`Attempt to add ${itemId} to inventory, which does not define a 'type'!`);
}
this.appendItemToSlot(slotId, itemId);
this.runEvent("insert", { slot_id: slotId, item_id: itemId });
const slot = $t(slotId, "slot");
slot.runEvent("insert", { inventory_id: this.id, item_id: itemId });
item.runEvent("insert", { inventory_id: this.id, slot_id: slotId});
this.applyEffects(slotId, itemId);
}
/**
* Determine whether effects should be applied to this inventory and the specified slot.
*
* @function shouldApplyEffects
* @memberof RezInventory
* @param {string} slot_id
*/
shouldApplyEffects(slotId) {
// apply_effects is defined in Rez @slot
if(this.owner) {
if(this.apply_effects) {
const slot = $t(slotId, "slot", true);
return slot.apply_effects;
} else {
return false;
}
} else {
// No owner object to apply the effect to
return false;
}
}
/**
* @function applyEffects
* @memberof RezInventory
* @param {string} slot_id
* @param {string} item_id
* @returns {boolean} whether the effect was applied
*/
applyEffects(slotId, itemId) {
if(!this.shouldApplyEffects(slotId)) {
return false;
}
const item = $(itemId);
if(!item.hasAttribute("effects")) {
// This item doesn't have any effects
return false;
}
for (const effectId of item.effects) {
const effect = $t(effectId, "effect");
effect.apply(this.owner_id, slotId, itemId);
}
return true;
}
/**
* @function removeItemForSlot
* @memberof RezInventory
* @param {string} slot_id
* @param {string} item_id
* @description removes the specified item from the specified inventory slot
*/
removeItemFromSlot(slotId, itemId) {
const contents = this.getItemsForSlot(slotId);
if(!contents.includes(itemId)) {
throw new Error(
"Attempt to remove item |" +
itemId +
"| from slot |" +
slotId +
"| on inventory |" +
this.id +
"|. No such item found!"
);
}
this.setItemsForSlot(slotId, contents.filter((id) => {
return id !== itemId;
}));
const slot = $(slotId);
slot.runEvent("remove", { inventory_id: this.id, item_id: itemId });
const item = $(itemId);
item.runEvent("remove", { inventory_id: this.id, slot_id: slotId });
this.runEvent("remove", { slot_id: slotId, item_id: itemId });
this.removeEffects(slotId, itemId);
}
/**
* @function removeEffects
* @memberof RezInventory
* @param {string} slot_id
* @param {string} item_id
*/
removeEffects(slotId, itemId) {
if(!this.shouldApplyEffects(slotId)) {
return false;
}
const item = $(itemId);
if(!item.hasAttribute("effects")) {
return false;
}
for (const effectId of item.effects) {
const effect = $t(effectId, "effect");
effect.remove(this.owner_id, slotId, itemId);
}
}
/**
* @function clearSlot
* @memberof RezInventory
* @param {string} slot_id
* @description remove all items from give slot, removes any effects granted by those items
*/
clearSlot(slotId) {
const items = this.getItemsForSlot(slotId);
items.forEach((itemId) => {
this.removeItemFromSlot(slotId, itemId);
});
}
}
window.Rez.RezInventory = RezInventory;
Source