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(item.name)}
+ ${statsText ? `
${statsText}
` : ''}
+ ${item.description ? `
${escapeHtml(item.description)}
` : ''}
+
+
+
+
+
+ `;
+ }
+
+ return `
+
+
+
${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 += `
+
+ `;
+
+ // 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 += `
+
+
+ ${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.
+
+