Add slotsWhenEquipped system and equipment Active Effect sync

- Add getTotalSlots() method to base-item.mjs as unified interface
- Add slotsWhenEquipped field to weapon, armor, and equipment schemas
- Implement getTotalSlots() in each item type respecting equipped state
- Update actor slot calculation to use getTotalSlots() uniformly
- Add _onUpdate hook to sync equipment effects with equipped state
- Update backpack with slotsWhenEquipped: 0 and +2 slot bonus effect

Backpack now correctly:
- Costs 1 slot when unequipped, 0 when equipped
- Grants +2 max item slots via Active Effect when equipped

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-12-17 00:34:16 -06:00
parent 77706eafe2
commit 1a36139387
7 changed files with 123 additions and 16 deletions

View File

@ -56,6 +56,15 @@ export default class ArmorData extends VagabondItemBase {
min: 0, 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) // Monetary value (in copper)
value: new fields.NumberField({ value: new fields.NumberField({
integer: true, 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() { getTotalSlots() {
return this.equipped ? this.slots : 0; if (this.equipped && this.slotsWhenEquipped !== null) {
return this.slotsWhenEquipped;
}
return this.slots || 0;
} }
/** /**

View File

@ -64,4 +64,15 @@ export default class VagabondItemBase extends foundry.abstract.TypeDataModel {
description: this.description, 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;
}
} }

View File

@ -38,6 +38,15 @@ export default class EquipmentData extends VagabondItemBase {
min: 0, 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 // Whether slots are per-item or for the whole stack
slotsPerItem: new fields.BooleanField({ initial: false }), 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. * 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 * @returns {number} Total slots used
*/ */
getTotalSlots() { 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) { if (this.slotsPerItem) {
return this.slots * this.quantity; return baseSlots * this.quantity;
} }
return this.slots; return baseSlots;
} }
/** /**

View File

@ -97,6 +97,15 @@ export default class WeaponData extends VagabondItemBase {
min: 0, 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) // Monetary value (in copper)
value: new fields.NumberField({ value: new fields.NumberField({
integer: true, 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() { getTotalSlots() {
return this.equipped ? this.slots : 0; const baseSlots =
this.equipped && this.slotsWhenEquipped !== null ? this.slotsWhenEquipped : this.slots || 0;
return baseSlots * (this.quantity || 1);
} }
/** /**

View File

@ -205,13 +205,11 @@ export default class VagabondActor extends Actor {
system.armor = totalArmor; system.armor = totalArmor;
// Calculate used item slots from inventory // Calculate used item slots from inventory
// Each item type implements getTotalSlots() with its own logic
let usedSlots = 0; let usedSlots = 0;
for (const item of this.items) { for (const item of this.items) {
// Only count items that take slots (not features, classes, etc.) if (typeof item.system.getTotalSlots === "function") {
if (["weapon", "armor", "equipment"].includes(item.type)) { usedSlots += item.system.getTotalSlots();
const slots = item.system.slots || 0;
const quantity = item.system.quantity || 1;
usedSlots += slots * quantity;
} }
} }
system.itemSlots.used = usedSlots; system.itemSlots.used = usedSlots;

View File

@ -1101,6 +1101,49 @@ export default class VagabondItem extends Item {
/* Equipment Helpers */ /* 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<void>}
*/
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. * Toggle the equipped state of this item.
* *

View File

@ -7,6 +7,7 @@
"description": "<p>Grants +2 Slots, occupies 1 Slot (0 while worn). Can only benefit from one at a time.</p>", "description": "<p>Grants +2 Slots, occupies 1 Slot (0 while worn). Can only benefit from one at a time.</p>",
"quantity": 1, "quantity": 1,
"slots": 1, "slots": 1,
"slotsWhenEquipped": 0,
"slotsPerItem": false, "slotsPerItem": false,
"value": 500, "value": 500,
"consumable": false, "consumable": false,
@ -34,7 +35,21 @@
"lore": "" "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", "_key": "!items!vagabondEquipBackpack",
"reviewed": true "reviewed": true
} }