diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index f7cbd81..fe33de9 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -56,6 +56,15 @@ export default class ArmorData extends VagabondItemBase { min: 0, }), + // Slot cost when equipped (null means same as slots) + // Allows magic armor to reduce slot cost when worn + slotsWhenEquipped: new fields.NumberField({ + integer: true, + initial: null, + nullable: true, + min: 0, + }), + // Monetary value (in copper) value: new fields.NumberField({ integer: true, @@ -134,12 +143,16 @@ export default class ArmorData extends VagabondItemBase { } /** - * Calculate slot cost when equipped. + * Calculate the total inventory slot cost for this armor. + * Respects slotsWhenEquipped for magic armor that reduces slot cost when worn. * - * @returns {number} Slot cost + * @returns {number} Total slots used */ - getEquippedSlots() { - return this.equipped ? this.slots : 0; + getTotalSlots() { + if (this.equipped && this.slotsWhenEquipped !== null) { + return this.slotsWhenEquipped; + } + return this.slots || 0; } /** diff --git a/module/data/item/base-item.mjs b/module/data/item/base-item.mjs index 5996e3e..c39b5b1 100644 --- a/module/data/item/base-item.mjs +++ b/module/data/item/base-item.mjs @@ -64,4 +64,15 @@ export default class VagabondItemBase extends foundry.abstract.TypeDataModel { description: this.description, }; } + + /** + * Calculate the total inventory slot cost for this item. + * Override in subclasses that have inventory slots (weapons, armor, equipment). + * Items without inventory presence (features, classes, ancestries) return 0. + * + * @returns {number} Total slots used + */ + getTotalSlots() { + return 0; + } } diff --git a/module/data/item/equipment.mjs b/module/data/item/equipment.mjs index 8c88dbc..8ff43b2 100644 --- a/module/data/item/equipment.mjs +++ b/module/data/item/equipment.mjs @@ -38,6 +38,15 @@ export default class EquipmentData extends VagabondItemBase { min: 0, }), + // Slot cost when equipped (null means same as slots) + // Used for items like backpacks that cost 0 slots when worn + slotsWhenEquipped: new fields.NumberField({ + integer: true, + initial: null, + nullable: true, + min: 0, + }), + // Whether slots are per-item or for the whole stack slotsPerItem: new fields.BooleanField({ initial: false }), @@ -125,14 +134,20 @@ export default class EquipmentData extends VagabondItemBase { /** * Calculate the total slot cost for this item stack. + * Respects slotsWhenEquipped for items like backpacks that cost + * less (or zero) slots when worn. * * @returns {number} Total slots used */ getTotalSlots() { + // Use slotsWhenEquipped if item is equipped and the field is set + const baseSlots = + this.equipped && this.slotsWhenEquipped !== null ? this.slotsWhenEquipped : this.slots; + if (this.slotsPerItem) { - return this.slots * this.quantity; + return baseSlots * this.quantity; } - return this.slots; + return baseSlots; } /** diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 50f9a42..4103bb0 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -97,6 +97,15 @@ export default class WeaponData extends VagabondItemBase { min: 0, }), + // Slot cost when equipped (null means same as slots) + // Allows magic weapons to reduce slot cost when wielded + slotsWhenEquipped: new fields.NumberField({ + integer: true, + initial: null, + nullable: true, + min: 0, + }), + // Monetary value (in copper) value: new fields.NumberField({ integer: true, @@ -229,12 +238,15 @@ export default class WeaponData extends VagabondItemBase { } /** - * Calculate the slot cost when equipped. + * Calculate the total inventory slot cost for this weapon. + * Respects slotsWhenEquipped for magic weapons that reduce slot cost when wielded. * - * @returns {number} Slot cost + * @returns {number} Total slots used */ - getEquippedSlots() { - return this.equipped ? this.slots : 0; + getTotalSlots() { + const baseSlots = + this.equipped && this.slotsWhenEquipped !== null ? this.slotsWhenEquipped : this.slots || 0; + return baseSlots * (this.quantity || 1); } /** diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 8939536..4f96b15 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -205,13 +205,11 @@ export default class VagabondActor extends Actor { system.armor = totalArmor; // Calculate used item slots from inventory + // Each item type implements getTotalSlots() with its own logic let usedSlots = 0; for (const item of this.items) { - // Only count items that take slots (not features, classes, etc.) - if (["weapon", "armor", "equipment"].includes(item.type)) { - const slots = item.system.slots || 0; - const quantity = item.system.quantity || 1; - usedSlots += slots * quantity; + if (typeof item.system.getTotalSlots === "function") { + usedSlots += item.system.getTotalSlots(); } } system.itemSlots.used = usedSlots; diff --git a/module/documents/item.mjs b/module/documents/item.mjs index cfbcd64..ac5e3e9 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -1101,6 +1101,49 @@ export default class VagabondItem extends Item { /* Equipment Helpers */ /* -------------------------------------------- */ + /** + * Handle item updates. Sync equipment Active Effects with equipped state. + * + * @override + */ + async _onUpdate(changed, options, userId) { + await super._onUpdate(changed, options, userId); + + // Only process for the updating user + if (game.user.id !== userId) return; + + // Sync equipment effects with equipped state + if ( + ["weapon", "armor", "equipment"].includes(this.type) && + changed.system?.equipped !== undefined && + this.actor + ) { + await this._syncEquippedEffects(changed.system.equipped); + } + } + + /** + * Sync Active Effects' disabled state with equipped state. + * Effects should be enabled when equipped, disabled when not. + * + * @private + * @param {boolean} equipped - The new equipped state + * @returns {Promise} + */ + async _syncEquippedEffects(equipped) { + // Find effects on the actor that originated from this item + const itemEffects = this.actor.effects.filter((e) => e.origin === this.uuid); + if (itemEffects.length === 0) return; + + // Update disabled state: disabled = !equipped + const updates = itemEffects.map((effect) => ({ + _id: effect.id, + disabled: !equipped, + })); + + await this.actor.updateEmbeddedDocuments("ActiveEffect", updates); + } + /** * Toggle the equipped state of this item. * diff --git a/packs/_source/equipment/backpack.json b/packs/_source/equipment/backpack.json index 14adccf..a60b28f 100644 --- a/packs/_source/equipment/backpack.json +++ b/packs/_source/equipment/backpack.json @@ -7,6 +7,7 @@ "description": "

Grants +2 Slots, occupies 1 Slot (0 while worn). Can only benefit from one at a time.

", "quantity": 1, "slots": 1, + "slotsWhenEquipped": 0, "slotsPerItem": false, "value": 500, "consumable": false, @@ -34,7 +35,21 @@ "lore": "" } }, - "effects": [], + "effects": [ + { + "name": "Backpack Slot Bonus", + "icon": "icons/svg/item-bag.svg", + "changes": [ + { + "key": "system.itemSlots.bonus", + "mode": 2, + "value": "2" + } + ], + "disabled": true, + "transfer": true + } + ], "_key": "!items!vagabondEquipBackpack", "reviewed": true }