fix: several issues
This commit is contained in:
@@ -74,152 +74,6 @@ function hasStructuredInfoBoxData(data) {
|
||||
isValidValue(data.time) || isValidValue(data.location) || hasEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the info box using structured JSON data
|
||||
* @param {Object} data - Structured infoBox data
|
||||
*/
|
||||
function renderStructuredInfoBox(data) {
|
||||
const config = extensionSettings.trackerConfig?.infoBox;
|
||||
const widgets = config?.widgets || {};
|
||||
|
||||
// Build widgets HTML
|
||||
let widgetsHtml = '';
|
||||
let widgetCount = 0;
|
||||
|
||||
// Date widget - skip null values
|
||||
if (widgets.date?.enabled && isValidValue(data.date)) {
|
||||
widgetCount++;
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-calendar-widget">
|
||||
<i class="fa-solid fa-calendar"></i>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.date">${i18n.getTranslation('infobox.date')}</span>
|
||||
<span class="rpg-widget-value rpg-editable" contenteditable="true" data-field="date">${data.date}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Weather widget - skip null values
|
||||
if (widgets.weather?.enabled && isValidValue(data.weather)) {
|
||||
widgetCount++;
|
||||
const { emoji, text } = separateEmojiFromText(data.weather);
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-weather-widget">
|
||||
<span class="rpg-weather-emoji rpg-editable" contenteditable="true" data-field="weatherEmoji">${emoji || '🌤️'}</span>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.weather">${i18n.getTranslation('infobox.weather')}</span>
|
||||
<span class="rpg-widget-value rpg-editable" contenteditable="true" data-field="weather">${text}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Temperature widget - skip null values
|
||||
if (widgets.temperature?.enabled && isValidValue(data.temperature)) {
|
||||
widgetCount++;
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-temperature-widget">
|
||||
<i class="fa-solid fa-temperature-half"></i>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.temperature">${i18n.getTranslation('infobox.temperature')}</span>
|
||||
<span class="rpg-widget-value rpg-editable" contenteditable="true" data-field="temperature">${data.temperature}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Time widget - skip null values
|
||||
if (widgets.time?.enabled && isValidValue(data.time)) {
|
||||
widgetCount++;
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-time-widget">
|
||||
<i class="fa-solid fa-clock"></i>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.time">${i18n.getTranslation('infobox.time')}</span>
|
||||
<span class="rpg-widget-value rpg-editable" contenteditable="true" data-field="time">${data.time}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Location widget - skip null values
|
||||
if (widgets.location?.enabled && isValidValue(data.location)) {
|
||||
widgetCount++;
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-location-widget">
|
||||
<i class="fa-solid fa-map-marker-alt"></i>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.location">${i18n.getTranslation('infobox.location')}</span>
|
||||
<span class="rpg-widget-value rpg-editable" contenteditable="true" data-field="location">${data.location}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Recent events widget - handle both string and array formats
|
||||
const recentEvents = Array.isArray(data.recentEvents)
|
||||
? data.recentEvents
|
||||
: (data.recentEvents ? [data.recentEvents] : []);
|
||||
if (widgets.recentEvents?.enabled && recentEvents.length > 0) {
|
||||
widgetCount++;
|
||||
const eventsHtml = recentEvents.map((event, i) =>
|
||||
`<li class="rpg-event-item rpg-editable" contenteditable="true" data-field="recentEvents" data-index="${i}">${event}</li>`
|
||||
).join('');
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-events-widget rpg-widget-wide">
|
||||
<i class="fa-solid fa-scroll"></i>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.recentEvents">${i18n.getTranslation('infobox.recentEvents')}</span>
|
||||
<ul class="rpg-events-list">${eventsHtml}</ul>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Determine layout class based on widget count
|
||||
const layoutClass = widgetCount <= 2 ? 'rpg-dashboard-row-1' :
|
||||
widgetCount <= 4 ? 'rpg-dashboard-row-2' : 'rpg-dashboard-row-3';
|
||||
|
||||
const html = `<div class="rpg-dashboard ${layoutClass}">${widgetsHtml}</div>`;
|
||||
|
||||
$infoBoxContainer.html(html);
|
||||
|
||||
// Remove updating animation
|
||||
if (extensionSettings.enableAnimations) {
|
||||
setTimeout(() => $infoBoxContainer.removeClass('rpg-content-updating'), 300);
|
||||
}
|
||||
|
||||
// Setup event listeners for editable fields
|
||||
setupStructuredInfoBoxEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for structured info box editing
|
||||
*/
|
||||
function setupStructuredInfoBoxEventListeners() {
|
||||
$infoBoxContainer.off('blur', '.rpg-editable').on('blur', '.rpg-editable', function() {
|
||||
const $this = $(this);
|
||||
const field = $this.data('field');
|
||||
const index = $this.data('index');
|
||||
const newValue = $this.text().trim();
|
||||
|
||||
if (!extensionSettings.infoBoxData) {
|
||||
extensionSettings.infoBoxData = {};
|
||||
}
|
||||
|
||||
if (field === 'recentEvents' && index !== undefined) {
|
||||
if (!extensionSettings.infoBoxData.recentEvents) {
|
||||
extensionSettings.infoBoxData.recentEvents = [];
|
||||
}
|
||||
extensionSettings.infoBoxData.recentEvents[index] = newValue;
|
||||
} else if (field === 'weatherEmoji') {
|
||||
// Combine emoji with existing weather text
|
||||
const currentWeather = extensionSettings.infoBoxData.weather || '';
|
||||
const { text } = separateEmojiFromText(currentWeather);
|
||||
extensionSettings.infoBoxData.weather = newValue + ' ' + text;
|
||||
} else {
|
||||
extensionSettings.infoBoxData[field] = newValue;
|
||||
}
|
||||
|
||||
saveChatData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the info box as a visual dashboard with calendar, weather, temperature, clock, and map widgets.
|
||||
* Includes event listeners for editable fields.
|
||||
@@ -651,9 +505,21 @@ export function renderInfoBox() {
|
||||
|
||||
// Row 3: Recent Events widget (notebook style) - show if enabled
|
||||
if (config?.widgets?.recentEvents?.enabled) {
|
||||
// Parse Recent Events from infoBox string
|
||||
// Get Recent Events from structured data (JSON) or text format
|
||||
let recentEvents = [];
|
||||
if (committedTrackerData.infoBox) {
|
||||
|
||||
// First check structured infoBoxData (from JSON parsing)
|
||||
if (extensionSettings.infoBoxData?.recentEvents) {
|
||||
const events = extensionSettings.infoBoxData.recentEvents;
|
||||
if (Array.isArray(events)) {
|
||||
recentEvents = events.filter(e => e && e !== 'null');
|
||||
} else if (typeof events === 'string' && events !== 'null') {
|
||||
recentEvents = [events];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to text format from committedTrackerData
|
||||
if (recentEvents.length === 0 && committedTrackerData.infoBox) {
|
||||
const recentEventsLine = committedTrackerData.infoBox.split('\n').find(line => line.startsWith('Recent Events:'));
|
||||
if (recentEventsLine) {
|
||||
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
|
||||
|
||||
@@ -64,7 +64,7 @@ export function renderInventorySubTabs(activeTab = 'onPerson') {
|
||||
|
||||
/**
|
||||
* Gets the description for an item from structured inventory data
|
||||
* @param {string} field - Field type ('onPerson', 'stored', 'assets')
|
||||
* @param {string} field - Field type ('onPerson', 'stored', 'assets', 'simplified')
|
||||
* @param {number} index - Item index
|
||||
* @param {string} [location] - Location name for stored items
|
||||
* @returns {string} Item description or empty string
|
||||
@@ -80,6 +80,8 @@ function getItemDescription(field, index, location = null) {
|
||||
items = inv3.assets;
|
||||
} else if (field === 'stored' && location) {
|
||||
items = inv3.stored?.[location];
|
||||
} else if (field === 'simplified') {
|
||||
items = inv3.simplified;
|
||||
}
|
||||
|
||||
if (!items || !Array.isArray(items) || !items[index]) return '';
|
||||
@@ -544,27 +546,39 @@ export function renderSimplifiedInventoryView(itemsString, viewMode = 'list') {
|
||||
itemsHtml = `<div class="rpg-inventory-empty" data-i18n-key="inventory.simplified.empty">${i18n.getTranslation('inventory.simplified.empty')}</div>`;
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
// Grid view: card-style items (same as onPerson)
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('simplified', index);
|
||||
return `
|
||||
<div class="rpg-item-card ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="simplified" data-index="${index}">
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="simplified" data-index="${index}" title="${i18n.getTranslation('inventory.simplified.removeTitle')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<div class="rpg-item-desc-row">
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(desc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => `
|
||||
// List view: full-width rows (same as onPerson)
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('simplified', index);
|
||||
return `
|
||||
<div class="rpg-item-row ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="simplified" data-index="${index}">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="simplified" data-index="${index}" title="${i18n.getTranslation('inventory.simplified.removeTitle')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<div class="rpg-item-main-row">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="simplified" data-index="${index}" title="${i18n.getTranslation('inventory.simplified.removeTitle')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-item-desc-row">
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(desc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,169 +624,6 @@ export function renderSimplifiedInventoryView(itemsString, viewMode = 'list') {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single structured item (with name + description)
|
||||
* @param {Object} item - Item object with name, description, grantsSkill
|
||||
* @param {string} field - Field type ('onPerson', 'stored', 'assets')
|
||||
* @param {number} index - Item index
|
||||
* @param {string} viewMode - 'list' or 'grid'
|
||||
* @param {string} [location] - Location name for stored items
|
||||
* @returns {string} HTML for the item
|
||||
*/
|
||||
function renderStructuredItem(item, field, index, viewMode, location = null) {
|
||||
// Normalize item - handle both string and object formats
|
||||
const normalizedItem = typeof item === 'string'
|
||||
? { name: item, description: '' }
|
||||
: { name: item?.name || 'Unknown', description: item?.description || '', grantsSkill: item?.grantsSkill };
|
||||
|
||||
const hasSkillLink = normalizedItem.grantsSkill || itemHasLinkedSkills(normalizedItem.name);
|
||||
const skillLinkHtml = hasSkillLink ? getSkillLinkIndicator(normalizedItem.name) : '';
|
||||
const grantsBadge = normalizedItem.grantsSkill
|
||||
? `<span class="rpg-item-grants-badge" title="Grants: ${escapeHtml(normalizedItem.grantsSkill)}"><i class="fa-solid fa-star"></i></span>`
|
||||
: '';
|
||||
|
||||
const locationAttr = location ? `data-location="${escapeHtml(location)}"` : '';
|
||||
|
||||
if (viewMode === 'grid') {
|
||||
return `
|
||||
<div class="rpg-item-card ${hasSkillLink ? 'rpg-has-skill-link' : ''}" data-field="${field}" data-index="${index}" ${locationAttr}>
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="${field}" data-index="${index}" ${locationAttr} title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<div class="rpg-item-content">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="${field}" data-index="${index}" ${locationAttr} title="Click to edit name">${escapeHtml(normalizedItem.name)}</span>
|
||||
${grantsBadge}
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="${field}" data-index="${index}" data-prop="description" ${locationAttr} title="Click to edit description">${escapeHtml(normalizedItem.description)}</span>
|
||||
</div>
|
||||
${skillLinkHtml}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="rpg-item-row ${hasSkillLink ? 'rpg-has-skill-link' : ''}" data-field="${field}" data-index="${index}" ${locationAttr}>
|
||||
<div class="rpg-item-info">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="${field}" data-index="${index}" ${locationAttr} title="Click to edit name">${escapeHtml(normalizedItem.name)}</span>
|
||||
${grantsBadge}
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="${field}" data-index="${index}" data-prop="description" ${locationAttr} title="Click to edit description">${escapeHtml(normalizedItem.description)}</span>
|
||||
</div>
|
||||
${skillLinkHtml}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="${field}" data-index="${index}" ${locationAttr} title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders structured inventory (v3 format with name + description)
|
||||
* @param {Object} inventoryV3 - Structured inventory data
|
||||
* @param {Object} options - Render options
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
function renderStructuredInventory(inventoryV3, options) {
|
||||
const { activeTab = 'onPerson' } = options;
|
||||
const viewModes = extensionSettings.inventoryViewModes || {};
|
||||
|
||||
// Sub-tabs
|
||||
let html = renderInventorySubTabs(activeTab);
|
||||
|
||||
// On Person tab
|
||||
const onPersonMode = viewModes.onPerson || 'list';
|
||||
const onPersonItems = inventoryV3.onPerson || [];
|
||||
let onPersonHtml = onPersonItems.length === 0
|
||||
? `<div class="rpg-inventory-empty">${i18n.getTranslation('inventory.onPerson.empty')}</div>`
|
||||
: onPersonItems.map((item, i) => renderStructuredItem(item, 'onPerson', i, onPersonMode)).join('');
|
||||
|
||||
html += `
|
||||
<div class="rpg-inventory-tab-content ${activeTab === 'onPerson' ? 'active' : ''}" data-tab="onPerson">
|
||||
<div class="rpg-inventory-header">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${onPersonMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${onPersonMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="onPerson" title="Add new item">
|
||||
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.onPerson.addItemButton')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-item-list rpg-item-${onPersonMode}-view">${onPersonHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Stored tab
|
||||
const storedMode = viewModes.stored || 'list';
|
||||
const stored = inventoryV3.stored || {};
|
||||
let storedHtml = '';
|
||||
|
||||
for (const [location, items] of Object.entries(stored)) {
|
||||
const locationItems = items.map((item, i) => renderStructuredItem(item, 'stored', i, storedMode, location)).join('');
|
||||
storedHtml += `
|
||||
<div class="rpg-storage-location" data-location="${escapeHtml(location)}">
|
||||
<div class="rpg-location-header">
|
||||
<span class="rpg-location-name">${escapeHtml(location)}</span>
|
||||
<span class="rpg-location-count">(${items.length})</span>
|
||||
</div>
|
||||
<div class="rpg-item-list rpg-item-${storedMode}-view">${locationItems}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (Object.keys(stored).length === 0) {
|
||||
storedHtml = `<div class="rpg-inventory-empty">${i18n.getTranslation('inventory.stored.empty')}</div>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="rpg-inventory-tab-content ${activeTab === 'stored' ? 'active' : ''}" data-tab="stored">
|
||||
<div class="rpg-inventory-header">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${storedMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${storedMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-location" title="Add storage location">
|
||||
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.stored.addLocationButton')}
|
||||
</button>
|
||||
</div>
|
||||
${storedHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Assets tab
|
||||
const assetsMode = viewModes.assets || 'list';
|
||||
const assets = inventoryV3.assets || [];
|
||||
let assetsHtml = assets.length === 0
|
||||
? `<div class="rpg-inventory-empty">${i18n.getTranslation('inventory.assets.empty')}</div>`
|
||||
: assets.map((item, i) => renderStructuredItem(item, 'assets', i, assetsMode)).join('');
|
||||
|
||||
html += `
|
||||
<div class="rpg-inventory-tab-content ${activeTab === 'assets' ? 'active' : ''}" data-tab="assets">
|
||||
<div class="rpg-inventory-header">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${assetsMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${assetsMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="assets" title="Add new asset">
|
||||
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.assets.addAssetButton')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-item-list rpg-item-${assetsMode}-view">${assetsHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `<div class="rpg-inventory-container rpg-structured">${html}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we have structured inventory data (v3 format)
|
||||
* @returns {boolean}
|
||||
@@ -782,7 +633,8 @@ function hasStructuredInventory() {
|
||||
return inv && (
|
||||
(inv.onPerson && inv.onPerson.length > 0) ||
|
||||
(inv.assets && inv.assets.length > 0) ||
|
||||
(inv.stored && Object.keys(inv.stored).length > 0)
|
||||
(inv.stored && Object.keys(inv.stored).length > 0) ||
|
||||
(inv.simplified && inv.simplified.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -815,7 +667,9 @@ export function renderInventory() {
|
||||
stored: Object.fromEntries(
|
||||
Object.entries(inv.stored || {}).map(([k, v]) => [k, itemsToString(v)])
|
||||
),
|
||||
assets: itemsToString(inv.assets)
|
||||
assets: itemsToString(inv.assets),
|
||||
// For simplified mode
|
||||
items: itemsToString(inv.simplified)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,11 @@ function serializeItems(items) {
|
||||
* @returns {string[]} Array of skill category names
|
||||
*/
|
||||
export function getSkillCategories() {
|
||||
return extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
const categories = extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
// Handle both old format (string array) and new format (object array)
|
||||
return categories
|
||||
.filter(cat => typeof cat === 'string' || cat.enabled !== false)
|
||||
.map(cat => typeof cat === 'string' ? cat : cat.name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,30 +315,93 @@ export function unlinkAbility(skillName, abilityName) {
|
||||
|
||||
/**
|
||||
* Gets all skill abilities linked to a specific inventory item
|
||||
* Checks both manual skillAbilityLinks and structured skillsV2 with grantedBy
|
||||
* @param {string} itemName - The inventory item name
|
||||
* @returns {Array<{skillName: string, abilityName: string}>} Array of linked abilities
|
||||
*/
|
||||
export function getAbilitiesLinkedToItem(itemName) {
|
||||
if (!extensionSettings.skillAbilityLinks || !itemName) return [];
|
||||
if (!itemName) return [];
|
||||
const linked = [];
|
||||
const normalizedItemName = itemName.toLowerCase().trim();
|
||||
for (const [key, linkedItem] of Object.entries(extensionSettings.skillAbilityLinks)) {
|
||||
// Case-insensitive comparison
|
||||
if (linkedItem && linkedItem.toLowerCase().trim() === normalizedItemName) {
|
||||
const [skillName, abilityName] = key.split('::');
|
||||
linked.push({ skillName, abilityName });
|
||||
|
||||
// Check manual skillAbilityLinks
|
||||
if (extensionSettings.skillAbilityLinks) {
|
||||
for (const [key, linkedItem] of Object.entries(extensionSettings.skillAbilityLinks)) {
|
||||
// Case-insensitive comparison
|
||||
if (linkedItem && linkedItem.toLowerCase().trim() === normalizedItemName) {
|
||||
const [skillName, abilityName] = key.split('::');
|
||||
linked.push({ skillName, abilityName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check structured skillsV2 for abilities with grantedBy matching this item
|
||||
const skillsV2 = extensionSettings.skillsV2;
|
||||
if (skillsV2 && typeof skillsV2 === 'object') {
|
||||
for (const [skillName, abilities] of Object.entries(skillsV2)) {
|
||||
if (!Array.isArray(abilities)) continue;
|
||||
for (const ability of abilities) {
|
||||
if (!ability || typeof ability !== 'object') continue;
|
||||
const grantedBy = (ability.grantedBy || '').toLowerCase().trim();
|
||||
if (grantedBy === normalizedItemName) {
|
||||
// Avoid duplicates
|
||||
const exists = linked.some(l => l.skillName === skillName && l.abilityName === ability.name);
|
||||
if (!exists) {
|
||||
linked.push({ skillName, abilityName: ability.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return linked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an inventory item has any linked skills
|
||||
* Checks both manual skillAbilityLinks and structured grantsSkill property
|
||||
* @param {string} itemName - The inventory item name
|
||||
* @returns {boolean} True if item has linked skills
|
||||
*/
|
||||
export function itemHasLinkedSkills(itemName) {
|
||||
return getAbilitiesLinkedToItem(itemName).length > 0;
|
||||
// Check manual links first
|
||||
if (getAbilitiesLinkedToItem(itemName).length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check structured inventory for grantsSkill property
|
||||
const inv = extensionSettings.inventoryV3;
|
||||
if (!inv || !itemName) return false;
|
||||
|
||||
const normalizedName = itemName.toLowerCase().trim();
|
||||
|
||||
// Helper to check if an item array contains the item with grantsSkill
|
||||
const checkItems = (items) => {
|
||||
if (!Array.isArray(items)) return false;
|
||||
return items.some(item => {
|
||||
if (!item || typeof item !== 'object') return false;
|
||||
const name = (item.name || '').toLowerCase().trim();
|
||||
return name === normalizedName && item.grantsSkill;
|
||||
});
|
||||
};
|
||||
|
||||
// Check onPerson
|
||||
if (checkItems(inv.onPerson)) return true;
|
||||
|
||||
// Check simplified
|
||||
if (checkItems(inv.simplified)) return true;
|
||||
|
||||
// Check assets
|
||||
if (checkItems(inv.assets)) return true;
|
||||
|
||||
// Check stored locations
|
||||
if (inv.stored && typeof inv.stored === 'object') {
|
||||
for (const items of Object.values(inv.stored)) {
|
||||
if (checkItems(items)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -101,196 +101,6 @@ function namesMatch(cardName, aiName) {
|
||||
return wordBoundary.test(aiCore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders character thoughts (Present Characters) panel.
|
||||
* Displays character cards with avatars, relationship badges, and traits.
|
||||
* Includes event listeners for editable character fields.
|
||||
*/
|
||||
/**
|
||||
* Converts structured character data to the internal format used by the renderer
|
||||
* @param {Array} charactersData - Array of structured character objects
|
||||
* @param {Object} config - Tracker configuration
|
||||
* @returns {Array} Array of character objects in the format expected by the renderer
|
||||
*/
|
||||
function convertStructuredCharactersToFormat(charactersData, config) {
|
||||
if (!charactersData || !Array.isArray(charactersData) || charactersData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enabledFields = config?.customFields?.filter(f => f && f.enabled && f.name) || [];
|
||||
const enabledCharStats = config?.characterStats?.enabled && config?.characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
const thoughtsFieldName = config?.thoughts?.name || 'Thoughts';
|
||||
|
||||
return charactersData.map(char => {
|
||||
const result = {
|
||||
name: char.name || 'Unknown',
|
||||
emoji: char.emoji || '😶',
|
||||
fields: {},
|
||||
relationship: char.relationship || null,
|
||||
stats: {},
|
||||
thoughts: char.thoughts || ''
|
||||
};
|
||||
|
||||
// Map custom fields - check both top-level and nested in char.fields
|
||||
const charFields = char.fields || {};
|
||||
enabledFields.forEach(field => {
|
||||
const fieldId = field.id || field.name.toLowerCase().replace(/\s+/g, '_');
|
||||
// First check char.fields (LLM format), then check top-level
|
||||
if (charFields[field.name] !== undefined) {
|
||||
result.fields[field.name] = charFields[field.name];
|
||||
} else if (charFields[fieldId] !== undefined) {
|
||||
result.fields[field.name] = charFields[fieldId];
|
||||
} else if (char[fieldId] !== undefined) {
|
||||
result.fields[field.name] = char[fieldId];
|
||||
} else if (char[field.name] !== undefined) {
|
||||
result.fields[field.name] = char[field.name];
|
||||
}
|
||||
});
|
||||
|
||||
// Map character stats - check both nested and top-level
|
||||
const charStats = char.stats || {};
|
||||
if (enabledCharStats.length > 0) {
|
||||
enabledCharStats.forEach(stat => {
|
||||
const statId = stat.id || stat.name.toLowerCase().replace(/\s+/g, '_');
|
||||
if (charStats[stat.name] !== undefined) {
|
||||
result.stats[stat.name] = charStats[stat.name];
|
||||
} else if (charStats[statId] !== undefined) {
|
||||
result.stats[stat.name] = charStats[statId];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Also add description if present
|
||||
if (char.description) {
|
||||
result.description = char.description;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders characters using structured data format
|
||||
* @param {Array} characters - Parsed character data
|
||||
* @param {Object} config - Tracker config
|
||||
* @param {Array} enabledFields - Enabled custom fields
|
||||
* @param {Array} enabledCharStats - Enabled character stats
|
||||
* @param {Array} relationshipFields - Available relationship types
|
||||
* @param {boolean} hasRelationshipEnabled - Whether relationships are enabled
|
||||
*/
|
||||
function renderStructuredCharacters(characters, config, enabledFields, enabledCharStats, relationshipFields, hasRelationshipEnabled) {
|
||||
debugLog('[RPG Thoughts] Rendering structured characters:', characters.length);
|
||||
|
||||
const thoughtsFieldName = config?.thoughts?.name || 'Thoughts';
|
||||
const thoughtsEnabled = config?.thoughts?.enabled;
|
||||
|
||||
// Build HTML for each character
|
||||
let html = '<div class="rpg-present-characters">';
|
||||
|
||||
for (const char of characters) {
|
||||
const avatarUrl = getCharacterAvatarUrl(char.name);
|
||||
const relationshipEmoji = getRelationshipEmoji(char.relationship);
|
||||
|
||||
html += `
|
||||
<div class="rpg-character-card rpg-structured" data-character="${escapeHtmlAttr(char.name)}">
|
||||
<div class="rpg-character-header">
|
||||
<div class="rpg-character-avatar">
|
||||
<img src="${avatarUrl}" onerror="this.src='${FALLBACK_AVATAR_DATA_URI}'" alt="${escapeHtmlAttr(char.name)}">
|
||||
${char.relationship ? `<span class="rpg-relationship-badge" title="${escapeHtmlAttr(char.relationship)}">${relationshipEmoji}</span>` : ''}
|
||||
</div>
|
||||
<div class="rpg-character-info">
|
||||
<div class="rpg-character-name">
|
||||
<span class="rpg-char-emoji rpg-editable" contenteditable="true" data-character="${escapeHtmlAttr(char.name)}" data-field="emoji">${char.emoji}</span>
|
||||
<span class="rpg-char-name-text rpg-editable" contenteditable="true" data-character="${escapeHtmlAttr(char.name)}" data-field="name">${char.name}</span>
|
||||
</div>
|
||||
${char.description ? `<div class="rpg-character-description">${char.description}</div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Custom fields - safely check if fields exists
|
||||
const charFields = char.fields || {};
|
||||
if (enabledFields.length > 0 && Object.keys(charFields).length > 0) {
|
||||
html += '<div class="rpg-character-fields">';
|
||||
for (const field of enabledFields) {
|
||||
const value = charFields[field.name] || '';
|
||||
if (value) {
|
||||
html += `
|
||||
<div class="rpg-character-field">
|
||||
<span class="rpg-field-label">${field.name}:</span>
|
||||
<span class="rpg-field-value rpg-editable" contenteditable="true" data-character="${escapeHtmlAttr(char.name)}" data-field="${field.name}">${value}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Character stats (health, arousal, etc.) - safely check if stats exists
|
||||
const charStats = char.stats || {};
|
||||
if (enabledCharStats.length > 0 && Object.keys(charStats).length > 0) {
|
||||
html += '<div class="rpg-character-stats">';
|
||||
for (const stat of enabledCharStats) {
|
||||
const value = charStats[stat.name];
|
||||
if (value !== undefined) {
|
||||
const color = getStatColor(value, stat.lowColor || '#ff0000', stat.highColor || '#00ff00');
|
||||
html += `
|
||||
<div class="rpg-char-stat">
|
||||
<span class="rpg-stat-name">${stat.name}</span>
|
||||
<div class="rpg-stat-bar">
|
||||
<div class="rpg-stat-fill" style="width: ${value}%; background-color: ${color};"></div>
|
||||
</div>
|
||||
<span class="rpg-stat-value rpg-editable" contenteditable="true" data-character="${escapeHtmlAttr(char.name)}" data-field="${stat.name}">${value}%</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Relationship field
|
||||
if (hasRelationshipEnabled && char.relationship) {
|
||||
// Add the character's relationship to options if not already in the list
|
||||
const allRelationships = [...relationshipFields];
|
||||
if (char.relationship && !allRelationships.includes(char.relationship)) {
|
||||
allRelationships.unshift(char.relationship);
|
||||
}
|
||||
html += `
|
||||
<div class="rpg-character-relationship">
|
||||
<span class="rpg-field-label">Relationship:</span>
|
||||
<select class="rpg-relationship-select" data-character="${escapeHtmlAttr(char.name)}" data-field="Relationship">
|
||||
${allRelationships.map(r => `<option value="${r}" ${char.relationship === r ? 'selected' : ''}>${r}</option>`).join('')}
|
||||
</select>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Thoughts
|
||||
if (thoughtsEnabled && char.thoughts) {
|
||||
html += `
|
||||
<div class="rpg-character-thoughts">
|
||||
<span class="rpg-thoughts-label">${thoughtsFieldName}:</span>
|
||||
<span class="rpg-thoughts-text rpg-editable" contenteditable="true" data-character="${escapeHtmlAttr(char.name)}" data-field="${thoughtsFieldName}">${char.thoughts}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// If no characters
|
||||
if (characters.length === 0) {
|
||||
html = '<div class="rpg-no-characters">No characters present</div>';
|
||||
}
|
||||
|
||||
$thoughtsContainer.html(html);
|
||||
|
||||
// Remove updating animation
|
||||
if (extensionSettings.enableAnimations) {
|
||||
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 300);
|
||||
}
|
||||
|
||||
// Setup event listeners for editable fields
|
||||
setupStructuredCharacterEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets relationship emoji from relationship string
|
||||
* Returns a default emoji (⚖️) if relationship is not in the predefined map
|
||||
@@ -330,55 +140,6 @@ function getCharacterAvatarUrl(characterName) {
|
||||
return FALLBACK_AVATAR_DATA_URI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for structured character editing
|
||||
*/
|
||||
function setupStructuredCharacterEventListeners() {
|
||||
$thoughtsContainer.off('blur', '.rpg-editable').on('blur', '.rpg-editable', function() {
|
||||
const $this = $(this);
|
||||
const characterName = $this.data('character');
|
||||
const field = $this.data('field');
|
||||
const newValue = $this.text().trim();
|
||||
|
||||
// Update the charactersData
|
||||
const charIndex = extensionSettings.charactersData?.findIndex(c => c.name === characterName);
|
||||
if (charIndex !== undefined && charIndex !== -1) {
|
||||
const char = extensionSettings.charactersData[charIndex];
|
||||
|
||||
if (field === 'name') {
|
||||
char.name = newValue;
|
||||
} else if (field === 'emoji') {
|
||||
char.emoji = newValue;
|
||||
} else if (field === 'Thoughts' || field === extensionSettings.trackerConfig?.presentCharacters?.thoughts?.name) {
|
||||
char.thoughts = newValue;
|
||||
} else {
|
||||
// Custom field or stat
|
||||
const fieldId = field.toLowerCase().replace(/\s+/g, '_');
|
||||
if (char.stats && char.stats[fieldId] !== undefined) {
|
||||
char.stats[fieldId] = parseInt(newValue.replace('%', '')) || 0;
|
||||
} else {
|
||||
char[fieldId] = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
saveChatData();
|
||||
}
|
||||
});
|
||||
|
||||
// Relationship select
|
||||
$thoughtsContainer.off('change', '.rpg-relationship-select').on('change', '.rpg-relationship-select', function() {
|
||||
const characterName = $(this).data('character');
|
||||
const newValue = $(this).val();
|
||||
|
||||
const charIndex = extensionSettings.charactersData?.findIndex(c => c.name === characterName);
|
||||
if (charIndex !== undefined && charIndex !== -1) {
|
||||
extensionSettings.charactersData[charIndex].relationship = newValue;
|
||||
saveChatData();
|
||||
renderThoughts(); // Re-render to update badge
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function renderThoughts() {
|
||||
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
|
||||
return;
|
||||
|
||||
@@ -57,8 +57,8 @@ export function buildUserStatsText() {
|
||||
|
||||
// Add inventory summary only if inventory is enabled
|
||||
if (extensionSettings.showInventory) {
|
||||
const inventorySummary = buildInventorySummary(stats.inventory);
|
||||
text += inventorySummary;
|
||||
const inventorySummary = buildInventorySummary(stats.inventory);
|
||||
text += inventorySummary;
|
||||
}
|
||||
|
||||
// Add skills if enabled AND not shown in separate tab
|
||||
@@ -82,19 +82,19 @@ export function renderUserStats() {
|
||||
const stats = extensionSettings.userStats;
|
||||
const config = extensionSettings.trackerConfig?.userStats || {
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
||||
{ id: 'health', name: 'Health', description: '', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', description: '', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', description: '', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', description: '', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', description: '', enabled: true }
|
||||
],
|
||||
rpgAttributes: [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
],
|
||||
statusSection: { enabled: true, showMoodEmoji: true, customFields: ['Conditions'] },
|
||||
skillsSection: { enabled: false, label: 'Skills' }
|
||||
@@ -187,12 +187,12 @@ export function renderUserStats() {
|
||||
if (showRPGAttributes) {
|
||||
// Use attributes from config, with fallback to defaults if not configured
|
||||
const rpgAttributes = (config.rpgAttributes && config.rpgAttributes.length > 0) ? config.rpgAttributes : [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
];
|
||||
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user