/**
* User Stats Rendering Module
* Handles rendering of the user stats panel with progress bars and classic RPG stats
*/
import { getContext } from '../../../../../../extensions.js';
import { user_avatar } from '../../../../../../../script.js';
import {
extensionSettings,
lastGeneratedData,
committedTrackerData,
$userStatsContainer,
FALLBACK_AVATAR_DATA_URI
} from '../../core/state.js';
import {
saveSettings,
saveChatData,
updateMessageSwipeData
} from '../../core/persistence.js';
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
import { buildInventorySummary } from '../generation/promptBuilder.js';
/**
* Builds the user stats text string using custom stat names
* @returns {string} Formatted stats text for tracker
*/
export function buildUserStatsText() {
const stats = extensionSettings.userStats;
const config = extensionSettings.trackerConfig?.userStats || {
customStats: [
{ id: 'health', name: 'Health', enabled: true },
{ id: 'satiety', name: 'Satiety', enabled: true },
{ id: 'energy', name: 'Energy', enabled: true },
{ id: 'hygiene', name: 'Hygiene', enabled: true },
{ id: 'arousal', name: 'Arousal', enabled: true }
],
statusSection: { enabled: true, showMoodEmoji: true, customFields: ['Conditions'] },
skillsSection: { enabled: false, label: 'Skills' }
};
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 only if inventory is enabled
if (extensionSettings.showInventory) {
const inventorySummary = buildInventorySummary(stats.inventory);
text += inventorySummary;
}
// Add skills if enabled AND not shown in separate tab
if (config.skillsSection.enabled && stats.skills && !extensionSettings.showSkills) {
text += `\n${config.skillsSection.label}: ${stats.skills}`;
}
return text.trim();
}
/**
* Renders the user stats panel with health bars, mood, inventory, and classic stats.
* Includes event listeners for editable fields.
```
*/
export function renderUserStats() {
if (!extensionSettings.showUserStats || !$userStatsContainer) {
return;
}
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 }
],
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 }
],
statusSection: { enabled: true, showMoodEmoji: true, customFields: ['Conditions'] },
skillsSection: { enabled: false, label: 'Skills' }
};
const userName = getContext().name1;
// Initialize lastGeneratedData.userStats if it doesn't exist
if (!lastGeneratedData.userStats) {
lastGeneratedData.userStats = buildUserStatsText();
}
// Get user portrait
let userPortrait = FALLBACK_AVATAR_DATA_URI;
if (user_avatar) {
const thumbnailUrl = getSafeThumbnailUrl('persona', user_avatar);
if (thumbnailUrl) {
userPortrait = thumbnailUrl;
}
}
// Create gradient from low to high color
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
let html = '
';
// User info row
html += `
${userName}|LVL${extensionSettings.level}
`;
// Dynamic stats grid - only show enabled stats
html += '
';
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 += `
${stat.name}:
${value}%
`;
}
html += '
';
// Status section (conditionally rendered)
if (config.statusSection.enabled) {
html += '
';
if (config.statusSection.showMoodEmoji) {
html += `
${stats.mood}
`;
}
// 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 += `
${conditionsValue}
`;
}
html += '
';
}
// Skills section (conditionally rendered) - only if NOT shown in separate tab
if (config.skillsSection.enabled && !extensionSettings.showSkills) {
const skillsValue = stats.skills || 'None';
html += `
`;
enabledAttributes.forEach(attr => {
const value = extensionSettings.classicStats[attr.id] !== undefined ? extensionSettings.classicStats[attr.id] : 10;
html += `
${attr.name}
${value}
`;
});
html += `
`;
}
}
html += '
'; // Close rpg-stats-content
$userStatsContainer.html(html);
// Add event listeners for editable stat values
$('.rpg-editable-stat').on('blur', function() {
const field = $(this).data('field');
const textValue = $(this).text().replace('%', '').trim();
let value = parseInt(textValue);
// Validate and clamp value between 0 and 100
if (isNaN(value)) {
value = 0;
}
value = Math.max(0, Math.min(100, value));
// Update the setting
extensionSettings.userStats[field] = value;
// Rebuild userStats text with custom stat names
const statsText = buildUserStatsText();
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
saveSettings();
saveChatData();
updateMessageSwipeData();
// Re-render to update the bar
renderUserStats();
});
// Add event listeners for mood/conditions editing
$('.rpg-mood-emoji.rpg-editable').on('blur', function() {
const value = $(this).text().trim();
extensionSettings.userStats.mood = value || '😐';
// Rebuild userStats text with custom stat names
const statsText = buildUserStatsText();
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
saveSettings();
saveChatData();
updateMessageSwipeData();
});
$('.rpg-mood-conditions.rpg-editable').on('blur', function() {
const value = $(this).text().trim();
extensionSettings.userStats.conditions = value || 'None';
// Rebuild userStats text with custom stat names
const statsText = buildUserStatsText();
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
saveSettings();
saveChatData();
updateMessageSwipeData();
});
// Add event listener for skills editing
$('.rpg-skills-value.rpg-editable').on('blur', function() {
const value = $(this).text().trim();
extensionSettings.userStats.skills = value || 'None';
// Rebuild userStats text
const statsText = buildUserStatsText();
// Update BOTH lastGeneratedData AND committedTrackerData
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
saveSettings();
saveChatData();
updateMessageSwipeData();
});
// Add event listeners for stat name editing
$('.rpg-editable-stat-name').on('blur', function() {
const field = $(this).data('field');
const value = $(this).text().trim().replace(':', '');
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
$('.rpg-level-value.rpg-editable').on('blur', function() {
let value = parseInt($(this).text().trim());
if (isNaN(value) || value < 1) {
value = 1;
}
// Set reasonable max level
value = Math.min(100, value);
extensionSettings.level = value;
saveSettings();
saveChatData();
updateMessageSwipeData();
// Re-render to update the display
renderUserStats();
});
// Prevent line breaks in level field
$('.rpg-level-value.rpg-editable').on('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
$(this).blur();
}
});
}