/** * Equipment Actions Module * Handles all user interactions with the equipment system */ import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js'; import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js'; import { renderEquipment } from '../rendering/equipment.js'; import { renderUserStats } from '../rendering/userStats.js'; import { EQUIPMENT_CATEGORIES, escapeHtml } from '../equipment/constants.js'; import { i18n } from '../../core/i18n.js'; /** * Check if a given slot is currently occupied by any item * @param {string} slotId - Slot to check * @param {Array} items - Equipment items array * @returns {boolean} */ function isSlotOccupied(slotId, items) { return items.some(item => item.slot === slotId); } /** * Find the first available slot for a given equipment type * @param {string} type - Equipment type (helmet, ring, accessory, etc.) * @param {Array} items - Equipment items array * @returns {string|null} Available slot ID or null if all full */ function findAvailableSlot(type, items) { const category = EQUIPMENT_CATEGORIES[type]; if (!category) return null; return category.slots.find(slot => !isSlotOccupied(slot, items)) || null; } /** * Get the slot ID currently assigned to an item * @param {Object} item - Equipment item * @returns {string|null} */ function getItemSlot(item) { return item.slot || null; } /** * Convert old specific slot type (e.g. 'ring3') to generic category (e.g. 'ring') * @param {string} type - Equipment type * @returns {string} Generic type */ function normalizeType(type) { if (!type) return type; if (type.startsWith('ring') && type !== 'ring') return 'ring'; if (type.startsWith('accessory') && type !== 'accessory') return 'accessory'; return type; } /** * Migrate old item types in the entire items array (for v6 → v7 migration) * @param {Array} items - Equipment items array */ function migrateItemTypes(items) { for (const item of items) { if (item.type) { item.type = normalizeType(item.type); } } } /** * Generate a unique ID for equipment items * @returns {string} Unique ID */ function generateItemId() { return 'eq_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6); } /** * Updates lastGeneratedData and committedTrackerData to include current equipment state */ function updateEquipmentData() { const equipment = extensionSettings.userStats?.equipment || { items: [], slots: {} }; const currentData = lastGeneratedData.userStats || committedTrackerData.userStats; if (currentData) { const trimmed = currentData.trim(); if (trimmed.startsWith('{') || trimmed.startsWith('[')) { try { const jsonData = JSON.parse(currentData); if (jsonData && typeof jsonData === 'object') { jsonData.equipment = JSON.parse(JSON.stringify(equipment)); const updatedJSON = JSON.stringify(jsonData, null, 2); lastGeneratedData.userStats = updatedJSON; committedTrackerData.userStats = updatedJSON; return; } } catch (e) { console.warn('[RPG Equipment] Failed to parse JSON, falling back to text format:', e); } } } // Fallback: rebuild text format const stats = extensionSettings.userStats; const config = extensionSettings.trackerConfig?.userStats || {}; let text = ''; const enabledStats = config.customStats?.filter(stat => stat && stat.enabled && stat.name && stat.id) || []; for (const stat of enabledStats) { const value = stats[stat.id] !== undefined ? stats[stat.id] : 100; text += `${stat.name}: ${value}%\n`; } if (config.statusSection?.enabled) { if (config.statusSection.showMoodEmoji) { text += `${stats.mood}: `; } text += `${stats.conditions || 'None'}\n`; } // Include equipment data in fallback const equipped = equipment.items.filter(item => item.slot); if (equipped.length > 0) { text += `\nEquipment:\n`; for (const item of equipped) { const bonuses = Object.entries(item.stats || {}) .filter(([_, val]) => val > 0) .map(([key, val]) => `${key.toUpperCase()}+${val}`) .join(' '); text += `- ${item.slot}: ${item.name}${bonuses ? ' (' + bonuses + ')' : ''}\n`; } } lastGeneratedData.userStats = text.trim(); committedTrackerData.userStats = text.trim(); } /** * Shows the equipment creation modal */ export function showCreateModal() { const modalHtml = generateCreateModalHTML(); $('body').append(modalHtml); const $modal = $('#rpg-equipment-modal'); $modal.hide().fadeIn(200); $('#rpg-eq-name').focus(); initStatCheckboxes(); } /** * Shows the equipment edit modal * @param {string} itemId - ID of the item to edit */ export function showEditModal(itemId) { const equipment = extensionSettings.userStats?.equipment || { items: [], slots: {} }; const item = equipment.items.find(i => i.id === itemId); if (!item) return; const modalHtml = generateEditModalHTML(item); $('body').append(modalHtml); const $modal = $('#rpg-equipment-modal'); $modal.hide().fadeIn(200); initStatCheckboxes(); } /** * Closes the equipment modal */ export function closeModal() { $('#rpg-equipment-modal').fadeOut(150, function() { $(this).remove(); }); } /** * Generates HTML for the create modal * @returns {string} Modal HTML */ function generateCreateModalHTML() { const attributes = getAvailableAttributes(); const typeOptions = generateTypeOptions(null); const statCheckboxes = generateStatCheckboxes(attributes, null); return ` `; } /** * Generates HTML for the edit modal * @param {Object} item - The item to edit * @returns {string} Modal HTML */ function generateEditModalHTML(item) { const attributes = getAvailableAttributes(); const typeOptions = generateTypeOptions(item.type); const statCheckboxes = generateStatCheckboxes(attributes, item); return ` `; } /** * Generates type dropdown options * @param {string|null} selectedType - Currently selected type or null * @returns {string} HTML options */ function generateTypeOptions(selectedType) { const types = Object.entries(EQUIPMENT_CATEGORIES).map(([value, def]) => ({ value, label: def.slots[0] === value ? def.slots[0].charAt(0).toUpperCase() + def.slots[0].slice(1) : value.charAt(0).toUpperCase() + value.slice(1) + (def.slots.length > 1 ? ` (max ${def.maxEquipped})` : '') })); return types.map(type => { const selected = selectedType === type.value ? 'selected' : ''; return ``; }).join(''); } /** * Generates stat checkboxes HTML * @param {string[]} attributes - Available attribute IDs * @param {Object|null} item - Current item for editing or null * @returns {string} HTML for stat checkboxes */ function generateStatCheckboxes(attributes, item) { return attributes.map(attr => { const checked = item && item.stats && item.stats[attr] > 0; const val = checked ? item.stats[attr] : 1; return ` `; }).join(''); } /** * Gets available RPG attribute IDs from config * @returns {string[]} Array of attribute IDs */ function getAvailableAttributes() { const config = extensionSettings.trackerConfig?.userStats || {}; const rpgAttributes = config.rpgAttributes || []; return rpgAttributes .filter(attr => attr && attr.enabled && attr.id) .map(attr => attr.id); } /** * Initializes stat checkbox change handlers */ function initStatCheckboxes() { $('.rpg-eq-stat-check').on('change', function() { const $input = $(this).siblings('.rpg-eq-stat-value-input'); $input.prop('disabled', !$(this).prop('checked')); }); } /** * Saves the equipment item from the modal form */ export function saveEquipmentItem() { const name = $('#rpg-eq-name').val().trim(); const type = $('#rpg-eq-type').val(); const description = $('#rpg-eq-description').val().trim(); const editId = $('#rpg-equipment-modal-save').data('edit-id'); if (!name) { $('#rpg-eq-name').focus(); return; } // Collect stats const stats = {}; $('.rpg-eq-stat-check:checked').each(function() { const attr = $(this).data('attr'); const val = parseInt($(this).siblings('.rpg-eq-stat-value-input').val()) || 1; stats[attr] = Math.max(1, Math.min(99, val)); }); const equipment = extensionSettings.userStats?.equipment || { items: [], slots: {} }; // Migrate old types (ring1-ring10 -> ring, accessory1-3 -> accessory) migrateItemTypes(equipment.items); if (editId) { // Edit existing item const item = equipment.items.find(i => i.id === editId); if (item) { const wasEquipped = !!item.slot; item.name = name; item.type = normalizeType(type); item.stats = stats; item.description = description; // If type changed and item was equipped, re-equip to valid slot for new type if (wasEquipped) { const newCategory = EQUIPMENT_CATEGORIES[item.type]; if (newCategory && !newCategory.slots.includes(item.slot)) { // Old slot is invalid for new type, find a new one const newSlot = findAvailableSlot(item.type, equipment.items.filter(i => i.id !== editId)); item.slot = newSlot || null; } } } } else { // Create new item const newItem = { id: generateItemId(), name: name, type: normalizeType(type), stats: stats, description: description, slot: null }; equipment.items.push(newItem); // Auto-equip to first available slot const availableSlot = findAvailableSlot(newItem.type, equipment.items); if (availableSlot) { newItem.slot = availableSlot; } else { console.warn(`[RPG Equipment] Created "${name}" but no available slot for type "${newItem.type}". Item added to inventory unequipped.`); } } updateEquipmentData(); saveSettings(); saveChatData(); updateMessageSwipeData(); closeModal(); renderEquipment(); renderUserStats(); } /** * Equips an item to its designated slot * @param {string} itemId - ID of the item to equip */ export function equipItem(itemId) { const equipment = extensionSettings.userStats?.equipment || { items: [], slots: {} }; const item = equipment.items.find(i => i.id === itemId); if (!item) return; const type = normalizeType(item.type); item.type = type; const category = EQUIPMENT_CATEGORIES[type]; if (!category) return; const availableSlot = findAvailableSlot(type, equipment.items); if (!availableSlot) { console.warn(`[RPG Equipment] No available slot for type "${type}". All ${EQUIPMENT_CATEGORIES[type].maxEquipped} slots are full.`); return; } item.slot = availableSlot; updateEquipmentData(); saveSettings(); saveChatData(); updateMessageSwipeData(); renderEquipment(); renderUserStats(); } /** * Unequips an item from a slot * @param {string} slotId - The slot to unequip from */ export function unequipItem(slotId) { const equipment = extensionSettings.userStats?.equipment || { items: [], slots: {} }; const item = equipment.items.find(i => i.slot === slotId); if (item) { item.slot = null; } updateEquipmentData(); saveSettings(); saveChatData(); updateMessageSwipeData(); renderEquipment(); renderUserStats(); } /** * Deletes an equipment item * @param {string} itemId - ID of the item to delete */ export function deleteItem(itemId) { const equipment = extensionSettings.userStats?.equipment || { items: [], slots: {} }; // Unequip if currently equipped const item = equipment.items.find(i => i.id === itemId); if (item && item.slot) { item.slot = null; } // Remove from items array equipment.items = equipment.items.filter(i => i.id !== itemId); updateEquipmentData(); saveSettings(); saveChatData(); updateMessageSwipeData(); renderEquipment(); renderUserStats(); } /** * Calculates total equipment bonuses per attribute from currently equipped items * @returns {Object} Map of attribute ID to total bonus value */ export function getEquipmentBonuses() { const equipment = extensionSettings.userStats?.equipment || { items: [], slots: {} }; const bonuses = {}; const items = equipment.items || []; for (const item of items) { if (!item.slot || !item.stats) continue; for (const [attr, val] of Object.entries(item.stats)) { if (val > 0) { bonuses[attr] = (bonuses[attr] || 0) + val; } } } return bonuses; } /** * Initializes all event listeners for equipment interactions */ export function initEquipmentEventListeners() { // Show create modal $(document).on('click', '[data-action="show-create-modal"]', function(e) { e.preventDefault(); showCreateModal(); }); // Equip item from inventory $(document).on('click', '[data-action="equip"]', function(e) { e.preventDefault(); const itemId = $(this).data('item-id'); equipItem(itemId); }); // Unequip item from slot $(document).on('click', '[data-action="unequip"]', function(e) { e.preventDefault(); const slot = $(this).closest('.rpg-equipment-slot').data('slot'); unequipItem(slot); }); // Edit item $(document).on('click', '[data-action="edit-item"]', function(e) { e.preventDefault(); const itemId = $(this).data('item-id'); showEditModal(itemId); }); // Delete item $(document).on('click', '[data-action="delete-item"]', function(e) { e.preventDefault(); const itemId = $(this).data('item-id'); deleteItem(itemId); }); // Modal close button $(document).on('click', '#rpg-equipment-modal-close', closeModal); // Modal cancel button $(document).on('click', '#rpg-equipment-modal-cancel', closeModal); // Modal save button $(document).on('click', '#rpg-equipment-modal-save', saveEquipmentItem); // Close modal on backdrop click $(document).on('click', '#rpg-equipment-modal', function(e) { if (e.target === this) { closeModal(); } }); // Enter key to save in modal inputs $(document).on('keypress', '#rpg-eq-name', function(e) { if (e.which === 13) { e.preventDefault(); saveEquipmentItem(); } }); }