Files
rpg-companion-sillytavern/src/systems/rendering/inventory.js
T
Lucas 'Paperboy' Rose-Winters abd3ade30e fix(inventory): refactor renderInventory() to match other render functions
Root cause: renderInventory() signature mismatch - was expecting parameters
but called without any, resulting in empty/broken rendering.

Changes:
- Rename renderInventory(inventory, options) → generateInventoryHTML() (internal)
- Create new renderInventory() with no parameters (like renderUserStats, etc.)
- New function gets container from state, data from settings, updates DOM directly
- Import getInventoryRenderOptions() to get current tab/collapse state
- Keep updateInventoryDisplay() for use by inventoryActions module

Now matches the pattern used by all other render modules, fixing the bug where
inventory section appeared empty in the panel.
2025-10-17 15:46:13 +11:00

258 lines
9.3 KiB
JavaScript

/**
* Inventory Rendering Module
* Handles UI rendering for inventory v2 system
*/
import { extensionSettings, $inventoryContainer } from '../../core/state.js';
import { getInventoryRenderOptions } from '../interaction/inventoryActions.js';
// Type imports
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
/**
* Renders the inventory sub-tab navigation (On Person, Stored, Assets)
* @param {string} activeTab - Currently active sub-tab ('onPerson', 'stored', 'assets')
* @returns {string} HTML for sub-tab navigation
*/
export function renderInventorySubTabs(activeTab = 'onPerson') {
return `
<div class="rpg-inventory-subtabs">
<button class="rpg-inventory-subtab ${activeTab === 'onPerson' ? 'active' : ''}" data-tab="onPerson">
On Person
</button>
<button class="rpg-inventory-subtab ${activeTab === 'stored' ? 'active' : ''}" data-tab="stored">
Stored
</button>
<button class="rpg-inventory-subtab ${activeTab === 'assets' ? 'active' : ''}" data-tab="assets">
Assets
</button>
</div>
`;
}
/**
* Renders the "On Person" inventory view
* @param {string} onPersonItems - Current on-person items
* @returns {string} HTML for on-person view with edit controls
*/
export function renderOnPersonView(onPersonItems) {
const displayText = onPersonItems || 'None';
return `
<div class="rpg-inventory-section" data-section="onPerson">
<div class="rpg-inventory-header">
<h4>Items Currently Carried</h4>
<button class="rpg-inventory-edit-btn" data-action="edit-onperson" title="Edit on-person inventory">
<i class="fa-solid fa-pen"></i> Edit
</button>
</div>
<div class="rpg-inventory-content">
<div class="rpg-inventory-text">${escapeHtml(displayText)}</div>
</div>
</div>
`;
}
/**
* Renders the "Stored" inventory view with collapsible locations
* @param {Object.<string, string>} stored - Stored items by location
* @param {string[]} collapsedLocations - Array of collapsed location names
* @returns {string} HTML for stored inventory with all locations
*/
export function renderStoredView(stored, collapsedLocations = []) {
const locations = Object.keys(stored || {});
let html = `
<div class="rpg-inventory-section" data-section="stored">
<div class="rpg-inventory-header">
<h4>Storage Locations</h4>
<button class="rpg-inventory-add-btn" data-action="add-location" title="Add new storage location">
<i class="fa-solid fa-plus"></i> Add Location
</button>
</div>
<div class="rpg-inventory-content">
`;
if (locations.length === 0) {
html += `
<div class="rpg-inventory-empty">
No storage locations yet. Click "Add Location" to create one.
</div>
`;
} else {
for (const location of locations) {
const items = stored[location];
const isCollapsed = collapsedLocations.includes(location);
html += `
<div class="rpg-storage-location ${isCollapsed ? 'collapsed' : ''}" data-location="${escapeHtml(location)}">
<div class="rpg-storage-header">
<button class="rpg-storage-toggle" data-action="toggle-location" data-location="${escapeHtml(location)}">
<i class="fa-solid fa-chevron-${isCollapsed ? 'right' : 'down'}"></i>
</button>
<h5 class="rpg-storage-name">${escapeHtml(location)}</h5>
<div class="rpg-storage-actions">
<button class="rpg-inventory-edit-btn" data-action="edit-location" data-location="${escapeHtml(location)}" title="Edit items at this location">
<i class="fa-solid fa-pen"></i>
</button>
<button class="rpg-inventory-remove-btn" data-action="remove-location" data-location="${escapeHtml(location)}" title="Remove this storage location">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
<div class="rpg-storage-content" ${isCollapsed ? 'style="display:none;"' : ''}>
<div class="rpg-inventory-text">${escapeHtml(items || 'None')}</div>
</div>
</div>
`;
}
}
html += `
</div>
</div>
`;
return html;
}
/**
* Renders the "Assets" inventory view
* @param {string} assets - Current assets (vehicles, property, equipment)
* @returns {string} HTML for assets view with edit controls
*/
export function renderAssetsView(assets) {
const displayText = assets || 'None';
return `
<div class="rpg-inventory-section" data-section="assets">
<div class="rpg-inventory-header">
<h4>Vehicles, Property & Major Possessions</h4>
<button class="rpg-inventory-edit-btn" data-action="edit-assets" title="Edit assets">
<i class="fa-solid fa-pen"></i> Edit
</button>
</div>
<div class="rpg-inventory-content">
<div class="rpg-inventory-text">${escapeHtml(displayText)}</div>
<div class="rpg-inventory-hint">
<i class="fa-solid fa-info-circle"></i>
Assets include vehicles (cars, motorcycles), property (homes, apartments),
and major equipment (workshop tools, special items).
</div>
</div>
</div>
`;
}
/**
* Generates inventory HTML (internal helper)
* @param {InventoryV2} inventory - Inventory data to render
* @param {Object} options - Rendering options
* @param {string} options.activeSubTab - Currently active sub-tab ('onPerson', 'stored', 'assets')
* @param {string[]} options.collapsedLocations - Collapsed storage locations
* @returns {string} Complete HTML for inventory tab content
*/
function generateInventoryHTML(inventory, options = {}) {
const {
activeSubTab = 'onPerson',
collapsedLocations = []
} = options;
// Handle legacy v1 format - convert to v2 for display
let v2Inventory = inventory;
if (typeof inventory === 'string') {
v2Inventory = {
version: 2,
onPerson: inventory,
stored: {},
assets: 'None'
};
}
// Ensure v2 structure has all required fields
if (!v2Inventory || typeof v2Inventory !== 'object') {
v2Inventory = {
version: 2,
onPerson: 'None',
stored: {},
assets: 'None'
};
}
let html = `
<div class="rpg-inventory-container">
${renderInventorySubTabs(activeSubTab)}
<div class="rpg-inventory-views">
`;
// Render the active view
switch (activeSubTab) {
case 'onPerson':
html += renderOnPersonView(v2Inventory.onPerson);
break;
case 'stored':
html += renderStoredView(v2Inventory.stored, collapsedLocations);
break;
case 'assets':
html += renderAssetsView(v2Inventory.assets);
break;
default:
html += renderOnPersonView(v2Inventory.onPerson);
}
html += `
</div>
</div>
`;
return html;
}
/**
* Updates the inventory display in the DOM (used by inventoryActions)
* @param {string} containerId - ID of container element to update
* @param {Object} options - Rendering options (passed to generateInventoryHTML)
*/
export function updateInventoryDisplay(containerId, options = {}) {
const container = document.getElementById(containerId);
if (!container) {
console.warn(`[RPG Companion] Inventory container not found: ${containerId}`);
return;
}
const inventory = extensionSettings.userStats.inventory;
const html = generateInventoryHTML(inventory, options);
container.innerHTML = html;
}
/**
* Main inventory rendering function (matches pattern of other render functions)
* Gets data from state/settings and updates DOM directly.
* Call this after AI generation, character changes, or swipes.
*/
export function renderInventory() {
// Early return if container doesn't exist or section is hidden
if (!$inventoryContainer || !extensionSettings.showInventory) {
return;
}
// Get inventory data from settings
const inventory = extensionSettings.userStats.inventory;
// Get current render options (active tab, collapsed locations)
const options = getInventoryRenderOptions();
// Generate HTML and update DOM
const html = generateInventoryHTML(inventory, options);
$inventoryContainer.html(html);
}
/**
* Escapes HTML special characters to prevent XSS
* @param {string} text - Text to escape
* @returns {string} Escaped text
*/
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}