feat: json format, et al.
This commit is contained in:
@@ -51,6 +51,175 @@ function separateEmojiFromText(str) {
|
||||
return { emoji: '', text: str };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is valid (not null, undefined, or the string "null")
|
||||
*/
|
||||
function isValidValue(val) {
|
||||
return val !== null && val !== undefined && val !== 'null' && val !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we have valid structured infoBox data
|
||||
* @param {Object} data - The infoBoxData object
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasStructuredInfoBoxData(data) {
|
||||
if (!data) return false;
|
||||
// Handle recentEvents as either string or array
|
||||
const hasEvents = data.recentEvents && (
|
||||
(Array.isArray(data.recentEvents) && data.recentEvents.length > 0) ||
|
||||
(typeof data.recentEvents === 'string' && data.recentEvents.length > 0 && data.recentEvents !== 'null')
|
||||
);
|
||||
return isValidValue(data.date) || isValidValue(data.weather) || isValidValue(data.temperature) ||
|
||||
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.
|
||||
@@ -65,8 +234,28 @@ export function renderInfoBox() {
|
||||
$infoBoxContainer.addClass('rpg-content-updating');
|
||||
}
|
||||
|
||||
// Use committedTrackerData as fallback if lastGeneratedData is empty (e.g., after page refresh)
|
||||
const infoBoxData = lastGeneratedData.infoBox || committedTrackerData.infoBox;
|
||||
// Convert structured JSON data to text format for the original fancy renderer
|
||||
const structuredData = extensionSettings.infoBoxData;
|
||||
let infoBoxData = lastGeneratedData.infoBox || committedTrackerData.infoBox;
|
||||
|
||||
// If we have structured data, convert it to text format
|
||||
if (structuredData && hasStructuredInfoBoxData(structuredData)) {
|
||||
const lines = [];
|
||||
if (isValidValue(structuredData.date)) lines.push(`Date: ${structuredData.date}`);
|
||||
if (isValidValue(structuredData.time)) lines.push(`Time: ${structuredData.time}`);
|
||||
if (isValidValue(structuredData.weather)) lines.push(`Weather: ${structuredData.weather}`);
|
||||
if (isValidValue(structuredData.temperature)) lines.push(`Temperature: ${structuredData.temperature}`);
|
||||
if (isValidValue(structuredData.location)) lines.push(`Location: ${structuredData.location}`);
|
||||
if (structuredData.recentEvents) {
|
||||
const events = Array.isArray(structuredData.recentEvents)
|
||||
? structuredData.recentEvents
|
||||
: [structuredData.recentEvents];
|
||||
events.filter(e => e && e !== 'null').forEach(e => lines.push(`Recent Events: ${e}`));
|
||||
}
|
||||
if (lines.length > 0) {
|
||||
infoBoxData = lines.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// If no data yet, show placeholder
|
||||
if (!infoBoxData) {
|
||||
@@ -117,105 +306,132 @@ export function renderInfoBox() {
|
||||
for (const line of lines) {
|
||||
// console.log('[RPG Companion] Processing line:', line);
|
||||
|
||||
// Helper to check if a value is valid (not null/empty)
|
||||
const isValidParsedValue = (val) => val && val !== 'null' && val !== 'undefined' && val.toLowerCase() !== 'none';
|
||||
|
||||
// Support both new text format (Date:) and legacy emoji format (🗓️:)
|
||||
// Prioritize text format over emoji format
|
||||
if (line.startsWith('Date:')) {
|
||||
if (!parsedFields.date) {
|
||||
// console.log('[RPG Companion] → Matched DATE (text format)');
|
||||
const dateStr = line.replace('Date:', '').trim();
|
||||
const dateParts = dateStr.split(',').map(p => p.trim());
|
||||
data.weekday = dateParts[0] || '';
|
||||
data.month = dateParts[1] || '';
|
||||
data.year = dateParts[2] || '';
|
||||
data.date = dateStr;
|
||||
parsedFields.date = true;
|
||||
if (isValidParsedValue(dateStr)) {
|
||||
const dateParts = dateStr.split(',').map(p => p.trim());
|
||||
data.weekday = dateParts[0] || '';
|
||||
data.month = dateParts[1] || '';
|
||||
data.year = dateParts[2] || '';
|
||||
data.date = dateStr;
|
||||
parsedFields.date = true;
|
||||
}
|
||||
}
|
||||
} else if (line.includes('🗓️:')) {
|
||||
if (!parsedFields.date) {
|
||||
// console.log('[RPG Companion] → Matched DATE (emoji format)');
|
||||
const dateStr = line.replace('🗓️:', '').trim();
|
||||
const dateParts = dateStr.split(',').map(p => p.trim());
|
||||
data.weekday = dateParts[0] || '';
|
||||
data.month = dateParts[1] || '';
|
||||
data.year = dateParts[2] || '';
|
||||
data.date = dateStr;
|
||||
parsedFields.date = true;
|
||||
if (isValidParsedValue(dateStr)) {
|
||||
const dateParts = dateStr.split(',').map(p => p.trim());
|
||||
data.weekday = dateParts[0] || '';
|
||||
data.month = dateParts[1] || '';
|
||||
data.year = dateParts[2] || '';
|
||||
data.date = dateStr;
|
||||
parsedFields.date = true;
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('Temperature:')) {
|
||||
if (!parsedFields.temperature) {
|
||||
// console.log('[RPG Companion] → Matched TEMPERATURE (text format)');
|
||||
const tempStr = line.replace('Temperature:', '').trim();
|
||||
data.temperature = tempStr;
|
||||
const tempMatch = tempStr.match(/(-?\d+)/);
|
||||
if (tempMatch) {
|
||||
data.tempValue = parseInt(tempMatch[1]);
|
||||
if (isValidParsedValue(tempStr)) {
|
||||
data.temperature = tempStr;
|
||||
const tempMatch = tempStr.match(/(-?\d+)/);
|
||||
if (tempMatch) {
|
||||
data.tempValue = parseInt(tempMatch[1]);
|
||||
}
|
||||
parsedFields.temperature = true;
|
||||
}
|
||||
parsedFields.temperature = true;
|
||||
}
|
||||
} else if (line.includes('🌡️:')) {
|
||||
if (!parsedFields.temperature) {
|
||||
// console.log('[RPG Companion] → Matched TEMPERATURE (emoji format)');
|
||||
const tempStr = line.replace('🌡️:', '').trim();
|
||||
data.temperature = tempStr;
|
||||
const tempMatch = tempStr.match(/(-?\d+)/);
|
||||
if (tempMatch) {
|
||||
data.tempValue = parseInt(tempMatch[1]);
|
||||
if (isValidParsedValue(tempStr)) {
|
||||
data.temperature = tempStr;
|
||||
const tempMatch = tempStr.match(/(-?\d+)/);
|
||||
if (tempMatch) {
|
||||
data.tempValue = parseInt(tempMatch[1]);
|
||||
}
|
||||
parsedFields.temperature = true;
|
||||
}
|
||||
parsedFields.temperature = true;
|
||||
}
|
||||
} else if (line.startsWith('Time:')) {
|
||||
if (!parsedFields.time) {
|
||||
// console.log('[RPG Companion] → Matched TIME (text format)');
|
||||
const timeStr = line.replace('Time:', '').trim();
|
||||
data.time = timeStr;
|
||||
const timeParts = timeStr.split('→').map(t => t.trim());
|
||||
data.timeStart = timeParts[0] || '';
|
||||
data.timeEnd = timeParts[1] || '';
|
||||
parsedFields.time = true;
|
||||
if (isValidParsedValue(timeStr)) {
|
||||
data.time = timeStr;
|
||||
const timeParts = timeStr.split('→').map(t => t.trim());
|
||||
data.timeStart = timeParts[0] || '';
|
||||
data.timeEnd = timeParts[1] || '';
|
||||
parsedFields.time = true;
|
||||
}
|
||||
}
|
||||
} else if (line.includes('🕒:')) {
|
||||
if (!parsedFields.time) {
|
||||
// console.log('[RPG Companion] → Matched TIME (emoji format)');
|
||||
const timeStr = line.replace('🕒:', '').trim();
|
||||
data.time = timeStr;
|
||||
const timeParts = timeStr.split('→').map(t => t.trim());
|
||||
data.timeStart = timeParts[0] || '';
|
||||
data.timeEnd = timeParts[1] || '';
|
||||
parsedFields.time = true;
|
||||
if (isValidParsedValue(timeStr)) {
|
||||
data.time = timeStr;
|
||||
const timeParts = timeStr.split('→').map(t => t.trim());
|
||||
data.timeStart = timeParts[0] || '';
|
||||
data.timeEnd = timeParts[1] || '';
|
||||
parsedFields.time = true;
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('Location:')) {
|
||||
if (!parsedFields.location) {
|
||||
// console.log('[RPG Companion] → Matched LOCATION (text format)');
|
||||
data.location = line.replace('Location:', '').trim();
|
||||
parsedFields.location = true;
|
||||
const locStr = line.replace('Location:', '').trim();
|
||||
if (isValidParsedValue(locStr)) {
|
||||
data.location = locStr;
|
||||
parsedFields.location = true;
|
||||
}
|
||||
}
|
||||
} else if (line.includes('🗺️:')) {
|
||||
if (!parsedFields.location) {
|
||||
// console.log('[RPG Companion] → Matched LOCATION (emoji format)');
|
||||
data.location = line.replace('🗺️:', '').trim();
|
||||
parsedFields.location = true;
|
||||
const locStr = line.replace('🗺️:', '').trim();
|
||||
if (isValidParsedValue(locStr)) {
|
||||
data.location = locStr;
|
||||
parsedFields.location = true;
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('Weather:')) {
|
||||
if (!parsedFields.weather) {
|
||||
// New text format: Weather: [Emoji], [Forecast] OR Weather: [Emoji][Forecast] (no separator - FIXED)
|
||||
const weatherStr = line.replace('Weather:', '').trim();
|
||||
const { emoji, text } = separateEmojiFromText(weatherStr);
|
||||
|
||||
if (emoji && text) {
|
||||
data.weatherEmoji = emoji;
|
||||
data.weatherForecast = text;
|
||||
} else if (weatherStr.includes(',')) {
|
||||
// Fallback to comma split if emoji detection failed
|
||||
const weatherParts = weatherStr.split(',').map(p => p.trim());
|
||||
data.weatherEmoji = weatherParts[0] || '';
|
||||
data.weatherForecast = weatherParts[1] || '';
|
||||
|
||||
// Skip null/invalid values
|
||||
if (!isValidParsedValue(weatherStr)) {
|
||||
parsedFields.weather = true; // Mark as parsed so we don't try again
|
||||
} else {
|
||||
// No clear separation - assume it's all forecast text
|
||||
data.weatherEmoji = '🌤️'; // Default emoji
|
||||
data.weatherForecast = weatherStr;
|
||||
}
|
||||
const { emoji, text } = separateEmojiFromText(weatherStr);
|
||||
|
||||
parsedFields.weather = true;
|
||||
if (emoji && text) {
|
||||
data.weatherEmoji = emoji;
|
||||
data.weatherForecast = text;
|
||||
} else if (weatherStr.includes(',')) {
|
||||
// Fallback to comma split if emoji detection failed
|
||||
const weatherParts = weatherStr.split(',').map(p => p.trim());
|
||||
data.weatherEmoji = weatherParts[0] || '';
|
||||
data.weatherForecast = weatherParts[1] || '';
|
||||
} else {
|
||||
// No clear separation - assume it's all forecast text
|
||||
data.weatherEmoji = '🌤️'; // Default emoji
|
||||
data.weatherForecast = weatherStr;
|
||||
}
|
||||
|
||||
parsedFields.weather = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check if it's a legacy weather line (emoji format)
|
||||
@@ -271,6 +487,20 @@ export function renderInfoBox() {
|
||||
// location: data.location
|
||||
// });
|
||||
|
||||
// Sanitize parsed values - filter out "null" strings and invalid values
|
||||
const sanitize = (val) => (val && val !== 'null' && val !== 'undefined' && val.toLowerCase() !== 'none') ? val : '';
|
||||
data.date = sanitize(data.date);
|
||||
data.weekday = sanitize(data.weekday);
|
||||
data.month = sanitize(data.month);
|
||||
data.year = sanitize(data.year);
|
||||
data.weatherEmoji = sanitize(data.weatherEmoji);
|
||||
data.weatherForecast = sanitize(data.weatherForecast);
|
||||
data.temperature = sanitize(data.temperature);
|
||||
data.time = sanitize(data.time);
|
||||
data.timeStart = sanitize(data.timeStart);
|
||||
data.timeEnd = sanitize(data.timeEnd);
|
||||
data.location = sanitize(data.location);
|
||||
|
||||
// Get tracker configuration
|
||||
const config = extensionSettings.trackerConfig?.infoBox;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getInventoryRenderOptions, restoreFormStates } from '../interaction/inv
|
||||
import { updateInventoryItem } from '../interaction/inventoryEdit.js';
|
||||
import { parseItems } from '../../utils/itemParser.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { itemHasLinkedSkills, navigateToLinkedSkills } from './skills.js';
|
||||
|
||||
// Type imports
|
||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||
@@ -23,6 +24,23 @@ export function getLocationId(locationName) {
|
||||
return locationName.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the skill link indicator for an inventory item
|
||||
* @param {string} itemName - The item name
|
||||
* @returns {string} HTML string for the link indicator (empty if no links)
|
||||
*/
|
||||
function getSkillLinkIndicator(itemName) {
|
||||
if (!extensionSettings.enableItemSkillLinks || !extensionSettings.showSkills) {
|
||||
return '';
|
||||
}
|
||||
if (itemHasLinkedSkills(itemName)) {
|
||||
return `<button class="rpg-item-skill-link" data-action="goto-linked-skills" data-item="${escapeHtml(itemName)}" title="${i18n.getTranslation('inventory.gotoLinkedSkills')}">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
</button>`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the inventory sub-tab navigation (On Person, Stored, Assets)
|
||||
* @param {string} activeTab - Currently active sub-tab ('onPerson', 'stored', 'assets')
|
||||
@@ -44,6 +62,31 @@ export function renderInventorySubTabs(activeTab = 'onPerson') {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the description for an item from structured inventory data
|
||||
* @param {string} field - Field type ('onPerson', 'stored', 'assets')
|
||||
* @param {number} index - Item index
|
||||
* @param {string} [location] - Location name for stored items
|
||||
* @returns {string} Item description or empty string
|
||||
*/
|
||||
function getItemDescription(field, index, location = null) {
|
||||
const inv3 = extensionSettings.inventoryV3;
|
||||
if (!inv3) return '';
|
||||
|
||||
let items;
|
||||
if (field === 'onPerson') {
|
||||
items = inv3.onPerson;
|
||||
} else if (field === 'assets') {
|
||||
items = inv3.assets;
|
||||
} else if (field === 'stored' && location) {
|
||||
items = inv3.stored?.[location];
|
||||
}
|
||||
|
||||
if (!items || !Array.isArray(items) || !items[index]) return '';
|
||||
const item = items[index];
|
||||
return (typeof item === 'object' ? item.description : '') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the "On Person" inventory view with list or grid display
|
||||
* @param {string} onPersonItems - Current on-person items (comma-separated string)
|
||||
@@ -59,24 +102,38 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-card" data-field="onPerson" data-index="${index}">
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('onPerson', index);
|
||||
return `
|
||||
<div class="rpg-item-card ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="onPerson" data-index="${index}">
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" 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="onPerson" 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) => `
|
||||
<div class="rpg-item-row" data-field="onPerson" data-index="${index}">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('onPerson', index);
|
||||
return `
|
||||
<div class="rpg-item-row ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="onPerson" data-index="${index}">
|
||||
<div class="rpg-item-main-row">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
|
||||
<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="onPerson" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(desc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,24 +238,38 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-card" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('stored', index, location);
|
||||
return `
|
||||
<div class="rpg-item-card ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" 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="stored" data-location="${escapeHtml(location)}" 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) => `
|
||||
<div class="rpg-item-row" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('stored', index, location);
|
||||
return `
|
||||
<div class="rpg-item-row ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||
<div class="rpg-item-main-row">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
|
||||
<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="stored" data-location="${escapeHtml(location)}" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(desc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,24 +348,38 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-card" data-field="assets" data-index="${index}">
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('assets', index);
|
||||
return `
|
||||
<div class="rpg-item-card ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="assets" data-index="${index}">
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="Remove asset">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" 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="assets" 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) => `
|
||||
<div class="rpg-item-row" data-field="assets" data-index="${index}">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="${i18n.getTranslation('inventory.assets.removeAssetTitle')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('assets', index);
|
||||
return `
|
||||
<div class="rpg-item-row ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="assets" data-index="${index}">
|
||||
<div class="rpg-item-main-row">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="${i18n.getTranslation('inventory.assets.removeAssetTitle')}">
|
||||
<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="assets" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(desc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,18 +546,20 @@ export function renderSimplifiedInventoryView(itemsString, viewMode = 'list') {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-card" data-field="simplified" data-index="${index}">
|
||||
<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>
|
||||
`).join('');
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-row" data-field="simplified" data-index="${index}">
|
||||
<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>
|
||||
@@ -523,6 +610,182 @@ 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}
|
||||
*/
|
||||
function hasStructuredInventory() {
|
||||
const inv = extensionSettings.inventoryV3;
|
||||
return inv && (
|
||||
(inv.onPerson && inv.onPerson.length > 0) ||
|
||||
(inv.assets && inv.assets.length > 0) ||
|
||||
(inv.stored && Object.keys(inv.stored).length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main inventory rendering function (matches pattern of other render functions)
|
||||
* Gets data from state/settings and updates DOM directly.
|
||||
@@ -534,24 +797,35 @@ export function renderInventory() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get inventory data from settings
|
||||
const inventory = extensionSettings.userStats.inventory;
|
||||
|
||||
let html;
|
||||
|
||||
// Convert structured inventory (v3) to legacy format if present
|
||||
// This ensures we always use the original renderer
|
||||
let inventory = extensionSettings.userStats.inventory;
|
||||
if (hasStructuredInventory()) {
|
||||
const inv = extensionSettings.inventoryV3;
|
||||
// Convert structured items to comma-separated strings
|
||||
const itemsToString = (items) => {
|
||||
if (!items || items.length === 0) return 'None';
|
||||
return items.map(i => typeof i === 'string' ? i : i.name).join(', ');
|
||||
};
|
||||
inventory = {
|
||||
version: 2,
|
||||
onPerson: itemsToString(inv.onPerson),
|
||||
stored: Object.fromEntries(
|
||||
Object.entries(inv.stored || {}).map(([k, v]) => [k, itemsToString(v)])
|
||||
),
|
||||
assets: itemsToString(inv.assets)
|
||||
};
|
||||
}
|
||||
|
||||
// Check if we should render simplified inventory
|
||||
if (extensionSettings.useSimplifiedInventory) {
|
||||
// For simplified mode, combine all items into a single string
|
||||
// Use the 'items' field if available (from simplified parsing),
|
||||
// otherwise fall back to onPerson
|
||||
const itemsString = inventory.items || inventory.onPerson || 'None';
|
||||
// Get view mode from settings (use 'simplified' key or fall back to 'onPerson')
|
||||
const viewModes = extensionSettings.inventoryViewModes || {};
|
||||
const viewMode = viewModes.simplified || viewModes.onPerson || 'list';
|
||||
html = renderSimplifiedInventoryView(itemsString, viewMode);
|
||||
} else {
|
||||
// Full categorized inventory
|
||||
// Get current render options (active tab, collapsed locations)
|
||||
const options = getInventoryRenderOptions();
|
||||
html = generateInventoryHTML(inventory, options);
|
||||
}
|
||||
@@ -569,6 +843,45 @@ export function renderInventory() {
|
||||
const newName = $(this).text().trim();
|
||||
updateInventoryItem(field, index, newName, location);
|
||||
});
|
||||
|
||||
// Event listener for editing item descriptions (structured mode)
|
||||
$inventoryContainer.find('.rpg-item-description.rpg-editable').on('blur', function() {
|
||||
const field = $(this).data('field');
|
||||
const index = parseInt($(this).data('index'));
|
||||
const location = $(this).data('location');
|
||||
const newDesc = $(this).text().trim();
|
||||
updateStructuredItemDescription(field, index, newDesc, location);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an item's description in structured inventory
|
||||
* @param {string} field - 'onPerson', 'stored', or 'assets'
|
||||
* @param {number} index - Item index
|
||||
* @param {string} newDescription - New description
|
||||
* @param {string} [location] - Location for stored items
|
||||
*/
|
||||
function updateStructuredItemDescription(field, index, newDescription, location) {
|
||||
const inv = extensionSettings.inventoryV3;
|
||||
if (!inv) return;
|
||||
|
||||
let item;
|
||||
if (field === 'onPerson' && inv.onPerson?.[index]) {
|
||||
item = inv.onPerson[index];
|
||||
} else if (field === 'assets' && inv.assets?.[index]) {
|
||||
item = inv.assets[index];
|
||||
} else if (field === 'stored' && location && inv.stored?.[location]?.[index]) {
|
||||
item = inv.stored[location][index];
|
||||
}
|
||||
|
||||
if (item) {
|
||||
item.description = newDescription;
|
||||
// Save changes
|
||||
import('../../core/persistence.js').then(({ saveSettings, saveChatData }) => {
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+103
-17
@@ -18,6 +18,48 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we have structured quests data (v2 format with name + description)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasStructuredQuests() {
|
||||
const q = extensionSettings.questsV2;
|
||||
return q && (q.main !== undefined || q.optional !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the main quest (supports both legacy and structured format)
|
||||
* @returns {{name: string, description: string}|null}
|
||||
*/
|
||||
function getMainQuest() {
|
||||
if (hasStructuredQuests() && extensionSettings.questsV2.main) {
|
||||
return extensionSettings.questsV2.main;
|
||||
}
|
||||
// Legacy format
|
||||
const title = extensionSettings.quests?.main;
|
||||
if (title && title !== 'None') {
|
||||
return { name: title, description: extensionSettings.quests?.mainDescription || '' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets optional quests (supports both legacy and structured format)
|
||||
* @returns {Array<{name: string, description: string}>}
|
||||
*/
|
||||
function getOptionalQuests() {
|
||||
if (hasStructuredQuests() && extensionSettings.questsV2.optional) {
|
||||
return extensionSettings.questsV2.optional;
|
||||
}
|
||||
// Legacy format
|
||||
const titles = extensionSettings.quests?.optional || [];
|
||||
const descriptions = extensionSettings.quests?.optionalDescriptions || [];
|
||||
return titles.map((title, i) => ({
|
||||
name: title,
|
||||
description: descriptions[i] || ''
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the quests sub-tab navigation (Main, Optional)
|
||||
* @param {string} activeTab - Currently active sub-tab ('main', 'optional')
|
||||
@@ -38,12 +80,15 @@ export function renderQuestsSubTabs(activeTab = 'main') {
|
||||
|
||||
/**
|
||||
* Renders the main quest view
|
||||
* @param {string} mainQuest - Current main quest title
|
||||
* @param {string} mainQuest - Current main quest title (legacy param, ignored if structured)
|
||||
* @returns {string} HTML for main quest view
|
||||
*/
|
||||
export function renderMainQuestView(mainQuest) {
|
||||
const questDisplay = (mainQuest && mainQuest !== 'None') ? mainQuest : '';
|
||||
const hasQuest = questDisplay.length > 0;
|
||||
// Use structured data helpers
|
||||
const quest = getMainQuest();
|
||||
const hasQuest = quest !== null;
|
||||
const questName = quest?.name || '';
|
||||
const questDesc = quest?.description || '';
|
||||
|
||||
return `
|
||||
<div class="rpg-quest-section">
|
||||
@@ -56,7 +101,8 @@ export function renderMainQuestView(mainQuest) {
|
||||
<div class="rpg-quest-content">
|
||||
${hasQuest ? `
|
||||
<div class="rpg-inline-form" id="rpg-edit-quest-form-main" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-edit-quest-main" value="${escapeHtml(questDisplay)}" />
|
||||
<input type="text" class="rpg-inline-input" id="rpg-edit-quest-main" value="${escapeHtml(questName)}" placeholder="Quest name" />
|
||||
<input type="text" class="rpg-inline-input" id="rpg-edit-quest-desc-main" value="${escapeHtml(questDesc)}" placeholder="Description (optional)" />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-edit-quest" data-field="main">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
@@ -67,7 +113,7 @@ export function renderMainQuestView(mainQuest) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-quest-item" data-field="main">
|
||||
<div class="rpg-quest-title">${escapeHtml(questDisplay)}</div>
|
||||
<div class="rpg-quest-title">${escapeHtml(questName)}</div>
|
||||
<div class="rpg-quest-actions">
|
||||
<button class="rpg-quest-edit" data-action="edit-quest" data-field="main" title="Edit quest">
|
||||
<i class="fa-solid fa-edit"></i>
|
||||
@@ -106,7 +152,8 @@ export function renderMainQuestView(mainQuest) {
|
||||
* @returns {string} HTML for optional quests view
|
||||
*/
|
||||
export function renderOptionalQuestsView(optionalQuests) {
|
||||
const quests = optionalQuests.filter(q => q && q !== 'None');
|
||||
// Use structured data helpers
|
||||
const quests = getOptionalQuests().filter(q => q && q.name && q.name !== 'None');
|
||||
|
||||
let questsHtml = '';
|
||||
if (quests.length === 0) {
|
||||
@@ -114,7 +161,7 @@ export function renderOptionalQuestsView(optionalQuests) {
|
||||
} else {
|
||||
questsHtml = quests.map((quest, index) => `
|
||||
<div class="rpg-quest-item" data-field="optional" data-index="${index}">
|
||||
<div class="rpg-quest-title rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" title="Click to edit">${escapeHtml(quest)}</div>
|
||||
<div class="rpg-quest-title rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" title="Click to edit">${escapeHtml(quest.name)}</div>
|
||||
<div class="rpg-quest-actions">
|
||||
<button class="rpg-quest-remove" data-action="remove-quest" data-field="optional" data-index="${index}" title="Complete/Remove quest">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
@@ -218,17 +265,29 @@ function attachQuestEventHandlers() {
|
||||
// Save add quest
|
||||
$questsContainer.find('[data-action="save-add-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
const input = $(`#rpg-new-quest-${field}`);
|
||||
const questTitle = input.val().trim();
|
||||
const nameInput = $(`#rpg-new-quest-${field}`);
|
||||
const descInput = $(`#rpg-new-quest-desc-${field}`);
|
||||
const questTitle = nameInput.val().trim();
|
||||
const questDesc = descInput?.val()?.trim() || '';
|
||||
|
||||
if (questTitle) {
|
||||
// Ensure structured format exists
|
||||
if (!extensionSettings.questsV2) {
|
||||
extensionSettings.questsV2 = { main: null, optional: [] };
|
||||
}
|
||||
|
||||
if (field === 'main') {
|
||||
extensionSettings.quests.main = questTitle;
|
||||
extensionSettings.questsV2.main = { name: questTitle, description: questDesc };
|
||||
} else {
|
||||
if (!extensionSettings.quests.optional) {
|
||||
extensionSettings.quests.optional = [];
|
||||
}
|
||||
if (!extensionSettings.questsV2.optional) {
|
||||
extensionSettings.questsV2.optional = [];
|
||||
}
|
||||
extensionSettings.quests.optional.push(questTitle);
|
||||
extensionSettings.questsV2.optional.push({ name: questTitle, description: questDesc });
|
||||
}
|
||||
saveSettings();
|
||||
renderQuests();
|
||||
@@ -250,13 +309,21 @@ function attachQuestEventHandlers() {
|
||||
$('.rpg-quest-item[data-field="main"]').show();
|
||||
});
|
||||
|
||||
// Save edit quest
|
||||
// Save edit quest (main)
|
||||
$questsContainer.find('[data-action="save-edit-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
const input = $(`#rpg-edit-quest-${field}`);
|
||||
const questTitle = input.val().trim();
|
||||
const nameInput = $(`#rpg-edit-quest-${field}`);
|
||||
const descInput = $(`#rpg-edit-quest-desc-${field}`);
|
||||
const questTitle = nameInput.val().trim();
|
||||
const questDesc = descInput.val()?.trim() || '';
|
||||
|
||||
if (questTitle) {
|
||||
// Use structured format
|
||||
if (!extensionSettings.questsV2) {
|
||||
extensionSettings.questsV2 = { main: null, optional: [] };
|
||||
}
|
||||
extensionSettings.questsV2.main = { name: questTitle, description: questDesc };
|
||||
// Also update legacy for backwards compatibility
|
||||
extensionSettings.quests.main = questTitle;
|
||||
saveSettings();
|
||||
renderQuests();
|
||||
@@ -270,22 +337,41 @@ function attachQuestEventHandlers() {
|
||||
|
||||
if (field === 'main') {
|
||||
extensionSettings.quests.main = 'None';
|
||||
if (extensionSettings.questsV2) {
|
||||
extensionSettings.questsV2.main = null;
|
||||
}
|
||||
} else {
|
||||
extensionSettings.quests.optional.splice(index, 1);
|
||||
if (extensionSettings.questsV2?.optional) {
|
||||
extensionSettings.questsV2.optional.splice(index, 1);
|
||||
}
|
||||
}
|
||||
saveSettings();
|
||||
renderQuests();
|
||||
});
|
||||
|
||||
// Inline editing for optional quests
|
||||
$questsContainer.find('.rpg-quest-title.rpg-editable').on('blur', function() {
|
||||
// Inline editing for optional quests (name and description)
|
||||
$questsContainer.find('.rpg-quest-title.rpg-editable, .rpg-quest-description.rpg-editable').on('blur', function() {
|
||||
const $this = $(this);
|
||||
const field = $this.data('field');
|
||||
const index = $this.data('index');
|
||||
const newTitle = $this.text().trim();
|
||||
const prop = $this.data('prop') || 'name';
|
||||
const newValue = $this.text().trim();
|
||||
|
||||
if (newTitle && field === 'optional' && index !== undefined) {
|
||||
extensionSettings.quests.optional[index] = newTitle;
|
||||
if (field === 'optional' && index !== undefined) {
|
||||
// Ensure structured format exists
|
||||
if (!extensionSettings.questsV2) {
|
||||
extensionSettings.questsV2 = { main: null, optional: [] };
|
||||
}
|
||||
if (!extensionSettings.questsV2.optional[index]) {
|
||||
extensionSettings.questsV2.optional[index] = { name: '', description: '' };
|
||||
}
|
||||
extensionSettings.questsV2.optional[index][prop] = newValue;
|
||||
|
||||
// Also update legacy for backwards compatibility
|
||||
if (prop === 'name') {
|
||||
extensionSettings.quests.optional[index] = newValue;
|
||||
}
|
||||
saveSettings();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,985 @@
|
||||
/**
|
||||
* Skills Rendering Module
|
||||
* Handles rendering of the skills section with skill categories (like inventory)
|
||||
* Each configured skill becomes a category, and abilities/items can be added within each
|
||||
*/
|
||||
|
||||
import { extensionSettings, $skillsContainer } from '../../core/state.js';
|
||||
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { parseItems } from '../../utils/itemParser.js';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an array of items into a comma-separated string
|
||||
* @param {string[]} items - Array of items
|
||||
* @returns {string} Comma-separated string or 'None'
|
||||
*/
|
||||
function serializeItems(items) {
|
||||
if (!items || items.length === 0) return 'None';
|
||||
return items.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured skill categories from settings
|
||||
* @returns {string[]} Array of skill category names
|
||||
*/
|
||||
export function getSkillCategories() {
|
||||
return extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the items/abilities for a specific skill category
|
||||
* Checks both skillsData (from parser) and skills.categories (manual entries)
|
||||
* @param {string} skillName - The skill category name
|
||||
* @returns {string} Comma-separated items string or 'None'
|
||||
*/
|
||||
export function getSkillItems(skillName) {
|
||||
// Check skillsData first (populated by parser)
|
||||
if (extensionSettings.skillsData?.[skillName]) {
|
||||
return extensionSettings.skillsData[skillName];
|
||||
}
|
||||
// Fall back to skills.categories (manual entries)
|
||||
if (extensionSettings.skills?.categories?.[skillName]) {
|
||||
return extensionSettings.skills.categories[skillName];
|
||||
}
|
||||
return 'None';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the items/abilities for a specific skill category
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {string} itemsString - Comma-separated items string
|
||||
*/
|
||||
export function setSkillItems(skillName, itemsString) {
|
||||
// Initialize structures if needed
|
||||
if (!extensionSettings.skillsData) {
|
||||
extensionSettings.skillsData = {};
|
||||
}
|
||||
if (!extensionSettings.skills) {
|
||||
extensionSettings.skills = { categories: {}, list: [] };
|
||||
}
|
||||
if (!extensionSettings.skills.categories) {
|
||||
extensionSettings.skills.categories = {};
|
||||
}
|
||||
|
||||
// Store in both places for compatibility
|
||||
extensionSettings.skillsData[skillName] = itemsString || 'None';
|
||||
extensionSettings.skills.categories[skillName] = itemsString || 'None';
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
updateMessageSwipeData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to a skill category
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {string} item - The item to add
|
||||
*/
|
||||
export function addSkillItem(skillName, item, description = '') {
|
||||
// Check for structured data first
|
||||
const skillsV2 = extensionSettings.skillsV2;
|
||||
if (skillsV2 && skillsV2[skillName] !== undefined) {
|
||||
if (!Array.isArray(skillsV2[skillName])) {
|
||||
skillsV2[skillName] = [];
|
||||
}
|
||||
// Check if ability already exists
|
||||
const exists = skillsV2[skillName].some(a => a.name === item);
|
||||
if (!exists) {
|
||||
skillsV2[skillName].push({ name: item, description: description, grantedBy: null });
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to legacy format
|
||||
const currentItems = parseItems(getSkillItems(skillName));
|
||||
if (!currentItems.includes(item)) {
|
||||
currentItems.push(item);
|
||||
setSkillItems(skillName, serializeItems(currentItems));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from a skill category
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {number} index - Index of item to remove
|
||||
*/
|
||||
export function removeSkillItem(skillName, index) {
|
||||
// Check for structured data first
|
||||
const skillsV2 = extensionSettings.skillsV2;
|
||||
if (skillsV2 && skillsV2[skillName] !== undefined && Array.isArray(skillsV2[skillName])) {
|
||||
if (index >= 0 && index < skillsV2[skillName].length) {
|
||||
const removedAbility = skillsV2[skillName][index];
|
||||
skillsV2[skillName].splice(index, 1);
|
||||
|
||||
// Remove any skill-ability links
|
||||
if (extensionSettings.skillAbilityLinks) {
|
||||
const linkKey = `${skillName}::${removedAbility.name}`;
|
||||
delete extensionSettings.skillAbilityLinks[linkKey];
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to legacy format
|
||||
const currentItems = parseItems(getSkillItems(skillName));
|
||||
if (index >= 0 && index < currentItems.length) {
|
||||
const removedItem = currentItems[index];
|
||||
currentItems.splice(index, 1);
|
||||
setSkillItems(skillName, serializeItems(currentItems));
|
||||
|
||||
// Handle item-skill link removal if enabled
|
||||
if (extensionSettings.enableItemSkillLinks && extensionSettings.itemSkillLinks) {
|
||||
// Check if this item was linked and remove the link
|
||||
for (const [itemName, linkedSkill] of Object.entries(extensionSettings.itemSkillLinks)) {
|
||||
if (linkedSkill === skillName && itemName === removedItem) {
|
||||
delete extensionSettings.itemSkillLinks[itemName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an item in a skill category
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {number} index - Index of item to update
|
||||
* @param {string} newValue - New item value
|
||||
*/
|
||||
export function updateSkillItem(skillName, index, newValue) {
|
||||
// Check for structured data first
|
||||
const skillsV2 = extensionSettings.skillsV2;
|
||||
if (skillsV2 && skillsV2[skillName] && Array.isArray(skillsV2[skillName]) && skillsV2[skillName][index]) {
|
||||
skillsV2[skillName][index].name = newValue;
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to legacy format
|
||||
const currentItems = parseItems(getSkillItems(skillName));
|
||||
if (index >= 0 && index < currentItems.length) {
|
||||
currentItems[index] = newValue;
|
||||
setSkillItems(skillName, serializeItems(currentItems));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a skill ability's description (structured format only)
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {number} index - Index of ability to update
|
||||
* @param {string} newDescription - New description
|
||||
*/
|
||||
function updateStructuredSkillDescription(skillName, index, newDescription) {
|
||||
const skillsV2 = extensionSettings.skillsV2;
|
||||
if (skillsV2 && skillsV2[skillName] && Array.isArray(skillsV2[skillName]) && skillsV2[skillName][index]) {
|
||||
skillsV2[skillName][index].description = newDescription;
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item is removed from inventory
|
||||
* Based on deleteSkillWithItem setting:
|
||||
* - false (default): Just removes the link, skill remains
|
||||
* - true: Deletes the linked skill abilities entirely
|
||||
* @param {string} itemName - The name of the removed item
|
||||
*/
|
||||
export function handleItemRemoved(itemName) {
|
||||
if (!extensionSettings.enableItemSkillLinks) return;
|
||||
if (!extensionSettings.skillAbilityLinks) return;
|
||||
|
||||
const itemNameLower = itemName.toLowerCase().trim();
|
||||
const linksToRemove = [];
|
||||
|
||||
// Find all skill abilities linked to this item
|
||||
for (const [key, linkedItem] of Object.entries(extensionSettings.skillAbilityLinks)) {
|
||||
if (linkedItem && linkedItem.toLowerCase().trim() === itemNameLower) {
|
||||
linksToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (linksToRemove.length === 0) return;
|
||||
|
||||
// Remove the links
|
||||
for (const key of linksToRemove) {
|
||||
delete extensionSettings.skillAbilityLinks[key];
|
||||
|
||||
// If deleteSkillWithItem is enabled, also delete the skill ability itself
|
||||
if (extensionSettings.deleteSkillWithItem) {
|
||||
const [skillName, abilityName] = key.split('::');
|
||||
deleteSkillAbility(skillName, abilityName);
|
||||
}
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
renderSkills();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a skill ability from the skills data
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {string} abilityName - The ability name to delete
|
||||
*/
|
||||
function deleteSkillAbility(skillName, abilityName) {
|
||||
// Delete from structured skills (skillsV2)
|
||||
if (extensionSettings.skillsV2 && extensionSettings.skillsV2[skillName]) {
|
||||
const abilities = extensionSettings.skillsV2[skillName];
|
||||
if (Array.isArray(abilities)) {
|
||||
const index = abilities.findIndex(a =>
|
||||
(typeof a === 'string' ? a : a.name)?.toLowerCase().trim() === abilityName.toLowerCase().trim()
|
||||
);
|
||||
if (index !== -1) {
|
||||
abilities.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from legacy skillsData
|
||||
if (extensionSettings.skillsData && extensionSettings.skillsData[skillName]) {
|
||||
const currentItems = parseItems(extensionSettings.skillsData[skillName]);
|
||||
const index = currentItems.findIndex(item =>
|
||||
item.toLowerCase().trim() === abilityName.toLowerCase().trim()
|
||||
);
|
||||
if (index !== -1) {
|
||||
currentItems.splice(index, 1);
|
||||
extensionSettings.skillsData[skillName] = currentItems.length > 0 ? currentItems.join(', ') : 'None';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the linked item for a skill ability
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {string} abilityName - The ability name
|
||||
* @returns {string|null} The linked item name or null
|
||||
*/
|
||||
export function getLinkedItem(skillName, abilityName) {
|
||||
if (!extensionSettings.skillAbilityLinks) return null;
|
||||
const key = `${skillName}::${abilityName}`;
|
||||
return extensionSettings.skillAbilityLinks[key] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Links a skill ability to an inventory item
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {string} abilityName - The ability name
|
||||
* @param {string} itemName - The inventory item name
|
||||
*/
|
||||
export function linkAbilityToItem(skillName, abilityName, itemName) {
|
||||
if (!extensionSettings.skillAbilityLinks) {
|
||||
extensionSettings.skillAbilityLinks = {};
|
||||
}
|
||||
const key = `${skillName}::${abilityName}`;
|
||||
extensionSettings.skillAbilityLinks[key] = itemName;
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlinks a skill ability from its inventory item
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {string} abilityName - The ability name
|
||||
*/
|
||||
export function unlinkAbility(skillName, abilityName) {
|
||||
if (!extensionSettings.skillAbilityLinks) return;
|
||||
const key = `${skillName}::${abilityName}`;
|
||||
delete extensionSettings.skillAbilityLinks[key];
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all skill abilities linked to a specific inventory item
|
||||
* @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 [];
|
||||
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 });
|
||||
}
|
||||
}
|
||||
return linked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an inventory item has any linked skills
|
||||
* @param {string} itemName - The inventory item name
|
||||
* @returns {boolean} True if item has linked skills
|
||||
*/
|
||||
export function itemHasLinkedSkills(itemName) {
|
||||
return getAbilitiesLinkedToItem(itemName).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the inventory tab and highlights an item
|
||||
* @param {string} itemName - The item to highlight
|
||||
*/
|
||||
export function navigateToInventoryItem(itemName) {
|
||||
// Switch to inventory tab if on desktop
|
||||
if (window.innerWidth > 1000) {
|
||||
const $inventoryTab = $('.rpg-tab-btn[data-tab="inventory"]');
|
||||
if ($inventoryTab.length) {
|
||||
$inventoryTab.click();
|
||||
}
|
||||
}
|
||||
|
||||
// Find and highlight the item after a delay for tab switch animation
|
||||
setTimeout(() => {
|
||||
// Search in inventory container specifically
|
||||
const $inventoryContainer = $('#rpg-inventory');
|
||||
const $items = $inventoryContainer.find('.rpg-item-name');
|
||||
let found = false;
|
||||
|
||||
$items.each(function() {
|
||||
const text = $(this).text().trim();
|
||||
// Match exact or partial (for items that might have quantities etc)
|
||||
if (text === itemName || text.toLowerCase() === itemName.toLowerCase()) {
|
||||
const $row = $(this).closest('.rpg-item-row, .rpg-item-card');
|
||||
if ($row.length) {
|
||||
// Scroll into view
|
||||
$row[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// Add highlight class
|
||||
$row.addClass('rpg-highlight-item');
|
||||
found = true;
|
||||
// Remove after 3.5 seconds (after 3 animation cycles)
|
||||
setTimeout(() => {
|
||||
$row.removeClass('rpg-highlight-item');
|
||||
}, 3500);
|
||||
}
|
||||
return false; // Break the loop
|
||||
}
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
toastr.warning(`Item "${itemName}" not found in inventory`);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the skills tab and highlights abilities linked to an item
|
||||
* @param {string} itemName - The item whose linked abilities to highlight
|
||||
*/
|
||||
export function navigateToLinkedSkills(itemName) {
|
||||
const linkedAbilities = getAbilitiesLinkedToItem(itemName);
|
||||
if (linkedAbilities.length === 0) {
|
||||
toastr.info(`No skills linked to "${itemName}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch to skills tab if on desktop
|
||||
if (window.innerWidth > 1000) {
|
||||
const $skillsTab = $('.rpg-tab-btn[data-tab="skills"]');
|
||||
if ($skillsTab.length) {
|
||||
$skillsTab.click();
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight all linked abilities after a delay for tab switch
|
||||
setTimeout(() => {
|
||||
let firstHighlighted = false;
|
||||
|
||||
linkedAbilities.forEach(({ skillName, abilityName }) => {
|
||||
// Find the skill category
|
||||
const $category = $(`.rpg-skill-category[data-skill="${skillName}"]`);
|
||||
if ($category.length) {
|
||||
// Find items within this category
|
||||
const $items = $category.find('.rpg-item-row, .rpg-item-card');
|
||||
$items.each(function() {
|
||||
const $row = $(this);
|
||||
const $name = $row.find('.rpg-item-name');
|
||||
if ($name.text().trim() === abilityName) {
|
||||
// Scroll first match into view
|
||||
if (!firstHighlighted) {
|
||||
$row[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
firstHighlighted = true;
|
||||
}
|
||||
// Add highlight class
|
||||
$row.addClass('rpg-highlight-item');
|
||||
// Remove after 3.5 seconds
|
||||
setTimeout(() => {
|
||||
$row.removeClass('rpg-highlight-item');
|
||||
}, 3500);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all inventory items (from all categories) for linking dropdown
|
||||
* Supports both legacy (v2) and structured (v3) inventory formats
|
||||
* @returns {string[]} Array of item names
|
||||
*/
|
||||
function getAllInventoryItems() {
|
||||
const items = [];
|
||||
|
||||
// Check structured inventory (v3) first
|
||||
const inv3 = extensionSettings.inventoryV3;
|
||||
if (inv3) {
|
||||
// On Person
|
||||
if (inv3.onPerson && Array.isArray(inv3.onPerson)) {
|
||||
items.push(...inv3.onPerson.map(i => typeof i === 'string' ? i : i.name).filter(Boolean));
|
||||
}
|
||||
// Stored
|
||||
if (inv3.stored && typeof inv3.stored === 'object') {
|
||||
for (const locationItems of Object.values(inv3.stored)) {
|
||||
if (Array.isArray(locationItems)) {
|
||||
items.push(...locationItems.map(i => typeof i === 'string' ? i : i.name).filter(Boolean));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Assets
|
||||
if (inv3.assets && Array.isArray(inv3.assets)) {
|
||||
items.push(...inv3.assets.map(i => typeof i === 'string' ? i : i.name).filter(Boolean));
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to legacy inventory if no v3 items found
|
||||
if (items.length === 0) {
|
||||
const inventory = extensionSettings.userStats?.inventory;
|
||||
if (inventory) {
|
||||
// On Person
|
||||
if (inventory.onPerson && inventory.onPerson.toLowerCase() !== 'none') {
|
||||
items.push(...parseItems(inventory.onPerson));
|
||||
}
|
||||
|
||||
// Stored locations
|
||||
if (inventory.stored && typeof inventory.stored === 'object') {
|
||||
for (const locationItems of Object.values(inventory.stored)) {
|
||||
if (locationItems && locationItems.toLowerCase() !== 'none') {
|
||||
items.push(...parseItems(locationItems));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assets
|
||||
if (inventory.assets && inventory.assets.toLowerCase() !== 'none') {
|
||||
items.push(...parseItems(inventory.assets));
|
||||
}
|
||||
|
||||
// Simplified inventory
|
||||
if (inventory.items && inventory.items.toLowerCase() !== 'none') {
|
||||
items.push(...parseItems(inventory.items));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(items)];
|
||||
}
|
||||
|
||||
// Track open add forms
|
||||
let openAddForms = {};
|
||||
|
||||
/**
|
||||
* Shows the add item form for a skill category
|
||||
* @param {string} skillName - The skill category name
|
||||
*/
|
||||
function showAddForm(skillName) {
|
||||
openAddForms[skillName] = true;
|
||||
renderSkills();
|
||||
// Focus the input after render
|
||||
setTimeout(() => {
|
||||
$(`#rpg-new-skill-item-${CSS.escape(skillName)}`).focus();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the add item form for a skill category
|
||||
* @param {string} skillName - The skill category name
|
||||
*/
|
||||
function hideAddForm(skillName) {
|
||||
openAddForms[skillName] = false;
|
||||
renderSkills();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new item from the add form
|
||||
* @param {string} skillName - The skill category name
|
||||
*/
|
||||
function saveAddItem(skillName) {
|
||||
const input = $(`#rpg-new-skill-item-${CSS.escape(skillName)}`);
|
||||
const value = input.val()?.trim();
|
||||
if (value) {
|
||||
addSkillItem(skillName, value);
|
||||
}
|
||||
hideAddForm(skillName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a structured skill ability (with name + description)
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {Object} ability - Structured ability object {name, description, grantedBy}
|
||||
* @param {number} index - The item index
|
||||
* @param {string} viewMode - View mode ('list' or 'grid')
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
function renderStructuredSkillAbility(skillName, ability, index, viewMode) {
|
||||
// Normalize ability - handle both string and object formats
|
||||
const normalizedAbility = typeof ability === 'string'
|
||||
? { name: ability, description: '', grantedBy: null }
|
||||
: { name: ability?.name || 'Unknown', description: ability?.description || '', grantedBy: ability?.grantedBy || null };
|
||||
|
||||
// Check for linked item - first from ability.grantedBy, then from skillAbilityLinks
|
||||
const linkedItem = normalizedAbility.grantedBy ||
|
||||
(extensionSettings.enableItemSkillLinks ? getLinkedItem(skillName, normalizedAbility.name) : null);
|
||||
const itemClass = viewMode === 'grid' ? 'rpg-item-card' : 'rpg-item-row';
|
||||
const hasLink = !!linkedItem;
|
||||
|
||||
// Link indicator HTML - shows the item that grants this skill
|
||||
const linkIndicator = hasLink
|
||||
? `<span class="rpg-skill-link-badge" data-action="goto-linked-item" data-item="${escapeHtml(linkedItem)}" title="${i18n.getTranslation('skills.gotoItem')}: ${escapeHtml(linkedItem)}">
|
||||
<i class="fa-solid fa-link"></i>
|
||||
<span class="rpg-link-item-name">${escapeHtml(linkedItem)}</span>
|
||||
</span>`
|
||||
: (extensionSettings.enableItemSkillLinks
|
||||
? `<button class="rpg-skill-link-btn" data-action="show-link-dropdown" data-skill="${escapeHtml(skillName)}" data-ability="${escapeHtml(normalizedAbility.name)}" data-index="${index}" title="${i18n.getTranslation('skills.linkToItem')}">
|
||||
<i class="fa-solid fa-link"></i>
|
||||
</button>`
|
||||
: '');
|
||||
|
||||
// Unlink button
|
||||
const unlinkBtn = hasLink && extensionSettings.enableItemSkillLinks
|
||||
? `<button class="rpg-skill-unlink-btn" data-action="unlink-ability" data-skill="${escapeHtml(skillName)}" data-ability="${escapeHtml(normalizedAbility.name)}" title="${i18n.getTranslation('skills.unlinkItem')}">
|
||||
<i class="fa-solid fa-unlink"></i>
|
||||
</button>`
|
||||
: '';
|
||||
|
||||
if (viewMode === 'list') {
|
||||
return `
|
||||
<div class="${itemClass} rpg-structured ${hasLink ? 'rpg-has-link' : ''}" data-skill="${escapeHtml(skillName)}" data-index="${index}" data-ability="${escapeHtml(normalizedAbility.name)}">
|
||||
<div class="rpg-skill-ability-row">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-skill="${escapeHtml(skillName)}" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit')}">${escapeHtml(normalizedAbility.name)}</span>
|
||||
${linkIndicator}
|
||||
${unlinkBtn}
|
||||
<button class="rpg-item-remove" data-action="remove-skill-item" data-skill="${escapeHtml(skillName)}" data-index="${index}" title="${i18n.getTranslation('skills.removeAbility')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-skill-ability-desc-row">
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-skill="${escapeHtml(skillName)}" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(normalizedAbility.description)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="${itemClass} rpg-structured ${hasLink ? 'rpg-has-link' : ''}" data-skill="${escapeHtml(skillName)}" data-index="${index}" data-ability="${escapeHtml(normalizedAbility.name)}">
|
||||
<div class="rpg-card-actions">
|
||||
${unlinkBtn}
|
||||
<button class="rpg-item-remove" data-action="remove-skill-item" data-skill="${escapeHtml(skillName)}" data-index="${index}" title="${i18n.getTranslation('skills.removeAbility')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-skill="${escapeHtml(skillName)}" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit')}">${escapeHtml(normalizedAbility.name)}</span>
|
||||
${linkIndicator}
|
||||
<div class="rpg-skill-ability-desc-row">
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-skill="${escapeHtml(skillName)}" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(normalizedAbility.description)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single skill ability item with link indicator (legacy string format)
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {string} abilityName - The ability name
|
||||
* @param {number} index - The item index
|
||||
* @param {string} viewMode - View mode ('list' or 'grid')
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
function renderSkillAbilityItem(skillName, abilityName, index, viewMode) {
|
||||
const linkedItem = extensionSettings.enableItemSkillLinks ? getLinkedItem(skillName, abilityName) : null;
|
||||
const itemClass = viewMode === 'grid' ? 'rpg-item-card' : 'rpg-item-row';
|
||||
const hasLink = !!linkedItem;
|
||||
|
||||
// Link indicator HTML
|
||||
const linkIndicator = extensionSettings.enableItemSkillLinks ? (hasLink
|
||||
? `<span class="rpg-skill-link-badge" data-action="goto-linked-item" data-item="${escapeHtml(linkedItem)}" title="${i18n.getTranslation('skills.gotoItem')}: ${escapeHtml(linkedItem)}">
|
||||
<i class="fa-solid fa-link"></i>
|
||||
<span class="rpg-link-item-name">${escapeHtml(linkedItem)}</span>
|
||||
</span>`
|
||||
: `<button class="rpg-skill-link-btn" data-action="show-link-dropdown" data-skill="${escapeHtml(skillName)}" data-ability="${escapeHtml(abilityName)}" data-index="${index}" title="${i18n.getTranslation('skills.linkToItem')}">
|
||||
<i class="fa-solid fa-link"></i>
|
||||
</button>`
|
||||
) : '';
|
||||
|
||||
// Unlink button (only shown if linked)
|
||||
const unlinkBtn = hasLink && extensionSettings.enableItemSkillLinks
|
||||
? `<button class="rpg-skill-unlink-btn" data-action="unlink-ability" data-skill="${escapeHtml(skillName)}" data-ability="${escapeHtml(abilityName)}" title="${i18n.getTranslation('skills.unlinkItem')}">
|
||||
<i class="fa-solid fa-unlink"></i>
|
||||
</button>`
|
||||
: '';
|
||||
|
||||
if (viewMode === 'list') {
|
||||
return `
|
||||
<div class="${itemClass} ${hasLink ? 'rpg-has-link' : ''}" data-skill="${escapeHtml(skillName)}" data-index="${index}" data-ability="${escapeHtml(abilityName)}">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-skill="${escapeHtml(skillName)}" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit')}">${escapeHtml(abilityName)}</span>
|
||||
${linkIndicator}
|
||||
${unlinkBtn}
|
||||
<button class="rpg-item-remove" data-action="remove-skill-item" data-skill="${escapeHtml(skillName)}" data-index="${index}" title="${i18n.getTranslation('skills.removeAbility')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="${itemClass} ${hasLink ? 'rpg-has-link' : ''}" data-skill="${escapeHtml(skillName)}" data-index="${index}" data-ability="${escapeHtml(abilityName)}">
|
||||
<div class="rpg-card-actions">
|
||||
${unlinkBtn}
|
||||
<button class="rpg-item-remove" data-action="remove-skill-item" data-skill="${escapeHtml(skillName)}" data-index="${index}" title="${i18n.getTranslation('skills.removeAbility')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-skill="${escapeHtml(skillName)}" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit')}">${escapeHtml(abilityName)}</span>
|
||||
${linkIndicator}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we have structured skills data (v2 format)
|
||||
* @param {string} skillName - The skill category name
|
||||
* @returns {Array|null} Structured abilities array or null
|
||||
*/
|
||||
function getStructuredSkillAbilities(skillName) {
|
||||
const skillsV2 = extensionSettings.skillsV2;
|
||||
if (skillsV2 && skillsV2[skillName] && Array.isArray(skillsV2[skillName])) {
|
||||
return skillsV2[skillName];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single skill category section
|
||||
* @param {string} skillName - The skill category name
|
||||
* @param {string} viewMode - View mode ('list' or 'grid')
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
function renderSkillCategory(skillName, viewMode) {
|
||||
// Check for structured data first
|
||||
const structuredAbilities = getStructuredSkillAbilities(skillName);
|
||||
const isStructured = structuredAbilities !== null;
|
||||
|
||||
const items = isStructured ? structuredAbilities : parseItems(getSkillItems(skillName));
|
||||
const safeSkillName = skillName.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const isFormOpen = openAddForms[skillName];
|
||||
|
||||
let itemsHtml = '';
|
||||
if (items.length === 0) {
|
||||
itemsHtml = `<div class="rpg-skill-items-empty" data-i18n-key="skills.noAbilities">${i18n.getTranslation('skills.noAbilities')}</div>`;
|
||||
} else {
|
||||
if (isStructured) {
|
||||
// Render structured abilities with name + description
|
||||
itemsHtml = items.map((ability, index) => renderStructuredSkillAbility(skillName, ability, index, viewMode)).join('');
|
||||
} else {
|
||||
// Render legacy string-based abilities
|
||||
itemsHtml = items.map((item, index) => renderSkillAbilityItem(skillName, item, index, viewMode)).join('');
|
||||
}
|
||||
}
|
||||
|
||||
const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view';
|
||||
|
||||
return `
|
||||
<div class="rpg-skill-category" data-skill="${escapeHtml(skillName)}">
|
||||
<div class="rpg-skill-category-header">
|
||||
<h5 class="rpg-skill-category-title">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
<span>${escapeHtml(skillName)}</span>
|
||||
<span class="rpg-skill-category-count">(${items.length})</span>
|
||||
</h5>
|
||||
<div class="rpg-skill-category-actions">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-skill-view" data-skill="${escapeHtml(skillName)}" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-skill-view" data-skill="${escapeHtml(skillName)}" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-skill-add-btn" data-action="show-add-skill-item" data-skill="${escapeHtml(skillName)}" title="${i18n.getTranslation('skills.addAbility')}">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="skills.addAbilityButton">${i18n.getTranslation('skills.addAbilityButton')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-skill-category-content">
|
||||
<div class="rpg-inline-form" id="rpg-add-skill-form-${safeSkillName}" style="display: ${isFormOpen ? 'flex' : 'none'};">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-skill-item-${safeSkillName}" placeholder="${i18n.getTranslation('skills.addAbilityPlaceholder')}" />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-skill-item" data-skill="${escapeHtml(skillName)}">
|
||||
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-skill-item" data-skill="${escapeHtml(skillName)}">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-item-list ${listViewClass}">
|
||||
${itemsHtml}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the full skills section HTML
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
export function generateSkillsHTML() {
|
||||
const skillCategories = getSkillCategories();
|
||||
|
||||
if (skillCategories.length === 0) {
|
||||
return `
|
||||
<div class="rpg-skills-container">
|
||||
<div class="rpg-skills-empty">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
<p data-i18n-key="skills.empty">${i18n.getTranslation('skills.empty')}</p>
|
||||
<small data-i18n-key="skills.emptyNote">${i18n.getTranslation('skills.emptyNote')}</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Get view modes for each skill (default to list)
|
||||
const viewModes = extensionSettings.skillsViewModes || {};
|
||||
|
||||
let html = `<div class="rpg-skills-container">`;
|
||||
|
||||
// Render each skill category
|
||||
for (const skillName of skillCategories) {
|
||||
const viewMode = viewModes[skillName] || 'list';
|
||||
html += renderSkillCategory(skillName, viewMode);
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event listeners for skills section
|
||||
*/
|
||||
function setupSkillsEventListeners() {
|
||||
if (!$skillsContainer || $skillsContainer.length === 0) return;
|
||||
|
||||
// Show add form
|
||||
$skillsContainer.off('click', '[data-action="show-add-skill-item"]').on('click', '[data-action="show-add-skill-item"]', function() {
|
||||
const skillName = $(this).data('skill');
|
||||
showAddForm(skillName);
|
||||
});
|
||||
|
||||
// Cancel add form
|
||||
$skillsContainer.off('click', '[data-action="cancel-add-skill-item"]').on('click', '[data-action="cancel-add-skill-item"]', function() {
|
||||
const skillName = $(this).data('skill');
|
||||
hideAddForm(skillName);
|
||||
});
|
||||
|
||||
// Save add form
|
||||
$skillsContainer.off('click', '[data-action="save-add-skill-item"]').on('click', '[data-action="save-add-skill-item"]', function() {
|
||||
const skillName = $(this).data('skill');
|
||||
saveAddItem(skillName);
|
||||
});
|
||||
|
||||
// Enter key in add form
|
||||
$skillsContainer.off('keypress', '.rpg-inline-input').on('keypress', '.rpg-inline-input', function(e) {
|
||||
if (e.which === 13) { // Enter key
|
||||
const skillName = $(this).closest('.rpg-skill-category').data('skill');
|
||||
saveAddItem(skillName);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove item
|
||||
$skillsContainer.off('click', '[data-action="remove-skill-item"]').on('click', '[data-action="remove-skill-item"]', function() {
|
||||
const skillName = $(this).data('skill');
|
||||
const index = $(this).data('index');
|
||||
removeSkillItem(skillName, index);
|
||||
renderSkills();
|
||||
});
|
||||
|
||||
// Edit item name (blur event for contenteditable)
|
||||
$skillsContainer.off('blur', '.rpg-item-name.rpg-editable').on('blur', '.rpg-item-name.rpg-editable', function() {
|
||||
const skillName = $(this).data('skill');
|
||||
const index = $(this).data('index');
|
||||
const newValue = $(this).text().trim();
|
||||
if (newValue) {
|
||||
updateSkillItem(skillName, index, newValue);
|
||||
} else {
|
||||
// If empty, remove the item
|
||||
removeSkillItem(skillName, index);
|
||||
renderSkills();
|
||||
}
|
||||
});
|
||||
|
||||
// Edit item description (for structured skills)
|
||||
$skillsContainer.off('blur', '.rpg-item-description.rpg-editable').on('blur', '.rpg-item-description.rpg-editable', function() {
|
||||
const skillName = $(this).data('skill');
|
||||
const index = $(this).data('index');
|
||||
const newDesc = $(this).text().trim();
|
||||
updateStructuredSkillDescription(skillName, index, newDesc);
|
||||
});
|
||||
|
||||
// Switch view mode for a skill category
|
||||
$skillsContainer.off('click', '[data-action="switch-skill-view"]').on('click', '[data-action="switch-skill-view"]', function() {
|
||||
const skillName = $(this).data('skill');
|
||||
const view = $(this).data('view');
|
||||
if (!extensionSettings.skillsViewModes) {
|
||||
extensionSettings.skillsViewModes = {};
|
||||
}
|
||||
extensionSettings.skillsViewModes[skillName] = view;
|
||||
saveSettings();
|
||||
renderSkills();
|
||||
});
|
||||
|
||||
// Show link dropdown
|
||||
$skillsContainer.off('click', '[data-action="show-link-dropdown"]').on('click', '[data-action="show-link-dropdown"]', function(e) {
|
||||
e.stopPropagation();
|
||||
const $btn = $(this);
|
||||
const skillName = $btn.data('skill');
|
||||
const abilityName = $btn.data('ability');
|
||||
showLinkDropdown($btn, skillName, abilityName);
|
||||
});
|
||||
|
||||
// Go to linked item in inventory
|
||||
$skillsContainer.off('click', '[data-action="goto-linked-item"]').on('click', '[data-action="goto-linked-item"]', function() {
|
||||
const itemName = $(this).data('item');
|
||||
if (itemName) {
|
||||
navigateToInventoryItem(itemName);
|
||||
}
|
||||
});
|
||||
|
||||
// Unlink ability from item
|
||||
$skillsContainer.off('click', '[data-action="unlink-ability"]').on('click', '[data-action="unlink-ability"]', function(e) {
|
||||
e.stopPropagation();
|
||||
const skillName = $(this).data('skill');
|
||||
const abilityName = $(this).data('ability');
|
||||
unlinkAbility(skillName, abilityName);
|
||||
renderSkills();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dropdown to select an inventory item to link
|
||||
*/
|
||||
function showLinkDropdown($btn, skillName, abilityName) {
|
||||
// Remove any existing dropdown
|
||||
$('.rpg-link-dropdown').remove();
|
||||
|
||||
const inventoryItems = getAllInventoryItems();
|
||||
|
||||
if (inventoryItems.length === 0) {
|
||||
toastr.info(i18n.getTranslation('skills.noItemsToLink'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Build dropdown HTML
|
||||
let dropdownHtml = `<div class="rpg-link-dropdown">
|
||||
<div class="rpg-link-dropdown-header">
|
||||
<span>${i18n.getTranslation('skills.selectItemToLink')}</span>
|
||||
<button class="rpg-link-dropdown-close" data-action="close-link-dropdown"><i class="fa-solid fa-times"></i></button>
|
||||
</div>
|
||||
<div class="rpg-link-dropdown-list">`;
|
||||
|
||||
for (const item of inventoryItems) {
|
||||
dropdownHtml += `<div class="rpg-link-dropdown-item" data-action="link-to-item" data-skill="${escapeHtml(skillName)}" data-ability="${escapeHtml(abilityName)}" data-item="${escapeHtml(item)}">
|
||||
<i class="fa-solid fa-box"></i> <span>${escapeHtml(item)}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
dropdownHtml += `</div></div>`;
|
||||
|
||||
const $dropdown = $(dropdownHtml);
|
||||
$('body').append($dropdown);
|
||||
|
||||
// Position near the button
|
||||
const btnOffset = $btn.offset();
|
||||
$dropdown.css({
|
||||
position: 'fixed',
|
||||
top: btnOffset.top + $btn.outerHeight() + 5,
|
||||
left: Math.min(btnOffset.left, $(window).width() - $dropdown.outerWidth() - 10),
|
||||
zIndex: 10000
|
||||
});
|
||||
|
||||
// Event handlers
|
||||
$dropdown.on('click', '[data-action="close-link-dropdown"]', () => $dropdown.remove());
|
||||
$dropdown.on('click', '[data-action="link-to-item"]', function() {
|
||||
linkAbilityToItem($(this).data('skill'), $(this).data('ability'), $(this).data('item'));
|
||||
$dropdown.remove();
|
||||
renderSkills();
|
||||
toastr.success(i18n.getTranslation('skills.linkCreated'));
|
||||
});
|
||||
|
||||
// Close when clicking outside
|
||||
setTimeout(() => {
|
||||
$(document).one('click', function(e) {
|
||||
if (!$(e.target).closest('.rpg-link-dropdown, [data-action="show-link-dropdown"]').length) {
|
||||
$dropdown.remove();
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main render function for skills section
|
||||
*/
|
||||
export function renderSkills() {
|
||||
if (!extensionSettings.showSkills || !$skillsContainer || $skillsContainer.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = generateSkillsHTML();
|
||||
|
||||
$skillsContainer.html(html);
|
||||
setupSkillsEventListeners();
|
||||
|
||||
// Apply i18n translations (pass DOM element, not jQuery object)
|
||||
const domElement = $skillsContainer[0];
|
||||
if (domElement) {
|
||||
i18n.applyTranslations(domElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a summary string of all skills for prompt injection
|
||||
* @returns {string} Formatted skills summary
|
||||
*/
|
||||
export function buildSkillsSummary() {
|
||||
const skillCategories = getSkillCategories();
|
||||
if (skillCategories.length === 0) return '';
|
||||
|
||||
let summary = '';
|
||||
for (const skillName of skillCategories) {
|
||||
const items = getSkillItems(skillName);
|
||||
summary += `${skillName}: ${items}\n`;
|
||||
}
|
||||
return summary.trim();
|
||||
}
|
||||
|
||||
@@ -106,6 +106,279 @@ function namesMatch(cardName, aiName) {
|
||||
* 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
|
||||
*/
|
||||
function getRelationshipEmoji(relationship) {
|
||||
if (!relationship) return null;
|
||||
const map = {
|
||||
'Enemy': '⚔️',
|
||||
'Neutral': '⚖️',
|
||||
'Friend': '⭐',
|
||||
'Lover': '❤️',
|
||||
'Ally': '🤝',
|
||||
'Rival': '🎯',
|
||||
'Family': '👨👩👧',
|
||||
'Stranger': '❓'
|
||||
};
|
||||
// Return mapped emoji or default '⚖️' for unknown relationships
|
||||
return map[relationship] || '⚖️';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets character avatar URL
|
||||
*/
|
||||
function getCharacterAvatarUrl(characterName) {
|
||||
// Try to find matching character from SillyTavern
|
||||
try {
|
||||
const context = getContext();
|
||||
if (context && characters) {
|
||||
const char = characters.find(c => namesMatch(c.name, characterName));
|
||||
if (char && char.avatar) {
|
||||
return getSafeThumbnailUrl('avatar', char.avatar);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog('[RPG Thoughts] Error getting avatar:', e);
|
||||
}
|
||||
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;
|
||||
@@ -127,9 +400,48 @@ export function renderThoughts() {
|
||||
const enabledCharStats = characterStatsConfig?.enabled && characterStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
const relationshipFields = config?.relationshipFields || [];
|
||||
const hasRelationshipEnabled = relationshipFields.length > 0;
|
||||
|
||||
// Use committedTrackerData as fallback if lastGeneratedData is empty (e.g., after page refresh)
|
||||
const characterThoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts || '';
|
||||
|
||||
// Convert structured character data to text format for the original fancy renderer
|
||||
let characterThoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts || '';
|
||||
|
||||
// If we have structured data, convert it to text format
|
||||
if (extensionSettings.charactersData && Array.isArray(extensionSettings.charactersData) && extensionSettings.charactersData.length > 0) {
|
||||
const lines = [];
|
||||
for (const char of extensionSettings.charactersData) {
|
||||
// Character name line
|
||||
lines.push(`- ${char.name || 'Unknown'}`);
|
||||
|
||||
// Details line with emoji and fields
|
||||
const details = [char.emoji || '😶'];
|
||||
const charFields = char.fields || {};
|
||||
for (const [key, value] of Object.entries(charFields)) {
|
||||
if (value) details.push(`${key}: ${value}`);
|
||||
}
|
||||
lines.push(`Details: ${details.join(' | ')}`);
|
||||
|
||||
// Relationship line
|
||||
if (char.relationship) {
|
||||
lines.push(`Relationship: ${char.relationship}`);
|
||||
}
|
||||
|
||||
// Stats line
|
||||
const charStats = char.stats || {};
|
||||
if (Object.keys(charStats).length > 0) {
|
||||
const statsStr = Object.entries(charStats).map(([k, v]) => `${k}: ${v}%`).join(' | ');
|
||||
lines.push(`Stats: ${statsStr}`);
|
||||
}
|
||||
|
||||
// Thoughts line
|
||||
if (char.thoughts) {
|
||||
const thoughtsFieldName = config?.thoughts?.name || 'Thoughts';
|
||||
lines.push(`${thoughtsFieldName}: ${char.thoughts}`);
|
||||
}
|
||||
}
|
||||
if (lines.length > 0) {
|
||||
characterThoughtsData = lines.join('\n');
|
||||
debugLog('[RPG Thoughts] Converted structured data to text format');
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('[RPG Thoughts] Raw characterThoughts data:', characterThoughtsData);
|
||||
debugLog('[RPG Thoughts] Data length:', characterThoughtsData.length + ' chars');
|
||||
@@ -376,14 +688,16 @@ export function renderThoughts() {
|
||||
debugLog(`[RPG Thoughts] Final avatar for ${char.name}:`, characterPortrait.substring(0, 50) + '...');
|
||||
|
||||
// Get relationship badge - only if relationships are enabled in config
|
||||
let relationshipBadge = '⚖️'; // Default
|
||||
let relationshipBadge = '⚖️'; // Default emoji
|
||||
let relationshipText = 'Neutral'; // Default text for tooltip
|
||||
let relationshipFieldName = 'Relationship';
|
||||
|
||||
if (hasRelationshipEnabled) {
|
||||
// In the new format, relationship is always stored in char.Relationship
|
||||
if (char.Relationship) {
|
||||
// Try to map text to emoji
|
||||
relationshipBadge = relationshipEmojis[char.Relationship] || char.Relationship;
|
||||
relationshipText = char.Relationship;
|
||||
// Try to map text to emoji, fall back to default link emoji for unknown types
|
||||
relationshipBadge = relationshipEmojis[char.Relationship] || '⚖️';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +710,7 @@ export function renderThoughts() {
|
||||
<div class="rpg-character-card" data-character-name="${escapedName}">
|
||||
<div class="rpg-character-avatar">
|
||||
<img src="${characterPortrait}" alt="${escapedName}" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
||||
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
|
||||
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="${relationshipFieldName}" title="${escapeHtmlAttr(relationshipText)}">${relationshipBadge}</div>` : ''}
|
||||
</div>
|
||||
<div class="rpg-character-content">
|
||||
<div class="rpg-character-info">
|
||||
|
||||
@@ -61,8 +61,8 @@ export function buildUserStatsText() {
|
||||
text += inventorySummary;
|
||||
}
|
||||
|
||||
// Add skills if enabled
|
||||
if (config.skillsSection.enabled && stats.skills) {
|
||||
// Add skills if enabled AND not shown in separate tab
|
||||
if (config.skillsSection.enabled && stats.skills && !extensionSettings.showSkills) {
|
||||
text += `\n${config.skillsSection.label}: ${stats.skills}`;
|
||||
}
|
||||
|
||||
@@ -167,8 +167,8 @@ export function renderUserStats() {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Skills section (conditionally rendered)
|
||||
if (config.skillsSection.enabled) {
|
||||
// Skills section (conditionally rendered) - only if NOT shown in separate tab
|
||||
if (config.skillsSection.enabled && !extensionSettings.showSkills) {
|
||||
const skillsValue = stats.skills || 'None';
|
||||
html += `
|
||||
<div class="rpg-skills-section">
|
||||
|
||||
Reference in New Issue
Block a user