merge: integrate upstream tracker customization system

Merged upstream/main (82b9564) which includes:
- Full tracker customization system
- Tracker editor UI component
- Custom stat names in AI prompts
- Multi-line tracker format updates

Conflict resolutions:
- src/core/persistence.js: Kept both dashboard v2 and trackerConfig migrations
- style.css: Accepted upstream responsive calendar styling with clamp()

Both migration systems are now active and will run in sequence.
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-11-02 09:57:15 +11:00
13 changed files with 2535 additions and 668 deletions
+3
View File
@@ -118,6 +118,9 @@ export function onGenerationStarted(type, data) {
// Don't include HTML prompt in instructions - inject it separately to avoid duplication on swipes
const instructions = generateTrackerInstructions(false, true);
// Clear separate mode context injection - we don't use contextual summary in together mode
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
// console.log('[RPG Companion] Example:', example ? 'exists' : 'empty');
// console.log('[RPG Companion] Chat length:', chat ? chat.length : 'chat is null');
+135 -90
View File
@@ -47,10 +47,11 @@ function separateEmojiFromText(str) {
}
/**
* Helper to strip enclosing brackets from text
* Removes [], {}, and () from the entire text if it's wrapped
* @param {string} text - Text that may be wrapped in brackets
* @returns {string} Text with brackets removed
* Helper to strip enclosing brackets from text and remove placeholder brackets
* Removes [], {}, and () from the entire text if it's wrapped, plus removes
* placeholder content like [Location], [Mood Emoji], etc.
* @param {string} text - Text that may contain brackets
* @returns {string} Text with brackets and placeholders removed
*/
function stripBrackets(text) {
if (!text) return text;
@@ -68,7 +69,58 @@ function stripBrackets(text) {
text = text.substring(1, text.length - 1).trim();
}
return text;
// Remove placeholder text patterns like [Location], [Mood Emoji], [Name], etc.
// Pattern matches: [anything with letters/spaces inside]
// This preserves actual content while removing template placeholders
const placeholderPattern = /\[([A-Za-z\s\/]+)\]/g;
// Check if a bracketed text looks like a placeholder vs real content
const isPlaceholder = (match, content) => {
// Common placeholder words to detect
const placeholderKeywords = [
'location', 'mood', 'emoji', 'name', 'description', 'placeholder',
'time', 'date', 'weather', 'temperature', 'action', 'appearance',
'skill', 'quest', 'item', 'character', 'field', 'value', 'details',
'relationship', 'thoughts', 'stat', 'status', 'lover', 'friend',
'enemy', 'neutral', 'weekday', 'month', 'year', 'forecast'
];
const lowerContent = content.toLowerCase().trim();
// If it contains common placeholder keywords, it's likely a placeholder
if (placeholderKeywords.some(keyword => lowerContent.includes(keyword))) {
return true;
}
// If it's a short generic phrase (1-3 words) with only letters/spaces, might be placeholder
const wordCount = content.trim().split(/\s+/).length;
if (wordCount <= 3 && /^[A-Za-z\s\/]+$/.test(content)) {
return true;
}
return false;
};
// Replace placeholders with empty string, keep real content
text = text.replace(placeholderPattern, (match, content) => {
if (isPlaceholder(match, content)) {
return ''; // Remove placeholder
}
return match; // Keep real bracketed content
});
// Clean up any resulting empty labels (e.g., "Status: " with nothing after)
text = text.replace(/^([A-Za-z\s]+):\s*$/gm, ''); // Remove lines that are just "Label: " with nothing
text = text.replace(/^([A-Za-z\s]+):\s*,/gm, '$1:'); // Fix "Label: ," patterns
text = text.replace(/:\s*\|/g, ':'); // Fix ": |" patterns
text = text.replace(/\|\s*\|/g, '|'); // Fix "| |" patterns (double pipes from removed content)
text = text.replace(/\|\s*$/gm, ''); // Remove trailing pipes at end of lines
// Clean up multiple spaces and empty lines
text = text.replace(/\s{2,}/g, ' '); // Multiple spaces to single space
text = text.replace(/^\s*\n/gm, ''); // Remove empty lines
return text.trim();
}
/**
@@ -173,8 +225,8 @@ export function parseResponse(responseText) {
content.match(/Present Characters\s*\n\s*---/i) ||
content.match(/Characters\s*\n\s*---/i) ||
content.match(/Character Thoughts\s*\n\s*---/i) ||
// Fallback: look for table-like structure with emoji and pipes
(content.includes(" | ") && (content.includes("Thoughts") || content.includes("💭")));
// Fallback: look for new multi-line format patterns
(content.match(/^-\s+\w+/m) && content.match(/Details:/i));
if (isStats && !result.userStats) {
result.userStats = stripBrackets(content);
@@ -193,7 +245,7 @@ export function parseResponse(responseText) {
debugLog('[RPG Parser] - Has "Info Box\\n---"?', !!content.match(/Info Box\s*\n\s*---/i));
debugLog('[RPG Parser] - Has info keywords?', !!(content.match(/Date:/i) && content.match(/Location:/i)));
debugLog('[RPG Parser] - Has "Present Characters\\n---"?', !!content.match(/Present Characters\s*\n\s*---/i));
debugLog('[RPG Parser] - Has " | " + thoughts?', !!(content.includes(" | ") && (content.includes("Thoughts") || content.includes("💭"))));
debugLog('[RPG Parser] - Has new format ("- Name" + "Details:")?', !!(content.match(/^-\s+\w+/m) && content.match(/Details:/i)));
}
}
}
@@ -219,89 +271,93 @@ export function parseUserStats(statsText) {
debugLog('[RPG Parser] Stats text preview:', statsText.substring(0, 200));
try {
// Extract percentages and mood/conditions
const healthMatch = statsText.match(/Health:\s*(\d+)%/);
const satietyMatch = statsText.match(/Satiety:\s*(\d+)%/);
const energyMatch = statsText.match(/Energy:\s*(\d+)%/);
const hygieneMatch = statsText.match(/Hygiene:\s*(\d+)%/);
const arousalMatch = statsText.match(/Arousal:\s*(\d+)%/);
// Get custom stat configuration
const trackerConfig = extensionSettings.trackerConfig;
const customStats = trackerConfig?.userStats?.customStats || [];
const enabledStats = customStats.filter(s => s && s.enabled && s.name && s.id);
debugLog('[RPG Parser] Stat matches:', {
health: healthMatch ? healthMatch[1] : 'NOT FOUND',
satiety: satietyMatch ? satietyMatch[1] : 'NOT FOUND',
energy: energyMatch ? energyMatch[1] : 'NOT FOUND',
hygiene: hygieneMatch ? hygieneMatch[1] : 'NOT FOUND',
arousal: arousalMatch ? arousalMatch[1] : 'NOT FOUND'
});
debugLog('[RPG Parser] Enabled custom stats:', enabledStats.map(s => s.name));
// Match mood/status with multiple format variations
// Format 1: Status: [Emoji, Conditions]
// Format 2: Status: [Emoji], [Conditions]
// Format 3: [Emoji]: [Conditions] (legacy)
// Format 4: Mood: [Emoji] - [Conditions]
// Format 5: Status: [Emoji Conditions] (no separator - FIXED)
let moodMatch = null;
// Try new format: Status: emoji, conditions OR Status: emojiConditions
const statusMatch = statsText.match(/Status:\s*(.+)/i);
if (statusMatch) {
const statusContent = statusMatch[1].trim();
const { emoji, text } = separateEmojiFromText(statusContent);
if (emoji && text) {
moodMatch = [null, emoji, text];
} else if (statusContent.includes(',')) {
// Fallback to comma split if emoji detection failed
const parts = statusContent.split(',').map(p => p.trim());
moodMatch = [null, parts[0], parts.slice(1).join(', ')];
// Dynamically parse custom stats
for (const stat of enabledStats) {
const statRegex = new RegExp(`${stat.name}:\\s*(\\d+)%`, 'i');
const match = statsText.match(statRegex);
if (match) {
// Store using the stat ID (lowercase normalized name)
const statId = stat.id;
extensionSettings.userStats[statId] = parseInt(match[1]);
debugLog(`[RPG Parser] Parsed ${stat.name}:`, match[1]);
} else {
debugLog(`[RPG Parser] ${stat.name} NOT FOUND`);
}
}
// Try alternative: Mood: emoji, conditions OR Mood: emojiConditions
if (!moodMatch) {
const moodAltMatch = statsText.match(/Mood:\s*(.+)/i);
if (moodAltMatch) {
const moodContent = moodAltMatch[1].trim();
const { emoji, text } = separateEmojiFromText(moodContent);
if (emoji && text) {
moodMatch = [null, emoji, text];
} else if (moodContent.includes(',') || moodContent.includes('-')) {
// Fallback to comma/dash split if emoji detection failed
const parts = moodContent.split(/[,\-]/).map(p => p.trim());
moodMatch = [null, parts[0], parts.slice(1).join(', ')];
// Parse RPG attributes if enabled
if (trackerConfig?.userStats?.showRPGAttributes) {
const strMatch = statsText.match(/STR:\s*(\d+)/i);
const dexMatch = statsText.match(/DEX:\s*(\d+)/i);
const conMatch = statsText.match(/CON:\s*(\d+)/i);
const intMatch = statsText.match(/INT:\s*(\d+)/i);
const wisMatch = statsText.match(/WIS:\s*(\d+)/i);
const chaMatch = statsText.match(/CHA:\s*(\d+)/i);
const lvlMatch = statsText.match(/LVL:\s*(\d+)/i);
if (strMatch) extensionSettings.classicStats.str = parseInt(strMatch[1]);
if (dexMatch) extensionSettings.classicStats.dex = parseInt(dexMatch[1]);
if (conMatch) extensionSettings.classicStats.con = parseInt(conMatch[1]);
if (intMatch) extensionSettings.classicStats.int = parseInt(intMatch[1]);
if (wisMatch) extensionSettings.classicStats.wis = parseInt(wisMatch[1]);
if (chaMatch) extensionSettings.classicStats.cha = parseInt(chaMatch[1]);
if (lvlMatch) extensionSettings.level = parseInt(lvlMatch[1]);
debugLog('[RPG Parser] RPG Attributes parsed');
}
// Match status section if enabled
const statusConfig = trackerConfig?.userStats?.statusSection;
if (statusConfig?.enabled) {
let moodMatch = null;
// Try Status: format
const statusMatch = statsText.match(/Status:\s*(.+)/i);
if (statusMatch) {
const statusContent = statusMatch[1].trim();
// Extract mood emoji if enabled
if (statusConfig.showMoodEmoji) {
const { emoji, text } = separateEmojiFromText(statusContent);
if (emoji) {
extensionSettings.userStats.mood = emoji;
// Remaining text contains custom status fields
if (text) {
extensionSettings.userStats.conditions = text;
}
moodMatch = true;
}
} else {
// No mood emoji, whole status is conditions
extensionSettings.userStats.conditions = statusContent;
moodMatch = true;
}
}
debugLog('[RPG Parser] Status match:', {
found: !!moodMatch,
mood: extensionSettings.userStats.mood,
conditions: extensionSettings.userStats.conditions
});
}
// Legacy format fallback: [Emoji]: [Conditions]
if (!moodMatch) {
const lines = statsText.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Skip lines with percentages or known keywords
if (line.includes('%') ||
line.toLowerCase().startsWith('inventory:') ||
line.toLowerCase().startsWith('status:') ||
line.toLowerCase().startsWith('health:') ||
line.toLowerCase().startsWith('energy:') ||
line.toLowerCase().startsWith('satiety:') ||
line.toLowerCase().startsWith('hygiene:') ||
line.toLowerCase().startsWith('arousal:')) continue;
// Match emoji/mood followed by colon and conditions
const match = line.match(/^(.+?):\s*(.+)$/);
if (match && match[1].length <= 10) { // Emoji/mood should be short
moodMatch = match;
break;
}
// Parse skills section if enabled
const skillsConfig = trackerConfig?.userStats?.skillsSection;
if (skillsConfig?.enabled) {
const skillsMatch = statsText.match(/Skills:\s*(.+)/i);
if (skillsMatch) {
extensionSettings.userStats.skills = skillsMatch[1].trim();
debugLog('[RPG Parser] Skills extracted:', skillsMatch[1].trim());
}
}
debugLog('[RPG Parser] Mood/Status match:', {
found: !!moodMatch,
emoji: moodMatch ? moodMatch[1] : 'NOT FOUND',
conditions: moodMatch ? moodMatch[2] : 'NOT FOUND'
});
// Extract inventory - use v2 parser if feature flag enabled, otherwise fallback to v1
if (FEATURE_FLAGS.useNewInventory) {
const inventoryData = extractInventory(statsText);
@@ -322,17 +378,6 @@ export function parseUserStats(statsText) {
}
}
// Update extension settings
if (healthMatch) extensionSettings.userStats.health = parseInt(healthMatch[1]);
if (satietyMatch) extensionSettings.userStats.satiety = parseInt(satietyMatch[1]);
if (energyMatch) extensionSettings.userStats.energy = parseInt(energyMatch[1]);
if (hygieneMatch) extensionSettings.userStats.hygiene = parseInt(hygieneMatch[1]);
if (arousalMatch) extensionSettings.userStats.arousal = parseInt(arousalMatch[1]);
if (moodMatch) {
extensionSettings.userStats.mood = moodMatch[1].trim(); // Emoji
extensionSettings.userStats.conditions = moodMatch[2].trim(); // Conditions
}
// Extract quests
const mainQuestMatch = statsText.match(/Main Quests?:\s*(.+)/i);
if (mainQuestMatch) {
+135 -131
View File
@@ -92,6 +92,7 @@ export function generateTrackerExample() {
export function generateTrackerInstructions(includeHtmlPrompt = true, includeContinuation = true) {
const userName = getContext().name1;
const classicStats = extensionSettings.classicStats;
const trackerConfig = extensionSettings.trackerConfig;
let instructions = '';
// Check if any trackers are enabled
@@ -104,24 +105,36 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
// Add format specifications for each enabled tracker
if (extensionSettings.showUserStats) {
// Get custom stat names with fallback defaults
const statNames = extensionSettings.statNames || {
health: 'Health',
satiety: 'Satiety',
energy: 'Energy',
hygiene: 'Hygiene',
arousal: 'Arousal'
};
const userStatsConfig = trackerConfig?.userStats;
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
instructions += '```\n';
instructions += `${userName}'s Stats\n`;
instructions += '---\n';
instructions += `- ${statNames.health}: X%\n`;
instructions += `- ${statNames.satiety}: X%\n`;
instructions += `- ${statNames.energy}: X%\n`;
instructions += `- ${statNames.hygiene}: X%\n`;
instructions += `- ${statNames.arousal}: X%\n`;
instructions += 'Status: [Mood Emoji, Conditions (up to three traits)]\n';
// Add custom stats dynamically
for (const stat of enabledStats) {
instructions += `- ${stat.name}: X%\n`;
}
// Add status section if enabled
if (userStatsConfig?.statusSection?.enabled) {
const statusFields = userStatsConfig.statusSection.customFields || [];
const statusFieldsText = statusFields.map(f => `${f}`).join(', ');
if (userStatsConfig.statusSection.showMoodEmoji) {
instructions += `Status: [Mood Emoji${statusFieldsText ? ', ' + statusFieldsText : ''}]\n`;
} else if (statusFieldsText) {
instructions += `Status: [${statusFieldsText}]\n`;
}
}
// Add skills section if enabled
if (userStatsConfig?.skillsSection?.enabled) {
const skillFields = userStatsConfig.skillsSection.customFields || [];
const skillFieldsText = skillFields.map(f => `[${f}]`).join(', ');
instructions += `Skills: [${skillFieldsText || 'Skill1, Skill2, etc.'}]\n`;
}
// Add inventory format based on feature flag
if (FEATURE_FLAGS.useNewInventory) {
@@ -142,23 +155,90 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
}
if (extensionSettings.showInfoBox) {
const infoBoxConfig = trackerConfig?.infoBox;
const widgets = infoBoxConfig?.widgets || {};
instructions += '```\n';
instructions += 'Info Box\n';
instructions += '---\n';
instructions += 'Date: [Weekday, Month, Year]\n';
instructions += 'Weather: [Weather Emoji, Forecast]\n';
instructions += 'Temperature: [Temperature in °C]\n';
instructions += 'Time: [Time Start → Time End]\n';
instructions += 'Location: [Location]\n';
instructions += 'Recent Events: [Up to three past events leading to the ongoing scene (short descriptors with no details, for example, "last-night date with Mary")]\n';
// Add only enabled widgets
if (widgets.date?.enabled) {
instructions += 'Date: [Weekday, Month, Year]\n';
}
if (widgets.weather?.enabled) {
instructions += 'Weather: [Weather Emoji, Forecast]\n';
}
if (widgets.temperature?.enabled) {
const unit = widgets.temperature.unit === 'fahrenheit' ? '°F' : '°C';
instructions += `Temperature: [Temperature in ${unit}]\n`;
}
if (widgets.time?.enabled) {
instructions += 'Time: [Time Start → Time End]\n';
}
if (widgets.location?.enabled) {
instructions += 'Location: [Location]\n';
}
if (widgets.recentEvents?.enabled) {
instructions += 'Recent Events: [Up to three past events leading to the ongoing scene (short descriptors with no details, for example, "last-night date with Mary")]\n';
}
instructions += '```\n\n';
}
if (extensionSettings.showCharacterThoughts) {
const presentCharsConfig = trackerConfig?.presentCharacters;
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
const relationshipFields = presentCharsConfig?.relationshipFields || [];
const thoughtsConfig = presentCharsConfig?.thoughts;
const characterStats = presentCharsConfig?.characterStats;
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
instructions += '```\n';
instructions += 'Present Characters\n';
instructions += '---\n';
instructions += `[Present Character's Emoji (do not include ${userName}; state "Unavailable" if no major characters are present in the scene)]: [Name, Visible Physical State (up to three traits), Observable Demeanor Cue (one trait)] | [Enemy/Neutral/Friend/Lover] | [Internal Monologue (in first person POV, up to three sentences long)]\n`;
// Build relationship placeholders (e.g., "Lover/Friend")
const relationshipPlaceholders = relationshipFields
.filter(r => r && r.trim())
.map(r => `${r}`)
.join('/');
// Build custom field placeholders (e.g., "[Appearance] | [Current Action]")
const fieldPlaceholders = enabledFields
.map(f => `[${f.name}]`)
.join(' | ');
// Character block format
instructions += `- [Name (do not include ${userName}; state "Unavailable" if no major characters are present in the scene)]\n`;
// Details line with emoji and custom fields
if (fieldPlaceholders) {
instructions += `Details: [Present Character's Emoji] | ${fieldPlaceholders}\n`;
} else {
instructions += `Details: [Present Character's Emoji]\n`;
}
// Relationship line (only if relationships are enabled)
if (relationshipPlaceholders) {
instructions += `Relationship: [${relationshipPlaceholders}]\n`;
}
// Stats line (if enabled)
if (enabledCharStats.length > 0) {
const statPlaceholders = enabledCharStats.map(s => `${s.name}: X%`).join(' | ');
instructions += `Stats: ${statPlaceholders}\n`;
}
// Thoughts line (if enabled)
if (thoughtsConfig?.enabled) {
const thoughtsName = thoughtsConfig.name || 'Thoughts';
const thoughtsDescription = thoughtsConfig.description || 'Internal monologue (in first person POV, up to three sentences long)';
instructions += `${thoughtsName}: [${thoughtsDescription}]\n`;
}
instructions += `- … (Repeat the format above for every other present major character)\n`;
instructions += '```\n\n';
}
@@ -197,7 +277,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
/**
* Generates a formatted contextual summary for SEPARATE mode injection.
* This creates a hybrid summary with clean formatting for main roleplay generation.
* Includes the full tracker data in original format (without code fences and separators).
* Uses COMMITTED data (not displayed data) for generation context.
*
* @returns {string} Formatted contextual summary
@@ -207,124 +287,52 @@ export function generateContextualSummary() {
const userName = getContext().name1;
let summary = '';
// console.log('[RPG Companion] generateContextualSummary called');
// console.log('[RPG Companion] committedTrackerData.userStats:', committedTrackerData.userStats);
// console.log('[RPG Companion] extensionSettings.userStats:', JSON.stringify(extensionSettings.userStats));
// Helper function to clean tracker data (remove code fences and separator lines)
const cleanTrackerData = (data) => {
if (!data) return '';
return data
.split('\n')
.filter(line => {
const trimmed = line.trim();
return trimmed &&
!trimmed.startsWith('```') &&
trimmed !== '---';
})
.join('\n');
};
// Parse the data into readable format
// Add User Stats tracker data if enabled
if (extensionSettings.showUserStats && committedTrackerData.userStats) {
const stats = extensionSettings.userStats;
// console.log('[RPG Companion] Building stats summary with:', stats);
summary += `${userName}'s Stats:\n`;
summary += `Condition: Health ${stats.health}%, Satiety ${stats.satiety}%, Energy ${stats.energy}%, Hygiene ${stats.hygiene}%, Arousal ${stats.arousal}% | ${stats.mood} ${stats.conditions}\n`;
// Add inventory summary using v2-aware builder
if (stats.inventory) {
const inventorySummary = buildInventorySummary(stats.inventory);
if (inventorySummary && inventorySummary !== 'None') {
summary += `${inventorySummary}\n`;
}
const cleanedStats = cleanTrackerData(committedTrackerData.userStats);
if (cleanedStats) {
summary += cleanedStats + '\n\n';
}
// Add quests summary
if (extensionSettings.quests) {
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
summary += `Main Quests: ${extensionSettings.quests.main}\n`;
}
if (extensionSettings.quests.optional && extensionSettings.quests.optional.length > 0) {
const optionalQuests = extensionSettings.quests.optional.filter(q => q && q !== 'None').join(', ');
if (optionalQuests) {
summary += `Optional Quests: ${optionalQuests}\n`;
}
}
}
// Include classic stats (attributes) and dice roll only if there was a dice roll
if (extensionSettings.lastDiceRoll) {
const classicStats = extensionSettings.classicStats;
const roll = extensionSettings.lastDiceRoll;
summary += `Attributes: STR ${classicStats.str}, DEX ${classicStats.dex}, CON ${classicStats.con}, INT ${classicStats.int}, WIS ${classicStats.wis}, CHA ${classicStats.cha}, LVL ${extensionSettings.level}\n`;
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeed or fail the action they attempt.\n`;
}
summary += `\n`;
}
// Add Info Box tracker data if enabled
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
// Parse info box data - support both new and legacy formats
const lines = committedTrackerData.infoBox.split('\n');
let date = '', weather = '', temp = '', time = '', location = '', recentEvents = '';
// console.log('[RPG Companion] 🔍 Parsing Info Box lines:', lines);
for (const line of lines) {
// console.log('[RPG Companion] 🔍 Processing line:', line);
// New format with text labels
if (line.startsWith('Date:')) {
date = line.replace('Date:', '').trim();
} else if (line.startsWith('Weather:')) {
weather = line.replace('Weather:', '').trim();
} else if (line.startsWith('Temperature:')) {
temp = line.replace('Temperature:', '').trim();
} else if (line.startsWith('Time:')) {
time = line.replace('Time:', '').trim();
} else if (line.startsWith('Location:')) {
location = line.replace('Location:', '').trim();
} else if (line.startsWith('Recent Events:')) {
recentEvents = line.replace('Recent Events:', '').trim();
}
// Legacy format with emojis (for backward compatibility)
else if (line.includes('🗓️:')) {
date = line.replace('🗓️:', '').trim();
} else if (line.includes('🌡️:')) {
temp = line.replace('🌡️:', '').trim();
} else if (line.includes('🕒:')) {
time = line.replace('🕒:', '').trim();
} else if (line.includes('🗺️:')) {
location = line.replace('🗺️:', '').trim();
} else {
// Check for weather emojis in legacy format
const weatherEmojis = ['🌤️', '☀️', '⛅', '🌦️', '🌧️', '⛈️', '🌩️', '🌨️', '❄️', '🌫️'];
const startsWithWeatherEmoji = weatherEmojis.some(emoji => line.startsWith(emoji + ':'));
if (startsWithWeatherEmoji && !line.includes('🌡️') && !line.includes('🗺️')) {
weather = line.substring(line.indexOf(':') + 1).trim();
}
}
}
// console.log('[RPG Companion] 🔍 Parsed values - date:', date, 'weather:', weather, 'temp:', temp, 'time:', time, 'location:', location);
if (date || weather || temp || time || location || recentEvents) {
summary += `Information:\n`;
summary += `Scene: `;
if (date) summary += `${date}`;
if (location) summary += ` | ${location}`;
if (time) summary += ` | ${time}`;
if (weather) summary += ` | ${weather}`;
if (temp) summary += ` | ${temp}`;
summary += `\n`;
if (recentEvents) summary += `Recent Events: ${recentEvents}\n`;
summary += `\n`;
const cleanedInfoBox = cleanTrackerData(committedTrackerData.infoBox);
if (cleanedInfoBox) {
summary += cleanedInfoBox + '\n\n';
}
}
// Add Present Characters tracker data if enabled
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
const lines = committedTrackerData.characterThoughts.split('\n').filter(l => l.trim() && !l.includes('---') && !l.includes('Present Characters'));
if (lines.length > 0 && !lines[0].toLowerCase().includes('unavailable')) {
summary += `Present Characters And Their Thoughts:\n`;
for (const line of lines) {
const parts = line.split('|').map(p => p.trim());
if (parts.length >= 3) {
const nameAndState = parts[0]; // Emoji, name, physical state, demeanor
const relationship = parts[1];
const thoughts = parts[2];
summary += `${nameAndState} (${relationship}) | ${thoughts}\n`;
}
}
const cleanedThoughts = cleanTrackerData(committedTrackerData.characterThoughts);
if (cleanedThoughts) {
summary += cleanedThoughts + '\n\n';
}
}
// Include attributes and dice roll only if there was a dice roll
if (extensionSettings.lastDiceRoll) {
const classicStats = extensionSettings.classicStats;
const roll = extensionSettings.lastDiceRoll;
summary += `${userName}'s attributes: STR ${classicStats.str}, DEX ${classicStats.dex}, CON ${classicStats.con}, INT ${classicStats.int}, WIS ${classicStats.wis}, CHA ${classicStats.cha}, LVL ${extensionSettings.level}\n`;
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
}
return summary.trim();
}
@@ -354,14 +362,10 @@ export function generateRPGPromptText() {
if (extensionSettings.quests) {
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
promptText += `Main Quests: ${extensionSettings.quests.main}\n`;
} else {
promptText += `Main Quests: None\n`;
}
if (extensionSettings.quests.optional && extensionSettings.quests.optional.length > 0) {
const optionalQuests = extensionSettings.quests.optional.filter(q => q && q !== 'None').join(', ');
promptText += `Optional Quests: ${optionalQuests || 'None'}\n`;
} else {
promptText += `Optional Quests: None\n`;
}
promptText += `\n`;
}