diff --git a/README.md b/README.md index 25e5cb4..b03d301 100644 --- a/README.md +++ b/README.md @@ -27,30 +27,37 @@ An immersive RPG extension for browsers that tracks character stats, scene infor ### Core Functionality -- **πŸ“Š User Stats Tracker**: Visual progress bars for health, sustenance, energy, hygiene, arousal, mood, and conditions -- **🌍 Info Box Dashboard**: Beautiful widgets displaying date, weather, temperature, time, and location of the current scene -- **πŸ’­ Character Thoughts**: Floating thought bubbles showing AI characters' internal monologue +- **πŸ“Š User Stats Tracker**: Fully customizable stats with visual progress bars, custom status fields, skills section, and dynamic inventory management +- **🌍 Info Box Dashboard**: Configurable widgets for date, weather, temperature, time, location, and recent events +- **πŸ’­ Present Characters Panel**: Track multiple characters with custom fields, relationship badges, character-specific stats, and internal thoughts +- **🎭 Floating Thought Bubbles**: Optional thought bubbles positioned next to character avatars in chat - **🎲 Classic RPG Stats**: STR, DEX, CON, INT, WIS, CHA attributes with dice roll support -- **πŸ“¦ Inventory System**: Track items your character is carrying +- **πŸ“¦ Advanced Inventory System**: Multi-location storage (On Person, Stored locations, Assets) with v2 format +- **🎯 Character Stats**: Track health, energy, or any custom stats for each present character with color interpolation - **πŸ“œ Immersive HTML**: Enhance the immersion by including creative HTML/CSS/JS elements in your roleplay - **➑️ Plot Progression**: Progress the plot with randomized events or natural progression with a click of a button - **🎨 Multiple Themes**: Cyberpunk, Fantasy, Minimal, Dark, Light, and Custom themes -- **✏️ Live Editing**: Edit stats, thoughts, weather, and more directly in the panels +- **✏️ Live Editing**: Edit all tracker fields directly in the panels with auto-save - **πŸ’Ύ Per-Swipe Data Storage**: Each swipe preserves its own tracker data +- **πŸŽ›οΈ Tracker Configuration**: Customize every aspect of trackers - add/remove stats, fields, widgets, and more ### Smart Features - **πŸ”„ Swipe Detection**: Automatically handles swipes and maintains correct tracker context - **πŸ“ Context-Aware**: Weather, stats, and character states naturally influence the narrative -- **🎭 Multiple Characters**: Tracks thoughts and relationships for all present characters +- **🎭 Multiple Characters**: Tracks thoughts, relationships, and stats for all present characters - **πŸ“ Thought Bubbles in Chat**: Optional floating thought bubbles positioned next to character avatars - **🌈 Customizable Colors**: Create your own theme with custom color schemes -- **πŸ“± Mobile Support**: Works on mobile and tablet devices +- **πŸ“± Mobile Support**: Responsive design with horizontal scrolling for stats +- **πŸ”§ Advanced Configuration**: Add custom stats, fields, and widgets through Tracker Settings +- **🎨 Color Interpolation**: Stats smoothly transition from low to high colors based on values +- **πŸ’¬ Multi-line Format**: Clean, structured format for AI generation and parsing +- **🧹 Auto-cleanup**: Automatically removes placeholder brackets from AI responses ### To-Do 1. Allow users to use a different model for the separate trackers generation -2. Make all trackers and fields customizable +2. ~~Make all trackers and fields customizable~~ βœ… Done! 3. ~~Kill myself~~ ## βš™οΈ Settings @@ -140,11 +147,31 @@ Cons: You can edit most fields by clicking on them: -- **Stats**: Click on percentage values, mood emoji, conditions, or inventory -- **Info Box**: Click on date fields, weather, temperature, time, or location -- **Character Thoughts**: Click on emoji, name, traits, relationship, or thoughts +- **User Stats**: Click on stat percentages, mood emoji, status fields, skills, inventory items, or quests +- **Info Box**: Click on date fields, weather, temperature, time, location, or recent events +- **Present Characters**: Click on character emoji, name, custom fields, relationship badge, or stats +- **Thought Bubbles**: Click on thought text to edit (bubble will refresh to maintain positioning) -Note: When editing character thoughts in the floating bubble, the bubble will refresh to maintain proper positioning. +### Tracker Configuration + +Access comprehensive customization through the Tracker Settings button: + +**User Stats Configuration:** +- Add/remove custom stats with unique names +- Configure Status section (mood emoji + custom fields) +- Configure Skills section with custom skill fields +- Toggle RPG attributes display + +**Info Box Configuration:** +- Enable/disable individual widgets (Date, Weather, Temperature, Time, Location, Recent Events) +- Choose temperature unit (Celsius/Fahrenheit) + +**Present Characters Configuration:** +- Add custom character fields (appearance, action, demeanor, etc.) +- Configure relationship status options +- Enable character-specific stats tracking +- Customize thought bubble label and description +- All fields are dynamically generated in prompts ### Swipe Support diff --git a/index.js b/index.js index 1ef0bf5..e97b582 100644 --- a/index.js +++ b/index.js @@ -86,6 +86,9 @@ import { addDiceQuickReply, getSettingsModal } from './src/systems/ui/modals.js'; +import { + initTrackerEditor +} from './src/systems/ui/trackerEditor.js'; import { togglePlotButtons, updateCollapseToggleIcon, @@ -513,6 +516,7 @@ async function initUI() { setupDiceRoller(); setupClassicStatsButtons(); setupSettingsPopup(); + initTrackerEditor(); addDiceQuickReply(); setupPlotButtons(sendPlotProgression); setupMobileKeyboardHandling(); diff --git a/src/core/persistence.js b/src/core/persistence.js index 77d308b..58535a4 100644 --- a/src/core/persistence.js +++ b/src/core/persistence.js @@ -4,7 +4,6 @@ */ import { saveSettingsDebounced, chat_metadata, saveChatDebounced } from '../../../../../../script.js'; -import { power_user } from '../../../../../power-user.js'; import { getContext } from '../../../../../extensions.js'; import { extensionSettings, @@ -58,19 +57,17 @@ function validateSettings(settings) { */ export function loadSettings() { try { - // Validate power_user structure - if (!power_user || typeof power_user !== 'object') { - console.warn('[RPG Companion] power_user is not available, using default settings'); + const context = getContext(); + const extension_settings = context.extension_settings || context.extensionSettings; + + // Validate extension_settings structure + if (!extension_settings || typeof extension_settings !== 'object') { + console.warn('[RPG Companion] extension_settings is not available, using default settings'); return; } - if (!power_user.extensions) { - power_user.extensions = {}; - // console.log('[RPG Companion] Created power_user.extensions object'); - } - - if (power_user.extensions[extensionName]) { - const savedSettings = power_user.extensions[extensionName]; + if (extension_settings[extensionName]) { + const savedSettings = extension_settings[extensionName]; // Validate loaded settings if (!validateSettings(savedSettings)) { @@ -110,6 +107,13 @@ export function loadSettings() { saveSettings(); } } + + // Migrate to trackerConfig if it doesn't exist + if (!extensionSettings.trackerConfig) { + console.log('[RPG Companion] Migrating to trackerConfig format'); + migrateToTrackerConfig(); + saveSettings(); // Persist migration + } } catch (error) { console.error('[RPG Companion] Error loading settings:', error); console.error('[RPG Companion] Error details:', error.message, error.stack); @@ -125,10 +129,15 @@ export function loadSettings() { * Saves the extension settings to the global settings object. */ export function saveSettings() { - if (!power_user.extensions) { - power_user.extensions = {}; + const context = getContext(); + const extension_settings = context.extension_settings || context.extensionSettings; + + if (!extension_settings) { + console.error('[RPG Companion] extension_settings is not available, cannot save'); + return; } - power_user.extensions[extensionName] = extensionSettings; + + extension_settings[extensionName] = extensionSettings; saveSettingsDebounced(); } @@ -360,3 +369,137 @@ function validateInventoryStructure(inventory, source) { } } } + +/** + * Migrates old settings format to new trackerConfig format + * Converts statNames to customStats array and sets up default config + */ +function migrateToTrackerConfig() { + // Initialize trackerConfig if it doesn't exist + if (!extensionSettings.trackerConfig) { + extensionSettings.trackerConfig = { + userStats: { + customStats: [], + showRPGAttributes: true, + statusSection: { + enabled: true, + showMoodEmoji: true, + customFields: ['Conditions'] + }, + skillsSection: { + enabled: false, + label: 'Skills' + } + }, + infoBox: { + widgets: { + date: { enabled: true, format: 'Weekday, Month, Year' }, + weather: { enabled: true }, + temperature: { enabled: true, unit: 'C' }, + time: { enabled: true }, + location: { enabled: true }, + recentEvents: { enabled: true } + } + }, + presentCharacters: { + showEmoji: true, + showName: true, + customFields: [ + { id: 'physicalState', label: 'Physical State', enabled: true, placeholder: 'Visible Physical State (up to three traits)' }, + { id: 'demeanor', label: 'Demeanor Cue', enabled: true, placeholder: 'Observable Demeanor Cue (one trait)' }, + { id: 'relationship', label: 'Relationship', enabled: true, type: 'relationship', placeholder: 'Enemy/Neutral/Friend/Lover' }, + { id: 'internalMonologue', label: 'Internal Monologue', enabled: true, placeholder: 'Internal Monologue (in first person POV, up to three sentences long)' } + ], + characterStats: { + enabled: false, + stats: [] + } + } + }; + } + + // Migrate old statNames to customStats if statNames exists + if (extensionSettings.statNames && extensionSettings.trackerConfig.userStats.customStats.length === 0) { + const statOrder = ['health', 'satiety', 'energy', 'hygiene', 'arousal']; + extensionSettings.trackerConfig.userStats.customStats = statOrder.map(id => ({ + id: id, + name: extensionSettings.statNames[id] || id.charAt(0).toUpperCase() + id.slice(1), + enabled: true + })); + console.log('[RPG Companion] Migrated statNames to customStats array'); + } + + // Ensure all stats have corresponding values in userStats + if (extensionSettings.userStats) { + for (const stat of extensionSettings.trackerConfig.userStats.customStats) { + if (extensionSettings.userStats[stat.id] === undefined) { + extensionSettings.userStats[stat.id] = stat.id === 'arousal' ? 0 : 100; + } + } + } + + // Migrate old presentCharacters structure to new format + if (extensionSettings.trackerConfig.presentCharacters) { + const pc = extensionSettings.trackerConfig.presentCharacters; + + // Check if using old flat customFields structure (has 'label' or 'placeholder' keys) + if (pc.customFields && pc.customFields.length > 0) { + const hasOldFormat = pc.customFields.some(f => f.label || f.placeholder || f.type === 'relationship'); + + if (hasOldFormat) { + console.log('[RPG Companion] Migrating Present Characters to new structure'); + + // Extract relationship fields from old customFields + const relationshipFields = ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral']; + + // Extract non-relationship fields and convert to new format + const newCustomFields = pc.customFields + .filter(f => f.type !== 'relationship' && f.id !== 'internalMonologue') + .map(f => ({ + id: f.id, + name: f.label || f.name || 'Field', + enabled: f.enabled !== false, + description: f.placeholder || f.description || '' + })); + + // Extract thoughts config from old Internal Monologue field + const thoughtsField = pc.customFields.find(f => f.id === 'internalMonologue'); + const thoughts = { + enabled: thoughtsField ? (thoughtsField.enabled !== false) : true, + name: 'Thoughts', + description: thoughtsField?.placeholder || 'Internal monologue (in first person POV, up to three sentences long)' + }; + + // Update to new structure + pc.relationshipFields = relationshipFields; + pc.customFields = newCustomFields; + pc.thoughts = thoughts; + + console.log('[RPG Companion] Present Characters migration complete'); + saveSettings(); // Persist the migration + } + } + + // Ensure new structure exists even if migration wasn't needed + if (!pc.relationshipFields) { + pc.relationshipFields = ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral']; + } + if (!pc.relationshipEmojis) { + // Create default emoji mapping from relationshipFields + pc.relationshipEmojis = { + 'Lover': '❀️', + 'Friend': '⭐', + 'Ally': '🀝', + 'Enemy': 'βš”οΈ', + 'Neutral': 'βš–οΈ' + }; + } + if (!pc.thoughts) { + pc.thoughts = { + enabled: true, + name: 'Thoughts', + description: 'Internal monologue (in first person POV, up to three sentences long)' + }; + } + } +} diff --git a/src/core/state.js b/src/core/state.js index 8b18610..d0069c0 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -60,6 +60,76 @@ export let extensionSettings = { hygiene: 'Hygiene', arousal: 'Arousal' }, + // Tracker customization configuration + trackerConfig: { + userStats: { + // Array of custom stats (allows add/remove/rename) + 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 } + ], + // RPG Attributes toggle + showRPGAttributes: true, + // Status section config + statusSection: { + enabled: true, + showMoodEmoji: true, + customFields: ['Conditions'] // User can edit what to track + }, + // Optional skills field + skillsSection: { + enabled: false, + label: 'Skills' // User-editable + } + }, + infoBox: { + widgets: { + date: { enabled: true, format: 'Weekday, Month, Year' }, // Format options in UI + weather: { enabled: true }, + temperature: { enabled: true, unit: 'C' }, // 'C' or 'F' + time: { enabled: true }, + location: { enabled: true }, + recentEvents: { enabled: true } + } + }, + presentCharacters: { + // Fixed fields (always shown) + showEmoji: true, + showName: true, + // Relationship fields (shown after name, separated by /) + relationshipFields: ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral'], + // Relationship to emoji mapping (shown on character portraits) + relationshipEmojis: { + 'Lover': '❀️', + 'Friend': '⭐', + 'Ally': '🀝', + 'Enemy': 'βš”οΈ', + 'Neutral': 'βš–οΈ' + }, + // Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |) + customFields: [ + { id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)' }, + { id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state' } + ], + // Thoughts configuration (separate line) + thoughts: { + enabled: true, + name: 'Thoughts', + description: 'Internal monologue (in first person POV, up to three sentences long)' + }, + // Character stats toggle (optional feature) + characterStats: { + enabled: false, + customStats: [ + { id: 'health', name: 'Health', enabled: true }, + { id: 'arousal', name: 'Arousal', enabled: true } + ] + } + } + }, quests: { main: "None", // Current main quest title optional: [] // Array of optional quest titles diff --git a/src/systems/generation/injector.js b/src/systems/generation/injector.js index c98d447..e4d8955 100644 --- a/src/systems/generation/injector.js +++ b/src/systems/generation/injector.js @@ -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'); diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js index 2472ffc..3a6d020 100644 --- a/src/systems/generation/parser.js +++ b/src/systems/generation/parser.js @@ -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) { diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index 2392776..4049536 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -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`; } diff --git a/src/systems/rendering/infoBox.js b/src/systems/rendering/infoBox.js index c42d4c9..4a4a5e6 100644 --- a/src/systems/rendering/infoBox.js +++ b/src/systems/rendering/infoBox.js @@ -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 = '
'; // Row 1: Date, Weather, Temperature, Time widgets - html += '
'; + 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 += ` -
-
${monthShort}
-
${weekdayShort}
-
${yearDisplay}
-
- `; + // 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 += ` -
-
${weatherEmoji}
-
${weatherForecast}
-
- `; + // 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 += ` -
-
-
-
-
-
+ row1Widgets.push(` +
+
${monthDisplay}
+
${weekdayDisplay}
+
${yearDisplay}
-
${tempDisplay}
-
- `; - - // 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 += ` -
-
-
-
-
-
+ + // Weather widget - show if enabled + if (config?.widgets?.weather?.enabled) { + const weatherEmoji = data.weatherEmoji || '🌀️'; + const weatherForecast = data.weatherForecast || 'Weather'; + row1Widgets.push(` +
+
${weatherEmoji}
+
${weatherForecast}
+
+ `); + } + + // 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(` +
+
+
+
+
+
+
+
${tempDisplay}
+
+ `); + } + + // 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(` +
+
+
+
+
+
+
+
+
${timeDisplay}
+
+ `); + } + + // Only create row 1 if there are widgets to show + if (row1Widgets.length > 0) { + html += '
'; + html += row1Widgets.join(''); + html += '
'; + } + + // Row 2: Location widget (full width) - show if enabled + if (config?.widgets?.location?.enabled) { + const locationDisplay = data.location || 'Location'; + html += ` +
+
+
+
πŸ“
+
+
${locationDisplay}
-
${timeDisplay}
-
- `; + `; + } - html += '
'; - - // Row 2: Location widget (full width) - always show (editable even if empty) - const locationDisplay = data.location || 'Location'; - html += ` -
-
-
-
πŸ“
-
-
${locationDisplay}
-
-
- `; - - // 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 += ` -
-
-
-
-
-
-
-
Recent Events
-
- `; - - // Dynamically generate event lines (max 3) - for (let i = 0; i < Math.min(validEvents.length, 3); i++) { html += ` -
- β€’ - ${validEvents[i]} +
+
+
+
+
+
+
Recent Events
+
`; - } - // 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 += ` +
+ β€’ + ${validEvents[i]} +
+ `; + } + + // If we have less than 3 events, add empty placeholders with + icon + for (let i = validEvents.length; i < 3; i++) { + html += ` +
+ + + Add event... +
+ `; + } + html += ` -
- + - Add event...
- `; - } - - html += `
-
- `; + `; + } // Close the scrollable content wrapper html += '
'; diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index a4c0d89..fa1fbc0 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -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() { 😊 ${defaultName}
-
Traits
+ `; + + // Add custom fields dynamically + for (const field of enabledFields) { + const fieldId = field.name.toLowerCase().replace(/\s+/g, '-'); + html += ` +
+ `; + } + + html += `
`; @@ -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() {
${char.name} -
${relationshipEmoji}
+ ${hasRelationshipEnabled ? `
${relationshipBadge}
` : ''}
-
-
- ${char.emoji} - ${char.name} +
+
+
+ ${char.emoji} + ${char.name} +
+ `; + + // Render custom fields dynamically + for (const field of enabledFields) { + const fieldValue = char[field.name] || ''; + const fieldId = field.name.toLowerCase().replace(/\s+/g, '-'); + html += ` +
${fieldValue}
+ `; + } + + html += `
-
${char.traits}
+ `; + + // Render character stats if enabled (outside rpg-character-info) + if (enabledCharStats.length > 0) { + html += `
`; + for (const stat of enabledCharStats) { + const statValue = char[stat.name] || 0; + const statColor = getStatColor(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh); + html += ` +
+ ${stat.name}: ${statValue}% +
+ `; + } + html += `
`; + } + + html += `
`; @@ -346,132 +447,133 @@ export function renderThoughts() { /** * Updates a specific character field in Present Characters data and re-renders. - * Handles character creation if character doesn't exist yet. + * Works with the new multi-line format. * * @param {string} characterName - Name of the character to update - * @param {string} field - Field to update (emoji, name, traits, thoughts, relationship) + * @param {string} field - Field to update (emoji, name, custom field name, Relationship, stat name) * @param {string} value - New value for the field */ export function updateCharacterField(characterName, field, value) { - // console.log('[RPG Companion] πŸ“ updateCharacterField called - character:', characterName, 'field:', field, 'value:', value); - // console.log('[RPG Companion] πŸ“ Current lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts); - // Initialize if it doesn't exist if (!lastGeneratedData.characterThoughts) { lastGeneratedData.characterThoughts = 'Present Characters\n---\n'; } const lines = lastGeneratedData.characterThoughts.split('\n'); + const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters; + const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || []; + const characterStats = presentCharsConfig?.characterStats; + const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || []; + let characterFound = false; + let inTargetCharacter = false; + let characterStartIndex = -1; + let characterEndIndex = -1; - const updatedLines = lines.map(line => { - // Case-insensitive character name matching - if (line.toLowerCase().includes(characterName.toLowerCase())) { - characterFound = true; - const parts = line.split('|').map(p => p.trim()); - if (parts.length >= 2) { - const firstPart = parts[0]; - const emojiMatch = firstPart.match(/^(.+?):\s*(.+)$/); + // Find the character block + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); - if (emojiMatch) { - let emoji = emojiMatch[1].trim(); - let info = emojiMatch[2].trim(); - let relationship = parts[1] ? parts[1].trim() : ''; - let thoughts = parts[2] ? parts[2].trim() : ''; - - // Handle 4-part format (with demeanor) - if (parts.length >= 4) { - relationship = parts[2] ? parts[2].trim() : ''; - thoughts = parts[3] ? parts[3].trim() : ''; - } - - const infoParts = info.split(',').map(p => p.trim()); - let name = infoParts[0]; - let traits = infoParts.slice(1).join(', '); - - if (field === 'emoji') { - emoji = value; - } else if (field === 'name') { - name = value; - } else if (field === 'traits') { - traits = value; - } else if (field === 'thoughts') { - thoughts = value; - } else if (field === 'relationship') { - const emojiToRelationship = { - 'βš”οΈ': 'Enemy', - 'βš–οΈ': 'Neutral', - '⭐': 'Friend', - '❀️': 'Lover' - }; - relationship = emojiToRelationship[value] || value; - } - - const newInfo = traits ? `${name}, ${traits}` : name; - return `${emoji}: ${newInfo} | ${relationship} | ${thoughts}`; - } + if (line.startsWith('- ')) { + const name = line.substring(2).trim(); + if (name.toLowerCase() === characterName.toLowerCase()) { + characterFound = true; + inTargetCharacter = true; + characterStartIndex = i; + } else if (inTargetCharacter) { + characterEndIndex = i; + break; } } - return line; - }); - - // If character wasn't found, create a new character line - if (!characterFound) { - // Find the divider line - const dividerIndex = updatedLines.findIndex(line => line.includes('---')); - if (dividerIndex >= 0) { - // Create initial character line with the edited field - let emoji = '😊'; - let name = characterName; - let traits = 'Traits'; - let relationship = 'Neutral'; - let thoughts = ''; - - // Apply the edited field - if (field === 'emoji') { - emoji = value; - } else if (field === 'name') { - name = value; - } else if (field === 'traits') { - traits = value; - } else if (field === 'thoughts') { - thoughts = value; - } else if (field === 'relationship') { - const emojiToRelationship = { - 'βš”οΈ': 'Enemy', - 'βš–οΈ': 'Neutral', - '⭐': 'Friend', - '❀️': 'Lover' - }; - relationship = emojiToRelationship[value] || value; - } - - const newCharacterLine = `${emoji}: ${name}, ${traits} | ${relationship} | ${thoughts}`; - // Insert after the divider - updatedLines.splice(dividerIndex + 1, 0, newCharacterLine); - } } - lastGeneratedData.characterThoughts = updatedLines.join('\n'); - // console.log('[RPG Companion] πŸ’Ύ Updated lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts); + if (characterFound && characterEndIndex === -1) { + characterEndIndex = lines.length; + } - // Update BOTH lastGeneratedData AND committedTrackerData - // This makes manual edits immediately visible to AI - committedTrackerData.characterThoughts = updatedLines.join('\n'); + if (characterFound) { + // Update the specific field within the character block + for (let i = characterStartIndex; i < characterEndIndex; i++) { + const line = lines[i].trim(); + + if (field === 'name' && line.startsWith('- ')) { + lines[i] = `- ${value}`; + } + else if (field === 'emoji' && line.startsWith('Details:')) { + const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim()); + parts[0] = value; + lines[i] = `Details: ${parts.join(' | ')}`; + } + else if (line.startsWith('Details:')) { + const fieldIndex = enabledFields.findIndex(f => f.name === field); + if (fieldIndex !== -1) { + const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim()); + if (parts.length > fieldIndex + 1) { + parts[fieldIndex + 1] = value; + lines[i] = `Details: ${parts.join(' | ')}`; + } + } + } + else if (field === 'Relationship' && line.startsWith('Relationship:')) { + const emojiToRelationship = { 'βš”οΈ': 'Enemy', 'βš–οΈ': 'Neutral', '⭐': 'Friend', '❀️': 'Lover' }; + const relationshipValue = emojiToRelationship[value] || value; + lines[i] = `Relationship: ${relationshipValue}`; + } + else if (line.startsWith('Stats:')) { + const statIndex = enabledCharStats.findIndex(s => s.name === field); + if (statIndex !== -1) { + const statsContent = line.substring(line.indexOf(':') + 1).trim(); + const statParts = statsContent.split('|').map(p => p.trim()); + + for (let j = 0; j < statParts.length; j++) { + if (statParts[j].startsWith(field + ':')) { + statParts[j] = `${field}: ${value}%`; + break; + } + } + lines[i] = `Stats: ${statParts.join(' | ')}`; + } + } + } + } else { + // Create new character block + const dividerIndex = lines.findIndex(line => line.includes('---')); + if (dividerIndex >= 0) { + const newCharacterLines = [`- ${characterName}`]; + + let detailsParts = [field === 'emoji' ? value : '😊']; + for (let i = 0; i < enabledFields.length; i++) { + detailsParts.push(field === enabledFields[i].name ? value : ''); + } + newCharacterLines.push(`Details: ${detailsParts.join(' | ')}`); + + if (presentCharsConfig?.relationshipFields?.length > 0) { + const emojiToRelationship = { 'βš”οΈ': 'Enemy', 'βš–οΈ': 'Neutral', '⭐': 'Friend', '❀️': 'Lover' }; + const relationshipValue = field === 'Relationship' ? (emojiToRelationship[value] || value) : 'Neutral'; + newCharacterLines.push(`Relationship: ${relationshipValue}`); + } + + if (enabledCharStats.length > 0) { + const statsParts = enabledCharStats.map(s => `${s.name}: ${field === s.name ? value : '0'}%`); + newCharacterLines.push(`Stats: ${statsParts.join(' | ')}`); + } + + lines.splice(dividerIndex + 1, 0, ...newCharacterLines); + } + } + + lastGeneratedData.characterThoughts = lines.join('\n'); + committedTrackerData.characterThoughts = lines.join('\n'); - // Also update the last assistant message's swipe data const chat = getContext().chat; if (chat && chat.length > 0) { - // Find the last assistant message for (let i = chat.length - 1; i >= 0; i--) { const message = chat[i]; if (!message.is_user) { - // Found last assistant message - update its swipe data if (message.extra && message.extra.rpg_companion_swipes) { const swipeId = message.swipe_id || 0; if (message.extra.rpg_companion_swipes[swipeId]) { - message.extra.rpg_companion_swipes[swipeId].characterThoughts = updatedLines.join('\n'); - // console.log('[RPG Companion] Updated thoughts in message swipe data'); + message.extra.rpg_companion_swipes[swipeId].characterThoughts = lines.join('\n'); } } break; @@ -480,18 +582,12 @@ export function updateCharacterField(characterName, field, value) { } saveChatData(); - - // Always update the sidebar panel renderThoughts(); - // For thoughts edited from the bubble, delay recreation to allow blur event to complete - // This ensures the edit is saved first, then the bubble is recreated with correct layout - if (field === 'thoughts') { - setTimeout(() => { - updateChatThoughts(); - }, 100); + const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts'; + if (field === thoughtsFieldName) { + setTimeout(() => updateChatThoughts(), 100); } else { - // For other fields, recreate immediately updateChatThoughts(); } } @@ -523,50 +619,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'); diff --git a/src/systems/rendering/userStats.js b/src/systems/rendering/userStats.js index b51299f..12de415 100644 --- a/src/systems/rendering/userStats.js +++ b/src/systems/rendering/userStats.js @@ -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 = ` -
-
- -
-
- ${statNames.health}: -
-
-
- ${stats.health}% -
- -
- ${statNames.satiety}: -
-
-
- ${stats.satiety}% -
- -
- ${statNames.energy}: -
-
-
- ${stats.energy}% -
- -
- ${statNames.hygiene}: -
-
-
- ${stats.hygiene}% -
- -
- ${statNames.arousal}: -
-
-
- ${stats.arousal}% -
-
- -
-
${stats.mood}
-
${stats.conditions}
+ let html = '
'; + + // User info row + html += ` + + `; + + // 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) + if (config.skillsSection.enabled) { + const skillsValue = stats.skills || 'None'; + html += ` +
+ ${config.skillsSection.label}: +
${skillsValue}
+
+ `; + } + + html += '
'; // Close rpg-stats-left + + // RPG Attributes section (conditionally rendered) + if (config.showRPGAttributes) { + html += `
@@ -190,8 +228,10 @@ export function renderUserStats() {
-
- `; + `; + } + + html += '
'; // Close rpg-stats-content $userStatsContainer.html(html); diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js new file mode 100644 index 0000000..4e31561 --- /dev/null +++ b/src/systems/ui/trackerEditor.js @@ -0,0 +1,708 @@ +/** + * Tracker Editor Module + * Provides UI for customizing tracker configurations + */ + +import { extensionSettings } from '../../core/state.js'; +import { saveSettings } from '../../core/persistence.js'; +import { renderUserStats } from '../rendering/userStats.js'; +import { renderInfoBox } from '../rendering/infoBox.js'; +import { renderThoughts } from '../rendering/thoughts.js'; + +let $editorModal = null; +let activeTab = 'userStats'; +let tempConfig = null; // Temporary config for cancel functionality + +/** + * Initialize the tracker editor modal + */ +export function initTrackerEditor() { + // Modal will be in template.html, just set up event listeners + $editorModal = $('#rpg-tracker-editor-popup'); + + if (!$editorModal.length) { + console.error('[RPG Companion] Tracker editor modal not found in template'); + return; + } + + // Tab switching + $(document).on('click', '.rpg-editor-tab', function() { + $('.rpg-editor-tab').removeClass('active'); + $(this).addClass('active'); + + activeTab = $(this).data('tab'); + $('.rpg-editor-tab-content').hide(); + $(`#rpg-editor-tab-${activeTab}`).show(); + }); + + // Save button + $(document).on('click', '#rpg-editor-save', function() { + applyTrackerConfig(); + closeTrackerEditor(); + }); + + // Cancel button + $(document).on('click', '#rpg-editor-cancel', function() { + closeTrackerEditor(); + }); + + // Close X button + $(document).on('click', '#rpg-close-tracker-editor', function() { + closeTrackerEditor(); + }); + + // Reset button + $(document).on('click', '#rpg-editor-reset', function() { + resetToDefaults(); + renderEditorUI(); + }); + + // Close on background click + $(document).on('click', '#rpg-tracker-editor-popup', function(e) { + if (e.target.id === 'rpg-tracker-editor-popup') { + closeTrackerEditor(); + } + }); + + // Open button + $(document).on('click', '#rpg-open-tracker-editor', function() { + openTrackerEditor(); + }); +} + +/** + * Open the tracker editor modal + */ +function openTrackerEditor() { + // Create temporary copy for cancel functionality + tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig)); + + // Set theme to match current extension theme + const theme = extensionSettings.theme || 'modern'; + $editorModal.attr('data-theme', theme); + + renderEditorUI(); + $editorModal.addClass('is-open').css('display', ''); +} + +/** + * Close the tracker editor modal + */ +function closeTrackerEditor() { + // Restore from temp if canceling + if (tempConfig) { + extensionSettings.trackerConfig = tempConfig; + tempConfig = null; + } + + $editorModal.removeClass('is-open').addClass('is-closing'); + setTimeout(() => { + $editorModal.removeClass('is-closing').hide(); + }, 200); +} + +/** + * Apply the tracker configuration and refresh all trackers + */ +function applyTrackerConfig() { + tempConfig = null; // Clear temp config + saveSettings(); + + // Re-render all trackers with new config + renderUserStats(); + renderInfoBox(); + renderThoughts(); +} + +/** + * Reset configuration to defaults + */ +function resetToDefaults() { + 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' + } + }, + infoBox: { + widgets: { + date: { enabled: true, format: 'Weekday, Month, Year' }, + weather: { enabled: true }, + temperature: { enabled: true, unit: 'C' }, + time: { enabled: true }, + location: { enabled: true }, + recentEvents: { enabled: true } + } + }, + presentCharacters: { + showEmoji: true, + showName: true, + relationshipFields: ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral'], + relationshipEmojis: { + 'Lover': '❀️', + 'Friend': '⭐', + 'Ally': '🀝', + 'Enemy': 'βš”οΈ', + 'Neutral': 'βš–οΈ' + }, + customFields: [ + { id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)' }, + { id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state' } + ], + thoughts: { + enabled: true, + name: 'Thoughts', + description: 'Internal monologue (in first person POV, up to three sentences long)' + }, + characterStats: { + enabled: false, + customStats: [ + { id: 'health', name: 'Health', enabled: true, colorLow: '#ff4444', colorHigh: '#44ff44' }, + { id: 'energy', name: 'Energy', enabled: true, colorLow: '#ffaa00', colorHigh: '#44ffff' } + ] + } + } + }; +} + +/** + * Render the editor UI based on current config + */ +function renderEditorUI() { + renderUserStatsTab(); + renderInfoBoxTab(); + renderPresentCharactersTab(); +} + +/** + * Render User Stats configuration tab + */ +function renderUserStatsTab() { + const config = extensionSettings.trackerConfig.userStats; + let html = '
'; + + // Custom Stats section + html += '

Custom Stats

'; + html += '
'; + + config.customStats.forEach((stat, index) => { + html += ` +
+ + + +
+ `; + }); + + html += '
'; + html += ''; + + // RPG Attributes toggle + html += '
'; + html += ``; + html += ''; + html += '
'; + + // Status Section + html += '

Status Section

'; + html += '
'; + html += ``; + html += ''; + html += '
'; + + html += '
'; + html += ``; + html += ''; + html += '
'; + + html += ''; + html += ``; + + // Skills Section + html += '

Skills Section

'; + html += '
'; + html += ``; + html += ''; + html += '
'; + + html += ''; + html += ``; + + html += '
'; + + $('#rpg-editor-tab-userStats').html(html); + setupUserStatsListeners(); +} + +/** + * Set up event listeners for User Stats tab + */ +function setupUserStatsListeners() { + // Add stat + $('#rpg-add-stat').off('click').on('click', function() { + const newId = 'custom_' + Date.now(); + extensionSettings.trackerConfig.userStats.customStats.push({ + id: newId, + name: 'New Stat', + enabled: true + }); + // Initialize value if doesn't exist + if (extensionSettings.userStats[newId] === undefined) { + extensionSettings.userStats[newId] = 100; + } + renderUserStatsTab(); + }); + + // Remove stat + $('.rpg-stat-remove').off('click').on('click', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.userStats.customStats.splice(index, 1); + renderUserStatsTab(); + }); + + // Toggle stat + $('.rpg-stat-toggle').off('change').on('change', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.userStats.customStats[index].enabled = $(this).is(':checked'); + }); + + // Rename stat + $('.rpg-stat-name').off('blur').on('blur', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.userStats.customStats[index].name = $(this).val(); + }); + + // RPG attributes toggle + $('#rpg-show-rpg-attrs').off('change').on('change', function() { + extensionSettings.trackerConfig.userStats.showRPGAttributes = $(this).is(':checked'); + }); + + // Status section toggles + $('#rpg-status-enabled').off('change').on('change', function() { + extensionSettings.trackerConfig.userStats.statusSection.enabled = $(this).is(':checked'); + }); + + $('#rpg-mood-emoji').off('change').on('change', function() { + extensionSettings.trackerConfig.userStats.statusSection.showMoodEmoji = $(this).is(':checked'); + }); + + $('#rpg-status-fields').off('blur').on('blur', function() { + const fields = $(this).val().split(',').map(f => f.trim()).filter(f => f); + extensionSettings.trackerConfig.userStats.statusSection.customFields = fields; + }); + + // Skills section toggles + $('#rpg-skills-enabled').off('change').on('change', function() { + extensionSettings.trackerConfig.userStats.skillsSection.enabled = $(this).is(':checked'); + }); + + $('#rpg-skills-label').off('blur').on('blur', function() { + extensionSettings.trackerConfig.userStats.skillsSection.label = $(this).val(); + }); +} + +/** + * Render Info Box configuration tab + */ +function renderInfoBoxTab() { + const config = extensionSettings.trackerConfig.infoBox; + let html = '
'; + + html += '

Widgets

'; + + // Date widget + html += '
'; + html += ``; + html += ''; + html += ''; + html += '
'; + + // Weather widget + html += '
'; + html += ``; + html += ''; + html += '
'; + + // Temperature widget + html += '
'; + html += ``; + html += ''; + html += '
'; + html += ``; + html += ``; + html += '
'; + html += '
'; + + // Time widget + html += '
'; + html += ``; + html += ''; + html += '
'; + + // Location widget + html += '
'; + html += ``; + html += ''; + html += '
'; + + // Recent Events widget + html += '
'; + html += ``; + html += ''; + html += '
'; + + html += '
'; + + $('#rpg-editor-tab-infoBox').html(html); + setupInfoBoxListeners(); +} + +/** + * Set up event listeners for Info Box tab + */ +function setupInfoBoxListeners() { + const widgets = extensionSettings.trackerConfig.infoBox.widgets; + + $('#rpg-widget-date').off('change').on('change', function() { + widgets.date.enabled = $(this).is(':checked'); + }); + + $('#rpg-date-format').off('change').on('change', function() { + widgets.date.format = $(this).val(); + }); + + $('#rpg-widget-weather').off('change').on('change', function() { + widgets.weather.enabled = $(this).is(':checked'); + }); + + $('#rpg-widget-temperature').off('change').on('change', function() { + widgets.temperature.enabled = $(this).is(':checked'); + }); + + $('input[name="temp-unit"]').off('change').on('change', function() { + widgets.temperature.unit = $(this).val(); + }); + + $('#rpg-widget-time').off('change').on('change', function() { + widgets.time.enabled = $(this).is(':checked'); + }); + + $('#rpg-widget-location').off('change').on('change', function() { + widgets.location.enabled = $(this).is(':checked'); + }); + + $('#rpg-widget-events').off('change').on('change', function() { + widgets.recentEvents.enabled = $(this).is(':checked'); + }); +} + +/** + * Render Present Characters configuration tab + */ +function renderPresentCharactersTab() { + const config = extensionSettings.trackerConfig.presentCharacters; + let html = '
'; + + // Relationship Fields Section + html += '

Relationship Status Fields

'; + html += '

Define relationship types with corresponding emojis shown on character portraits

'; + + html += '
'; + // Show existing relationships as field β†’ emoji pairs + const relationshipEmojis = config.relationshipEmojis || { + 'Lover': '❀️', + 'Friend': '⭐', + 'Ally': '🀝', + 'Enemy': 'βš”οΈ', + 'Neutral': 'βš–οΈ' + }; + + for (const [relationship, emoji] of Object.entries(relationshipEmojis)) { + html += ` +
+ + β†’ + + +
+ `; + } + html += '
'; + html += ''; + + // Custom Fields Section + html += '

Appearance/Demeanor Fields

'; + html += '

Fields shown below character name, separated by |

'; + + html += '
'; + + config.customFields.forEach((field, index) => { + html += ` +
+
+ + +
+ + + + +
+ `; + }); + + html += '
'; + html += ''; + + // Thoughts Section + html += '

Thoughts Configuration

'; + html += '
'; + html += ``; + html += ''; + html += '
'; + + html += '
'; + html += '
'; + html += ''; + html += ``; + html += '
'; + html += '
'; + html += ''; + html += ``; + html += '
'; + html += '
'; + + // Character Stats + html += '

Character Stats

'; + html += '
'; + html += ``; + html += ''; + html += '
'; + + html += '

Create stats to track for each character (displayed as colored bars)

'; + html += '
'; + + const charStats = config.characterStats?.customStats || []; + charStats.forEach((stat, index) => { + html += ` +
+ + + +
+ `; + }); + + html += '
'; + html += ''; + + html += '
'; + + $('#rpg-editor-tab-presentCharacters').html(html); + setupPresentCharactersListeners(); +} + +/** + * Set up event listeners for Present Characters tab + */ +function setupPresentCharactersListeners() { + // Add new relationship + $('#rpg-add-relationship').off('click').on('click', function() { + if (!extensionSettings.trackerConfig.presentCharacters.relationshipEmojis) { + extensionSettings.trackerConfig.presentCharacters.relationshipEmojis = {}; + } + extensionSettings.trackerConfig.presentCharacters.relationshipEmojis['New Relationship'] = '😊'; + + // Sync relationshipFields + extensionSettings.trackerConfig.presentCharacters.relationshipFields = + Object.keys(extensionSettings.trackerConfig.presentCharacters.relationshipEmojis); + + renderPresentCharactersTab(); + }); + + // Remove relationship + $('.rpg-remove-relationship').off('click').on('click', function() { + const relationship = $(this).data('relationship'); + if (extensionSettings.trackerConfig.presentCharacters.relationshipEmojis) { + delete extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[relationship]; + } + + // Sync relationshipFields + extensionSettings.trackerConfig.presentCharacters.relationshipFields = + Object.keys(extensionSettings.trackerConfig.presentCharacters.relationshipEmojis); + + renderPresentCharactersTab(); + }); + + // Update relationship name + $('.rpg-relationship-name').off('blur').on('blur', function() { + const newName = $(this).val(); + const $item = $(this).closest('.rpg-relationship-item'); + const emoji = $item.find('.rpg-relationship-emoji').val(); + + // Find the old name by matching the emoji + const oldName = Object.keys(extensionSettings.trackerConfig.presentCharacters.relationshipEmojis).find( + key => extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[key] === emoji && + key !== newName + ); + + if (oldName && oldName !== newName) { + delete extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[oldName]; + extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[newName] = emoji; + + // Sync relationshipFields + extensionSettings.trackerConfig.presentCharacters.relationshipFields = + Object.keys(extensionSettings.trackerConfig.presentCharacters.relationshipEmojis); + } + }); + + // Update relationship emoji + $('.rpg-relationship-emoji').off('blur').on('blur', function() { + const name = $(this).closest('.rpg-relationship-item').find('.rpg-relationship-name').val(); + if (!extensionSettings.trackerConfig.presentCharacters.relationshipEmojis) { + extensionSettings.trackerConfig.presentCharacters.relationshipEmojis = {}; + } + extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[name] = $(this).val(); + }); + + // Thoughts configuration + $('#rpg-thoughts-enabled').off('change').on('change', function() { + if (!extensionSettings.trackerConfig.presentCharacters.thoughts) { + extensionSettings.trackerConfig.presentCharacters.thoughts = {}; + } + extensionSettings.trackerConfig.presentCharacters.thoughts.enabled = $(this).is(':checked'); + }); + + $('#rpg-thoughts-name').off('blur').on('blur', function() { + if (!extensionSettings.trackerConfig.presentCharacters.thoughts) { + extensionSettings.trackerConfig.presentCharacters.thoughts = {}; + } + extensionSettings.trackerConfig.presentCharacters.thoughts.name = $(this).val(); + }); + + $('#rpg-thoughts-description').off('blur').on('blur', function() { + if (!extensionSettings.trackerConfig.presentCharacters.thoughts) { + extensionSettings.trackerConfig.presentCharacters.thoughts = {}; + } + extensionSettings.trackerConfig.presentCharacters.thoughts.description = $(this).val(); + }); + + // Add field + $('#rpg-add-field').off('click').on('click', function() { + extensionSettings.trackerConfig.presentCharacters.customFields.push({ + id: 'custom_' + Date.now(), + name: 'New Field', + enabled: true, + description: 'Description for AI' + }); + renderPresentCharactersTab(); + }); + + // Remove field + $('.rpg-field-remove').off('click').on('click', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.presentCharacters.customFields.splice(index, 1); + renderPresentCharactersTab(); + }); + + // Move field up + $('.rpg-field-move-up').off('click').on('click', function() { + const index = $(this).data('index'); + if (index > 0) { + const fields = extensionSettings.trackerConfig.presentCharacters.customFields; + [fields[index - 1], fields[index]] = [fields[index], fields[index - 1]]; + renderPresentCharactersTab(); + } + }); + + // Move field down + $('.rpg-field-move-down').off('click').on('click', function() { + const index = $(this).data('index'); + const fields = extensionSettings.trackerConfig.presentCharacters.customFields; + if (index < fields.length - 1) { + [fields[index], fields[index + 1]] = [fields[index + 1], fields[index]]; + renderPresentCharactersTab(); + } + }); + + // Toggle field + $('.rpg-field-toggle').off('change').on('change', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.presentCharacters.customFields[index].enabled = $(this).is(':checked'); + }); + + // Rename field + $('.rpg-field-label').off('blur').on('blur', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.presentCharacters.customFields[index].name = $(this).val(); + }); + + // Update description + $('.rpg-field-placeholder').off('blur').on('blur', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.presentCharacters.customFields[index].description = $(this).val(); + }); + + // Character stats toggle + $('#rpg-char-stats-enabled').off('change').on('change', function() { + if (!extensionSettings.trackerConfig.presentCharacters.characterStats) { + extensionSettings.trackerConfig.presentCharacters.characterStats = { enabled: false, customStats: [] }; + } + extensionSettings.trackerConfig.presentCharacters.characterStats.enabled = $(this).is(':checked'); + }); + + // Add character stat + $('#rpg-add-char-stat').off('click').on('click', function() { + if (!extensionSettings.trackerConfig.presentCharacters.characterStats) { + extensionSettings.trackerConfig.presentCharacters.characterStats = { enabled: false, customStats: [] }; + } + if (!extensionSettings.trackerConfig.presentCharacters.characterStats.customStats) { + extensionSettings.trackerConfig.presentCharacters.characterStats.customStats = []; + } + extensionSettings.trackerConfig.presentCharacters.characterStats.customStats.push({ + id: `stat-${Date.now()}`, + name: 'New Stat', + enabled: true + }); + renderPresentCharactersTab(); + }); + + // Remove character stat + $('.rpg-char-stat-remove').off('click').on('click', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.presentCharacters.characterStats.customStats.splice(index, 1); + renderPresentCharactersTab(); + }); + + // Toggle character stat + $('.rpg-char-stat-toggle').off('change').on('change', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.presentCharacters.characterStats.customStats[index].enabled = $(this).is(':checked'); + }); + + // Rename character stat + $('.rpg-char-stat-label').off('blur').on('blur', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.presentCharacters.characterStats.customStats[index].name = $(this).val(); + }); +} diff --git a/style.css b/style.css index 44ada3e..806755d 100644 --- a/style.css +++ b/style.css @@ -19,7 +19,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { --rpg-accent: var(--black30a, rgba(22, 33, 62, 0.9)); --rpg-text: var(--SmartThemeBodyColor, #eaeaea); --rpg-highlight: var(--SmartThemeQuoteColor, #e94560); - --rpg-border: var(--SmartThemeBorderColor, #0f3460); + --rpg-border: var(--SmartThemeBorderColor, #4a7ba7); --rpg-shadow: rgba(0, 0, 0, 0.5); } @@ -938,6 +938,34 @@ body:has(.rpg-panel.rpg-position-left) #sheld { font-weight: 600; } +/* Skills Section */ +.rpg-skills-section { + display: flex; + align-items: center; + gap: 0.375em; + font-size: clamp(0.4vw, 0.5vw, 0.6vw); + padding: clamp(4px, 0.6vh, 6px) 0.375em; + background: rgba(0, 0, 0, 0.3); + border-radius: 0.25em; + border: 1px solid var(--rpg-border); + flex-shrink: 0; + margin-top: 0.375em; +} + +.rpg-skills-label { + font-weight: 700; + color: var(--rpg-highlight); + flex-shrink: 0; +} + +.rpg-skills-value { + flex: 1; + min-width: 0; + line-height: 1.2; + color: var(--rpg-text); + font-weight: 600; +} + /* Classic RPG Stats - Will match height of stats box automatically */ .rpg-classic-stats { display: flex; @@ -2214,7 +2242,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { .rpg-calendar-day { background: rgba(255, 255, 255, 0.1); color: var(--rpg-text); - font-size: 1.8rem; + font-size: clamp(0.5vw, 0.7vw, 0.85vw); font-weight: bold; padding: 0.1em; width: 100%; @@ -2225,7 +2253,10 @@ body:has(.rpg-panel.rpg-position-left) #sheld { display: flex; align-items: center; justify-content: center; - min-height: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; } .rpg-calendar-year { @@ -2851,8 +2882,31 @@ body:has(.rpg-panel.rpg-position-left) #sheld { border-left: none; /* Remove left border to avoid double accent with parent container */ transition: all 0.2s ease; width: 100%; /* Ensure cards take full width */ + max-height: clamp(120px, 18vh, 200px); box-sizing: border-box; /* Include padding and border in width calculation */ flex-shrink: 0; /* Prevent cards from shrinking */ + overflow-x: hidden; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--rpg-border) transparent; +} + +.rpg-character-card::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +.rpg-character-card::-webkit-scrollbar-track { + background: transparent; +} + +.rpg-character-card::-webkit-scrollbar-thumb { + background: var(--rpg-border); + border-radius: 2px; +} + +.rpg-character-card::-webkit-scrollbar-thumb:hover { + background: var(--rpg-highlight); } .rpg-character-card:hover { @@ -2893,9 +2947,15 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } /* Character info section */ -.rpg-character-info { +.rpg-character-content { flex: 1; min-width: 0; + display: flex; + flex-direction: column; + gap: 0; +} + +.rpg-character-info { display: flex; flex-direction: column; gap: clamp(3px, 0.5vh, 5px); @@ -2926,8 +2986,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld { text-overflow: ellipsis; } -/* Character traits/status line */ -.rpg-character-traits { +/* Character traits/status line and custom fields */ +.rpg-character-traits, +.rpg-character-field { font-size: clamp(0.6vw, 0.7vw, 0.8vw); color: var(--rpg-text); opacity: 0.8; @@ -2936,6 +2997,69 @@ body:has(.rpg-panel.rpg-position-left) #sheld { word-wrap: break-word; } +/* Placeholder for empty editable character fields */ +.rpg-character-field.rpg-editable:empty::before { + content: 'Click to edit...'; + color: var(--rpg-highlight); + opacity: 0.5; + font-style: italic; +} + +/* Character stats display */ +.rpg-character-stats { + width: 100%; + max-height: clamp(50px, 7vh, 70px); + margin-top: clamp(3px, 0.5vh, 5px); + padding: clamp(3px, 0.4vh, 5px) clamp(4px, 0.5vw, 6px); + background: var(--rpg-bg); + border: 1px solid var(--rpg-border); + border-radius: clamp(2px, 0.3vh, 4px); + box-sizing: border-box; + overflow: auto; + scrollbar-width: thin; + scrollbar-color: var(--rpg-border) transparent; +} + +.rpg-character-stats::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +.rpg-character-stats::-webkit-scrollbar-track { + background: transparent; +} + +.rpg-character-stats::-webkit-scrollbar-thumb { + background: var(--rpg-border); + border-radius: 2px; +} + +.rpg-character-stats::-webkit-scrollbar-thumb:hover { + background: var(--rpg-highlight); +} + +.rpg-character-stats-inner { + display: flex; + flex-wrap: wrap; + gap: clamp(6px, 1vw, 12px); + align-items: center; +} + +.rpg-character-stat { + flex-shrink: 0; + font-size: clamp(0.5vw, 0.6vw, 0.7vw) !important; + font-weight: 600 !important; + white-space: nowrap !important; +} + +.rpg-character-stat .rpg-stat-label { + color: var(--rpg-text) !important; +} + +.rpg-character-stat .rpg-stat-value { + font-weight: bold !important; +} + /* Placeholder styles for empty sections */ .rpg-thoughts-placeholder, .rpg-placeholder-widget { @@ -4518,6 +4642,433 @@ body:has(.rpg-panel.rpg-position-left) #sheld { transform: translateY(0); } +/* Settings buttons row (Edit Trackers + Settings side by side) */ +.rpg-settings-buttons-row { + display: flex; + gap: 0.5em; + width: 100%; +} + +.rpg-btn-half { + flex: 1; + min-width: 0; +} + +/* ============================================ + TRACKER EDITOR MODAL + ============================================ */ + +/* Editor tabs */ +.rpg-editor-tabs { + display: flex; + gap: 0; + border-bottom: 2px solid var(--rpg-border); + margin-bottom: 1em; +} + +.rpg-editor-tab { + flex: 1; + padding: 0.75em 1em; + background: var(--rpg-accent); + border: none; + border-bottom: 3px solid transparent; + color: var(--rpg-text); + font-size: 0.9em; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5em; +} + +.rpg-editor-tab:hover { + background: var(--rpg-bg); +} + +.rpg-editor-tab.active { + background: var(--rpg-bg); + border-bottom-color: var(--rpg-highlight); +} + +.rpg-editor-tab-content { + max-height: 60vh; + overflow-y: auto; +} + +.rpg-editor-section { + padding: 1em 0; +} + +.rpg-editor-section h4 { + color: var(--rpg-highlight); + margin: 1em 0 0.5em 0; + display: flex; + align-items: center; + gap: 0.5em; +} + +.rpg-editor-section h4:first-child { + margin-top: 0; +} + +.rpg-editor-hint { + font-size: 0.9em; + color: var(--rpg-text); + opacity: 0.7; + margin-bottom: 1em; +} + +/* Stats list */ +.rpg-editor-stats-list { + display: flex; + flex-direction: column; + gap: 0.5em; + margin-bottom: 1em; +} + +.rpg-editor-stat-item { + display: flex; + align-items: center; + gap: 0.5em; + padding: 0.5em; + background: var(--rpg-accent); + border: 1px solid var(--rpg-border); + border-radius: 0.375em; +} + +.rpg-stat-toggle { + flex-shrink: 0; +} + +.rpg-stat-name { + flex: 1; + padding: 0.375em 0.5em; + background: var(--rpg-bg); + border: 1px solid var(--rpg-border); + border-radius: 0.25em; + color: var(--rpg-text); + font-size: 0.95em; +} + +.rpg-stat-remove { + flex-shrink: 0; + padding: 0.375em 0.625em; + background: var(--rpg-highlight); + border: none; + border-radius: 0.25em; + color: white; + cursor: pointer; + transition: opacity 0.2s; +} + +.rpg-stat-remove:hover { + opacity: 0.8; +} + +/* Toggle rows */ +.rpg-editor-toggle-row { + display: flex; + align-items: center; + gap: 0.5em; + margin: 0.75em 0; +} + +.rpg-editor-toggle-row input[type="checkbox"] { + flex-shrink: 0; +} + +.rpg-editor-toggle-row label { + flex: 1; + color: var(--rpg-text); +} + +/* Text inputs */ +.rpg-text-input { + width: 100%; + padding: 0.5em; + background: var(--rpg-bg); + border: 1px solid var(--rpg-border); + border-radius: 0.375em; + color: var(--rpg-text); + font-size: 0.95em; + margin-top: 0.25em; +} + +/* Widget rows */ +.rpg-editor-widget-row { + display: flex; + align-items: center; + gap: 0.75em; + padding: 0.625em; + background: var(--rpg-accent); + border: 1px solid var(--rpg-border); + border-radius: 0.375em; + margin-bottom: 0.5em; +} + +.rpg-editor-widget-row input[type="checkbox"] { + flex-shrink: 0; +} + +.rpg-editor-widget-row label { + flex: 1; + color: var(--rpg-text); +} + +.rpg-select-mini { + padding: 0.375em 0.5em; + background: var(--rpg-bg); + border: 1px solid var(--rpg-border); + border-radius: 0.25em; + color: var(--rpg-text); + font-size: 0.9em; +} + +.rpg-radio-group { + display: flex; + gap: 1em; +} + +.rpg-radio-group label { + display: flex; + align-items: center; + gap: 0.375em; + color: var(--rpg-text); +} + +/* Character fields list */ +.rpg-editor-fields-list { + display: flex; + flex-direction: column; + gap: 0.5em; + margin-bottom: 1em; +} + +/* Relationship Mapping Styles */ +.rpg-relationship-mapping-list { + display: flex; + flex-direction: column; + gap: 0.5em; + margin-bottom: 1em; +} + +.rpg-relationship-item { + display: grid; + grid-template-columns: 1fr auto 80px auto; + align-items: center; + gap: 0.75em; + padding: 0.5em; + background: var(--rpg-accent); + border: 1px solid var(--rpg-border); + border-radius: 0.375em; +} + +.rpg-relationship-name, +.rpg-relationship-emoji { + padding: 0.375em 0.5em; + background: var(--rpg-bg); + border: 1px solid var(--rpg-border); + border-radius: 0.25em; + color: var(--rpg-text); + font-size: 0.95em; +} + +.rpg-relationship-emoji { + text-align: center; + font-size: 1.2em; +} + +.rpg-arrow { + color: var(--rpg-highlight); + font-weight: bold; + font-size: 1.2em; +} + +/* Thoughts Configuration Input Groups */ +.rpg-thoughts-config { + display: flex; + flex-direction: column; + gap: 1em; + margin-top: 0.75em; +} + +.rpg-editor-input-group { + display: flex; + flex-direction: column; + gap: 0.375em; +} + +.rpg-editor-input-group label { + font-size: 0.9em; + color: var(--rpg-text); + opacity: 0.9; +} + +.rpg-editor-input-group input[type="text"] { + width: 100%; + padding: 0.5em; + background: var(--rpg-accent); + border: 1px solid var(--rpg-border); + border-radius: 0.25em; + color: var(--rpg-text); + font-size: 0.95em; +} + +.rpg-editor-field-item { + display: grid; + grid-template-columns: auto auto 1fr 2fr auto; + align-items: center; + gap: 0.5em; + padding: 0.5em; + background: var(--rpg-accent); + border: 1px solid var(--rpg-border); + border-radius: 0.375em; +} + +.rpg-field-controls { + display: flex; + flex-direction: column; + gap: 0.25em; +} + +.rpg-field-move-up, +.rpg-field-move-down { + padding: 0.125em 0.375em; + background: var(--rpg-bg); + border: 1px solid var(--rpg-border); + border-radius: 0.25em; + color: var(--rpg-text); + cursor: pointer; + font-size: 0.75em; +} + +.rpg-field-move-up:disabled, +.rpg-field-move-down:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.rpg-field-toggle { + flex-shrink: 0; +} + +.rpg-field-label, +.rpg-char-stat-label, +.rpg-field-placeholder { + padding: 0.375em 0.5em; + background: var(--rpg-bg); + border: 1px solid var(--rpg-border); + border-radius: 0.25em; + color: var(--rpg-text); + font-size: 0.95em; +} + +.rpg-field-remove { + flex-shrink: 0; + padding: 0.375em 0.625em; + background: var(--rpg-highlight); + border: none; + border-radius: 0.25em; + color: white; + cursor: pointer; + transition: opacity 0.2s; +} + +.rpg-field-remove:hover { + opacity: 0.8; +} + +/* Character stats checkboxes */ +.rpg-char-stats-checkboxes { + display: flex; + flex-wrap: wrap; + gap: 1em; + margin-top: 0.5em; +} + +.rpg-char-stats-checkboxes label { + display: flex; + align-items: center; + gap: 0.375em; + color: var(--rpg-text); +} + +/* Footer buttons */ +.rpg-settings-popup-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1em; + border-top: 2px solid var(--rpg-border); + gap: 1em; + background: var(--rpg-accent); +} + +.rpg-footer-right { + display: flex; + gap: 0.5em; +} + +/* Editor buttons */ +.rpg-btn-primary, +.rpg-btn-secondary, +.rpg-btn-cancel, +.rpg-btn-reset { + padding: 0.625em 1.25em; + border: 1px solid var(--rpg-border); + border-radius: 0.375em; + cursor: pointer; + font-size: 0.95em; + font-weight: 600; + transition: all 0.2s; +} + +.rpg-btn-primary { + background: var(--rpg-highlight); + color: white; + border-color: var(--rpg-highlight); +} + +.rpg-btn-primary:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +.rpg-btn-secondary { + background: var(--rpg-bg); + color: var(--rpg-text); + border-color: var(--rpg-border); +} + +.rpg-btn-secondary:hover { + background: var(--rpg-accent); + transform: translateY(-1px); +} + +.rpg-btn-cancel { + background: var(--rpg-accent); + color: var(--rpg-text); + border-color: var(--rpg-border); +} + +.rpg-btn-cancel:hover { + background: var(--rpg-bg); +} + +.rpg-btn-reset { + background: transparent; + color: var(--rpg-text); + border-color: var(--rpg-border); + opacity: 0.8; +} + +.rpg-btn-reset:hover { + opacity: 1; + background: var(--rpg-accent); +} + /* ============================================ SETTINGS MODAL - MOBILE FIRST ============================================ */ @@ -4642,6 +5193,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld { -webkit-overflow-scrolling: touch; flex: 1 1 auto; min-height: 0; + background: transparent; + color: inherit; } /* Settings Groups */ @@ -4665,13 +5218,24 @@ body:has(.rpg-panel.rpg-position-left) #sheld { font-weight: 600; } -/* Theme Support */ +/* Theme Support - Default Theme (no data-theme attribute) */ +.rpg-settings-popup-content { + --rpg-bg: var(--SmartThemeBlurTintColor, rgba(26, 26, 46, 0.9)); + --rpg-accent: var(--black30a, rgba(22, 33, 62, 0.9)); + --rpg-text: var(--SmartThemeBodyColor, #eaeaea); + --rpg-highlight: var(--SmartThemeQuoteColor, #e94560); + --rpg-border: #6b9fd4; +} + +/* Theme Support - Settings Modal */ #rpg-settings-popup[data-theme="sci-fi"] .rpg-settings-popup-content { --rpg-bg: #0a0e27; --rpg-accent: #1a1f3a; --rpg-text: #00ffff; --rpg-highlight: #ff00ff; --rpg-border: #00ffff; + background: rgba(10, 14, 39, 0.95); + color: #00ffff; } #rpg-settings-popup[data-theme="fantasy"] .rpg-settings-popup-content { @@ -4680,6 +5244,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld { --rpg-text: #f4e4c1; --rpg-highlight: #d4af37; --rpg-border: #8b6914; + background: rgba(43, 24, 16, 0.95); + color: #f4e4c1; } #rpg-settings-popup[data-theme="cyberpunk"] .rpg-settings-popup-content { @@ -4688,6 +5254,39 @@ body:has(.rpg-panel.rpg-position-left) #sheld { --rpg-text: #00ff9f; --rpg-highlight: #ff00ff; --rpg-border: #ff00ff; + background: rgba(13, 2, 33, 0.95); + color: #00ff9f; +} + +/* Theme Support - Tracker Editor Modal */ +#rpg-tracker-editor-popup[data-theme="sci-fi"] .rpg-settings-popup-content { + --rpg-bg: #0a0e27; + --rpg-accent: #1a1f3a; + --rpg-text: #00ffff; + --rpg-highlight: #ff00ff; + --rpg-border: #00ffff; + background: rgba(10, 14, 39, 0.95); + color: #00ffff; +} + +#rpg-tracker-editor-popup[data-theme="fantasy"] .rpg-settings-popup-content { + --rpg-bg: #2b1810; + --rpg-accent: #3d2516; + --rpg-text: #f4e4c1; + --rpg-highlight: #d4af37; + --rpg-border: #8b6914; + background: rgba(43, 24, 16, 0.95); + color: #f4e4c1; +} + +#rpg-tracker-editor-popup[data-theme="cyberpunk"] .rpg-settings-popup-content { + --rpg-bg: #0d0221; + --rpg-accent: #1a0b2e; + --rpg-text: #00ff9f; + --rpg-highlight: #ff00ff; + --rpg-border: #ff00ff; + background: rgba(13, 2, 33, 0.95); + color: #00ff9f; } /* Desktop Enhancement (1001px+) */ @@ -5264,6 +5863,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld { height: 100%; min-height: 0; margin: -12px -12px 16px -12px; + overflow-x: auto; + overflow-y: hidden; } /* Tab container at top of panel */ @@ -5406,7 +6007,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-calendar-day { - font-size: clamp(11px, 2.9vw, 14px) !important; + font-size: clamp(10px, 2.9vw, 14px) !important; } .rpg-calendar-year { @@ -5899,8 +6500,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld { font-size: clamp(12px, 3.1vw, 16px) !important; } - /* Readable character traits on mobile */ - .rpg-character-traits { + /* Readable character traits and custom fields on mobile */ + .rpg-character-traits, + .rpg-character-field { font-size: clamp(11px, 2.8vw, 14px) !important; } diff --git a/template.html b/template.html index 014e796..f6cff33 100644 --- a/template.html +++ b/template.html @@ -64,10 +64,15 @@ Refresh RPG Info - - + +
+ + +
@@ -312,3 +317,48 @@
+ + +