Revert "All the features"
This commit is contained in:
+109
-196
@@ -51,29 +51,6 @@ 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 as a visual dashboard with calendar, weather, temperature, clock, and map widgets.
|
||||
* Includes event listeners for editable fields.
|
||||
@@ -88,28 +65,8 @@ export function renderInfoBox() {
|
||||
$infoBoxContainer.addClass('rpg-content-updating');
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
// Use committedTrackerData as fallback if lastGeneratedData is empty (e.g., after page refresh)
|
||||
const infoBoxData = lastGeneratedData.infoBox || committedTrackerData.infoBox;
|
||||
|
||||
// If no data yet, show placeholder
|
||||
if (!infoBoxData) {
|
||||
@@ -128,7 +85,11 @@ export function renderInfoBox() {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('[RPG Companion] renderInfoBox called with data:', infoBoxData);
|
||||
|
||||
// Parse the info box data
|
||||
const lines = infoBoxData.split('\n');
|
||||
// console.log('[RPG Companion] Info Box split into lines:', lines);
|
||||
const data = {
|
||||
date: '',
|
||||
weekday: '',
|
||||
@@ -154,124 +115,107 @@ export function renderInfoBox() {
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
// Helper to check if a value is valid (not null/empty)
|
||||
const isValidParsedValue = (val) => val && val !== 'null' && val !== 'undefined' && val.toLowerCase() !== 'none';
|
||||
|
||||
// console.log('[RPG Companion] Processing line:', line);
|
||||
|
||||
// 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();
|
||||
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;
|
||||
}
|
||||
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();
|
||||
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;
|
||||
}
|
||||
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();
|
||||
if (isValidParsedValue(tempStr)) {
|
||||
data.temperature = tempStr;
|
||||
const tempMatch = tempStr.match(/(-?\d+)/);
|
||||
if (tempMatch) {
|
||||
data.tempValue = parseInt(tempMatch[1]);
|
||||
}
|
||||
parsedFields.temperature = true;
|
||||
data.temperature = tempStr;
|
||||
const tempMatch = tempStr.match(/(-?\d+)/);
|
||||
if (tempMatch) {
|
||||
data.tempValue = parseInt(tempMatch[1]);
|
||||
}
|
||||
parsedFields.temperature = true;
|
||||
}
|
||||
} else if (line.includes('🌡️:')) {
|
||||
if (!parsedFields.temperature) {
|
||||
// console.log('[RPG Companion] → Matched TEMPERATURE (emoji format)');
|
||||
const tempStr = line.replace('🌡️:', '').trim();
|
||||
if (isValidParsedValue(tempStr)) {
|
||||
data.temperature = tempStr;
|
||||
const tempMatch = tempStr.match(/(-?\d+)/);
|
||||
if (tempMatch) {
|
||||
data.tempValue = parseInt(tempMatch[1]);
|
||||
}
|
||||
parsedFields.temperature = true;
|
||||
data.temperature = tempStr;
|
||||
const tempMatch = tempStr.match(/(-?\d+)/);
|
||||
if (tempMatch) {
|
||||
data.tempValue = parseInt(tempMatch[1]);
|
||||
}
|
||||
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();
|
||||
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;
|
||||
}
|
||||
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();
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
const locStr = line.replace('Location:', '').trim();
|
||||
if (isValidParsedValue(locStr)) {
|
||||
data.location = locStr;
|
||||
parsedFields.location = true;
|
||||
}
|
||||
// console.log('[RPG Companion] → Matched LOCATION (text format)');
|
||||
data.location = line.replace('Location:', '').trim();
|
||||
parsedFields.location = true;
|
||||
}
|
||||
} else if (line.includes('🗺️:')) {
|
||||
if (!parsedFields.location) {
|
||||
const locStr = line.replace('🗺️:', '').trim();
|
||||
if (isValidParsedValue(locStr)) {
|
||||
data.location = locStr;
|
||||
parsedFields.location = true;
|
||||
}
|
||||
// console.log('[RPG Companion] → Matched LOCATION (emoji format)');
|
||||
data.location = line.replace('🗺️:', '').trim();
|
||||
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();
|
||||
|
||||
// Skip null/invalid values
|
||||
if (!isValidParsedValue(weatherStr)) {
|
||||
parsedFields.weather = true; // Mark as parsed so we don't try again
|
||||
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] || '';
|
||||
} else {
|
||||
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] || '';
|
||||
} else {
|
||||
// No clear separation - assume it's all forecast text
|
||||
data.weatherEmoji = '🌤️'; // Default emoji
|
||||
data.weatherForecast = weatherStr;
|
||||
}
|
||||
|
||||
parsedFields.weather = true;
|
||||
// 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)
|
||||
@@ -283,41 +227,50 @@ export function renderInfoBox() {
|
||||
const notDivider = !line.includes('---');
|
||||
const notCodeFence = !line.trim().startsWith('```');
|
||||
|
||||
// console.log('[RPG Companion] → Checking weather conditions:', {
|
||||
// line: line,
|
||||
// hasColon: hasColon,
|
||||
// notInfoBox: notInfoBox,
|
||||
// notDivider: notDivider
|
||||
// });
|
||||
|
||||
if (hasColon && notInfoBox && notDivider && notCodeFence && line.trim().length > 0) {
|
||||
// Match format: [Weather Emoji]: [Forecast]
|
||||
// Capture everything before colon as emoji, everything after as forecast
|
||||
// console.log('[RPG Companion] → Testing WEATHER match for:', line);
|
||||
const weatherMatch = line.match(/^\s*([^:]+):\s*(.+)$/);
|
||||
if (weatherMatch) {
|
||||
const potentialEmoji = weatherMatch[1].trim();
|
||||
const forecast = weatherMatch[2].trim();
|
||||
|
||||
// If the first part is short (likely emoji), treat as weather
|
||||
if (potentialEmoji.length <= 5) {
|
||||
data.weatherEmoji = potentialEmoji;
|
||||
data.weatherForecast = forecast;
|
||||
parsedFields.weather = true;
|
||||
// console.log('[RPG Companion] ✓ Weather parsed:', data.weatherEmoji, data.weatherForecast);
|
||||
} else {
|
||||
// console.log('[RPG Companion] ✗ First part too long for emoji:', potentialEmoji);
|
||||
}
|
||||
} else {
|
||||
// console.log('[RPG Companion] ✗ Weather regex did not match');
|
||||
}
|
||||
} else {
|
||||
// console.log('[RPG Companion] → No match for this line');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('[RPG Companion] Parsed Info Box data:', {
|
||||
// date: data.date,
|
||||
// weatherEmoji: data.weatherEmoji,
|
||||
// weatherForecast: data.weatherForecast,
|
||||
// temperature: data.temperature,
|
||||
// timeStart: data.timeStart,
|
||||
// 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;
|
||||
|
||||
@@ -468,21 +421,9 @@ export function renderInfoBox() {
|
||||
|
||||
// Row 3: Recent Events widget (notebook style) - show if enabled
|
||||
if (config?.widgets?.recentEvents?.enabled) {
|
||||
// Get Recent Events from structured data (JSON) or text format
|
||||
// Parse Recent Events from infoBox string
|
||||
let recentEvents = [];
|
||||
|
||||
// First check structured infoBoxData (from JSON parsing)
|
||||
if (extensionSettings.infoBoxData?.recentEvents) {
|
||||
const events = extensionSettings.infoBoxData.recentEvents;
|
||||
if (Array.isArray(events)) {
|
||||
recentEvents = events.filter(e => e && e !== 'null');
|
||||
} else if (typeof events === 'string' && events !== 'null') {
|
||||
recentEvents = [events];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to text format from committedTrackerData
|
||||
if (recentEvents.length === 0 && committedTrackerData.infoBox) {
|
||||
if (committedTrackerData.infoBox) {
|
||||
const recentEventsLine = committedTrackerData.infoBox.split('\n').find(line => line.startsWith('Recent Events:'));
|
||||
if (recentEventsLine) {
|
||||
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
|
||||
@@ -598,12 +539,14 @@ export function updateInfoBoxField(field, value) {
|
||||
// Reconstruct the Info Box text with updated field
|
||||
const lines = lastGeneratedData.infoBox.split('\n');
|
||||
let dateLineFound = false;
|
||||
let dateLineIndex = -1;
|
||||
let weatherLineIndex = -1;
|
||||
|
||||
// Find the date line
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].includes('🗓️:') || lines[i].startsWith('Date:')) {
|
||||
dateLineFound = true;
|
||||
dateLineIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -837,6 +780,7 @@ export function updateInfoBoxField(field, value) {
|
||||
const swipeId = message.swipe_id || 0;
|
||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||
message.extra.rpg_companion_swipes[swipeId].infoBox = updatedLines.join('\n');
|
||||
// console.log('[RPG Companion] Updated infoBox in message swipe data');
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -867,57 +811,34 @@ function updateRecentEvent(field, value) {
|
||||
}[field];
|
||||
|
||||
if (eventIndex !== undefined) {
|
||||
// Get existing events - prioritize structured data (same logic as renderInfoBox)
|
||||
// Parse current infoBox to get existing events
|
||||
const lines = (committedTrackerData.infoBox || '').split('\n');
|
||||
let recentEvents = [];
|
||||
|
||||
// First check structured infoBoxData (from JSON parsing)
|
||||
if (extensionSettings.infoBoxData?.recentEvents) {
|
||||
const events = extensionSettings.infoBoxData.recentEvents;
|
||||
if (Array.isArray(events)) {
|
||||
// Get all valid events, preserving order (max 3)
|
||||
recentEvents = events.filter(e => e && e !== 'null').slice(0, 3);
|
||||
} else if (typeof events === 'string' && events !== 'null') {
|
||||
recentEvents = [events];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to text format from committedTrackerData
|
||||
if (recentEvents.length === 0 && committedTrackerData.infoBox) {
|
||||
const lines = (committedTrackerData.infoBox || '').split('\n');
|
||||
const recentEventsLine = lines.find(line => line.startsWith('Recent Events:'));
|
||||
if (recentEventsLine) {
|
||||
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
|
||||
if (eventsString) {
|
||||
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e).slice(0, 3);
|
||||
}
|
||||
|
||||
// Find existing Recent Events line
|
||||
const recentEventsLine = lines.find(line => line.startsWith('Recent Events:'));
|
||||
if (recentEventsLine) {
|
||||
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
|
||||
if (eventsString) {
|
||||
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out placeholder text - treat it as empty
|
||||
const placeholderText = i18n.getTranslation('infobox.recentEvents.addEventPlaceholder');
|
||||
const cleanedValue = (value === placeholderText || value === 'Add event...' || value === 'Click to add event') ? '' : value.trim();
|
||||
|
||||
// Update the specific event in the array
|
||||
// Ensure array has enough slots for the index we're updating
|
||||
// Ensure array has enough slots
|
||||
while (recentEvents.length <= eventIndex) {
|
||||
recentEvents.push('');
|
||||
}
|
||||
|
||||
// Update the specific event
|
||||
recentEvents[eventIndex] = cleanedValue;
|
||||
|
||||
// Filter out empty events for final storage (but preserve order of non-empty ones)
|
||||
recentEvents[eventIndex] = value;
|
||||
|
||||
// Filter out empty events and rebuild the line
|
||||
const validEvents = recentEvents.filter(e => e && e.trim());
|
||||
|
||||
const newRecentEventsLine = validEvents.length > 0
|
||||
? `Recent Events: ${validEvents.join(', ')}`
|
||||
: '';
|
||||
|
||||
// Update infoBox with new Recent Events line
|
||||
// Need to get lines from committedTrackerData if we haven't already
|
||||
let lines = [];
|
||||
if (committedTrackerData.infoBox) {
|
||||
lines = committedTrackerData.infoBox.split('\n');
|
||||
}
|
||||
const updatedLines = lines.filter(line => !line.startsWith('Recent Events:'));
|
||||
if (newRecentEventsLine) {
|
||||
// Add Recent Events line at the end (before any empty lines)
|
||||
@@ -934,14 +855,6 @@ function updateRecentEvent(field, value) {
|
||||
committedTrackerData.infoBox = updatedLines.join('\n');
|
||||
lastGeneratedData.infoBox = updatedLines.join('\n');
|
||||
|
||||
// Also update the structured data to keep it in sync
|
||||
// Store only valid events (renderInfoBox will handle showing placeholders for empty slots)
|
||||
// This prevents renderInfoBox() from using stale structured data
|
||||
if (!extensionSettings.infoBoxData) {
|
||||
extensionSettings.infoBoxData = {};
|
||||
}
|
||||
extensionSettings.infoBoxData.recentEvents = validEvents;
|
||||
|
||||
// Update the message's swipe data
|
||||
const chat = getContext().chat;
|
||||
if (chat && chat.length > 0) {
|
||||
|
||||
@@ -8,7 +8,6 @@ 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 } from './skills.js';
|
||||
|
||||
// Type imports
|
||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||
@@ -24,23 +23,6 @@ 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')
|
||||
@@ -62,33 +44,6 @@ export function renderInventorySubTabs(activeTab = 'onPerson') {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the description for an item from structured inventory data
|
||||
* @param {string} field - Field type ('onPerson', 'stored', 'assets', 'simplified')
|
||||
* @param {number} index - Item index
|
||||
* @param {string} [location] - Location name for stored items
|
||||
* @returns {string} Item description or empty string
|
||||
*/
|
||||
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];
|
||||
} else if (field === 'simplified') {
|
||||
items = inv3.simplified;
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -104,38 +59,24 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
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}">
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-card" 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) => {
|
||||
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>
|
||||
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>
|
||||
</div>
|
||||
`}).join('');
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,38 +181,24 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
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}">
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-card" 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) => {
|
||||
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>
|
||||
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>
|
||||
</div>
|
||||
`}).join('');
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,38 +277,24 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
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}">
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-card" 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) => {
|
||||
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>
|
||||
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>
|
||||
</div>
|
||||
`}).join('');
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,9 +356,18 @@ function generateInventoryHTML(inventory, options = {}) {
|
||||
collapsedLocations = []
|
||||
} = options;
|
||||
|
||||
// Ensure v2 structure has all required fields
|
||||
// Note: Migration functions handle v1→v2 conversion on load, so inventory should always be v2 here
|
||||
// Handle legacy v1 format - convert to v2 for display
|
||||
let v2Inventory = inventory;
|
||||
if (typeof inventory === 'string') {
|
||||
v2Inventory = {
|
||||
version: 2,
|
||||
onPerson: inventory,
|
||||
stored: {},
|
||||
assets: 'None'
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure v2 structure has all required fields
|
||||
if (!v2Inventory || typeof v2Inventory !== 'object') {
|
||||
v2Inventory = {
|
||||
version: 2,
|
||||
@@ -522,113 +444,6 @@ export function updateInventoryDisplay(containerId, options = {}) {
|
||||
restoreFormStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the simplified (single-list) inventory view
|
||||
* Used when useSimplifiedInventory setting is enabled
|
||||
* @param {string} itemsString - All items as a comma-separated string
|
||||
* @param {string} viewMode - View mode ('list' or 'grid')
|
||||
* @returns {string} HTML for simplified inventory view
|
||||
*/
|
||||
export function renderSimplifiedInventoryView(itemsString, viewMode = 'list') {
|
||||
const items = parseItems(itemsString);
|
||||
|
||||
let itemsHtml = '';
|
||||
if (items.length === 0) {
|
||||
itemsHtml = `<div class="rpg-inventory-empty" data-i18n-key="inventory.simplified.empty">${i18n.getTranslation('inventory.simplified.empty')}</div>`;
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items (same as onPerson)
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('simplified', index);
|
||||
return `
|
||||
<div class="rpg-item-card ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="simplified" data-index="${index}">
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="simplified" data-index="${index}" title="${i18n.getTranslation('inventory.simplified.removeTitle')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<div class="rpg-item-desc-row">
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(desc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`}).join('');
|
||||
} else {
|
||||
// List view: full-width rows (same as onPerson)
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('simplified', index);
|
||||
return `
|
||||
<div class="rpg-item-row ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="simplified" data-index="${index}">
|
||||
<div class="rpg-item-main-row">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="simplified" data-index="${index}" title="${i18n.getTranslation('inventory.simplified.removeTitle')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-item-desc-row">
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(desc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view';
|
||||
|
||||
return `
|
||||
<div class="rpg-inventory-container">
|
||||
<div class="rpg-inventory-section" data-section="simplified">
|
||||
<div class="rpg-inventory-header">
|
||||
<h4 data-i18n-key="inventory.simplified.title">${i18n.getTranslation('inventory.simplified.title')}</h4>
|
||||
<div class="rpg-inventory-header-actions">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="simplified" 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-view" data-field="simplified" 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="simplified" title="Add new item">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="inventory.simplified.addItemButton">${i18n.getTranslation('inventory.simplified.addItemButton')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-inventory-content">
|
||||
<div class="rpg-inline-form" id="rpg-add-item-form-simplified" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-simplified" placeholder="${i18n.getTranslation('inventory.simplified.addItemPlaceholder')}" data-i18n-placeholder-key="inventory.simplified.addItemPlaceholder" />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="simplified">
|
||||
<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-item" data-field="simplified">
|
||||
<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>
|
||||
</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) ||
|
||||
(inv.simplified && inv.simplified.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main inventory rendering function (matches pattern of other render functions)
|
||||
* Gets data from state/settings and updates DOM directly.
|
||||
@@ -640,43 +455,17 @@ export function renderInventory() {
|
||||
return;
|
||||
}
|
||||
|
||||
let html;
|
||||
// Get inventory data from settings
|
||||
const inventory = extensionSettings.userStats.inventory;
|
||||
|
||||
// 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),
|
||||
// For simplified mode
|
||||
items: itemsToString(inv.simplified)
|
||||
};
|
||||
}
|
||||
|
||||
// Check if we should render simplified inventory
|
||||
if (extensionSettings.useSimplifiedInventory) {
|
||||
const itemsString = inventory.items || inventory.onPerson || 'None';
|
||||
const viewModes = extensionSettings.inventoryViewModes || {};
|
||||
const viewMode = viewModes.simplified || viewModes.onPerson || 'list';
|
||||
html = renderSimplifiedInventoryView(itemsString, viewMode);
|
||||
} else {
|
||||
const options = getInventoryRenderOptions();
|
||||
html = generateInventoryHTML(inventory, options);
|
||||
}
|
||||
// Get current render options (active tab, collapsed locations)
|
||||
const options = getInventoryRenderOptions();
|
||||
|
||||
// Generate HTML and update DOM
|
||||
const html = generateInventoryHTML(inventory, options);
|
||||
$inventoryContainer.html(html);
|
||||
|
||||
// Restore form states after re-rendering (fixes Bug #1)
|
||||
restoreFormStates();
|
||||
|
||||
// Event listener for editing item names (mobile-friendly contenteditable)
|
||||
@@ -687,45 +476,6 @@ 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+137
-154
@@ -1,11 +1,10 @@
|
||||
/**
|
||||
* Quests Rendering Module
|
||||
* Handles UI rendering for quests system (main and optional quests)
|
||||
* Uses the same structure and styling as items/skills
|
||||
*/
|
||||
|
||||
import { extensionSettings, $questsContainer } from '../../core/state.js';
|
||||
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
|
||||
/**
|
||||
@@ -19,25 +18,6 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the main quest (migration handles legacy format conversion)
|
||||
* @returns {{name: string, description: string}|null}
|
||||
*/
|
||||
function getMainQuest() {
|
||||
if (extensionSettings.questsV2?.main) {
|
||||
return extensionSettings.questsV2.main;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets optional quests (migration handles legacy format conversion)
|
||||
* @returns {Array<{name: string, description: string}>}
|
||||
*/
|
||||
function getOptionalQuests() {
|
||||
return extensionSettings.questsV2?.optional || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the quests sub-tab navigation (Main, Optional)
|
||||
* @param {string} activeTab - Currently active sub-tab ('main', 'optional')
|
||||
@@ -57,90 +37,89 @@ export function renderQuestsSubTabs(activeTab = 'main') {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the main quest view (matches items/skills structure)
|
||||
* Renders the main quest view
|
||||
* @param {string} mainQuest - Current main quest title
|
||||
* @returns {string} HTML for main quest view
|
||||
*/
|
||||
export function renderMainQuestView() {
|
||||
const quest = getMainQuest();
|
||||
const hasQuest = quest !== null;
|
||||
const questName = quest?.name || '';
|
||||
const questDesc = quest?.description || '';
|
||||
|
||||
// Track if add form is open
|
||||
const isFormOpen = openAddForms?.main || false;
|
||||
|
||||
let itemsHtml = '';
|
||||
if (hasQuest) {
|
||||
// Render quest as item (list view style, matching items/skills)
|
||||
itemsHtml = `
|
||||
<div class="rpg-item-row" data-field="main">
|
||||
<div class="rpg-item-main-row">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="main" data-prop="name" title="Click to edit">${escapeHtml(questName)}</span>
|
||||
<button class="rpg-item-remove" data-action="remove-quest" data-field="main" title="Complete/Remove quest">
|
||||
<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="main" data-prop="description" title="Click to edit description">${escapeHtml(questDesc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
export function renderMainQuestView(mainQuest) {
|
||||
const questDisplay = (mainQuest && mainQuest !== 'None') ? mainQuest : '';
|
||||
const hasQuest = questDisplay.length > 0;
|
||||
|
||||
return `
|
||||
<div class="rpg-quest-section">
|
||||
<div class="rpg-quest-header">
|
||||
<h3 class="rpg-quest-section-title" data-i18n-key="quests.main.title">${i18n.getTranslation('quests.main.title')}</h3>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-quest" data-field="main" title="${i18n.getTranslation('quests.main.addQuestTitle')}">
|
||||
${!hasQuest ? `<button class="rpg-add-quest-btn" data-action="add-quest" data-field="main" title="${i18n.getTranslation('quests.main.addQuestTitle')}">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
</button>
|
||||
</button>` : ''}
|
||||
</div>
|
||||
<div class="rpg-quest-content">
|
||||
<div class="rpg-inline-form" id="rpg-add-quest-form-main" style="display: ${isFormOpen ? 'flex' : 'none'};">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-main" placeholder="${i18n.getTranslation('quests.main.addQuestPlaceholder')}" data-i18n-placeholder-key="quests.main.addQuestPlaceholder" />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="main">
|
||||
<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-quest" data-field="main">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
</button>
|
||||
${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)}" />
|
||||
<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>
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-edit-quest" data-field="main">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.save">${i18n.getTranslation('global.save')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-item-list rpg-item-list-view">
|
||||
${itemsHtml || `<div class="rpg-inventory-empty" data-i18n-key="quests.main.empty">${i18n.getTranslation('quests.main.empty')}</div>`}
|
||||
</div>
|
||||
<div class="rpg-quest-item" data-field="main">
|
||||
<div class="rpg-quest-title">${escapeHtml(questDisplay)}</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>
|
||||
</button>
|
||||
<button class="rpg-quest-remove" data-action="remove-quest" data-field="main" title="Complete/Remove quest">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
` : `
|
||||
<div class="rpg-inline-form" id="rpg-add-quest-form-main" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-main" placeholder="${i18n.getTranslation('quests.main.addQuestPlaceholder')}" data-i18n-placeholder-key="quests.main.addQuestPlaceholder" />
|
||||
<div class="rpg-inline-actions">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="main">
|
||||
<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-quest" data-field="main">
|
||||
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-quest-empty" data-i18n-key="quests.main.empty">${i18n.getTranslation('quests.main.empty')}</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="rpg-quest-hint">
|
||||
<i class="fa-solid fa-lightbulb"></i>
|
||||
<span data-i18n-key="quests.main.hint">${i18n.getTranslation('quests.main.hint')}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the optional quests view (matches items/skills structure)
|
||||
* Renders the optional quests view
|
||||
* @param {string[]} optionalQuests - Array of optional quest titles
|
||||
* @returns {string} HTML for optional quests view
|
||||
*/
|
||||
export function renderOptionalQuestsView() {
|
||||
const quests = getOptionalQuests().filter(q => q && q.name && q.name !== 'None');
|
||||
|
||||
// Track if add form is open
|
||||
const isFormOpen = openAddForms?.optional || false;
|
||||
export function renderOptionalQuestsView(optionalQuests) {
|
||||
const quests = optionalQuests.filter(q => q && q !== 'None');
|
||||
|
||||
let itemsHtml = '';
|
||||
let questsHtml = '';
|
||||
if (quests.length === 0) {
|
||||
itemsHtml = `<div class="rpg-inventory-empty" data-i18n-key="quests.optional.empty">${i18n.getTranslation('quests.optional.empty')}</div>`;
|
||||
questsHtml = `<div class="rpg-quest-empty" data-i18n-key="quests.optional.empty">${i18n.getTranslation('quests.optional.empty')}</div>`;
|
||||
} else {
|
||||
// Render quests as items (list view style, matching items/skills)
|
||||
itemsHtml = quests.map((quest, index) => `
|
||||
<div class="rpg-item-row" data-field="optional" data-index="${index}">
|
||||
<div class="rpg-item-main-row">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" data-prop="name" title="Click to edit">${escapeHtml(quest.name)}</span>
|
||||
<button class="rpg-item-remove" data-action="remove-quest" data-field="optional" data-index="${index}" title="Complete/Remove quest">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
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-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>
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-item-desc-row">
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(quest.description || '')}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
@@ -149,12 +128,12 @@ export function renderOptionalQuestsView() {
|
||||
<div class="rpg-quest-section">
|
||||
<div class="rpg-quest-header">
|
||||
<h3 class="rpg-quest-section-title" data-i18n-key="quests.optional.title">${i18n.getTranslation('quests.optional.title')}</h3>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-quest" data-field="optional" title="${i18n.getTranslation('quests.optional.addQuestTitle')}">
|
||||
<button class="rpg-add-quest-btn" data-action="add-quest" data-field="optional" title="${i18n.getTranslation('quests.optional.addQuestTitle')}">
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-quest-content">
|
||||
<div class="rpg-inline-form" id="rpg-add-quest-form-optional" style="display: ${isFormOpen ? 'flex' : 'none'};">
|
||||
<div class="rpg-inline-form" id="rpg-add-quest-form-optional" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-optional" placeholder="${i18n.getTranslation('quests.optional.addQuestPlaceholder')}" data-i18n-placeholder-key="quests.optional.addQuestPlaceholder" />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="optional">
|
||||
@@ -165,28 +144,33 @@ export function renderOptionalQuestsView() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-item-list rpg-item-list-view">
|
||||
${itemsHtml}
|
||||
<div class="rpg-quest-list">
|
||||
${questsHtml}
|
||||
</div>
|
||||
<div class="rpg-quest-hint">
|
||||
<i class="fa-solid fa-info-circle"></i>
|
||||
<span data-i18n-key="quests.optional.hint">${i18n.getTranslation('quests.optional.hint')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Track open add forms (matching items/skills pattern)
|
||||
let openAddForms = {};
|
||||
|
||||
/**
|
||||
* Main render function for quests
|
||||
*/
|
||||
export function renderQuests() {
|
||||
if (!extensionSettings.showQuests || !$questsContainer) {
|
||||
if (!extensionSettings.showInventory || !$questsContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current sub-tab from container or default to 'main'
|
||||
const activeSubTab = $questsContainer.data('active-subtab') || 'main';
|
||||
|
||||
// Get quests data
|
||||
const mainQuest = extensionSettings.quests.main || 'None';
|
||||
const optionalQuests = extensionSettings.quests.optional || [];
|
||||
|
||||
// Build HTML
|
||||
let html = '<div class="rpg-quests-wrapper">';
|
||||
html += renderQuestsSubTabs(activeSubTab);
|
||||
@@ -194,9 +178,9 @@ export function renderQuests() {
|
||||
// Render active sub-tab
|
||||
html += '<div class="rpg-quests-panels">';
|
||||
if (activeSubTab === 'main') {
|
||||
html += renderMainQuestView();
|
||||
html += renderMainQuestView(mainQuest);
|
||||
} else {
|
||||
html += renderOptionalQuestsView();
|
||||
html += renderOptionalQuestsView(optionalQuests);
|
||||
}
|
||||
html += '</div></div>';
|
||||
|
||||
@@ -207,118 +191,117 @@ export function renderQuests() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach event handlers for quest interactions (matching items/skills pattern)
|
||||
* Attach event handlers for quest interactions
|
||||
*/
|
||||
function attachQuestEventHandlers() {
|
||||
// Sub-tab switching
|
||||
$questsContainer.find('.rpg-quests-subtab').off('click').on('click', function() {
|
||||
$questsContainer.find('.rpg-quests-subtab').on('click', function() {
|
||||
const tab = $(this).data('tab');
|
||||
$questsContainer.data('active-subtab', tab);
|
||||
renderQuests();
|
||||
});
|
||||
|
||||
// Add quest button
|
||||
$questsContainer.find('[data-action="add-quest"]').off('click').on('click', function() {
|
||||
$questsContainer.find('[data-action="add-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
openAddForms[field] = true;
|
||||
renderQuests();
|
||||
setTimeout(() => {
|
||||
$(`#rpg-new-quest-${field}`).focus();
|
||||
}, 50);
|
||||
$(`#rpg-add-quest-form-${field}`).show();
|
||||
$(`#rpg-new-quest-${field}`).focus();
|
||||
});
|
||||
|
||||
// Cancel add quest
|
||||
$questsContainer.find('[data-action="cancel-add-quest"]').off('click').on('click', function() {
|
||||
$questsContainer.find('[data-action="cancel-add-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
openAddForms[field] = false;
|
||||
$(`#rpg-add-quest-form-${field}`).hide();
|
||||
$(`#rpg-new-quest-${field}`).val('');
|
||||
renderQuests();
|
||||
});
|
||||
|
||||
// Save add quest
|
||||
$questsContainer.find('[data-action="save-add-quest"]').off('click').on('click', function() {
|
||||
$questsContainer.find('[data-action="save-add-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
const nameInput = $(`#rpg-new-quest-${field}`);
|
||||
const questTitle = nameInput.val().trim();
|
||||
const input = $(`#rpg-new-quest-${field}`);
|
||||
const questTitle = input.val().trim();
|
||||
|
||||
if (questTitle) {
|
||||
// Ensure structured format exists
|
||||
if (!extensionSettings.questsV2) {
|
||||
extensionSettings.questsV2 = { main: null, optional: [] };
|
||||
}
|
||||
|
||||
if (field === 'main') {
|
||||
extensionSettings.questsV2.main = { name: questTitle, description: '' };
|
||||
extensionSettings.quests.main = questTitle;
|
||||
} else {
|
||||
if (!extensionSettings.questsV2.optional) {
|
||||
extensionSettings.questsV2.optional = [];
|
||||
if (!extensionSettings.quests.optional) {
|
||||
extensionSettings.quests.optional = [];
|
||||
}
|
||||
extensionSettings.questsV2.optional.push({ name: questTitle, description: '' });
|
||||
extensionSettings.quests.optional.push(questTitle);
|
||||
}
|
||||
|
||||
openAddForms[field] = false;
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
renderQuests();
|
||||
}
|
||||
});
|
||||
|
||||
// Edit quest (main only)
|
||||
$questsContainer.find('[data-action="edit-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
$(`#rpg-edit-quest-form-${field}`).show();
|
||||
$('.rpg-quest-item[data-field="main"]').hide();
|
||||
$(`#rpg-edit-quest-${field}`).focus();
|
||||
});
|
||||
|
||||
// Cancel edit quest
|
||||
$questsContainer.find('[data-action="cancel-edit-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
$(`#rpg-edit-quest-form-${field}`).hide();
|
||||
$('.rpg-quest-item[data-field="main"]').show();
|
||||
});
|
||||
|
||||
// Save edit quest
|
||||
$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();
|
||||
|
||||
if (questTitle) {
|
||||
extensionSettings.quests.main = questTitle;
|
||||
saveSettings();
|
||||
renderQuests();
|
||||
}
|
||||
});
|
||||
|
||||
// Remove quest
|
||||
$questsContainer.find('[data-action="remove-quest"]').off('click').on('click', function() {
|
||||
$questsContainer.find('[data-action="remove-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
const index = $(this).data('index');
|
||||
|
||||
if (field === 'main') {
|
||||
if (extensionSettings.questsV2) {
|
||||
extensionSettings.questsV2.main = null;
|
||||
}
|
||||
extensionSettings.quests.main = 'None';
|
||||
} else {
|
||||
if (extensionSettings.questsV2?.optional) {
|
||||
extensionSettings.questsV2.optional.splice(index, 1);
|
||||
}
|
||||
extensionSettings.quests.optional.splice(index, 1);
|
||||
}
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
renderQuests();
|
||||
});
|
||||
|
||||
// Inline editing for quests (name and description) - matching items/skills pattern
|
||||
$questsContainer.off('blur', '.rpg-item-name.rpg-editable, .rpg-item-description.rpg-editable')
|
||||
.on('blur', '.rpg-item-name.rpg-editable, .rpg-item-description.rpg-editable', function() {
|
||||
// Inline editing for optional quests
|
||||
$questsContainer.find('.rpg-quest-title.rpg-editable').on('blur', function() {
|
||||
const $this = $(this);
|
||||
const field = $this.data('field');
|
||||
const index = $this.data('index');
|
||||
const prop = $this.data('prop') || 'name';
|
||||
const newValue = $this.text().trim();
|
||||
const newTitle = $this.text().trim();
|
||||
|
||||
// Ensure structured format exists
|
||||
if (!extensionSettings.questsV2) {
|
||||
extensionSettings.questsV2 = { main: null, optional: [] };
|
||||
if (newTitle && field === 'optional' && index !== undefined) {
|
||||
extensionSettings.quests.optional[index] = newTitle;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
if (field === 'main') {
|
||||
// Update main quest
|
||||
if (!extensionSettings.questsV2.main) {
|
||||
extensionSettings.questsV2.main = { name: '', description: '' };
|
||||
}
|
||||
extensionSettings.questsV2.main[prop] = newValue;
|
||||
} else if (field === 'optional' && index !== undefined) {
|
||||
// Update optional quest
|
||||
if (!extensionSettings.questsV2.optional[index]) {
|
||||
extensionSettings.questsV2.optional[index] = { name: '', description: '' };
|
||||
}
|
||||
extensionSettings.questsV2.optional[index][prop] = newValue;
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
});
|
||||
|
||||
// Enter key to save in forms (matching items/skills pattern)
|
||||
$questsContainer.find('.rpg-inline-input').off('keypress').on('keypress', function(e) {
|
||||
// Enter key to save in forms
|
||||
$questsContainer.find('.rpg-inline-input').on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
const field = $(this).attr('id').replace('rpg-new-quest-', '');
|
||||
$(`[data-action="save-add-quest"][data-field="${field}"]`).click();
|
||||
const field = $(this).attr('id').includes('edit') ?
|
||||
$(this).attr('id').replace('rpg-edit-quest-', '') :
|
||||
$(this).attr('id').replace('rpg-new-quest-', '');
|
||||
|
||||
if ($(this).attr('id').includes('edit')) {
|
||||
$(`[data-action="save-edit-quest"][data-field="${field}"]`).click();
|
||||
} else {
|
||||
$(`[data-action="save-add-quest"][data-field="${field}"]`).click();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -101,6 +101,11 @@ function namesMatch(cardName, aiName) {
|
||||
return wordBoundary.test(aiCore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders character thoughts (Present Characters) panel.
|
||||
* Displays character cards with avatars, relationship badges, and traits.
|
||||
* Includes event listeners for editable character fields.
|
||||
*/
|
||||
export function renderThoughts() {
|
||||
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
|
||||
return;
|
||||
@@ -122,49 +127,9 @@ 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;
|
||||
|
||||
// Convert structured character data to text format for the original fancy renderer
|
||||
// Use nullish coalescing so an empty string from the latest response clears UI
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// Use committedTrackerData as fallback if lastGeneratedData is empty (e.g., after page refresh)
|
||||
const characterThoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts || '';
|
||||
|
||||
debugLog('[RPG Thoughts] Raw characterThoughts data:', characterThoughtsData);
|
||||
debugLog('[RPG Thoughts] Data length:', characterThoughtsData.length + ' chars');
|
||||
@@ -411,16 +376,14 @@ 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 emoji
|
||||
let relationshipText = 'Neutral'; // Default text for tooltip
|
||||
let relationshipBadge = '⚖️'; // Default
|
||||
let relationshipFieldName = 'Relationship';
|
||||
|
||||
if (hasRelationshipEnabled) {
|
||||
// In the new format, relationship is always stored in char.Relationship
|
||||
if (char.Relationship) {
|
||||
relationshipText = char.Relationship;
|
||||
// Try to map text to emoji, fall back to default link emoji for unknown types
|
||||
relationshipBadge = relationshipEmojis[char.Relationship] || '⚖️';
|
||||
// Try to map text to emoji
|
||||
relationshipBadge = relationshipEmojis[char.Relationship] || char.Relationship;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,14 +396,13 @@ 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="${escapeHtmlAttr(relationshipText)}">${relationshipBadge}</div>` : ''}
|
||||
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
|
||||
</div>
|
||||
<div class="rpg-character-content">
|
||||
<div class="rpg-character-info">
|
||||
<div class="rpg-character-header">
|
||||
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
|
||||
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="name" title="Click to edit name">${char.name}</span>
|
||||
<button class="rpg-character-remove" data-character="${escapedName}" title="Remove character">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -504,15 +466,6 @@ export function renderThoughts() {
|
||||
updateCharacterField(character, field, value);
|
||||
});
|
||||
|
||||
// Add event handlers for remove character buttons
|
||||
$thoughtsContainer.find('.rpg-character-remove').on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const characterName = $(this).data('character');
|
||||
if (characterName && confirm(`Remove ${characterName} from present characters?`)) {
|
||||
removeCharacter(characterName);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove updating class after animation
|
||||
if (extensionSettings.enableAnimations) {
|
||||
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
|
||||
@@ -752,114 +705,17 @@ export function updateCharacterField(characterName, field, value) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a character from Present Characters data and re-renders.
|
||||
* Works with both structured (charactersData) and text (characterThoughts) formats.
|
||||
*
|
||||
* @param {string} characterName - Name of the character to remove
|
||||
*/
|
||||
export function removeCharacter(characterName) {
|
||||
console.log('[RPG Companion] Removing character:', characterName);
|
||||
|
||||
// Remove from structured data if it exists
|
||||
if (extensionSettings.charactersData && Array.isArray(extensionSettings.charactersData)) {
|
||||
const initialLength = extensionSettings.charactersData.length;
|
||||
extensionSettings.charactersData = extensionSettings.charactersData.filter(
|
||||
char => char.name && char.name.toLowerCase() !== characterName.toLowerCase()
|
||||
);
|
||||
if (extensionSettings.charactersData.length < initialLength) {
|
||||
console.log('[RPG Companion] Removed character from structured data');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from text format
|
||||
if (!lastGeneratedData.characterThoughts) {
|
||||
console.log('[RPG Companion] No characterThoughts data to remove from');
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = lastGeneratedData.characterThoughts.split('\n');
|
||||
|
||||
let characterFound = false;
|
||||
let inTargetCharacter = false;
|
||||
let characterEndIndex = -1;
|
||||
const linesToRemove = [];
|
||||
|
||||
// Find the character block
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (line.startsWith('- ')) {
|
||||
const name = line.substring(2).trim();
|
||||
if (name.toLowerCase() === characterName.toLowerCase()) {
|
||||
characterFound = true;
|
||||
inTargetCharacter = true;
|
||||
linesToRemove.push(i);
|
||||
} else if (inTargetCharacter) {
|
||||
characterEndIndex = i;
|
||||
break;
|
||||
}
|
||||
} else if (inTargetCharacter) {
|
||||
// Include all lines until the next character or end of file
|
||||
linesToRemove.push(i);
|
||||
// Check if this is a character name line (next character)
|
||||
if (line.startsWith('- ')) {
|
||||
characterEndIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (characterFound && characterEndIndex === -1) {
|
||||
characterEndIndex = lines.length;
|
||||
}
|
||||
|
||||
if (characterFound && linesToRemove.length > 0) {
|
||||
// Remove lines in reverse order to maintain indices
|
||||
for (let i = linesToRemove.length - 1; i >= 0; i--) {
|
||||
lines.splice(linesToRemove[i], 1);
|
||||
}
|
||||
|
||||
// Clean up any trailing empty lines after removal
|
||||
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
lastGeneratedData.characterThoughts = lines.join('\n');
|
||||
committedTrackerData.characterThoughts = lines.join('\n');
|
||||
|
||||
console.log('[RPG Companion] Removed character from text format');
|
||||
|
||||
// Update chat swipe data
|
||||
const chat = getContext().chat;
|
||||
if (chat && chat.length > 0) {
|
||||
for (let i = chat.length - 1; i >= 0; i--) {
|
||||
const message = chat[i];
|
||||
if (!message.is_user) {
|
||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||
const swipeId = message.swipe_id || 0;
|
||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||
message.extra.rpg_companion_swipes[swipeId].characterThoughts = lines.join('\n');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveChatData();
|
||||
renderThoughts();
|
||||
updateChatThoughts();
|
||||
} else {
|
||||
console.log('[RPG Companion] Character not found in text format:', characterName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates or removes thought overlays in the chat.
|
||||
* Creates floating thought bubbles positioned near character avatars.
|
||||
*/
|
||||
export function updateChatThoughts() {
|
||||
// console.log('[RPG Companion] ======== updateChatThoughts called ========');
|
||||
// console.log('[RPG Companion] Extension enabled:', extensionSettings.enabled);
|
||||
// console.log('[RPG Companion] showThoughtsInChat setting:', extensionSettings.showThoughtsInChat);
|
||||
// console.log('[RPG Companion] Toggle element checked:', $('#rpg-toggle-thoughts-in-chat').prop('checked'));
|
||||
// console.log('[RPG Companion] lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts);
|
||||
|
||||
// Remove existing thought panel and icon
|
||||
$('#rpg-thought-panel').remove();
|
||||
$('#rpg-thought-icon').remove();
|
||||
@@ -867,7 +723,9 @@ export function updateChatThoughts() {
|
||||
$(window).off('resize.thoughtPanel');
|
||||
$(document).off('click.thoughtPanel');
|
||||
|
||||
// If extension is disabled, thoughts in chat are disabled, or no thoughts, just return
|
||||
if (!extensionSettings.enabled || !extensionSettings.showThoughtsInChat || !lastGeneratedData.characterThoughts) {
|
||||
// console.log('[RPG Companion] Thoughts in chat disabled or no data');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -877,6 +735,8 @@ export function updateChatThoughts() {
|
||||
const thoughtsConfig = extensionSettings.trackerConfig?.presentCharacters?.thoughts;
|
||||
const thoughtsLabel = thoughtsConfig?.name || 'Thoughts';
|
||||
|
||||
// console.log('[RPG Companion] Parsing thoughts from lines:', lines);
|
||||
|
||||
// Parse new format to build character map and thoughts
|
||||
let currentCharName = null;
|
||||
let currentCharEmoji = null;
|
||||
@@ -933,9 +793,13 @@ export function updateChatThoughts() {
|
||||
|
||||
// If no thoughts parsed, return
|
||||
if (thoughtsArray.length === 0) {
|
||||
// console.log('[RPG Companion] No thoughts parsed, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('[RPG Companion] Total thoughts:', thoughtsArray.length);
|
||||
// console.log('[RPG Companion] Thoughts array:', thoughtsArray);
|
||||
|
||||
// Find the last message to position near
|
||||
const $messages = $('#chat .mes');
|
||||
let $targetMessage = null;
|
||||
@@ -950,6 +814,7 @@ export function updateChatThoughts() {
|
||||
}
|
||||
|
||||
if (!$targetMessage) {
|
||||
// console.log('[RPG Companion] No target message found');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -969,8 +834,10 @@ export function createThoughtPanel($message, thoughtsArray) {
|
||||
$('#rpg-thought-panel').remove();
|
||||
$('#rpg-thought-icon').remove();
|
||||
|
||||
// Get the avatar position from the message
|
||||
const $avatar = $message.find('.avatar img');
|
||||
if (!$avatar.length) {
|
||||
// console.log('[RPG Companion] No avatar found in message');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1167,10 +1034,14 @@ export function createThoughtPanel($message, thoughtsArray) {
|
||||
});
|
||||
}
|
||||
|
||||
// console.log('[RPG Companion] Thought panel created at:', { top, left });
|
||||
|
||||
// Add event handlers for editable thoughts in the bubble
|
||||
$thoughtPanel.find('.rpg-editable').on('blur', function() {
|
||||
const character = $(this).data('character');
|
||||
const field = $(this).data('field');
|
||||
const value = $(this).text().trim();
|
||||
// console.log('[RPG Companion] 💭 Thought bubble blur event - character:', character, 'field:', field, 'value:', value);
|
||||
updateCharacterField(character, field, value);
|
||||
});
|
||||
|
||||
|
||||
@@ -55,15 +55,13 @@ export function buildUserStatsText() {
|
||||
text += `${stats.conditions || 'None'}\n`;
|
||||
}
|
||||
|
||||
// Add inventory summary only if inventory is enabled
|
||||
if (extensionSettings.showInventory) {
|
||||
// Add inventory summary
|
||||
const inventorySummary = buildInventorySummary(stats.inventory);
|
||||
text += inventorySummary;
|
||||
}
|
||||
|
||||
// Add skills if enabled AND not shown in separate tab
|
||||
if (config.skillsSection.enabled && !extensionSettings.showSkills) {
|
||||
text += `\n${config.skillsSection.label}: ${extensionSettings.userStats?.skills || 'None'}`;
|
||||
// Add skills if enabled
|
||||
if (config.skillsSection.enabled && stats.skills) {
|
||||
text += `\n${config.skillsSection.label}: ${stats.skills}`;
|
||||
}
|
||||
|
||||
return text.trim();
|
||||
@@ -82,19 +80,19 @@ export function renderUserStats() {
|
||||
const stats = extensionSettings.userStats;
|
||||
const config = extensionSettings.trackerConfig?.userStats || {
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', description: '', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', description: '', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', description: '', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', description: '', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', description: '', enabled: true }
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
||||
],
|
||||
rpgAttributes: [
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
],
|
||||
statusSection: { enabled: true, showMoodEmoji: true, customFields: ['Conditions'] },
|
||||
skillsSection: { enabled: false, label: 'Skills' }
|
||||
@@ -167,8 +165,8 @@ export function renderUserStats() {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Skills section (conditionally rendered) - only if NOT shown in separate tab
|
||||
if (config.skillsSection.enabled && !extensionSettings.showSkills) {
|
||||
// Skills section (conditionally rendered)
|
||||
if (config.skillsSection.enabled) {
|
||||
const skillsValue = stats.skills || 'None';
|
||||
html += `
|
||||
<div class="rpg-skills-section">
|
||||
@@ -187,12 +185,12 @@ export function renderUserStats() {
|
||||
if (showRPGAttributes) {
|
||||
// Use attributes from config, with fallback to defaults if not configured
|
||||
const rpgAttributes = (config.rpgAttributes && config.rpgAttributes.length > 0) ? config.rpgAttributes : [
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
];
|
||||
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
|
||||
|
||||
@@ -317,19 +315,23 @@ export function renderUserStats() {
|
||||
const field = $(this).data('field');
|
||||
const value = $(this).text().trim().replace(':', '');
|
||||
|
||||
// Update the stat name in customStats array (new format)
|
||||
const config = extensionSettings.trackerConfig?.userStats;
|
||||
if (config && config.customStats) {
|
||||
const stat = config.customStats.find(s => s.id === field);
|
||||
if (stat && value) {
|
||||
stat.name = value;
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
|
||||
// Re-render to update the display
|
||||
renderUserStats();
|
||||
}
|
||||
if (!extensionSettings.statNames) {
|
||||
extensionSettings.statNames = {
|
||||
health: 'Health',
|
||||
satiety: 'Satiety',
|
||||
energy: 'Energy',
|
||||
hygiene: 'Hygiene',
|
||||
arousal: 'Arousal'
|
||||
};
|
||||
}
|
||||
|
||||
extensionSettings.statNames[field] = value || extensionSettings.statNames[field];
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
|
||||
// Re-render to update the display
|
||||
renderUserStats();
|
||||
});
|
||||
|
||||
// Add event listener for level editing
|
||||
|
||||
Reference in New Issue
Block a user