Major update: Full tracker customization system
Features: - Complete tracker configuration UI with add/remove functionality - User Stats: Custom stats, status fields, skills section - Info Box: Configurable widgets (date, weather, temp, time, location, events) - Present Characters: Custom fields, relationships, character stats, thoughts - Character-specific stats with color interpolation - New multi-line format for cleaner AI generation and parsing - Auto-cleanup of placeholder brackets in AI responses - Relationship badges with emoji mapping - Advanced inventory v2 system with multi-location storage - Responsive mobile support with horizontal scrolling - Removed legacy format support for cleaner codebase - Fixed context injection for together mode (no duplication) - Updated README with new features and configuration guide
This commit is contained in:
+179
-119
@@ -270,151 +270,211 @@ export function renderInfoBox() {
|
||||
// location: data.location
|
||||
// });
|
||||
|
||||
// Get tracker configuration
|
||||
const config = extensionSettings.trackerConfig?.infoBox;
|
||||
|
||||
// Build visual dashboard HTML
|
||||
// Wrap all content in a scrollable container
|
||||
let html = '<div class="rpg-info-content">';
|
||||
|
||||
// Row 1: Date, Weather, Temperature, Time widgets
|
||||
html += '<div class="rpg-dashboard rpg-dashboard-row-1">';
|
||||
const row1Widgets = [];
|
||||
|
||||
// Calendar widget - always show (editable even if empty)
|
||||
// Display abbreviated version but allow editing full value
|
||||
const monthShort = data.month ? data.month.substring(0, 3).toUpperCase() : 'MON';
|
||||
const weekdayShort = data.weekday ? data.weekday.substring(0, 3).toUpperCase() : 'DAY';
|
||||
const yearDisplay = data.year || 'YEAR';
|
||||
html += `
|
||||
<div class="rpg-dashboard-widget rpg-calendar-widget">
|
||||
<div class="rpg-calendar-top rpg-editable" contenteditable="true" data-field="month" data-full-value="${data.month || ''}" title="Click to edit">${monthShort}</div>
|
||||
<div class="rpg-calendar-day rpg-editable" contenteditable="true" data-field="weekday" data-full-value="${data.weekday || ''}" title="Click to edit">${weekdayShort}</div>
|
||||
<div class="rpg-calendar-year rpg-editable" contenteditable="true" data-field="year" data-full-value="${data.year || ''}" title="Click to edit">${yearDisplay}</div>
|
||||
</div>
|
||||
`;
|
||||
// Calendar widget - show if enabled
|
||||
if (config?.widgets?.date?.enabled) {
|
||||
// Apply date format conversion
|
||||
let monthDisplay = data.month || 'MON';
|
||||
let weekdayDisplay = data.weekday || 'DAY';
|
||||
let yearDisplay = data.year || 'YEAR';
|
||||
|
||||
// Weather widget - always show (editable even if empty)
|
||||
const weatherEmoji = data.weatherEmoji || '🌤️';
|
||||
const weatherForecast = data.weatherForecast || 'Weather';
|
||||
html += `
|
||||
<div class="rpg-dashboard-widget rpg-weather-widget">
|
||||
<div class="rpg-weather-icon rpg-editable" contenteditable="true" data-field="weatherEmoji" title="Click to edit emoji">${weatherEmoji}</div>
|
||||
<div class="rpg-weather-forecast rpg-editable" contenteditable="true" data-field="weatherForecast" title="Click to edit">${weatherForecast}</div>
|
||||
</div>
|
||||
`;
|
||||
// Apply format based on config
|
||||
const dateFormat = config.widgets.date.format || 'dd/mm/yy';
|
||||
if (dateFormat === 'dd/mm/yy') {
|
||||
monthDisplay = monthDisplay.substring(0, 3).toUpperCase();
|
||||
weekdayDisplay = weekdayDisplay.substring(0, 3).toUpperCase();
|
||||
} else if (dateFormat === 'mm/dd/yy') {
|
||||
// For US format, show month first, day second
|
||||
monthDisplay = monthDisplay.substring(0, 3).toUpperCase();
|
||||
weekdayDisplay = weekdayDisplay.substring(0, 3).toUpperCase();
|
||||
} else if (dateFormat === 'yyyy-mm-dd') {
|
||||
// ISO format - show full names
|
||||
monthDisplay = monthDisplay;
|
||||
weekdayDisplay = weekdayDisplay;
|
||||
}
|
||||
|
||||
// Temperature widget - always show (editable even if empty)
|
||||
const tempDisplay = data.temperature || '20°C';
|
||||
const tempValue = data.tempValue || 20;
|
||||
const tempPercent = Math.min(100, Math.max(0, ((tempValue + 20) / 60) * 100));
|
||||
const tempColor = tempValue < 10 ? '#4a90e2' : tempValue < 25 ? '#67c23a' : '#e94560';
|
||||
html += `
|
||||
<div class="rpg-dashboard-widget rpg-temp-widget">
|
||||
<div class="rpg-thermometer">
|
||||
<div class="rpg-thermometer-bulb"></div>
|
||||
<div class="rpg-thermometer-tube">
|
||||
<div class="rpg-thermometer-fill" style="height: ${tempPercent}%; background: ${tempColor}"></div>
|
||||
</div>
|
||||
row1Widgets.push(`
|
||||
<div class="rpg-dashboard-widget rpg-calendar-widget">
|
||||
<div class="rpg-calendar-top rpg-editable" contenteditable="true" data-field="month" data-full-value="${data.month || ''}" title="Click to edit">${monthDisplay}</div>
|
||||
<div class="rpg-calendar-day rpg-editable" contenteditable="true" data-field="weekday" data-full-value="${data.weekday || ''}" title="Click to edit">${weekdayDisplay}</div>
|
||||
<div class="rpg-calendar-year rpg-editable" contenteditable="true" data-field="year" data-full-value="${data.year || ''}" title="Click to edit">${yearDisplay}</div>
|
||||
</div>
|
||||
<div class="rpg-temp-value rpg-editable" contenteditable="true" data-field="temperature" title="Click to edit">${tempDisplay}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Time widget - always show (editable even if empty)
|
||||
// Display the end time (second time in range) if available, otherwise start time
|
||||
const timeDisplay = data.timeEnd || data.timeStart || '12:00';
|
||||
// Parse time for clock hands
|
||||
const timeMatch = timeDisplay.match(/(\d+):(\d+)/);
|
||||
let hourAngle = 0;
|
||||
let minuteAngle = 0;
|
||||
if (timeMatch) {
|
||||
const hours = parseInt(timeMatch[1]);
|
||||
const minutes = parseInt(timeMatch[2]);
|
||||
hourAngle = (hours % 12) * 30 + minutes * 0.5; // 30° per hour + 0.5° per minute
|
||||
minuteAngle = minutes * 6; // 6° per minute
|
||||
`);
|
||||
}
|
||||
html += `
|
||||
<div class="rpg-dashboard-widget rpg-clock-widget">
|
||||
<div class="rpg-clock">
|
||||
<div class="rpg-clock-face">
|
||||
<div class="rpg-clock-hour" style="transform: rotate(${hourAngle}deg)"></div>
|
||||
<div class="rpg-clock-minute" style="transform: rotate(${minuteAngle}deg)"></div>
|
||||
<div class="rpg-clock-center"></div>
|
||||
|
||||
// Weather widget - show if enabled
|
||||
if (config?.widgets?.weather?.enabled) {
|
||||
const weatherEmoji = data.weatherEmoji || '🌤️';
|
||||
const weatherForecast = data.weatherForecast || 'Weather';
|
||||
row1Widgets.push(`
|
||||
<div class="rpg-dashboard-widget rpg-weather-widget">
|
||||
<div class="rpg-weather-icon rpg-editable" contenteditable="true" data-field="weatherEmoji" title="Click to edit emoji">${weatherEmoji}</div>
|
||||
<div class="rpg-weather-forecast rpg-editable" contenteditable="true" data-field="weatherForecast" title="Click to edit">${weatherForecast}</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Temperature widget - show if enabled
|
||||
if (config?.widgets?.temperature?.enabled) {
|
||||
let tempDisplay = data.temperature || '20°C';
|
||||
let tempValue = data.tempValue || 20;
|
||||
|
||||
// Apply temperature unit conversion
|
||||
const preferredUnit = config.widgets.temperature.unit || 'celsius';
|
||||
if (data.temperature) {
|
||||
// Detect current unit in the data
|
||||
const isCelsius = tempDisplay.includes('°C');
|
||||
const isFahrenheit = tempDisplay.includes('°F');
|
||||
|
||||
if (preferredUnit === 'fahrenheit' && isCelsius) {
|
||||
// Convert C to F
|
||||
const fahrenheit = Math.round((tempValue * 9/5) + 32);
|
||||
tempDisplay = `${fahrenheit}°F`;
|
||||
tempValue = fahrenheit;
|
||||
} else if (preferredUnit === 'celsius' && isFahrenheit) {
|
||||
// Convert F to C
|
||||
const celsius = Math.round((tempValue - 32) * 5/9);
|
||||
tempDisplay = `${celsius}°C`;
|
||||
tempValue = celsius;
|
||||
}
|
||||
} else {
|
||||
// No data yet, use default for preferred unit
|
||||
tempDisplay = preferredUnit === 'fahrenheit' ? '68°F' : '20°C';
|
||||
tempValue = preferredUnit === 'fahrenheit' ? 68 : 20;
|
||||
}
|
||||
|
||||
const tempPercent = Math.min(100, Math.max(0, ((tempValue + 20) / 60) * 100));
|
||||
const tempColor = tempValue < 10 ? '#4a90e2' : tempValue < 25 ? '#67c23a' : '#e94560';
|
||||
row1Widgets.push(`
|
||||
<div class="rpg-dashboard-widget rpg-temp-widget">
|
||||
<div class="rpg-thermometer">
|
||||
<div class="rpg-thermometer-bulb"></div>
|
||||
<div class="rpg-thermometer-tube">
|
||||
<div class="rpg-thermometer-fill" style="height: ${tempPercent}%; background: ${tempColor}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-temp-value rpg-editable" contenteditable="true" data-field="temperature" title="Click to edit">${tempDisplay}</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Time widget - show if enabled
|
||||
if (config?.widgets?.time?.enabled) {
|
||||
const timeDisplay = data.timeEnd || data.timeStart || '12:00';
|
||||
// Parse time for clock hands
|
||||
const timeMatch = timeDisplay.match(/(\d+):(\d+)/);
|
||||
let hourAngle = 0;
|
||||
let minuteAngle = 0;
|
||||
if (timeMatch) {
|
||||
const hours = parseInt(timeMatch[1]);
|
||||
const minutes = parseInt(timeMatch[2]);
|
||||
hourAngle = (hours % 12) * 30 + minutes * 0.5; // 30° per hour + 0.5° per minute
|
||||
minuteAngle = minutes * 6; // 6° per minute
|
||||
}
|
||||
row1Widgets.push(`
|
||||
<div class="rpg-dashboard-widget rpg-clock-widget">
|
||||
<div class="rpg-clock">
|
||||
<div class="rpg-clock-face">
|
||||
<div class="rpg-clock-hour" style="transform: rotate(${hourAngle}deg)"></div>
|
||||
<div class="rpg-clock-minute" style="transform: rotate(${minuteAngle}deg)"></div>
|
||||
<div class="rpg-clock-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeStart" title="Click to edit">${timeDisplay}</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Only create row 1 if there are widgets to show
|
||||
if (row1Widgets.length > 0) {
|
||||
html += '<div class="rpg-dashboard rpg-dashboard-row-1">';
|
||||
html += row1Widgets.join('');
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Row 2: Location widget (full width) - show if enabled
|
||||
if (config?.widgets?.location?.enabled) {
|
||||
const locationDisplay = data.location || 'Location';
|
||||
html += `
|
||||
<div class="rpg-dashboard rpg-dashboard-row-2">
|
||||
<div class="rpg-dashboard-widget rpg-location-widget">
|
||||
<div class="rpg-map-bg">
|
||||
<div class="rpg-map-marker">📍</div>
|
||||
</div>
|
||||
<div class="rpg-location-text rpg-editable" contenteditable="true" data-field="location" title="Click to edit">${locationDisplay}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeStart" title="Click to edit">${timeDisplay}</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Row 2: Location widget (full width) - always show (editable even if empty)
|
||||
const locationDisplay = data.location || 'Location';
|
||||
html += `
|
||||
<div class="rpg-dashboard rpg-dashboard-row-2">
|
||||
<div class="rpg-dashboard-widget rpg-location-widget">
|
||||
<div class="rpg-map-bg">
|
||||
<div class="rpg-map-marker">📍</div>
|
||||
</div>
|
||||
<div class="rpg-location-text rpg-editable" contenteditable="true" data-field="location" title="Click to edit">${locationDisplay}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Row 3: Recent Events widget (notebook style) - dynamically show 1-3 events
|
||||
// Parse Recent Events from infoBox string
|
||||
let recentEvents = [];
|
||||
if (committedTrackerData.infoBox) {
|
||||
const recentEventsLine = committedTrackerData.infoBox.split('\n').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);
|
||||
// Row 3: Recent Events widget (notebook style) - show if enabled
|
||||
if (config?.widgets?.recentEvents?.enabled) {
|
||||
// Parse Recent Events from infoBox string
|
||||
let recentEvents = [];
|
||||
if (committedTrackerData.infoBox) {
|
||||
const recentEventsLine = committedTrackerData.infoBox.split('\n').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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validEvents = recentEvents.filter(e => e && e.trim() && e !== 'Event 1' && e !== 'Event 2' && e !== 'Event 3');
|
||||
const validEvents = recentEvents.filter(e => e && e.trim() && e !== 'Event 1' && e !== 'Event 2' && e !== 'Event 3');
|
||||
|
||||
// If no valid events, show at least one placeholder
|
||||
if (validEvents.length === 0) {
|
||||
validEvents.push('Click to add event');
|
||||
}
|
||||
// If no valid events, show at least one placeholder
|
||||
if (validEvents.length === 0) {
|
||||
validEvents.push('Click to add event');
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="rpg-dashboard rpg-dashboard-row-3">
|
||||
<div class="rpg-dashboard-widget rpg-events-widget">
|
||||
<div class="rpg-notebook-header">
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
</div>
|
||||
<div class="rpg-notebook-title">Recent Events</div>
|
||||
<div class="rpg-notebook-lines">
|
||||
`;
|
||||
|
||||
// Dynamically generate event lines (max 3)
|
||||
for (let i = 0; i < Math.min(validEvents.length, 3); i++) {
|
||||
html += `
|
||||
<div class="rpg-notebook-line">
|
||||
<span class="rpg-bullet">•</span>
|
||||
<span class="rpg-event-text rpg-editable" contenteditable="true" data-field="event${i + 1}" title="Click to edit">${validEvents[i]}</span>
|
||||
<div class="rpg-dashboard rpg-dashboard-row-3">
|
||||
<div class="rpg-dashboard-widget rpg-events-widget">
|
||||
<div class="rpg-notebook-header">
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
</div>
|
||||
<div class="rpg-notebook-title">Recent Events</div>
|
||||
<div class="rpg-notebook-lines">
|
||||
`;
|
||||
}
|
||||
|
||||
// If we have less than 3 events, add empty placeholders with + icon
|
||||
for (let i = validEvents.length; i < 3; i++) {
|
||||
// Dynamically generate event lines (max 3)
|
||||
for (let i = 0; i < Math.min(validEvents.length, 3); i++) {
|
||||
html += `
|
||||
<div class="rpg-notebook-line">
|
||||
<span class="rpg-bullet">•</span>
|
||||
<span class="rpg-event-text rpg-editable" contenteditable="true" data-field="event${i + 1}" title="Click to edit">${validEvents[i]}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// If we have less than 3 events, add empty placeholders with + icon
|
||||
for (let i = validEvents.length; i < 3; i++) {
|
||||
html += `
|
||||
<div class="rpg-notebook-line rpg-event-add">
|
||||
<span class="rpg-bullet">+</span>
|
||||
<span class="rpg-event-text rpg-editable rpg-event-placeholder" contenteditable="true" data-field="event${i + 1}" title="Click to add event">Add event...</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="rpg-notebook-line rpg-event-add">
|
||||
<span class="rpg-bullet">+</span>
|
||||
<span class="rpg-event-text rpg-editable rpg-event-placeholder" contenteditable="true" data-field="event${i + 1}" title="Click to add event">Add event...</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
// Close the scrollable content wrapper
|
||||
html += '</div>';
|
||||
|
||||
+222
-106
@@ -27,6 +27,40 @@ function debugLog(message, data = null) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates color based on percentage value between low and high colors
|
||||
* @param {number} percentage - Value from 0-100
|
||||
* @param {string} lowColor - Hex color for low values (e.g., '#ff0000')
|
||||
* @param {string} highColor - Hex color for high values (e.g., '#00ff00')
|
||||
* @returns {string} Interpolated hex color
|
||||
*/
|
||||
function getStatColor(percentage, lowColor, highColor) {
|
||||
// Clamp percentage to 0-100
|
||||
const percent = Math.max(0, Math.min(100, percentage)) / 100;
|
||||
|
||||
// Parse hex colors
|
||||
const parsehex = (hex) => {
|
||||
const clean = hex.replace('#', '');
|
||||
return {
|
||||
r: parseInt(clean.substring(0, 2), 16),
|
||||
g: parseInt(clean.substring(2, 4), 16),
|
||||
b: parseInt(clean.substring(4, 6), 16)
|
||||
};
|
||||
};
|
||||
|
||||
const low = parsehex(lowColor);
|
||||
const high = parsehex(highColor);
|
||||
|
||||
// Interpolate each channel
|
||||
const r = Math.round(low.r + (high.r - low.r) * percent);
|
||||
const g = Math.round(low.g + (high.g - low.g) * percent);
|
||||
const b = Math.round(low.b + (high.b - low.b) * percent);
|
||||
|
||||
// Convert back to hex
|
||||
const toHex = (n) => n.toString(16).padStart(2, '0');
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy name matching that handles:
|
||||
* - Exact matches: "Sabrina" === "Sabrina"
|
||||
@@ -76,11 +110,21 @@ export function renderThoughts() {
|
||||
$thoughtsContainer.addClass('rpg-content-updating');
|
||||
}
|
||||
|
||||
// Get tracker configuration
|
||||
const config = extensionSettings.trackerConfig?.presentCharacters;
|
||||
const enabledFields = config?.customFields?.filter(f => f && f.enabled && f.name) || [];
|
||||
const characterStatsConfig = config?.characterStats;
|
||||
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 || '';
|
||||
|
||||
debugLog('[RPG Thoughts] Raw characterThoughts data:', characterThoughtsData);
|
||||
debugLog('[RPG Thoughts] Data length:', characterThoughtsData.length + ' chars');
|
||||
debugLog('[RPG Thoughts] Enabled custom fields:', enabledFields.map(f => f.name));
|
||||
debugLog('[RPG Thoughts] Enabled character stats:', enabledCharStats.map(s => s.name));
|
||||
|
||||
const lines = characterThoughtsData.split('\n');
|
||||
const presentCharacters = [];
|
||||
@@ -88,88 +132,96 @@ export function renderThoughts() {
|
||||
debugLog('[RPG Thoughts] Split into lines count:', lines.length);
|
||||
debugLog('[RPG Thoughts] Lines:', lines);
|
||||
|
||||
// Parse format: [Emoji]: [Name, Status, Demeanor] | [Relationship] | [Thoughts]
|
||||
// Also supports 4-part format: [Emoji]: [Name, Status] | [Demeanor] | [Relationship] | [Thoughts]
|
||||
// Parse new multi-line format:
|
||||
// - [Name]
|
||||
// Details: [Emoji] | [Field1] | [Field2] | ...
|
||||
// Relationship: [Relationship]
|
||||
// Stats: Stat1: X% | Stat2: X% | ...
|
||||
// Thoughts: [Description]
|
||||
let lineNumber = 0;
|
||||
let currentCharacter = null;
|
||||
|
||||
for (const line of lines) {
|
||||
lineNumber++;
|
||||
|
||||
// Skip empty lines, headers, dividers, and code fences
|
||||
if (line.trim() &&
|
||||
!line.includes('Present Characters') &&
|
||||
!line.includes('---') &&
|
||||
!line.trim().startsWith('```')) {
|
||||
if (!line.trim() ||
|
||||
line.includes('Present Characters') ||
|
||||
line.includes('---') ||
|
||||
line.trim().startsWith('```') ||
|
||||
line.trim() === '- …' ||
|
||||
line.includes('(Repeat the format')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
debugLog(`[RPG Thoughts] Processing line ${lineNumber}:`, line);
|
||||
debugLog(`[RPG Thoughts] Processing line ${lineNumber}:`, line);
|
||||
|
||||
// Match the new format with pipes
|
||||
const parts = line.split('|').map(p => p.trim());
|
||||
debugLog(`[RPG Thoughts] Split into ${parts.length} parts:`, parts);
|
||||
// Check if this is a character name line (starts with "- ")
|
||||
if (line.trim().startsWith('- ')) {
|
||||
const name = line.trim().substring(2).trim();
|
||||
|
||||
// Require at least 3 parts (Emoji:Name | Relationship | Thoughts)
|
||||
// This matches updateChatThoughts() and the current prompt format
|
||||
if (parts.length >= 3) {
|
||||
// First part: [Emoji]: [Name, Status, Demeanor]
|
||||
const firstPart = parts[0].trim();
|
||||
const emojiMatch = firstPart.match(/^(.+?):\s*(.+)$/);
|
||||
|
||||
if (emojiMatch) {
|
||||
const emoji = emojiMatch[1].trim();
|
||||
const info = emojiMatch[2].trim();
|
||||
|
||||
debugLog(`[RPG Thoughts] Emoji match found - emoji: "${emoji}", info: "${info}"`);
|
||||
|
||||
// Handle both 3-part and 4-part formats
|
||||
let relationship, thoughts, traits;
|
||||
|
||||
if (parts.length === 3) {
|
||||
// 3-part format: Emoji:Name,traits | Relationship | Thoughts
|
||||
relationship = parts[1].trim();
|
||||
thoughts = parts[2].trim();
|
||||
const infoParts = info.split(',').map(p => p.trim());
|
||||
traits = infoParts.slice(1).join(', ');
|
||||
debugLog('[RPG Thoughts] Parsed as 3-part format');
|
||||
} else {
|
||||
// 4-part format: Emoji:Name,traits | Demeanor | Relationship | Thoughts
|
||||
// Add the demeanor to traits and use last two parts for relationship/thoughts
|
||||
const demeanor = parts[1].trim();
|
||||
relationship = parts[2].trim();
|
||||
thoughts = parts[3].trim();
|
||||
const infoParts = info.split(',').map(p => p.trim());
|
||||
const baseTraits = infoParts.slice(1).join(', ');
|
||||
traits = baseTraits ? `${baseTraits}, ${demeanor}` : demeanor;
|
||||
debugLog('[RPG Thoughts] Parsed as 4-part format');
|
||||
}
|
||||
|
||||
// Parse name from info (first part before comma)
|
||||
const infoParts = info.split(',').map(p => p.trim());
|
||||
const name = infoParts[0] || '';
|
||||
|
||||
debugLog(`[RPG Thoughts] Extracted - name: "${name}", traits: "${traits}", relationship: "${relationship}", thoughts: "${thoughts}"`);
|
||||
|
||||
if (name && name.toLowerCase() !== 'unavailable') {
|
||||
presentCharacters.push({ emoji, name, traits, relationship, thoughts });
|
||||
debugLog(`[RPG Thoughts] ✓ Added character: ${name}`);
|
||||
} else {
|
||||
debugLog(`[RPG Thoughts] ✗ Rejected character - name: "${name}" (unavailable or empty)`);
|
||||
}
|
||||
} else {
|
||||
debugLog('[RPG Thoughts] ✗ No emoji match found in first part');
|
||||
}
|
||||
if (name && name.toLowerCase() !== 'unavailable') {
|
||||
currentCharacter = { name };
|
||||
presentCharacters.push(currentCharacter);
|
||||
debugLog(`[RPG Thoughts] ✓ Started new character: ${name}`);
|
||||
} else {
|
||||
debugLog(`[RPG Thoughts] ✗ Not enough parts (${parts.length} < 3, need at least Emoji:Name | Relationship | Thoughts)`);
|
||||
currentCharacter = null;
|
||||
debugLog(`[RPG Thoughts] ✗ Rejected character - name: "${name}" (unavailable or empty)`);
|
||||
}
|
||||
}
|
||||
// Check if this is a Details line
|
||||
else if (line.trim().startsWith('Details:') && currentCharacter) {
|
||||
const detailsContent = line.substring(line.indexOf(':') + 1).trim();
|
||||
const parts = detailsContent.split('|').map(p => p.trim());
|
||||
|
||||
// First part is the emoji
|
||||
if (parts.length > 0) {
|
||||
currentCharacter.emoji = parts[0];
|
||||
debugLog(`[RPG Thoughts] Parsed emoji: ${parts[0]}`);
|
||||
}
|
||||
|
||||
// Remaining parts are custom fields
|
||||
for (let i = 0; i < enabledFields.length && i + 1 < parts.length; i++) {
|
||||
const fieldName = enabledFields[i].name;
|
||||
currentCharacter[fieldName] = parts[i + 1];
|
||||
debugLog(`[RPG Thoughts] Parsed field ${fieldName}: ${parts[i + 1]}`);
|
||||
}
|
||||
}
|
||||
// Check if this is a Relationship line
|
||||
else if (line.trim().startsWith('Relationship:') && currentCharacter) {
|
||||
const relationship = line.substring(line.indexOf(':') + 1).trim();
|
||||
currentCharacter.Relationship = relationship;
|
||||
debugLog(`[RPG Thoughts] Parsed relationship: ${relationship}`);
|
||||
}
|
||||
// Check if this is a Stats line
|
||||
else if (line.trim().startsWith('Stats:') && currentCharacter && enabledCharStats.length > 0) {
|
||||
const statsContent = line.substring(line.indexOf(':') + 1).trim();
|
||||
const statParts = statsContent.split('|').map(p => p.trim());
|
||||
|
||||
for (const statPart of statParts) {
|
||||
const statMatch = statPart.match(/^(.+?):\s*(\d+)%$/);
|
||||
if (statMatch) {
|
||||
const statName = statMatch[1].trim();
|
||||
const statValue = parseInt(statMatch[2]);
|
||||
currentCharacter[statName] = statValue;
|
||||
debugLog(`[RPG Thoughts] Parsed stat: ${statName} = ${statValue}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if this is a Thoughts line (handled separately for thought bubbles)
|
||||
else if (line.trim().match(/^[A-Z][a-z]+:/) && currentCharacter) {
|
||||
// This could be Thoughts, Feelings, etc. - skip for now, handled in thought bubble rendering
|
||||
debugLog(`[RPG Thoughts] Skipping thoughts/feelings line (handled in bubble rendering)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Relationship status to emoji mapping
|
||||
// Relationship status to emoji mapping (for backward compatibility with old "relationship" field)
|
||||
const relationshipEmojis = {
|
||||
'Enemy': '⚔️',
|
||||
'Neutral': '⚖️',
|
||||
'Friend': '⭐',
|
||||
'Lover': '❤️'
|
||||
};
|
||||
|
||||
debugLog('[RPG Thoughts] ==================== PARSING COMPLETE ====================');
|
||||
debugLog('[RPG Thoughts] Total characters parsed:', presentCharacters.length);
|
||||
debugLog('[RPG Thoughts] Characters array:', presentCharacters);
|
||||
@@ -183,8 +235,7 @@ export function renderThoughts() {
|
||||
// If no characters parsed, show a placeholder editable card
|
||||
if (presentCharacters.length === 0) {
|
||||
debugLog('[RPG Thoughts] ⚠ No characters parsed - showing placeholder card');
|
||||
// Get default character portrait (try to use the current character if in 1-on-1 chat)
|
||||
// Use a base64-encoded SVG placeholder as fallback to avoid 400 errors
|
||||
// Get default character portrait
|
||||
let defaultPortrait = FALLBACK_AVATAR_DATA_URI;
|
||||
let defaultName = 'Character';
|
||||
|
||||
@@ -210,7 +261,17 @@ export function renderThoughts() {
|
||||
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="emoji" title="Click to edit emoji">😊</span>
|
||||
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="name" title="Click to edit name">${defaultName}</span>
|
||||
</div>
|
||||
<div class="rpg-character-traits rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="traits" title="Click to edit traits">Traits</div>
|
||||
`;
|
||||
|
||||
// Add custom fields dynamically
|
||||
for (const field of enabledFields) {
|
||||
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
|
||||
html += `
|
||||
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="${field.name}" title="Click to edit ${field.name}"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -286,8 +347,17 @@ export function renderThoughts() {
|
||||
|
||||
debugLog(`[RPG Thoughts] Final avatar for ${char.name}:`, characterPortrait.substring(0, 50) + '...');
|
||||
|
||||
// Get relationship emoji
|
||||
const relationshipEmoji = relationshipEmojis[char.relationship] || '⚖️';
|
||||
// Get relationship badge - only if relationships are enabled in config
|
||||
let relationshipBadge = '⚖️'; // Default
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
debugLog(`[RPG Thoughts] Building HTML card for ${char.name}...`);
|
||||
|
||||
@@ -295,14 +365,45 @@ export function renderThoughts() {
|
||||
<div class="rpg-character-card" data-character-name="${char.name}">
|
||||
<div class="rpg-character-avatar">
|
||||
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
||||
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipEmoji}</div>
|
||||
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
|
||||
</div>
|
||||
<div class="rpg-character-info">
|
||||
<div class="rpg-character-header">
|
||||
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
|
||||
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
|
||||
<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="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
|
||||
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Render custom fields dynamically
|
||||
for (const field of enabledFields) {
|
||||
const fieldValue = char[field.name] || '';
|
||||
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
|
||||
html += `
|
||||
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>
|
||||
<div class="rpg-character-traits rpg-editable" contenteditable="true" data-character="${char.name}" data-field="traits" title="Click to edit traits">${char.traits}</div>
|
||||
`;
|
||||
|
||||
// Render character stats if enabled (outside rpg-character-info)
|
||||
if (enabledCharStats.length > 0) {
|
||||
html += `<div class="rpg-character-stats"><div class="rpg-character-stats-inner">`;
|
||||
for (const stat of enabledCharStats) {
|
||||
const statValue = char[stat.name] || 0;
|
||||
const statColor = getStatColor(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh);
|
||||
html += `
|
||||
<div class="rpg-character-stat">
|
||||
<span class="rpg-stat-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" title="Click to edit ${stat.name}">${stat.name}: <span style="color: ${statColor}">${statValue}%</span></span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += `</div></div>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -523,50 +624,65 @@ export function updateChatThoughts() {
|
||||
// Parse the Present Characters data to get thoughts
|
||||
const lines = lastGeneratedData.characterThoughts.split('\n');
|
||||
const thoughtsArray = []; // Array of {name, emoji, thought}
|
||||
const thoughtsConfig = extensionSettings.trackerConfig?.presentCharacters?.thoughts;
|
||||
const thoughtsLabel = thoughtsConfig?.name || 'Thoughts';
|
||||
|
||||
// console.log('[RPG Companion] Parsing thoughts from lines:', lines);
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() &&
|
||||
!line.includes('Present Characters') &&
|
||||
!line.includes('---') &&
|
||||
!line.trim().startsWith('```')) {
|
||||
// Parse new format to build character map and thoughts
|
||||
let currentCharName = null;
|
||||
let currentCharEmoji = null;
|
||||
|
||||
const parts = line.split('|').map(p => p.trim());
|
||||
// console.log('[RPG Companion] Line parts:', parts);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// Handle both 3-part and 4-part formats
|
||||
if (parts.length >= 3) {
|
||||
const firstPart = parts[0].trim();
|
||||
const emojiMatch = firstPart.match(/^(.+?):\s*(.+)$/);
|
||||
if (!line ||
|
||||
line.includes('Present Characters') ||
|
||||
line.includes('---') ||
|
||||
line.startsWith('```') ||
|
||||
line.trim() === '- …' ||
|
||||
line.includes('(Repeat the format')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (emojiMatch) {
|
||||
const emoji = emojiMatch[1].trim();
|
||||
const info = emojiMatch[2].trim();
|
||||
// Check if this is a character name line (starts with "- ")
|
||||
if (line.startsWith('- ')) {
|
||||
const name = line.substring(2).trim();
|
||||
if (name && name.toLowerCase() !== 'unavailable') {
|
||||
currentCharName = name;
|
||||
currentCharEmoji = null; // Reset emoji for new character
|
||||
} else {
|
||||
currentCharName = null;
|
||||
currentCharEmoji = null;
|
||||
}
|
||||
}
|
||||
// Check if this is a Details line (contains the emoji)
|
||||
else if (line.startsWith('Details:') && currentCharName) {
|
||||
const detailsContent = line.substring(line.indexOf(':') + 1).trim();
|
||||
const parts = detailsContent.split('|').map(p => p.trim());
|
||||
|
||||
let thoughts;
|
||||
if (parts.length === 3) {
|
||||
// 3-part format: Emoji:Name,traits | Relationship | Thoughts
|
||||
thoughts = parts[2].trim();
|
||||
} else if (parts.length >= 4) {
|
||||
// 4-part format: Emoji:Name,traits | Demeanor | Relationship | Thoughts
|
||||
thoughts = parts[3].trim();
|
||||
}
|
||||
// First part is the emoji
|
||||
if (parts.length > 0) {
|
||||
currentCharEmoji = parts[0];
|
||||
}
|
||||
}
|
||||
// Check if this is a Thoughts line
|
||||
else if (line.startsWith(thoughtsLabel + ':') && currentCharName && currentCharEmoji) {
|
||||
const thoughtContent = line.substring(thoughtsLabel.length + 1).trim();
|
||||
|
||||
const infoParts = info.split(',').map(p => p.trim());
|
||||
const name = infoParts[0] || '';
|
||||
|
||||
// console.log('[RPG Companion] Parsed thought - Name:', name, 'Thought:', thoughts);
|
||||
|
||||
if (name && thoughts && name.toLowerCase() !== 'unavailable') {
|
||||
thoughtsArray.push({ name: name.toLowerCase(), emoji, thought: thoughts });
|
||||
// console.log('[RPG Companion] Added to thoughtsArray:', name.toLowerCase());
|
||||
}
|
||||
}
|
||||
// The thought content is just the text (no emoji prefix in new format)
|
||||
if (thoughtContent) {
|
||||
thoughtsArray.push({
|
||||
name: currentCharName.toLowerCase(),
|
||||
emoji: currentCharEmoji,
|
||||
thought: thoughtContent
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('[RPG Thoughts] Parsed thoughts:', thoughtsArray);
|
||||
|
||||
// If no thoughts parsed, return
|
||||
if (thoughtsArray.length === 0) {
|
||||
// console.log('[RPG Companion] No thoughts parsed, returning');
|
||||
|
||||
@@ -26,16 +26,45 @@ import { buildInventorySummary } from '../generation/promptBuilder.js';
|
||||
*/
|
||||
export function buildUserStatsText() {
|
||||
const stats = extensionSettings.userStats;
|
||||
const statNames = extensionSettings.statNames || {
|
||||
health: 'Health',
|
||||
satiety: 'Satiety',
|
||||
energy: 'Energy',
|
||||
hygiene: 'Hygiene',
|
||||
arousal: 'Arousal'
|
||||
const config = extensionSettings.trackerConfig?.userStats || {
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
||||
],
|
||||
statusSection: { enabled: true, showMoodEmoji: true, customFields: ['Conditions'] },
|
||||
skillsSection: { enabled: false, label: 'Skills' }
|
||||
};
|
||||
const inventorySummary = buildInventorySummary(stats.inventory);
|
||||
|
||||
return `${statNames.health}: ${stats.health}%\n${statNames.satiety}: ${stats.satiety}%\n${statNames.energy}: ${stats.energy}%\n${statNames.hygiene}: ${stats.hygiene}%\n${statNames.arousal}: ${stats.arousal}%\n${stats.mood}: ${stats.conditions}\n${inventorySummary}`;
|
||||
let text = '';
|
||||
|
||||
// Add enabled custom stats
|
||||
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
|
||||
for (const stat of enabledStats) {
|
||||
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
|
||||
text += `${stat.name}: ${value}%\n`;
|
||||
}
|
||||
|
||||
// Add status section if enabled
|
||||
if (config.statusSection.enabled) {
|
||||
if (config.statusSection.showMoodEmoji) {
|
||||
text += `${stats.mood}: `;
|
||||
}
|
||||
text += `${stats.conditions || 'None'}\n`;
|
||||
}
|
||||
|
||||
// Add inventory summary
|
||||
const inventorySummary = buildInventorySummary(stats.inventory);
|
||||
text += inventorySummary;
|
||||
|
||||
// Add skills if enabled
|
||||
if (config.skillsSection.enabled && stats.skills) {
|
||||
text += `\n${config.skillsSection.label}: ${stats.skills}`;
|
||||
}
|
||||
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,12 +78,17 @@ export function renderUserStats() {
|
||||
}
|
||||
|
||||
const stats = extensionSettings.userStats;
|
||||
const statNames = extensionSettings.statNames || {
|
||||
health: 'Health',
|
||||
satiety: 'Satiety',
|
||||
energy: 'Energy',
|
||||
hygiene: 'Hygiene',
|
||||
arousal: 'Arousal'
|
||||
const config = extensionSettings.trackerConfig?.userStats || {
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
||||
],
|
||||
showRPGAttributes: true,
|
||||
statusSection: { enabled: true, showMoodEmoji: true, customFields: ['Conditions'] },
|
||||
skillsSection: { enabled: false, label: 'Skills' }
|
||||
};
|
||||
const userName = getContext().name1;
|
||||
|
||||
@@ -63,12 +97,9 @@ export function renderUserStats() {
|
||||
lastGeneratedData.userStats = buildUserStatsText();
|
||||
}
|
||||
|
||||
// Get user portrait - handle both default-user and custom persona folders
|
||||
// Use a base64-encoded SVG placeholder as fallback to avoid 400 errors
|
||||
// Get user portrait
|
||||
let userPortrait = FALLBACK_AVATAR_DATA_URI;
|
||||
|
||||
if (user_avatar) {
|
||||
// Try to get the thumbnail using our safe helper
|
||||
const thumbnailUrl = getSafeThumbnailUrl('persona', user_avatar);
|
||||
if (thumbnailUrl) {
|
||||
userPortrait = thumbnailUrl;
|
||||
@@ -78,64 +109,71 @@ export function renderUserStats() {
|
||||
// Create gradient from low to high color
|
||||
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
|
||||
|
||||
const html = `
|
||||
<div class="rpg-stats-content">
|
||||
<div class="rpg-stats-left">
|
||||
<div class="rpg-user-info-row">
|
||||
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
||||
<span class="rpg-user-name">${userName}</span>
|
||||
<span style="opacity: 0.5;">|</span>
|
||||
<span class="rpg-level-label">LVL</span>
|
||||
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${extensionSettings.level}</span>
|
||||
</div>
|
||||
<div class="rpg-stats-grid">
|
||||
<div class="rpg-stat-row">
|
||||
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="health" title="Click to edit stat name">${statNames.health}:</span>
|
||||
<div class="rpg-stat-bar" style="background: ${gradient}">
|
||||
<div class="rpg-stat-fill" style="width: ${100 - stats.health}%"></div>
|
||||
</div>
|
||||
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="health" title="Click to edit">${stats.health}%</span>
|
||||
</div>
|
||||
|
||||
<div class="rpg-stat-row">
|
||||
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="satiety" title="Click to edit stat name">${statNames.satiety}:</span>
|
||||
<div class="rpg-stat-bar" style="background: ${gradient}">
|
||||
<div class="rpg-stat-fill" style="width: ${100 - stats.satiety}%"></div>
|
||||
</div>
|
||||
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="satiety" title="Click to edit">${stats.satiety}%</span>
|
||||
</div>
|
||||
|
||||
<div class="rpg-stat-row">
|
||||
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="energy" title="Click to edit stat name">${statNames.energy}:</span>
|
||||
<div class="rpg-stat-bar" style="background: ${gradient}">
|
||||
<div class="rpg-stat-fill" style="width: ${100 - stats.energy}%"></div>
|
||||
</div>
|
||||
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="energy" title="Click to edit">${stats.energy}%</span>
|
||||
</div>
|
||||
|
||||
<div class="rpg-stat-row">
|
||||
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="hygiene" title="Click to edit stat name">${statNames.hygiene}:</span>
|
||||
<div class="rpg-stat-bar" style="background: ${gradient}">
|
||||
<div class="rpg-stat-fill" style="width: ${100 - stats.hygiene}%"></div>
|
||||
</div>
|
||||
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="hygiene" title="Click to edit">${stats.hygiene}%</span>
|
||||
</div>
|
||||
|
||||
<div class="rpg-stat-row">
|
||||
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="arousal" title="Click to edit stat name">${statNames.arousal}:</span>
|
||||
<div class="rpg-stat-bar" style="background: ${gradient}">
|
||||
<div class="rpg-stat-fill" style="width: ${100 - stats.arousal}%"></div>
|
||||
</div>
|
||||
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="arousal" title="Click to edit">${stats.arousal}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpg-mood">
|
||||
<div class="rpg-mood-emoji rpg-editable" contenteditable="true" data-field="mood" title="Click to edit emoji">${stats.mood}</div>
|
||||
<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="conditions" title="Click to edit conditions">${stats.conditions}</div>
|
||||
let html = '<div class="rpg-stats-content"><div class="rpg-stats-left">';
|
||||
|
||||
// User info row
|
||||
html += `
|
||||
<div class="rpg-user-info-row">
|
||||
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
||||
<span class="rpg-user-name">${userName}</span>
|
||||
<span style="opacity: 0.5;">|</span>
|
||||
<span class="rpg-level-label">LVL</span>
|
||||
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${extensionSettings.level}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Dynamic stats grid - only show enabled stats
|
||||
html += '<div class="rpg-stats-grid">';
|
||||
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
|
||||
|
||||
for (const stat of enabledStats) {
|
||||
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
|
||||
html += `
|
||||
<div class="rpg-stat-row">
|
||||
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="${stat.id}" title="Click to edit stat name">${stat.name}:</span>
|
||||
<div class="rpg-stat-bar" style="background: ${gradient}">
|
||||
<div class="rpg-stat-fill" style="width: ${100 - value}%"></div>
|
||||
</div>
|
||||
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="${stat.id}" title="Click to edit">${value}%</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Status section (conditionally rendered)
|
||||
if (config.statusSection.enabled) {
|
||||
html += '<div class="rpg-mood">';
|
||||
|
||||
if (config.statusSection.showMoodEmoji) {
|
||||
html += `<div class="rpg-mood-emoji rpg-editable" contenteditable="true" data-field="mood" title="Click to edit emoji">${stats.mood}</div>`;
|
||||
}
|
||||
|
||||
// Render custom status fields
|
||||
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
|
||||
// For now, use first field as "conditions" for backward compatibility
|
||||
const conditionsValue = stats.conditions || 'None';
|
||||
html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="conditions" title="Click to edit conditions">${conditionsValue}</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Skills section (conditionally rendered)
|
||||
if (config.skillsSection.enabled) {
|
||||
const skillsValue = stats.skills || 'None';
|
||||
html += `
|
||||
<div class="rpg-skills-section">
|
||||
<span class="rpg-skills-label">${config.skillsSection.label}:</span>
|
||||
<div class="rpg-skills-value rpg-editable" contenteditable="true" data-field="skills" title="Click to edit skills">${skillsValue}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>'; // Close rpg-stats-left
|
||||
|
||||
// RPG Attributes section (conditionally rendered)
|
||||
if (config.showRPGAttributes) {
|
||||
html += `
|
||||
<div class="rpg-stats-right">
|
||||
<div class="rpg-classic-stats">
|
||||
<div class="rpg-classic-stats-grid">
|
||||
@@ -190,8 +228,10 @@ export function renderUserStats() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>'; // Close rpg-stats-content
|
||||
|
||||
$userStatsContainer.html(html);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user