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:
parent
77706eafe2
commit
1a36139387
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user