Compare commits

..

1 Commits

Author SHA1 Message Date
Spicy_Marinara fd8afba7f2 v3.6.1: Dynamic combat actions and bug fixes
- Added dynamic action updates: AI can now modify available attacks/items based on combat state
- Items decrease when used, abilities change based on status effects
- Fixed event delegation for encounter buttons to work reliably on mobile
- Fixed multiple JSON parsing validation errors
- Added proper dialogue handling in combat summaries
- UI now re-renders action buttons when actions change
- Improved prompt instructions for item quantities and dynamic actions
2026-01-13 19:21:49 +01:00
30 changed files with 488 additions and 1579 deletions
+6 -4
View File
@@ -7,12 +7,14 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New ## 🆕 What's New
### v3.7.2 ### v3.6.1
- Minor bug fixes - Fixed the bugs in the encounter system where you couldn't use the buttons after performing any custom action.
- Improved combat actions and made them dynamic, depending on the current situation.
- Added Russian as a supported language.
**Special thanks to all the other contributors for this project:** **Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein! Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
## 📥 Installation ## 📥 Installation
@@ -265,7 +267,7 @@ If you enjoy this extension, consider supporting development:
## 🙏 Credits ## 🙏 Credits
**Contributors:** **Contributors:**
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein. SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
## 🚀 Planned Features ## 🚀 Planned Features
+3 -101
View File
@@ -1,5 +1,5 @@
import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings } from '../../../extensions.js'; import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings } from '../../../extensions.js';
import { eventSource, event_types, substituteParams, chat, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate, getRequestHeaders } from '../../../../script.js'; import { eventSource, event_types, substituteParams, chat, generateRaw, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate, getRequestHeaders } from '../../../../script.js';
import { selected_group, getGroupMembers } from '../../../group-chats.js'; import { selected_group, getGroupMembers } from '../../../group-chats.js';
import { power_user } from '../../../power-user.js'; import { power_user } from '../../../power-user.js';
@@ -151,6 +151,7 @@ import {
onMessageReceived, onMessageReceived,
onCharacterChanged, onCharacterChanged,
onMessageSwiped, onMessageSwiped,
onMessageDeleted,
updatePersonaAvatar, updatePersonaAvatar,
clearExtensionPrompts, clearExtensionPrompts,
onGenerationEnded, onGenerationEnded,
@@ -384,11 +385,6 @@ async function initUI() {
saveSettings(); saveSettings();
}); });
$('#rpg-toggle-omniscience').on('change', function() {
extensionSettings.enableOmniscienceFilter = $(this).prop('checked');
saveSettings();
});
$('#rpg-toggle-cyoa').on('change', function() { $('#rpg-toggle-cyoa').on('change', function() {
extensionSettings.enableCYOA = $(this).prop('checked'); extensionSettings.enableCYOA = $(this).prop('checked');
saveSettings(); saveSettings();
@@ -577,12 +573,6 @@ async function initUI() {
updateFeatureTogglesVisibility(); updateFeatureTogglesVisibility();
}); });
$('#rpg-toggle-show-omniscience-toggle').on('change', function() {
extensionSettings.showOmniscienceToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-cyoa-toggle').on('change', function() { $('#rpg-toggle-show-cyoa-toggle').on('change', function() {
extensionSettings.showCYOAToggle = $(this).prop('checked'); extensionSettings.showCYOAToggle = $(this).prop('checked');
saveSettings(); saveSettings();
@@ -816,30 +806,12 @@ async function initUI() {
renderUserStats(); // Re-render with new colors renderUserStats(); // Re-render with new colors
}); });
$('#rpg-stat-bar-color-low-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.statBarColorLowOpacity = opacity;
$('#rpg-stat-bar-color-low-opacity-value').text(opacity + '%');
renderUserStats();
}).on('change', function() {
saveSettings();
});
$('#rpg-stat-bar-color-high').on('change', function() { $('#rpg-stat-bar-color-high').on('change', function() {
extensionSettings.statBarColorHigh = String($(this).val()); extensionSettings.statBarColorHigh = String($(this).val());
saveSettings(); saveSettings();
renderUserStats(); // Re-render with new colors renderUserStats(); // Re-render with new colors
}); });
$('#rpg-stat-bar-color-high-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.statBarColorHighOpacity = opacity;
$('#rpg-stat-bar-color-high-opacity-value').text(opacity + '%');
renderUserStats();
}).on('change', function() {
saveSettings();
});
// Theme selection // Theme selection
$('#rpg-theme-select').on('change', function() { $('#rpg-theme-select').on('change', function() {
extensionSettings.theme = String($(this).val()); extensionSettings.theme = String($(this).val());
@@ -861,19 +833,6 @@ async function initUI() {
} }
}); });
$('#rpg-custom-bg-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.bgOpacity = opacity;
$('#rpg-custom-bg-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-accent').on('change', function() { $('#rpg-custom-accent').on('change', function() {
extensionSettings.customColors.accent = String($(this).val()); extensionSettings.customColors.accent = String($(this).val());
saveSettings(); saveSettings();
@@ -884,19 +843,6 @@ async function initUI() {
} }
}); });
$('#rpg-custom-accent-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.accentOpacity = opacity;
$('#rpg-custom-accent-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-text').on('change', function() { $('#rpg-custom-text').on('change', function() {
extensionSettings.customColors.text = String($(this).val()); extensionSettings.customColors.text = String($(this).val());
saveSettings(); saveSettings();
@@ -907,19 +853,6 @@ async function initUI() {
} }
}); });
$('#rpg-custom-text-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.textOpacity = opacity;
$('#rpg-custom-text-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-highlight').on('change', function() { $('#rpg-custom-highlight').on('change', function() {
extensionSettings.customColors.highlight = String($(this).val()); extensionSettings.customColors.highlight = String($(this).val());
saveSettings(); saveSettings();
@@ -930,19 +863,6 @@ async function initUI() {
} }
}); });
$('#rpg-custom-highlight-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.highlightOpacity = opacity;
$('#rpg-custom-highlight-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
// External API settings event handlers // External API settings event handlers
$('#rpg-external-base-url').on('change', function() { $('#rpg-external-base-url').on('change', function() {
if (!extensionSettings.externalApiSettings) { if (!extensionSettings.externalApiSettings) {
@@ -1050,7 +970,6 @@ async function initUI() {
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt); $('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
$('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring); $('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring);
$('#rpg-toggle-deception').prop('checked', extensionSettings.enableDeceptionSystem ?? false); $('#rpg-toggle-deception').prop('checked', extensionSettings.enableDeceptionSystem ?? false);
$('#rpg-toggle-omniscience').prop('checked', extensionSettings.enableOmniscienceFilter ?? false);
$('#rpg-toggle-cyoa').prop('checked', extensionSettings.enableCYOA ?? false); $('#rpg-toggle-cyoa').prop('checked', extensionSettings.enableCYOA ?? false);
$('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic); $('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic);
@@ -1061,7 +980,6 @@ async function initUI() {
$('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true); $('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true);
$('#rpg-toggle-show-dialogue-coloring-toggle').prop('checked', extensionSettings.showDialogueColoringToggle ?? true); $('#rpg-toggle-show-dialogue-coloring-toggle').prop('checked', extensionSettings.showDialogueColoringToggle ?? true);
$('#rpg-toggle-show-deception-toggle').prop('checked', extensionSettings.showDeceptionToggle ?? true); $('#rpg-toggle-show-deception-toggle').prop('checked', extensionSettings.showDeceptionToggle ?? true);
$('#rpg-toggle-show-omniscience-toggle').prop('checked', extensionSettings.showOmniscienceToggle ?? true);
$('#rpg-toggle-show-cyoa-toggle').prop('checked', extensionSettings.showCYOAToggle ?? true); $('#rpg-toggle-show-cyoa-toggle').prop('checked', extensionSettings.showCYOAToggle ?? true);
$('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true); $('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true);
$('#rpg-toggle-show-dynamic-weather-toggle').prop('checked', extensionSettings.showDynamicWeatherToggle ?? true); $('#rpg-toggle-show-dynamic-weather-toggle').prop('checked', extensionSettings.showDynamicWeatherToggle ?? true);
@@ -1124,29 +1042,12 @@ async function initUI() {
$('#rpg-strip-widget-options').toggle(stripWidgets.enabled || false); $('#rpg-strip-widget-options').toggle(stripWidgets.enabled || false);
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow); $('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
$('#rpg-stat-bar-color-low-opacity').val(extensionSettings.statBarColorLowOpacity ?? 100);
$('#rpg-stat-bar-color-low-opacity-value').text((extensionSettings.statBarColorLowOpacity ?? 100) + '%');
$('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh); $('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh);
$('#rpg-stat-bar-color-high-opacity').val(extensionSettings.statBarColorHighOpacity ?? 100);
$('#rpg-stat-bar-color-high-opacity-value').text((extensionSettings.statBarColorHighOpacity ?? 100) + '%');
$('#rpg-theme-select').val(extensionSettings.theme); $('#rpg-theme-select').val(extensionSettings.theme);
$('#rpg-custom-bg').val(extensionSettings.customColors.bg); $('#rpg-custom-bg').val(extensionSettings.customColors.bg);
$('#rpg-custom-bg-opacity').val(extensionSettings.customColors.bgOpacity ?? 100);
$('#rpg-custom-bg-opacity-value').text((extensionSettings.customColors.bgOpacity ?? 100) + '%');
$('#rpg-custom-accent').val(extensionSettings.customColors.accent); $('#rpg-custom-accent').val(extensionSettings.customColors.accent);
$('#rpg-custom-accent-opacity').val(extensionSettings.customColors.accentOpacity ?? 100);
$('#rpg-custom-accent-opacity-value').text((extensionSettings.customColors.accentOpacity ?? 100) + '%');
$('#rpg-custom-text').val(extensionSettings.customColors.text); $('#rpg-custom-text').val(extensionSettings.customColors.text);
$('#rpg-custom-text-opacity').val(extensionSettings.customColors.textOpacity ?? 100);
$('#rpg-custom-text-opacity-value').text((extensionSettings.customColors.textOpacity ?? 100) + '%');
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight); $('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
$('#rpg-custom-highlight-opacity').val(extensionSettings.customColors.highlightOpacity ?? 100);
$('#rpg-custom-highlight-opacity-value').text((extensionSettings.customColors.highlightOpacity ?? 100) + '%');
// Initialize External API settings values // Initialize External API settings values
if (extensionSettings.externalApiSettings) { if (extensionSettings.externalApiSettings) {
@@ -1354,6 +1255,7 @@ jQuery(async () => {
[event_types.GENERATION_ENDED]: onGenerationEnded, [event_types.GENERATION_ENDED]: onGenerationEnded,
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts], [event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts],
[event_types.MESSAGE_SWIPED]: onMessageSwiped, [event_types.MESSAGE_SWIPED]: onMessageSwiped,
[event_types.MESSAGE_DELETED]: onMessageDeleted,
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar, [event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar [event_types.SETTINGS_UPDATED]: updatePersonaAvatar
}); });
+1 -1
View File
@@ -6,6 +6,6 @@
"js": "index.js", "js": "index.js",
"css": "style.css", "css": "style.css",
"author": "Marinara", "author": "Marinara",
"version": "3.7.2", "version": "3.6.1",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
} }
+2 -3
View File
@@ -15,7 +15,6 @@
<select id="rpg-companion-language-select" class="text_pole"> <select id="rpg-companion-language-select" class="text_pole">
<option value="en" data-i18n-key="settings.language.option.en">English</option> <option value="en" data-i18n-key="settings.language.option.en">English</option>
<option value="zh-tw" data-i18n-key="settings.language.option.zh-tw">繁體中文</option> <option value="zh-tw" data-i18n-key="settings.language.option.zh-tw">繁體中文</option>
<option value="ru" data-i18n-key="settings.language.option.ru">Русский</option>
</select> </select>
</div> </div>
@@ -44,12 +43,12 @@
<i class="fa-solid fa-users"></i> <strong>Contributors:</strong> <i class="fa-solid fa-users"></i> <strong>Contributors:</strong>
</div> </div>
<div style="opacity: 0.8; font-size: 0.9em;"> <div style="opacity: 0.8; font-size: 0.9em;">
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein. SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
</div> </div>
</div> </div>
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;"> <div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
v3.7.2 v3.6.1
</div> </div>
</div> </div>
</div> </div>
-16
View File
@@ -118,22 +118,6 @@ export function loadSettings() {
settingsChanged = true; settingsChanged = true;
} }
// Migration to version 5: Add opacity properties for all colors
if (currentVersion < 5) {
// console.log('[RPG Companion] Migrating settings to version 5 (adding color opacity)');
if (!extensionSettings.customColors) {
extensionSettings.customColors = {};
}
if (extensionSettings.customColors.bgOpacity === undefined) extensionSettings.customColors.bgOpacity = 100;
if (extensionSettings.customColors.accentOpacity === undefined) extensionSettings.customColors.accentOpacity = 100;
if (extensionSettings.customColors.textOpacity === undefined) extensionSettings.customColors.textOpacity = 100;
if (extensionSettings.customColors.highlightOpacity === undefined) extensionSettings.customColors.highlightOpacity = 100;
if (extensionSettings.statBarColorLowOpacity === undefined) extensionSettings.statBarColorLowOpacity = 100;
if (extensionSettings.statBarColorHighOpacity === undefined) extensionSettings.statBarColorHighOpacity = 100;
extensionSettings.settingsVersion = 5;
settingsChanged = true;
}
// Save migrated settings // Save migrated settings
if (settingsChanged) { if (settingsChanged) {
saveSettings(); saveSettings();
+1 -11
View File
@@ -23,15 +23,12 @@ export let extensionSettings = {
showThoughtsInChat: true, // Show thoughts overlay in chat showThoughtsInChat: true, // Show thoughts overlay in chat
narratorMode: false, // Use character card as narrator instead of fixed character references narratorMode: false, // Use character card as narrator instead of fixed character references
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default) customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default)
enableHtmlPrompt: false, // Enable immersive HTML prompt injection enableHtmlPrompt: false, // Enable immersive HTML prompt injection
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default) customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
enableDialogueColoring: false, // Enable dialogue coloring prompt injection enableDialogueColoring: false, // Enable dialogue coloring prompt injection
customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default) customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default)
enableDeceptionSystem: false, // Enable deception tracking with <lie> tags enableDeceptionSystem: false, // Enable deception tracking with <lie> tags
customDeceptionPrompt: '', // Custom deception prompt text (empty = use default) customDeceptionPrompt: '', // Custom deception prompt text (empty = use default)
enableOmniscienceFilter: false, // Enable omniscience filter with <filter> tags
customOmnisciencePrompt: '', // Custom omniscience filter prompt text (empty = use default)
enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices
customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default) customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default)
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs) enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
@@ -44,7 +41,6 @@ export let extensionSettings = {
showHtmlToggle: true, // Show Immersive HTML toggle in main panel showHtmlToggle: true, // Show Immersive HTML toggle in main panel
showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default) showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default)
showDeceptionToggle: true, // Show Deception System toggle in main panel showDeceptionToggle: true, // Show Deception System toggle in main panel
showOmniscienceToggle: true, // Show Omniscience Filter toggle in main panel
showCYOAToggle: true, // Show CYOA toggle in main panel showCYOAToggle: true, // Show CYOA toggle in main panel
showSpotifyToggle: true, // Show Spotify Music toggle in main panel showSpotifyToggle: true, // Show Spotify Music toggle in main panel
@@ -66,18 +62,12 @@ export let extensionSettings = {
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
customColors: { customColors: {
bg: '#1a1a2e', bg: '#1a1a2e',
bgOpacity: 100,
accent: '#16213e', accent: '#16213e',
accentOpacity: 100,
text: '#eaeaea', text: '#eaeaea',
textOpacity: 100, highlight: '#e94560'
highlight: '#e94560',
highlightOpacity: 100
}, },
statBarColorLow: '#cc3333', // Color for low stat values (red) statBarColorLow: '#cc3333', // Color for low stat values (red)
statBarColorLowOpacity: 100,
statBarColorHigh: '#33cc66', // Color for high stat values (green) statBarColorHigh: '#33cc66', // Color for high stat values (green)
statBarColorHighOpacity: 100,
enableAnimations: true, // Enable smooth animations for stats and content updates enableAnimations: true, // Enable smooth animations for stats and content updates
mobileFabPosition: { mobileFabPosition: {
top: 'calc(var(--topBarBlockSize) + 60px)', top: 'calc(var(--topBarBlockSize) + 60px)',
+1 -7
View File
@@ -52,10 +52,6 @@
"template.settingsModal.display.showImmersiveHtmlToggleNote": "Display a toggle button to enable/disable HTML formatting in messages.", "template.settingsModal.display.showImmersiveHtmlToggleNote": "Display a toggle button to enable/disable HTML formatting in messages.",
"template.settingsModal.display.showDialogueColoringToggle": "Show Colored Dialogues", "template.settingsModal.display.showDialogueColoringToggle": "Show Colored Dialogues",
"template.settingsModal.display.showDialogueColoringToggleNote": "Display a toggle button to enable/disable colored dialogue formatting.", "template.settingsModal.display.showDialogueColoringToggleNote": "Display a toggle button to enable/disable colored dialogue formatting.",
"template.settingsModal.display.showDeceptionToggle": "Show Deception System",
"template.settingsModal.display.showDeceptionToggleNote": "Display a toggle button to enable/disable the Deception System for marking lies and deceptions.",
"template.settingsModal.display.showOmniscienceToggle": "Show Omniscience Filter",
"template.settingsModal.display.showOmniscienceToggleNote": "Display a toggle button to enable/disable the Omniscience Filter for filtering hidden events.",
"template.settingsModal.display.showSpotifyMusicToggle": "Show Spotify Music", "template.settingsModal.display.showSpotifyMusicToggle": "Show Spotify Music",
"template.settingsModal.display.showSpotifyMusicToggleNote": "Display Spotify music player with AI-suggested scene-appropriate tracks.", "template.settingsModal.display.showSpotifyMusicToggleNote": "Display Spotify music player with AI-suggested scene-appropriate tracks.",
"template.settingsModal.display.showSnowflakesToggle": "Show Snowflakes Effect", "template.settingsModal.display.showSnowflakesToggle": "Show Snowflakes Effect",
@@ -159,15 +155,13 @@
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:", "template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:",
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats", "template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats",
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Track Character Stats", "template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Track Character Stats",
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Create stats to track for each character (displayed as colored numbers).", "template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Create stats to track for each character (displayed as colored bars).",
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Add Character Stat", "template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Add Character Stat",
"template.mainPanel.title": "RPG Companion", "template.mainPanel.title": "RPG Companion",
"template.mainPanel.lastRoll": "Last Roll:", "template.mainPanel.lastRoll": "Last Roll:",
"template.mainPanel.clearLastRoll": "Clear last roll", "template.mainPanel.clearLastRoll": "Clear last roll",
"template.mainPanel.immersiveHtml": "Immersive HTML", "template.mainPanel.immersiveHtml": "Immersive HTML",
"template.mainPanel.coloredDialogues": "Colored Dialogues", "template.mainPanel.coloredDialogues": "Colored Dialogues",
"template.mainPanel.deceptionSystem": "Deception System",
"template.mainPanel.omniscienceFilter": "Omniscience Filter",
"template.mainPanel.spotifyMusic": "Spotify Music", "template.mainPanel.spotifyMusic": "Spotify Music",
"template.mainPanel.snowflakesEffect": "Snowflakes Effect", "template.mainPanel.snowflakesEffect": "Snowflakes Effect",
"template.mainPanel.dynamicWeatherEffects": "Dynamic Weather", "template.mainPanel.dynamicWeatherEffects": "Dynamic Weather",
+2 -3
View File
@@ -9,8 +9,7 @@
* - Manual regeneration support * - Manual regeneration support
*/ */
import { characters, this_chid } from '../../../../../../../script.js'; import { generateRaw, characters, this_chid } from '../../../../../../../script.js';
import { safeGenerateRaw } from '../../utils/responseExtractor.js';
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
import { selected_group, getGroupMembers } from '../../../../../../group-chats.js'; import { selected_group, getGroupMembers } from '../../../../../../group-chats.js';
import { extensionSettings, sessionAvatarPrompts, setSessionAvatarPrompt } from '../../core/state.js'; import { extensionSettings, sessionAvatarPrompts, setSessionAvatarPrompt } from '../../core/state.js';
@@ -255,7 +254,7 @@ async function generateAvatarPrompt(characterName) {
// console.log('[RPG Avatar] Using external API for avatar prompt generation'); // console.log('[RPG Avatar] Using external API for avatar prompt generation');
response = await generateWithExternalAPI(promptMessages); response = await generateWithExternalAPI(promptMessages);
} else { } else {
response = await safeGenerateRaw({ response = await generateRaw({
prompt: promptMessages, prompt: promptMessages,
quietToLoud: false quietToLoud: false
}); });
-8
View File
@@ -20,10 +20,6 @@ export function setupClassicStatsButtons() {
// Delegated event listener for increase buttons // Delegated event listener for increase buttons
$userStatsContainer.on('click', '.rpg-stat-increase', function() { $userStatsContainer.on('click', '.rpg-stat-increase', function() {
const stat = $(this).data('stat'); const stat = $(this).data('stat');
// Initialize custom attributes if they don't exist
if (extensionSettings.classicStats[stat] === undefined) {
extensionSettings.classicStats[stat] = 10;
}
if (extensionSettings.classicStats[stat] < 999) { if (extensionSettings.classicStats[stat] < 999) {
extensionSettings.classicStats[stat]++; extensionSettings.classicStats[stat]++;
saveSettings(); saveSettings();
@@ -37,10 +33,6 @@ export function setupClassicStatsButtons() {
// Delegated event listener for decrease buttons // Delegated event listener for decrease buttons
$userStatsContainer.on('click', '.rpg-stat-decrease', function() { $userStatsContainer.on('click', '.rpg-stat-decrease', function() {
const stat = $(this).data('stat'); const stat = $(this).data('stat');
// Initialize custom attributes if they don't exist
if (extensionSettings.classicStats[stat] === undefined) {
extensionSettings.classicStats[stat] = 10;
}
if (extensionSettings.classicStats[stat] > 1) { if (extensionSettings.classicStats[stat] > 1) {
extensionSettings.classicStats[stat]--; extensionSettings.classicStats[stat]--;
saveSettings(); saveSettings();
+1 -1
View File
@@ -76,7 +76,7 @@ export async function ensureJsonCleaningRegex(st_extension_settings, saveSetting
} }
// Small delay to ensure save completes // Small delay to ensure save completes
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.3 settings.'); console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.6 settings.');
} else { } else {
console.log('[RPG Companion] JSON Cleaning Regex is up to date.'); console.log('[RPG Companion] JSON Cleaning Regex is up to date.');
} }
+7 -7
View File
@@ -3,9 +3,8 @@
* Handles API calls for RPG tracker generation * Handles API calls for RPG tracker generation
*/ */
import { chat, eventSource } from '../../../../../../../script.js'; import { generateRaw, chat, eventSource } from '../../../../../../../script.js';
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
import { safeGenerateRaw, extractTextFromResponse } from '../../utils/responseExtractor.js';
// Custom event name for when RPG Companion finishes updating tracker data // Custom event name for when RPG Companion finishes updating tracker data
// Other extensions can listen for this event to know when RPG Companion is done // Other extensions can listen for this event to know when RPG Companion is done
@@ -108,10 +107,11 @@ export async function generateWithExternalAPI(messages) {
const data = await response.json(); const data = await response.json();
const content = extractTextFromResponse(data); if (!data.choices || !data.choices[0] || !data.choices[0].message) {
if (!content || !content.trim()) { throw new Error('Invalid response format from external API');
throw new Error('Invalid response format from external API — no text content found');
} }
const content = data.choices[0].message.content;
// console.log('[RPG Companion] External API response received successfully'); // console.log('[RPG Companion] External API response received successfully');
return content; return content;
@@ -255,8 +255,8 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
// console.log('[RPG Companion] Using external API for tracker generation'); // console.log('[RPG Companion] Using external API for tracker generation');
response = await generateWithExternalAPI(prompt); response = await generateWithExternalAPI(prompt);
} else { } else {
// Separate mode: Use SillyTavern's generateRaw (with extended thinking fallback) // Separate mode: Use SillyTavern's generateRaw
response = await safeGenerateRaw({ response = await generateRaw({
prompt: prompt, prompt: prompt,
quietToLoud: false quietToLoud: false
}); });
+19 -59
View File
@@ -4,7 +4,7 @@
*/ */
import { getContext } from '../../../../../../extensions.js'; import { getContext } from '../../../../../../extensions.js';
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, eventSource, event_types } from '../../../../../../../script.js'; import { setExtensionPrompt, extension_prompt_types, extension_prompt_roles, eventSource, event_types } from '../../../../../../../script.js';
import { import {
extensionSettings, extensionSettings,
committedTrackerData, committedTrackerData,
@@ -22,11 +22,8 @@ import {
DEFAULT_HTML_PROMPT, DEFAULT_HTML_PROMPT,
DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT,
DEFAULT_DECEPTION_PROMPT, DEFAULT_DECEPTION_PROMPT,
DEFAULT_OMNISCIENCE_FILTER_PROMPT,
DEFAULT_CYOA_PROMPT, DEFAULT_CYOA_PROMPT,
DEFAULT_SPOTIFY_PROMPT, DEFAULT_SPOTIFY_PROMPT,
DEFAULT_NARRATOR_PROMPT,
DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT,
SPOTIFY_FORMAT_INSTRUCTION SPOTIFY_FORMAT_INSTRUCTION
} from './promptBuilder.js'; } from './promptBuilder.js';
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js'; import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
@@ -481,7 +478,6 @@ function onGenerateBeforeCombinePrompts(eventData) {
/** /**
* Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion). * Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion).
* This is now a backup/fallback - primary injection happens in BEFORE_COMBINE. * This is now a backup/fallback - primary injection happens in BEFORE_COMBINE.
* Also fixes newline spacing after </context> tag.
* *
* @param {Object} eventData - Event data with prompt property * @param {Object} eventData - Event data with prompt property
*/ */
@@ -494,25 +490,24 @@ function onGenerateAfterCombinePrompts(eventData) {
return; return;
} }
let didInjectHistory = false; // Skip if injection already happened in BEFORE_COMBINE
if (historyInjectionDone) {
return;
}
// Only inject if we have pending context
if (pendingContextMap.size === 0) {
return;
}
// Inject historical context if available and not already done
if (!historyInjectionDone && pendingContextMap.size > 0) {
// Fallback injection for edge cases where BEFORE_COMBINE didn't work // Fallback injection for edge cases where BEFORE_COMBINE didn't work
console.log('[RPG Companion] Using fallback string-based injection (AFTER_COMBINE)'); console.log('[RPG Companion] Using fallback string-based injection (AFTER_COMBINE)');
eventData.prompt = injectContextIntoTextPrompt(eventData.prompt); eventData.prompt = injectContextIntoTextPrompt(eventData.prompt);
didInjectHistory = true;
}
// Always fix newlines around context tags (whether we just injected or not)
eventData.prompt = eventData.prompt.replace(/<context>/g, '\n<context>');
eventData.prompt = eventData.prompt.replace(/<\/context>/g, '</context>\n');
} }
/** /**
* Event handler for CHAT_COMPLETION_PROMPT_READY. * Event handler for CHAT_COMPLETION_PROMPT_READY.
* Injects historical context into the chat message array. * Injects historical context into the chat message array.
* Also fixes newline spacing around <context> tags.
* *
* @param {Object} eventData - Event data with chat property * @param {Object} eventData - Event data with chat property
*/ */
@@ -525,22 +520,16 @@ function onChatCompletionPromptReady(eventData) {
return; return;
} }
// Inject historical context if we have pending context // Only inject if we have pending context
if (pendingContextMap.size > 0) { if (pendingContextMap.size === 0) {
return;
}
eventData.chat = injectContextIntoChatPrompt(eventData.chat); eventData.chat = injectContextIntoChatPrompt(eventData.chat);
// DON'T clear pendingContextMap here - let it persist for other generations // DON'T clear pendingContextMap here - let it persist for other generations
// (e.g., prewarm extensions). It will be cleared on GENERATION_ENDED. // (e.g., prewarm extensions). It will be cleared on GENERATION_ENDED.
} }
// Fix newlines around context tags for all messages
for (const message of eventData.chat) {
if (message.content && typeof message.content === 'string') {
message.content = message.content.replace(/<context>/g, '\n<context>');
message.content = message.content.replace(/<\/context>/g, '</context>\n');
}
}
}
/** /**
* Event handler for generation start. * Event handler for generation start.
* Manages tracker data commitment and prompt injection based on generation mode. * Manages tracker data commitment and prompt injection based on generation mode.
@@ -803,19 +792,6 @@ export async function onGenerationStarted(type, data, dryRun) {
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
} }
// Inject Omniscience Filter prompt separately at depth 0 if enabled
if (extensionSettings.enableOmniscienceFilter && !shouldSuppress) {
// Use custom Omniscience Filter prompt if set, otherwise use default
const omnisciencePromptText = extensionSettings.customOmnisciencePrompt || DEFAULT_OMNISCIENCE_FILTER_PROMPT;
const omnisciencePrompt = `\n${omnisciencePromptText}\n`;
setExtensionPrompt('rpg-companion-omniscience', omnisciencePrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected Omniscience Filter prompt at depth 0 for together mode');
} else {
// Clear Omniscience Filter prompt if disabled
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
}
// Inject Spotify prompt separately at depth 0 if enabled // Inject Spotify prompt separately at depth 0 if enabled
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) { if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
// Use custom Spotify prompt if set, otherwise use default // Use custom Spotify prompt if set, otherwise use default
@@ -847,14 +823,12 @@ export async function onGenerationStarted(type, data, dryRun) {
const contextSummary = generateContextualSummary(); const contextSummary = generateContextualSummary();
if (contextSummary) { if (contextSummary) {
// Use custom context instructions prompt if set, otherwise use default const wrappedContext = `\nHere is context information about the current scene, and what follows is the last message in the chat history:
const contextInstructionsText = extensionSettings.customContextInstructionsPrompt || DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT;
const wrappedContext = `
<context> <context>
${contextSummary} ${contextSummary}
${contextInstructionsText}
</context>`; 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.
</context>\n\n`;
// Inject context at depth 1 (before last user message) as SYSTEM // Inject context at depth 1 (before last user message) as SYSTEM
// Skip when a guided generation injection is present to avoid conflicting instructions // Skip when a guided generation injection is present to avoid conflicting instructions
@@ -906,19 +880,6 @@ ${contextInstructionsText}
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
} }
// Inject Omniscience Filter prompt separately at depth 0 if enabled
if (extensionSettings.enableOmniscienceFilter && !shouldSuppress) {
// Use custom Omniscience Filter prompt if set, otherwise use default
const omnisciencePromptText = extensionSettings.customOmnisciencePrompt || DEFAULT_OMNISCIENCE_FILTER_PROMPT;
const omnisciencePrompt = `\n${omnisciencePromptText}\n`;
setExtensionPrompt('rpg-companion-omniscience', omnisciencePrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected Omniscience Filter prompt at depth 0 for separate/external mode');
} else {
// Clear Omniscience Filter prompt if disabled
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
}
// Inject Spotify prompt separately at depth 0 if enabled // Inject Spotify prompt separately at depth 0 if enabled
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) { if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
// Use custom Spotify prompt if set, otherwise use default // Use custom Spotify prompt if set, otherwise use default
@@ -956,7 +917,6 @@ ${contextInstructionsText}
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-zzz-cyoa', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-zzz-cyoa', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
} }
+3 -21
View File
@@ -5,8 +5,6 @@
import { extensionSettings, committedTrackerData } from '../../core/state.js'; import { extensionSettings, committedTrackerData } from '../../core/state.js';
import { getContext } from '../../../../../../extensions.js'; import { getContext } from '../../../../../../extensions.js';
import { getWeatherKeywordsAsPromptString } from '../ui/weatherEffects.js';
import { i18n } from '../../core/i18n.js';
/** /**
* Converts a field name to snake_case for use as JSON key * Converts a field name to snake_case for use as JSON key
@@ -21,19 +19,6 @@ function toSnakeCase(name) {
.replace(/^_+|_+$/g, ''); .replace(/^_+|_+$/g, '');
} }
/**
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
* Parenthetical content is treated as a description/hint, not part of the key.
* Example: "Conditions (up to 5 traits)" -> "conditions"
* Example: "Status Effects" -> "status_effects"
* @param {string} name - Field name, possibly with parenthetical description
* @returns {string} snake_case key from the base name only
*/
function toFieldKey(name) {
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
return toSnakeCase(baseName);
}
/** /**
* Builds User Stats JSON format instruction * Builds User Stats JSON format instruction
* @returns {string} JSON format instruction for user stats * @returns {string} JSON format instruction for user stats
@@ -73,12 +58,12 @@ export function buildUserStatsJSONInstruction() {
if (customFields.length > 0) { if (customFields.length > 0) {
for (let i = 0; i < customFields.length; i++) { for (let i = 0; i < customFields.length; i++) {
const fieldName = customFields[i].toLowerCase(); const fieldName = customFields[i].toLowerCase();
const fieldKey = toFieldKey(fieldName); const fieldKey = toSnakeCase(fieldName);
const comma = (i === customFields.length - 1 && !userStatsConfig.statusSection.showMoodEmoji) ? '' : (userStatsConfig.statusSection.showMoodEmoji || i < customFields.length - 1 ? ',\n' : '\n'); const comma = (i === customFields.length - 1 && !userStatsConfig.statusSection.showMoodEmoji) ? '' : (userStatsConfig.statusSection.showMoodEmoji || i < customFields.length - 1 ? ',\n' : '\n');
if (i === 0 && userStatsConfig.statusSection.showMoodEmoji) { if (i === 0 && userStatsConfig.statusSection.showMoodEmoji) {
instruction += ',\n'; instruction += ',\n';
} }
instruction += ` "${fieldKey}": "[${fieldName}]"${comma}`; instruction += ` "${fieldKey}": "[${fieldName}1, ${fieldName}2]"${comma}`;
} }
} }
if (!userStatsConfig.statusSection.showMoodEmoji && customFields.length > 0) { if (!userStatsConfig.statusSection.showMoodEmoji && customFields.length > 0) {
@@ -147,10 +132,7 @@ export function buildInfoBoxJSONInstruction() {
} }
if (widgets.weather?.enabled) { if (widgets.weather?.enabled) {
// Get valid weather keywords for the current language to guide LLM generation instruction += (hasFields ? ',\n' : '') + ' "weather": {"emoji": "Weather Emoji", "forecast": "Forecast"}';
const currentLang = i18n.currentLanguage || 'en';
const weatherHint = getWeatherKeywordsAsPromptString(currentLang);
instruction += (hasFields ? ',\n' : '') + ` "weather": {"emoji": "Weather Emoji", "forecast": "Forecast"} // ${weatherHint}`;
hasFields = true; hasFields = true;
} }
+11 -14
View File
@@ -98,19 +98,16 @@ function applyUserStatsLocks(data, lockedItems) {
} }
} }
// Lock inventory items - match by item name instead of index // Lock inventory items - handle bracket notation paths like "inventory.onPerson[0]"
if (data.inventory && lockedItems.inventory) { if (data.inventory && lockedItems.inventory) {
// Helper function to apply locks based on item name // Helper function to parse bracket notation and apply lock
const applyInventoryLocks = (items, category) => { const applyInventoryLocks = (items, category) => {
if (!Array.isArray(items)) return items; if (!Array.isArray(items)) return items;
if (!lockedItems.inventory[category]) return items;
return items.map((item) => { return items.map((item, index) => {
// Get item name (handle both string and object formats) // Check if this specific item is locked using bracket notation with inventory prefix
const itemName = typeof item === 'string' ? item : (item.item || item.name || ''); const bracketPath = `${category}[${index}]`;
if (lockedItems.inventory[bracketPath]) {
// Check if this specific item name is locked
if (lockedItems.inventory[category][itemName]) {
return typeof item === 'string' return typeof item === 'string'
? { item, locked: true } ? { item, locked: true }
: { ...item, locked: true }; : { ...item, locked: true };
@@ -134,13 +131,13 @@ function applyUserStatsLocks(data, lockedItems) {
data.inventory.assets = applyInventoryLocks(data.inventory.assets, 'assets'); data.inventory.assets = applyInventoryLocks(data.inventory.assets, 'assets');
} }
// Apply locks to stored items - match by item name // Apply locks to stored items (nested structure with inventory.stored.location[index])
if (data.inventory.stored && lockedItems.inventory.stored) { if (data.inventory.stored && lockedItems.inventory.stored) {
for (const location in data.inventory.stored) { for (const location in data.inventory.stored) {
if (Array.isArray(data.inventory.stored[location]) && lockedItems.inventory.stored[location]) { if (Array.isArray(data.inventory.stored[location])) {
data.inventory.stored[location] = data.inventory.stored[location].map((item) => { data.inventory.stored[location] = data.inventory.stored[location].map((item, index) => {
const itemName = typeof item === 'string' ? item : (item.item || item.name || ''); const bracketPath = `${location}[${index}]`;
if (lockedItems.inventory.stored[location][itemName]) { if (lockedItems.inventory.stored[bracketPath]) {
return typeof item === 'string' return typeof item === 'string'
? { item, locked: true } ? { item, locked: true }
: { ...item, locked: true }; : { ...item, locked: true };
+4 -27
View File
@@ -9,20 +9,6 @@ import { saveSettings } from '../../core/persistence.js';
import { extractInventory } from './inventoryParser.js'; import { extractInventory } from './inventoryParser.js';
import { repairJSON } from '../../utils/jsonRepair.js'; import { repairJSON } from '../../utils/jsonRepair.js';
/**
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
* Example: "Conditions (up to 5 traits)" -> "conditions"
* @param {string} name - Field name, possibly with parenthetical description
* @returns {string} snake_case key from the base name only
*/
function toFieldKey(name) {
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
return baseName
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');
}
/** /**
* Helper to separate emoji from text in a string * Helper to separate emoji from text in a string
* Handles cases where there's no comma or space after emoji * Handles cases where there's no comma or space after emoji
@@ -573,12 +559,10 @@ export function parseUserStats(statsText) {
const trackerConfig = extensionSettings.trackerConfig; const trackerConfig = extensionSettings.trackerConfig;
const customFields = trackerConfig?.userStats?.statusSection?.customFields || []; const customFields = trackerConfig?.userStats?.statusSection?.customFields || [];
for (const fieldName of customFields) { for (const fieldName of customFields) {
const fieldKey = toFieldKey(fieldName); const fieldKey = fieldName.toLowerCase();
// Try the base key first (e.g., "conditions"), then fall back to full lowercase name if (statsData.status[fieldKey]) {
const value = statsData.status[fieldKey] || statsData.status[fieldName.toLowerCase()]; extensionSettings.userStats[fieldKey] = statsData.status[fieldKey];
if (value) { // console.log(`[RPG Parser] ✓ Set ${fieldKey} =`, statsData.status[fieldKey]);
extensionSettings.userStats[fieldKey] = value;
// console.log(`[RPG Parser] ✓ Set ${fieldKey} =`, value);
} }
} }
} }
@@ -633,13 +617,6 @@ export function parseUserStats(statsText) {
if (!quest) return ''; if (!quest) return '';
if (typeof quest === 'string') return quest; if (typeof quest === 'string') return quest;
if (typeof quest === 'object') { if (typeof quest === 'object') {
// Check for locked format: {value, locked}
// Recursively extract value if it's nested
let extracted = quest;
while (typeof extracted === 'object' && extracted.value !== undefined) {
extracted = extracted.value;
}
if (typeof extracted === 'string') return extracted;
// v3 format: {title, description, status} // v3 format: {title, description, status}
return quest.title || quest.description || JSON.stringify(quest); return quest.title || quest.description || JSON.stringify(quest);
} }
+8 -44
View File
@@ -33,15 +33,6 @@ export const DEFAULT_DIALOGUE_COLORING_PROMPT = `Wrap all character/NPC "dialogu
*/ */
export const DEFAULT_DECEPTION_PROMPT = `When a character is lying or deceiving, you should follow up that line with the <lie> tag, containing a brief description of the truth and the lie's reason, using the template below (replace placeholders in quotation marks). This will be hidden from the user's view, but not to you, making it useful for future consequences: <lie character="name" type="lying/deceiving/omitting" truth="truth" reason="reason"/>.`; export const DEFAULT_DECEPTION_PROMPT = `When a character is lying or deceiving, you should follow up that line with the <lie> tag, containing a brief description of the truth and the lie's reason, using the template below (replace placeholders in quotation marks). This will be hidden from the user's view, but not to you, making it useful for future consequences: <lie character="name" type="lying/deceiving/omitting" truth="truth" reason="reason"/>.`;
/**
* Default Omniscience Filter prompt text
* This instructs the AI to separate information the player character cannot perceive
*/
export const DEFAULT_OMNISCIENCE_FILTER_PROMPT = `You must strictly separate what the player can directly perceive from what they cannot. They should only read limited narrative content that their persona can actually see, hear, smell, touch, or otherwise directly sense. Before writing any narrative content that involves events, actions, or details the player directly cannot perceive (because they're not looking, too far away, behind them, in another room, happening silently, include NPCs' internal thoughts, etc.), you absolutely must output that hidden information inside a <filter> tag using this exact format:
<filter event="[Brief description of what is happening that the player cannot perceive]" reason="[Why the player character cannot perceive this - e.g., 'behind them', 'in another room', 'too quiet to hear', 'focused elsewhere']"/>
Example: <filter event="Zandik quietly takes the key from the table and slips out the back door" reason="Zandik is behind Mari, who is absorbed in reading, and he moves silently"/> You hear a faint click from somewhere behind you, but when you glance up from your newspaper, the room seems unchanged.`;
/** /**
* Default CYOA prompt text * Default CYOA prompt text
*/ */
@@ -62,11 +53,6 @@ export const SPOTIFY_FORMAT_INSTRUCTION = `Include it in this exact format: <spo
*/ */
export const DEFAULT_NARRATOR_PROMPT = `Infer the identity and details of characters present in each scene from the story context below. Do not use fixed character references; instead, identify characters naturally based on their actions, dialogue, and descriptions in the narrative.`; export const DEFAULT_NARRATOR_PROMPT = `Infer the identity and details of characters present in each scene from the story context below. Do not use fixed character references; instead, identify characters naturally based on their actions, dialogue, and descriptions in the narrative.`;
/**
* Default Context Instructions prompt text (customizable by users)
*/
export const DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT = `The context above is information about the current scene, and what follows is the last message in the chat history. 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.`;
/** /**
* Gets character card information for current chat (handles both single and group chats) * Gets character card information for current chat (handles both single and group chats)
* @returns {string} Formatted character information * @returns {string} Formatted character information
@@ -560,33 +546,12 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
if (trackerType === 'userStats') { if (trackerType === 'userStats') {
formatted += `${userName}'s Stats:\n`; formatted += `${userName}'s Stats:\n`;
// Get display mode and custom stats config for maxValue lookup
const userStatsConfig = extensionSettings.trackerConfig?.userStats;
const displayMode = userStatsConfig?.statsDisplayMode || 'percentage';
const customStats = userStatsConfig?.customStats || [];
// Helper to get maxValue for a stat by id
const getMaxValue = (statId) => {
const statConfig = customStats.find(s => s.id === statId);
return statConfig?.maxValue || 100;
};
// Helper to format stat value based on display mode
const formatStatValue = (value, statId) => {
if (displayMode === 'number') {
const maxValue = getMaxValue(statId);
return `${value}/${maxValue}`;
}
return value;
};
// Handle stats array format: [{id, name, value}, ...] // Handle stats array format: [{id, name, value}, ...]
if (data.stats && Array.isArray(data.stats)) { if (data.stats && Array.isArray(data.stats)) {
for (const stat of data.stats) { for (const stat of data.stats) {
if (stat && stat.value !== undefined) { if (stat && stat.value !== undefined) {
const statName = stat.name || (stat.id ? stat.id.charAt(0).toUpperCase() + stat.id.slice(1) : 'Unknown'); const statName = stat.name || (stat.id ? stat.id.charAt(0).toUpperCase() + stat.id.slice(1) : 'Unknown');
const statId = stat.id || statName.toLowerCase(); formatted += `${statName}: ${stat.value}\n`;
formatted += `${statName}: ${formatStatValue(stat.value, statId)}\n`;
} }
} }
} else { } else {
@@ -599,7 +564,7 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
const value = getValue(data[statName]); const value = getValue(data[statName]);
if (value) { if (value) {
const displayName = statName.charAt(0).toUpperCase() + statName.slice(1); const displayName = statName.charAt(0).toUpperCase() + statName.slice(1);
formatted += `${displayName}: ${formatStatValue(value, statName)}\n`; formatted += `${displayName}: ${value}\n`;
} }
} }
} }
@@ -608,7 +573,7 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
if (!statFieldOrder.includes(key) && !specialFields.includes(key) && typeof value === 'number') { if (!statFieldOrder.includes(key) && !specialFields.includes(key) && typeof value === 'number') {
const displayName = key.charAt(0).toUpperCase() + key.slice(1); const displayName = key.charAt(0).toUpperCase() + key.slice(1);
formatted += `${displayName}: ${formatStatValue(getValue(value), key)}\n`; formatted += `${displayName}: ${getValue(value)}\n`;
} }
} }
} }
@@ -739,14 +704,13 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
} }
} }
// Relationship - check both Relationship (new format) and relationship (old format) // Relationship
const relationshipValue = char.Relationship || char.relationship; if (char.relationship) {
if (relationshipValue) {
let relValue; let relValue;
if (typeof relationshipValue === 'object' && !Array.isArray(relationshipValue) && 'status' in relationshipValue) { if (typeof char.relationship === 'object' && !Array.isArray(char.relationship) && 'status' in char.relationship) {
relValue = getValue(relationshipValue.status); relValue = getValue(char.relationship.status);
} else { } else {
relValue = getValue(relationshipValue); relValue = getValue(char.relationship);
} }
if (relValue) formatted += ` Relationship: ${relValue}\n`; if (relValue) formatted += ` Relationship: ${relValue}\n`;
} }
+202 -19
View File
@@ -359,7 +359,8 @@ export function onMessageSwiped(messageIndex) {
// console.log('[RPG Companion] 🔵 EVENT: onMessageSwiped at index:', messageIndex); // console.log('[RPG Companion] 🔵 EVENT: onMessageSwiped at index:', messageIndex);
// Get the message that was swiped // Get the message that was swiped
const message = chat[messageIndex]; const currentChat = getContext().chat;
const message = currentChat[messageIndex];
if (!message || message.is_user) { if (!message || message.is_user) {
// console.log('[RPG Companion] 🔵 Ignoring swipe - message is user or undefined'); // console.log('[RPG Companion] 🔵 Ignoring swipe - message is user or undefined');
return; return;
@@ -379,40 +380,80 @@ export function onMessageSwiped(messageIndex) {
setLastActionWasSwipe(true); setLastActionWasSwipe(true);
setIsAwaitingNewMessage(true); setIsAwaitingNewMessage(true);
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true'); // console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
// CRITICAL: For new swipes, commit data from the PREVIOUS assistant message
// This ensures the LLM gets context from BEFORE the message being regenerated,
// not the message itself (which would cause time/story to advance incorrectly)
for (let i = messageIndex - 1; i >= 0; i--) {
const prevMessage = currentChat[i];
if (!prevMessage.is_user && prevMessage.extra?.rpg_companion_swipes) {
const prevSwipeId = prevMessage.swipe_id || 0;
const prevSwipeData = prevMessage.extra.rpg_companion_swipes[prevSwipeId];
if (prevSwipeData) {
// console.log('[RPG Companion] 🔵 Committing tracker data from PREVIOUS message at index', i);
committedTrackerData.userStats = prevSwipeData.userStats || null;
committedTrackerData.infoBox = prevSwipeData.infoBox || null;
committedTrackerData.characterThoughts = prevSwipeData.characterThoughts || null;
} else {
// Previous message has no swipe data - clear committed data
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
}
break;
}
// If we hit index 0 without finding a previous assistant message, clear committed data
if (i === 0) {
// console.log('[RPG Companion] 🔵 No previous assistant message found - clearing committed data');
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
}
}
// Edge case: if messageIndex is 0 (first message being swiped), clear committed data
if (messageIndex === 0) {
// console.log('[RPG Companion] 🔵 Swiping first message - clearing committed data');
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
}
// For new swipes, also update lastGeneratedData to match committed data
// This ensures the UI shows the "before" state while waiting for the new response
lastGeneratedData.userStats = committedTrackerData.userStats;
lastGeneratedData.infoBox = committedTrackerData.infoBox;
lastGeneratedData.characterThoughts = committedTrackerData.characterThoughts;
// Parse user stats for display if available
if (committedTrackerData.userStats) {
parseUserStats(committedTrackerData.userStats);
}
} else { } else {
// This is navigating to an EXISTING swipe - don't change the flag // This is navigating to an EXISTING swipe - don't change the flag
// console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe); // console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe);
}
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId); // Load RPG data for this existing swipe for DISPLAY purposes
// IMPORTANT: onMessageSwiped is for DISPLAY only!
// lastGeneratedData is for DISPLAY, committedTrackerData is for GENERATION
// It's safe to load swipe data into lastGeneratedData - it won't be committed due to !lastActionWasSwipe check
if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) { if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) {
const swipeData = message.extra.rpg_companion_swipes[currentSwipeId]; const swipeData = message.extra.rpg_companion_swipes[currentSwipeId];
// Load swipe data into lastGeneratedData for display (both modes) // Load swipe data into lastGeneratedData for display
lastGeneratedData.userStats = swipeData.userStats || null; lastGeneratedData.userStats = swipeData.userStats || null;
lastGeneratedData.infoBox = swipeData.infoBox || null; lastGeneratedData.infoBox = swipeData.infoBox || null;
// Normalize characterThoughts to string format (for backward compatibility with old object format)
if (swipeData.characterThoughts && typeof swipeData.characterThoughts === 'object') {
lastGeneratedData.characterThoughts = JSON.stringify(swipeData.characterThoughts, null, 2);
} else {
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null; lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
}
// DON'T parse user stats when loading swipe data // Parse user stats if available
// This would overwrite manually edited fields (like Conditions) with old swipe data if (swipeData.userStats) {
// The lastGeneratedData is loaded for display purposes only parseUserStats(swipeData.userStats);
// parseUserStats() updates extensionSettings.userStats which should only be modified }
// by new generations or manual edits, not by swipe navigation
// console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId); // console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId);
} else { } else {
// console.log('[RPG Companion] ️ No stored data for swipe:', currentSwipeId); // console.log('[RPG Companion] ️ No stored data for swipe:', currentSwipeId);
} }
}
// Re-render the panels // Re-render the panels
renderUserStats(); renderUserStats();
@@ -426,6 +467,148 @@ export function onMessageSwiped(messageIndex) {
updateChatThoughts(); updateChatThoughts();
} }
/**
* Event handler for when a message is deleted.
* Restores RPG state from the last assistant message with RPG data,
* or clears state if no messages remain.
*/
export function onMessageDeleted(messageIndex) {
if (!extensionSettings.enabled) {
return;
}
// console.log('[RPG Companion] 🗑️ EVENT: onMessageDeleted at index:', messageIndex);
const context = getContext();
const currentChat = context.chat;
// If chat is empty, clear all RPG state
if (!currentChat || currentChat.length === 0) {
// console.log('[RPG Companion] 🗑️ Chat is empty - clearing RPG state');
lastGeneratedData.userStats = null;
lastGeneratedData.infoBox = null;
lastGeneratedData.characterThoughts = null;
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
// Clear parsed stats from extensionSettings
if (extensionSettings.userStats) {
extensionSettings.userStats = null;
}
// Re-render empty panels
renderUserStats();
renderInfoBox();
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update FAB widgets and strip widgets
updateFabWidgets();
updateStripWidgets();
// Update chat thought overlays (removes any remaining)
updateChatThoughts();
// Save the cleared state
saveChatData();
return;
}
// Find the last assistant message with RPG data
for (let i = currentChat.length - 1; i >= 0; i--) {
const message = currentChat[i];
if (!message.is_user && message.extra?.rpg_companion_swipes) {
const swipeId = message.swipe_id || 0;
const swipeData = message.extra.rpg_companion_swipes[swipeId];
if (swipeData) {
// Check if this is the same data we already have displayed
const sameUserStats = lastGeneratedData.userStats === swipeData.userStats;
const sameInfoBox = lastGeneratedData.infoBox === swipeData.infoBox;
const sameThoughts = lastGeneratedData.characterThoughts === swipeData.characterThoughts;
if (sameUserStats && sameInfoBox && sameThoughts) {
// console.log('[RPG Companion] 🗑️ RPG state already matches last message - no restore needed');
return;
}
// console.log('[RPG Companion] 🗑️ Restoring RPG state from message index', i, 'swipe', swipeId);
// Restore state from this message
lastGeneratedData.userStats = swipeData.userStats || null;
lastGeneratedData.infoBox = swipeData.infoBox || null;
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
// Also update committed data so next generation uses correct context
committedTrackerData.userStats = swipeData.userStats || null;
committedTrackerData.infoBox = swipeData.infoBox || null;
committedTrackerData.characterThoughts = swipeData.characterThoughts || null;
// Parse user stats if available
if (swipeData.userStats) {
parseUserStats(swipeData.userStats);
}
// Re-render panels with restored data
renderUserStats();
renderInfoBox();
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update FAB widgets and strip widgets
updateFabWidgets();
updateStripWidgets();
// Update chat thought overlays
updateChatThoughts();
// Save the restored state
saveChatData();
return;
}
}
}
// No assistant message with RPG data found - clear state
// console.log('[RPG Companion] 🗑️ No assistant message with RPG data found - clearing state');
lastGeneratedData.userStats = null;
lastGeneratedData.infoBox = null;
lastGeneratedData.characterThoughts = null;
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
// Clear parsed stats
if (extensionSettings.userStats) {
extensionSettings.userStats = null;
}
// Re-render empty panels
renderUserStats();
renderInfoBox();
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update FAB widgets and strip widgets
updateFabWidgets();
updateStripWidgets();
// Update chat thought overlays
updateChatThoughts();
// Save the cleared state
saveChatData();
}
/** /**
* Update the persona avatar image when user switches personas * Update the persona avatar image when user switches personas
*/ */
+8 -8
View File
@@ -81,7 +81,7 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
if (viewMode === 'grid') { if (viewMode === 'grid') {
// Grid view: card-style items // Grid view: card-style items
itemsHtml = items.map((item, index) => { itemsHtml = items.map((item, index) => {
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson.${item}`); const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson[${index}]`);
return ` return `
<div class="rpg-item-card" data-field="onPerson" data-index="${index}"> <div class="rpg-item-card" data-field="onPerson" data-index="${index}">
${lockIconHtml} ${lockIconHtml}
@@ -94,7 +94,7 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
} else { } else {
// List view: full-width rows // List view: full-width rows
itemsHtml = items.map((item, index) => { itemsHtml = items.map((item, index) => {
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson.${item}`); const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson[${index}]`);
return ` return `
<div class="rpg-item-row" data-field="onPerson" data-index="${index}"> <div class="rpg-item-row" data-field="onPerson" data-index="${index}">
${lockIconHtml} ${lockIconHtml}
@@ -163,7 +163,7 @@ export function renderClothingView(clothingItems, viewMode = 'list') {
if (viewMode === 'grid') { if (viewMode === 'grid') {
// Grid view: card-style items // Grid view: card-style items
itemsHtml = items.map((item, index) => { itemsHtml = items.map((item, index) => {
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing.${item}`); const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing[${index}]`);
return ` return `
<div class="rpg-item-card" data-field="clothing" data-index="${index}"> <div class="rpg-item-card" data-field="clothing" data-index="${index}">
${lockIconHtml} ${lockIconHtml}
@@ -176,7 +176,7 @@ export function renderClothingView(clothingItems, viewMode = 'list') {
} else { } else {
// List view: full-width rows // List view: full-width rows
itemsHtml = items.map((item, index) => { itemsHtml = items.map((item, index) => {
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing.${item}`); const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing[${index}]`);
return ` return `
<div class="rpg-item-row" data-field="clothing" data-index="${index}"> <div class="rpg-item-row" data-field="clothing" data-index="${index}">
${lockIconHtml} ${lockIconHtml}
@@ -291,7 +291,7 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
if (viewMode === 'grid') { if (viewMode === 'grid') {
// Grid view: card-style items // Grid view: card-style items
itemsHtml = items.map((item, index) => { itemsHtml = items.map((item, index) => {
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}.${item}`); const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}[${index}]`);
return ` return `
<div class="rpg-item-card" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}"> <div class="rpg-item-card" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
${lockIconHtml} ${lockIconHtml}
@@ -304,7 +304,7 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
} else { } else {
// List view: full-width rows // List view: full-width rows
itemsHtml = items.map((item, index) => { itemsHtml = items.map((item, index) => {
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}.${item}`); const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}[${index}]`);
return ` return `
<div class="rpg-item-row" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}"> <div class="rpg-item-row" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
${lockIconHtml} ${lockIconHtml}
@@ -393,7 +393,7 @@ export function renderAssetsView(assets, viewMode = 'list') {
if (viewMode === 'grid') { if (viewMode === 'grid') {
// Grid view: card-style items // Grid view: card-style items
itemsHtml = items.map((item, index) => { itemsHtml = items.map((item, index) => {
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets.${item}`); const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`);
return ` return `
<div class="rpg-item-card" data-field="assets" data-index="${index}"> <div class="rpg-item-card" data-field="assets" data-index="${index}">
${lockIconHtml} ${lockIconHtml}
@@ -406,7 +406,7 @@ export function renderAssetsView(assets, viewMode = 'list') {
} else { } else {
// List view: full-width rows // List view: full-width rows
itemsHtml = items.map((item, index) => { itemsHtml = items.map((item, index) => {
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets.${item}`); const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`);
return ` return `
<div class="rpg-item-row" data-field="assets" data-index="${index}"> <div class="rpg-item-row" data-field="assets" data-index="${index}">
${lockIconHtml} ${lockIconHtml}
+2 -6
View File
@@ -212,12 +212,8 @@ export function renderQuests() {
// Get current sub-tab from container or default to 'main' // Get current sub-tab from container or default to 'main'
const activeSubTab = $questsContainer.data('active-subtab') || 'main'; const activeSubTab = $questsContainer.data('active-subtab') || 'main';
// Get quests data - extract value if it's a locked object // Get quests data
let mainQuest = extensionSettings.quests.main || 'None'; const mainQuest = extensionSettings.quests.main || 'None';
// Recursively extract value if it's nested objects
while (typeof mainQuest === 'object' && mainQuest.value !== undefined) {
mainQuest = mainQuest.value;
}
const optionalQuests = extensionSettings.quests.optional || []; const optionalQuests = extensionSettings.quests.optional || [];
// Build HTML // Build HTML
+45 -301
View File
@@ -50,11 +50,9 @@ function debugLog(message, data = null) {
* @param {number} percentage - Value from 0-100 * @param {number} percentage - Value from 0-100
* @param {string} lowColor - Hex color for low values (e.g., '#ff0000') * @param {string} lowColor - Hex color for low values (e.g., '#ff0000')
* @param {string} highColor - Hex color for high values (e.g., '#00ff00') * @param {string} highColor - Hex color for high values (e.g., '#00ff00')
* @param {number} lowOpacity - Opacity for low values (0-100) * @returns {string} Interpolated hex color
* @param {number} highOpacity - Opacity for high values (0-100)
* @returns {string} Interpolated rgba color
*/ */
function getStatColor(percentage, lowColor, highColor, lowOpacity = 100, highOpacity = 100) { function getStatColor(percentage, lowColor, highColor) {
// Clamp percentage to 0-100 // Clamp percentage to 0-100
const percent = Math.max(0, Math.min(100, percentage)) / 100; const percent = Math.max(0, Math.min(100, percentage)) / 100;
@@ -75,9 +73,10 @@ function getStatColor(percentage, lowColor, highColor, lowOpacity = 100, highOpa
const r = Math.round(low.r + (high.r - low.r) * percent); const r = Math.round(low.r + (high.r - low.r) * percent);
const g = Math.round(low.g + (high.g - low.g) * percent); const g = Math.round(low.g + (high.g - low.g) * percent);
const b = Math.round(low.b + (high.b - low.b) * percent); const b = Math.round(low.b + (high.b - low.b) * percent);
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
return `rgba(${r}, ${g}, ${b}, ${a})`; // Convert back to hex
const toHex = (n) => n.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
} }
/** /**
@@ -153,20 +152,11 @@ function namesMatch(cardName, aiName) {
* Displays character cards with avatars, relationship badges, and traits. * Displays character cards with avatars, relationship badges, and traits.
* Includes event listeners for editable character fields. * Includes event listeners for editable character fields.
*/ */
export function renderThoughts({ preserveScroll = false } = {}) { export function renderThoughts() {
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) { if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
return; return;
} }
// Save scroll position before re-render if requested
let savedContentScroll = 0;
if (preserveScroll) {
const $content = $thoughtsContainer.find('.rpg-thoughts-content');
if ($content.length) {
savedContentScroll = $content[0].scrollTop;
}
}
// Don't render if no data exists (e.g., after cache clear) // Don't render if no data exists (e.g., after cache clear)
const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts; const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts;
if (!thoughtsData) { if (!thoughtsData) {
@@ -501,19 +491,17 @@ export function renderThoughts({ preserveScroll = false } = {}) {
html += ` html += `
<div class="rpg-character-card" data-character-name="${char.name}"> <div class="rpg-character-card" data-character-name="${char.name}">
<div class="rpg-character-header-row">
<div class="rpg-character-avatar rpg-avatar-upload" data-character="${char.name}" title="Click to upload avatar"> <div class="rpg-character-avatar rpg-avatar-upload" data-character="${char.name}" title="Click to upload avatar">
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" /> <img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''} ${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
</div> </div>
<div class="rpg-character-content">
<div class="rpg-character-info">
<div class="rpg-character-header"> <div class="rpg-character-header">
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span> <span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span> <span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
<button class="rpg-character-remove" data-character="${char.name}" title="Remove character">×</button> <button class="rpg-character-remove" data-character="${char.name}" title="Remove character">×</button>
</div> </div>
</div>
<div class="rpg-character-content">
<div class="rpg-character-info">
`; `;
// Render custom fields dynamically // Render custom fields dynamically
@@ -524,20 +512,17 @@ export function renderThoughts({ preserveScroll = false } = {}) {
const fieldNameLower = field.name.toLowerCase(); const fieldNameLower = field.name.toLowerCase();
// Skip lock icons for thoughts field // Skip lock icons for thoughts field
const showLock = !fieldNameLower.includes('thought'); const showLock = !fieldNameLower.includes('thought');
// Add placeholder for empty fields
const placeholder = fieldValue ? '' : `data-placeholder="${field.name}"`;
const emptyClass = fieldValue ? '' : ' rpg-empty-field';
if (showLock) { if (showLock) {
const lockIconHtml = getLockIconHtml('characters', `${char.name}.${field.name}`); const lockIconHtml = getLockIconHtml('characters', `${char.name}.${field.name}`);
html += ` html += `
<div class="rpg-character-field rpg-character-${fieldId}" style="position: relative;"> <div class="rpg-character-field rpg-character-${fieldId}" style="position: relative;">
${lockIconHtml} ${lockIconHtml}
<span class="rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}" ${placeholder}>${fieldValue}</span> <span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</span>
</div> </div>
`; `;
} else { } else {
html += ` html += `
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}" ${placeholder}>${fieldValue}</div> <div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</div>
`; `;
} }
} }
@@ -554,13 +539,7 @@ export function renderThoughts({ preserveScroll = false } = {}) {
<div class="rpg-character-stats-inner">`; <div class="rpg-character-stats-inner">`;
for (const stat of enabledCharStats) { for (const stat of enabledCharStats) {
const statValue = char[stat.name] || 0; const statValue = char[stat.name] || 0;
const statColor = getStatColor( const statColor = getStatColor(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh);
statValue,
extensionSettings.statBarColorLow,
extensionSettings.statBarColorHigh,
extensionSettings.statBarColorLowOpacity ?? 100,
extensionSettings.statBarColorHighOpacity ?? 100
);
html += ` html += `
<div class="rpg-character-stat"> <div class="rpg-character-stat">
<span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" style="color: ${statColor}" title="Click to edit ${stat.name}">${statValue}%</span> <span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" style="color: ${statColor}" title="Click to edit ${stat.name}">${statValue}%</span>
@@ -585,16 +564,6 @@ export function renderThoughts({ preserveScroll = false } = {}) {
} }
debugLog('[RPG Thoughts] Finished building all character cards'); debugLog('[RPG Thoughts] Finished building all character cards');
// Add "Add Character" button if data exists (inside rpg-thoughts-content)
if (presentCharacters.length > 0) {
html += `
<button class="rpg-add-character-btn" title="Add a new character">
<i class="fa-solid fa-plus"></i> Add Character
</button>
`;
}
html += '</div>'; html += '</div>';
} }
@@ -693,44 +662,11 @@ export function renderThoughts({ preserveScroll = false } = {}) {
fileInput.trigger('click'); fileInput.trigger('click');
}); });
// Add event listener for "Add Character" button (support both click and touch for mobile)
$thoughtsContainer.find('.rpg-add-character-btn').on('click touchend', function(e) {
e.preventDefault();
e.stopPropagation();
addNewCharacter();
});
// Handle empty field focus - remove placeholder styling on focus
$thoughtsContainer.find('.rpg-editable.rpg-empty-field').on('focus', function() {
$(this).removeClass('rpg-empty-field');
$(this).removeAttr('data-placeholder');
});
// Restore placeholder if field becomes empty on blur (after the main blur handler)
$thoughtsContainer.find('.rpg-editable').on('blur', function() {
const $this = $(this);
if (!$this.text().trim()) {
const field = $this.data('field');
if (field) {
$this.addClass('rpg-empty-field');
$this.attr('data-placeholder', field);
}
}
});
// Remove updating class after animation // Remove updating class after animation
if (extensionSettings.enableAnimations) { if (extensionSettings.enableAnimations) {
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600); setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
} }
// Restore scroll position after re-render
if (preserveScroll) {
const $content = $thoughtsContainer.find('.rpg-thoughts-content');
if ($content.length) {
$content[0].scrollTop = savedContentScroll;
}
}
// Update chat overlay if enabled // Update chat overlay if enabled
if (extensionSettings.showThoughtsInChat) { if (extensionSettings.showThoughtsInChat) {
updateChatThoughts(); updateChatThoughts();
@@ -852,136 +788,6 @@ export function removeCharacter(characterName) {
renderThoughts(); renderThoughts();
} }
/**
* Adds a new blank character to Present Characters data.
* Creates a character with empty fields based on the tracker template.
*/
export function addNewCharacter() {
const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters;
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
const characterStats = presentCharsConfig?.characterStats;
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
const hasRelationship = presentCharsConfig?.relationshipFields?.length > 0;
// Check if data is in JSON format
let isJSON = false;
let parsedData = null;
try {
parsedData = typeof lastGeneratedData.characterThoughts === 'string'
? JSON.parse(lastGeneratedData.characterThoughts)
: lastGeneratedData.characterThoughts;
if (Array.isArray(parsedData) || (parsedData && parsedData.characters)) {
isJSON = true;
}
} catch (e) {
// Not JSON, treat as text format
}
if (isJSON) {
// JSON format - add new character object
const charactersArray = Array.isArray(parsedData) ? parsedData : (parsedData.characters || []);
const newCharacter = {
name: 'New Character',
emoji: '👤',
details: {}
};
// Add all enabled custom fields as empty
for (const field of enabledFields) {
newCharacter.details[field.name] = '';
}
// Add relationship if enabled
if (hasRelationship) {
newCharacter.relationship = 'Neutral';
}
// Add stats if enabled
if (enabledCharStats.length > 0) {
newCharacter.stats = {};
for (const stat of enabledCharStats) {
newCharacter.stats[stat.name] = 100;
}
}
charactersArray.push(newCharacter);
// Save back as JSON string
lastGeneratedData.characterThoughts = JSON.stringify(
Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray },
null,
2
);
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
} else {
// Text format - add new character block
const lines = lastGeneratedData.characterThoughts.split('\n');
const dividerIndex = lines.findIndex(line => line.includes('---'));
if (dividerIndex >= 0) {
const newCharacterLines = ['- New Character'];
// Add custom detail fields as standalone lines
for (const customField of enabledFields) {
newCharacterLines.push(` ${customField.name}: `);
}
// Add Relationship field if enabled
if (hasRelationship) {
newCharacterLines.push(` Relationship: Neutral`);
}
// Add Stats if enabled
if (enabledCharStats.length > 0) {
const statsParts = enabledCharStats.map(s => `${s.name}: 100%`);
newCharacterLines.push(` Stats: ${statsParts.join(' | ')}`);
}
// Find the last character and add after it, or after divider if no characters
let insertIndex = dividerIndex + 1;
for (let i = lines.length - 1; i > dividerIndex; i--) {
if (lines[i].trim().startsWith('- ')) {
// Find the end of this character block
insertIndex = i + 1;
while (insertIndex < lines.length && lines[insertIndex].trim() && !lines[insertIndex].trim().startsWith('- ')) {
insertIndex++;
}
break;
}
}
lines.splice(insertIndex, 0, ...newCharacterLines);
lastGeneratedData.characterThoughts = lines.join('\n');
committedTrackerData.characterThoughts = lines.join('\n');
}
}
// Update message 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].characterThoughts = lastGeneratedData.characterThoughts;
}
}
break;
}
}
}
saveChatData();
// Re-render to show new character
renderThoughts();
}
/** /**
* Updates a specific character field in Present Characters data and re-renders. * Updates a specific character field in Present Characters data and re-renders.
* Works with the new multi-line format. * Works with the new multi-line format.
@@ -1049,27 +855,18 @@ export function updateCharacterField(characterName, field, value) {
} else if (field === 'emoji') { } else if (field === 'emoji') {
char.emoji = value; char.emoji = value;
} else if (field === 'Relationship') { } else if (field === 'Relationship') {
// Store relationship in the correct nested format // Store relationship as text, converting emoji if needed
// Remove old flat format if it exists
if (char.Relationship) {
delete char.Relationship;
}
// First check if it's an emoji → convert to text // First check if it's an emoji → convert to text
let relationshipValue;
if (emojiToRelationship[value]) { if (emojiToRelationship[value]) {
relationshipValue = emojiToRelationship[value]; char.Relationship = emojiToRelationship[value];
} else { } else {
// It's text - find matching relationship name (case-insensitive) // It's text - find matching relationship name (case-insensitive)
const matchingRelationship = Object.keys(relationshipEmojis).find( const matchingRelationship = Object.keys(relationshipEmojis).find(
name => name.toLowerCase() === value.toLowerCase() name => name.toLowerCase() === value.toLowerCase()
); );
relationshipValue = matchingRelationship || value; char.Relationship = matchingRelationship || value;
} }
// console.log('[RPG Companion] After update - char.Relationship:', char.Relationship);
// Store in the correct nested format
char.relationship = { status: relationshipValue };
// console.log('[RPG Companion] After update - char.relationship:', char.relationship);
// console.log('[RPG Companion] relationshipEmojis:', relationshipEmojis); // console.log('[RPG Companion] relationshipEmojis:', relationshipEmojis);
// console.log('[RPG Companion] emojiToRelationship:', emojiToRelationship); // console.log('[RPG Companion] emojiToRelationship:', emojiToRelationship);
} else if (field.toLowerCase() === 'thoughts' || field === (presentCharsConfig?.thoughts?.name || 'Thoughts')) { } else if (field.toLowerCase() === 'thoughts' || field === (presentCharsConfig?.thoughts?.name || 'Thoughts')) {
@@ -1079,64 +876,21 @@ export function updateCharacterField(characterName, field, value) {
// Check if it's a character stat // Check if it's a character stat
const isStatField = enabledCharStats.findIndex(s => s.name === field) !== -1; const isStatField = enabledCharStats.findIndex(s => s.name === field) !== -1;
if (isStatField) { if (isStatField) {
if (!char.stats) char.stats = {};
let numValue = parseInt(value.replace('%', '').trim()); let numValue = parseInt(value.replace('%', '').trim());
if (isNaN(numValue)) numValue = 0; if (isNaN(numValue)) numValue = 0;
numValue = Math.max(0, Math.min(100, numValue)); numValue = Math.max(0, Math.min(100, numValue));
// Handle both array format (from LLM) and object format
if (Array.isArray(char.stats)) {
// Array format: [{name: "Health", value: 80}]
const statIndex = char.stats.findIndex(s => s.name === field);
if (statIndex !== -1) {
char.stats[statIndex].value = numValue;
} else {
// Stat not found in array - add it
char.stats.push({ name: field, value: numValue });
}
} else {
// Object format: {Health: 80} or undefined
if (!char.stats) char.stats = {};
char.stats[field] = numValue; char.stats[field] = numValue;
}
} else { } else {
// It's a custom detail field - store in details object // It's a custom detail field
if (!char.details) char.details = {}; if (!char.details) char.details = {};
char.details[field] = value; char.details[field] = value;
// Clean up snake_case version if it exists (from AI generation)
const fieldKey = toSnakeCase(field);
if (fieldKey !== field && char.details[fieldKey] !== undefined) {
delete char.details[fieldKey];
}
// Clean up old root-level field if it exists (from v2 format)
if (char[field] !== undefined && field !== 'name' && field !== 'emoji') {
delete char[field];
}
if (char[fieldKey] !== undefined && fieldKey !== 'name' && fieldKey !== 'emoji') {
delete char[fieldKey];
} }
} }
} }
// Clean up ALL duplicate snake_case fields in details (not just the edited field) // Save back to lastGeneratedData
// This prevents duplicates from AI-generated data lastGeneratedData.characterThoughts = Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray };
if (char.details) {
for (const customField of enabledFields) {
const fieldName = customField.name;
const snakeCaseKey = toSnakeCase(fieldName);
// If both versions exist, keep the properly-cased one and remove snake_case
if (snakeCaseKey !== fieldName &&
char.details[fieldName] !== undefined &&
char.details[snakeCaseKey] !== undefined) {
delete char.details[snakeCaseKey];
}
}
}
}
// Save back to lastGeneratedData as JSON string (consistent with infoBox and userStats)
lastGeneratedData.characterThoughts = JSON.stringify(Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray }, null, 2);
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts; committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
// console.log('[RPG Companion] Saved to lastGeneratedData.characterThoughts:', JSON.stringify(lastGeneratedData.characterThoughts)); // console.log('[RPG Companion] Saved to lastGeneratedData.characterThoughts:', JSON.stringify(lastGeneratedData.characterThoughts));
@@ -1164,8 +918,8 @@ export function updateCharacterField(characterName, field, value) {
// console.log('[RPG Companion] JSON format updated successfully'); // console.log('[RPG Companion] JSON format updated successfully');
// console.log('[RPG Companion] Updated data:', lastGeneratedData.characterThoughts); // console.log('[RPG Companion] Updated data:', lastGeneratedData.characterThoughts);
// Re-render the thoughts panel to show updated value (preserve scroll position) // Re-render the thoughts panel to show updated value
renderThoughts({ preserveScroll: true }); renderThoughts();
// Update chat thought overlays if editing thoughts // Update chat thought overlays if editing thoughts
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts'; const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
@@ -1217,9 +971,6 @@ export function updateCharacterField(characterName, field, value) {
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts'; const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
const isThoughtsField = field.toLowerCase() === 'thoughts' || field === thoughtsFieldName; const isThoughtsField = field.toLowerCase() === 'thoughts' || field === thoughtsFieldName;
// Track if field was found and updated
let fieldUpdated = false;
// First pass: check if Stats line exists and update other fields // First pass: check if Stats line exists and update other fields
for (let i = characterStartIndex; i < characterEndIndex; i++) { for (let i = characterStartIndex; i < characterEndIndex; i++) {
const line = lines[i].trim(); const line = lines[i].trim();
@@ -1227,37 +978,35 @@ export function updateCharacterField(characterName, field, value) {
if (line.startsWith('Stats:')) { if (line.startsWith('Stats:')) {
statsLineExists = true; statsLineExists = true;
statsLineIndex = i; statsLineIndex = i;
continue; // Skip to next line
} }
// Check for name update
if (field === 'name' && line.startsWith('- ')) { if (field === 'name' && line.startsWith('- ')) {
lines[i] = `- ${value}`; lines[i] = `- ${value}`;
fieldUpdated = true;
continue;
} }
else if (field === 'emoji' && line.startsWith('Details:')) {
// Check for Relationship field const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim());
if (field === 'Relationship' && line.startsWith('Relationship:')) { parts[0] = value;
lines[i] = `Details: ${parts.join(' | ')}`;
}
else if (line.startsWith('Details:')) {
const fieldIndex = enabledFields.findIndex(f => f.name === field);
if (fieldIndex !== -1) {
const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim());
if (parts.length > fieldIndex + 1) {
parts[fieldIndex + 1] = value;
lines[i] = `Details: ${parts.join(' | ')}`;
}
}
}
else if (field === 'Relationship' && line.startsWith('Relationship:')) {
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' }; const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
const relationshipValue = emojiToRelationship[value] || value; const relationshipValue = emojiToRelationship[value] || value;
lines[i] = `Relationship: ${relationshipValue}`; lines[i] = `Relationship: ${relationshipValue}`;
fieldUpdated = true;
continue;
} }
else if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
// Check for Thoughts field // Update thoughts field
if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
lines[i] = `${thoughtsFieldName}: ${value}`; lines[i] = `${thoughtsFieldName}: ${value}`;
fieldUpdated = true; // console.log('[RPG Companion] Updated thoughts:', lines[i]);
continue;
}
// Check for v3 text format standalone field lines (e.g., "Appearance: ...", "Demeanor: ...")
if (line.startsWith(field + ':')) {
lines[i] = ` ${field}: ${value}`;
fieldUpdated = true;
// Don't break - update ALL instances of this field (in case of duplicates from previous bugs)
} }
} }
@@ -1324,28 +1073,23 @@ export function updateCharacterField(characterName, field, value) {
} }
} }
} else { } else {
// Create new character block (v3 text format only) // Create new character block
const dividerIndex = lines.findIndex(line => line.includes('---')); const dividerIndex = lines.findIndex(line => line.includes('---'));
if (dividerIndex >= 0) { if (dividerIndex >= 0) {
const newCharacterLines = [`- ${characterName}`]; const newCharacterLines = [`- ${characterName}`];
// Add custom detail fields as standalone lines let detailsParts = [field === 'emoji' ? value : '😊'];
for (const customField of enabledFields) { for (let i = 0; i < enabledFields.length; i++) {
if (field === customField.name) { detailsParts.push(field === enabledFields[i].name ? value : '');
newCharacterLines.push(` ${customField.name}: ${value}`);
} else {
newCharacterLines.push(` ${customField.name}: `);
}
} }
newCharacterLines.push(`Details: ${detailsParts.join(' | ')}`);
// Add Relationship field if enabled
if (presentCharsConfig?.relationshipFields?.length > 0) { if (presentCharsConfig?.relationshipFields?.length > 0) {
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' }; const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
const relationshipValue = field === 'Relationship' ? (emojiToRelationship[value] || value) : 'Neutral'; const relationshipValue = field === 'Relationship' ? (emojiToRelationship[value] || value) : 'Neutral';
newCharacterLines.push(`Relationship: ${relationshipValue}`); newCharacterLines.push(`Relationship: ${relationshipValue}`);
} }
// Add Stats if enabled
if (enabledCharStats.length > 0) { if (enabledCharStats.length > 0) {
const statsParts = enabledCharStats.map(s => { const statsParts = enabledCharStats.map(s => {
if (field === s.name) { if (field === s.name) {
+6 -25
View File
@@ -21,21 +21,6 @@ import { getSafeThumbnailUrl } from '../../utils/avatars.js';
import { buildInventorySummary } from '../generation/promptBuilder.js'; import { buildInventorySummary } from '../generation/promptBuilder.js';
import { isItemLocked, setItemLock } from '../generation/lockManager.js'; import { isItemLocked, setItemLock } from '../generation/lockManager.js';
import { updateFabWidgets } from '../ui/mobile.js'; import { updateFabWidgets } from '../ui/mobile.js';
import { getStatBarColors } from '../ui/theme.js';
/**
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
* Example: "Conditions (up to 5 traits)" -> "conditions"
* @param {string} name - Field name, possibly with parenthetical description
* @returns {string} snake_case key from the base name only
*/
function toFieldKey(name) {
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
return baseName
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');
}
/** /**
* Builds the user stats text string using custom stat names * Builds the user stats text string using custom stat names
@@ -121,7 +106,7 @@ function updateUserStatsData() {
// Then, add any other numeric stats from extensionSettings that aren't in config // Then, add any other numeric stats from extensionSettings that aren't in config
// (these could be custom stats the AI added or disabled stats) // (these could be custom stats the AI added or disabled stats)
const customFields = config.statusSection?.customFields || []; const customFields = config.statusSection?.customFields || [];
const excludeFields = new Set(['mood', ...customFields.map(f => toFieldKey(f)), 'inventory', 'skills', 'level']); const excludeFields = new Set(['mood', ...customFields.map(f => f.toLowerCase()), 'inventory', 'skills', 'level']);
Object.entries(stats).forEach(([key, value]) => { Object.entries(stats).forEach(([key, value]) => {
if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') { if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') {
statsArray.push({ statsArray.push({
@@ -141,7 +126,7 @@ function updateUserStatsData() {
// Add all custom status fields // Add all custom status fields
for (const fieldName of customFields) { for (const fieldName of customFields) {
const fieldKey = toFieldKey(fieldName); const fieldKey = fieldName.toLowerCase();
jsonData.status[fieldKey] = stats[fieldKey] || 'None'; jsonData.status[fieldKey] = stats[fieldKey] || 'None';
} }
@@ -266,9 +251,8 @@ export function renderUserStats() {
} }
} }
// Create gradient from low to high color with opacity // Create gradient from low to high color
const colors = getStatBarColors(); const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
const gradient = `linear-gradient(to right, ${colors.low}, ${colors.high})`;
// Check if stats bars section is locked // Check if stats bars section is locked
const isStatsLocked = isItemLocked('userStats', 'stats'); const isStatsLocked = isItemLocked('userStats', 'stats');
@@ -348,13 +332,10 @@ export function renderUserStats() {
// Render custom status fields // Render custom status fields
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) { if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
for (const fieldName of config.statusSection.customFields) { for (const fieldName of config.statusSection.customFields) {
const fieldKey = toFieldKey(fieldName); const fieldKey = fieldName.toLowerCase();
let fieldValue = stats[fieldKey] || 'None'; let fieldValue = stats[fieldKey] || 'None';
// Handle array format (from JSON)
if (Array.isArray(fieldValue)) {
fieldValue = fieldValue.join(', ') || 'None';
} else if (typeof fieldValue === 'string') {
// Strip brackets if present (from JSON array format) // Strip brackets if present (from JSON array format)
if (typeof fieldValue === 'string') {
fieldValue = fieldValue.replace(/^\[|\]$/g, '').trim(); fieldValue = fieldValue.replace(/^\[|\]$/g, '').trim();
} }
html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="${fieldKey}" title="Click to edit ${fieldName}">${fieldValue}</div>`; html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="${fieldKey}" title="Click to edit ${fieldName}">${fieldValue}</div>`;
+2 -6
View File
@@ -5,7 +5,6 @@
import { i18n } from '../../core/i18n.js'; import { i18n } from '../../core/i18n.js';
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js'; import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
import { hexToRgba } from './theme.js';
/** /**
* Helper to parse time string and calculate clock hand angles * Helper to parse time string and calculate clock hand angles
@@ -238,9 +237,7 @@ export function updateStripWidgets() {
*/ */
function getStatColor(value) { function getStatColor(value) {
const lowColor = extensionSettings.statBarColorLow || '#cc3333'; const lowColor = extensionSettings.statBarColorLow || '#cc3333';
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
const highColor = extensionSettings.statBarColorHigh || '#33cc66'; const highColor = extensionSettings.statBarColorHigh || '#33cc66';
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
// Simple linear interpolation between low and high colors // Simple linear interpolation between low and high colors
const percent = Math.min(100, Math.max(0, value)) / 100; const percent = Math.min(100, Math.max(0, value)) / 100;
@@ -249,14 +246,13 @@ function getStatColor(value) {
const lowRGB = hexToRgb(lowColor); const lowRGB = hexToRgb(lowColor);
const highRGB = hexToRgb(highColor); const highRGB = hexToRgb(highColor);
if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity); if (!lowRGB || !highRGB) return value > 50 ? highColor : lowColor;
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent); const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent); const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent); const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
return `rgba(${r}, ${g}, ${b}, ${a})`; return `rgb(${r}, ${g}, ${b})`;
} }
/** /**
+4 -5
View File
@@ -4,8 +4,7 @@
*/ */
import { getContext } from '../../../../../../extensions.js'; import { getContext } from '../../../../../../extensions.js';
import { chat, saveChatDebounced, characters, this_chid, user_avatar } from '../../../../../../../script.js'; import { generateRaw, chat, saveChatDebounced, characters, this_chid, user_avatar } from '../../../../../../../script.js';
import { safeGenerateRaw } from '../../utils/responseExtractor.js';
import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js'; import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js';
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
import { extensionSettings } from '../../core/state.js'; import { extensionSettings } from '../../core/state.js';
@@ -82,7 +81,7 @@ export class EncounterModal {
// Store request for potential regeneration // Store request for potential regeneration
this.lastRequest = { type: 'init', prompt: initPrompt }; this.lastRequest = { type: 'init', prompt: initPrompt };
const response = await safeGenerateRaw({ const response = await generateRaw({
prompt: initPrompt, prompt: initPrompt,
quietToLoud: false quietToLoud: false
}); });
@@ -817,7 +816,7 @@ export class EncounterModal {
// Store request for potential regeneration // Store request for potential regeneration
this.lastRequest = { type: 'action', action, prompt: actionPrompt }; this.lastRequest = { type: 'action', action, prompt: actionPrompt };
const response = await safeGenerateRaw({ const response = await generateRaw({
prompt: actionPrompt, prompt: actionPrompt,
quietToLoud: false quietToLoud: false
}); });
@@ -1079,7 +1078,7 @@ export class EncounterModal {
// Generate summary // Generate summary
const summaryPrompt = await buildCombatSummaryPrompt(currentEncounter.encounterLog, result); const summaryPrompt = await buildCombatSummaryPrompt(currentEncounter.encounterLog, result);
const summaryResponse = await safeGenerateRaw({ const summaryResponse = await generateRaw({
prompt: summaryPrompt, prompt: summaryPrompt,
quietToLoud: false quietToLoud: false
}); });
+2 -11
View File
@@ -8,7 +8,6 @@ import { saveSettings } from '../../core/persistence.js';
import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js'; import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js';
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js'; import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
import { i18n } from '../../core/i18n.js'; import { i18n } from '../../core/i18n.js';
import { hexToRgba } from './theme.js';
/** /**
* Updates the text labels of the mobile navigation tabs based on the current language. * Updates the text labels of the mobile navigation tabs based on the current language.
@@ -794,17 +793,12 @@ export function setupMobileKeyboardHandling() {
/** /**
* Handles focus on contenteditable fields to ensure they're visible when keyboard appears. * Handles focus on contenteditable fields to ensure they're visible when keyboard appears.
* Uses smooth scrolling to bring focused field into view with proper padding. * Uses smooth scrolling to bring focused field into view with proper padding.
* Only applies on mobile viewports where virtual keyboard can obscure content.
*/ */
export function setupContentEditableScrolling() { export function setupContentEditableScrolling() {
const $panel = $('#rpg-companion-panel'); const $panel = $('#rpg-companion-panel');
// Use event delegation for all contenteditable fields // Use event delegation for all contenteditable fields
$panel.on('focusin', '[contenteditable="true"]', function(e) { $panel.on('focusin', '[contenteditable="true"]', function(e) {
// Only apply scrolling behavior on mobile (where virtual keyboard appears)
const isMobile = window.innerWidth <= 1000;
if (!isMobile) return;
const $field = $(this); const $field = $(this);
// Small delay to let keyboard animate in // Small delay to let keyboard animate in
@@ -1573,9 +1567,7 @@ export function updateFabWidgets() {
*/ */
function getStatColor(value) { function getStatColor(value) {
const lowColor = extensionSettings.statBarColorLow || '#cc3333'; const lowColor = extensionSettings.statBarColorLow || '#cc3333';
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
const highColor = extensionSettings.statBarColorHigh || '#33cc66'; const highColor = extensionSettings.statBarColorHigh || '#33cc66';
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
// Simple linear interpolation between low and high colors // Simple linear interpolation between low and high colors
const percent = Math.min(100, Math.max(0, value)) / 100; const percent = Math.min(100, Math.max(0, value)) / 100;
@@ -1584,14 +1576,13 @@ function getStatColor(value) {
const lowRGB = hexToRgb(lowColor); const lowRGB = hexToRgb(lowColor);
const highRGB = hexToRgb(highColor); const highRGB = hexToRgb(highColor);
if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity); if (!lowRGB || !highRGB) return value > 50 ? highColor : lowColor;
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent); const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent); const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent); const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
return `rgba(${r}, ${g}, ${b}, ${a})`; return `rgb(${r}, ${g}, ${b})`;
} }
/** /**
+1 -19
View File
@@ -4,7 +4,7 @@
*/ */
import { extensionSettings } from '../../core/state.js'; import { extensionSettings } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js'; import { saveSettings } from '../../core/persistence.js';
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_OMNISCIENCE_FILTER_PROMPT, DEFAULT_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT, DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT } from '../generation/promptBuilder.js'; import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT } from '../generation/promptBuilder.js';
let $editorModal = null; let $editorModal = null;
let tempPrompts = null; // Temporary prompts for cancel functionality let tempPrompts = null; // Temporary prompts for cancel functionality
@@ -14,11 +14,9 @@ const DEFAULT_PROMPTS = {
html: DEFAULT_HTML_PROMPT, html: DEFAULT_HTML_PROMPT,
dialogueColoring: DEFAULT_DIALOGUE_COLORING_PROMPT, dialogueColoring: DEFAULT_DIALOGUE_COLORING_PROMPT,
deception: DEFAULT_DECEPTION_PROMPT, deception: DEFAULT_DECEPTION_PROMPT,
omniscience: DEFAULT_OMNISCIENCE_FILTER_PROMPT,
cyoa: DEFAULT_CYOA_PROMPT, cyoa: DEFAULT_CYOA_PROMPT,
spotify: DEFAULT_SPOTIFY_PROMPT, spotify: DEFAULT_SPOTIFY_PROMPT,
narrator: DEFAULT_NARRATOR_PROMPT, narrator: DEFAULT_NARRATOR_PROMPT,
contextInstructions: DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT,
plotRandom: '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.', plotRandom: '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.',
plotNatural: '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.', plotNatural: '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.',
avatar: `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons; however, your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable. avatar: `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons; however, your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable.
@@ -98,11 +96,9 @@ function openPromptsEditor() {
html: extensionSettings.customHtmlPrompt || '', html: extensionSettings.customHtmlPrompt || '',
dialogueColoring: extensionSettings.customDialogueColoringPrompt || '', dialogueColoring: extensionSettings.customDialogueColoringPrompt || '',
deception: extensionSettings.customDeceptionPrompt || '', deception: extensionSettings.customDeceptionPrompt || '',
omniscience: extensionSettings.customOmnisciencePrompt || '',
cyoa: extensionSettings.customCYOAPrompt || '', cyoa: extensionSettings.customCYOAPrompt || '',
spotify: extensionSettings.customSpotifyPrompt || '', spotify: extensionSettings.customSpotifyPrompt || '',
narrator: extensionSettings.customNarratorPrompt || '', narrator: extensionSettings.customNarratorPrompt || '',
contextInstructions: extensionSettings.customContextInstructionsPrompt || '',
plotRandom: extensionSettings.customPlotRandomPrompt || '', plotRandom: extensionSettings.customPlotRandomPrompt || '',
plotNatural: extensionSettings.customPlotNaturalPrompt || '', plotNatural: extensionSettings.customPlotNaturalPrompt || '',
avatar: extensionSettings.avatarLLMCustomInstruction || '', avatar: extensionSettings.avatarLLMCustomInstruction || '',
@@ -115,11 +111,9 @@ function openPromptsEditor() {
$('#rpg-prompt-html').val(extensionSettings.customHtmlPrompt || DEFAULT_PROMPTS.html); $('#rpg-prompt-html').val(extensionSettings.customHtmlPrompt || DEFAULT_PROMPTS.html);
$('#rpg-prompt-dialogue-coloring').val(extensionSettings.customDialogueColoringPrompt || DEFAULT_PROMPTS.dialogueColoring); $('#rpg-prompt-dialogue-coloring').val(extensionSettings.customDialogueColoringPrompt || DEFAULT_PROMPTS.dialogueColoring);
$('#rpg-prompt-deception').val(extensionSettings.customDeceptionPrompt || DEFAULT_PROMPTS.deception); $('#rpg-prompt-deception').val(extensionSettings.customDeceptionPrompt || DEFAULT_PROMPTS.deception);
$('#rpg-prompt-omniscience').val(extensionSettings.customOmnisciencePrompt || DEFAULT_PROMPTS.omniscience);
$('#rpg-prompt-cyoa').val(extensionSettings.customCYOAPrompt || DEFAULT_PROMPTS.cyoa); $('#rpg-prompt-cyoa').val(extensionSettings.customCYOAPrompt || DEFAULT_PROMPTS.cyoa);
$('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify); $('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify);
$('#rpg-prompt-narrator').val(extensionSettings.customNarratorPrompt || DEFAULT_PROMPTS.narrator); $('#rpg-prompt-narrator').val(extensionSettings.customNarratorPrompt || DEFAULT_PROMPTS.narrator);
$('#rpg-prompt-context-instructions').val(extensionSettings.customContextInstructionsPrompt || DEFAULT_PROMPTS.contextInstructions);
$('#rpg-prompt-plot-random').val(extensionSettings.customPlotRandomPrompt || DEFAULT_PROMPTS.plotRandom); $('#rpg-prompt-plot-random').val(extensionSettings.customPlotRandomPrompt || DEFAULT_PROMPTS.plotRandom);
$('#rpg-prompt-plot-natural').val(extensionSettings.customPlotNaturalPrompt || DEFAULT_PROMPTS.plotNatural); $('#rpg-prompt-plot-natural').val(extensionSettings.customPlotNaturalPrompt || DEFAULT_PROMPTS.plotNatural);
$('#rpg-prompt-avatar').val(extensionSettings.avatarLLMCustomInstruction || DEFAULT_PROMPTS.avatar); $('#rpg-prompt-avatar').val(extensionSettings.avatarLLMCustomInstruction || DEFAULT_PROMPTS.avatar);
@@ -156,11 +150,9 @@ function savePrompts() {
extensionSettings.customHtmlPrompt = $('#rpg-prompt-html').val().trim(); extensionSettings.customHtmlPrompt = $('#rpg-prompt-html').val().trim();
extensionSettings.customDialogueColoringPrompt = $('#rpg-prompt-dialogue-coloring').val().trim(); extensionSettings.customDialogueColoringPrompt = $('#rpg-prompt-dialogue-coloring').val().trim();
extensionSettings.customDeceptionPrompt = $('#rpg-prompt-deception').val().trim(); extensionSettings.customDeceptionPrompt = $('#rpg-prompt-deception').val().trim();
extensionSettings.customOmnisciencePrompt = $('#rpg-prompt-omniscience').val().trim();
extensionSettings.customCYOAPrompt = $('#rpg-prompt-cyoa').val().trim(); extensionSettings.customCYOAPrompt = $('#rpg-prompt-cyoa').val().trim();
extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').val().trim(); extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').val().trim();
extensionSettings.customNarratorPrompt = $('#rpg-prompt-narrator').val().trim(); extensionSettings.customNarratorPrompt = $('#rpg-prompt-narrator').val().trim();
extensionSettings.customContextInstructionsPrompt = $('#rpg-prompt-context-instructions').val().trim();
extensionSettings.customPlotRandomPrompt = $('#rpg-prompt-plot-random').val().trim(); extensionSettings.customPlotRandomPrompt = $('#rpg-prompt-plot-random').val().trim();
extensionSettings.customPlotNaturalPrompt = $('#rpg-prompt-plot-natural').val().trim(); extensionSettings.customPlotNaturalPrompt = $('#rpg-prompt-plot-natural').val().trim();
extensionSettings.avatarLLMCustomInstruction = $('#rpg-prompt-avatar').val().trim(); extensionSettings.avatarLLMCustomInstruction = $('#rpg-prompt-avatar').val().trim();
@@ -190,9 +182,6 @@ function restorePromptToDefault(promptType) {
case 'deception': case 'deception':
extensionSettings.customDeceptionPrompt = ''; extensionSettings.customDeceptionPrompt = '';
break; break;
case 'omniscience':
extensionSettings.customOmnisciencePrompt = '';
break;
case 'cyoa': case 'cyoa':
extensionSettings.customCYOAPrompt = ''; extensionSettings.customCYOAPrompt = '';
break; break;
@@ -202,9 +191,6 @@ function restorePromptToDefault(promptType) {
case 'narrator': case 'narrator':
extensionSettings.customNarratorPrompt = ''; extensionSettings.customNarratorPrompt = '';
break; break;
case 'contextInstructions':
extensionSettings.customContextInstructionsPrompt = '';
break;
case 'plotRandom': case 'plotRandom':
extensionSettings.customPlotRandomPrompt = ''; extensionSettings.customPlotRandomPrompt = '';
break; break;
@@ -235,11 +221,9 @@ function restoreAllToDefaults() {
$('#rpg-prompt-html').val(DEFAULT_PROMPTS.html); $('#rpg-prompt-html').val(DEFAULT_PROMPTS.html);
$('#rpg-prompt-dialogue-coloring').val(DEFAULT_PROMPTS.dialogueColoring); $('#rpg-prompt-dialogue-coloring').val(DEFAULT_PROMPTS.dialogueColoring);
$('#rpg-prompt-deception').val(DEFAULT_PROMPTS.deception); $('#rpg-prompt-deception').val(DEFAULT_PROMPTS.deception);
$('#rpg-prompt-omniscience').val(DEFAULT_PROMPTS.omniscience);
$('#rpg-prompt-cyoa').val(DEFAULT_PROMPTS.cyoa); $('#rpg-prompt-cyoa').val(DEFAULT_PROMPTS.cyoa);
$('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify); $('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify);
$('#rpg-prompt-narrator').val(DEFAULT_PROMPTS.narrator); $('#rpg-prompt-narrator').val(DEFAULT_PROMPTS.narrator);
$('#rpg-prompt-context-instructions').val(DEFAULT_PROMPTS.contextInstructions);
$('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom); $('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom);
$('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural); $('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural);
$('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar); $('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar);
@@ -251,11 +235,9 @@ function restoreAllToDefaults() {
extensionSettings.customHtmlPrompt = ''; extensionSettings.customHtmlPrompt = '';
extensionSettings.customDialogueColoringPrompt = ''; extensionSettings.customDialogueColoringPrompt = '';
extensionSettings.customDeceptionPrompt = ''; extensionSettings.customDeceptionPrompt = '';
extensionSettings.customOmnisciencePrompt = '';
extensionSettings.customCYOAPrompt = ''; extensionSettings.customCYOAPrompt = '';
extensionSettings.customSpotifyPrompt = ''; extensionSettings.customSpotifyPrompt = '';
extensionSettings.customNarratorPrompt = ''; extensionSettings.customNarratorPrompt = '';
extensionSettings.customContextInstructionsPrompt = '';
extensionSettings.customPlotRandomPrompt = ''; extensionSettings.customPlotRandomPrompt = '';
extensionSettings.customPlotNaturalPrompt = ''; extensionSettings.customPlotNaturalPrompt = '';
extensionSettings.avatarLLMCustomInstruction = ''; extensionSettings.avatarLLMCustomInstruction = '';
+12 -55
View File
@@ -5,37 +5,6 @@
import { extensionSettings, $panelContainer } from '../../core/state.js'; import { extensionSettings, $panelContainer } from '../../core/state.js';
/**
* Converts hex color and opacity percentage to rgba string
* @param {string} hex - Hex color (e.g., '#ff0000')
* @param {number} opacity - Opacity percentage (0-100)
* @returns {string} - RGBA color string
*/
export function hexToRgba(hex, opacity = 100) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const a = opacity / 100;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
/**
* Gets stat bar colors with opacity applied
* @returns {{low: string, high: string}} RGBA color strings for stat bars
*/
export function getStatBarColors() {
return {
low: hexToRgba(
extensionSettings.statBarColorLow || '#cc3333',
extensionSettings.statBarColorLowOpacity ?? 100
),
high: hexToRgba(
extensionSettings.statBarColorHigh || '#33cc66',
extensionSettings.statBarColorHighOpacity ?? 100
)
};
}
/** /**
* Applies the selected theme to the panel. * Applies the selected theme to the panel.
*/ */
@@ -106,33 +75,24 @@ export function applyCustomTheme() {
const colors = extensionSettings.customColors; const colors = extensionSettings.customColors;
// Convert hex colors with opacity to rgba
const bgColor = hexToRgba(colors.bg, colors.bgOpacity ?? 100);
const accentColor = hexToRgba(colors.accent, colors.accentOpacity ?? 100);
const textColor = hexToRgba(colors.text, colors.textOpacity ?? 100);
const highlightColor = hexToRgba(colors.highlight, colors.highlightOpacity ?? 100);
// Create shadow with 50% opacity of highlight color
const shadowColor = hexToRgba(colors.highlight, (colors.highlightOpacity ?? 100) * 0.5);
// Apply custom CSS variables as inline styles to main panel // Apply custom CSS variables as inline styles to main panel
$panelContainer.css({ $panelContainer.css({
'--rpg-bg': bgColor, '--rpg-bg': colors.bg,
'--rpg-accent': accentColor, '--rpg-accent': colors.accent,
'--rpg-text': textColor, '--rpg-text': colors.text,
'--rpg-highlight': highlightColor, '--rpg-highlight': colors.highlight,
'--rpg-border': highlightColor, '--rpg-border': colors.highlight,
'--rpg-shadow': shadowColor '--rpg-shadow': `${colors.highlight}80` // Add alpha for shadow
}); });
// Apply custom colors to mobile toggle and thought elements // Apply custom colors to mobile toggle and thought elements
const customStyles = { const customStyles = {
'--rpg-bg': bgColor, '--rpg-bg': colors.bg,
'--rpg-accent': accentColor, '--rpg-accent': colors.accent,
'--rpg-text': textColor, '--rpg-text': colors.text,
'--rpg-highlight': highlightColor, '--rpg-highlight': colors.highlight,
'--rpg-border': highlightColor, '--rpg-border': colors.highlight,
'--rpg-shadow': shadowColor '--rpg-shadow': `${colors.highlight}80`
}; };
const $mobileToggle = $('#rpg-mobile-toggle'); const $mobileToggle = $('#rpg-mobile-toggle');
@@ -179,7 +139,6 @@ export function updateFeatureTogglesVisibility() {
const $htmlToggle = $('#rpg-html-toggle-wrapper'); const $htmlToggle = $('#rpg-html-toggle-wrapper');
const $dialogueColoringToggle = $('#rpg-dialogue-coloring-toggle-wrapper'); const $dialogueColoringToggle = $('#rpg-dialogue-coloring-toggle-wrapper');
const $deceptionToggle = $('#rpg-deception-toggle-wrapper'); const $deceptionToggle = $('#rpg-deception-toggle-wrapper');
const $omniscienceToggle = $('#rpg-omniscience-toggle-wrapper');
const $cyoaToggle = $('#rpg-cyoa-toggle-wrapper'); const $cyoaToggle = $('#rpg-cyoa-toggle-wrapper');
const $spotifyToggle = $('#rpg-spotify-toggle-wrapper'); const $spotifyToggle = $('#rpg-spotify-toggle-wrapper');
@@ -191,7 +150,6 @@ export function updateFeatureTogglesVisibility() {
$htmlToggle.toggle(extensionSettings.showHtmlToggle); $htmlToggle.toggle(extensionSettings.showHtmlToggle);
$dialogueColoringToggle.toggle(extensionSettings.showDialogueColoringToggle); $dialogueColoringToggle.toggle(extensionSettings.showDialogueColoringToggle);
$deceptionToggle.toggle(extensionSettings.showDeceptionToggle ?? true); $deceptionToggle.toggle(extensionSettings.showDeceptionToggle ?? true);
$omniscienceToggle.toggle(extensionSettings.showOmniscienceToggle ?? true);
$cyoaToggle.toggle(extensionSettings.showCYOAToggle ?? true); $cyoaToggle.toggle(extensionSettings.showCYOAToggle ?? true);
$spotifyToggle.toggle(extensionSettings.showSpotifyToggle); $spotifyToggle.toggle(extensionSettings.showSpotifyToggle);
@@ -203,7 +161,6 @@ export function updateFeatureTogglesVisibility() {
const anyVisible = extensionSettings.showHtmlToggle || const anyVisible = extensionSettings.showHtmlToggle ||
extensionSettings.showDialogueColoringToggle || extensionSettings.showDialogueColoringToggle ||
(extensionSettings.showDeceptionToggle ?? true) || (extensionSettings.showDeceptionToggle ?? true) ||
(extensionSettings.showOmniscienceToggle ?? true) ||
(extensionSettings.showCYOAToggle ?? true) || (extensionSettings.showCYOAToggle ?? true) ||
extensionSettings.showSpotifyToggle || extensionSettings.showSpotifyToggle ||
extensionSettings.showDynamicWeatherToggle || extensionSettings.showDynamicWeatherToggle ||
+31 -228
View File
@@ -105,106 +105,41 @@ function getCurrentTime() {
return null; return null;
} }
// Patterns for specific weather conditions (order matters - combined effects first)
// Grouped by languages for easy editing
// EXPORTED: Used by jsonPromptHelpers.js to provide valid weather keywords to LLM
export const WEATHER_PATTERNS_BY_LANGUAGE = {
en: [
{ id: "blizzard", patterns: [ "blizzard" ] }, // Snow + Wind
{ id: "storm", patterns: [ "storm", "thunder", "lightning" ] }, // Rain + Lightning
{ id: "wind", patterns: [ "wind", "breeze", "gust", "gale" ] },
{ id: "snow", patterns: [ "snow", "flurries" ] },
{ id: "rain", patterns: [ "rain", "drizzle", "shower" ] },
{ id: "mist", patterns: [ "mist", "fog", "haze" ] },
{ id: "sunny", patterns: [ "sunny", "clear", "bright" ] },
{ id: "none", patterns: [ "cloud", "overcast", "indoor", "inside" ] },
],
ru: [
{ id: "blizzard", patterns: [ "метель" ] },
{ id: "storm", patterns: [ "гроза", "буря", "шторм" ] },
{ id: "wind", patterns: [ "ветер", "ветрено", "ветерок", "бриз", "легкий бриз", "слегка ветрено", "легкий ветер", "шквал,буря" ] },
{ id: "snow", patterns: [ "снег", "снегопад" ] },
{ id: "rain", patterns: [ "дождь", "морось", "ливень" ] },
{ id: "mist", patterns: [ "мгла", "туман", "туманно" ] },
{ id: "sunny", patterns: [ "солнечно", "ясно", "ярко", "ясное утро", "ясный день" ] },
{ id: "none", patterns: [ "облачно", "пасмурно", "в помещении", "внутри" ] },
],
}
/**
* Get valid weather keywords for LLM prompt injection.
* Returns weather patterns for specified language or all languages.
* This ensures LLM generates responses that exactly match our expected patterns.
*
* @param {string} [language] - Language code (e.g., 'en', 'ru'). If not specified, returns all languages.
* @returns {Object} Object with weather type IDs as keys and arrays of valid keywords as values
* @example
* // Returns: { blizzard: ["blizzard"], storm: ["storm", "thunder", "lightning"], ... }
* getWeatherKeywordsForPrompt('en');
*/
export function getWeatherKeywordsForPrompt(language) {
const result = {};
// Get patterns for specified language or merge all languages
const languagesToProcess = language && WEATHER_PATTERNS_BY_LANGUAGE[language]
? { [language]: WEATHER_PATTERNS_BY_LANGUAGE[language] }
: WEATHER_PATTERNS_BY_LANGUAGE;
for (const [lang, patterns] of Object.entries(languagesToProcess)) {
for (const { id, patterns: keywords } of patterns) {
if (!result[id]) {
result[id] = [];
}
// Add keywords, avoiding duplicates
for (const keyword of keywords) {
if (!result[id].includes(keyword)) {
result[id].push(keyword);
}
}
}
}
return result;
}
/**
* Get weather keywords as a formatted string for LLM instructions.
* Provides a clear template showing valid weather forecast values.
*
* @param {string} [language] - Language code. If not specified, uses all available patterns.
* @returns {string} Formatted string for prompt injection
* @example
* // Returns: 'Valid forecast values: "blizzard", "storm", "thunder", "lightning", "wind", ...'
* getWeatherKeywordsAsPromptString('en');
*/
export function getWeatherKeywordsAsPromptString(language) {
const keywords = getWeatherKeywordsForPrompt(language);
const allKeywords = [];
for (const patterns of Object.values(keywords)) {
allKeywords.push(...patterns);
}
return `Valid forecast values (use one of these exactly): ${allKeywords.map(k => `"${k}"`).join(', ')}`;
}
/** /**
* Parse weather text to determine effect type * Parse weather text to determine effect type
*/ */
function parseWeatherType(weatherText) { function parseWeatherType(weatherText) {
if (!weatherText) return "none"; if (!weatherText) return 'none';
const text = weatherText.toLowerCase(); const text = weatherText.toLowerCase();
for (const language of Object.values(WEATHER_PATTERNS_BY_LANGUAGE)) { // Check for specific weather conditions (order matters - check combined effects first)
for (const { id, patterns } of language) { if (text.includes('blizzard')) {
if (patterns.some(p => text.includes(p))) { return 'blizzard'; // Snow + Wind
return id;
} }
if (text.includes('storm') || text.includes('thunder') || text.includes('lightning')) {
return 'storm'; // Rain + Lightning
} }
if (text.includes('wind') || text.includes('breeze') || text.includes('gust') || text.includes('gale')) {
return 'wind';
}
if (text.includes('snow') || text.includes('flurries')) {
return 'snow';
}
if (text.includes('rain') || text.includes('drizzle') || text.includes('shower')) {
return 'rain';
}
if (text.includes('mist') || text.includes('fog') || text.includes('haze')) {
return 'mist';
}
if (text.includes('sunny') || text.includes('clear') || text.includes('bright')) {
return 'sunny';
}
if (text.includes('cloud') || text.includes('overcast') || text.includes('indoor') || text.includes('inside')) {
return 'none';
} }
return "none"; return 'none';
} }
/** /**
@@ -302,9 +237,9 @@ function createMist() {
* Returns { left: vw%, top: dvh% } * Returns { left: vw%, top: dvh% }
*/ */
function calculateSunPosition(hour) { function calculateSunPosition(hour) {
// Daytime is roughly 5 AM to 8 PM (5-20) // Daytime is roughly 6 AM to 8 PM (6-20)
// Map hour to position along an arc // Map hour to position along an arc
// 5 AM = far left, low | 12 PM = center, high | 8 PM = far right, low // 6 AM = far left, low | 12 PM = center, high | 6 PM = far right, low
if (hour === null) hour = 12; // Default to noon if unknown if (hour === null) hour = 12; // Default to noon if unknown
@@ -314,14 +249,14 @@ function calculateSunPosition(hour) {
// Normalize to 0-1 range (5 AM = 0, 20 PM = 1) // Normalize to 0-1 range (5 AM = 0, 20 PM = 1)
const progress = (clampedHour - 5) / 15; const progress = (clampedHour - 5) / 15;
// Horizontal position: 3% to 92% (left to right, wider range) // Horizontal position: 5% to 85% (left to right)
const left = 3 + progress * 89; const left = 5 + progress * 80;
// Vertical position: parabolic arc (high at noon, low at dawn/dusk) // Vertical position: parabolic arc (high at noon, low at dawn/dusk)
// At progress 0.5 (noon), top should be ~8% (high) // At progress 0.5 (noon), top should be ~8% (high)
// At progress 0 or 1, top should be ~40% (low, near horizon) // At progress 0 or 1, top should be ~35% (low, near horizon)
const normalizedProgress = (progress - 0.5) * 2; // -1 to 1 const normalizedProgress = (progress - 0.5) * 2; // -1 to 1
const top = 8 + 32 * (normalizedProgress * normalizedProgress); const top = 8 + 27 * (normalizedProgress * normalizedProgress);
return { left, top }; return { left, top };
} }
@@ -392,134 +327,6 @@ function createSunshine(hour) {
return container; return container;
} }
/**
* Create sunrise effect (dawn - warm orange/pink sky gradient with low sun)
*/
function createSunrise(hour) {
const container = document.createElement('div');
container.className = 'rpg-weather-particles rpg-sunrise-weather';
// Create sunrise gradient overlay
const sunriseOverlay = document.createElement('div');
sunriseOverlay.className = 'rpg-weather-particle rpg-sunrise-overlay';
container.appendChild(sunriseOverlay);
// Calculate sun position (rising from left horizon)
const sunPos = calculateSunPosition(hour);
// Create the rising sun
const sun = document.createElement('div');
sun.className = 'rpg-weather-particle rpg-clear-sun rpg-sunrise-sun';
sun.style.left = `${sunPos.left}vw`;
sun.style.top = `${sunPos.top}dvh`;
container.appendChild(sun);
// Create sun glow (more orange during sunrise)
const sunGlow = document.createElement('div');
sunGlow.className = 'rpg-weather-particle rpg-clear-sun-glow rpg-sunrise-glow';
sunGlow.style.left = `${sunPos.left}vw`;
sunGlow.style.top = `${sunPos.top}dvh`;
container.appendChild(sunGlow);
// Create horizon glow
const horizonGlow = document.createElement('div');
horizonGlow.className = 'rpg-weather-particle rpg-sunrise-horizon-glow';
container.appendChild(horizonGlow);
// Add some fading stars (still visible at dawn)
for (let i = 0; i < 15; i++) {
const star = document.createElement('div');
star.className = 'rpg-weather-particle rpg-night-star rpg-sunrise-fading-star';
star.style.left = `${Math.random() * 100}vw`;
star.style.top = `${Math.random() * 40}dvh`;
star.style.animationDelay = `${Math.random() * 3}s`;
const size = 1 + Math.random() * 1.5;
star.style.width = `${size}px`;
star.style.height = `${size}px`;
container.appendChild(star);
}
// Add some golden dust motes
for (let i = 0; i < 12; i++) {
const particle = document.createElement('div');
particle.className = 'rpg-weather-particle rpg-clear-dust-mote';
particle.style.left = `${Math.random() * 100}vw`;
particle.style.top = `${Math.random() * 100}dvh`;
particle.style.animationDelay = `${Math.random() * 15}s`;
particle.style.animationDuration = `${12 + Math.random() * 8}s`;
const size = 2 + Math.random() * 3;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
container.appendChild(particle);
}
return container;
}
/**
* Create sunset effect (dusk - warm red/purple sky gradient with low sun)
*/
function createSunset(hour) {
const container = document.createElement('div');
container.className = 'rpg-weather-particles rpg-sunset-weather';
// Create sunset gradient overlay
const sunsetOverlay = document.createElement('div');
sunsetOverlay.className = 'rpg-weather-particle rpg-sunset-overlay';
container.appendChild(sunsetOverlay);
// Calculate sun position (setting on right horizon)
const sunPos = calculateSunPosition(hour);
// Create the setting sun
const sun = document.createElement('div');
sun.className = 'rpg-weather-particle rpg-clear-sun rpg-sunset-sun';
sun.style.left = `${sunPos.left}vw`;
sun.style.top = `${sunPos.top}dvh`;
container.appendChild(sun);
// Create sun glow (more red during sunset)
const sunGlow = document.createElement('div');
sunGlow.className = 'rpg-weather-particle rpg-clear-sun-glow rpg-sunset-glow';
sunGlow.style.left = `${sunPos.left}vw`;
sunGlow.style.top = `${sunPos.top}dvh`;
container.appendChild(sunGlow);
// Create horizon glow
const horizonGlow = document.createElement('div');
horizonGlow.className = 'rpg-weather-particle rpg-sunset-horizon-glow';
container.appendChild(horizonGlow);
// Add some early stars (appearing at dusk)
for (let i = 0; i < 20; i++) {
const star = document.createElement('div');
star.className = 'rpg-weather-particle rpg-night-star rpg-sunset-emerging-star';
star.style.left = `${Math.random() * 100}vw`;
star.style.top = `${Math.random() * 50}dvh`;
star.style.animationDelay = `${Math.random() * 5}s`;
const size = 1 + Math.random() * 1.5;
star.style.width = `${size}px`;
star.style.height = `${size}px`;
container.appendChild(star);
}
// Add some golden/pink dust motes
for (let i = 0; i < 12; i++) {
const particle = document.createElement('div');
particle.className = 'rpg-weather-particle rpg-clear-dust-mote rpg-sunset-dust';
particle.style.left = `${Math.random() * 100}vw`;
particle.style.top = `${Math.random() * 100}dvh`;
particle.style.animationDelay = `${Math.random() * 15}s`;
particle.style.animationDuration = `${12 + Math.random() * 8}s`;
const size = 2 + Math.random() * 3;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
container.appendChild(particle);
}
return container;
}
/** /**
* Create clear nighttime weather effect with moon, stars, and fireflies * Create clear nighttime weather effect with moon, stars, and fireflies
*/ */
@@ -765,13 +572,9 @@ export function updateWeatherEffect() {
weatherContainer = createMist(); weatherContainer = createMist();
break; break;
case 'sunny': case 'sunny':
// Use appropriate effect based on time of day // Use nighttime effect for clear weather at night
if (timeOfDay === 'night') { if (timeOfDay === 'night') {
weatherContainer = createNighttime(hour); weatherContainer = createNighttime(hour);
} else if (timeOfDay === 'dawn') {
weatherContainer = createSunrise(hour);
} else if (timeOfDay === 'dusk') {
weatherContainer = createSunset(hour);
} else { } else {
weatherContainer = createSunshine(hour); weatherContainer = createSunshine(hour);
} }
-122
View File
@@ -1,122 +0,0 @@
/**
* Response Extractor Utility
*
* Handles extraction of text content from various API response formats.
* Fixes the "No message generated" error caused by Claude models with
* extended thinking, where the API response `content` field is an array
* of content blocks instead of a single string.
*
* Also provides a safe wrapper around SillyTavern's `generateRaw` that
* intercepts the raw fetch response as a fallback.
*/
import { generateRaw } from '../../../../../../../script.js';
/**
* Extracts text from any API response shape (Anthropic content-block arrays,
* OpenAI choices, plain strings, etc.).
*
* @param {*} response - The raw API response (string, array, or object)
* @returns {string} The extracted text content
*/
export function extractTextFromResponse(response) {
if (!response) return '';
if (typeof response === 'string') return response;
// Response itself is an array of content blocks (Anthropic extended thinking)
if (Array.isArray(response)) {
const texts = response
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
.map(b => b.text);
if (texts.length > 0) return texts.join('\n');
const strings = response.filter(item => typeof item === 'string');
if (strings.length > 0) return strings.join('\n');
return JSON.stringify(response);
}
// response.content (string or Anthropic content array)
if (response.content !== undefined && response.content !== null) {
if (typeof response.content === 'string') return response.content;
if (Array.isArray(response.content)) {
const texts = response.content
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
.map(b => b.text);
if (texts.length > 0) return texts.join('\n');
}
}
// OpenAI choices format
if (response.choices?.[0]?.message?.content) {
const c = response.choices[0].message.content;
if (typeof c === 'string') return c;
if (Array.isArray(c)) {
const texts = c
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
.map(b => b.text);
if (texts.length > 0) return texts.join('\n');
}
}
// Other common fields
if (typeof response.text === 'string') return response.text;
if (typeof response.message === 'string') return response.message;
if (response.message?.content && typeof response.message.content === 'string') {
return response.message.content;
}
return JSON.stringify(response);
}
/**
* Safe wrapper around SillyTavern's `generateRaw`.
*
* Temporarily intercepts `window.fetch` to capture the raw API response.
* If `generateRaw` throws "No message generated" (e.g. because the first
* content block from Claude extended thinking is empty), we extract the
* real text from the captured raw data ourselves.
*
* @param {object} options - Options passed directly to `generateRaw`
* @param {Array<{role: string, content: string}>} options.prompt - Message array
* @param {boolean} [options.quietToLoud] - Whether to use quiet-to-loud mode
* @returns {Promise<string>} The generated text
*/
export async function safeGenerateRaw(options) {
let capturedRawData = null;
const originalFetch = window.fetch;
window.fetch = async function (...args) {
const response = await originalFetch.apply(this, args);
try {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
if (url.includes('/api/backends/chat-completions/generate') ||
(url.includes('/api/backends/') && url.includes('/generate'))) {
const clone = response.clone();
capturedRawData = await clone.json();
}
} catch (e) {
/* ignore clone/parse errors */
}
return response;
};
try {
const result = await generateRaw(options);
return result;
} catch (genErr) {
if (genErr.message?.includes('No message generated') && capturedRawData) {
console.warn(
'[RPG Companion] generateRaw failed (likely extended thinking). Extracting from raw API data.',
);
const extracted = extractTextFromResponse(capturedRawData);
if (!extracted || !extracted.trim()) {
throw new Error('Could not extract text from API response');
}
return extracted;
}
throw genErr; // Re-throw non-related errors
} finally {
window.fetch = originalFetch; // ALWAYS restore original fetch
}
}
+14 -290
View File
@@ -2118,8 +2118,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
/* Present Characters - Character Cards */ /* Present Characters - Character Cards */
.rpg-character-card { .rpg-character-card {
display: flex; display: flex;
flex-direction: column; align-items: flex-start;
gap: clamp(6px, 0.8vh, 10px); gap: clamp(8px, 1vw, 12px);
padding: clamp(6px, 1vh, 8px); padding: clamp(6px, 1vh, 8px);
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border-radius: clamp(4px, 0.5vh, 6px); border-radius: clamp(4px, 0.5vh, 6px);
@@ -2157,14 +2157,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
border-color: var(--rpg-highlight); border-color: var(--rpg-highlight);
} }
/* Header row with avatar and name */
.rpg-character-header-row {
display: flex;
align-items: center;
gap: clamp(8px, 1vw, 12px);
width: 100%;
}
/* Character avatar container with relationship badge */ /* Character avatar container with relationship badge */
.rpg-character-avatar { .rpg-character-avatar {
position: relative; position: relative;
@@ -2172,8 +2164,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
} }
.rpg-character-avatar img { .rpg-character-avatar img {
width: clamp(30px, 5vh, 40px); width: clamp(35px, 6vh, 45px);
height: clamp(30px, 5vh, 40px); height: clamp(35px, 6vh, 45px);
border-radius: 50%; border-radius: 50%;
border: 2px solid var(--rpg-highlight); border: 2px solid var(--rpg-highlight);
object-fit: cover; object-fit: cover;
@@ -2240,12 +2232,13 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
} }
/* Character info section - now takes full width below header row */ /* Character info section */
.rpg-character-content { .rpg-character-content {
width: 100%; flex: 1;
min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: clamp(3px, 0.5vh, 5px); gap: 0;
overflow: hidden; overflow: hidden;
} }
@@ -2278,14 +2271,13 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
background: var(--rpg-highlight); background: var(--rpg-highlight);
} }
/* Character header with emoji and name - now inside header row */ /* Character header with emoji and name */
.rpg-character-header { .rpg-character-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: clamp(4px, 0.5vw, 6px); gap: clamp(4px, 0.5vw, 6px);
flex-wrap: nowrap; /* Prevent wrapping */ flex-wrap: nowrap; /* Prevent wrapping */
flex: 1; position: relative;
min-width: 0;
} }
.rpg-character-emoji { .rpg-character-emoji {
@@ -2337,40 +2329,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
transform: scale(0.95); transform: scale(0.95);
} }
/* Add Character Button (inside rpg-thoughts-content, after last character) */
.rpg-add-character-btn {
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
color: var(--rpg-text);
padding: 0;
margin: 0.5rem auto 0;
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: 500;
cursor: pointer;
border-radius: 3px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.25rem;
opacity: 0.6;
width: auto;
}
.rpg-add-character-btn:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
color: var(--rpg-bg);
opacity: 1;
}
.rpg-add-character-btn:active {
transform: scale(0.95);
}
.rpg-add-character-btn i {
font-size: 0.875em;
}
/* Character traits/status line and custom fields */ /* Character traits/status line and custom fields */
.rpg-character-traits, .rpg-character-traits,
.rpg-character-field { .rpg-character-field {
@@ -2382,20 +2340,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
word-wrap: break-word; word-wrap: break-word;
} }
/* Empty field placeholder using data-placeholder attribute */ /* Placeholder for empty editable character fields */
.rpg-editable.rpg-empty-field:empty::before { .rpg-character-field.rpg-editable:empty::before {
content: attr(data-placeholder); content: 'Click to edit...';
color: var(--rpg-highlight); color: var(--rpg-highlight);
opacity: 0.4; opacity: 0.5;
font-style: italic; font-style: italic;
pointer-events: none;
}
/* Ensure empty fields have minimum height for clickability */
.rpg-editable.rpg-empty-field {
min-height: 1.2em;
display: inline-block;
min-width: 3em;
} }
/* Character stats display */ /* Character stats display */
@@ -9948,200 +9898,6 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
} }
} }
/* ===== Sunrise Effects (Dawn) ===== */
.rpg-sunrise-weather {
overflow: hidden;
}
/* Sunrise sky gradient overlay */
.rpg-sunrise-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100dvh;
background: linear-gradient(to bottom,
rgba(40, 30, 80, 0.1) 0%,
rgba(120, 60, 120, 0.08) 15%,
rgba(200, 100, 100, 0.1) 35%,
rgba(255, 140, 100, 0.12) 55%,
rgba(255, 180, 120, 0.1) 75%,
rgba(255, 200, 150, 0.08) 100%);
animation: rpg-sunrise-sky-transition 30s ease-in-out infinite alternate;
pointer-events: none;
}
@keyframes rpg-sunrise-sky-transition {
0% {
opacity: 0.8;
}
100% {
opacity: 1;
}
}
/* Sunrise sun - more orange/red */
.rpg-sunrise-sun {
background: radial-gradient(circle at 40% 40%,
rgba(255, 255, 220, 1) 0%,
rgba(255, 220, 150, 1) 30%,
rgba(255, 160, 80, 0.9) 60%,
rgba(255, 100, 50, 0.6) 80%,
rgba(255, 80, 30, 0) 100%) !important;
box-shadow:
0 0 40px 15px rgba(255, 180, 100, 0.6),
0 0 80px 30px rgba(255, 140, 80, 0.4),
0 0 120px 50px rgba(255, 100, 50, 0.2) !important;
}
/* Sunrise sun glow - warm orange */
.rpg-sunrise-glow {
background: radial-gradient(circle at center,
rgba(255, 200, 150, 0.35) 0%,
rgba(255, 160, 100, 0.2) 30%,
rgba(255, 120, 80, 0.1) 50%,
transparent 70%) !important;
}
/* Horizon glow for sunrise */
.rpg-sunrise-horizon-glow {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 40dvh;
background: linear-gradient(to top,
rgba(255, 160, 100, 0.15) 0%,
rgba(255, 140, 90, 0.1) 20%,
rgba(255, 120, 80, 0.05) 50%,
rgba(255, 100, 70, 0.02) 75%,
transparent 100%);
animation: rpg-horizon-glow-pulse 8s ease-in-out infinite;
pointer-events: none;
}
@keyframes rpg-horizon-glow-pulse {
0%, 100% {
opacity: 0.7;
}
50% {
opacity: 1;
}
}
/* Fading stars at sunrise */
.rpg-sunrise-fading-star {
opacity: 0.3 !important;
animation: rpg-star-fade-out 4s ease-in-out infinite !important;
}
@keyframes rpg-star-fade-out {
0%, 100% {
opacity: 0.2;
}
50% {
opacity: 0.4;
}
}
/* ===== Sunset Effects (Dusk) ===== */
.rpg-sunset-weather {
overflow: hidden;
}
/* Sunset sky gradient overlay */
.rpg-sunset-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100dvh;
background: linear-gradient(to bottom,
rgba(30, 20, 60, 0.12) 0%,
rgba(80, 40, 100, 0.1) 15%,
rgba(150, 60, 90, 0.12) 30%,
rgba(220, 80, 70, 0.12) 50%,
rgba(255, 120, 80, 0.12) 70%,
rgba(255, 160, 100, 0.1) 85%,
rgba(255, 180, 120, 0.06) 100%);
animation: rpg-sunset-sky-transition 30s ease-in-out infinite alternate;
pointer-events: none;
}
@keyframes rpg-sunset-sky-transition {
0% {
opacity: 1;
}
100% {
opacity: 0.8;
}
}
/* Sunset sun - more red/deep orange */
.rpg-sunset-sun {
background: radial-gradient(circle at 40% 40%,
rgba(255, 240, 200, 1) 0%,
rgba(255, 180, 100, 1) 30%,
rgba(255, 120, 60, 0.9) 60%,
rgba(255, 80, 40, 0.6) 80%,
rgba(200, 50, 30, 0) 100%) !important;
box-shadow:
0 0 40px 15px rgba(255, 140, 80, 0.6),
0 0 80px 30px rgba(255, 100, 60, 0.4),
0 0 120px 50px rgba(200, 60, 40, 0.2) !important;
}
/* Sunset sun glow - deep orange/red */
.rpg-sunset-glow {
background: radial-gradient(circle at center,
rgba(255, 160, 120, 0.35) 0%,
rgba(255, 120, 80, 0.2) 30%,
rgba(200, 80, 60, 0.1) 50%,
transparent 70%) !important;
}
/* Horizon glow for sunset */
.rpg-sunset-horizon-glow {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 45dvh;
background: linear-gradient(to top,
rgba(255, 120, 60, 0.18) 0%,
rgba(255, 100, 50, 0.12) 20%,
rgba(220, 70, 50, 0.06) 45%,
rgba(150, 50, 60, 0.02) 70%,
transparent 100%);
animation: rpg-horizon-glow-pulse 8s ease-in-out infinite;
pointer-events: none;
}
/* Emerging stars at sunset */
.rpg-sunset-emerging-star {
animation: rpg-star-emerge 5s ease-in-out infinite !important;
}
@keyframes rpg-star-emerge {
0%, 100% {
opacity: 0.3;
}
50% {
opacity: 0.7;
}
}
/* Sunset dust motes - pinkish tint */
.rpg-sunset-dust {
background: radial-gradient(circle at 30% 30%,
rgba(255, 200, 180, 0.9) 0%,
rgba(255, 180, 160, 0.6) 50%,
rgba(255, 160, 140, 0) 100%) !important;
box-shadow: 0 0 6px 2px rgba(255, 180, 160, 0.4) !important;
}
/* Lens flare effect */ /* Lens flare effect */
.rpg-clear-lens-flare { .rpg-clear-lens-flare {
position: fixed; position: fixed;
@@ -10516,12 +10272,6 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
.rpg-night-shooting-star { .rpg-night-shooting-star {
animation-duration: 18s; animation-duration: 18s;
} }
/* Sunrise/Sunset mobile optimizations */
.rpg-sunrise-horizon-glow,
.rpg-sunset-horizon-glow {
height: 35%;
}
} }
/* Foreground mode - reduced opacity for celestial bodies to not obstruct content */ /* Foreground mode - reduced opacity for celestial bodies to not obstruct content */
@@ -10569,32 +10319,6 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
opacity: 0.6; opacity: 0.6;
} }
/* Sunrise/Sunset foreground mode */
.rpg-weather-foreground .rpg-sunrise-overlay,
.rpg-weather-foreground .rpg-sunset-overlay {
opacity: 0.4;
}
.rpg-weather-foreground .rpg-sunrise-horizon-glow,
.rpg-weather-foreground .rpg-sunset-horizon-glow {
opacity: 0.3;
}
.rpg-weather-foreground .rpg-sunrise-sun,
.rpg-weather-foreground .rpg-sunset-sun {
opacity: 0.5 !important;
}
.rpg-weather-foreground .rpg-sunrise-glow,
.rpg-weather-foreground .rpg-sunset-glow {
opacity: 0.3 !important;
}
.rpg-weather-foreground .rpg-sunrise-fading-star,
.rpg-weather-foreground .rpg-sunset-emerging-star {
opacity: 0.2 !important;
}
/* Lightning flash effect */ /* Lightning flash effect */
.rpg-lightning { .rpg-lightning {
position: fixed; position: fixed;
+11 -78
View File
@@ -139,15 +139,6 @@
</label> </label>
</div> </div>
<!-- Omniscience Filter Toggle -->
<div class="rpg-toggle-container rpg-feature-col" id="rpg-omniscience-toggle-wrapper">
<label class="rpg-toggle-label" title="Omniscience Filter">
<input type="checkbox" id="rpg-toggle-omniscience">
<i class="fa-solid fa-eye-slash"></i>
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.omniscienceFilter">Omniscience Filter</span>
</label>
</div>
<!-- CYOA Toggle --> <!-- CYOA Toggle -->
<div class="rpg-toggle-container rpg-feature-col" id="rpg-cyoa-toggle-wrapper"> <div class="rpg-toggle-container rpg-feature-col" id="rpg-cyoa-toggle-wrapper">
<label class="rpg-toggle-label" title="CYOA"> <label class="rpg-toggle-label" title="CYOA">
@@ -250,49 +241,29 @@
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-custom-bg" <label for="rpg-custom-bg"
data-i18n-key="template.settingsModal.themeOptions.custom.background">Background:</label> data-i18n-key="template.settingsModal.themeOptions.custom.background">Background:</label>
<div style="display: flex; gap: 8px; align-items: center;"> <input type="color" id="rpg-custom-bg" value="#1a1a2e" />
<input type="color" id="rpg-custom-bg" value="#1a1a2e" style="width: 60px;" />
<input type="range" id="rpg-custom-bg-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-bg-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div> </div>
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-custom-accent" <label for="rpg-custom-accent"
data-i18n-key="template.settingsModal.themeOptions.custom.accent">Accent:</label> data-i18n-key="template.settingsModal.themeOptions.custom.accent">Accent:</label>
<div style="display: flex; gap: 8px; align-items: center;"> <input type="color" id="rpg-custom-accent" value="#16213e" />
<input type="color" id="rpg-custom-accent" value="#16213e" style="width: 60px;" />
<input type="range" id="rpg-custom-accent-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-accent-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div> </div>
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-custom-text" <label for="rpg-custom-text"
data-i18n-key="template.settingsModal.themeOptions.custom.text">Text:</label> data-i18n-key="template.settingsModal.themeOptions.custom.text">Text:</label>
<div style="display: flex; gap: 8px; align-items: center;"> <input type="color" id="rpg-custom-text" value="#eaeaea" />
<input type="color" id="rpg-custom-text" value="#eaeaea" style="width: 60px;" />
<input type="range" id="rpg-custom-text-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-text-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div> </div>
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-custom-highlight" <label for="rpg-custom-highlight"
data-i18n-key="template.settingsModal.themeOptions.custom.highlight">Highlight:</label> data-i18n-key="template.settingsModal.themeOptions.custom.highlight">Highlight:</label>
<div style="display: flex; gap: 8px; align-items: center;"> <input type="color" id="rpg-custom-highlight" value="#e94560" />
<input type="color" id="rpg-custom-highlight" value="#e94560" style="width: 60px;" />
<input type="range" id="rpg-custom-highlight-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-highlight-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div> </div>
</div> </div>
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-stat-bar-color-low" data-i18n-key="template.settingsModal.theme.statBarLow">Stat Bar <label for="rpg-stat-bar-color-low" data-i18n-key="template.settingsModal.theme.statBarLow">Stat Bar
Color (Low):</label> Color (Low):</label>
<div style="display: flex; gap: 8px; align-items: center;"> <input type="color" id="rpg-stat-bar-color-low" value="#cc3333" />
<input type="color" id="rpg-stat-bar-color-low" value="#cc3333" style="width: 60px;" />
<input type="range" id="rpg-stat-bar-color-low-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-stat-bar-color-low-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
<small data-i18n-key="template.settingsModal.theme.statBarLowNote">Color when stats are at <small data-i18n-key="template.settingsModal.theme.statBarLowNote">Color when stats are at
0%.</small> 0%.</small>
</div> </div>
@@ -300,11 +271,7 @@
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-stat-bar-color-high" data-i18n-key="template.settingsModal.theme.statBarHigh">Stat <label for="rpg-stat-bar-color-high" data-i18n-key="template.settingsModal.theme.statBarHigh">Stat
Bar Color (High):</label> Bar Color (High):</label>
<div style="display: flex; gap: 8px; align-items: center;"> <input type="color" id="rpg-stat-bar-color-high" value="#33cc66" />
<input type="color" id="rpg-stat-bar-color-high" value="#33cc66" style="width: 60px;" />
<input type="range" id="rpg-stat-bar-color-high-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-stat-bar-color-high-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
<small data-i18n-key="template.settingsModal.theme.statBarHighNote">Color when stats are at <small data-i18n-key="template.settingsModal.theme.statBarHighNote">Color when stats are at
100%.</small> 100%.</small>
</div> </div>
@@ -421,15 +388,6 @@
Display a toggle button to enable/disable special formatting of lies and deceptions crafted by the model, allowing it to easily track whenever one was committed, without showing it to the user. Display a toggle button to enable/disable special formatting of lies and deceptions crafted by the model, allowing it to easily track whenever one was committed, without showing it to the user.
</small> </small>
<label class="checkbox_label">
<input type="checkbox" id="rpg-toggle-show-omniscience-toggle" />
<span data-i18n-key="template.settingsModal.display.showOmniscienceToggle">Show Omniscience Filter</span>
</label>
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
data-i18n-key="template.settingsModal.display.showOmniscienceToggleNote">
Display a toggle button to enable/disable the omniscience filter, which instructs the AI to hide information the player character cannot perceive (events behind them, in other rooms, etc.) in special tags.
</small>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="rpg-toggle-show-cyoa-toggle" /> <input type="checkbox" id="rpg-toggle-show-cyoa-toggle" />
<span data-i18n-key="template.settingsModal.display.showCYOAToggle">Show CYOA</span> <span data-i18n-key="template.settingsModal.display.showCYOAToggle">Show CYOA</span>
@@ -1038,20 +996,6 @@
</button> </button>
</div> </div>
<!-- Omniscience Filter Prompt -->
<div class="rpg-prompt-editor-section">
<label for="rpg-prompt-omniscience" style="display: block; margin-bottom: 8px; font-weight: 600;">
<i class="fa-solid fa-eye-slash"></i> Omniscience Filter Prompt
</label>
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
Injected when "Enable Omniscience Filter" is enabled. Instructs AI to separate information the player character cannot perceive into hidden filter tags.
</small>
<textarea id="rpg-prompt-omniscience" class="rpg-prompt-textarea" rows="6"></textarea>
<button class="menu_button rpg-restore-prompt-btn" data-prompt="omniscience" style="margin-top: 8px;">
<i class="fa-solid fa-rotate-left"></i>&nbsp;Restore Default
</button>
</div>
<!-- CYOA Prompt --> <!-- CYOA Prompt -->
<div class="rpg-prompt-editor-section"> <div class="rpg-prompt-editor-section">
<label for="rpg-prompt-cyoa" style="display: block; margin-bottom: 8px; font-weight: 600;"> <label for="rpg-prompt-cyoa" style="display: block; margin-bottom: 8px; font-weight: 600;">
@@ -1094,20 +1038,6 @@
</button> </button>
</div> </div>
<!-- Context Instructions Prompt -->
<div class="rpg-prompt-editor-section">
<label for="rpg-prompt-context-instructions" style="display: block; margin-bottom: 8px; font-weight: 600;">
<i class="fa-solid fa-comment-dots"></i> Context Instructions Prompt
</label>
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
Injected in Separate/External mode after the context summary. Tells the AI how to use the context.
</small>
<textarea id="rpg-prompt-context-instructions" class="rpg-prompt-textarea" rows="4"></textarea>
<button class="menu_button rpg-restore-prompt-btn" data-prompt="contextInstructions" style="margin-top: 8px;">
<i class="fa-solid fa-rotate-left"></i>&nbsp;Restore Default
</button>
</div>
<!-- Random Plot Progression Prompt --> <!-- Random Plot Progression Prompt -->
<div class="rpg-prompt-editor-section"> <div class="rpg-prompt-editor-section">
<label for="rpg-prompt-plot-random" style="display: block; margin-bottom: 8px; font-weight: 600;"> <label for="rpg-prompt-plot-random" style="display: block; margin-bottom: 8px; font-weight: 600;">
@@ -1194,6 +1124,9 @@
</div> </div>
<footer class="rpg-settings-popup-footer"> <footer class="rpg-settings-popup-footer">
<button id="rpg-prompts-restore-all" class="rpg-btn-secondary" type="button">
<i class="fa-solid fa-rotate-left"></i> Restore All To Default
</button>
<div class="rpg-footer-right"> <div class="rpg-footer-right">
<button id="rpg-prompts-cancel" class="rpg-btn-secondary" type="button">Cancel</button> <button id="rpg-prompts-cancel" class="rpg-btn-secondary" type="button">Cancel</button>
<button id="rpg-prompts-save" class="rpg-btn-primary" type="button"> <button id="rpg-prompts-save" class="rpg-btn-primary" type="button">
@@ -1259,9 +1192,9 @@
For the extension to work properly, **it is not recommended to use any models below 20B, especially if they're old.** It works best with the SOTA models such as Deepseek, Claude, GPT, or Gemini. For the extension to work properly, **it is not recommended to use any models below 20B, especially if they're old.** It works best with the SOTA models such as Deepseek, Claude, GPT, or Gemini.
</p> </p>
<h4 style="margin-top: 20px; margin-bottom: 10px;"><strong>Special thanks to all the contributors for this project:</strong></h4> <h4 style="margin-top: 20px; margin-bottom: 10px;"><strong>Special thanks to all the other contributors for this project:</strong></h4>
<p style="margin-left: 20px; line-height: 1.6;"> <p style="margin-left: 20px; line-height: 1.6;">
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude (???), IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Olaroll. Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude (???), IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
</p> </p>
<div style="margin-top: 20px; text-align: center;"> <div style="margin-top: 20px; text-align: center;">