Release v3.0.0 - Major update with JSON format, lock/unlock trackers, reorganized UI, colored dialogues, editable prompts, and numerous bug fixes
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
} from '../../core/persistence.js';
|
||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||
import { buildInventorySummary } from '../generation/promptBuilder.js';
|
||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||
|
||||
/**
|
||||
* Builds the user stats text string using custom stat names
|
||||
@@ -67,6 +68,107 @@ export function buildUserStatsText() {
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lastGeneratedData.userStats and committedTrackerData.userStats
|
||||
* Maintains JSON format if current data is JSON, otherwise uses text format.
|
||||
* @private
|
||||
*/
|
||||
function updateUserStatsData() {
|
||||
// Check if current data is in JSON format
|
||||
const currentData = lastGeneratedData.userStats || committedTrackerData.userStats;
|
||||
if (currentData) {
|
||||
const trimmed = currentData.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
// Maintain JSON format
|
||||
try {
|
||||
const jsonData = JSON.parse(currentData);
|
||||
if (jsonData && typeof jsonData === 'object') {
|
||||
const stats = extensionSettings.userStats;
|
||||
const config = extensionSettings.trackerConfig?.userStats || {};
|
||||
const enabledStats = config.customStats?.filter(stat => stat && stat.enabled && stat.name && stat.id) || [];
|
||||
|
||||
// Build stats array - include all stats from extensionSettings, not just enabled ones
|
||||
// This preserves custom stats that AI might have added or that user has disabled
|
||||
const statsArray = [];
|
||||
const processedIds = new Set();
|
||||
|
||||
// First, add all enabled stats from config (maintains order)
|
||||
enabledStats.forEach(stat => {
|
||||
statsArray.push({
|
||||
id: stat.id,
|
||||
name: stat.name,
|
||||
value: stats[stat.id] !== undefined ? stats[stat.id] : 100
|
||||
});
|
||||
processedIds.add(stat.id);
|
||||
});
|
||||
|
||||
// Then, add any other numeric stats from extensionSettings that aren't in config
|
||||
// (these could be custom stats the AI added or disabled stats)
|
||||
const excludeFields = new Set(['mood', 'conditions', 'inventory', 'skills', 'level']);
|
||||
Object.entries(stats).forEach(([key, value]) => {
|
||||
if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') {
|
||||
statsArray.push({
|
||||
id: key,
|
||||
name: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
value: value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
jsonData.stats = statsArray;
|
||||
|
||||
// Update status
|
||||
jsonData.status = {
|
||||
mood: stats.mood || '😐',
|
||||
conditions: stats.conditions || 'None'
|
||||
};
|
||||
|
||||
// Update inventory (convert to v3 format)
|
||||
const convertToV3Items = (itemString) => {
|
||||
if (!itemString) return [];
|
||||
const items = itemString.split(',').map(s => s.trim()).filter(s => s);
|
||||
return items.map(item => {
|
||||
const qtyMatch = item.match(/^(\\d+)x\\s+(.+)$/);
|
||||
if (qtyMatch) {
|
||||
return { name: qtyMatch[2].trim(), quantity: parseInt(qtyMatch[1]) };
|
||||
}
|
||||
return { name: item, quantity: 1 };
|
||||
});
|
||||
};
|
||||
|
||||
jsonData.inventory = {
|
||||
onPerson: convertToV3Items(stats.inventory?.onPerson),
|
||||
clothing: convertToV3Items(stats.inventory?.clothing),
|
||||
stored: stats.inventory?.stored || {},
|
||||
assets: convertToV3Items(stats.inventory?.assets)
|
||||
};
|
||||
|
||||
// Update quests
|
||||
jsonData.quests = extensionSettings.quests || { main: '', optional: [] };
|
||||
|
||||
// Update skills if present
|
||||
if (stats.skills) {
|
||||
jsonData.skills = Array.isArray(stats.skills) ? stats.skills :
|
||||
stats.skills.split(',').map(s => s.trim()).filter(s => s);
|
||||
}
|
||||
|
||||
const updatedJSON = JSON.stringify(jsonData, null, 2);
|
||||
lastGeneratedData.userStats = updatedJSON;
|
||||
committedTrackerData.userStats = updatedJSON;
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RPG Companion] Failed to parse JSON, falling back to text format:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to text format
|
||||
const statsText = buildUserStatsText();
|
||||
lastGeneratedData.userStats = statsText;
|
||||
committedTrackerData.userStats = statsText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the user stats panel with health bars, mood, inventory, and classic stats.
|
||||
* Includes event listeners for editable fields.
|
||||
@@ -77,7 +179,36 @@ export function renderUserStats() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't render if no data exists (e.g., after cache clear)
|
||||
// Check both lastGeneratedData and committedTrackerData
|
||||
console.log('[RPG UserStats Render] Checking data:', {
|
||||
hasLastGenerated: !!lastGeneratedData.userStats,
|
||||
hasCommitted: !!committedTrackerData.userStats,
|
||||
lastGeneratedPreview: lastGeneratedData.userStats ? lastGeneratedData.userStats.substring(0, 100) : 'null',
|
||||
committedPreview: committedTrackerData.userStats ? committedTrackerData.userStats.substring(0, 100) : 'null'
|
||||
});
|
||||
|
||||
if (!lastGeneratedData.userStats && !committedTrackerData.userStats) {
|
||||
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
|
||||
$userStatsContainer.html('<div class="rpg-inventory-empty">No statuses generated yet</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use lastGeneratedData if available, otherwise fall back to committed data
|
||||
if (!lastGeneratedData.userStats && committedTrackerData.userStats) {
|
||||
lastGeneratedData.userStats = committedTrackerData.userStats;
|
||||
}
|
||||
|
||||
const stats = extensionSettings.userStats;
|
||||
console.log('[RPG UserStats Render] Current extensionSettings.userStats:', {
|
||||
health: stats.health,
|
||||
satiety: stats.satiety,
|
||||
energy: stats.energy,
|
||||
hygiene: stats.hygiene,
|
||||
arousal: stats.arousal,
|
||||
mood: stats.mood,
|
||||
conditions: stats.conditions
|
||||
});
|
||||
const config = extensionSettings.trackerConfig?.userStats || {
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
@@ -116,20 +247,32 @@ export function renderUserStats() {
|
||||
// Create gradient from low to high color
|
||||
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
|
||||
|
||||
let html = '<div class="rpg-stats-content"><div class="rpg-stats-left">';
|
||||
// Check if stats bars section is locked
|
||||
const isStatsLocked = isItemLocked('userStats', 'stats');
|
||||
const lockIcon = isStatsLocked ? '🔒' : '🔓';
|
||||
const lockTitle = isStatsLocked ? 'Locked - AI cannot change stats' : 'Unlocked - AI can change stats';
|
||||
const lockedClass = isStatsLocked ? ' locked' : '';
|
||||
|
||||
let html = '<div class="rpg-stats-content">';
|
||||
html += '<div class="rpg-stats-left">';
|
||||
|
||||
// User info row
|
||||
const showLevel = extensionSettings.trackerConfig?.userStats?.showLevel !== false;
|
||||
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>
|
||||
${showLevel ? `<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>
|
||||
<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
|
||||
const showLockIcons = extensionSettings.showLockIcons ?? true;
|
||||
if (showLockIcons) {
|
||||
html += `<span class="rpg-section-lock-icon${lockedClass}" data-tracker="userStats" data-path="stats" title="${lockTitle}">${lockIcon}</span>`;
|
||||
}
|
||||
html += '<div class="rpg-stats-grid">';
|
||||
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
|
||||
|
||||
@@ -149,7 +292,14 @@ export function renderUserStats() {
|
||||
|
||||
// Status section (conditionally rendered)
|
||||
if (config.statusSection.enabled) {
|
||||
const isMoodLocked = isItemLocked('userStats', 'status');
|
||||
const moodLockIcon = isMoodLocked ? '🔒' : '🔓';
|
||||
const moodLockTitle = isMoodLocked ? 'Locked - AI cannot change mood' : 'Unlocked - AI can change mood';
|
||||
const moodLockedClass = isMoodLocked ? ' locked' : '';
|
||||
html += '<div class="rpg-mood">';
|
||||
if (showLockIcons) {
|
||||
html += `<span class="rpg-section-lock-icon${moodLockedClass}" data-tracker="userStats" data-path="status" title="${moodLockTitle}">${moodLockIcon}</span>`;
|
||||
}
|
||||
|
||||
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>`;
|
||||
@@ -158,7 +308,11 @@ export function renderUserStats() {
|
||||
// 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';
|
||||
let conditionsValue = stats.conditions || 'None';
|
||||
// Strip brackets if present (from JSON array format)
|
||||
if (typeof conditionsValue === 'string') {
|
||||
conditionsValue = conditionsValue.replace(/^\[|\]$/g, '').trim();
|
||||
}
|
||||
html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="conditions" title="Click to edit conditions">${conditionsValue}</div>`;
|
||||
}
|
||||
|
||||
@@ -167,9 +321,24 @@ export function renderUserStats() {
|
||||
|
||||
// Skills section (conditionally rendered)
|
||||
if (config.skillsSection.enabled) {
|
||||
const skillsValue = stats.skills || 'None';
|
||||
const isSkillsLocked = isItemLocked('userStats', 'skills');
|
||||
const skillsLockIcon = isSkillsLocked ? '🔒' : '🔓';
|
||||
const skillsLockTitle = isSkillsLocked ? 'Locked - AI cannot change skills' : 'Unlocked - AI can change skills';
|
||||
const skillsLockedClass = isSkillsLocked ? ' locked' : '';
|
||||
let skillsValue = 'None';
|
||||
// Handle JSON array format: [{name: "Art"}, {name: "Coding"}]
|
||||
if (Array.isArray(stats.skills)) {
|
||||
skillsValue = stats.skills.map(s => s.name || s).join(', ') || 'None';
|
||||
} else if (stats.skills) {
|
||||
skillsValue = stats.skills;
|
||||
}
|
||||
html += `
|
||||
<div class="rpg-skills-section">`;
|
||||
if (showLockIcons) {
|
||||
html += `
|
||||
<span class="rpg-section-lock-icon${skillsLockedClass}" data-tracker="userStats" data-path="skills" title="${skillsLockTitle}">${skillsLockIcon}</span>`;
|
||||
}
|
||||
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>
|
||||
@@ -225,7 +394,13 @@ export function renderUserStats() {
|
||||
|
||||
html += '</div>'; // Close rpg-stats-content
|
||||
|
||||
console.log('[RPG UserStats Render] Generated HTML length:', html.length);
|
||||
console.log('[RPG UserStats Render] HTML preview:', html.substring(0, 300));
|
||||
console.log('[RPG UserStats Render] Container exists:', !!$userStatsContainer, '$userStatsContainer length:', $userStatsContainer?.length);
|
||||
|
||||
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
|
||||
$userStatsContainer.html(html);
|
||||
console.log('[RPG UserStats Render] ✓ HTML rendered to #rpg-user-stats container');
|
||||
|
||||
// Add event listeners for editable stat values
|
||||
$('.rpg-editable-stat').on('blur', function() {
|
||||
@@ -242,13 +417,8 @@ export function renderUserStats() {
|
||||
// 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;
|
||||
// Update userStats data (maintains JSON or text format)
|
||||
updateUserStatsData();
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
@@ -263,13 +433,8 @@ export function renderUserStats() {
|
||||
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;
|
||||
// Update userStats data (maintains JSON or text format)
|
||||
updateUserStatsData();
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
@@ -280,13 +445,8 @@ export function renderUserStats() {
|
||||
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;
|
||||
// Update userStats data (maintains JSON or text format)
|
||||
updateUserStatsData();
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
@@ -298,12 +458,8 @@ export function renderUserStats() {
|
||||
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;
|
||||
// Update userStats data (maintains JSON or text format)
|
||||
updateUserStatsData();
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
@@ -359,4 +515,29 @@ export function renderUserStats() {
|
||||
$(this).blur();
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for section lock icon clicks (support both click and touch)
|
||||
$('.rpg-section-lock-icon').on('click touchend', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const $icon = $(this);
|
||||
const trackerType = $icon.data('tracker');
|
||||
const itemPath = $icon.data('path');
|
||||
const currentlyLocked = isItemLocked(trackerType, itemPath);
|
||||
|
||||
// Toggle lock state
|
||||
setItemLock(trackerType, itemPath, !currentlyLocked);
|
||||
|
||||
// Update icon
|
||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||
const newTitle = !currentlyLocked ? 'Locked - AI cannot change this section' : 'Unlocked - AI can change this section';
|
||||
$icon.text(newIcon);
|
||||
$icon.attr('title', newTitle);
|
||||
|
||||
// Toggle 'locked' class for persistent visibility
|
||||
$icon.toggleClass('locked', !currentlyLocked);
|
||||
|
||||
// Save settings
|
||||
saveSettings();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user