- Update HTML input max attribute from 20 to 99 in generateStatCheckboxes() - Update JS clamping logic Math.min(20, val) to Math.min(99, val) in saveEquipmentItem()
579 lines
21 KiB
JavaScript
579 lines
21 KiB
JavaScript
/**
|
|
* 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 `
|
|
<div id="rpg-equipment-modal" class="rpg-equipment-modal" role="dialog" aria-modal="true">
|
|
<div class="rpg-equipment-modal-content">
|
|
<header class="rpg-equipment-modal-header">
|
|
<h3>
|
|
<i class="fa-solid fa-shield-halved"></i>
|
|
<span>${i18n.getTranslation('equipment.createItemTitle') || 'Create Equipment'}</span>
|
|
</h3>
|
|
<button id="rpg-equipment-modal-close" class="rpg-popup-close" type="button">
|
|
<i class="fa-solid fa-times"></i>
|
|
</button>
|
|
</header>
|
|
<div class="rpg-equipment-modal-body">
|
|
<div class="rpg-equipment-form-group">
|
|
<label for="rpg-eq-name">${i18n.getTranslation('equipment.name') || 'Name'}:</label>
|
|
<input type="text" id="rpg-eq-name" class="rpg-input" maxlength="50" placeholder="${i18n.getTranslation('equipment.namePlaceholder') || 'Enter equipment name...'}" />
|
|
</div>
|
|
<div class="rpg-equipment-form-group">
|
|
<label for="rpg-eq-type">${i18n.getTranslation('equipment.type') || 'Type'}:</label>
|
|
<select id="rpg-eq-type" class="rpg-select">${typeOptions}</select>
|
|
</div>
|
|
<div class="rpg-equipment-form-group">
|
|
<label>${i18n.getTranslation('equipment.stats') || 'Stats'}:</label>
|
|
<div class="rpg-eq-stats-grid">${statCheckboxes}</div>
|
|
</div>
|
|
<div class="rpg-equipment-form-group">
|
|
<label for="rpg-eq-description">${i18n.getTranslation('equipment.description') || 'Description'}:</label>
|
|
<textarea id="rpg-eq-description" class="rpg-textarea" maxlength="200" rows="2" placeholder="${i18n.getTranslation('equipment.descriptionPlaceholder') || 'Enter description (optional)...'}"></textarea>
|
|
</div>
|
|
</div>
|
|
<footer class="rpg-equipment-modal-footer">
|
|
<button id="rpg-equipment-modal-cancel" class="rpg-btn-secondary" type="button">
|
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
|
</button>
|
|
<button id="rpg-equipment-modal-save" class="rpg-btn-primary" type="button">
|
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.save') || 'Save'}
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 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 `
|
|
<div id="rpg-equipment-modal" class="rpg-equipment-modal" role="dialog" aria-modal="true">
|
|
<div class="rpg-equipment-modal-content">
|
|
<header class="rpg-equipment-modal-header">
|
|
<h3>
|
|
<i class="fa-solid fa-shield-halved"></i>
|
|
<span>${i18n.getTranslation('equipment.editItemTitle') || 'Edit Equipment'}</span>
|
|
</h3>
|
|
<button id="rpg-equipment-modal-close" class="rpg-popup-close" type="button">
|
|
<i class="fa-solid fa-times"></i>
|
|
</button>
|
|
</header>
|
|
<div class="rpg-equipment-modal-body">
|
|
<div class="rpg-equipment-form-group">
|
|
<label for="rpg-eq-name">${i18n.getTranslation('equipment.name') || 'Name'}:</label>
|
|
<input type="text" id="rpg-eq-name" class="rpg-input" maxlength="50" value="${escapeHtml(item.name)}" placeholder="${i18n.getTranslation('equipment.namePlaceholder') || 'Enter equipment name...'}" />
|
|
</div>
|
|
<div class="rpg-equipment-form-group">
|
|
<label for="rpg-eq-type">${i18n.getTranslation('equipment.type') || 'Type'}:</label>
|
|
<select id="rpg-eq-type" class="rpg-select">${typeOptions}</select>
|
|
</div>
|
|
<div class="rpg-equipment-form-group">
|
|
<label>${i18n.getTranslation('equipment.stats') || 'Stats'}:</label>
|
|
<div class="rpg-eq-stats-grid">${statCheckboxes}</div>
|
|
</div>
|
|
<div class="rpg-equipment-form-group">
|
|
<label for="rpg-eq-description">${i18n.getTranslation('equipment.description') || 'Description'}:</label>
|
|
<textarea id="rpg-eq-description" class="rpg-textarea" maxlength="200" rows="2" placeholder="${i18n.getTranslation('equipment.descriptionPlaceholder') || 'Enter description (optional)...'}">${escapeHtml(item.description || '')}</textarea>
|
|
</div>
|
|
</div>
|
|
<footer class="rpg-equipment-modal-footer">
|
|
<button id="rpg-equipment-modal-cancel" class="rpg-btn-secondary" type="button">
|
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
|
</button>
|
|
<button id="rpg-equipment-modal-save" class="rpg-btn-primary" type="button" data-edit-id="${item.id}">
|
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.save') || 'Save'}
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 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 `<option value="${type.value}" ${selected}>${type.label}</option>`;
|
|
}).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 `
|
|
<label class="rpg-eq-stat-checkbox">
|
|
<input type="checkbox" class="rpg-eq-stat-check" data-attr="${attr}" ${checked ? 'checked' : ''} />
|
|
<span class="rpg-eq-stat-check-label">${attr.toUpperCase()}</span>
|
|
<input type="number" class="rpg-eq-stat-value-input" data-attr="${attr}" value="${val}" min="1" max="99" ${!checked ? 'disabled' : ''} />
|
|
</label>
|
|
`;
|
|
}).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();
|
|
}
|
|
});
|
|
}
|