diff --git a/index.js b/index.js index 738c91e..dbc01b7 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,7 @@ import { $infoBoxContainer, $thoughtsContainer, $inventoryContainer, + $equipmentContainer, $questsContainer, $musicPlayerContainer, setExtensionSettings, @@ -38,6 +39,7 @@ import { setInfoBoxContainer, setThoughtsContainer, setInventoryContainer, + setEquipmentContainer, setQuestsContainer, setMusicPlayerContainer, clearSessionAvatarPrompts @@ -69,6 +71,7 @@ import { createThoughtPanel } from './src/systems/rendering/thoughts.js'; import { renderInventory } from './src/systems/rendering/inventory.js'; +import { renderEquipment } from './src/systems/rendering/equipment.js'; import { renderQuests } from './src/systems/rendering/quests.js'; import { renderMusicPlayer } from './src/systems/rendering/musicPlayer.js'; import { toggleSnowflakes, initSnowflakes } from './src/systems/ui/snowflakes.js'; @@ -76,6 +79,7 @@ import { toggleDynamicWeather, initWeatherEffects, updateWeatherEffect } from '. // Interaction modules import { initInventoryEventListeners } from './src/systems/interaction/inventoryActions.js'; +import { initEquipmentEventListeners } from './src/systems/interaction/equipmentActions.js'; // UI Systems modules import { @@ -313,6 +317,7 @@ async function initUI() { setInfoBoxContainer($('#rpg-info-box')); setThoughtsContainer($('#rpg-thoughts')); setInventoryContainer($('#rpg-inventory')); + setEquipmentContainer($('#rpg-equipment')); setQuestsContainer($('#rpg-quests')); setMusicPlayerContainer($('#rpg-music-player')); @@ -389,6 +394,12 @@ async function initUI() { updateSectionVisibility(); }); + $('#rpg-toggle-equipment').on('change', function() { + extensionSettings.showEquipment = $(this).prop('checked'); + saveSettings(); + updateSectionVisibility(); + }); + $('#rpg-toggle-quests').on('change', function() { extensionSettings.showQuests = $(this).prop('checked'); saveSettings(); @@ -403,6 +414,7 @@ async function initUI() { renderInfoBox(); renderThoughts(); renderInventory(); + renderEquipment(); renderQuests(); }); @@ -862,7 +874,7 @@ async function initUI() { if (lastAssistantIndex !== -1) { commitTrackerDataFromPriorMessage(lastAssistantIndex); } - await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); + await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, renderEquipment); }); // Strip widget refresh button - same functionality as main refresh button @@ -881,7 +893,7 @@ async function initUI() { if (lastAssistantIndex !== -1) { commitTrackerDataFromPriorMessage(lastAssistantIndex); } - await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); + await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, renderEquipment); }); $('#rpg-stat-bar-color-low').on('change', function() { @@ -1121,6 +1133,7 @@ async function initUI() { $('#rpg-toggle-thought-based-expressions').prop('checked', extensionSettings.enableThoughtBasedExpressions === true); $('#rpg-toggle-hide-default-expressions').prop('checked', extensionSettings.hideDefaultExpressionDisplay === true); $('#rpg-toggle-inventory').prop('checked', extensionSettings.showInventory); + $('#rpg-toggle-equipment').prop('checked', extensionSettings.showEquipment); $('#rpg-toggle-quests').prop('checked', extensionSettings.showQuests); $('#rpg-toggle-lock-icons').prop('checked', extensionSettings.showLockIcons ?? true); $('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat); @@ -1271,6 +1284,7 @@ async function initUI() { renderInfoBox(); renderThoughts(); renderInventory(); + renderEquipment(); renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); updateDiceDisplay(); @@ -1284,6 +1298,7 @@ async function initUI() { setupMobileKeyboardHandling(); setupContentEditableScrolling(); initInventoryEventListeners(); + initEquipmentEventListeners(); // Initialize chapter checkpoint UI initChapterCheckpointUI(); diff --git a/src/core/persistence.js b/src/core/persistence.js index 79e58ca..72d67ce 100644 --- a/src/core/persistence.js +++ b/src/core/persistence.js @@ -23,7 +23,7 @@ import { validateStoredInventory, cleanItemString } from '../utils/security.js'; import { migrateToV3JSON } from '../utils/jsonMigration.js'; const extensionName = 'third-party/rpg-companion-sillytavern'; -const CURRENT_SETTINGS_VERSION = 5; +const CURRENT_SETTINGS_VERSION = 7; const DEFAULT_USER_STATS = { health: 100, @@ -616,6 +616,69 @@ export function loadSettings() { settingsChanged = true; } + // Migration to version 6: Add equipment data structure + if (currentVersion < 6) { + // console.log('[RPG Companion] Migrating settings to version 6 (adding equipment)'); + if (!extensionSettings.userStats.equipment) { + extensionSettings.userStats.equipment = { + items: [], + slots: { + helmet: null, + ring1: null, + ring2: null, + ring3: null, + ring4: null, + ring5: null, + ring6: null, + ring7: null, + ring8: null, + ring9: null, + ring10: null, + necklace: null, + bodyArmor: null, + pants: null, + shoes: null, + gloves: null, + accessory1: null, + accessory2: null, + accessory3: null + } + }; + } + if (extensionSettings.showEquipment === undefined) { + extensionSettings.showEquipment = true; + } + extensionSettings.settingsVersion = 6; + settingsChanged = true; + } + + // Migration to version 7: Convert equipment types to generic categories + add item.slot + if (currentVersion < 7) { + const equipment = extensionSettings.userStats?.equipment; + if (equipment) { + const typeMap = { + ring1: 'ring', ring2: 'ring', ring3: 'ring', ring4: 'ring', ring5: 'ring', + ring6: 'ring', ring7: 'ring', ring8: 'ring', ring9: 'ring', ring10: 'ring', + accessory1: 'accessory', accessory2: 'accessory', accessory3: 'accessory' + }; + for (const item of equipment.items || []) { + if (!item.slot && equipment.slots) { + for (const [slotId, itemId] of Object.entries(equipment.slots)) { + if (itemId === item.id) { + item.slot = slotId; + break; + } + } + } + if (item.type && typeMap[item.type]) { + item.type = typeMap[item.type]; + } + } + } + extensionSettings.settingsVersion = 7; + settingsChanged = true; + } + // Normalize additive settings without introducing another schema bump. if (!extensionSettings.thoughtsInChatStyle) { extensionSettings.thoughtsInChatStyle = 'corner'; diff --git a/src/core/state.js b/src/core/state.js index c2925ef..7c4abd4 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -10,7 +10,7 @@ * Extension settings - persisted to SillyTavern settings */ export let extensionSettings = { - settingsVersion: 5, // Version number for settings migrations + settingsVersion: 6, // Version number for settings migrations enabled: true, autoUpdate: false, updateDepth: 4, // How many messages to include in the context @@ -22,6 +22,7 @@ export let extensionSettings = { enableThoughtBasedExpressions: false, hideDefaultExpressionDisplay: false, showInventory: true, // Show inventory section (v2 system) + showEquipment: true, // Show equipment section showQuests: true, // Show quests section showThoughtsInChat: true, // Show thoughts overlay in chat thoughtsInChatStyle: 'corner', // 'corner' or 'inline' @@ -123,6 +124,30 @@ export let extensionSettings = { clothing: "None", stored: {}, assets: "None" + }, + equipment: { + items: [], // Array of {id, name, type, slot, stats: {str: 2, dex: 1, ...}, description} + slots: { + helmet: null, + ring1: null, + ring2: null, + ring3: null, + ring4: null, + ring5: null, + ring6: null, + ring7: null, + ring8: null, + ring9: null, + ring10: null, + necklace: null, + bodyArmor: null, + pants: null, + shoes: null, + gloves: null, + accessory1: null, + accessory2: null, + accessory3: null + } } }, statNames: { @@ -472,6 +497,7 @@ export let $thoughtsContainer = null; export let $inventoryContainer = null; export let $questsContainer = null; export let $musicPlayerContainer = null; +export let $equipmentContainer = null; /** * State setters - provide controlled mutation of state variables @@ -564,3 +590,7 @@ export function setQuestsContainer($element) { export function setMusicPlayerContainer($element) { $musicPlayerContainer = $element; } + +export function setEquipmentContainer($element) { + $equipmentContainer = $element; +} diff --git a/src/i18n/en.json b/src/i18n/en.json index ec15696..16e3dc7 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -459,6 +459,52 @@ "global.locked": "Locked", "global.unlocked": "Unlocked", "global.confirm": "Confirm", + "global.equipment": "Equipment", + "equipment.title": "Equipment", + "equipment.createItem": "Create Equipment", + "equipment.createItemTitle": "Create Equipment", + "equipment.editItemTitle": "Edit Equipment", + "equipment.name": "Name", + "equipment.namePlaceholder": "Enter equipment name...", + "equipment.type": "Type", + "equipment.stats": "Stats", + "equipment.description": "Description", + "equipment.descriptionPlaceholder": "Enter description (optional)...", + "equipment.emptySlot": "Empty", + "equipment.unequip": "Unequip", + "equipment.equip": "Equip", + "equipment.editItem": "Edit item", + "equipment.deleteItem": "Delete item", + "equipment.inventoryTitle": "Inventory", + "equipment.slots.helmet": "Helmet", + "equipment.slots.necklace": "Necklace", + "equipment.slots.bodyArmor": "Body Armor", + "equipment.slots.gloves": "Gloves", + "equipment.slots.pants": "Pants", + "equipment.slots.shoes": "Shoes", + "equipment.slots.ring1": "Ring 1", + "equipment.slots.ring2": "Ring 2", + "equipment.slots.ring3": "Ring 3", + "equipment.slots.ring4": "Ring 4", + "equipment.slots.ring5": "Ring 5", + "equipment.slots.ring6": "Ring 6", + "equipment.slots.ring7": "Ring 7", + "equipment.slots.ring8": "Ring 8", + "equipment.slots.ring9": "Ring 9", + "equipment.slots.ring10": "Ring 10", + "equipment.slots.accessory1": "Accessory 1", + "equipment.slots.accessory2": "Accessory 2", + "equipment.slots.accessory3": "Accessory 3", + "equipment.types.helmet": "Helmet", + "equipment.types.necklace": "Necklace", + "equipment.types.bodyArmor": "Body Armor", + "equipment.types.gloves": "Gloves", + "equipment.types.pants": "Pants", + "equipment.types.shoes": "Shoes", + "equipment.types.ring": "Ring", + "equipment.types.accessory": "Accessory", + "template.settingsModal.display.showEquipment": "Show Equipment", + "template.settingsModal.display.showEquipmentNote": "Manage equipped gear and stat bonuses from items.", "inventory.addItemPlaceholder": "Enter item name...", "inventory.stored.removeLocationConfirm": "Remove \"{location}\"? This will delete all items stored there.", "userStats.clickToEdit": "Click to edit", diff --git a/src/systems/equipment/constants.js b/src/systems/equipment/constants.js new file mode 100644 index 0000000..ab514f9 --- /dev/null +++ b/src/systems/equipment/constants.js @@ -0,0 +1,41 @@ +/** + * Equipment Constants + * Shared definitions for the equipment system + */ + +/** + * Equipment category definitions: maps type to allowed slots, max equipped, and icon + */ +export const EQUIPMENT_CATEGORIES = { + helmet: { slots: ['helmet'], maxEquipped: 1, icon: 'fa-hat-cowboy-side' }, + necklace: { slots: ['necklace'], maxEquipped: 1, icon: 'fa-circle-nodes' }, + bodyArmor: { slots: ['bodyArmor'], maxEquipped: 1, icon: 'fa-vest' }, + gloves: { slots: ['gloves'], maxEquipped: 1, icon: 'fa-hand' }, + pants: { slots: ['pants'], maxEquipped: 1, icon: 'fa-socks' }, + shoes: { slots: ['shoes'], maxEquipped: 1, icon: 'fa-shoe-prints' }, + ring: { slots: ['ring1', 'ring2', 'ring3', 'ring4', 'ring5', 'ring6', 'ring7', 'ring8', 'ring9', 'ring10'], maxEquipped: 10, icon: 'fa-ring' }, + accessory: { slots: ['accessory1', 'accessory2', 'accessory3'], maxEquipped: 3, icon: 'fa-gem' } +}; + +/** + * Flat list of all slots with their category info + */ +export const SLOTS_LIST = Object.entries(EQUIPMENT_CATEGORIES).flatMap(([type, def]) => + def.slots.map(slotId => ({ + id: slotId, + type, + icon: def.icon + })) +); + +/** + * Escapes HTML special characters to prevent XSS + * @param {string} text - Text to escape + * @returns {string} Escaped text + */ +export function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index 164e9a6..755476c 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -32,6 +32,7 @@ import { renderInfoBox } from '../rendering/infoBox.js'; import { removeLocks } from './lockManager.js'; import { renderThoughts } from '../rendering/thoughts.js'; import { renderInventory } from '../rendering/inventory.js'; +import { renderEquipment } from '../rendering/equipment.js'; import { renderQuests } from '../rendering/quests.js'; import { renderMusicPlayer } from '../rendering/musicPlayer.js'; import { i18n } from '../../core/i18n.js'; @@ -356,6 +357,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough renderInfoBox(); renderThoughts(); renderInventory(); + renderEquipment(); renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); diff --git a/src/systems/integration/sillytavern.js b/src/systems/integration/sillytavern.js index 7b64734..c162242 100644 --- a/src/systems/integration/sillytavern.js +++ b/src/systems/integration/sillytavern.js @@ -50,6 +50,7 @@ import { renderUserStats } from '../rendering/userStats.js'; import { renderInfoBox } from '../rendering/infoBox.js'; import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js'; import { renderInventory } from '../rendering/inventory.js'; +import { renderEquipment } from '../rendering/equipment.js'; import { renderQuests } from '../rendering/quests.js'; import { renderMusicPlayer } from '../rendering/musicPlayer.js'; @@ -327,6 +328,7 @@ function rerenderRpgState() { renderInfoBox(); renderThoughts(); renderInventory(); + renderEquipment(); renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); updateFabWidgets(); @@ -549,6 +551,7 @@ export async function onMessageReceived(data) { renderInfoBox(); renderThoughts(); renderInventory(); + renderEquipment(); renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); @@ -770,6 +773,7 @@ export function onMessageSwiped(messageIndex) { renderInfoBox(); renderThoughts(); renderInventory(); + renderEquipment(); renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); diff --git a/src/systems/interaction/equipmentActions.js b/src/systems/interaction/equipmentActions.js new file mode 100644 index 0000000..2d3b520 --- /dev/null +++ b/src/systems/interaction/equipmentActions.js @@ -0,0 +1,578 @@ +/** + * 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; + 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; + 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(20, val)); + }); + + const equipment = extensionSettings.userStats.equipment; + + // 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; + 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; + 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; + + // 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; + 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(); + } + }); +} diff --git a/src/systems/rendering/equipment.js b/src/systems/rendering/equipment.js new file mode 100644 index 0000000..319f557 --- /dev/null +++ b/src/systems/rendering/equipment.js @@ -0,0 +1,157 @@ +/** + * Equipment Rendering Module + * Handles UI rendering for the equipment grid and item creation + */ + +import { extensionSettings, $equipmentContainer } from '../../core/state.js'; +import { i18n } from '../../core/i18n.js'; +import { EQUIPMENT_CATEGORIES, SLOTS_LIST, escapeHtml } from '../equipment/constants.js'; + +/** + * Renders a single equipment slot + * @param {Object} slotDef - Slot definition from SLOTS_LIST + * @param {Object|null} item - The equipped item or null + * @returns {string} HTML for the slot + */ +function renderSlot(slotDef, item) { + const slotId = slotDef.id; + const slotName = i18n.getTranslation(`equipment.slots.${slotId}`) || slotId.replace(/(\d+)/, ' $1'); + const equippedClass = item ? 'equipped' : ''; + + if (item) { + const statsText = Object.entries(item.stats || {}) + .filter(([_, val]) => val > 0) + .map(([key, val]) => `${escapeHtml(key.toUpperCase())}+${val}`) + .join(''); + + return ` +
+
+ + ${escapeHtml(slotName)} + +
+
${escapeHtml(item.name)}
+ ${statsText ? `
${statsText}
` : ''} + ${item.description ? `
${escapeHtml(item.description)}
` : ''} +
+ + +
+
+ `; + } + + return ` +
+
+ + ${escapeHtml(slotName)} +
+
${i18n.getTranslation('equipment.emptySlot') || 'Empty'}
+
+ `; +} + +/** + * Generates the full equipment section HTML + * @returns {string} Complete HTML for the equipment section + */ +function generateEquipmentHTML() { + const equipment = extensionSettings.userStats.equipment; + const slots = equipment.slots || {}; + const items = equipment.items || []; + + let html = '
'; + + // Header with add button + html += ` +
+

+ + ${i18n.getTranslation('equipment.title') || 'Equipment'} +

+ +
+ `; + + // Equipment grid + html += '
'; + + // Render each slot + for (const slotDef of SLOTS_LIST) { + const item = items.find(i => i.slot === slotDef.id); + html += renderSlot(slotDef, item); + } + + html += '
'; + + // Inventory list (items not currently equipped) + const unequipped = items.filter(item => !item.slot); + if (unequipped.length > 0) { + html += '
'; + html += `

${i18n.getTranslation('equipment.inventoryTitle') || 'Inventory'}

`; + html += '
'; + + for (const item of unequipped) { + const category = EQUIPMENT_CATEGORIES[item.type]; + const statsText = Object.entries(item.stats || {}) + .filter(([_, val]) => val > 0) + .map(([key, val]) => `${escapeHtml(key.toUpperCase())}+${val}`) + .join(''); + + html += ` +
+
+ + ${escapeHtml(item.name)} + ${category ? (i18n.getTranslation(`equipment.types.${item.type}`) || item.type) : escapeHtml(item.type)} +
+ ${statsText ? `
${statsText}
` : ''} +
+ + + +
+
+ `; + } + + html += '
'; + html += '
'; + } + + html += '
'; + + return html; +} + +/** + * Main equipment rendering function + * Gets data from state/settings and updates DOM directly. + */ +export function renderEquipment() { + if (!$equipmentContainer || !extensionSettings.showEquipment) { + return; + } + + const html = generateEquipmentHTML(); + $equipmentContainer.html(html); + + // Re-apply translations + i18n.applyTranslations($equipmentContainer[0]); +} diff --git a/src/systems/rendering/userStats.js b/src/systems/rendering/userStats.js index 755dd55..909a8f7 100644 --- a/src/systems/rendering/userStats.js +++ b/src/systems/rendering/userStats.js @@ -23,6 +23,7 @@ import { buildInventorySummary } from '../generation/promptBuilder.js'; import { isItemLocked, setItemLock } from '../generation/lockManager.js'; import { updateFabWidgets } from '../ui/mobile.js'; import { getStatBarColors } from '../ui/theme.js'; +import { getEquipmentBonuses } from '../interaction/equipmentActions.js'; /** * Extracts the base name (before parentheses) and converts to snake_case for use as JSON key. @@ -410,20 +411,23 @@ export function renderUserStats() { const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id); if (enabledAttributes.length > 0) { + const equipmentBonuses = getEquipmentBonuses(); html += `
- `; + `; enabledAttributes.forEach(attr => { const value = extensionSettings.classicStats[attr.id] !== undefined ? extensionSettings.classicStats[attr.id] : 10; + const bonus = equipmentBonuses[attr.id] || 0; + const bonusHtml = bonus > 0 ? ` +${bonus}` : ''; html += `
${attr.name}
- ${value} + ${value}${bonusHtml}
diff --git a/src/systems/ui/desktop.js b/src/systems/ui/desktop.js index 07e2916..edfa5f0 100644 --- a/src/systems/ui/desktop.js +++ b/src/systems/ui/desktop.js @@ -292,16 +292,18 @@ export function setupDesktopTabs() { const $infoBox = $('#rpg-info-box'); const $thoughts = $('#rpg-thoughts'); const $inventory = $('#rpg-inventory'); + const $equipment = $('#rpg-equipment'); const $quests = $('#rpg-quests'); // If no sections exist, nothing to organize - if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0 && $quests.length === 0) { + if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0 && $equipment.length === 0 && $quests.length === 0) { return; } // Build tab navigation dynamically based on enabled settings const tabButtons = []; const hasInventory = $inventory.length > 0 && extensionSettings.showInventory; + const hasEquipment = $equipment.length > 0 && extensionSettings.showEquipment; const hasQuests = $quests.length > 0 && extensionSettings.showQuests; // Status tab (always present if any status content exists) @@ -322,6 +324,16 @@ export function setupDesktopTabs() { `); } + // Equipment tab (only if enabled in settings) + if (hasEquipment) { + tabButtons.push(` + + `); + } + // Quests tab (only if enabled in settings) if (hasQuests) { tabButtons.push(` @@ -337,6 +349,7 @@ export function setupDesktopTabs() { // Create tab content containers const $statusTab = $('
'); const $inventoryTab = $('
'); + const $equipmentTab = $('
'); const $questsTab = $('
'); // Move sections into their respective tabs (detach to preserve event handlers) @@ -361,6 +374,11 @@ export function setupDesktopTabs() { // Only show if enabled (will be part of tab structure) if (hasInventory) $inventory.show(); } + if ($equipment.length > 0) { + $equipmentTab.append($equipment.detach()); + // Only show if enabled (will be part of tab structure) + if (hasEquipment) $equipment.show(); + } if ($quests.length > 0) { $questsTab.append($quests.detach()); // Only show if enabled (will be part of tab structure) @@ -378,6 +396,7 @@ export function setupDesktopTabs() { // Always append inventory and quests tabs to preserve the elements // But they'll only show if enabled (via tab button visibility) $tabsContainer.append($inventoryTab); + $tabsContainer.append($equipmentTab); $tabsContainer.append($questsTab); // Replace content box with tabs container @@ -410,6 +429,7 @@ export function removeDesktopTabs() { const $infoBox = $('#rpg-info-box').detach(); const $thoughts = $('#rpg-thoughts').detach(); const $inventory = $('#rpg-inventory').detach(); + const $equipment = $('#rpg-equipment').detach(); const $quests = $('#rpg-quests').detach(); // Remove tabs container @@ -419,16 +439,19 @@ export function removeDesktopTabs() { const $dividerStats = $('#rpg-divider-stats'); const $dividerInfo = $('#rpg-divider-info'); const $dividerThoughts = $('#rpg-divider-thoughts'); + const $dividerInventory = $('#rpg-divider-inventory'); + const $dividerEquipment = $('#rpg-divider-equipment'); // Restore original sections to content box in correct order const $contentBox = $('.rpg-content-box'); - // Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory, Quests + // Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory, Equipment, Quests if ($dividerStats.length) { $dividerStats.before($userStats); $dividerInfo.before($infoBox); $dividerThoughts.before($thoughts); - $contentBox.append($inventory); + $dividerInventory.before($inventory); + $dividerEquipment.before($equipment); $contentBox.append($quests); } else { // Fallback if dividers don't exist @@ -436,6 +459,7 @@ export function removeDesktopTabs() { $contentBox.append($infoBox); $contentBox.append($thoughts); $contentBox.append($inventory); + $contentBox.append($equipment); $contentBox.append($quests); } @@ -447,6 +471,7 @@ export function removeDesktopTabs() { } if (extensionSettings.showCharacterThoughts) $thoughts.show(); if (extensionSettings.showInventory) $inventory.show(); + if (extensionSettings.showEquipment) $equipment.show(); if (extensionSettings.showQuests) $quests.show(); $('.rpg-divider').show(); } diff --git a/src/systems/ui/layout.js b/src/systems/ui/layout.js index 6b51db8..7b204f4 100644 --- a/src/systems/ui/layout.js +++ b/src/systems/ui/layout.js @@ -317,6 +317,12 @@ export function updateSectionVisibility() { $('#rpg-quests').hide(); } + if (extensionSettings.showEquipment) { + $('#rpg-equipment').show(); + } else { + $('#rpg-equipment').hide(); + } + if ($musicPlayerContainer) { if (extensionSettings.enableSpotifyMusic) { $musicPlayerContainer.show(); @@ -328,7 +334,7 @@ export function updateSectionVisibility() { // Show/hide dividers intelligently // Divider after User Stats: shown if User Stats is visible AND at least one section after it is visible const showDividerAfterStats = extensionSettings.showUserStats && - (extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic); + (extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showEquipment || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic); if (showDividerAfterStats) { $('#rpg-divider-stats').show(); } else { @@ -337,7 +343,7 @@ export function updateSectionVisibility() { // Divider after Info Box: shown if Info Box is visible AND at least one section after it is visible const showDividerAfterInfo = extensionSettings.showInfoBox && - (extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showQuests); + (extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showEquipment || extensionSettings.showQuests); if (showDividerAfterInfo) { $('#rpg-divider-info').show(); } else { @@ -346,21 +352,29 @@ export function updateSectionVisibility() { // Divider after Thoughts: shown if Thoughts is visible AND at least one section after it is visible const showDividerAfterThoughts = extensionSettings.showCharacterThoughts && - (extensionSettings.showInventory || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic); + (extensionSettings.showInventory || extensionSettings.showEquipment || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic); if (showDividerAfterThoughts) { $('#rpg-divider-thoughts').show(); } else { $('#rpg-divider-thoughts').hide(); } - // Divider after Inventory: shown if Inventory is visible AND (Quests or Music) is visible - const showDividerAfterInventory = extensionSettings.showInventory && (extensionSettings.showQuests || extensionSettings.enableSpotifyMusic); + // Divider after Inventory: shown if Inventory is visible AND (Equipment, Quests or Music) is visible + const showDividerAfterInventory = extensionSettings.showInventory && (extensionSettings.showEquipment || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic); if (showDividerAfterInventory) { $('#rpg-divider-inventory').show(); } else { $('#rpg-divider-inventory').hide(); } + // Divider after Equipment: shown if Equipment is visible AND (Quests or Music) is visible + const showDividerAfterEquipment = extensionSettings.showEquipment && (extensionSettings.showQuests || extensionSettings.enableSpotifyMusic); + if (showDividerAfterEquipment) { + $('#rpg-divider-equipment').show(); + } else { + $('#rpg-divider-equipment').hide(); + } + // Divider after Quests: shown if Quests is visible AND Music is visible const showDividerAfterQuests = extensionSettings.showQuests && extensionSettings.enableSpotifyMusic; if (showDividerAfterQuests) { diff --git a/src/systems/ui/mobile.js b/src/systems/ui/mobile.js index 173fb41..cf47c5e 100644 --- a/src/systems/ui/mobile.js +++ b/src/systems/ui/mobile.js @@ -32,6 +32,9 @@ export function updateMobileTabLabels() { case 'inventory': translationKey = 'global.inventory'; break; + case 'equipment': + translationKey = 'equipment.title'; + break; case 'quests': translationKey = 'global.quests'; break; @@ -49,6 +52,9 @@ export function updateMobileTabLabels() { case 'inventory': fallback = 'Inventory'; break; + case 'equipment': + fallback = 'Equipment'; + break; case 'quests': fallback = 'Quests'; break; @@ -606,10 +612,11 @@ export function setupMobileTabs() { const $infoBox = $('#rpg-info-box'); const $thoughts = $('#rpg-thoughts'); const $inventory = $('#rpg-inventory'); + const $equipment = $('#rpg-equipment'); const $quests = $('#rpg-quests'); // If no sections exist, nothing to organize - if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0 && $quests.length === 0) { + if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0 && $equipment.length === 0 && $quests.length === 0) { return; } @@ -618,6 +625,7 @@ export function setupMobileTabs() { const hasStats = $userStats.length > 0; const hasInfo = $infoBox.length > 0 || $thoughts.length > 0; const hasInventory = $inventory.length > 0 && extensionSettings.showInventory; + const hasEquipment = $equipment.length > 0 && extensionSettings.showEquipment; const hasQuests = $quests.length > 0 && extensionSettings.showQuests; // Tab 1: Stats (User Stats only) @@ -632,6 +640,10 @@ export function setupMobileTabs() { if (hasInventory) { tabs.push(''); } + // Tab 3.5: Equipment + if (hasEquipment) { + tabs.push(''); + } // Tab 4: Quests if (hasQuests) { tabs.push(''); @@ -644,12 +656,14 @@ export function setupMobileTabs() { if (hasStats) firstTab = 'stats'; else if (hasInfo) firstTab = 'info'; else if (hasInventory) firstTab = 'inventory'; + else if (hasEquipment) firstTab = 'equipment'; else if (hasQuests) firstTab = 'quests'; // Create tab content wrappers const $statsTab = $('
'); const $infoTab = $('
'); const $inventoryTab = $('
'); + const $equipmentTab = $('
'); const $questsTab = $('
'); // Move sections into their respective tabs (detach to preserve event handlers) @@ -677,6 +691,12 @@ export function setupMobileTabs() { $inventory.show(); } + // Equipment tab: Equipment only + if ($equipment.length > 0) { + $equipmentTab.append($equipment.detach()); + $equipment.show(); + } + // Quests tab: Quests only if ($quests.length > 0) { $questsTab.append($quests.detach()); @@ -695,6 +715,7 @@ export function setupMobileTabs() { $mobileContainer.append($statsTab); $mobileContainer.append($infoTab); $mobileContainer.append($inventoryTab); + $mobileContainer.append($equipmentTab); $mobileContainer.append($questsTab); // Insert mobile tab structure at the beginning of content box @@ -723,6 +744,7 @@ export function removeMobileTabs() { const $infoBox = $('#rpg-info-box').detach(); const $thoughts = $('#rpg-thoughts').detach(); const $inventory = $('#rpg-inventory').detach(); + const $equipment = $('#rpg-equipment').detach(); const $quests = $('#rpg-quests').detach(); // Remove mobile tab container @@ -732,20 +754,24 @@ export function removeMobileTabs() { const $dividerStats = $('#rpg-divider-stats'); const $dividerInfo = $('#rpg-divider-info'); const $dividerThoughts = $('#rpg-divider-thoughts'); + const $dividerInventory = $('#rpg-divider-inventory'); + const $dividerEquipment = $('#rpg-divider-equipment'); // Restore original sections to content box in correct order const $contentBox = $('.rpg-content-box'); - // Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory, Quests + // Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory, Equipment, Quests if ($dividerStats.length) { $dividerStats.before($userStats); $dividerInfo.before($infoBox); $dividerThoughts.before($thoughts); - $contentBox.append($inventory); + $dividerInventory.before($inventory); + $dividerEquipment.before($equipment); $contentBox.append($quests); } else { // Fallback if dividers don't exist $contentBox.prepend($quests); + $contentBox.prepend($equipment); $contentBox.prepend($inventory); $contentBox.prepend($thoughts); $contentBox.prepend($infoBox); @@ -760,6 +786,7 @@ export function removeMobileTabs() { } if (extensionSettings.showCharacterThoughts) $thoughts.show(); if (extensionSettings.showInventory) $inventory.show(); + if (extensionSettings.showEquipment) $equipment.show(); if (extensionSettings.showQuests) $quests.show(); $('.rpg-divider').show(); } diff --git a/src/systems/ui/modals.js b/src/systems/ui/modals.js index ac07fd5..fe4cecf 100644 --- a/src/systems/ui/modals.js +++ b/src/systems/ui/modals.js @@ -22,6 +22,7 @@ import { renderInfoBox } from '../rendering/infoBox.js'; import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js'; import { renderQuests } from '../rendering/quests.js'; import { renderInventory } from '../rendering/inventory.js'; +import { renderEquipment } from '../rendering/equipment.js'; import { rollDice as rollDiceCore, clearDiceRoll as clearDiceRollCore, @@ -503,6 +504,7 @@ export function setupSettingsPopup() { updateDiceDisplayCore(); updateChatThoughts(); renderInventory(); + renderEquipment(); renderQuests(); // console.log('[RPG Companion] Cache cleared successfully'); diff --git a/style.css b/style.css index 55b6256..073ee28 100644 --- a/style.css +++ b/style.css @@ -11908,3 +11908,389 @@ body.documentstyle .rpg-inline-thoughts { line-height: 1.45; } + +/* ============================================ + Equipment Section Styles + ============================================ */ + +.rpg-equipment-section { + display: none; +} + +.rpg-equipment-container { + padding: 8px 0; +} + +.rpg-equipment-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--SmartThemeBorderColor); +} + +.rpg-equipment-header h3 { + margin: 0; + font-size: 14px; + color: var(--SmartThemeTitleColor); + display: flex; + align-items: center; + gap: 6px; +} + +.rpg-equipment-add-btn { + background: var(--SmartThemeAccentColor); + color: var(--SmartThemeBodyColor); + border: none; + border-radius: 4px; + padding: 5px 10px; + font-size: 11px; + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + transition: background-color 0.2s; +} + +.rpg-equipment-add-btn:hover { + background: var(--SmartThemeButtonColor); +} + +.rpg-equipment-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 8px; + margin-bottom: 16px; +} + +.rpg-equipment-slot { + background: rgba(0, 0, 0, 0.2); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 6px; + padding: 8px; + min-height: 70px; + transition: border-color 0.2s, background-color 0.2s; +} + +.rpg-equipment-slot.equipped { + border-color: var(--SmartThemeAccentColor); + background: rgba(0, 0, 0, 0.3); +} + +.rpg-equipment-slot-header { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 4px; + font-size: 10px; + color: var(--SmartThemeTitleColor); + opacity: 0.7; +} + +.rpg-equipment-slot-header i { + font-size: 9px; +} + +.rpg-equipment-slot-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-equipment-unequip-btn { + background: none; + border: none; + color: var(--SmartThemeBodyColor); + opacity: 0.5; + cursor: pointer; + padding: 2px; + font-size: 9px; + transition: opacity 0.2s; +} + +.rpg-equipment-unequip-btn:hover { + opacity: 1; +} + +.rpg-equipment-item-name { + font-size: 12px; + font-weight: 600; + color: var(--SmartThemeTitleColor); + margin-bottom: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-equipment-empty { + font-size: 11px; + color: var(--SmartThemeBodyColor); + opacity: 0.4; + font-style: italic; + padding: 4px 0; +} + +.rpg-equipment-stats { + display: flex; + flex-wrap: wrap; + gap: 3px; + margin: 4px 0; +} + +.rpg-eq-stat { + display: inline-flex; + align-items: center; + gap: 2px; + font-size: 9px; + background: rgba(0, 0, 0, 0.3); + padding: 1px 4px; + border-radius: 3px; +} + +.rpg-eq-stat-label { + color: var(--SmartThemeBodyColor); + opacity: 0.7; +} + +.rpg-eq-stat-value { + color: #4caf50; + font-weight: 600; +} + +.rpg-equipment-description { + font-size: 10px; + color: var(--SmartThemeBodyColor); + opacity: 0.6; + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-equipment-item-actions { + display: flex; + gap: 4px; + margin-top: 4px; + padding-top: 4px; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.rpg-equipment-edit-btn, +.rpg-equipment-delete-btn, +.rpg-equipment-equip-btn { + background: none; + border: none; + color: var(--SmartThemeBodyColor); + opacity: 0.5; + cursor: pointer; + padding: 2px 4px; + font-size: 10px; + transition: opacity 0.2s, color 0.2s; +} + +.rpg-equipment-edit-btn:hover { + opacity: 1; + color: #4fc3f7; +} + +.rpg-equipment-delete-btn:hover { + opacity: 1; + color: #f44336; +} + +.rpg-equipment-equip-btn:hover { + opacity: 1; + color: #4caf50; +} + +/* Equipment Inventory (unequipped items) */ +.rpg-equipment-inventory { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--SmartThemeBorderColor); +} + +.rpg-equipment-inventory h4 { + margin: 0 0 8px 0; + font-size: 12px; + color: var(--SmartThemeTitleColor); + opacity: 0.8; +} + +.rpg-equipment-inventory-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.rpg-equipment-inventory-item { + background: rgba(0, 0, 0, 0.2); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 6px; + padding: 8px; + transition: border-color 0.2s; +} + +.rpg-equipment-inventory-item:hover { + border-color: var(--SmartThemeAccentColor); +} + +.rpg-equipment-inventory-item-header { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 4px; +} + +.rpg-equipment-inventory-item-header i { + font-size: 10px; + opacity: 0.6; +} + +.rpg-equipment-inventory-item-name { + flex: 1; + font-size: 12px; + font-weight: 600; + color: var(--SmartThemeTitleColor); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-equipment-inventory-item-type { + font-size: 9px; + color: var(--SmartThemeBodyColor); + opacity: 0.5; + background: rgba(0, 0, 0, 0.2); + padding: 1px 5px; + border-radius: 3px; +} + +/* Equipment Modal */ +.rpg-equipment-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; +} + +.rpg-equipment-modal-content { + background: var(--SmartThemeDialogBgColor); + border-radius: 8px; + width: 90%; + max-width: 420px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); +} + +.rpg-equipment-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--SmartThemeBorderColor); +} + +.rpg-equipment-modal-header h3 { + margin: 0; + font-size: 14px; + color: var(--SmartThemeTitleColor); + display: flex; + align-items: center; + gap: 6px; +} + +.rpg-equipment-modal-body { + padding: 16px; +} + +.rpg-equipment-form-group { + margin-bottom: 12px; +} + +.rpg-equipment-form-group label { + display: block; + font-size: 11px; + color: var(--SmartThemeTitleColor); + margin-bottom: 4px; + opacity: 0.8; +} + +.rpg-equipment-form-group .rpg-input, +.rpg-equipment-form-group .rpg-select, +.rpg-equipment-form-group .rpg-textarea { + width: 100%; + padding: 6px 8px; + background: var(--SmartThemeInputBg); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 4px; + color: var(--SmartThemeBodyColor); + font-size: 12px; +} + +.rpg-equipment-form-group .rpg-textarea { + resize: vertical; +} + +.rpg-eq-stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 6px; +} + +.rpg-eq-stat-checkbox { + display: flex; + align-items: center; + gap: 3px; + font-size: 10px; + cursor: pointer; + background: rgba(0, 0, 0, 0.2); + padding: 3px 6px; + border-radius: 3px; +} + +.rpg-eq-stat-checkbox input[type="checkbox"] { + margin: 0; + cursor: pointer; +} + +.rpg-eq-stat-check-label { + color: var(--SmartThemeBodyColor); + opacity: 0.8; +} + +.rpg-eq-stat-value-input { + width: 30px; + padding: 1px 2px; + background: var(--SmartThemeInputBg); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 2px; + color: var(--SmartThemeBodyColor); + font-size: 10px; + text-align: center; +} + +.rpg-equipment-modal-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid var(--SmartThemeBorderColor); +} + +/* Equipment bonus display on RPG attributes */ +.rpg-classic-stat-bonus { + color: #4caf50; + font-size: 0.85em; + font-weight: 400; + margin-left: 2px; +} diff --git a/template.html b/template.html index 1cd1f5e..8af4c0e 100644 --- a/template.html +++ b/template.html @@ -96,6 +96,14 @@
+ +
+ +
+ + +
+
@@ -448,6 +456,15 @@ Track items carried, clothing worn, stored items, and assets. + + + Manage equipped gear and stat bonuses from items. + +