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:
+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');
|
||||
|
||||
Reference in New Issue
Block a user