feat: add Equipment tab with slot-type validation
Add a new Equipment tab to manage player gear and stat bonuses. Features: - 19 equipment slots across 8 categories (helmet, necklace, body armor, gloves, pants, shoes, rings, accessories) - Type-to-slot validation: each type has max equipped limits (1 helmet, 10 rings, 3 accessories, etc.) - Auto-slot assignment: equipping a ring fills the first available ring slot - Stat bonuses from equipped items display on RPG attributes (e.g. STR 10 +2) - Create/edit modal with stat checkboxes per RPG attribute - Inventory list for unequipped items Architecture: - Shared constants in src/systems/equipment/constants.js - Category-based types (Ring, Accessory) with auto-slot assignment - v7 migration converts legacy slot-specific types to generic categories - Full i18n support for all UI strings Files: - New: src/systems/equipment/constants.js - New: src/systems/interaction/equipmentActions.js - New: src/systems/rendering/equipment.js - Modified: state.js, persistence.js, template.html, index.js - Modified: userStats.js, desktop.js, mobile.js, layout.js, modals.js - Modified: apiClient.js, sillytavern.js, style.css, en.json
This commit is contained in:
@@ -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(`
|
||||
<button class="rpg-tab-btn" data-tab="equipment">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
<span data-i18n-key="equipment.title">Equipment</span>
|
||||
</button>
|
||||
`);
|
||||
}
|
||||
|
||||
// Quests tab (only if enabled in settings)
|
||||
if (hasQuests) {
|
||||
tabButtons.push(`
|
||||
@@ -337,6 +349,7 @@ export function setupDesktopTabs() {
|
||||
// Create tab content containers
|
||||
const $statusTab = $('<div class="rpg-tab-content active" data-tab-content="status"></div>');
|
||||
const $inventoryTab = $('<div class="rpg-tab-content" data-tab-content="inventory"></div>');
|
||||
const $equipmentTab = $('<div class="rpg-tab-content" data-tab-content="equipment"></div>');
|
||||
const $questsTab = $('<div class="rpg-tab-content" data-tab-content="quests"></div>');
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="inventory"><i class="fa-solid fa-box"></i><span>' + (i18n.getTranslation('global.inventory') || 'Inventory') + '</span></button>');
|
||||
}
|
||||
// Tab 3.5: Equipment
|
||||
if (hasEquipment) {
|
||||
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="equipment"><i class="fa-solid fa-shield-halved"></i><span>' + (i18n.getTranslation('equipment.title') || 'Equipment') + '</span></button>');
|
||||
}
|
||||
// Tab 4: Quests
|
||||
if (hasQuests) {
|
||||
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="quests"><i class="fa-solid fa-scroll"></i><span>' + (i18n.getTranslation('global.quests') || 'Quests') + '</span></button>');
|
||||
@@ -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 = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'stats' ? 'active' : '') + '" data-tab-content="stats"></div>');
|
||||
const $infoTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'info' ? 'active' : '') + '" data-tab-content="info"></div>');
|
||||
const $inventoryTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'inventory' ? 'active' : '') + '" data-tab-content="inventory"></div>');
|
||||
const $equipmentTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'equipment' ? 'active' : '') + '" data-tab-content="equipment"></div>');
|
||||
const $questsTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'quests' ? 'active' : '') + '" data-tab-content="quests"></div>');
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user