/** * VagabondItem Document Class * * Extended Item document for Vagabond RPG system. * Provides document-level functionality including: * - Chat card generation for items * - Roll methods for weapons and spells * - Usage tracking for consumables and limited-use items * * Data models handle schema and base calculations. * This class handles document operations and Foundry integration. * * @extends Item */ export default class VagabondItem extends Item { /* -------------------------------------------- */ /* Data Preparation */ /* -------------------------------------------- */ /** * Prepare data for the item. * * @override */ prepareData() { super.prepareData(); } /** * Prepare derived data for the item. * * @override */ prepareDerivedData() { super.prepareDerivedData(); // Type-specific preparation switch (this.type) { case "spell": this._prepareSpellData(); break; case "weapon": this._prepareWeaponData(); break; } } /** * Prepare spell-specific derived data. * Pre-calculates mana costs for common configurations. * * @private */ _prepareSpellData() { const system = this.system; if (!system) return; // Calculate base mana cost (damage dice count) // Full formula: base dice + delivery cost + duration modifier // This will be calculated dynamically in the cast dialog } /** * Prepare weapon-specific derived data. * * @private */ _prepareWeaponData() { const system = this.system; if (!system) return; // Determine attack skill based on properties if (!system.attackSkill) { if (system.properties?.includes("finesse")) { system.attackSkill = "finesse"; } else if (system.properties?.includes("brawl")) { system.attackSkill = "brawl"; } else if (system.gripType === "ranged" || system.properties?.includes("thrown")) { system.attackSkill = "ranged"; } else { system.attackSkill = "melee"; } } } /* -------------------------------------------- */ /* Roll Data */ /* -------------------------------------------- */ /** * Get the roll data for this item. * Includes item stats and owner's roll data if applicable. * * @override * @returns {Object} Roll data object */ getRollData() { const data = { ...this.system }; // Include owner's roll data if this item belongs to an actor if (this.actor) { data.actor = this.actor.getRollData(); } return data; } /* -------------------------------------------- */ /* Chat Card Generation */ /* -------------------------------------------- */ /** * Display the item in chat as a card. * Shows item details and provides roll buttons where applicable. * * @param {Object} options - Chat message options * @returns {Promise} The created chat message */ async toChat(options = {}) { const speaker = ChatMessage.getSpeaker({ actor: this.actor }); // Build chat card content based on item type const content = await this._getChatCardContent(); const chatData = { user: game.user.id, speaker, content, flavor: this.name, ...options, }; return ChatMessage.create(chatData); } /** * Generate HTML content for the item's chat card. * * @private * @returns {Promise} HTML content */ async _getChatCardContent() { const data = { item: this, system: this.system, actor: this.actor, isOwner: this.isOwner, config: CONFIG.VAGABOND, }; // Use type-specific template if available, otherwise generic const templatePath = `systems/vagabond/templates/chat/${this.type}-card.hbs`; const genericPath = "systems/vagabond/templates/chat/item-card.hbs"; try { return await renderTemplate(templatePath, data); } catch { // Fall back to generic template try { return await renderTemplate(genericPath, data); } catch { // If no templates exist yet, return basic HTML return this._getBasicChatCardHTML(); } } } /** * Generate basic HTML for chat card when templates aren't available. * * @private * @returns {string} Basic HTML content */ _getBasicChatCardHTML() { const system = this.system; let content = `
`; content += `

${this.name}

`; content += `
`; // Type-specific details switch (this.type) { case "weapon": content += `

Damage: ${system.damage || "1d6"}

`; if (system.properties?.length) { content += `

Properties: ${system.properties.join(", ")}

`; } break; case "armor": content += `

Armor: ${system.armorValue || 0}

`; content += `

Type: ${system.armorType || "light"}

`; break; case "spell": content += `

Base Cost: ${system.baseCost || 1} Mana

`; if (system.effect) { content += `

Effect: ${system.effect}

`; } break; case "perk": if (system.prerequisites?.length) { content += `

Prerequisites:

`; } break; } // Description if (system.description) { content += `
${system.description}
`; } content += `
`; return content; } /* -------------------------------------------- */ /* Item Actions */ /* -------------------------------------------- */ /** * Use the item (attack with weapon, cast spell, use consumable). * Opens appropriate dialog based on item type. * * @param {Object} options - Usage options * @returns {Promise} */ async use(options = {}) { if (!this.actor) { ui.notifications.warn("This item must be owned by an actor to be used."); return; } switch (this.type) { case "weapon": return this._useWeapon(options); case "spell": return this._useSpell(options); case "equipment": if (this.system.consumable) { return this._useConsumable(options); } break; case "feature": if (!this.system.passive) { return this._useFeature(options); } break; } // Default: just post to chat return this.toChat(); } /** * Attack with this weapon. * * @private * @param {Object} options - Attack options * @returns {Promise} */ async _useWeapon(_options = {}) { // TODO: Implement attack roll dialog (Phase 2.6) // For now, just post to chat await this.toChat(); // Placeholder for attack roll const attackSkill = this.system.attackSkill || "melee"; ui.notifications.info(`Attack with ${this.name} using ${attackSkill} skill`); } /** * Cast this spell. * * @private * @param {Object} options - Casting options * @returns {Promise} */ async _useSpell(_options = {}) { // TODO: Implement spell casting dialog (Phase 2.8) // For now, just post to chat await this.toChat(); // Placeholder for spell cast const baseCost = this.system.baseCost || 1; ui.notifications.info(`Casting ${this.name} (Base cost: ${baseCost} Mana)`); } /** * Use a consumable item. * * @private * @param {Object} options - Usage options * @returns {Promise} */ async _useConsumable(_options = {}) { const quantity = this.system.quantity || 1; if (quantity <= 0) { ui.notifications.warn(`No ${this.name} remaining!`); return; } // Post to chat await this.toChat(); // Reduce quantity const newQuantity = quantity - 1; await this.update({ "system.quantity": newQuantity }); if (newQuantity <= 0) { ui.notifications.info(`Used last ${this.name}`); } } /** * Use an active feature. * * @private * @param {Object} options - Usage options * @returns {Promise} */ async _useFeature(_options = {}) { // Check if feature has uses if (this.system.uses) { const current = this.system.uses.value || 0; const max = this.system.uses.max || 0; if (max > 0 && current <= 0) { ui.notifications.warn(`No uses of ${this.name} remaining!`); return; } // Post to chat await this.toChat(); // Reduce uses if (max > 0) { await this.update({ "system.uses.value": current - 1 }); } } else { // No use tracking, just post to chat await this.toChat(); } } /* -------------------------------------------- */ /* Spell Helpers */ /* -------------------------------------------- */ /** * Calculate the mana cost for a spell with given options. * * @param {Object} options - Casting options * @param {number} options.extraDice - Additional damage dice * @param {string} options.delivery - Delivery type * @param {string} options.duration - Duration type * @returns {number} Total mana cost */ calculateManaCost(options = {}) { if (this.type !== "spell") return 0; const system = this.system; const { extraDice = 0, delivery = "touch" } = options; // Note: duration affects Focus mechanics but not mana cost directly // Base cost is number of damage dice const baseDice = system.baseDamageDice || 1; let cost = baseDice + extraDice; // Add delivery cost const deliveryCosts = CONFIG.VAGABOND?.spellDelivery || {}; const deliveryData = deliveryCosts[delivery]; if (deliveryData) { cost += deliveryData.cost || 0; } // Duration doesn't add cost, but Focus duration has ongoing effects return Math.max(1, cost); } /** * Get available delivery types for this spell. * * @returns {string[]} Array of valid delivery type keys */ getValidDeliveryTypes() { if (this.type !== "spell") return []; const validTypes = this.system.validDeliveryTypes || []; if (validTypes.length === 0) { // Default to touch and remote return ["touch", "remote"]; } return validTypes; } /* -------------------------------------------- */ /* Perk Helpers */ /* -------------------------------------------- */ /** * Check if this perk's prerequisites are met by an actor. * * @param {VagabondActor} actor - The actor to check against * @returns {Object} Result with met (boolean) and missing (array of unmet prereqs) */ checkPrerequisites(actor) { if (this.type !== "perk" || !actor) { return { met: true, missing: [] }; } const prereqs = this.system.prerequisites || []; const missing = []; for (const prereq of prereqs) { let met = false; switch (prereq.type) { case "stat": { // Check stat minimum const statValue = actor.system.stats?.[prereq.stat]?.value || 0; met = statValue >= (prereq.value || 0); break; } case "training": { // Check if trained in skill const skillData = actor.system.skills?.[prereq.skill]; met = skillData?.trained === true; break; } case "spell": { // Check if actor knows the spell const knownSpells = actor.getSpells?.() || []; met = knownSpells.some((s) => s.name === prereq.spellName); break; } case "perk": { // Check if actor has the prerequisite perk const perks = actor.getPerks?.() || []; met = perks.some((p) => p.name === prereq.perkName); break; } case "level": // Check minimum level met = (actor.system.level || 1) >= (prereq.value || 1); break; case "class": { // Check if actor has the class const classes = actor.getClasses?.() || []; met = classes.some((c) => c.name === prereq.className); break; } } if (!met) { missing.push(prereq); } } return { met: missing.length === 0, missing, }; } /* -------------------------------------------- */ /* Class Helpers */ /* -------------------------------------------- */ /** * Get features granted at a specific level for this class. * * @param {number} level - The level to check * @returns {Object[]} Array of feature definitions */ getFeaturesAtLevel(level) { if (this.type !== "class") return []; const progression = this.system.progression || []; const levelData = progression.find((p) => p.level === level); return levelData?.features || []; } /** * Get cumulative features up to and including a level. * * @param {number} level - The maximum level * @returns {Object[]} Array of all features up to this level */ getAllFeaturesUpToLevel(level) { if (this.type !== "class") return []; const progression = this.system.progression || []; const features = []; for (const levelData of progression) { if (levelData.level <= level) { features.push(...(levelData.features || [])); } } return features; } /* -------------------------------------------- */ /* Equipment Helpers */ /* -------------------------------------------- */ /** * Toggle the equipped state of this item. * * @returns {Promise} The updated item */ async toggleEquipped() { if (!["weapon", "armor", "equipment"].includes(this.type)) { return this; } const equipped = !this.system.equipped; return this.update({ "system.equipped": equipped }); } /** * Get the total value of this item in copper pieces. * * @returns {number} Value in copper */ getValueInCopper() { const value = this.system.value || {}; const gold = value.gold || 0; const silver = value.silver || 0; const copper = value.copper || 0; // 1 gold = 10 silver = 100 copper return gold * 100 + silver * 10 + copper; } }