commit 518f2763aaece6bdc3a9b1807cd6518c81cc846c Author: Spicy_Marinara Date: Tue Oct 14 00:01:23 2025 +0200 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e0c0953 --- /dev/null +++ b/LICENSE @@ -0,0 +1,34 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +[Full AGPLv3 license text would go here - this is abbreviated for brevity] + +For the full license text, see: https://www.gnu.org/licenses/agpl-3.0.txt + +--- + +RPG Companion Extension for SillyTavern +Copyright (C) 2024 Marysia (marinara_spaghetti) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bfc3c4 --- /dev/null +++ b/README.md @@ -0,0 +1,367 @@ +# RPG Companion Extension for SillyTavern# RPG Companion Extension for SillyTavern + +An immersive RPG extension that tracks character stats, scene information, and character thoughts in a beautiful, customizable UI panel. All automated! Works with any preset. Choose between Together or Separate generation modes for context and generations control. + +[![Discord](https://img.shields.io/badge/Discord-Join%20Server-7289da)](https://discord.com/invite/KdAkTg94ME)## Features + +[![Support](https://img.shields.io/badge/Ko--fi-Support%20Creator-ff5e5b)](https://ko-fi.com/marinara_spaghetti) + +- **User Stats Tracker**: Displays health, sustenance, energy, hygiene, arousal, mood, and conditions with visual progress bars + +## ✨ Features- **Info Box**: Shows scene information including date, time, location, weather, and present characters + +- **Character Thoughts**: Reveals the AI character's internal monologue + +### Core Functionality- **Automatic Updates**: Automatically updates RPG data after each message exchange + +- **📊 User Stats Tracker**: Visual progress bars for health, sustenance, energy, hygiene, arousal, mood, and conditions- **Customizable**: Control what information is displayed and when + +- **🌍 Info Box Dashboard**: Beautiful widgets displaying date, weather, temperature, time, and location- **Non-Intrusive**: Keeps RPG mechanics separate from the main roleplay, reducing prompt clutter + +- **💭 Character Thoughts**: Floating thought bubbles showing AI characters' internal monologue (editable in real-time!) + +- **🎲 Classic RPG Stats**: STR, DEX, CON, INT, WIS, CHA attributes with dice roll support## Installation + +- **đŸ“Ļ Inventory System**: Track items your character is carrying + +- **🎨 Multiple Themes**: Cyberpunk, Fantasy, Minimal, Dark, Light, and Custom themes1. The extension should already be in your `public/scripts/extensions/rpg-companion/` folder + +- **âœī¸ Live Editing**: Edit stats, thoughts, weather, and more directly in the panels2. Restart SillyTavern if it's running + +- **💾 Per-Swipe Data Storage**: Each swipe preserves its own tracker data3. Go to Extensions > Manage Extensions + +4. Enable "RPG Companion" + +5. Reload the page + +### Generation Modes + + + +#### Together Mode## How It Works + +- Generates tracker data **within the main AI response** + +- Cleaner, single-generation approachInstead of having the AI model generate RPG companion data in its main response, this extension: + +- Data automatically extracted and formatted in the sidebar + +- Best for: Users who want seamless integration without extra API calls1. Lets you roleplay normally without RPG prompts cluttering the conversation + +2. After each AI response, automatically sends a separate request to the model + +#### Separate Mode3. Includes only the last few messages (configurable) for context + +- Generates tracker data in a **separate API call** after the main response4. Asks the model to generate ONLY the RPG companion data + +- Main roleplay stays clean without tracker formatting5. Displays the formatted data in a dedicated panel + +- Contextual summary injected for immersive storytelling + +- Best for: Users who want pure roleplay responses and don't mind extra API callsThis approach: + +- ✅ Keeps your main roleplay clean and focused + +### Smart Features- ✅ Reduces token usage in the main conversation + +- **🔄 Swipe Detection**: Automatically handles swipes and maintains correct tracker context- ✅ Allows the model to focus on roleplay quality + +- **📝 Context-Aware**: Weather, stats, and character states naturally influence the narrative- ✅ Provides a better visual presentation of stats and info + +- **🎭 Multiple Characters**: Tracks thoughts and relationships for all present characters + +- **📍 Thought Bubbles in Chat**: Optional floating thought bubbles positioned next to character avatars## Settings + +- **🌈 Customizable Colors**: Create your own theme with custom color schemes + +- **📱 Mobile Responsive**: Works beautifully on all devices### Main Controls + +- **Enable RPG Companion**: Turn the extension on/off + +## đŸ“Ļ Installation- **Auto-update after messages**: Automatically refresh RPG data after each message + +- **Context Messages**: How many recent messages to include when generating updates (default: 4) + +1. Download or clone this repository into your SillyTavern extensions folder: + + ```### Display Options + + SillyTavern/public/scripts/extensions/rpg-companion/- **Show User Stats**: Display the character stats panel + + ```- **Show Info Box**: Display the scene information panel + +- **Show Character Thoughts**: Display the AI character's internal thoughts + +2. Restart SillyTavern + +### Model Selection + +3. Go to **Extensions** tab → Find **RPG Companion** → Enable it- **Use main chat model**: Use the same model as your chat (recommended) + +- Custom model selection (coming soon) + +4. Open the extension panel (appears on the right side by default) + +## Manual Update + +5. Configure your settings and start roleplaying! + +If auto-update is disabled, you can click the "Manual Update" button in the settings to refresh the RPG data at any time. + +## 🎮 How to Use + +## Planned Features + +### Quick Start + +- [ ] Support for selecting a different model for RPG updates + +1. **Enable the extension** in the Extensions tab- [ ] Relationship/Standing system with characters + +2. **Choose your generation mode**:- [ ] Support for immersive HTML elements + + - **Together**: Tracker data generated with the AI response- [ ] Random plot push integration + + - **Separate**: Tracker data generated in a separate call (requires auto-update)- [ ] Export/import RPG data + +3. **Select which panels to display** (User Stats, Info Box, Character Thoughts)- [ ] Historical stats tracking + +4. **Start chatting!** The tracker updates automatically- [ ] Custom stat categories + +- [ ] Integration with character cards + +### Generation Modes Explained + +## Tips + +#### Together Mode + +```1. **Context Messages**: Start with 4 messages and adjust based on your needs. More messages = better context but slower updates + +User: *walks into the tavern*2. **Performance**: If updates are slow, consider reducing the context depth or using a faster model + +AI: [Full roleplay response]3. **Customization**: You can modify the prompts in `index.js` to add your own stat categories or change the format + +``` + +↓ Extension extracts tracker data from response## Compatibility + +↓ Displays in sidebar panels + +↓ Main chat shows clean roleplay text- Requires SillyTavern 1.11.0 or higher + +- Works with all AI backends (OpenAI, Claude, KoboldAI, etc.) + +#### Separate Mode- Mobile-responsive design + +``` + +User: *walks into the tavern*## Credits + +AI: [Pure roleplay response - no tracker data] + +```- Stats Tracker: Original concept by user + +↓ Extension sends separate request with context- Info Box: Credit to MidnightSleeper for the original prompt + +↓ AI generates only tracker data- Immersive HTML: Credit to u/melted_walrus for the original concept + +↓ Displays in sidebar panels- Extension Development: Marysia with assistance from GitHub Copilot + +↓ Context summary injected into next generation + +## Troubleshooting + +### Editing Tracker Data + +### Extension doesn't appear + +You can edit most fields by clicking on them:- Make sure you've restarted SillyTavern after installation + +- **Stats**: Click on percentage values, mood emoji, conditions, or inventory- Check browser console (F12) for errors + +- **Info Box**: Click on date fields, weather, temperature, time, or location- Verify all files are in the correct location + +- **Character Thoughts**: Click on emoji, name, traits, relationship, or thoughts + +### Stats not updating + +**Note**: When editing character thoughts in the floating bubble, the bubble will refresh to maintain proper positioning.- Check that "Auto-update" is enabled + +- Try clicking "Manual Update" to test + +### Swipe Support- Verify your AI backend is responding correctly + +- Check browser console for error messages + +The extension fully supports swipes: + +- Each swipe stores its own tracker data### Display issues + +- Swiping loads the data for that specific swipe- Try refreshing the page + +- New swipe generation uses the committed data from before the swipe- Check if other extensions are conflicting + +- User edits are preserved across swipes- Verify CSS is loading correctly + + + +## âš™ī¸ Settings## License + + + +### Main Panel ControlsMIT License - Feel free to modify and share! + +- **Panel Position**: Left or Right side of the chat + +- **Theme**: Choose from 6 built-in themes or create custom## Support + +- **Generation Mode**: Together or Separate + +- **Auto-update**: Toggle automatic updates (required for Separate mode)For issues, suggestions, or contributions, please visit the SillyTavern GitHub repository. + +- **Update Depth**: Number of messages to include as context (1-10) + +### Display Toggles +- **Show User Stats**: Character stats panel +- **Show Info Box**: Scene information dashboard +- **Show Character Thoughts**: Character thoughts panel +- **Show Thoughts in Chat**: Floating thought bubbles next to avatars + +### Advanced Options +- **Enable Animations**: Smooth transitions for panel updates +- **Enable HTML Prompt**: Allow creative HTML/CSS/JS elements in responses +- **Classic Stats**: STR, DEX, CON, INT, WIS, CHA attributes +- **Dice Rolling**: Roll checks against your classic stats + +## 🎨 Themes + +Built-in themes: +- **Cyberpunk**: Neon colors and futuristic vibes +- **Fantasy**: Warm, medieval aesthetic +- **Minimal**: Clean and simple +- **Dark**: Low-light, high contrast +- **Light**: Bright and airy +- **Custom**: Define your own colors! + +## 🔧 Technical Details + +### Data Flow (Together Mode) +1. User sends message → flag set to `false` (new message) +2. Extension injects tracker instructions into prompt +3. AI generates response with tracker data in code blocks +4. Extension extracts and parses tracker data +5. Updates `lastGeneratedData` (displayed) +6. Stores per-swipe data in message.extra +7. On next user message, commits data to `committedTrackerData` (used for generation) + +### Data Flow (Separate Mode) +1. User sends message → flag set to `false` +2. Extension injects contextual summary into prompt +3. AI generates pure roleplay response +4. Extension sends separate request for tracker update +5. AI generates only tracker data +6. Updates and stores data same as Together mode + +### Swipe Detection +- Uses `MESSAGE_SENT` and `MESSAGE_SWIPED` events +- Distinguishes between new swipe generation and navigation +- Maintains separate committed data and displayed data +- Ensures consistency across swipe operations + +## đŸŽ¯ Prompting Tips + +### For Best Results (Together Mode) +The extension provides clear instructions to the AI. The model will: +- Generate tracker data in code blocks +- Update only changed values +- Maintain consistency across messages + +### For Best Results (Separate Mode) +- Use 3-5 message depth for good context +- The AI receives a clean context summary +- Tracker updates focus only on changes + +### HTML Elements (Optional) +Enable "HTML Prompt" to allow creative visual elements: +- Computer screens, signs, posters, books +- 3D effects, animations, interactive elements +- Styled thematically to match your setting +- No external dependencies required + +## 🤝 Credits + +- **Extension Development**: Marysia with assistance from GitHub Copilot +- **Immersive HTML Concept**: u/melted_walrus +- **Community Feedback**: SillyTavern Discord community + +## 📝 License + +``` +RPG Companion Extension for SillyTavern +Copyright (C) 2024 Marysia (marinara_spaghetti) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +``` + +See [LICENSE](LICENSE) file for full license text. + +## đŸ’Ŧ Support & Community + +- **Discord**: [Join our server](https://discord.com/invite/KdAkTg94ME) +- **Support the Creator**: [Ko-fi](https://ko-fi.com/marinara_spaghetti) +- **Issues**: Report bugs via GitHub issues +- **Contributions**: Pull requests welcome! + +## 🐛 Troubleshooting + +### Extension doesn't appear +- Restart SillyTavern after installation +- Check browser console (F12) for errors +- Verify all files are in `/public/scripts/extensions/rpg-companion/` + +### Tracker data not updating +- **Together Mode**: Check that instructions are being included in prompts +- **Separate Mode**: Ensure auto-update is enabled +- Verify your AI model is responding correctly +- Check browser console for errors + +### Thought bubbles not appearing +- Enable "Show Thoughts in Chat" toggle +- Verify character thoughts data exists +- Check that panel position doesn't conflict with chat layout + +### Edits not saving +- Ensure you click away from the field after editing (blur event) +- Check browser console for errors +- Verify chat data is saving correctly + +### Swipe data issues +- Each swipe stores its own data independently +- If data seems wrong, try regenerating that swipe +- Check that committedTrackerData is properly initialized + +## 🚀 Future Ideas + +- Custom stat categories +- Historical stats tracking/graphs +- Export/import functionality +- Advanced relationship systems +- Quest/objective tracking +- Achievement system +- Integration with character card metadata + +--- + +**Enjoy your immersive RPG experience!** 🎲✨ diff --git a/index.js b/index.js new file mode 100644 index 0000000..0899a37 --- /dev/null +++ b/index.js @@ -0,0 +1,3113 @@ +import { getContext, renderExtensionTemplateAsync } from '../../extensions.js'; +import { eventSource, event_types, substituteParams, chat, generateRaw, Generate, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat } from '../../../script.js'; +import { selected_group, getGroupMembers } from '../../group-chats.js'; +import { power_user } from '../../power-user.js'; + +const extensionName = 'rpg-companion'; +const extensionFolderPath = `scripts/extensions/${extensionName}`; + +let extensionSettings = { + enabled: true, + autoUpdate: true, + updateDepth: 4, // How many messages to include in the context + generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately + showUserStats: true, + showInfoBox: true, + showCharacterThoughts: true, + showThoughtsInChat: false, // Show thoughts overlay in chat + enableHtmlPrompt: false, // Enable immersive HTML prompt injection + enablePlotButtons: true, // Show plot progression buttons above chat input + panelPosition: 'right', // 'left', 'right', or 'top' + theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom + customColors: { + bg: '#1a1a2e', + accent: '#16213e', + text: '#eaeaea', + highlight: '#e94560' + }, + statBarColorLow: '#cc3333', // Color for low stat values (red) + statBarColorHigh: '#33cc66', // Color for high stat values (green) + enableAnimations: true, // Enable smooth animations for stats and content updates + userStats: { + health: 100, + sustenance: 100, + energy: 100, + hygiene: 100, + arousal: 0, + mood: '😐', + conditions: 'None', + inventory: 'None' + }, + classicStats: { + str: 10, + dex: 10, + con: 10, + int: 10, + wis: 10, + cha: 10 + }, + lastDiceRoll: null // Store last dice roll result +}; + +let lastGeneratedData = { + userStats: null, + infoBox: null, + characterThoughts: null, + html: null +}; + +// Tracks the "committed" tracker data that should be used as source for next generation +// This gets updated when user sends a new message or first time generation +let committedTrackerData = { + userStats: null, + infoBox: null, + characterThoughts: null +}; + +// Tracks whether the last action was a swipe (for separate mode) +// Used to determine whether to commit lastGeneratedData to committedTrackerData +let lastActionWasSwipe = false; + +let isGenerating = false; + +// UI Elements +let $panelContainer = null; +let $userStatsContainer = null; +let $infoBoxContainer = null; +let $thoughtsContainer = null; + +/** + * Loads the extension settings from the global settings object. + */ +function loadSettings() { + if (power_user.extensions && power_user.extensions[extensionName]) { + Object.assign(extensionSettings, power_user.extensions[extensionName]); + // console.log('[RPG Companion] Settings loaded:', extensionSettings); + } else { + // console.log('[RPG Companion] No saved settings found, using defaults'); + } +} + +/** + * Saves the extension settings to the global settings object. + */ +function saveSettings() { + if (!power_user.extensions) { + power_user.extensions = {}; + } + power_user.extensions[extensionName] = extensionSettings; + saveSettingsDebounced(); +} + +/** + * Saves RPG data to the current chat's metadata. + */ +function saveChatData() { + if (!chat_metadata) { + return; + } + + chat_metadata.rpg_companion = { + userStats: extensionSettings.userStats, + classicStats: extensionSettings.classicStats, + lastGeneratedData: lastGeneratedData, + timestamp: Date.now() + }; + + saveChatDebounced(); +} + +/** + * Updates the last assistant message's swipe data with current tracker data. + * This ensures user edits are preserved across swipes and included in generation context. + */ +function updateMessageSwipeData() { + const chat = getContext().chat; + if (!chat || chat.length === 0) { + return; + } + + // 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 = {}; + } + if (!message.extra.rpg_companion_swipes) { + message.extra.rpg_companion_swipes = {}; + } + + const swipeId = message.swipe_id || 0; + message.extra.rpg_companion_swipes[swipeId] = { + userStats: lastGeneratedData.userStats, + infoBox: lastGeneratedData.infoBox, + characterThoughts: lastGeneratedData.characterThoughts + }; + + // console.log('[RPG Companion] Updated message swipe data after user edit'); + break; + } + } +} + +/** + * Loads RPG data from the current chat's metadata. + */ +function loadChatData() { + if (!chat_metadata || !chat_metadata.rpg_companion) { + // Reset to defaults if no data exists + extensionSettings.userStats = { + health: 100, + sustenance: 100, + energy: 100, + hygiene: 100, + arousal: 0, + mood: '😐', + conditions: 'None', + inventory: 'None' + }; + lastGeneratedData = { + userStats: null, + infoBox: null, + characterThoughts: null, + html: null + }; + return; + } + + const savedData = chat_metadata.rpg_companion; + + // Restore stats + if (savedData.userStats) { + extensionSettings.userStats = { ...savedData.userStats }; + } + + // Restore classic stats + if (savedData.classicStats) { + extensionSettings.classicStats = { ...savedData.classicStats }; + } + + // Restore last generated data + if (savedData.lastGeneratedData) { + lastGeneratedData = { ...savedData.lastGeneratedData }; + } + + // console.log('[RPG Companion] Loaded chat data:', savedData); +} + +/** + * Applies the selected theme to the panel. + */ +function applyTheme() { + if (!$panelContainer) return; + + const theme = extensionSettings.theme; + + // Remove all theme attributes first + $panelContainer.removeAttr('data-theme'); + + // Clear any inline CSS variable overrides + $panelContainer.css({ + '--rpg-bg': '', + '--rpg-accent': '', + '--rpg-text': '', + '--rpg-highlight': '', + '--rpg-border': '', + '--rpg-shadow': '' + }); + + // Apply the selected theme + if (theme === 'custom') { + applyCustomTheme(); + } else if (theme !== 'default') { + // For non-default themes, set the data-theme attribute + // which will trigger the CSS theme rules + $panelContainer.attr('data-theme', theme); + } + // For 'default', we do nothing - it will use the CSS variables from .rpg-panel class + // which fall back to SillyTavern's theme variables +} + +/** + * Applies custom colors when custom theme is selected. + */ +function applyCustomTheme() { + if (!$panelContainer) return; + + const colors = extensionSettings.customColors; + + // Apply custom CSS variables as inline styles + $panelContainer.css({ + '--rpg-bg': colors.bg, + '--rpg-accent': colors.accent, + '--rpg-text': colors.text, + '--rpg-highlight': colors.highlight, + '--rpg-border': colors.highlight, + '--rpg-shadow': `${colors.highlight}80` // Add alpha for shadow + }); +} + +/** + * Toggles visibility of custom color pickers. + */ +function toggleCustomColors() { + const isCustom = extensionSettings.theme === 'custom'; + $('#rpg-custom-colors').toggle(isCustom); +} + +/** + * Toggles animations on/off by adding/removing a class to the panel. + */ +function toggleAnimations() { + if (extensionSettings.enableAnimations) { + $panelContainer.addClass('rpg-animations-enabled'); + } else { + $panelContainer.removeClass('rpg-animations-enabled'); + } +} + +/** + * Adds the extension settings to the Extensions tab. + */ +async function addExtensionSettings() { + const settingsHtml = await renderExtensionTemplateAsync(extensionName, 'settings'); + $('#rpg_companion_container').append(settingsHtml); + + // Set up the enable/disable toggle + $('#rpg-extension-enabled').prop('checked', extensionSettings.enabled).on('change', function() { + extensionSettings.enabled = $(this).prop('checked'); + saveSettings(); + updatePanelVisibility(); + + if (!extensionSettings.enabled) { + // Clear extension prompts and thought bubbles when disabled + clearExtensionPrompts(); + updateChatThoughts(); // This will remove the thought bubble since extension is disabled + } else { + // Re-create thought bubbles when re-enabled + updateChatThoughts(); // This will re-create the thought bubble if data exists + } + }); +} + +/** + * Initializes the UI for the extension. + */ +async function initUI() { + // Load the HTML template using SillyTavern's template system + const templateHtml = await renderExtensionTemplateAsync(extensionName, 'template'); + + // Add to the appropriate container based on position + if (extensionSettings.panelPosition === 'right') { + // Add as a sidebar next to the chat area (sheld) + $('#sheld').after(templateHtml); + } else { + // Add above the chat area + $('#sheld').before(templateHtml); + } // Cache UI elements + $panelContainer = $('#rpg-companion-panel'); + $userStatsContainer = $('#rpg-user-stats'); + $infoBoxContainer = $('#rpg-info-box'); + $thoughtsContainer = $('#rpg-thoughts'); + + // Set up event listeners (enable/disable is handled in Extensions tab) + $('#rpg-toggle-auto-update').on('change', function() { + extensionSettings.autoUpdate = $(this).prop('checked'); + saveSettings(); + }); + + $('#rpg-position-select').on('change', function() { + extensionSettings.panelPosition = String($(this).val()); + saveSettings(); + applyPanelPosition(); + // Recreate thought bubbles to update their position + updateChatThoughts(); + }); + + $('#rpg-update-depth').on('change', function() { + const value = $(this).val(); + extensionSettings.updateDepth = parseInt(String(value)); + saveSettings(); + }); + + $('#rpg-generation-mode').on('change', function() { + extensionSettings.generationMode = String($(this).val()); + saveSettings(); + updateGenerationModeUI(); + }); + + $('#rpg-toggle-user-stats').on('change', function() { + extensionSettings.showUserStats = $(this).prop('checked'); + saveSettings(); + updateSectionVisibility(); + }); + + $('#rpg-toggle-info-box').on('change', function() { + extensionSettings.showInfoBox = $(this).prop('checked'); + saveSettings(); + updateSectionVisibility(); + }); + + $('#rpg-toggle-thoughts').on('change', function() { + extensionSettings.showCharacterThoughts = $(this).prop('checked'); + saveSettings(); + updateSectionVisibility(); + }); + + $('#rpg-toggle-thoughts-in-chat').on('change', function() { + extensionSettings.showThoughtsInChat = $(this).prop('checked'); + // console.log('[RPG Companion] Toggle showThoughtsInChat changed to:', extensionSettings.showThoughtsInChat); + saveSettings(); + updateChatThoughts(); + }); + + $('#rpg-toggle-html-prompt').on('change', function() { + extensionSettings.enableHtmlPrompt = $(this).prop('checked'); + // console.log('[RPG Companion] Toggle enableHtmlPrompt changed to:', extensionSettings.enableHtmlPrompt); + saveSettings(); + }); + + $('#rpg-toggle-plot-buttons').on('change', function() { + extensionSettings.enablePlotButtons = $(this).prop('checked'); + // console.log('[RPG Companion] Toggle enablePlotButtons changed to:', extensionSettings.enablePlotButtons); + saveSettings(); + togglePlotButtons(); + }); + + $('#rpg-toggle-animations').on('change', function() { + extensionSettings.enableAnimations = $(this).prop('checked'); + saveSettings(); + toggleAnimations(); + }); + + $('#rpg-manual-update').on('click', async function() { + if (!extensionSettings.enabled) { + // console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.'); + return; + } + await updateRPGData(); + }); + + $('#rpg-stat-bar-color-low').on('change', function() { + extensionSettings.statBarColorLow = String($(this).val()); + saveSettings(); + renderUserStats(); // Re-render with new colors + }); + + $('#rpg-stat-bar-color-high').on('change', function() { + extensionSettings.statBarColorHigh = String($(this).val()); + saveSettings(); + renderUserStats(); // Re-render with new colors + }); + + // Theme selection + $('#rpg-theme-select').on('change', function() { + extensionSettings.theme = String($(this).val()); + saveSettings(); + applyTheme(); + toggleCustomColors(); + updateSettingsPopupTheme(); // Update popup theme instantly + updateChatThoughts(); // Recreate thought bubbles with new theme + }); + + // Custom color pickers + $('#rpg-custom-bg').on('change', function() { + extensionSettings.customColors.bg = String($(this).val()); + saveSettings(); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(); // Update popup theme instantly + updateChatThoughts(); // Update thought bubbles + } + }); + + $('#rpg-custom-accent').on('change', function() { + extensionSettings.customColors.accent = String($(this).val()); + saveSettings(); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(); // Update popup theme instantly + updateChatThoughts(); // Update thought bubbles + } + }); + + $('#rpg-custom-text').on('change', function() { + extensionSettings.customColors.text = String($(this).val()); + saveSettings(); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(); // Update popup theme instantly + updateChatThoughts(); // Update thought bubbles + } + }); + + $('#rpg-custom-highlight').on('change', function() { + extensionSettings.customColors.highlight = String($(this).val()); + saveSettings(); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(); // Update popup theme instantly + updateChatThoughts(); // Update thought bubbles + } + }); + + // Initialize UI state (enable/disable is in Extensions tab) + $('#rpg-toggle-auto-update').prop('checked', extensionSettings.autoUpdate); + $('#rpg-position-select').val(extensionSettings.panelPosition); + $('#rpg-update-depth').val(extensionSettings.updateDepth); + $('#rpg-use-main-model').prop('checked', extensionSettings.useMainModel); + $('#rpg-toggle-user-stats').prop('checked', extensionSettings.showUserStats); + $('#rpg-toggle-info-box').prop('checked', extensionSettings.showInfoBox); + $('#rpg-toggle-thoughts').prop('checked', extensionSettings.showCharacterThoughts); + $('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat); + $('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt); + $('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons); + $('#rpg-toggle-animations').prop('checked', extensionSettings.enableAnimations); + $('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow); + $('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh); + $('#rpg-theme-select').val(extensionSettings.theme); + $('#rpg-custom-bg').val(extensionSettings.customColors.bg); + $('#rpg-custom-accent').val(extensionSettings.customColors.accent); + $('#rpg-custom-text').val(extensionSettings.customColors.text); + $('#rpg-custom-highlight').val(extensionSettings.customColors.highlight); + $('#rpg-generation-mode').val(extensionSettings.generationMode); + + updatePanelVisibility(); + updateSectionVisibility(); + updateGenerationModeUI(); + applyTheme(); + applyPanelPosition(); + toggleCustomColors(); + toggleAnimations(); + + // Render initial data if available + renderUserStats(); + renderInfoBox(); + renderThoughts(); + updateDiceDisplay(); + setupDiceRoller(); + setupSettingsPopup(); + addDiceQuickReply(); + setupPlotButtons(); +} + +/** + * Sets up the plot progression buttons inside the send form area. + */ +function setupPlotButtons() { + // Remove existing buttons if any + $('#rpg-plot-buttons').remove(); + + // Create wrapper if it doesn't exist (shared with other extensions like Spotify) + if ($('#extension-buttons-wrapper').length === 0) { + $('#send_form').prepend('
'); + } + + // Create the button container + const buttonHtml = ` + + `; + + // Insert into the wrapper + $('#extension-buttons-wrapper').append(buttonHtml); + + // Add event handlers for buttons + $('#rpg-plot-random').on('click', () => sendPlotProgression('random')); + $('#rpg-plot-natural').on('click', () => sendPlotProgression('natural')); + + // Show/hide based on setting + togglePlotButtons(); +}/** + * Toggles the visibility of plot buttons based on settings. + */ +function togglePlotButtons() { + if (extensionSettings.enablePlotButtons && extensionSettings.enabled) { + $('#rpg-plot-buttons').show(); + } else { + $('#rpg-plot-buttons').hide(); + } +} + +/** + * Sends a plot progression request and appends the result to the last message. + * @param {string} type - 'random' or 'natural' + */ +async function sendPlotProgression(type) { + if (!extensionSettings.enabled) { + // console.log('[RPG Companion] Extension is disabled'); + return; + } + + // Disable buttons to prevent multiple clicks + $('#rpg-plot-random, #rpg-plot-natural').prop('disabled', true).css('opacity', '0.5'); + + // Store original enabled state and temporarily disable extension + const wasEnabled = extensionSettings.enabled; + extensionSettings.enabled = false; + + try { + // console.log(`[RPG Companion] Sending ${type} plot progression request...`); + + // Build the prompt based on type + let prompt = ''; + if (type === 'random') { + prompt = 'Actually, the scene is getting stale. Introduce {{random::stakes::a plot twist::a new character::a cataclysm::a fourth-wall-breaking joke::a sudden atmospheric phenomenon::a plot hook::a running gag::an ecchi scenario::Death from Discworld::a new stake::a drama::a conflict::an angered entity::a god::a vision::a prophetic dream::Il Dottore from Genshin Impact::a new development::a civilian in need::an emotional bit::a threat::a villain::an important memory recollection::a marriage proposal::a date idea::an angry horde of villagers with pitchforks::a talking animal::an enemy::a cliffhanger::a short omniscient POV shift to a completely different character::a quest::an unexpected revelation::a scandal::an evil clone::death of an important character::harm to an important character::a romantic setup::a gossip::a messenger::a plot point from the past::a plot hole::a tragedy::a ghost::an otherworldly occurrence::a plot device::a curse::a magic device::a rival::an unexpected pregnancy::a brothel::a prostitute::a new location::a past lover::a completely random thing::a what-if scenario::a significant choice::war::love::a monster::lewd undertones::Professor Mari::a travelling troupe::a secret::a fortune-teller::something completely different::a killer::a murder mystery::a mystery::a skill check::a deus ex machina::three raccoons in a trench coat::a pet::a slave::an orphan::a psycho::tentacles::"there is only one bed" trope::accidental marriage::a fun twist::a boss battle::sexy corn::an eldritch horror::a character getting hungry, thirsty, or exhausted::horniness::a need for a bathroom break need::someone fainting::an assassination attempt::a meta narration of this all being an out of hand DND session::a dungeon::a friend in need::an old friend::a small time skip::a scene shift::Aurora Borealis, at this time of year, at this time of day, at this part of the country::a grand ball::a surprise party::zombies::foreshadowing::a Spanish Inquisition (nobody expects it)::a natural plot progression}} to make things more interesting! Be creative, but stay grounded in the setting.'; + } else { + prompt = 'Actually, the scene is getting stale. Progress it, to make things more interesting! Reintroduce an unresolved plot point from the past, or push the story further towards the current main goal. Be creative, but stay grounded in the setting.'; + } + + // Add HTML prompt if enabled + if (extensionSettings.enableHtmlPrompt) { + prompt += '\n\n' + `If appropriate, include inline HTML, CSS, and JS elements for creative, visual storytelling throughout your response: +- Use them liberally to depict any in-world content that can be visualized (screens, posters, books, signs, letters, logos, crests, seals, medallions, labels, etc.), with creative license for animations, 3D effects, pop-ups, dropdowns, websites, and so on. +- Style them thematically to match the theme (e.g., sleek for sci-fi, rustic for fantasy), ensuring text is visible. +- Embed all resources directly (e.g., inline SVGs) so nothing relies on external fonts or libraries. +- Place elements naturally in the narrative where characters would see or use them, with no limits on format or application. +- These HTML/CSS/JS elements must be rendered directly without enclosing them in code fences.`; + } + + // Inject the plot prompt as a quiet prompt + // This replaces the default continuation message + setExtensionPrompt('rpg-plot-progression', prompt, extension_prompt_types.IN_CHAT, 0, false, extension_prompt_roles.USER); + + // Trigger a continuation with the custom prompt + // The quiet_prompt parameter allows extension prompts to override the default continuation message + // Since extensionSettings.enabled = false, onGenerationStarted won't inject RPG prompts + await Generate('continue', { quiet_prompt: prompt }); + + // Clear the temporary prompt after generation + setExtensionPrompt('rpg-plot-progression', '', extension_prompt_types.IN_CHAT, 0, false); + + // console.log('[RPG Companion] Plot progression generation triggered'); + } catch (error) { + console.error('[RPG Companion] Error sending plot progression:', error); + // Clear the prompt in case of error + setExtensionPrompt('rpg-plot-progression', '', extension_prompt_types.IN_CHAT, 0, false); + } finally { + // Restore original enabled state + extensionSettings.enabled = wasEnabled; + + // Re-enable buttons + $('#rpg-plot-random, #rpg-plot-natural').prop('disabled', false).css('opacity', '1'); + } +} + +/** + * Sets up the dice roller functionality. + */ +function setupDiceRoller() { + // Click dice display to open popup + $('#rpg-dice-display').on('click', function() { + openDicePopup(); + }); + + // Close popup + $('#rpg-dice-popup-close, .rpg-dice-popup-overlay').on('click', function() { + closeDicePopup(); + }); + + // Roll dice button + $('#rpg-dice-roll-btn').on('click', async function() { + await rollDice(); + }); + + // Save roll button (closes popup) + $('#rpg-dice-save-btn').on('click', function() { + closeDicePopup(); + }); + + // Reset on Enter key + $('#rpg-dice-count, #rpg-dice-sides').on('keypress', function(e) { + if (e.which === 13) { + rollDice(); + } + }); + + // Clear dice roll button + $('#rpg-clear-dice').on('click', function(e) { + e.stopPropagation(); // Prevent opening the dice popup + clearDiceRoll(); + }); +} + +/** + * Clears the last dice roll. + */ +function clearDiceRoll() { + extensionSettings.lastDiceRoll = null; + saveSettings(); + updateDiceDisplay(); +} + +/** + * Opens the dice rolling popup. + */ +function openDicePopup() { + // Apply current theme to popup + const theme = extensionSettings.theme; + $('#rpg-dice-popup').attr('data-theme', theme); + + $('#rpg-dice-popup').fadeIn(200); + $('#rpg-dice-animation').hide(); + $('#rpg-dice-result').hide(); + $('#rpg-dice-roll-btn').show(); + + // Apply custom theme if selected + if (theme === 'custom') { + applyCustomThemeToPopup(); + } +} + +/** + * Applies custom theme colors to the dice popup. + */ +function applyCustomThemeToPopup() { + const $popup = $('#rpg-dice-popup'); + $popup.find('.rpg-dice-popup-content').css({ + '--rpg-bg': extensionSettings.customColors.bg, + '--rpg-accent': extensionSettings.customColors.accent, + '--rpg-text': extensionSettings.customColors.text, + '--rpg-highlight': extensionSettings.customColors.highlight + }); +} + +/** + * Closes the dice rolling popup. + */ +function closeDicePopup() { + $('#rpg-dice-popup').fadeOut(200); +} + +/** + * Rolls the dice and displays result. + */ +async function rollDice() { + const count = parseInt(String($('#rpg-dice-count').val())) || 1; + const sides = parseInt(String($('#rpg-dice-sides').val())) || 20; + + // Hide roll button and show animation + $('#rpg-dice-roll-btn').hide(); + $('#rpg-dice-animation').show(); + $('#rpg-dice-result').hide(); + + // Wait for animation (simulate rolling) + await new Promise(resolve => setTimeout(resolve, 1200)); + + // Execute /roll command + const rollCommand = `/roll ${count}d${sides}`; + const rollResult = await executeRollCommand(rollCommand); + + // Parse result + const total = rollResult.total || 0; + const rolls = rollResult.rolls || []; + + // Store result + extensionSettings.lastDiceRoll = { + formula: `${count}d${sides}`, + total: total, + rolls: rolls, + timestamp: Date.now() + }; + saveSettings(); + + // Hide animation and show result + $('#rpg-dice-animation').hide(); + $('#rpg-dice-result').show(); + $('#rpg-dice-result-value').text(total); + + if (rolls.length > 1) { + $('#rpg-dice-result-details').text(`Rolls: ${rolls.join(', ')}`); + } else { + $('#rpg-dice-result-details').text(''); + } + + // Update sidebar display + updateDiceDisplay(); +} + +/** + * Executes a /roll command and returns the result. + */ +async function executeRollCommand(command) { + try { + // Parse the dice notation (e.g., "2d20") + const match = command.match(/(\d+)d(\d+)/); + if (!match) { + return { total: 0, rolls: [] }; + } + + const count = parseInt(match[1]); + const sides = parseInt(match[2]); + const rolls = []; + let total = 0; + + for (let i = 0; i < count; i++) { + const roll = Math.floor(Math.random() * sides) + 1; + rolls.push(roll); + total += roll; + } + + return { total, rolls }; + } catch (error) { + console.error('[RPG Companion] Error rolling dice:', error); + return { total: 0, rolls: [] }; + } +} + +/** + * Updates the dice display in the sidebar. + */ +function updateDiceDisplay() { + const lastRoll = extensionSettings.lastDiceRoll; + if (lastRoll) { + $('#rpg-last-roll-text').text(`Last Roll (${lastRoll.formula}): ${lastRoll.total}`); + } else { + $('#rpg-last-roll-text').text('Last Roll: None'); + } +} + +/** + * Adds the Roll Dice quick reply button. + */ +function addDiceQuickReply() { + // Create quick reply button if Quick Replies exist + if (window.quickReplyApi) { + // Quick Reply API integration would go here + // For now, the dice display in the sidebar serves as the button + } +} + +/** + * Opens the settings popup. + */ +function openSettingsPopup() { + const theme = extensionSettings.theme || 'default'; + $('#rpg-settings-popup').attr('data-theme', theme); + + // Apply custom theme colors if custom theme is selected + if (theme === 'custom') { + applyCustomThemeToSettingsPopup(); + } + + $('#rpg-settings-popup').fadeIn(200); +} + +/** + * Closes the settings popup. + */ +function closeSettingsPopup() { + $('#rpg-settings-popup').fadeOut(200); +} + +/** + * Applies custom theme colors to the settings popup. + */ +function applyCustomThemeToSettingsPopup() { + const popup = $('#rpg-settings-popup .rpg-settings-popup-content'); + popup.css({ + '--rpg-bg': extensionSettings.customColors.bg, + '--rpg-accent': extensionSettings.customColors.accent, + '--rpg-text': extensionSettings.customColors.text, + '--rpg-highlight': extensionSettings.customColors.highlight + }); +} + +/** + * Updates the settings popup theme in real-time. + */ +function updateSettingsPopupTheme() { + const theme = extensionSettings.theme || 'default'; + const popup = $('#rpg-settings-popup .rpg-settings-popup-content'); + + $('#rpg-settings-popup').attr('data-theme', theme); + + // Apply custom theme colors if custom theme is selected + if (theme === 'custom') { + applyCustomThemeToSettingsPopup(); + } else { + // Clear custom CSS variables to let theme CSS take over + popup.css({ + '--rpg-bg': '', + '--rpg-accent': '', + '--rpg-text': '', + '--rpg-highlight': '' + }); + } +} + +/** + * Sets up the settings popup functionality. + */ +function setupSettingsPopup() { + // Open settings popup + $('#rpg-open-settings').on('click', function() { + openSettingsPopup(); + }); + + // Close settings popup + $('#rpg-close-settings, .rpg-settings-popup-overlay').on('click', function() { + closeSettingsPopup(); + }); + + // Clear cache button + $('#rpg-clear-cache').on('click', function() { + if (confirm('Are you sure you want to clear all cached RPG data for this chat? This will reset Stats, Info Box, and Mind Reading.')) { + // Clear the data + lastGeneratedData.userStats = null; + lastGeneratedData.infoBox = null; + lastGeneratedData.characterThoughts = null; + + // Clear committed tracker data (used for generation context) + committedTrackerData.userStats = null; + committedTrackerData.infoBox = null; + committedTrackerData.characterThoughts = null; + + // Clear all message swipe data + const chat = getContext().chat; + if (chat && chat.length > 0) { + for (let i = 0; i < chat.length; i++) { + const message = chat[i]; + if (message.extra && message.extra.rpg_companion_swipes) { + delete message.extra.rpg_companion_swipes; + // console.log('[RPG Companion] Cleared swipe data from message at index', i); + } + } + } + + // Clear the UI + if ($infoBoxContainer) { + $infoBoxContainer.empty(); + } + if ($thoughtsContainer) { + $thoughtsContainer.empty(); + } + + // Reset stats to defaults and re-render + extensionSettings.userStats = { + health: 100, + sustenance: 100, + energy: 100, + hygiene: 100, + arousal: 0, + mood: '😐', + conditions: 'None', + inventory: 'None' + }; + + // Reset classic stats (attributes) to defaults + extensionSettings.classicStats = { + str: 10, + dex: 10, + con: 10, + int: 10, + wis: 10, + cha: 10 + }; + + // Clear dice roll + extensionSettings.lastDiceRoll = null; + + // Save everything + saveChatData(); + saveSettings(); + + // Re-render user stats and dice display + renderUserStats(); + updateDiceDisplay(); + updateChatThoughts(); // Clear the thought bubble in chat + + // console.log('[RPG Companion] Chat cache cleared'); + alert('Chat cache cleared successfully!'); + } + }); +} + +/** + * Updates the visibility of the entire panel. + */ +function updatePanelVisibility() { + if (extensionSettings.enabled) { + $panelContainer.show(); + togglePlotButtons(); // Update plot button visibility + } else { + $panelContainer.hide(); + $('#rpg-plot-buttons').hide(); // Hide plot buttons when disabled + } +} + +/** + * Clears all extension prompts. + */ +function clearExtensionPrompts() { + setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false); + setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false); + setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false); + setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false); + // console.log('[RPG Companion] Cleared all extension prompts'); +} + +/** + * Updates the visibility of individual sections. + */ +function updateSectionVisibility() { + // Show/hide sections based on settings + $userStatsContainer.toggle(extensionSettings.showUserStats); + $infoBoxContainer.toggle(extensionSettings.showInfoBox); + $thoughtsContainer.toggle(extensionSettings.showCharacterThoughts); + + // Show/hide dividers intelligently + // Divider after User Stats: shown if User Stats is visible AND at least one section after it is visible + const showDividerAfterStats = extensionSettings.showUserStats && + (extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts); + $('#rpg-divider-stats').toggle(showDividerAfterStats); + + // Divider after Info Box: shown if Info Box is visible AND Mind Reading is visible + const showDividerAfterInfo = extensionSettings.showInfoBox && + extensionSettings.showCharacterThoughts; + $('#rpg-divider-info').toggle(showDividerAfterInfo); +} + +/** + * Applies the selected panel position. + */ +function applyPanelPosition() { + if (!$panelContainer) return; + + // Remove all position classes + $panelContainer.removeClass('rpg-position-left rpg-position-right rpg-position-top'); + + // Add the appropriate position class + $panelContainer.addClass(`rpg-position-${extensionSettings.panelPosition}`); +} + +/** + * Updates the model selector visibility. + */ +/** + * Updates the UI based on generation mode selection. + */ +function updateGenerationModeUI() { + if (extensionSettings.generationMode === 'together') { + // In "together" mode, manual update button is hidden + $('#rpg-manual-update').hide(); + } else { + // In "separate" mode, manual update button is visible + $('#rpg-manual-update').show(); + } +} + +/** + * Generates just the example portion - previous tracker data without tags or explanations. + * This will be appended to the last assistant message to show the format. + * Each section is wrapped in markdown code blocks. + */ +function generateTrackerExample() { + let example = ''; + + // Use COMMITTED data for generation context, not displayed data + // Wrap each tracker section in markdown code blocks + if (extensionSettings.showUserStats && committedTrackerData.userStats) { + example += '```\n' + committedTrackerData.userStats + '\n```\n\n'; + } + + if (extensionSettings.showInfoBox && committedTrackerData.infoBox) { + example += '```\n' + committedTrackerData.infoBox + '\n```\n\n'; + } + + if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) { + example += '```\n' + committedTrackerData.characterThoughts + '\n```'; + } + + return example.trim(); +} + +/** + * Generates the instruction portion - format specifications and guidelines. + * @param {boolean} includeHtmlPrompt - Whether to include the HTML prompt (true for main generation, false for separate tracker generation) + * @param {boolean} includeContinuation - Whether to include "After updating the trackers, continue..." instruction + */ +function generateTrackerInstructions(includeHtmlPrompt = true, includeContinuation = true) { + const userName = getContext().name1; + const classicStats = extensionSettings.classicStats; + let instructions = ''; + + // Check if any trackers are enabled + const hasAnyTrackers = extensionSettings.showUserStats || extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts; + + // Only add tracker instructions if at least one tracker is enabled + if (hasAnyTrackers) { + // Universal instruction header + instructions += `\nYou must start your response with an appropriate update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. Replace X with proper numbers and placeholders in [brackets] with in-world details ${userName} perceives about the current scene and the present characters. Consider the last trackers in the conversation (if they exist). Manage them accordingly; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences:\n`; + + // Add format specifications for each enabled tracker + if (extensionSettings.showUserStats) { + instructions += '```\n'; + instructions += `${userName}'s Stats\n`; + instructions += '---\n'; + instructions += '- Health: X%\n'; + instructions += '- Sustenance: X%\n'; + instructions += '- Energy: X%\n'; + instructions += '- Hygiene: X%\n'; + instructions += '- Arousal: X%\n'; + instructions += '[Mood Emoji]: [Conditions (up to three traits)]\n'; + instructions += 'Inventory: [Clothing/Armor, Inventory Items (list of important items/none)]\n'; + instructions += '```\n\n'; + } + + if (extensionSettings.showInfoBox) { + instructions += '```\n'; + instructions += 'Info Box\n'; + instructions += '---\n'; + instructions += 'đŸ—“ī¸: [Weekday, Month, Year]\n'; + instructions += '[Weather Emoji]: [Forecast]\n'; + instructions += 'đŸŒĄī¸: [Temperature in °C]\n'; + instructions += '🕒: [Time Start → Time End]\n'; + instructions += 'đŸ—ēī¸: [Location]\n'; + instructions += '```\n\n'; + } + + if (extensionSettings.showCharacterThoughts) { + instructions += '```\n'; + instructions += 'Present Characters\n'; + instructions += '---\n'; + instructions += `[Present Character's Emoji (do not include ${userName}; state "Unavailable" if no NPCs 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`; + instructions += '```\n\n'; + } + + // Only add continuation instruction if includeContinuation is true + if (includeContinuation) { + instructions += `After updating the trackers, continue directly from where the last message in the chat history left off. Ensure the trackers you provide naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, a character's emotional state coloring their responses, and so on.\n\n`; + } + + // Include attributes and dice roll only if there was a dice roll + if (extensionSettings.lastDiceRoll) { + const roll = extensionSettings.lastDiceRoll; + instructions += `${userName}'s attributes: STR ${classicStats.str}, DEX ${classicStats.dex}, CON ${classicStats.con}, INT ${classicStats.int}, WIS ${classicStats.wis}, CHA ${classicStats.cha}\n`; + instructions += `${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`; + } + } + + // Append HTML prompt if enabled AND includeHtmlPrompt is true + if (extensionSettings.enableHtmlPrompt && includeHtmlPrompt) { + // Add newlines only if we had tracker instructions + if (hasAnyTrackers) { + instructions += ``; + } else { + instructions += `\n`; + } + + instructions += `If appropriate, include inline HTML, CSS, and JS elements for creative, visual storytelling throughout your response: +- Use them liberally to depict any in-world content that can be visualized (screens, posters, books, signs, letters, logos, crests, seals, medallions, labels, etc.), with creative license for animations, 3D effects, pop-ups, dropdowns, websites, and so on. +- Style them thematically to match the theme (e.g., sleek for sci-fi, rustic for fantasy), ensuring text is visible. +- Embed all resources directly (e.g., inline SVGs) so nothing relies on external fonts or libraries. +- Place elements naturally in the narrative where characters would see or use them, with no limits on format or application. +- These HTML/CSS/JS elements must be rendered directly without enclosing them in code fences.`; + } + + return instructions; +} + +/** + * Generates a formatted contextual summary for SEPARATE mode injection. + * This creates a hybrid summary with clean formatting for main roleplay generation. + */ +function generateContextualSummary() { + // Use COMMITTED data for generation context, not displayed data + 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)); + + // Parse the data into readable format + 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}%, Sustenance ${stats.sustenance}%, Energy ${stats.energy}%, Hygiene ${stats.hygiene}%, Arousal ${stats.arousal}% | ${stats.mood} ${stats.conditions}\n`; + if (stats.inventory && stats.inventory !== 'None') { + summary += `Inventory: ${stats.inventory}\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}\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`; + } + + if (extensionSettings.showInfoBox && committedTrackerData.infoBox) { + // Parse info box data + const lines = committedTrackerData.infoBox.split('\n'); + let date = '', weather = '', temp = '', time = '', location = ''; + + // console.log('[RPG Companion] 🔍 Parsing Info Box lines:', lines); + + for (const line of lines) { + // console.log('[RPG Companion] 🔍 Processing line:', line); + // Use separate if statements (not else if) so each line is checked against all conditions + if (line.includes('đŸ—“ī¸:')) { + date = line.replace('đŸ—“ī¸:', '').trim(); + // console.log('[RPG Companion] 📅 Found date:', date); + } + if (line.includes('đŸŒĄī¸:')) { + temp = line.replace('đŸŒĄī¸:', '').trim(); + // console.log('[RPG Companion] đŸŒĄī¸ Found temp:', temp); + } + if (line.includes('🕒:')) { + time = line.replace('🕒:', '').trim(); + // console.log('[RPG Companion] 🕒 Found time:', time); + } + if (line.includes('đŸ—ēī¸:')) { + location = line.replace('đŸ—ēī¸:', '').trim(); + // console.log('[RPG Companion] đŸ—ēī¸ Found location:', location); + } + // Check for weather emojis - use a simpler approach + const weatherEmojis = ['đŸŒ¤ī¸', 'â˜€ī¸', '⛅', 'đŸŒĻī¸', 'đŸŒ§ī¸', 'â›ˆī¸', 'đŸŒŠī¸', 'đŸŒ¨ī¸', 'â„ī¸', 'đŸŒĢī¸']; + const startsWithWeatherEmoji = weatherEmojis.some(emoji => line.startsWith(emoji + ':')); + if (startsWithWeatherEmoji && !line.includes('đŸŒĄī¸') && !line.includes('đŸ—ēī¸')) { + // Extract weather description (remove emoji and colon) + weather = line.substring(line.indexOf(':') + 1).trim(); + // console.log('[RPG Companion] đŸŒ§ī¸ Found weather:', weather); + } + } + + // console.log('[RPG Companion] 🔍 Parsed values - date:', date, 'weather:', weather, 'temp:', temp, 'time:', time, 'location:', location); + + if (date || weather || temp || time || location) { + 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\n`; + } + } + + 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`; + } + } + } + } + + return summary.trim(); +} + +/** + * Generates the RPG tracking prompt text (for backward compatibility with separate mode). + */ +function generateRPGPromptText() { + // Use COMMITTED data for generation context, not displayed data + const userName = getContext().name1; + + let promptText = ''; + + promptText += `Here are the previous trackers in the roleplay that you should consider when responding:\n`; + promptText += `\n`; + + if (extensionSettings.showUserStats) { + if (committedTrackerData.userStats) { + promptText += `Last ${userName}'s Stats:\n${committedTrackerData.userStats}\n\n`; + } else { + promptText += `Last ${userName}'s Stats:\nNone - this is the first update.\n\n`; + } + } + + if (extensionSettings.showInfoBox) { + if (committedTrackerData.infoBox) { + promptText += `Last Info Box:\n${committedTrackerData.infoBox}\n\n`; + } else { + promptText += `Last Info Box:\nNone - this is the first update.\n\n`; + } + } + + if (extensionSettings.showCharacterThoughts) { + if (committedTrackerData.characterThoughts) { + promptText += `Last Present Characters:\n${committedTrackerData.characterThoughts}\n`; + } else { + promptText += `Last Present Characters:\nNone - this is the first update.\n`; + } + } + + promptText += `\n`; + + // Don't include HTML prompt or continuation instruction for separate tracker generation + promptText += generateTrackerInstructions(false, false); + + return promptText; +} + +/** + * Generates the full prompt for SEPARATE generation mode (with chat history). + */ +function generateSeparateUpdatePrompt() { + const depth = extensionSettings.updateDepth; + const userName = getContext().name1; + + const messages = []; + + // System message introducing the history section + let systemMessage = `You will be acting as an uncensored RPG Companion. Your goal is to provide, track, and manage details in the user's roleplay. You will be replying with information in a specified format only.\n\n`; + systemMessage += `You should maintain an objective tone.\n\n`; + systemMessage += `Here is the list of all the active characters:\n\nPlayed by the user: {{user}}.\nPlayed by the roleplayer assistant: {{groupNotMuted}}.\n\n\n`; + + systemMessage += `Here are the last few messages in the conversation history (between the user and the roleplayer assistant) you should reference when responding:\n`; + + messages.push({ + role: 'system', + content: systemMessage + }); + + // Add chat history as separate user/assistant messages + const recentMessages = chat.slice(-depth); + for (const message of recentMessages) { + messages.push({ + role: message.is_user ? 'user' : 'assistant', + content: message.mes + }); + } + + // Build the instruction message + let instructionMessage = `\n\n`; + instructionMessage += generateRPGPromptText().replace('start your response with', 'respond with'); + instructionMessage += `Provide ONLY the requested data in the exact formats specified above. Do not include any roleplay response, other text, or commentary.`; + + messages.push({ + role: 'user', + content: instructionMessage + }); + + return messages; +} + +/** + * Parses the model response to extract the different data sections. + */ +function parseResponse(responseText) { + const result = { + userStats: null, + infoBox: null, + characterThoughts: null + }; + + // Extract code blocks + const codeBlockRegex = /```([^`]+)```/g; + const matches = [...responseText.matchAll(codeBlockRegex)]; + + // console.log('[RPG Companion] Found'); + + for (const match of matches) { + const content = match[1].trim(); + + // console.log('[RPG Companion] Checking code block (first 200 chars):', content.substring(0, 200)); + + // Match Stats section + if (content.match(/Stats\s*\n\s*---/i)) { + result.userStats = content; + // console.log('[RPG Companion] ✓ Found Stats section'); + } + // Match Info Box section + else if (content.match(/Info Box\s*\n\s*---/i)) { + result.infoBox = content; + // console.log('[RPG Companion] ✓ Found Info Box section'); + } + // Match Present Characters section - flexible matching + else if (content.match(/Present Characters\s*\n\s*---/i) || content.includes(" | ")) { + result.characterThoughts = content; + // console.log('[RPG Companion] ✓ Found Present Characters section:', content); + } else { + // console.log('[RPG Companion] ✗ Code block did not match any section'); + } + } + + // console.log('[RPG Companion] Parse results:', { + // hasStats: !!result.userStats, + // hasInfoBox: !!result.infoBox, + // hasThoughts: !!result.characterThoughts + // }); + + return result; +} + +/** + * Main function to update RPG data by calling the AI model (SEPARATE MODE ONLY). + */ +async function updateRPGData() { + if (isGenerating) { + // console.log('[RPG Companion] Already generating, skipping...'); + return; + } + + if (!extensionSettings.enabled) { + return; + } + + if (extensionSettings.generationMode !== 'separate') { + // console.log('[RPG Companion] Not in separate mode, skipping manual update'); + return; + } + + try { + isGenerating = true; + + // Update button to show "Updating..." state + const $updateBtn = $('#rpg-manual-update'); + const originalHtml = $updateBtn.html(); + $updateBtn.html(' Updating...').prop('disabled', true); + + const prompt = generateSeparateUpdatePrompt(); + + // Generate using raw prompt (uses current preset, no chat history) + const response = await generateRaw({ + prompt: prompt, + quietToLoud: false + }); + + if (response) { + // console.log('[RPG Companion] Raw AI response:', response); + const parsedData = parseResponse(response); + // console.log('[RPG Companion] Parsed data:', parsedData); + // console.log('[RPG Companion] parsedData.userStats:', parsedData.userStats ? parsedData.userStats.substring(0, 100) + '...' : 'null'); + + // DON'T update lastGeneratedData here - it should only reflect the data + // from the assistant message the user replied to, not auto-generated updates + // This ensures swipes/regenerations use consistent source data + + // Store RPG data for the last assistant message (separate mode) + const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null; + // console.log('[RPG Companion] Last message is_user:', lastMessage ? lastMessage.is_user : 'no message'); + if (lastMessage && !lastMessage.is_user) { + if (!lastMessage.extra) { + lastMessage.extra = {}; + } + if (!lastMessage.extra.rpg_companion_swipes) { + lastMessage.extra.rpg_companion_swipes = {}; + } + + const currentSwipeId = lastMessage.swipe_id || 0; + lastMessage.extra.rpg_companion_swipes[currentSwipeId] = { + userStats: parsedData.userStats, + infoBox: parsedData.infoBox, + characterThoughts: parsedData.characterThoughts + }; + + // console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId); + + // Update lastGeneratedData for display AND future commit + if (parsedData.userStats) { + lastGeneratedData.userStats = parsedData.userStats; + parseUserStats(parsedData.userStats); + } + if (parsedData.infoBox) { + lastGeneratedData.infoBox = parsedData.infoBox; + } + if (parsedData.characterThoughts) { + lastGeneratedData.characterThoughts = parsedData.characterThoughts; + } + // console.log('[RPG Companion] 💾 SEPARATE MODE: Updated lastGeneratedData:', { + // userStats: lastGeneratedData.userStats ? 'exists' : 'null', + // infoBox: lastGeneratedData.infoBox ? 'exists' : 'null', + // characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null' + // }); + + // If there's no committed data yet (first time), commit immediately + if (!committedTrackerData.userStats && !committedTrackerData.infoBox && !committedTrackerData.characterThoughts) { + committedTrackerData.userStats = parsedData.userStats; + committedTrackerData.infoBox = parsedData.infoBox; + committedTrackerData.characterThoughts = parsedData.characterThoughts; + // console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data'); + } + + // Render the updated data + renderUserStats(); + renderInfoBox(); + renderThoughts(); + } else { + // No assistant message to attach to - just update display + if (parsedData.userStats) { + parseUserStats(parsedData.userStats); + } + renderUserStats(); + renderInfoBox(); + renderThoughts(); + } + + // Save to chat metadata + saveChatData(); + } + + } catch (error) { + console.error('[RPG Companion] Error updating RPG data:', error); + } finally { + isGenerating = false; + + // Restore button to original state + const $updateBtn = $('#rpg-manual-update'); + $updateBtn.html(' Refresh RPG Info').prop('disabled', false); + + // Reset the flag after tracker generation completes + // This ensures the flag persists through both main generation AND tracker generation + // console.log('[RPG Companion] 🔄 Tracker generation complete - resetting lastActionWasSwipe to false'); + lastActionWasSwipe = false; + } +} + +/** + * Parses user stats from the text and updates the settings. + */ +function parseUserStats(statsText) { + try { + // Extract percentages and mood/conditions + const healthMatch = statsText.match(/Health:\s*(\d+)%/); + const sustenanceMatch = statsText.match(/Sustenance:\s*(\d+)%/); + const energyMatch = statsText.match(/Energy:\s*(\d+)%/); + const hygieneMatch = statsText.match(/Hygiene:\s*(\d+)%/); + const arousalMatch = statsText.match(/Arousal:\s*(\d+)%/); + + // Match new format: [Emoji]: [Conditions] + // Look for emoji followed by colon, then conditions + const moodMatch = statsText.match(/(\p{Emoji}):\s*(.+)/u); + + // Extract inventory + const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i); + + if (healthMatch) extensionSettings.userStats.health = parseInt(healthMatch[1]); + if (sustenanceMatch) extensionSettings.userStats.sustenance = parseInt(sustenanceMatch[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 + } + if (inventoryMatch) { + extensionSettings.userStats.inventory = inventoryMatch[1].trim(); + } + + saveSettings(); + } catch (error) { + console.error('[RPG Companion] Error parsing user stats:', error); + } +} + +/** + * Renders the user stats with fancy progress bars. + */ +/** + * Renders the user stats with fancy progress bars. + */ +function renderUserStats() { + if (!extensionSettings.showUserStats || !$userStatsContainer) { + return; + } + + const stats = extensionSettings.userStats; + const userName = getContext().name1; + + // Get user portrait + const userPortrait = getThumbnailUrl('persona', user_avatar); + + // Create gradient from low to high color + const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`; + + const html = ` +
+
+ ${userName} +
${userName}
+
+
+
+ ${stats.inventory || 'None'} +
+
+
+ +
+
+
+
+ Health: +
+
+
+ ${stats.health}% +
+ +
+ Sustenance: +
+
+
+ ${stats.sustenance}% +
+ +
+ Energy: +
+
+
+ ${stats.energy}% +
+ +
+ Hygiene: +
+
+
+ ${stats.hygiene}% +
+ +
+ Arousal: +
+
+
+ ${stats.arousal}% +
+
+ +
+
${stats.mood}
+
${stats.conditions}
+
+
+ +
+
+
+
+ STR +
+ + ${extensionSettings.classicStats.str} + +
+
+
+ DEX +
+ + ${extensionSettings.classicStats.dex} + +
+
+
+ CON +
+ + ${extensionSettings.classicStats.con} + +
+
+
+ INT +
+ + ${extensionSettings.classicStats.int} + +
+
+
+ WIS +
+ + ${extensionSettings.classicStats.wis} + +
+
+
+ CHA +
+ + ${extensionSettings.classicStats.cha} + +
+
+
+
+
+
+ `; + + $userStatsContainer.html(html); + + // Add event listeners for classic stat buttons + $('.rpg-stat-increase').on('click', function() { + const stat = $(this).data('stat'); + if (extensionSettings.classicStats[stat] < 20) { + extensionSettings.classicStats[stat]++; + saveSettings(); + saveChatData(); + // Update only the specific stat value, not the entire stats panel + $(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]); + } + }); + + $('.rpg-stat-decrease').on('click', function() { + const stat = $(this).data('stat'); + if (extensionSettings.classicStats[stat] > 1) { + extensionSettings.classicStats[stat]--; + saveSettings(); + saveChatData(); + // Update only the specific stat value, not the entire stats panel + $(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]); + } + }); + + // Add event listeners for editable stat values + $('.rpg-editable-stat').on('blur', function() { + const field = $(this).data('field'); + const textValue = $(this).text().replace('%', '').trim(); + let value = parseInt(textValue); + + // Validate and clamp value between 0 and 100 + if (isNaN(value)) { + value = 0; + } + value = Math.max(0, Math.min(100, value)); + + // Update the setting + extensionSettings.userStats[field] = value; + + // Also update lastGeneratedData to keep it in sync + if (!lastGeneratedData.userStats) { + lastGeneratedData.userStats = ''; + } + // Regenerate the userStats text with updated value + const statsText = `Health: ${extensionSettings.userStats.health}%\nSustenance: ${extensionSettings.userStats.sustenance}%\nEnergy: ${extensionSettings.userStats.energy}%\nHygiene: ${extensionSettings.userStats.hygiene}%\nArousal: ${extensionSettings.userStats.arousal}%\n${extensionSettings.userStats.mood}: ${extensionSettings.userStats.conditions}\nInventory: ${extensionSettings.userStats.inventory}`; + lastGeneratedData.userStats = statsText; + + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + + // Re-render to update the bar + renderUserStats(); + }); + + // Add event listener for inventory editing + $('.rpg-inventory-items.rpg-editable').on('blur', function() { + const value = $(this).text().trim(); + extensionSettings.userStats.inventory = value || 'None'; + + // Update lastGeneratedData + const statsText = `Health: ${extensionSettings.userStats.health}%\nSustenance: ${extensionSettings.userStats.sustenance}%\nEnergy: ${extensionSettings.userStats.energy}%\nHygiene: ${extensionSettings.userStats.hygiene}%\nArousal: ${extensionSettings.userStats.arousal}%\n${extensionSettings.userStats.mood}: ${extensionSettings.userStats.conditions}\nInventory: ${extensionSettings.userStats.inventory}`; + lastGeneratedData.userStats = statsText; + + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + }); + + // Add event listeners for mood/conditions editing + $('.rpg-mood-emoji.rpg-editable').on('blur', function() { + const value = $(this).text().trim(); + extensionSettings.userStats.mood = value || '😐'; + + // Update lastGeneratedData + const statsText = `Health: ${extensionSettings.userStats.health}%\nSustenance: ${extensionSettings.userStats.sustenance}%\nEnergy: ${extensionSettings.userStats.energy}%\nHygiene: ${extensionSettings.userStats.hygiene}%\nArousal: ${extensionSettings.userStats.arousal}%\n${extensionSettings.userStats.mood}: ${extensionSettings.userStats.conditions}\nInventory: ${extensionSettings.userStats.inventory}`; + lastGeneratedData.userStats = statsText; + + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + }); + + $('.rpg-mood-conditions.rpg-editable').on('blur', function() { + const value = $(this).text().trim(); + extensionSettings.userStats.conditions = value || 'None'; + + // Update lastGeneratedData + const statsText = `Health: ${extensionSettings.userStats.health}%\nSustenance: ${extensionSettings.userStats.sustenance}%\nEnergy: ${extensionSettings.userStats.energy}%\nHygiene: ${extensionSettings.userStats.hygiene}%\nArousal: ${extensionSettings.userStats.arousal}%\n${extensionSettings.userStats.mood}: ${extensionSettings.userStats.conditions}\nInventory: ${extensionSettings.userStats.inventory}`; + lastGeneratedData.userStats = statsText; + + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + }); +} + +/** + * Renders the info box as a visual dashboard. + */ +function renderInfoBox() { + if (!extensionSettings.showInfoBox || !$infoBoxContainer) { + return; + } + + // Add updating class for animation + if (extensionSettings.enableAnimations) { + $infoBoxContainer.addClass('rpg-content-updating'); + } + + // If no data yet, show placeholder + if (!lastGeneratedData.infoBox) { + const placeholderHtml = ` +
+
+
No data yet
+
Click "Refresh RPG Info" to generate
+
+
+ `; + $infoBoxContainer.html(placeholderHtml); + if (extensionSettings.enableAnimations) { + setTimeout(() => $infoBoxContainer.removeClass('rpg-content-updating'), 500); + } + return; + } + + // console.log('[RPG Companion] renderInfoBox called with data:', lastGeneratedData.infoBox); + + // Parse the info box data + const lines = lastGeneratedData.infoBox.split('\n'); + // console.log('[RPG Companion] Info Box split into lines:', lines); + const data = { + date: '', + weekday: '', + month: '', + year: '', + weatherEmoji: '', + weatherForecast: '', + temperature: '', + tempValue: 0, + timeStart: '', + timeEnd: '', + location: '', + characters: [] + }; + + for (const line of lines) { + // console.log('[RPG Companion] Processing line:', line); + + if (line.includes('đŸ—“ī¸:')) { + // console.log('[RPG Companion] → Matched DATE'); + const dateStr = line.replace('đŸ—“ī¸:', '').trim(); + // Parse format: "Weekday, Month Day, Year" or "Weekday, Month, Year" + const dateParts = dateStr.split(',').map(p => p.trim()); + data.weekday = dateParts[0] || ''; + data.month = dateParts[1] || ''; + data.year = dateParts[2] || ''; + data.date = dateStr; + } else if (line.includes('đŸŒĄī¸:')) { + // console.log('[RPG Companion] → Matched TEMPERATURE'); + const tempStr = line.replace('đŸŒĄī¸:', '').trim(); + data.temperature = tempStr; + // Extract numeric value + const tempMatch = tempStr.match(/(-?\d+)/); + if (tempMatch) { + data.tempValue = parseInt(tempMatch[1]); + } + } else if (line.includes('🕒:')) { + // console.log('[RPG Companion] → Matched TIME'); + const timeStr = line.replace('🕒:', '').trim(); + data.time = timeStr; + // Parse "HH:MM → HH:MM" format + const timeParts = timeStr.split('→').map(t => t.trim()); + data.timeStart = timeParts[0] || ''; + data.timeEnd = timeParts[1] || ''; + } else if (line.includes('đŸ—ēī¸:')) { + // console.log('[RPG Companion] → Matched LOCATION'); + data.location = line.replace('đŸ—ēī¸:', '').trim(); + } else { + // Check if it's a weather line + // Since \p{Emoji} doesn't work reliably, use a simpler approach + const hasColon = line.includes(':'); + const notInfoBox = !line.includes('Info Box'); + const notDivider = !line.includes('---'); + const notCodeFence = !line.trim().startsWith('```'); + + // console.log('[RPG Companion] → Checking weather conditions:', { + // line: line, + // hasColon: hasColon, + // notInfoBox: notInfoBox, + // notDivider: notDivider + // }); + + if (hasColon && notInfoBox && notDivider && notCodeFence && line.trim().length > 0) { + // Match format: [Weather Emoji]: [Forecast] + // Capture everything before colon as emoji, everything after as forecast + // console.log('[RPG Companion] → Testing WEATHER match for:', line); + const weatherMatch = line.match(/^\s*([^:]+):\s*(.+)$/); + if (weatherMatch) { + const potentialEmoji = weatherMatch[1].trim(); + const forecast = weatherMatch[2].trim(); + + // If the first part is short (likely emoji), treat as weather + if (potentialEmoji.length <= 5) { + data.weatherEmoji = potentialEmoji; + data.weatherForecast = forecast; + // console.log('[RPG Companion] ✓ Weather parsed:', data.weatherEmoji, data.weatherForecast); + } else { + // console.log('[RPG Companion] ✗ First part too long for emoji:', potentialEmoji); + } + } else { + // console.log('[RPG Companion] ✗ Weather regex did not match'); + } + } else { + // console.log('[RPG Companion] → No match for this line'); + } + } + } + + // console.log('[RPG Companion] Parsed Info Box data:', { + // date: data.date, + // weatherEmoji: data.weatherEmoji, + // weatherForecast: data.weatherForecast, + // temperature: data.temperature, + // timeStart: data.timeStart, + // location: data.location + // }); + + // Build visual dashboard HTML + // Row 1: Date, Weather, Temperature, Time widgets + let html = '
'; + + // Calendar widget - actual visual calendar + if (data.date) { + const monthShort = data.month.substring(0, 3).toUpperCase(); + const weekdayShort = data.weekday.substring(0, 3).toUpperCase(); + html += ` +
+
${monthShort}
+
${weekdayShort}
+
${data.year}
+
+ `; + } + + // Weather widget - emoji with forecast text + if (data.weatherEmoji) { + html += ` +
+
${data.weatherEmoji}
+ ${data.weatherForecast ? `
${data.weatherForecast}
` : ''} +
+ `; + } + + // Temperature widget - thermometer visual + if (data.temperature) { + const tempPercent = Math.min(100, Math.max(0, ((data.tempValue + 20) / 60) * 100)); + const tempColor = data.tempValue < 10 ? '#4a90e2' : data.tempValue < 25 ? '#67c23a' : '#e94560'; + html += ` +
+
+
+
+
+
+
+
${data.temperature}
+
+ `; + } + + // Time widget - clock visual + if (data.timeStart) { + // Parse time for clock hands + const timeMatch = data.timeStart.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 += ` +
+
+
+
+
+
+
+
+
${data.timeStart}
+
+ `; + } + + html += '
'; + + // Row 2: Location widget (full width) + if (data.location) { + html += ` +
+
+
+
📍
+
+
${data.location}
+
+
+ `; + } + + $infoBoxContainer.html(html); + + // Add event handlers for editable Info Box fields + $infoBoxContainer.find('.rpg-editable').on('blur', function() { + const field = $(this).data('field'); + const value = $(this).text().trim(); + updateInfoBoxField(field, value); + }); + + // Remove updating class after animation + if (extensionSettings.enableAnimations) { + setTimeout(() => $infoBoxContainer.removeClass('rpg-content-updating'), 500); + } +} + +/** + * Renders character thoughts (Present Characters). + */ +function renderThoughts() { + if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) { + return; + } + + // Add updating class for animation + if (extensionSettings.enableAnimations) { + $thoughtsContainer.addClass('rpg-content-updating'); + } + + // If no data yet, show placeholder + if (!lastGeneratedData.characterThoughts) { + const placeholderHtml = ` +
+
+
No characters detected
+
Click "Refresh RPG Info" to generate
+
+
+ `; + $thoughtsContainer.html(placeholderHtml); + if (extensionSettings.enableAnimations) { + setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600); + } + return; + } + + const lines = lastGeneratedData.characterThoughts.split('\n'); + const presentCharacters = []; + + // console.log('[RPG Companion] Raw Present Characters:', lastGeneratedData.characterThoughts); + // console.log('[RPG Companion] Split into lines:', lines); + + // Parse format: [Emoji]: [Name, Status, Demeanor] | [Relationship] | [Thoughts] + for (const line of lines) { + // Skip empty lines, headers, dividers, and code fences + if (line.trim() && + !line.includes('Present Characters') && + !line.includes('---') && + !line.trim().startsWith('```')) { + + // Match the new format with pipes + const parts = line.split('|').map(p => p.trim()); + + if (parts.length >= 2) { + // 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(); + const relationship = parts[1].trim(); // Enemy/Neutral/Friend/Lover + const thoughts = parts[2] ? parts[2].trim() : ''; + + // Parse name from info (first part before comma) + const infoParts = info.split(',').map(p => p.trim()); + const name = infoParts[0] || ''; + const traits = infoParts.slice(1).join(', '); + + if (name && name.toLowerCase() !== 'unavailable') { + presentCharacters.push({ emoji, name, traits, relationship, thoughts }); + // console.log('[RPG Companion] Parsed character:', { name, relationship }); + } + } + } + } + } + + // Relationship status to emoji mapping + const relationshipEmojis = { + 'Enemy': 'âš”ī¸', + 'Neutral': 'âš–ī¸', + 'Friend': '⭐', + 'Lover': 'â¤ī¸' + }; + + // Build HTML + let html = ''; + + // console.log('[RPG Companion] Total characters parsed:', presentCharacters.length); + // console.log('[RPG Companion] Characters array:', presentCharacters); + + if (presentCharacters.length === 0) { + html += '
Unavailable
'; + } else { + html += '
'; + for (const char of presentCharacters) { + // Find character portrait + let characterPortrait = 'img/user-default.png'; + + // console.log('[RPG Companion] Looking for avatar for:', char.name); + + // For group chats, search through group members first + if (selected_group) { + const groupMembers = getGroupMembers(selected_group); + const matchingMember = groupMembers.find(member => + member && member.name && member.name.toLowerCase() === char.name.toLowerCase() + ); + + if (matchingMember && matchingMember.avatar && matchingMember.avatar !== 'none') { + characterPortrait = getThumbnailUrl('avatar', matchingMember.avatar); + } + } + + // For regular chats or if not found in group, search all characters + if (characterPortrait === 'img/user-default.png' && characters && characters.length > 0) { + const matchingCharacter = characters.find(c => + c && c.name && c.name.toLowerCase() === char.name.toLowerCase() + ); + + if (matchingCharacter && matchingCharacter.avatar && matchingCharacter.avatar !== 'none') { + characterPortrait = getThumbnailUrl('avatar', matchingCharacter.avatar); + } + } + + // If this is the current character in a 1-on-1 chat, use their portrait + if (this_chid !== undefined && characters[this_chid] && + characters[this_chid].name && characters[this_chid].name.toLowerCase() === char.name.toLowerCase()) { + characterPortrait = getThumbnailUrl('avatar', characters[this_chid].avatar); + } + + // Get relationship emoji + const relationshipEmoji = relationshipEmojis[char.relationship] || 'âš–ī¸'; + + html += ` +
+
+ ${char.name} +
${relationshipEmoji}
+
+
+
+ ${char.emoji} + ${char.name} +
+
${char.traits}
+
+
+ `; + } + html += '
'; + } + + $thoughtsContainer.html(html); + + // Add event handlers for editable character fields + $thoughtsContainer.find('.rpg-editable').on('blur', function() { + const character = $(this).data('character'); + const field = $(this).data('field'); + const value = $(this).text().trim(); + updateCharacterField(character, field, value); + }); + + // Remove updating class after animation + if (extensionSettings.enableAnimations) { + setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600); + } + + // Update chat overlay if enabled + if (extensionSettings.showThoughtsInChat) { + updateChatThoughts(); + } +} + +/** + * Updates a specific field in the Info Box data and re-renders. + */ +function updateInfoBoxField(field, value) { + if (!lastGeneratedData.infoBox) return; + + // Reconstruct the Info Box text with updated field + const lines = lastGeneratedData.infoBox.split('\n'); + const updatedLines = lines.map(line => { + if (field === 'month' && line.includes('đŸ—“ī¸:')) { + const parts = line.split(','); + if (parts.length >= 2) { + // parts[0] = "đŸ—“ī¸: Weekday", parts[1] = " Month", parts[2] = " Year" + parts[1] = ' ' + value; + return parts.join(','); + } + } else if (field === 'weekday' && line.includes('đŸ—“ī¸:')) { + const parts = line.split(','); + if (parts.length >= 1) { + // Keep the emoji, just update the weekday + parts[0] = 'đŸ—“ī¸: ' + value; + return parts.join(','); + } + } else if (field === 'year' && line.includes('đŸ—“ī¸:')) { + const parts = line.split(','); + if (parts.length >= 3) { + parts[2] = ' ' + value; + return parts.join(','); + } + } else if (field === 'weatherEmoji' && line.match(/^[^:]+:\s*.+$/) && !line.includes('đŸ—“ī¸') && !line.includes('đŸŒĄī¸') && !line.includes('🕒') && !line.includes('đŸ—ēī¸') && !line.includes('Info Box') && !line.includes('---')) { + // This is the weather line + const parts = line.split(':'); + if (parts.length >= 2) { + return `${value}: ${parts.slice(1).join(':').trim()}`; + } + } else if (field === 'weatherForecast' && line.match(/^[^:]+:\s*.+$/) && !line.includes('đŸ—“ī¸') && !line.includes('đŸŒĄī¸') && !line.includes('🕒') && !line.includes('đŸ—ēī¸') && !line.includes('Info Box') && !line.includes('---')) { + // This is the weather line + const parts = line.split(':'); + if (parts.length >= 2) { + return `${parts[0].trim()}: ${value}`; + } + } else if (field === 'temperature' && line.includes('đŸŒĄī¸:')) { + return `đŸŒĄī¸: ${value}`; + } else if (field === 'timeStart' && line.includes('🕒:')) { + // Update time format: "HH:MM → HH:MM" + const currentTime = line.replace('🕒:', '').trim(); + const timeParts = currentTime.split('→').map(t => t.trim()); + const timeEnd = timeParts[1] || timeParts[0]; // Keep end time or use start as both + return `🕒: ${value} → ${timeEnd}`; + } else if (field === 'location' && line.includes('đŸ—ēī¸:')) { + return `đŸ—ēī¸: ${value}`; + } + return line; + }); + + lastGeneratedData.infoBox = updatedLines.join('\n'); + + // Update the message's swipe data + const chat = getContext().chat; + if (chat && chat.length > 0) { + for (let i = chat.length - 1; i >= 0; i--) { + const message = chat[i]; + if (!message.is_user) { + 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].infoBox = updatedLines.join('\n'); + // console.log('[RPG Companion] Updated infoBox in message swipe data'); + } + } + break; + } + } + } + + saveChatData(); + renderInfoBox(); +} + +/** + * Updates a specific character field in Present Characters data and re-renders. + */ +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); + + if (!lastGeneratedData.characterThoughts) return; const lines = lastGeneratedData.characterThoughts.split('\n'); + const updatedLines = lines.map(line => { + // Case-insensitive character name matching + if (line.toLowerCase().includes(characterName.toLowerCase())) { + const parts = line.split('|').map(p => p.trim()); + if (parts.length >= 2) { + const firstPart = parts[0]; + const emojiMatch = firstPart.match(/^(.+?):\s*(.+)$/); + + if (emojiMatch) { + let emoji = emojiMatch[1].trim(); + let info = emojiMatch[2].trim(); + let relationship = parts[1]; + let thoughts = parts[2] || ''; + + 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}`; + } + } + } + return line; + }); + + lastGeneratedData.characterThoughts = updatedLines.join('\n'); + // console.log('[RPG Companion] 💾 Updated lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts); + + // 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'); + } + } + break; + } + } + } + + 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); + } else { + // For other fields, recreate immediately + updateChatThoughts(); + } +} + +/** + * Updates or removes thought overlays in the chat. + */ +function updateChatThoughts() { + // console.log('[RPG Companion] ======== updateChatThoughts called ========'); + // console.log('[RPG Companion] Extension enabled:', extensionSettings.enabled); + // console.log('[RPG Companion] showThoughtsInChat setting:', extensionSettings.showThoughtsInChat); + // console.log('[RPG Companion] Toggle element checked:', $('#rpg-toggle-thoughts-in-chat').prop('checked')); + // console.log('[RPG Companion] lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts); + + // Remove existing thought panel and icon + $('#rpg-thought-panel').remove(); + $('#rpg-thought-icon').remove(); + $('#chat').off('scroll.thoughtPanel'); + $(window).off('resize.thoughtPanel'); + $(document).off('click.thoughtPanel'); + + // If extension is disabled, thoughts in chat are disabled, or no thoughts, just return + if (!extensionSettings.enabled || !extensionSettings.showThoughtsInChat || !lastGeneratedData.characterThoughts) { + // console.log('[RPG Companion] Thoughts in chat disabled or no data'); + return; + } + + // Parse the Present Characters data to get thoughts + const lines = lastGeneratedData.characterThoughts.split('\n'); + const thoughtsArray = []; // Array of {name, emoji, thought} + + // 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('```')) { + + const parts = line.split('|').map(p => p.trim()); + // console.log('[RPG Companion] Line parts:', parts); + + if (parts.length >= 3) { + const firstPart = parts[0].trim(); + const emojiMatch = firstPart.match(/^(.+?):\s*(.+)$/); + + if (emojiMatch) { + const emoji = emojiMatch[1].trim(); + const info = emojiMatch[2].trim(); + const thoughts = parts[2] ? parts[2].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()); + } + } + } + } + } + + // If no thoughts parsed, return + if (thoughtsArray.length === 0) { + // console.log('[RPG Companion] No thoughts parsed, returning'); + return; + } + + // console.log('[RPG Companion] Total thoughts:', thoughtsArray.length); + // console.log('[RPG Companion] Thoughts array:', thoughtsArray); + + // Find the last message to position near + const $messages = $('#chat .mes'); + let $targetMessage = null; + + // Find the most recent non-user message + for (let i = $messages.length - 1; i >= 0; i--) { + const $message = $messages.eq(i); + if ($message.attr('is_user') !== 'true') { + $targetMessage = $message; + break; + } + } + + if (!$targetMessage) { + // console.log('[RPG Companion] No target message found'); + return; + } + + // Create the thought panel with all thoughts + createThoughtPanel($targetMessage, thoughtsArray); +} + +/** + * Creates or updates the floating thought panel positioned next to the character's avatar + */ +function createThoughtPanel($message, thoughtsArray) { + // Remove existing thought panel + $('#rpg-thought-panel').remove(); + $('#rpg-thought-icon').remove(); + + // Get the avatar position from the message + const $avatar = $message.find('.avatar img'); + if (!$avatar.length) { + // console.log('[RPG Companion] No avatar found in message'); + return; + } + + const avatarRect = $avatar[0].getBoundingClientRect(); + const panelPosition = extensionSettings.panelPosition; + const theme = extensionSettings.theme; + + // Build thought bubbles HTML + let thoughtsHtml = ''; + thoughtsArray.forEach((thought, index) => { + thoughtsHtml += ` +
+
+ ${thought.emoji} +
+
+ ${thought.thought} +
+
+ `; + // Add divider between thoughts (except for last one) + if (index < thoughtsArray.length - 1) { + thoughtsHtml += '
'; + } + }); + + // Create the floating thought panel with theme + const $thoughtPanel = $(` +
+ +
+
+
+
+
+
+ ${thoughtsHtml} +
+
+ `); + + // Create the collapsed thought icon + const $thoughtIcon = $(` +
+ 💭 +
+ `); + + // Apply custom theme colors if custom theme + if (theme === 'custom') { + const customStyles = { + '--rpg-bg': extensionSettings.customColors.bg, + '--rpg-accent': extensionSettings.customColors.accent, + '--rpg-text': extensionSettings.customColors.text, + '--rpg-highlight': extensionSettings.customColors.highlight + }; + $thoughtPanel.css(customStyles); + $thoughtIcon.css(customStyles); + } + + // Force a consistent width for the bubble to ensure proper positioning + $thoughtPanel.css('width', '350px'); + + // Append to body so it's not clipped by chat container + $('body').append($thoughtPanel); + $('body').append($thoughtIcon); + + // Position the panel next to the avatar + const panelWidth = 350; + const panelMargin = 20; + + let top = avatarRect.top + (avatarRect.height / 2); + let left; + let right; + let useRightPosition = false; + let iconTop = avatarRect.top; + let iconLeft; + + if (panelPosition === 'left') { + // Main panel is on left, so thought bubble goes to RIGHT side + // Mirror the left side positioning: bubble should be same distance from avatar + // but on the opposite side, extending to the right + const chatContainer = $('#chat')[0]; + const chatRect = chatContainer ? chatContainer.getBoundingClientRect() : { right: window.innerWidth }; + + // Position bubble starting from chat edge, extending right + left = chatRect.right + panelMargin; // Start at chat's right edge + margin + useRightPosition = false; // Use left positioning so it extends right + iconLeft = chatRect.right + 10; // Icon just at the chat edge + $thoughtPanel.addClass('rpg-thought-panel-right'); + $thoughtIcon.addClass('rpg-thought-icon-right'); + + // Position circles to flow from left (toward chat/avatar) to right (toward panel) + $thoughtPanel.find('.rpg-thought-circles').css({ + top: 'calc(50% - 50px)', + left: '-25px', + bottom: 'auto', + right: 'auto' + }); + // Mirror the circle flow for right side (left-to-right) + $thoughtPanel.find('.rpg-thought-circles').css('align-items', 'flex-start'); + $thoughtPanel.find('.rpg-circle-1').css({ 'align-self': 'flex-start', 'margin-right': '0', 'margin-left': '0' }); + $thoughtPanel.find('.rpg-circle-2').css({ 'align-self': 'flex-start', 'margin-right': '0', 'margin-left': '4px' }); + $thoughtPanel.find('.rpg-circle-3').css({ 'align-self': 'flex-start', 'margin-right': '0', 'margin-left': '8px' }); + } else { + // Main panel is on right, so thought bubble goes on left (near avatar) + left = avatarRect.left - panelWidth - panelMargin; + iconLeft = avatarRect.left - 40; + $thoughtPanel.addClass('rpg-thought-panel-left'); + $thoughtIcon.addClass('rpg-thought-icon-left'); + + // Position circles to flow from avatar (left) to bubble (more left) + // Circles should flow right-to-left when bubble is on left + $thoughtPanel.find('.rpg-thought-circles').css({ + top: 'calc(50% - 50px)', + right: '-25px', + bottom: 'auto', + left: 'auto' + }); + // Keep the circle flow for left side (right-to-left) - default from CSS + $thoughtPanel.find('.rpg-thought-circles').css('align-items', 'flex-end'); + $thoughtPanel.find('.rpg-circle-1').css({ 'align-self': 'flex-end', 'margin-left': '0', 'margin-right': '0' }); + $thoughtPanel.find('.rpg-circle-2').css({ 'align-self': 'flex-end', 'margin-left': '0', 'margin-right': '4px' }); + $thoughtPanel.find('.rpg-circle-3').css({ 'align-self': 'flex-end', 'margin-left': '0', 'margin-right': '8px' }); + } + + if (useRightPosition) { + $thoughtPanel.css({ + top: `${top}px`, + right: `${right}px`, + left: 'auto' // Clear left positioning + }); + } else { + $thoughtPanel.css({ + top: `${top}px`, + left: `${left}px`, + right: 'auto' // Clear right positioning + }); + } + + $thoughtIcon.css({ + top: `${iconTop}px`, + left: `${iconLeft}px`, + right: 'auto' // Clear any right positioning + }); + + // Initially hide the icon + $thoughtIcon.hide(); + + // console.log('[RPG Companion] Thought panel created at:', { top, left }); + + // Close button functionality + $thoughtPanel.find('.rpg-thought-close').on('click', function(e) { + e.stopPropagation(); + $thoughtPanel.fadeOut(200); + $thoughtIcon.fadeIn(200); + }); + + // Icon click to show panel + $thoughtIcon.on('click', function(e) { + e.stopPropagation(); + $thoughtIcon.fadeOut(200); + $thoughtPanel.fadeIn(200); + }); + + // Add event handlers for editable thoughts in the bubble + $thoughtPanel.find('.rpg-editable').on('blur', function() { + const character = $(this).data('character'); + const field = $(this).data('field'); + const value = $(this).text().trim(); + // console.log('[RPG Companion] 💭 Thought bubble blur event - character:', character, 'field:', field, 'value:', value); + updateCharacterField(character, field, value); + }); + + // Update position on scroll + const updatePanelPosition = () => { + if (!$message.is(':visible')) { + $thoughtPanel.hide(); + $thoughtIcon.hide(); + return; + } + + const newAvatarRect = $avatar[0].getBoundingClientRect(); + const newTop = newAvatarRect.top + (newAvatarRect.height / 2); + const newIconTop = newAvatarRect.top; + let newLeft, newIconLeft; + + if (panelPosition === 'left') { + // Position at chat's right edge, extending right + const chatContainer = $('#chat')[0]; + const chatRect = chatContainer ? chatContainer.getBoundingClientRect() : { right: window.innerWidth }; + newLeft = chatRect.right + panelMargin; + newIconLeft = chatRect.right + 10; + + $thoughtPanel.css({ + top: `${newTop}px`, + left: `${newLeft}px`, + right: 'auto' + }); + } else { + // Left position relative to avatar + newLeft = newAvatarRect.left - panelWidth - panelMargin; + newIconLeft = newAvatarRect.left - 40; + + $thoughtPanel.css({ + top: `${newTop}px`, + left: `${newLeft}px`, + right: 'auto' + }); + } + + $thoughtIcon.css({ + top: `${newIconTop}px`, + left: `${newIconLeft}px`, + right: 'auto' + }); + + if ($thoughtPanel.is(':visible')) { + $thoughtPanel.show(); + } + if ($thoughtIcon.is(':visible')) { + $thoughtIcon.show(); + } + }; + + // Update position on scroll and resize + $('#chat').on('scroll.thoughtPanel', updatePanelPosition); + $(window).on('resize.thoughtPanel', updatePanelPosition); + + // Remove panel when clicking outside (but not when clicking icon or panel) + $(document).on('click.thoughtPanel', function(e) { + if (!$(e.target).closest('#rpg-thought-panel, #rpg-thought-icon').length) { + // Hide the panel and show the icon instead of removing + $thoughtPanel.fadeOut(200); + $thoughtIcon.fadeIn(200); + } + }); +} + +/** + * Event handler for when generation is about to start (TOGETHER MODE). + * Injects RPG tracking prompt into the generation. + */ +function onGenerationStarted() { + // console.log('[RPG Companion] onGenerationStarted called'); + // console.log('[RPG Companion] enabled:', extensionSettings.enabled); + // console.log('[RPG Companion] generationMode:', extensionSettings.generationMode); + // console.log('[RPG Companion] ⚡ EVENT: onGenerationStarted - lastActionWasSwipe =', lastActionWasSwipe, '| isGenerating =', isGenerating); + + if (!extensionSettings.enabled) { + return; + } + + const chat = getContext().chat; + const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null; + + // For SEPARATE mode only: Check if we need to commit extension data + // BUT: Only do this for the MAIN generation, not the tracker update generation + // If isGenerating is true, this is the tracker update generation (second call), so skip flag logic + // console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts); + if (extensionSettings.generationMode === 'separate' && !isGenerating) { + if (!lastActionWasSwipe) { + // User sent a new message - commit lastGeneratedData before generation + // console.log('[RPG Companion] 📝 COMMIT: New message - committing lastGeneratedData'); + // console.log('[RPG Companion] BEFORE commit - committedTrackerData:', { + // userStats: committedTrackerData.userStats ? 'exists' : 'null', + // infoBox: committedTrackerData.infoBox ? 'exists' : 'null', + // characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null' + // }); + // console.log('[RPG Companion] BEFORE commit - lastGeneratedData:', { + // userStats: lastGeneratedData.userStats ? 'exists' : 'null', + // infoBox: lastGeneratedData.infoBox ? 'exists' : 'null', + // characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null' + // }); + committedTrackerData.userStats = lastGeneratedData.userStats; + committedTrackerData.infoBox = lastGeneratedData.infoBox; + committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts; + // console.log('[RPG Companion] AFTER commit - committedTrackerData:', { + // userStats: committedTrackerData.userStats ? 'exists' : 'null', + // infoBox: committedTrackerData.infoBox ? 'exists' : 'null', + // characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null' + // }); + + // Reset flag after committing (ready for next cycle) + + } else { + // console.log('[RPG Companion] 🔄 SWIPE: Using existing committedTrackerData (no commit)'); + // console.log('[RPG Companion] committedTrackerData:', { + // userStats: committedTrackerData.userStats ? 'exists' : 'null', + // infoBox: committedTrackerData.infoBox ? 'exists' : 'null', + // characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null' + // }); + // Reset flag after using it (swipe generation complete, ready for next action) + } + } + + // Use the committed tracker data as source for generation + // console.log('[RPG Companion] Using committedTrackerData for generation'); + // console.log('[RPG Companion] committedTrackerData.userStats:', committedTrackerData.userStats); + + // Parse stats from committed data to update the extensionSettings for prompt generation + if (committedTrackerData.userStats) { + // console.log('[RPG Companion] Parsing committed userStats into extensionSettings'); + parseUserStats(committedTrackerData.userStats); + // console.log('[RPG Companion] After parsing, extensionSettings.userStats:', JSON.stringify(extensionSettings.userStats)); + } + + if (extensionSettings.generationMode === 'together') { + // console.log('[RPG Companion] In together mode, generating prompts...'); + const example = generateTrackerExample(); + const instructions = generateTrackerInstructions(); + + // console.log('[RPG Companion] Example:', example ? 'exists' : 'empty'); + // console.log('[RPG Companion] Chat length:', chat ? chat.length : 'chat is null'); + + // Find the last assistant message in the chat history + let lastAssistantDepth = -1; // -1 means not found + if (chat && chat.length > 0) { + // console.log('[RPG Companion] Searching for last assistant message...'); + // Start from depth 1 (skip depth 0 which is usually user's message or prefill) + for (let depth = 1; depth < chat.length; depth++) { + const index = chat.length - 1 - depth; // Convert depth to index + const message = chat[index]; + // console.log('[RPG Companion] Checking depth', depth, 'index', index, 'message properties:', Object.keys(message)); + // Check for assistant message: not user and not system + if (!message.is_user && !message.is_system) { + // Found assistant message at this depth + // Inject at the SAME depth to prepend to this assistant message + lastAssistantDepth = depth; + // console.log('[RPG Companion] Found last assistant message at depth', depth, '-> injecting at same depth:', lastAssistantDepth); + break; + } + } + } + + // If we have previous tracker data and found an assistant message, inject it as an assistant message + if (example && lastAssistantDepth > 0) { + setExtensionPrompt('rpg-companion-example', example, extension_prompt_types.IN_CHAT, lastAssistantDepth, false, extension_prompt_roles.ASSISTANT); + // console.log('[RPG Companion] Injected tracker example as assistant message at depth:', lastAssistantDepth); + } else { + // console.log('[RPG Companion] NOT injecting example. example:', !!example, 'lastAssistantDepth:', lastAssistantDepth); + } + + // Inject the instructions as a user message at depth 0 (right before generation) + setExtensionPrompt('rpg-companion-inject', instructions, extension_prompt_types.IN_CHAT, 0, false, extension_prompt_roles.USER); + // console.log('[RPG Companion] Injected RPG tracking instructions at depth 0 (right before generation)'); + } else if (extensionSettings.generationMode === 'separate') { + // In SEPARATE mode, inject the contextual summary for main roleplay generation + const contextSummary = generateContextualSummary(); + + if (contextSummary) { + const wrappedContext = `Here is context information about the current scene, and what follows is the last message in the chat history: + +${contextSummary} + +Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses. + + +`; + + // Inject context at depth 1 (before last user message) as SYSTEM + setExtensionPrompt('rpg-companion-context', wrappedContext, extension_prompt_types.IN_CHAT, 1, false); + // console.log('[RPG Companion] Injected contextual summary for separate mode:', contextSummary); + } else { + // Clear if no data yet + setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false); + } + + // Inject HTML prompt separately at depth 0 if enabled (same as together mode pattern) + if (extensionSettings.enableHtmlPrompt) { + const htmlPrompt = `\nIf appropriate, include inline HTML, CSS, and JS elements for creative, visual storytelling throughout your response: +- Use them liberally to depict any in-world content that can be visualized (screens, posters, books, signs, letters, logos, crests, seals, medallions, labels, etc.), with creative license for animations, 3D effects, pop-ups, dropdowns, websites, and so on. +- Style them thematically to match the theme (e.g., sleek for sci-fi, rustic for fantasy), ensuring text is visible. +- Embed all resources directly (e.g., inline SVGs) so nothing relies on external fonts or libraries. +- Place elements naturally in the narrative where characters would see or use them, with no limits on format or application. +- These HTML/CSS/JS elements must be rendered directly without enclosing them in code fences.`; + + setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false); + // console.log('[RPG Companion] Injected HTML prompt at depth 0 for separate mode'); + } else { + // Clear HTML prompt if disabled + setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false); + } + + // Clear together mode injections + setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false); + setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false); + } else { + // Clear all injections + setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false); + setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false); + setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false); + } +} + +/** + * Commits the tracker data from the last assistant message to be used as source for next generation. + * This should be called when the user has replied to a message, ensuring all swipes of the next + * response use the same committed context. + */ +function commitTrackerData() { + const chat = getContext().chat; + if (!chat || chat.length === 0) { + return; + } + + // 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 - commit its tracker data + if (message.extra && message.extra.rpg_companion_swipes) { + const swipeId = message.swipe_id || 0; + const swipeData = message.extra.rpg_companion_swipes[swipeId]; + + if (swipeData) { + // console.log('[RPG Companion] Committing tracker data from assistant message at index', i, 'swipe', swipeId); + committedTrackerData.userStats = swipeData.userStats || null; + committedTrackerData.infoBox = swipeData.infoBox || null; + committedTrackerData.characterThoughts = swipeData.characterThoughts || null; + } else { + // console.log('[RPG Companion] No swipe data found for swipe', swipeId); + } + } else { + // console.log('[RPG Companion] No RPG data found in last assistant message'); + } + break; + } + } +} + +/** + * Event handler for when the user sends a message. + * Sets the flag to indicate this is NOT a swipe. + */ +function onMessageSent() { + if (!extensionSettings.enabled) return; + + // User sent a new message - NOT a swipe + lastActionWasSwipe = false; + // console.log('[RPG Companion] đŸŸĸ EVENT: onMessageSent - lastActionWasSwipe =', lastActionWasSwipe); +} + +/** + * Event handler for when a message is generated. + */ +async function onMessageReceived(data) { + if (!extensionSettings.enabled) { + return; + } + + if (extensionSettings.generationMode === 'together') { + // In together mode, parse the response to extract RPG data + // The message should be in chat[chat.length - 1] + const lastMessage = chat[chat.length - 1]; + if (lastMessage && !lastMessage.is_user) { + const responseText = lastMessage.mes; + // console.log('[RPG Companion] Parsing together mode response:', responseText); + + const parsedData = parseResponse(responseText); + + // Update stored data + if (parsedData.userStats) { + lastGeneratedData.userStats = parsedData.userStats; + parseUserStats(parsedData.userStats); + } + if (parsedData.infoBox) { + lastGeneratedData.infoBox = parsedData.infoBox; + } + if (parsedData.characterThoughts) { + lastGeneratedData.characterThoughts = parsedData.characterThoughts; + } + + // Store RPG data for this specific swipe in the message's extra field + if (!lastMessage.extra) { + lastMessage.extra = {}; + } + if (!lastMessage.extra.rpg_companion_swipes) { + lastMessage.extra.rpg_companion_swipes = {}; + } + + const currentSwipeId = lastMessage.swipe_id || 0; + lastMessage.extra.rpg_companion_swipes[currentSwipeId] = { + userStats: parsedData.userStats, + infoBox: parsedData.infoBox, + characterThoughts: parsedData.characterThoughts + }; + + // console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId); + + // If there's no committed data yet (first time generating), automatically commit + if (!committedTrackerData.userStats && !committedTrackerData.infoBox && !committedTrackerData.characterThoughts) { + committedTrackerData.userStats = parsedData.userStats; + committedTrackerData.infoBox = parsedData.infoBox; + committedTrackerData.characterThoughts = parsedData.characterThoughts; + // console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data'); + } else { + // console.log('[RPG Companion] Data will be committed when user replies'); + } + + // Remove the tracker code blocks from the visible message + let cleanedMessage = responseText; + // Remove all code blocks that contain tracker data + cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, ''); + cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, ''); + cleanedMessage = cleanedMessage.replace(/```[^`]*?Present Characters\s*\n\s*---[^`]*?```\s*/gi, ''); + // Remove any stray "---" dividers that might appear after the code blocks + cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, ''); + // Clean up multiple consecutive newlines + cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n'); + + // Update the message in chat history + lastMessage.mes = cleanedMessage.trim(); + + // Update the swipe text as well + if (lastMessage.swipes && lastMessage.swipes[currentSwipeId] !== undefined) { + lastMessage.swipes[currentSwipeId] = cleanedMessage.trim(); + } + + // console.log('[RPG Companion] Cleaned message, removed tracker code blocks'); + + // Render the updated data + renderUserStats(); + renderInfoBox(); + renderThoughts(); + + // Save to chat metadata + saveChatData(); + } + } else if (extensionSettings.generationMode === 'separate' && extensionSettings.autoUpdate) { + // In separate mode with auto-update, trigger update after message + setTimeout(async () => { + await updateRPGData(); + }, 500); + } + + // Reset the swipe flag after generation completes + // This ensures that if the user swiped → auto-reply generated → flag is now cleared + // so the next user message will be treated as a new message (not a swipe) + if (lastActionWasSwipe) { + // console.log('[RPG Companion] 🔄 Generation complete after swipe - resetting lastActionWasSwipe to false'); + lastActionWasSwipe = false; + } +} + +/** + * Event handler for character change. + */ +function onCharacterChanged() { + // Remove thought panel and icon when changing characters + $('#rpg-thought-panel').remove(); + $('#rpg-thought-icon').remove(); + $('#chat').off('scroll.thoughtPanel'); + $(window).off('resize.thoughtPanel'); + $(document).off('click.thoughtPanel'); + + // Load chat-specific data when switching chats + loadChatData(); + + // Commit tracker data from the last assistant message to initialize for this chat + commitTrackerData(); + + // Re-render with the loaded data + renderUserStats(); + renderInfoBox(); + renderThoughts(); + + // Update chat thought overlays + updateChatThoughts(); +} + +/** + * Event handler for when a message is swiped. + * Loads the RPG data for the swipe the user navigated to. + */ +function onMessageSwiped(messageIndex) { + if (!extensionSettings.enabled) { + return; + } + + // console.log('[RPG Companion] Message swiped at index:', messageIndex); + + // Get the message that was swiped + const message = chat[messageIndex]; + if (!message || message.is_user) { + return; + } + + const currentSwipeId = message.swipe_id || 0; + + // Only set flag to true if this swipe will trigger a NEW generation + // Check if the swipe already exists (has content in the swipes array) + const isExistingSwipe = message.swipes && + message.swipes[currentSwipeId] !== undefined && + message.swipes[currentSwipeId] !== null && + message.swipes[currentSwipeId].length > 0; + + if (!isExistingSwipe) { + // This is a NEW swipe that will trigger generation + lastActionWasSwipe = true; + // console.log('[RPG Companion] đŸ”ĩ EVENT: onMessageSwiped (NEW generation) - lastActionWasSwipe =', lastActionWasSwipe); + } else { + // This is navigating to an EXISTING swipe - don't change the flag + // console.log('[RPG Companion] đŸ”ĩ EVENT: onMessageSwiped (existing swipe navigation) - lastActionWasSwipe unchanged =', lastActionWasSwipe); + } + + // console.log('[RPG Companion] Loading data for swipe', currentSwipeId); + + // Load RPG data for this swipe into lastGeneratedData (for display only) + // This updates what the user sees, but does NOT commit it + // Committed data will be updated when/if the user replies to this swipe + if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) { + const swipeData = message.extra.rpg_companion_swipes[currentSwipeId]; + + // Update display data + lastGeneratedData.userStats = swipeData.userStats || null; + lastGeneratedData.infoBox = swipeData.infoBox || null; + lastGeneratedData.characterThoughts = swipeData.characterThoughts || null; + + // Parse user stats if available + if (swipeData.userStats) { + parseUserStats(swipeData.userStats); + } + + // console.log('[RPG Companion] Loaded RPG data for swipe', currentSwipeId, '(display only, NOT committed)'); + // console.log('[RPG Companion] committedTrackerData unchanged - will be updated if user replies to this swipe'); + } else { + // No data for this swipe - keep existing lastGeneratedData (don't clear it) + // This ensures the display remains consistent and data is available for next commit + // console.log('[RPG Companion] No RPG data for swipe', currentSwipeId, '- keeping existing lastGeneratedData'); + } + + // Re-render the panels (display only - committedTrackerData unchanged) + renderUserStats(); + renderInfoBox(); + renderThoughts(); + + // Update chat thought overlays + updateChatThoughts(); +} + +/** + * Main initialization function. + */ +jQuery(async () => { + loadSettings(); + await addExtensionSettings(); + await initUI(); + + // Load chat-specific data for current chat + loadChatData(); + + // Register event listeners + eventSource.on(event_types.MESSAGE_SENT, onMessageSent); + eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted); + eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived); + eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, onMessageReceived); + eventSource.on(event_types.CHAT_CHANGED, onCharacterChanged); + eventSource.on(event_types.MESSAGE_SWIPED, onMessageSwiped); + + // console.log('[RPG Companion] Extension loaded successfully'); +}); diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..f6811a7 --- /dev/null +++ b/manifest.json @@ -0,0 +1,11 @@ +{ + "display_name": "RPG Companion", + "loading_order": 100, + "requires": [], + "optional": [], + "js": "index.js", + "css": "style.css", + "author": "Marysia", + "version": "1.0.0", + "homePage": "https://github.com/SillyTavern/SillyTavern" +} diff --git a/settings.html b/settings.html new file mode 100644 index 0000000..a6d40ea --- /dev/null +++ b/settings.html @@ -0,0 +1,24 @@ +
+
+
+ RPG Companion +
+
+
+ + Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself. + + +
+
+
diff --git a/style.css b/style.css new file mode 100644 index 0000000..552394c --- /dev/null +++ b/style.css @@ -0,0 +1,2817 @@ +/* ============================================ + RPG COMPANION - GAME-LIKE UI + ============================================ */ + +/* CSS Variables for Theming - Default uses SillyTavern's variables */ +.rpg-panel, +#rpg-thought-panel, +#rpg-thought-icon { + --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: var(--SmartThemeBorderColor, #0f3460); + --rpg-shadow: rgba(0, 0, 0, 0.5); +} + +/* Main Panel Container - Fixed height, no overflow */ +.rpg-panel { + position: fixed; + top: var(--topBarBlockSize); + bottom: 0; + width: min(380px, 25vw); + max-width: 450px; + min-width: 300px; + background: var(--rpg-bg); + box-shadow: 0 0 20px var(--rpg-shadow); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + box-sizing: border-box; + z-index: 1000; + color: var(--rpg-text); + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Right Position (Default) */ +.rpg-panel.rpg-position-right { + right: 0; + border-left: 3px solid var(--rpg-border); + box-shadow: -5px 0 20px var(--rpg-shadow); +} + +body:has(.rpg-panel.rpg-position-right) #sheld { + margin-right: min(380px, 25vw); +} + +/* Left Position */ +.rpg-panel.rpg-position-left { + left: 0; + border-right: 3px solid var(--rpg-border); + box-shadow: 5px 0 20px var(--rpg-shadow); +} + +body:has(.rpg-panel.rpg-position-left) #sheld { + margin-left: min(380px, 25vw); +} + +/* Top Position */ +.rpg-panel.rpg-position-top { + left: 0; + right: 0; + bottom: auto; + width: 100%; + max-width: 100%; + height: auto; + max-height: 300px; + border-bottom: 3px solid var(--rpg-border); + box-shadow: 0 5px 20px var(--rpg-shadow); +} + +body:has(.rpg-panel.rpg-position-top) #sheld { + margin-top: 0; +} + +/* Top Position Layout Adjustments */ +.rpg-panel.rpg-position-top .rpg-content-box { + display: flex; + flex-direction: row; + gap: 15px; +} + +.rpg-panel.rpg-position-top .rpg-section { + flex: 1; + min-width: 0; +} + +.rpg-panel.rpg-position-top .rpg-divider { + display: none; +} + +.rpg-panel.rpg-position-top .rpg-stats-grid { + gap: 8px; +} + +.rpg-panel.rpg-position-top .rpg-stat-row { + gap: 8px; +} + +.rpg-panel.rpg-position-top .rpg-stat-label { + min-width: 90px; + font-size: 11px; +} + +.rpg-panel.rpg-position-top .rpg-stat-bar { + height: 16px; +} + +.rpg-panel.rpg-position-top .rpg-stat-value { + font-size: 11px; +} + +.rpg-panel.rpg-position-top .rpg-section-title { + font-size: 14px; + padding: 8px 12px; +} + +.rpg-panel.rpg-position-top .rpg-user-portrait { + width: 40px; + height: 40px; +} + +.rpg-panel.rpg-position-top .rpg-stats-title { + font-size: 14px; +} + +.rpg-panel.rpg-position-top .rpg-mood { + font-size: 11px; + padding: 6px; +} + +.rpg-panel.rpg-position-top .rpg-classic-stats { + margin-top: 8px; + padding: 8px; +} + +.rpg-panel.rpg-position-top .rpg-classic-stats-title { + font-size: 11px; + margin-bottom: 8px; +} + +.rpg-panel.rpg-position-top .rpg-classic-stats-grid { + gap: 6px; +} + +.rpg-panel.rpg-position-top .rpg-classic-stat { + padding: 4px; +} + +.rpg-panel.rpg-position-top .rpg-classic-stat-label { + font-size: 9px; +} + +.rpg-panel.rpg-position-top .rpg-classic-stat-value { + font-size: 14px; +} + +/* ============================================ + ANIMATIONS + ============================================ */ +/* Only apply animations when enabled */ +.rpg-panel.rpg-animations-enabled .rpg-stat-fill { + transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} + +.rpg-panel.rpg-animations-enabled .rpg-stat-bar { + transition: all 0.3s ease; +} + +.rpg-panel.rpg-animations-enabled .rpg-info-content, +.rpg-panel.rpg-animations-enabled .rpg-thoughts-content { + transition: opacity 0.4s ease, transform 0.4s ease; +} + +.rpg-panel.rpg-animations-enabled .rpg-info-line { + animation: slideInFromLeft 0.5s ease-out backwards; +} + +.rpg-panel.rpg-animations-enabled .rpg-info-line:nth-child(1) { animation-delay: 0.1s; } +.rpg-panel.rpg-animations-enabled .rpg-info-line:nth-child(2) { animation-delay: 0.2s; } +.rpg-panel.rpg-animations-enabled .rpg-info-line:nth-child(3) { animation-delay: 0.3s; } +.rpg-panel.rpg-animations-enabled .rpg-info-line:nth-child(4) { animation-delay: 0.4s; } +.rpg-panel.rpg-animations-enabled .rpg-info-line:nth-child(5) { animation-delay: 0.5s; } + +.rpg-panel.rpg-animations-enabled .rpg-thoughts-overlay { + animation: fadeInScale 0.6s ease-out; +} + +.rpg-panel.rpg-animations-enabled .rpg-mood-value { + transition: all 0.3s ease; +} + +.rpg-panel.rpg-animations-enabled .rpg-dice-animation { + animation: none; /* Control separately */ +} + +/* Disable dice animation when animations are off */ +.rpg-panel:not(.rpg-animations-enabled) .rpg-dice-rolling i { + animation: none !important; +} + +.rpg-panel:not(.rpg-animations-enabled) .rpg-dice-result-value { + animation: none !important; +} + +@keyframes slideInFromLeft { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes fadeInScale { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* Add updating class for smooth content changes */ +.rpg-panel.rpg-animations-enabled .rpg-content-updating { + animation: contentPulse 0.5s ease; +} + +@keyframes contentPulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + +.rpg-panel.rpg-position-top .rpg-classic-stat-btn { + padding: 2px; + font-size: 12px; +} + +.rpg-panel.rpg-position-top .rpg-info-content, +.rpg-panel.rpg-position-top .rpg-thoughts-content { + font-size: 12px; + max-height: 150px; +} + +/* Custom Scrollbar */ +.rpg-panel::-webkit-scrollbar, +#rpg-panel-content::-webkit-scrollbar, +.rpg-content-box::-webkit-scrollbar { + width: 8px; +} + +.rpg-panel::-webkit-scrollbar-track, +#rpg-panel-content::-webkit-scrollbar-track, +.rpg-content-box::-webkit-scrollbar-track { + background: var(--rpg-accent); +} + +.rpg-panel::-webkit-scrollbar-thumb, +#rpg-panel-content::-webkit-scrollbar-thumb, +.rpg-content-box::-webkit-scrollbar-thumb { + background: var(--rpg-highlight); + border-radius: 4px; +} + +.rpg-panel::-webkit-scrollbar-thumb:hover, +#rpg-panel-content::-webkit-scrollbar-thumb:hover, +.rpg-content-box::-webkit-scrollbar-thumb:hover { + background: var(--rpg-border); +} + +/* Game Container - Full height flex container */ +.rpg-game-container { + padding: 12px; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +/* Panel Content - Main scrollable area */ +#rpg-panel-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; + overflow-y: hidden; + overflow-x: hidden; + min-height: 0; +} + +/* Header - Fixed size, doesn't grow */ +.rpg-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 10px; + border-bottom: 2px solid var(--rpg-border); + flex-shrink: 0; +} + +.rpg-panel-header h3 { + margin: 0; + font-size: 18px; + font-weight: bold; + display: flex; + align-items: center; + gap: 8px; + color: var(--rpg-highlight); + text-shadow: 0 0 8px var(--rpg-highlight); + letter-spacing: 0.5px; +} + +.rpg-btn-icon { + padding: 8px 12px; + background: var(--rpg-accent); + border: 2px solid var(--rpg-border); + border-radius: 8px; + cursor: pointer; + color: var(--rpg-text); + transition: all 0.3s ease; +} + +.rpg-btn-icon:hover { + background: var(--rpg-highlight); + border-color: var(--rpg-highlight); + transform: scale(1.05); +} + +/* Loading Indicator - Fixed size */ +.rpg-loading { + text-align: center; + padding: 15px; + background: var(--rpg-accent); + border-radius: 12px; + color: var(--rpg-highlight); + font-size: 14px; + font-weight: bold; + animation: pulseGlow 1.5s ease-in-out infinite; + flex-shrink: 0; +} + +@keyframes pulseGlow { + 0%, 100% { + opacity: 1; + box-shadow: 0 0 10px var(--rpg-highlight); + } + 50% { + opacity: 0.7; + box-shadow: 0 0 20px var(--rpg-highlight); + } +} + +/* Dice Roll Display - More compact */ +.rpg-dice-display { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px; + background: rgba(0, 0, 0, 0.3); + border-radius: 8px; + border: 2px solid var(--rpg-border); + font-size: 11px; + font-weight: bold; + color: var(--rpg-text); + cursor: pointer; + transition: all 0.3s ease; + flex-shrink: 0; +} + +.rpg-dice-display:hover { + background: var(--rpg-accent); + border-color: var(--rpg-highlight); + transform: scale(1.02); +} + +.rpg-dice-display i { + color: var(--rpg-highlight); + font-size: 14px; +} + +/* Clear dice roll button */ +.rpg-clear-dice-btn { + background: rgba(255, 0, 0, 0.2); + border: 1px solid rgba(255, 0, 0, 0.4); + border-radius: 4px; + color: #ff6b6b; + font-size: 16px; + font-weight: bold; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + padding: 0; + line-height: 1; + margin-left: auto; +} + +.rpg-clear-dice-btn:hover { + background: rgba(255, 0, 0, 0.4); + border-color: #ff6b6b; + transform: scale(1.1); +} + +.rpg-clear-dice-btn:active { + transform: scale(0.95); +} + +/* Unified Content Box - Contains all sections, scales to fit viewport */ +.rpg-content-box { + background: linear-gradient(135deg, var(--rpg-accent) 0%, var(--rpg-bg) 100%); + border: 2px solid var(--rpg-border); + border-radius: 10px; + padding: 8px; + box-shadow: 0 4px 18px var(--rpg-shadow), inset 0 0 12px rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-height: 0; + overflow: hidden; +} + +/* Section Styling - Flexible to scale with available space */ +.rpg-section { + margin: 0; + padding: 0; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; +} + +.rpg-section:empty { + display: none; +} + +/* Divider - More compact */ +.rpg-divider { + width: 100%; + height: 1px; + background: linear-gradient(to right, transparent, var(--rpg-highlight), transparent); + margin: 8px 0; + position: relative; + flex-shrink: 0; +} + +.rpg-divider::after { + display: none; /* Remove diamond decoration for more space */ +} + +/* ============================================ + USER STATS SECTION + ============================================ */ +.rpg-stats-section { + text-align: center; + display: flex; + flex-direction: column; + min-height: 0; + flex: 1; + overflow-y: auto; + overflow-x: hidden; +} + +.rpg-stats-header { + display: flex; + align-items: stretch; + justify-content: space-between; + gap: clamp(6px, 1.2vh, 12px); + margin-bottom: clamp(4px, 1vh, 8px); + flex-shrink: 0; +} + +.rpg-stats-header-left { + display: flex; + align-items: center; + gap: clamp(4px, 1vh, 8px); +} + +.rpg-user-portrait { + width: clamp(24px, 4vh, 45px); + height: clamp(24px, 4vh, 45px); + border-radius: 50%; + border: 2px solid var(--rpg-highlight); + box-shadow: 0 0 10px var(--rpg-highlight); + object-fit: cover; + transition: transform 0.3s ease; +} + +.rpg-user-portrait:hover { + transform: scale(1.1) rotate(5deg); +} + +.rpg-stats-title { + font-size: clamp(8px, 1.6vh, 12px); + font-weight: bold; + color: var(--rpg-highlight); + text-shadow: 0 0 8px var(--rpg-highlight); + line-height: 1.2; +} + +/* Inventory Box - Styled like a treasure chest */ +.rpg-inventory-box { + flex: 1; + display: flex; + flex-direction: column; + position: relative; + background: linear-gradient(135deg, var(--rpg-accent) 0%, var(--rpg-bg) 100%); + border: 2px solid var(--rpg-highlight); + border-radius: clamp(4px, 0.8vh, 8px); + padding: clamp(4px, 0.8vh, 8px); + max-width: 200px; + box-shadow: + inset 0 2px 4px rgba(255, 255, 255, 0.1), + inset 0 -2px 4px rgba(0, 0, 0, 0.3), + 0 2px 8px rgba(0, 0, 0, 0.3); + overflow: hidden; +} + +/* Add decorative chest details with pseudo-elements */ +.rpg-inventory-box::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 60%; + height: 2px; + background: var(--rpg-highlight); + opacity: 0.5; +} + +.rpg-inventory-box::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 80%; + height: 1px; + background: var(--rpg-highlight); + opacity: 0.3; +} + +.rpg-inventory-items { + font-size: clamp(6px, 1.1vh, 9px); + color: var(--rpg-text); + line-height: 1.4; + overflow-y: auto; + max-height: clamp(35px, 5.5vh, 55px); + padding: clamp(2px, 0.4vh, 4px); + text-align: left; +} + +.rpg-inventory-items::-webkit-scrollbar { + width: 3px; +} + +.rpg-inventory-items::-webkit-scrollbar-track { + background: var(--rpg-bg); + border-radius: 2px; +} + +.rpg-inventory-items::-webkit-scrollbar-thumb { + background: var(--rpg-highlight); + border-radius: 2px; +} + +.rpg-inventory-items::-webkit-scrollbar-thumb:hover { + background: var(--rpg-text); +} + +/* Stats Content - Two-column layout */ +.rpg-stats-content { + display: flex; + gap: clamp(4px, 0.8vh, 8px); + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} + +/* Stats Left - Bars and mood */ +.rpg-stats-left { + flex: 1; + display: flex; + flex-direction: column; + gap: clamp(2px, 0.4vh, 4px); + min-height: 0; +} + +/* Stats Right - Attributes */ +.rpg-stats-right { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.rpg-stats-grid { + display: flex; + flex-direction: column; + gap: clamp(2px, 0.4vh, 4px); + flex: 1; + min-height: 0; +} + +.rpg-stat-row { + display: flex; + align-items: center; + gap: 4px; + flex: 1; + min-height: 0; +} + +.rpg-stat-label { + min-width: 65px; + font-size: clamp(7px, 1.4vh, 9px); + font-weight: 600; + text-align: left; + color: var(--rpg-text); + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.rpg-stat-bar { + flex: 1; + height: 12px; + min-height: 8px; + max-height: 16px; + position: relative; + border-radius: 6px; + overflow: hidden; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.rpg-stat-fill { + position: absolute; + right: 0; + top: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + /* Transition moved to .rpg-animations-enabled */ +} + +.rpg-stat-value { + color: #fff; + font-size: 9px; + font-weight: bold; + min-width: 30px; + text-align: right; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); +} + +.rpg-mood { + margin-top: 3px; + display: flex; + align-items: center; + gap: 6px; + font-size: 9px; + padding: 4px 6px; + background: rgba(0, 0, 0, 0.3); + border-radius: 4px; + border: 1px solid var(--rpg-border); + flex-shrink: 0; +} + +.rpg-mood-emoji { + font-size: 16px; + flex-shrink: 0; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5)); +} + +.rpg-mood-conditions { + 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; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.rpg-classic-stats-title { + text-align: center; + font-size: 9px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--rpg-highlight); + margin-bottom: 4px; + flex-shrink: 0; +} + +.rpg-classic-stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: clamp(3px, 0.5vh, 5px); + flex: 1; + align-content: stretch; + min-height: 0; +} + +.rpg-classic-stat { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-evenly; + gap: clamp(1px, 0.2vh, 2px); + padding: clamp(4px, 0.6vh, 6px); + background: rgba(0, 0, 0, 0.3); + border-radius: 4px; + border: 1px solid var(--rpg-border); + box-sizing: border-box; + min-height: 0; + height: 100%; +} + +.rpg-classic-stat-label { + font-size: clamp(7px, 0.9vh, 8px); + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.1px; + color: var(--rpg-text); + line-height: 1; + flex-shrink: 0; +} + +.rpg-classic-stat-value { + font-size: clamp(11px, 1.4vh, 14px); + font-weight: bold; + color: var(--rpg-highlight); + text-align: center; + line-height: 1; + flex-shrink: 0; +} + +.rpg-classic-stat-buttons { + display: flex; + gap: clamp(3px, 0.4vh, 5px); + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 100%; +} + +.rpg-classic-stat-btn { + width: clamp(18px, 2.2vh, 24px); + height: clamp(16px, 2vh, 20px); + padding: 0; + background: var(--rpg-accent); + border: 1px solid var(--rpg-border); + border-radius: 2px; + color: var(--rpg-text); + font-size: clamp(9px, 1.1vh, 12px); + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + line-height: 1; + flex: 0 0 auto; +} + +.rpg-classic-stat-btn:hover { + background: var(--rpg-highlight); + border-color: var(--rpg-highlight); + transform: scale(1.05); +} + +.rpg-classic-stat-btn:active { + transform: scale(0.95); +} + +/* ============================================ + INFO BOX SECTION + ============================================ */ + +.rpg-info-header { + font-size: 14px; + font-weight: bold; + margin-bottom: 10px; + color: var(--rpg-highlight); + text-shadow: 0 0 8px var(--rpg-highlight); + flex-shrink: 0; +} + +.rpg-info-content { + font-size: 12px; + line-height: 1.5; + text-align: left; + flex: 1; + min-height: 0; + overflow-y: auto; +} + +/* ============================================ + INFO BOX - VISUAL DASHBOARD + ============================================ */ +.rpg-info-section { + background: rgba(0, 0, 0, 0.2); + border: 2px solid var(--rpg-border); + border-radius: 12px; + padding: 6px; + margin-bottom: 0; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + gap: 4px; + align-items: stretch; + width: 100%; + overflow-y: auto; + overflow-x: hidden; +} + +.rpg-dashboard { + display: flex; + gap: 4px; + flex: 1; + min-height: 0; +} + +/* Row 1: 4 equal-width widgets */ +.rpg-dashboard-row-1 { + flex: 1; + min-height: 0; +} + +.rpg-dashboard-row-1 .rpg-dashboard-widget { + flex: 1 1 0; + min-width: 0; + height: 100%; +} + +/* Row 2: Full-width location */ +.rpg-dashboard-row-2 { + flex: 1; + min-height: 0; +} + +.rpg-dashboard-row-2 .rpg-dashboard-widget { + flex: 1; + width: 100%; + height: 100%; +} + +.rpg-dashboard-widget { + background: rgba(0, 0, 0, 0.5); + border: 2px solid var(--rpg-border); + border-radius: 6px; + padding: 4px; + transition: transform 0.2s ease, box-shadow 0.2s ease; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.rpg-dashboard-widget:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px var(--rpg-shadow); +} + +/* Location widget - flexible height */ +.rpg-location-widget { + height: 100%; +} + +/* Calendar Widget */ +.rpg-calendar-widget { + padding: 3px; +} + +.rpg-calendar-top { + background: var(--rpg-highlight); + color: var(--rpg-bg); + font-size: clamp(6px, 1.2vh, 8px); + font-weight: bold; + padding: 2px 6px; + border-radius: 3px 3px 0 0; + width: 100%; + text-align: center; +} + +.rpg-calendar-day { + background: rgba(255, 255, 255, 0.1); + color: var(--rpg-text); + font-size: clamp(8px, 1.5vw, 14px); + font-weight: bold; + padding: 6px; + width: 100%; + text-align: center; + border: 2px solid var(--rpg-highlight); + border-top: none; + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.rpg-calendar-year { + font-size: 7px; + color: var(--rpg-text); + opacity: 0.7; + margin-top: 1px; +} + +/* Weather Widget Icon */ +.rpg-weather-icon { + font-size: clamp(16px, 3vw, 32px); + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5)); +} + +.rpg-weather-forecast { + font-size: 7px; + text-align: center; + margin-top: 2px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.2px; + opacity: 0.85; + line-height: 1; + word-wrap: break-word; + max-width: 100%; +} + +/* Temperature Widget - Thermometer */ +.rpg-temp-widget { + gap: 3px; +} + +.rpg-thermometer { + position: relative; + width: 20px; + height: 40px; + display: flex; + flex-direction: column; + align-items: center; +} + +.rpg-thermometer-tube { + position: relative; + width: 8px; + height: 28px; + background: rgba(255, 255, 255, 0.1); + border: 2px solid var(--rpg-border); + border-radius: 10px 10px 0 0; + overflow: hidden; + display: flex; + align-items: flex-end; +} + +.rpg-thermometer-fill { + width: 100%; + background: linear-gradient(to top, #e94560, #ff6b6b); + transition: height 0.5s ease; + border-radius: 8px 8px 0 0; +} + +.rpg-thermometer-bulb { + position: absolute; + bottom: 0; + width: 14px; + height: 14px; + background: var(--rpg-highlight); + border: 2px solid var(--rpg-border); + border-radius: 50%; + z-index: 1; +} + +.rpg-temp-value { + font-size: clamp(7px, 1.5vh, 9px); + font-weight: bold; + color: var(--rpg-text); + text-align: center; +} + +/* Clock Widget */ +.rpg-clock-widget { + gap: 3px; +} + +.rpg-clock { + width: 42px; + height: 42px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.4); + border: 3px solid var(--rpg-border); + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5); + position: relative; +} + +.rpg-clock-face { + width: 100%; + height: 100%; + position: relative; +} + +.rpg-clock-hour, +.rpg-clock-minute { + position: absolute; + background: var(--rpg-highlight); + transform-origin: bottom center; + left: 50%; + bottom: 50%; + border-radius: 2px 2px 0 0; +} + +.rpg-clock-hour { + width: 3px; + height: 12px; + margin-left: -1.5px; + opacity: 0.9; +} + +.rpg-clock-minute { + width: 2px; + height: 16px; + margin-left: -1px; +} + +.rpg-clock-center { + position: absolute; + width: 5px; + height: 5px; + background: var(--rpg-highlight); + border-radius: 50%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 2; +} + +.rpg-time-value { + font-size: 8px; + font-weight: bold; + color: var(--rpg-text); +} + +/* Location Widget - Map */ +.rpg-map-bg { + width: 100%; + height: 30px; + background: + linear-gradient(45deg, rgba(255,255,255,0.05) 25%, transparent 25%), + linear-gradient(-45deg, rgba(255,255,255,0.05) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.05) 75%), + linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.05) 75%); + background-size: 6px 6px; + background-position: 0 0, 0 3px, 3px -3px, -3px 0px; + background-color: rgba(0, 0, 0, 0.3); + border: 2px solid var(--rpg-border); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + flex-shrink: 0; + margin-bottom: 3px; +} + +.rpg-map-marker { + font-size: 16px; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8)); + animation: markerPulse 2s ease-in-out infinite; +} + +@keyframes markerPulse { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-2px); } +} + +.rpg-location-text { + font-size: clamp(7px, 1.4vh, 9px); + font-weight: bold; + color: var(--rpg-text); + text-align: center; + line-height: 1.2; + padding: 2px 4px; + margin: 0; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; +} + +/* Character Status Cards */ +.rpg-character-status { + display: flex; + flex-direction: column; + gap: 4px; + margin: 0; + padding: 0; + flex-shrink: 0; +} + +.rpg-character-status-card { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 6px; + background: rgba(0, 0, 0, 0.3); + border-left: 3px solid var(--rpg-highlight); + border-radius: 4px; + transition: all 0.2s ease; +} + +.rpg-character-status-card:hover { + background: rgba(0, 0, 0, 0.5); + transform: translateX(3px); +} + +.rpg-char-emoji { + font-size: clamp(14px, 2.5vh, 18px); + flex-shrink: 0; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5)); +} + +.rpg-char-details { + flex: 1; + min-width: 0; +} + +.rpg-char-name { + font-size: clamp(8px, 1.5vh, 10px); + font-weight: bold; + color: var(--rpg-highlight); + margin-bottom: 1px; +} + +.rpg-char-traits { + font-size: clamp(7px, 1.3vh, 9px); + color: var(--rpg-text); + opacity: 0.8; + line-height: 1.2; +}/* Old info line styles (legacy) */ +.rpg-info-line { + margin: 6px 0; + padding: 6px 10px; + background: rgba(0, 0, 0, 0.2); + border-left: 2px solid var(--rpg-highlight); + border-radius: 4px; + transition: all 0.3s ease; +} + +.rpg-info-line:hover { + background: rgba(0, 0, 0, 0.4); + transform: translateX(5px); +} + +/* ============================================ + CHARACTER THOUGHTS SECTION + ============================================ */ +.rpg-thoughts-section { + text-align: center; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; +} + +.rpg-thoughts-header { + font-size: 14px; + font-weight: bold; + margin-bottom: 10px; + color: var(--rpg-highlight); + text-shadow: 0 0 8px var(--rpg-highlight); + flex-shrink: 0; +} + +.rpg-thoughts-content { + position: relative; + padding: 8px; + border-radius: 8px; + border-left: 3px solid var(--rpg-highlight); + box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.3); + text-align: left; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + gap: clamp(6px, 1vh, 8px); + overflow-y: auto; + overflow-x: hidden; + /* Remove centering for multiple character cards */ +} + +/* Individual thought item */ +.rpg-thought-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 8px; + margin-bottom: 8px; + background: rgba(0, 0, 0, 0.3); + border-radius: 6px; + width: 100%; /* Ensure items take full width */ + box-sizing: border-box; /* Include padding in width calculation */ +} + +.rpg-thought-item:last-child { + margin-bottom: 0; +} + +/* Character avatar with thought bubbles */ +.rpg-thought-avatar { + position: relative; + flex-shrink: 0; +} + +.rpg-thought-avatar img { + width: clamp(30px, 5vh, 40px); + height: clamp(30px, 5vh, 40px); + border-radius: 50%; + border: 2px solid var(--rpg-highlight); + object-fit: cover; +} + +/* Thought bubbles - Left side, ascending size from bottom-left to top-right */ +.rpg-thought-bubbles { + position: absolute; + bottom: -4px; + left: -8px; + display: flex; + flex-direction: column; + gap: 2px; + align-items: flex-start; +} + +.rpg-bubble { + background: var(--rpg-highlight); + border-radius: 50%; + opacity: 0.8; +} + +.rpg-bubble-1 { + width: 8px; + height: 8px; +} + +.rpg-bubble-2 { + width: 6px; + height: 6px; + margin-left: 2px; +} + +/* Thought content */ +.rpg-thought-content { + flex: 1; + min-width: 0; +} + +.rpg-thought-name { + font-weight: bold; + color: var(--rpg-highlight); + font-size: clamp(8px, 1.5vh, 10px); + margin-bottom: 3px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.rpg-thought-name::before, +.rpg-thought-name::after { + content: none !important; +} + +.rpg-thought-text { + font-size: clamp(9px, 1.6vh, 11px); + font-style: italic; + line-height: 1.3; + color: var(--rpg-text); + opacity: 0.9; +} + +.rpg-thought-text::before { + content: ''; /* Explicitly ensure no emoji */ +} + +/* Overlay to fade the background portrait and provide contrast */ +.rpg-thoughts-content::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 135deg, + rgba(0, 0, 0, 0.85) 0%, + rgba(0, 0, 0, 0.75) 50%, + rgba(0, 0, 0, 0.85) 100% + ); + z-index: 1; + display: none; /* Hide background overlay for new format */ +} + +/* Content wrapper to sit above the background */ +.rpg-thoughts-overlay { + position: relative; + z-index: 2; + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 12px; + font-style: italic; + line-height: 1.4; +} + +/* Present Characters - Character Cards */ +.rpg-character-card { + display: flex; + align-items: flex-start; + gap: clamp(8px, 1vw, 12px); + padding: clamp(6px, 1vh, 8px); + background: rgba(0, 0, 0, 0.3); + border-radius: clamp(4px, 0.5vh, 6px); + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.2s ease; + width: 100%; /* Ensure cards take full width */ + box-sizing: border-box; /* Include padding and border in width calculation */ + flex-shrink: 0; /* Prevent cards from shrinking */ +} + +.rpg-character-card:hover { + background: rgba(0, 0, 0, 0.4); + border-color: var(--rpg-highlight); +} + +/* Character avatar container with relationship badge */ +.rpg-character-avatar { + position: relative; + flex-shrink: 0; +} + +.rpg-character-avatar img { + width: clamp(35px, 6vh, 45px); + height: clamp(35px, 6vh, 45px); + border-radius: 50%; + border: 2px solid var(--rpg-highlight); + object-fit: cover; + display: block; /* Prevent inline spacing issues */ +} + +/* Relationship badge in top-right corner */ +.rpg-relationship-badge { + position: absolute; + top: -2px; + right: -2px; + background: var(--rpg-bg); + border: 1px solid var(--rpg-highlight); + border-radius: 50%; + width: clamp(16px, 2.5vh, 20px); + height: clamp(16px, 2.5vh, 20px); + display: flex; + align-items: center; + justify-content: center; + font-size: clamp(8px, 1.2vh, 12px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +/* Character info section */ +.rpg-character-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: clamp(3px, 0.5vh, 5px); + overflow: hidden; /* Prevent content from overflowing */ +} + +/* Character header with emoji and name */ +.rpg-character-header { + display: flex; + align-items: center; + gap: clamp(4px, 0.5vw, 6px); + flex-wrap: nowrap; /* Prevent wrapping */ +} + +.rpg-character-emoji { + font-size: clamp(12px, 2vh, 16px); + flex-shrink: 0; +} + +.rpg-character-name { + font-weight: bold; + color: var(--rpg-highlight); + font-size: clamp(9px, 1.5vh, 12px); + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; /* Prevent name from wrapping */ + overflow: hidden; + text-overflow: ellipsis; +} + +/* Character traits/status line */ +.rpg-character-traits { + font-size: clamp(8px, 1.3vh, 10px); + color: var(--rpg-text); + opacity: 0.8; + line-height: 1.3; + overflow-wrap: break-word; /* Allow long text to wrap */ + word-wrap: break-word; +} + +/* Placeholder styles for empty sections */ +.rpg-thoughts-placeholder, +.rpg-placeholder-widget { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: clamp(12px, 2vh, 20px); + text-align: center; + opacity: 0.6; +} + +.rpg-placeholder-text { + font-size: clamp(10px, 1.6vh, 14px); + color: var(--rpg-text); + font-weight: bold; + margin-bottom: clamp(4px, 0.6vh, 6px); +} + +.rpg-placeholder-hint { + font-size: clamp(8px, 1.2vh, 10px); + color: var(--rpg-text); + opacity: 0.7; + font-style: italic; +} + +/* Editable field styles */ +.rpg-editable, +.rpg-editable-stat { + cursor: text; + transition: all 0.2s ease; + border-radius: 2px; + padding: 1px 2px; +} + +.rpg-editable:hover, +.rpg-editable-stat:hover { + background: var(--rpg-accent); + outline: 1px solid var(--rpg-highlight); +} + +.rpg-editable:focus, +.rpg-editable-stat:focus { + background: var(--rpg-bg); + outline: 2px solid var(--rpg-highlight); + box-shadow: 0 0 8px var(--rpg-highlight); +} + +/* Edit button container and styling */ +.rpg-edit-button-container { + display: flex; + justify-content: center; + padding: clamp(4px, 0.8vh, 8px); + margin-top: clamp(4px, 0.8vh, 8px); +} + +.rpg-edit-button { + background: var(--rpg-accent); + border: 1px solid var(--rpg-highlight); + color: var(--rpg-text); + padding: clamp(3px, 0.6vh, 6px) clamp(6px, 1.2vh, 12px); + border-radius: clamp(3px, 0.6vh, 6px); + font-size: clamp(7px, 1.2vh, 10px); + cursor: pointer; + display: flex; + align-items: center; + gap: clamp(2px, 0.4vh, 4px); + transition: all 0.2s ease; +} + +.rpg-edit-button:hover { + background: var(--rpg-highlight); + color: var(--rpg-bg); + transform: translateY(-1px); + box-shadow: 0 2px 8px var(--rpg-highlight); +} + +.rpg-edit-button:active { + transform: translateY(0); +} + +.rpg-edit-button i { + font-size: clamp(7px, 1.2vh, 10px); +} + +/* Removed emoji icon styling - no longer needed */ + +/* Settings Styling */ +.rpg-settings { + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid #444; +} + +.rpg-settings summary { + cursor: pointer; + font-weight: bold; + padding: 6px; + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; +} + +.rpg-settings summary:hover { + background: rgba(255, 255, 255, 0.1); +} + +.rpg-settings-content { + padding: 8px; +} + +.rpg-settings-content label { + display: block; + margin: 6px 0; +} + +.rpg-setting-row { + margin: 8px 0; +} + +.rpg-setting-row label { + display: block; + margin-bottom: 3px; + font-size: 11px; +} + +.rpg-setting-row input[type="number"] { + width: 100%; + padding: 4px; + border: 1px solid #444; + border-radius: 3px; + background: rgba(0, 0, 0, 0.3); + color: inherit; + font-size: 11px; +} + +.rpg-setting-row small { + display: block; + margin-top: 3px; + color: #888; + font-size: 10px; +} + +#rpg-manual-update { + width: 100%; + margin-top: 8px; + padding: 6px; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 12px; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .rpg-panel { + max-width: 100%; + } + + .rpg-stat-label { + min-width: 80px; + font-size: 12px; + } +} + +/* Animation for stats updates */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + +.rpg-stat-row.updating { + animation: pulse 0.5s ease-in-out; +} +/* ============================================ + SETTINGS SECTION + ============================================ */ +.rpg-settings { + margin-top: 15px; + padding-top: 15px; + border-top: 2px solid var(--rpg-border); +} + +.rpg-settings summary { + cursor: pointer; + font-weight: bold; + padding: 12px; + background: var(--rpg-accent); + border: 2px solid var(--rpg-border); + border-radius: 10px; + margin-bottom: 15px; + display: flex; + align-items: center; + gap: 10px; + transition: all 0.3s ease; +} + +.rpg-settings summary:hover { + background: var(--rpg-highlight); + border-color: var(--rpg-highlight); + transform: translateX(5px); +} + +.rpg-settings-content { + padding: 15px; + background: var(--rpg-accent); + border-radius: 10px; +} + +.rpg-settings-group { + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid var(--rpg-border); +} + +.rpg-settings-group:last-child { + border-bottom: none; +} + +.rpg-settings-group h4 { + margin: 0 0 12px 0; + font-size: 16px; + color: var(--rpg-highlight); + display: flex; + align-items: center; + gap: 8px; +} + +.rpg-setting-row { + margin: 12px 0; +} + +.rpg-setting-row label { + display: block; + margin-bottom: 6px; + font-size: 13px; + font-weight: 600; +} + +.rpg-select, +.rpg-input { + width: 100%; + padding: 8px; + border: 2px solid var(--rpg-border); + border-radius: 6px; + background: var(--rpg-bg); + color: var(--rpg-text); + font-size: 14px; + transition: all 0.3s ease; +} + +.rpg-select:focus, +.rpg-input:focus { + outline: none; + border-color: var(--rpg-highlight); + box-shadow: 0 0 10px var(--rpg-highlight); +} + +.rpg-setting-row input[type="color"] { + width: 100%; + height: 40px; + padding: 4px; + border: 2px solid var(--rpg-border); + border-radius: 6px; + background: var(--rpg-bg); + cursor: pointer; + transition: all 0.3s ease; +} + +.rpg-setting-row input[type="color"]:hover { + border-color: var(--rpg-highlight); +} + +.rpg-setting-row small { + display: block; + margin-top: 4px; + color: #999; + font-size: 11px; + font-style: italic; +} + +.rpg-custom-colors { + margin-top: 10px; + padding: 15px; + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; + border: 1px solid var(--rpg-border); +} + +.checkbox_label { + display: flex; + align-items: center; + gap: 8px; + margin: 10px 0; + cursor: pointer; + transition: all 0.3s ease; +} + +.checkbox_label:hover { + color: var(--rpg-highlight); +} + +.rpg-btn-primary { + width: 100%; + padding: 12px; + background: var(--rpg-accent); + border: 2px solid var(--rpg-border); + border-radius: 10px; + color: var(--rpg-text); + font-size: 15px; + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + transition: all 0.3s ease; + box-shadow: 0 4px 15px var(--rpg-shadow); +} + +.rpg-btn-primary:hover { + background: var(--rpg-highlight); + border-color: var(--rpg-highlight); + transform: translateY(-2px); + box-shadow: 0 6px 20px var(--rpg-shadow); +} + +.rpg-btn-primary:active { + transform: translateY(0); +} + +/* Clear Cache Button - Danger style */ +.rpg-btn-clear-cache { + width: 100%; + padding: 10px; + background: rgba(220, 53, 69, 0.2); + border: 2px solid rgba(220, 53, 69, 0.5); + border-radius: 8px; + color: #ff6b6b; + font-size: 13px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.3s ease; +} + +.rpg-btn-clear-cache:hover { + background: rgba(220, 53, 69, 0.3); + border-color: rgba(220, 53, 69, 0.8); + color: #ff8787; + transform: translateY(-1px); +} + +.rpg-btn-clear-cache:active { + transform: translateY(0); +} + +/* ============================================ + THEME VARIATIONS + ============================================ */ + +/* Sci-Fi / Synthwave Theme */ +.rpg-panel[data-theme="sci-fi"] { + --rpg-bg: #0a0e27; + --rpg-accent: #1a1f3a; + --rpg-text: #00fff9; + --rpg-highlight: #ff006e; + --rpg-border: #8b00ff; + --rpg-shadow: rgba(139, 0, 255, 0.5); +} + +/* Apply sci-fi theme to thought panel */ +#rpg-thought-panel[data-theme="sci-fi"], +#rpg-thought-icon[data-theme="sci-fi"] { + --rpg-bg: #0a0e27; + --rpg-accent: #1a1f3a; + --rpg-text: #00fff9; + --rpg-highlight: #ff006e; + --rpg-border: #8b00ff; + --rpg-shadow: rgba(139, 0, 255, 0.5); +} + +.rpg-panel[data-theme="sci-fi"] .rpg-content-box { + background: linear-gradient(135deg, #1a1f3a 0%, #0a0e27 100%); + box-shadow: 0 0 40px rgba(139, 0, 255, 0.4), inset 0 0 30px rgba(0, 0, 0, 0.5); + border: 2px solid #8b00ff; +} + +.rpg-panel[data-theme="sci-fi"] .rpg-panel-header h3, +.rpg-panel[data-theme="sci-fi"] .rpg-stats-title, +.rpg-panel[data-theme="sci-fi"] .rpg-info-header, +.rpg-panel[data-theme="sci-fi"] .rpg-thoughts-header { + text-shadow: 0 0 20px #ff006e, 0 0 40px #8b00ff; +} + +.rpg-panel[data-theme="sci-fi"] .rpg-divider { + background: linear-gradient(to right, transparent, #8b00ff, #ff006e, #8b00ff, transparent); +} + +.rpg-panel[data-theme="sci-fi"] .rpg-thoughts-content::before { + background: linear-gradient( + 135deg, + rgba(10, 14, 39, 0.9) 0%, + rgba(26, 31, 58, 0.8) 50%, + rgba(10, 14, 39, 0.9) 100% + ); +} + +/* Fantasy / Rustic Parchment Theme */ +.rpg-panel[data-theme="fantasy"] { + --rpg-bg: #2b1810; + --rpg-accent: #3d2414; + --rpg-text: #f4e8d0; + --rpg-highlight: #d4af37; + --rpg-border: #8b6914; + --rpg-shadow: rgba(0, 0, 0, 0.7); +} + +/* Apply fantasy theme to thought panel */ +#rpg-thought-panel[data-theme="fantasy"], +#rpg-thought-icon[data-theme="fantasy"] { + --rpg-bg: #2b1810; + --rpg-accent: #3d2414; + --rpg-text: #f4e8d0; + --rpg-highlight: #d4af37; + --rpg-border: #8b6914; + --rpg-shadow: rgba(0, 0, 0, 0.7); +} + +.rpg-panel[data-theme="fantasy"] { + background-image: + linear-gradient(rgba(43, 24, 16, 0.9), rgba(43, 24, 16, 0.9)), + url('data:image/svg+xml;utf8,'); +} + +.rpg-panel[data-theme="fantasy"] .rpg-content-box { + background: linear-gradient(135deg, #3d2414 0%, #2b1810 100%); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(139, 105, 20, 0.2); + border: 3px solid #8b6914; + border-style: ridge; +} + +.rpg-panel[data-theme="fantasy"] .rpg-panel-header h3, +.rpg-panel[data-theme="fantasy"] .rpg-stats-title, +.rpg-panel[data-theme="fantasy"] .rpg-info-header, +.rpg-panel[data-theme="fantasy"] .rpg-thoughts-header { + font-family: 'Georgia', serif; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); +} + +.rpg-panel[data-theme="fantasy"] .rpg-divider::after { + content: 'âĻ'; + font-size: 16px; +} + +.rpg-panel[data-theme="fantasy"] .rpg-thoughts-content::before { + background: linear-gradient( + 135deg, + rgba(43, 24, 16, 0.92) 0%, + rgba(61, 36, 20, 0.88) 50%, + rgba(43, 24, 16, 0.92) 100% + ); +} + +/* Cyberpunk / Neon Grid Theme */ +.rpg-panel[data-theme="cyberpunk"] { + --rpg-bg: #000000; + --rpg-accent: #0d0d0d; + --rpg-text: #00ff41; + --rpg-highlight: #ff2a6d; + --rpg-border: #05d9e8; + --rpg-shadow: rgba(5, 217, 232, 0.5); +} + +/* Apply cyberpunk theme to thought panel */ +#rpg-thought-panel[data-theme="cyberpunk"], +#rpg-thought-icon[data-theme="cyberpunk"] { + --rpg-bg: #000000; + --rpg-accent: #0d0d0d; + --rpg-text: #00ff41; + --rpg-highlight: #ff2a6d; + --rpg-border: #05d9e8; + --rpg-shadow: rgba(5, 217, 232, 0.5); +} + +.rpg-panel[data-theme="cyberpunk"] { + background: linear-gradient(rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0.95)), + repeating-linear-gradient(0deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px), + repeating-linear-gradient(90deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px); + background-size: 100% 100%, 30px 30px, 30px 30px; +} + +.rpg-panel[data-theme="cyberpunk"] .rpg-content-box { + background: linear-gradient(135deg, rgba(13, 13, 13, 0.9) 0%, rgba(0, 0, 0, 0.9) 100%); + box-shadow: 0 0 40px rgba(255, 42, 109, 0.4), inset 0 0 30px rgba(5, 217, 232, 0.2); + border: 2px solid #05d9e8; +} + +.rpg-panel[data-theme="cyberpunk"] .rpg-panel-header h3, +.rpg-panel[data-theme="cyberpunk"] .rpg-stats-title, +.rpg-panel[data-theme="cyberpunk"] .rpg-info-header, +.rpg-panel[data-theme="cyberpunk"] .rpg-thoughts-header { + text-shadow: 0 0 10px #ff2a6d, 0 0 20px #05d9e8, 0 0 30px #ff2a6d; + font-family: 'Courier New', monospace; + letter-spacing: 2px; +} + +.rpg-panel[data-theme="cyberpunk"] .rpg-divider { + background: linear-gradient(to right, transparent, #05d9e8, #ff2a6d, #05d9e8, transparent); + height: 3px; +} + +.rpg-panel[data-theme="cyberpunk"] .rpg-thoughts-content::before { + background: linear-gradient( + 135deg, + rgba(0, 0, 0, 0.92) 0%, + rgba(13, 13, 13, 0.85) 50%, + rgba(0, 0, 0, 0.92) 100% + ); +} + +/* ============================================ + RESPONSIVE DESIGN + ============================================ */ +@media (max-width: 768px) { + .rpg-panel { + width: 100%; + max-width: 100%; + } + + body:has(.rpg-panel) #sheld { + margin-right: 0; + } + + .rpg-user-portrait { + width: 60px; + height: 60px; + } + + .rpg-stats-title { + font-size: 16px; + } +} + +/* ============================================ + ANIMATIONS + ============================================ */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.rpg-section { + animation: fadeIn 0.5s ease-out; +} + +.rpg-stat-row { + animation: fadeIn 0.3s ease-out; + animation-fill-mode: both; +} + +.rpg-stat-row:nth-child(1) { animation-delay: 0.1s; } +.rpg-stat-row:nth-child(2) { animation-delay: 0.15s; } +.rpg-stat-row:nth-child(3) { animation-delay: 0.2s; } +.rpg-stat-row:nth-child(4) { animation-delay: 0.25s; } +.rpg-stat-row:nth-child(5) { animation-delay: 0.3s; } + +/* ============================================ + DICE ROLL POPUP + ============================================ */ +.rpg-dice-popup { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; +} + +.rpg-dice-popup-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); +} + +.rpg-dice-popup-content { + position: relative; + width: 90%; + max-width: 500px; + background: var(--rpg-bg); + border: 3px solid var(--rpg-border); + border-radius: 15px; + box-shadow: 0 10px 50px rgba(0, 0, 0, 0.9); + overflow: hidden; + animation: popupSlideIn 0.3s ease-out; + color: var(--rpg-text); +} + +@keyframes popupSlideIn { + from { + opacity: 0; + transform: translateY(-50px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.rpg-dice-popup-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + background: var(--rpg-accent); + border-bottom: 2px solid var(--rpg-border); +} + +.rpg-dice-popup-header h3 { + margin: 0; + font-size: 20px; + color: var(--rpg-highlight); + display: flex; + align-items: center; + gap: 10px; +} + +.rpg-dice-popup-body { + padding: 25px; +} + +.rpg-dice-selector-container { + padding: 20px; + background: rgba(0, 0, 0, 0.3); + border-radius: 12px; + border: 2px solid var(--rpg-border); + margin-bottom: 20px; +} + +.rpg-dice-selector { + display: flex; + gap: 15px; + margin-bottom: 15px; +} + +.rpg-dice-input-group { + flex: 1; +} + +.rpg-dice-input-group label { + display: block; + margin-bottom: 8px; + font-size: 13px; + font-weight: bold; + color: var(--rpg-text); +} + +.rpg-dice-input-group input, +.rpg-dice-input-group select { + width: 100%; + padding: 10px; + border: 2px solid var(--rpg-border); + border-radius: 8px; + background: var(--rpg-accent); + color: var(--rpg-text); + font-size: 16px; + font-weight: bold; + text-align: center; + transition: all 0.3s ease; +} + +.rpg-dice-input-group input:focus, +.rpg-dice-input-group select:focus { + outline: none; + border-color: var(--rpg-highlight); + box-shadow: 0 0 10px var(--rpg-highlight); + background: rgba(0, 0, 0, 0.5); +} + +#rpg-dice-roll-btn { + width: 100%; + padding: 12px 20px; + background: linear-gradient(135deg, var(--rpg-highlight), var(--rpg-accent)); + border: 2px solid var(--rpg-highlight); + border-radius: 10px; + color: var(--rpg-text); + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +#rpg-dice-roll-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px var(--rpg-highlight); + background: var(--rpg-highlight); +} + +#rpg-dice-roll-btn:active { + transform: translateY(0); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); +} + +.rpg-dice-animation { + text-align: center; + padding: 40px 20px; +} + +.rpg-dice-rolling i { + font-size: 80px; + color: var(--rpg-highlight); + animation: diceRoll 0.8s ease-in-out infinite; +} + +@keyframes diceRoll { + 0%, 100% { + transform: rotate(0deg) scale(1); + } + 25% { + transform: rotate(90deg) scale(1.1); + } + 50% { + transform: rotate(180deg) scale(1); + } + 75% { + transform: rotate(270deg) scale(1.1); + } +} + +.rpg-dice-rolling-text { + margin-top: 20px; + font-size: 18px; + font-weight: bold; + color: var(--rpg-highlight); + animation: pulseGlow 1s ease-in-out infinite; +} + +.rpg-dice-result { + text-align: center; + padding: 30px 20px; + background: rgba(0, 0, 0, 0.3); + border-radius: 12px; + border: 2px solid var(--rpg-border); +} + +.rpg-dice-result-label { + font-size: 14px; + color: var(--rpg-text); + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.rpg-dice-result-value { + font-size: 60px; + font-weight: bold; + color: var(--rpg-highlight); + text-shadow: 0 0 20px var(--rpg-highlight); + animation: resultPop 0.5s ease-out; +} + +@keyframes resultPop { + 0% { + transform: scale(0); + opacity: 0; + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +.rpg-dice-result-details { + margin-top: 15px; + font-size: 14px; + color: var(--rpg-text); + opacity: 0.8; +} + +.rpg-dice-save-btn { + margin-top: 20px; + width: 100%; + max-width: 200px; + padding: 10px 20px; + background: linear-gradient(135deg, #28a745, #20c997); + border: 2px solid #28a745; + border-radius: 10px; + color: white; + font-size: 15px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(40, 167, 69, 0.4); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.rpg-dice-save-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(40, 167, 69, 0.6); + background: #28a745; +} + +.rpg-dice-save-btn:active { + transform: translateY(0); + box-shadow: 0 2px 10px rgba(40, 167, 69, 0.4); +} + +/* Theme-specific popup styles */ +.rpg-dice-popup[data-theme="sci-fi"] .rpg-dice-popup-content { + --rpg-bg: #0a0e27; + --rpg-accent: #1a1f3a; + --rpg-text: #00fff9; + --rpg-highlight: #ff006e; + --rpg-border: #00fff9; +} + +.rpg-dice-popup[data-theme="fantasy"] .rpg-dice-popup-content { + --rpg-bg: #2c1810; + --rpg-accent: #3d2817; + --rpg-text: #f4e8d0; + --rpg-highlight: #d4af37; + --rpg-border: #8b7355; +} + +.rpg-dice-popup[data-theme="cyberpunk"] .rpg-dice-popup-content { + --rpg-bg: #0d0221; + --rpg-accent: #1a0b2e; + --rpg-text: #00ff9f; + --rpg-highlight: #ff00ff; + --rpg-border: #ff00ff; +} + +/* ============================================ + HTML PROMPT TOGGLE + ============================================ */ +.rpg-toggle-container { + padding: 8px; + background: rgba(0, 0, 0, 0.2); + border-radius: 5px; + margin: 0 0 8px 0; +} + +.rpg-toggle-label { + display: flex; + align-items: center; + cursor: pointer; + user-select: none; +} + +.rpg-toggle-label input[type="checkbox"] { + margin: 0 8px 0 0; + cursor: pointer; +} + +.rpg-toggle-label i { + margin-right: 6px; +} + +/* ============================================ + MANUAL UPDATE BUTTON + ============================================ */ +.rpg-manual-update-btn { + width: 100%; + padding: 8px; + background: linear-gradient(135deg, var(--rpg-highlight), var(--rpg-accent)); + border: 2px solid var(--rpg-highlight); + border-radius: 8px; + color: var(--rpg-text); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); + flex-shrink: 0; + margin: 0 0 8px 0; +} + +.rpg-manual-update-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px var(--rpg-highlight); + background: var(--rpg-highlight); +} + +.rpg-manual-update-btn:active { + transform: translateY(0); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); +} + +/* ============================================ + SETTINGS BUTTON + ============================================ */ +.rpg-btn-settings { + width: 100%; + margin: 0; + padding: 8px; + background: var(--rpg-accent); + border: 1px solid var(--rpg-border); + border-radius: 6px; + color: var(--rpg-text); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + flex-shrink: 0; +} + +.rpg-btn-settings:hover { + background: var(--rpg-highlight); + border-color: var(--rpg-highlight); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +.rpg-btn-settings:active { + transform: translateY(0); +} + +/* ============================================ + SETTINGS POPUP + ============================================ */ +.rpg-settings-popup { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; +} + +.rpg-settings-popup-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); +} + +.rpg-settings-popup-content { + position: relative; + background: var(--rpg-bg); + border: 3px solid var(--rpg-border); + border-radius: 15px; + max-width: 500px; + width: 90%; + max-height: 80vh; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 10px 50px rgba(0, 0, 0, 0.9); + animation: popupSlideIn 0.3s ease-out; + color: var(--rpg-text); +} + +.rpg-settings-popup-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 15px 20px; + background: var(--rpg-accent); + border-bottom: 1px solid var(--rpg-border); +} + +.rpg-settings-popup-header h3 { + margin: 0; + font-size: 18px; + color: var(--rpg-highlight); + display: flex; + align-items: center; + gap: 10px; +} + +.rpg-popup-close { + background: transparent; + border: none; + color: var(--rpg-text); + font-size: 24px; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s ease; +} + +.rpg-popup-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--rpg-highlight); +} + +.rpg-settings-popup-body { + padding: 20px; + overflow-y: auto; + flex: 1; +} + +.rpg-settings-group { + margin-bottom: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--rpg-border); +} + +.rpg-settings-group:last-child { + border-bottom: none; +} + +.rpg-settings-group h4 { + margin: 0 0 15px 0; + font-size: 15px; + color: var(--rpg-highlight); + display: flex; + align-items: center; + gap: 8px; +} + +/* Apply theme to settings popup */ +#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; +} + +#rpg-settings-popup[data-theme="fantasy"] .rpg-settings-popup-content { + --rpg-bg: #2b1810; + --rpg-accent: #3d2516; + --rpg-text: #f4e4c1; + --rpg-highlight: #d4af37; + --rpg-border: #8b6914; +} + +#rpg-settings-popup[data-theme="cyberpunk"] .rpg-settings-popup-content { + --rpg-bg: #0d0221; + --rpg-accent: #1a0b2e; + --rpg-text: #00ff9f; + --rpg-highlight: #ff00ff; + --rpg-border: #ff00ff; +} + +/* ============================================ + CHAT THOUGHT OVERLAYS + ============================================ */ + +/* Container for thought overlay on chat messages */ +/* Floating thought panel - positioned next to character avatar */ +#rpg-thought-panel { + position: fixed; + z-index: 1000; /* Lower z-index to stay below dropdown menus */ + pointer-events: auto; + max-width: 350px; + transform: translateY(-50%); + animation: thoughtPanelFadeIn 0.4s ease-out; +} + +/* Close button */ +.rpg-thought-close { + position: absolute; + top: -8px; + right: -8px; + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--rpg-highlight, #e94560); + color: white; + border: 2px solid var(--rpg-bg, rgba(30, 30, 50, 0.95)); + font-size: 18px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + z-index: 10001; + transition: all 0.2s ease; +} + +.rpg-thought-close:hover { + transform: scale(1.1); + background: var(--rpg-text, #eaeaea); + color: var(--rpg-highlight, #e94560); +} + +/* Collapsed thought icon */ +#rpg-thought-icon { + position: fixed; + z-index: 1000; /* Lower z-index to stay below dropdown menus */ + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--rpg-bg, rgba(30, 30, 50, 0.95)); + border: 2px solid var(--rpg-highlight, #e94560); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + cursor: pointer; + animation: thoughtIconPulse 2s ease-in-out infinite; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(10px); +} + +#rpg-thought-icon:hover { + transform: scale(1.1); + animation: none; +} + +@keyframes thoughtIconPulse { + 0%, 100% { + transform: scale(1); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5); + } + 50% { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(233, 69, 96, 0.3); + } +} + +/* Thought circles floating from avatar */ +.rpg-thought-circles { + position: absolute; + display: flex; + flex-direction: column-reverse; /* Reverse so circles go upward */ + align-items: flex-end; /* Align to the right side */ + gap: 12px; + z-index: 1; +} + +.rpg-thought-circle { + background: var(--rpg-bg, rgba(30, 30, 50, 0.8)); + border: 2px solid var(--rpg-highlight, #e94560); + border-radius: 50%; + animation: thoughtCirclePulse 1.5s ease-in-out infinite; +} + +.rpg-circle-1 { + width: 8px; + height: 8px; + animation-delay: 0s; + align-self: flex-end; /* Circle 1 on the far right (at avatar) */ +} + +.rpg-circle-2 { + width: 12px; + height: 12px; + animation-delay: 0.2s; + align-self: flex-end; + margin-right: 4px; /* Move slightly left from circle 1 */ +} + +.rpg-circle-3 { + width: 16px; + height: 16px; + animation-delay: 0.4s; + align-self: flex-end; + margin-right: 8px; /* Move more left from circle 1 */ +} + +/* Thought bubble main container */ +.rpg-thought-bubble { + background: var(--rpg-bg, rgba(30, 30, 50, 0.95)); + border: 2px solid var(--rpg-highlight, #e94560); + border-radius: clamp(10px, 1.5vh, 14px); + padding: clamp(10px, 1.5vh, 14px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + position: relative; + backdrop-filter: blur(15px); + max-height: 60vh; + overflow-y: auto; + overflow-x: hidden; +} + +/* Custom scrollbar for thought bubble */ +.rpg-thought-bubble::-webkit-scrollbar { + width: 6px; +} + +.rpg-thought-bubble::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; +} + +.rpg-thought-bubble::-webkit-scrollbar-thumb { + background: var(--rpg-highlight, #e94560); + border-radius: 3px; + opacity: 0.5; +} + +.rpg-thought-bubble::-webkit-scrollbar-thumb:hover { + background: var(--rpg-highlight, #e94560); + opacity: 0.8; +} + +/* Individual thought item (for multiple characters) */ +.rpg-thought-item { + display: flex; + gap: clamp(10px, 1.5vh, 12px); + align-items: flex-start; +} + +/* Emoji box on the left */ +.rpg-thought-emoji-box { + flex-shrink: 0; + width: clamp(32px, 4vh, 40px); + height: clamp(32px, 4vh, 40px); + display: flex; + align-items: center; + justify-content: center; + background: var(--rpg-accent, rgba(50, 50, 70, 0.8)); + border: 1px solid var(--rpg-highlight, #e94560); + border-radius: clamp(6px, 1vh, 8px); + font-size: clamp(16px, 2vh, 20px); +} + +/* Thought content on the right */ +.rpg-thought-content { + flex: 1; + font-size: clamp(10px, 1.4vh, 12px); + line-height: 1.5; + color: var(--rpg-text, #eaeaea); + font-style: italic; + opacity: 0.95; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; +} + +/* Divider between multiple thoughts */ +.rpg-thought-divider { + height: 1px; + background: linear-gradient(to right, transparent, var(--rpg-highlight, #e94560), transparent); + margin: clamp(8px, 1vh, 10px) 0; + opacity: 0.5; +} + +/* Arrow pointing right (when panel is on left) */ +#rpg-thought-panel.rpg-thought-panel-left .rpg-thought-bubble::after { + content: ''; + position: absolute; + top: 50%; + right: -12px; + transform: translateY(-50%); + width: 0; + height: 0; + border-style: solid; + border-width: 10px 0 10px 12px; + border-color: transparent transparent transparent var(--rpg-highlight, #e94560); +} + +#rpg-thought-panel.rpg-thought-panel-left .rpg-thought-bubble::before { + content: ''; + position: absolute; + top: 50%; + right: -9px; + transform: translateY(-50%); + width: 0; + height: 0; + border-style: solid; + border-width: 8px 0 8px 10px; + border-color: transparent transparent transparent var(--rpg-bg, rgba(30, 30, 50, 0.95)); + z-index: 1; +} + +/* Arrow pointing left (when panel is on right) */ +#rpg-thought-panel.rpg-thought-panel-right .rpg-thought-bubble::after { + content: ''; + position: absolute; + top: 50%; + left: -12px; + transform: translateY(-50%); + width: 0; + height: 0; + border-style: solid; + border-width: 10px 12px 10px 0; + border-color: transparent var(--rpg-highlight, #e94560) transparent transparent; +} + +#rpg-thought-panel.rpg-thought-panel-right .rpg-thought-bubble::before { + content: ''; + position: absolute; + top: 50%; + left: -9px; + transform: translateY(-50%); + width: 0; + height: 0; + border-style: solid; + border-width: 8px 10px 8px 0; + border-color: transparent var(--rpg-bg, rgba(30, 30, 50, 0.95)) transparent transparent; + z-index: 1; +} + +/* Animations */ +@keyframes thoughtPanelFadeIn { + from { + opacity: 0; + transform: translateY(-50%) scale(0.9); + } + to { + opacity: 1; + transform: translateY(-50%) scale(1); + } +} + +@keyframes thoughtCirclePulse { + 0%, 100% { + opacity: 0.3; + transform: scale(0.8); + } + 50% { + opacity: 0.8; + transform: scale(1.1); + } +} + +/* Responsive positioning for mobile/narrow screens */ +@media (max-width: 768px) { + #rpg-thought-panel { + position: fixed !important; + max-width: 90vw !important; + left: 50% !important; + transform: translateX(-50%) translateY(-50%) !important; + } + + .rpg-thought-circles { + display: none !important; + } + + #rpg-thought-panel .rpg-thought-bubble::after, + #rpg-thought-panel .rpg-thought-bubble::before { + display: none !important; + } + + .rpg-thought-content { + font-size: clamp(9px, 1.2vh, 11px); + } +} + diff --git a/template.html b/template.html new file mode 100644 index 0000000..ce90fbe --- /dev/null +++ b/template.html @@ -0,0 +1,262 @@ +
+ +
+ +
+

+ + RPG Companion +

+
+ +
+ +
+ + Last Roll: None + +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + +
+ + +
+ +
+
+ + +
+ +
+ + + + + + +
+
+
+ + + + + +