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:
2026-07-03 11:11:23 +02:00
parent 38fb3d8c51
commit 10cfe581ac
16 changed files with 1428 additions and 17 deletions
+28 -3
View File
@@ -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();
}