Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea81dd0634 | |||
| 08474bd910 | |||
| 0bb2085305 | |||
| c6f13d18ff | |||
| 334f5fa5a3 | |||
| 5f9d67ebe8 | |||
| 93c37c25d7 | |||
| 0499f2c43e | |||
| 35bd55615b | |||
| f38f6850c3 | |||
| 989f511d01 | |||
| b827b77184 | |||
| 4f3d59bfb7 | |||
| c18fd39283 | |||
| f5825a7a24 | |||
| c14250e467 | |||
| e8edc42164 | |||
| acf119d4b4 | |||
| 6582095cc1 | |||
| 8aaf258ba3 | |||
| 7c1c140a2a | |||
| ce668c4793 | |||
| 3d6db2b0e9 | |||
| 2151b2dae3 | |||
| 4644e0fd93 | |||
| b18aaee0c0 | |||
| 0066b61746 | |||
| 6e9ff9812d | |||
| 3797e21912 | |||
| 7bac0d48f9 | |||
| 7081137fe3 | |||
| 3ceb64c3bd | |||
| 831c230b36 | |||
| 3a6acb37be | |||
| ce8db67de4 | |||
| 0262218ad0 | |||
| 3fc2cfa8ab | |||
| c614f7b8dc | |||
| 46e6de0eba | |||
| e2a48a4075 | |||
| 8d41010509 | |||
| 95d5616141 | |||
| 5918e38ade | |||
| bb3028adbb | |||
| bc4f50a82f | |||
| 126cfedaa4 | |||
| f3deead868 | |||
| d5d649f122 | |||
| 0cd764c39b | |||
| b9a15722d6 | |||
| db97f012b0 |
@@ -7,13 +7,13 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
|
|||||||
|
|
||||||
## 🆕 What's New
|
## 🆕 What's New
|
||||||
|
|
||||||
### v3.4.0
|
### v3.7.0
|
||||||
|
|
||||||
- Added History Persistance in Edit Trackers that allows you to control how many past trackers in the chat history to include.
|
- Added omniscience filter.
|
||||||
- New mobile displays were added that show all the most important trackers from the panel as small, floating widgets around the RPG Companion button, when the main panel is closed.
|
- Added opacity to the color selector.
|
||||||
- Added CYOA toggle.
|
- Overwritten SillyTavern's dumb-ahh trim logic when joining prompts.
|
||||||
- Added Deception System toggle.
|
- Fixed custom attributes not allowing value increase/decrease.
|
||||||
- The trackers are no longer sent together with an image generation request.
|
- Various bug fixes.
|
||||||
|
|
||||||
**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, and Tomt610.
|
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ import {
|
|||||||
} from './src/systems/ui/mobile.js';
|
} from './src/systems/ui/mobile.js';
|
||||||
import {
|
import {
|
||||||
setupDesktopTabs,
|
setupDesktopTabs,
|
||||||
removeDesktopTabs
|
removeDesktopTabs,
|
||||||
|
updateStripWidgets
|
||||||
} from './src/systems/ui/desktop.js';
|
} from './src/systems/ui/desktop.js';
|
||||||
|
|
||||||
// Feature modules
|
// Feature modules
|
||||||
@@ -152,7 +153,8 @@ import {
|
|||||||
onMessageSwiped,
|
onMessageSwiped,
|
||||||
updatePersonaAvatar,
|
updatePersonaAvatar,
|
||||||
clearExtensionPrompts,
|
clearExtensionPrompts,
|
||||||
onGenerationEnded
|
onGenerationEnded,
|
||||||
|
initHistoryInjection
|
||||||
} from './src/systems/integration/sillytavern.js';
|
} from './src/systems/integration/sillytavern.js';
|
||||||
|
|
||||||
// Old state variable declarations removed - now imported from core modules
|
// Old state variable declarations removed - now imported from core modules
|
||||||
@@ -382,6 +384,11 @@ 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();
|
||||||
@@ -570,6 +577,12 @@ 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();
|
||||||
@@ -592,6 +605,34 @@ async function initUI() {
|
|||||||
}
|
}
|
||||||
saveSettings();
|
saveSettings();
|
||||||
updateFeatureTogglesVisibility();
|
updateFeatureTogglesVisibility();
|
||||||
|
updateWeatherSubOptionsVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Weather sub-options (background and foreground) - radio buttons
|
||||||
|
$('#rpg-toggle-weather-background').on('change', function() {
|
||||||
|
if ($(this).prop('checked')) {
|
||||||
|
extensionSettings.weatherBackground = true;
|
||||||
|
extensionSettings.weatherForeground = false;
|
||||||
|
saveSettings();
|
||||||
|
// Re-apply weather effect
|
||||||
|
if (extensionSettings.enableDynamicWeather) {
|
||||||
|
toggleDynamicWeather(false);
|
||||||
|
toggleDynamicWeather(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-weather-foreground').on('change', function() {
|
||||||
|
if ($(this).prop('checked')) {
|
||||||
|
extensionSettings.weatherBackground = false;
|
||||||
|
extensionSettings.weatherForeground = true;
|
||||||
|
saveSettings();
|
||||||
|
// Re-apply weather effect
|
||||||
|
if (extensionSettings.enableDynamicWeather) {
|
||||||
|
toggleDynamicWeather(false);
|
||||||
|
toggleDynamicWeather(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#rpg-toggle-show-narrator-mode').on('change', function() {
|
$('#rpg-toggle-show-narrator-mode').on('change', function() {
|
||||||
@@ -696,6 +737,63 @@ async function initUI() {
|
|||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Desktop Strip Widget toggles
|
||||||
|
$('#rpg-toggle-strip-widgets-enabled').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
extensionSettings.desktopStripWidgets.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
$('#rpg-strip-widget-options').toggle(extensionSettings.desktopStripWidgets.enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-weather-icon').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.weatherIcon) extensionSettings.desktopStripWidgets.weatherIcon = {};
|
||||||
|
extensionSettings.desktopStripWidgets.weatherIcon.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-clock').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.clock) extensionSettings.desktopStripWidgets.clock = {};
|
||||||
|
extensionSettings.desktopStripWidgets.clock.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-date').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.date) extensionSettings.desktopStripWidgets.date = {};
|
||||||
|
extensionSettings.desktopStripWidgets.date.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-location').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.location) extensionSettings.desktopStripWidgets.location = {};
|
||||||
|
extensionSettings.desktopStripWidgets.location.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-stats').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.stats) extensionSettings.desktopStripWidgets.stats = {};
|
||||||
|
extensionSettings.desktopStripWidgets.stats.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-attributes').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.attributes) extensionSettings.desktopStripWidgets.attributes = {};
|
||||||
|
extensionSettings.desktopStripWidgets.attributes.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-manual-update').on('click', async function() {
|
$('#rpg-manual-update').on('click', async function() {
|
||||||
if (!extensionSettings.enabled) {
|
if (!extensionSettings.enabled) {
|
||||||
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
|
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
|
||||||
@@ -704,18 +802,44 @@ async function initUI() {
|
|||||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Strip widget refresh button - same functionality as main refresh button
|
||||||
|
$('#rpg-strip-refresh').on('click', async function() {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-stat-bar-color-low').on('change', function() {
|
$('#rpg-stat-bar-color-low').on('change', function() {
|
||||||
extensionSettings.statBarColorLow = String($(this).val());
|
extensionSettings.statBarColorLow = String($(this).val());
|
||||||
saveSettings();
|
saveSettings();
|
||||||
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());
|
||||||
@@ -737,6 +861,19 @@ 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();
|
||||||
@@ -747,6 +884,19 @@ 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();
|
||||||
@@ -757,6 +907,19 @@ 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();
|
||||||
@@ -767,6 +930,19 @@ 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) {
|
||||||
@@ -874,6 +1050,7 @@ 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);
|
||||||
|
|
||||||
@@ -884,9 +1061,12 @@ 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);
|
||||||
|
$('#rpg-toggle-weather-background').prop('checked', extensionSettings.weatherBackground ?? true);
|
||||||
|
$('#rpg-toggle-weather-foreground').prop('checked', extensionSettings.weatherForeground ?? false);
|
||||||
$('#rpg-toggle-show-narrator-mode').prop('checked', extensionSettings.showNarratorMode ?? true);
|
$('#rpg-toggle-show-narrator-mode').prop('checked', extensionSettings.showNarratorMode ?? true);
|
||||||
$('#rpg-toggle-show-auto-avatars').prop('checked', extensionSettings.showAutoAvatars ?? true);
|
$('#rpg-toggle-show-auto-avatars').prop('checked', extensionSettings.showAutoAvatars ?? true);
|
||||||
|
|
||||||
@@ -931,13 +1111,42 @@ async function initUI() {
|
|||||||
// Toggle visibility of widget options based on master toggle
|
// Toggle visibility of widget options based on master toggle
|
||||||
$('#rpg-fab-widget-options').toggle(fabWidgets.enabled || false);
|
$('#rpg-fab-widget-options').toggle(fabWidgets.enabled || false);
|
||||||
|
|
||||||
|
// Initialize Desktop Strip Widget checkboxes
|
||||||
|
const stripWidgets = extensionSettings.desktopStripWidgets || {};
|
||||||
|
$('#rpg-toggle-strip-widgets-enabled').prop('checked', stripWidgets.enabled || false);
|
||||||
|
$('#rpg-toggle-strip-weather-icon').prop('checked', stripWidgets.weatherIcon?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-clock').prop('checked', stripWidgets.clock?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-date').prop('checked', stripWidgets.date?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-location').prop('checked', stripWidgets.location?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-stats').prop('checked', stripWidgets.stats?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-attributes').prop('checked', stripWidgets.attributes?.enabled ?? true);
|
||||||
|
// Toggle visibility of strip widget options based on master toggle
|
||||||
|
$('#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) {
|
||||||
@@ -1074,8 +1283,9 @@ jQuery(async () => {
|
|||||||
// Load chat-specific data for current chat
|
// Load chat-specific data for current chat
|
||||||
try {
|
try {
|
||||||
loadChatData();
|
loadChatData();
|
||||||
// Initialize FAB widgets with any loaded data
|
// Initialize FAB widgets and strip widgets with any loaded data
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Chat data load failed, using defaults:', error);
|
console.error('[RPG Companion] Chat data load failed, using defaults:', error);
|
||||||
}
|
}
|
||||||
@@ -1125,6 +1335,15 @@ jQuery(async () => {
|
|||||||
// Non-critical - continue anyway
|
// Non-critical - continue anyway
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize history injection event listeners
|
||||||
|
// This must be done before event registration so listeners are ready
|
||||||
|
try {
|
||||||
|
initHistoryInjection();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RPG Companion] History injection init failed:', error);
|
||||||
|
// Non-critical - continue without it
|
||||||
|
}
|
||||||
|
|
||||||
// Register all event listeners
|
// Register all event listeners
|
||||||
try {
|
try {
|
||||||
registerAllEvents({
|
registerAllEvents({
|
||||||
@@ -1175,3 +1394,17 @@ jQuery(async () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visibility of weather sub-options in settings based on dynamic weather toggle
|
||||||
|
*/
|
||||||
|
function updateWeatherSubOptionsVisibility() {
|
||||||
|
const $weatherSubOptions = $('#rpg-weather-suboptions');
|
||||||
|
const isDynamicWeatherEnabled = extensionSettings.showDynamicWeatherToggle ?? true;
|
||||||
|
|
||||||
|
if (isDynamicWeatherEnabled) {
|
||||||
|
$weatherSubOptions.show();
|
||||||
|
} else {
|
||||||
|
$weatherSubOptions.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"js": "index.js",
|
"js": "index.js",
|
||||||
"css": "style.css",
|
"css": "style.css",
|
||||||
"author": "Marinara",
|
"author": "Marinara",
|
||||||
"version": "3.3.2",
|
"version": "3.7.0",
|
||||||
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -15,6 +15,7 @@
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
</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.4.0
|
v3.7.0
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -118,6 +118,22 @@ 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();
|
||||||
@@ -147,6 +163,12 @@ export function loadSettings() {
|
|||||||
|
|
||||||
// Migrate to preset manager system if presets don't exist
|
// Migrate to preset manager system if presets don't exist
|
||||||
migrateToPresetManager();
|
migrateToPresetManager();
|
||||||
|
|
||||||
|
// Initialize custom status fields
|
||||||
|
initializeCustomStatusFields();
|
||||||
|
|
||||||
|
// Ensure all stats have maxValue (for number display mode)
|
||||||
|
ensureStatsHaveMaxValue();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Error loading settings:', error);
|
console.error('[RPG Companion] Error loading settings:', error);
|
||||||
console.error('[RPG Companion] Error details:', error.message, error.stack);
|
console.error('[RPG Companion] Error details:', error.message, error.stack);
|
||||||
@@ -694,6 +716,45 @@ export function migrateToPresetManager() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes custom status fields in userStats based on trackerConfig
|
||||||
|
* Ensures all defined custom status fields have a value in the userStats object
|
||||||
|
*/
|
||||||
|
function initializeCustomStatusFields() {
|
||||||
|
const customFields = extensionSettings.trackerConfig?.userStats?.statusSection?.customFields || [];
|
||||||
|
|
||||||
|
// Initialize each custom field if it doesn't exist
|
||||||
|
for (const fieldName of customFields) {
|
||||||
|
const fieldKey = fieldName.toLowerCase();
|
||||||
|
if (extensionSettings.userStats[fieldKey] === undefined) {
|
||||||
|
extensionSettings.userStats[fieldKey] = 'None';
|
||||||
|
// console.log(`[RPG Companion] Initialized custom status field: ${fieldKey}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures all custom stats have a maxValue property
|
||||||
|
* This migration supports the number display mode feature
|
||||||
|
*/
|
||||||
|
function ensureStatsHaveMaxValue() {
|
||||||
|
const customStats = extensionSettings.trackerConfig?.userStats?.customStats || [];
|
||||||
|
|
||||||
|
for (const stat of customStats) {
|
||||||
|
if (stat && stat.maxValue === undefined) {
|
||||||
|
stat.maxValue = 100; // Default to 100 for backward compatibility
|
||||||
|
// console.log(`[RPG Companion] Added maxValue to stat: ${stat.id || stat.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure statsDisplayMode is set (default to percentage)
|
||||||
|
if (extensionSettings.trackerConfig?.userStats &&
|
||||||
|
extensionSettings.trackerConfig.userStats.statsDisplayMode === undefined) {
|
||||||
|
extensionSettings.trackerConfig.userStats.statsDisplayMode = 'percentage';
|
||||||
|
// console.log('[RPG Companion] Initialized statsDisplayMode to percentage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all available presets
|
* Gets all available presets
|
||||||
* @returns {Object} Map of preset ID to preset data
|
* @returns {Object} Map of preset ID to preset data
|
||||||
@@ -915,6 +976,17 @@ export function hasPresetAssociation() {
|
|||||||
return entityKey && extensionSettings.presetManager.characterAssociations[entityKey] !== undefined;
|
return entityKey && extensionSettings.presetManager.characterAssociations[entityKey] !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current character/group is associated with the currently active preset
|
||||||
|
* @returns {boolean} True if the current entity is associated with the active preset
|
||||||
|
*/
|
||||||
|
export function isAssociatedWithCurrentPreset() {
|
||||||
|
const entityKey = getCurrentEntityKey();
|
||||||
|
const activePresetId = extensionSettings.presetManager?.activePresetId;
|
||||||
|
if (!entityKey || !activePresetId) return false;
|
||||||
|
return extensionSettings.presetManager.characterAssociations[entityKey] === activePresetId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-switches to the preset associated with the current character/group
|
* Auto-switches to the preset associated with the current character/group
|
||||||
* Called when character changes. Falls back to default preset if no association.
|
* Called when character changes. Falls back to default preset if no association.
|
||||||
|
|||||||
+32
-7
@@ -23,22 +23,28 @@ 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)
|
||||||
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
||||||
|
|
||||||
enableDynamicWeather: true, // Enable dynamic weather effects based on Info Box weather field (v2: enabled by default)
|
enableDynamicWeather: true, // Enable dynamic weather effects based on Info Box weather field (v2: enabled by default)
|
||||||
|
weatherBackground: true, // Show weather effects in background (behind chat)
|
||||||
|
weatherForeground: false, // Show weather effects in foreground (on top of chat)
|
||||||
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
|
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
|
||||||
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
|
||||||
|
|
||||||
@@ -53,18 +59,25 @@ export let extensionSettings = {
|
|||||||
enabled: false, // Master toggle for history persistence feature
|
enabled: false, // Master toggle for history persistence feature
|
||||||
messageCount: 5, // Number of messages to include (0 = all available)
|
messageCount: 5, // Number of messages to include (0 = all available)
|
||||||
injectionPosition: 'assistant_message_end', // 'user_message_end', 'assistant_message_end', 'extra_user_message', 'extra_assistant_message'
|
injectionPosition: 'assistant_message_end', // 'user_message_end', 'assistant_message_end', 'extra_user_message', 'extra_assistant_message'
|
||||||
contextPreamble: '' // Optional custom preamble text (empty = use default short one)
|
contextPreamble: '', // Optional custom preamble text (empty = use default short one)
|
||||||
|
sendAllEnabledOnRefresh: false // If true, sends all enabled stats from preset instead of only persistInHistory-enabled stats on Refresh RPG Info
|
||||||
},
|
},
|
||||||
panelPosition: 'right', // 'left', 'right', or 'top'
|
panelPosition: 'right', // 'left', 'right', or 'top'
|
||||||
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',
|
||||||
highlight: '#e94560'
|
textOpacity: 100,
|
||||||
|
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)',
|
||||||
@@ -81,6 +94,16 @@ export let extensionSettings = {
|
|||||||
stats: { enabled: true, position: 5 }, // All stats as compact numbers
|
stats: { enabled: true, position: 5 }, // All stats as compact numbers
|
||||||
attributes: { enabled: true, position: 6 } // Compact RPG attributes display
|
attributes: { enabled: true, position: 6 } // Compact RPG attributes display
|
||||||
},
|
},
|
||||||
|
// Desktop strip widget display options (shown in collapsed panel strip)
|
||||||
|
desktopStripWidgets: {
|
||||||
|
enabled: true, // Master toggle for strip widgets (enabled by default)
|
||||||
|
weatherIcon: { enabled: true }, // Weather emoji (☀️, 🌧️, etc.)
|
||||||
|
clock: { enabled: true }, // Current time display
|
||||||
|
date: { enabled: true }, // Date display
|
||||||
|
location: { enabled: true }, // Location name
|
||||||
|
stats: { enabled: true }, // All stats as compact numbers
|
||||||
|
attributes: { enabled: true } // Compact RPG attributes display
|
||||||
|
},
|
||||||
userStats: JSON.stringify({
|
userStats: JSON.stringify({
|
||||||
stats: [
|
stats: [
|
||||||
{ id: 'health', name: 'Health', value: 100 },
|
{ id: 'health', name: 'Health', value: 100 },
|
||||||
@@ -112,13 +135,15 @@ export let extensionSettings = {
|
|||||||
// Tracker customization configuration
|
// Tracker customization configuration
|
||||||
trackerConfig: {
|
trackerConfig: {
|
||||||
userStats: {
|
userStats: {
|
||||||
|
// Stats display mode: 'percentage' or 'number'
|
||||||
|
statsDisplayMode: 'percentage',
|
||||||
// Array of custom stats (allows add/remove/rename)
|
// Array of custom stats (allows add/remove/rename)
|
||||||
customStats: [
|
customStats: [
|
||||||
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false },
|
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false },
|
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false },
|
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false },
|
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false }
|
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false, maxValue: 100 }
|
||||||
],
|
],
|
||||||
// RPG Attributes (customizable D&D-style attributes)
|
// RPG Attributes (customizable D&D-style attributes)
|
||||||
showRPGAttributes: true,
|
showRPGAttributes: true,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"settings.language.label": "Language",
|
"settings.language.label": "Language",
|
||||||
"settings.language.option.en": "English",
|
"settings.language.option.en": "English",
|
||||||
"settings.language.option.zh-tw": "繁體中文",
|
"settings.language.option.zh-tw": "繁體中文",
|
||||||
|
"settings.language.option.ru": "Русский",
|
||||||
"settings.extensionEnabled": "Enable RPG Companion",
|
"settings.extensionEnabled": "Enable RPG Companion",
|
||||||
"settings.note": "Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself.",
|
"settings.note": "Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself.",
|
||||||
"template.settingsTitle": "RPG Companion Settings",
|
"template.settingsTitle": "RPG Companion Settings",
|
||||||
@@ -51,6 +52,10 @@
|
|||||||
"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",
|
||||||
@@ -161,6 +166,8 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
{
|
||||||
|
"settings.language.label": "Язык",
|
||||||
|
"settings.language.option.en": "English",
|
||||||
|
"settings.language.option.zh-tw": "繁體中文",
|
||||||
|
"settings.language.option.ru": "Русский",
|
||||||
|
"settings.extensionEnabled": "Включить RPG Companion",
|
||||||
|
"settings.note": "Включить или отключить расширение RPG Companion. Дополнительные настройки производятся непосредственно в панели приложения.",
|
||||||
|
"template.settingsTitle": "Настройки RPG Companion",
|
||||||
|
"template.settingsModal.themeTitle": "Тема",
|
||||||
|
"template.settingsModal.themeLabel": "Стиль:",
|
||||||
|
"template.settingsModal.themeOptions.default": "По умолчанию",
|
||||||
|
"template.settingsModal.themeOptions.sciFi": "Скай-фай (Synthwave)",
|
||||||
|
"template.settingsModal.themeOptions.fantasy": "Фэнтези (Rustic Parchment)",
|
||||||
|
"template.settingsModal.themeOptions.cyberpunk": "Киберпанк (Neon Grid)",
|
||||||
|
"template.settingsModal.themeOptions.custom": "Своя",
|
||||||
|
"template.settingsModal.themeOptions.custom.background": "Фон:",
|
||||||
|
"template.settingsModal.themeOptions.custom.accent": "Акцент:",
|
||||||
|
"template.settingsModal.themeOptions.custom.text": "Текст:",
|
||||||
|
"template.settingsModal.themeOptions.custom.highlight": "Подсветка:",
|
||||||
|
"template.settingsModal.theme.statBarLow": "Цвет полоски характеристики (Низкие значения):",
|
||||||
|
"template.settingsModal.theme.statBarLowNote": "Цвет при значении показателей 0%.",
|
||||||
|
"template.settingsModal.theme.statBarHigh": "Цвет полоски характеристики (Высокие значения):",
|
||||||
|
"template.settingsModal.theme.statBarHighNote": "Цвет при значении показателей 100%.",
|
||||||
|
"template.settingsModal.displayTitle": "Настройки отображения",
|
||||||
|
"template.settingsModal.displayNote": "Вы можете вкючить/отключить расширение RPG Companion во вкладке расширений для SillyTavern.",
|
||||||
|
"template.settingsModal.display.panelPosition": "Положение боковой панели:",
|
||||||
|
"template.settingsModal.display.panelPositionOptions.right": "Справа",
|
||||||
|
"template.settingsModal.display.panelPositionOptions.left": "Слева",
|
||||||
|
"template.settingsModal.display.toggleAutoUpdate": "Авто-обновление после ответа",
|
||||||
|
"template.settingsModal.display.toggleAutoUpdateNote": "Автоматически обновлять информацию в трекрере после каждого ответа.",
|
||||||
|
"template.settingsModal.display.showUserStats": "Показать Характеристики Игрока",
|
||||||
|
"template.settingsModal.display.showUserStatsNote": "Включить Характеристики Игрока, которые отслеживают статистику используемой персоны - характеристики, настроение, навыки и т.д.",
|
||||||
|
"template.settingsModal.display.showInfoBox": "Показывать Инфо-панель",
|
||||||
|
"template.settingsModal.display.showInfoBoxNote": "Отображение локации, времени, погоды и недавних событий.",
|
||||||
|
"template.settingsModal.display.showPresentCharacters": "Показывать персонажей",
|
||||||
|
"template.settingsModal.display.showPresentCharactersNote": "Показывать портреты персонажей с их текущимы мыслями и статусом.",
|
||||||
|
"template.settingsModal.display.narratorMode": "Режим расказчика",
|
||||||
|
"template.settingsModal.display.narratorModeNote": "Использовать карточку персонажа в качестве расказчика. Персонажи берутся из контекста вместо фиксированных отсылок.",
|
||||||
|
"template.settingsModal.display.showInventory": "Показывать инвентарь",
|
||||||
|
"template.settingsModal.display.showInventoryNote": "Отслеживайте переносимые предметы, одежду, хранимые вещи и активы.",
|
||||||
|
"template.settingsModal.display.showQuests": "Показывать задания",
|
||||||
|
"template.settingsModal.display.showQuestsNote": "Управляйте основными и дополнительными заданиями с целями.",
|
||||||
|
"template.settingsModal.display.showLockIcons": "Показывать значки блокировки/разблокировки трекеров",
|
||||||
|
"template.settingsModal.display.showLockIconsNote": "Отображать значки блокировки/разблокировки на элементах трекера, чтобы предотвратить их изменение ИИ.",
|
||||||
|
"template.settingsModal.display.showThoughtsInChat": "Показывать мысли",
|
||||||
|
"template.settingsModal.display.showThoughtsInChatNote": "Отображать мысли персонажей в виде всплывающих пузырьков рядом с их сообщениями.",
|
||||||
|
"template.settingsModal.display.alwaysShowThoughtBubble": "Всегда показывать пузырь мыслей",
|
||||||
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Автоматически раскрывать пузырь мыслей без предварительного нажатия на значок",
|
||||||
|
"template.settingsModal.display.enableAnimations": "Включить анимации",
|
||||||
|
"template.settingsModal.display.enableAnimationsNote": "Плавные переходы для характеристик, обновления контента и бросков кубиков.",
|
||||||
|
"template.settingsModal.display.showImmersiveHtmlToggle": "Показывать переключатель Immersive HTML",
|
||||||
|
"template.settingsModal.display.showImmersiveHtmlToggleNote": "Отображать кнопку переключения для включения/отключения HTML-форматирования в сообщениях.",
|
||||||
|
"template.settingsModal.display.showDialogueColoringToggle": "Показывать переключатель цветных диалогов",
|
||||||
|
"template.settingsModal.display.showDialogueColoringToggleNote": "Отображать кнопку переключения для включения/отключения цветного форматирования диалогов.",
|
||||||
|
"template.settingsModal.display.showSpotifyMusicToggle": "Показывать переключатель музыки Spotify",
|
||||||
|
"template.settingsModal.display.showSpotifyMusicToggleNote": "Отображать музыкальный проигрыватель Spotify с предложенными ИИ треками, подходящими для сцены.",
|
||||||
|
"template.settingsModal.display.showSnowflakesToggle": "Показывать переключатель погодных эффектов",
|
||||||
|
"template.settingsModal.display.showDynamicWeatherToggle": "Показывать переключатель динамических погодных эффектов",
|
||||||
|
"template.settingsModal.display.showDynamicWeatherToggleNote": "Отображать кнопку переключения для включения/отключения анимированных погодных эффектов.",
|
||||||
|
"template.settingsModal.display.showNarratorMode": "Показывать переключатель режима рассказчика",
|
||||||
|
"template.settingsModal.display.showNarratorModeNote": "Отображать кнопку переключения для включения/отключения режима рассказчика (персонажи определяются из контекста).",
|
||||||
|
"template.settingsModal.display.showAutoAvatars": "Показывать переключатель автоматической генерации аватаров",
|
||||||
|
"template.settingsModal.display.showAutoAvatarsNote": "Отображать кнопку переключения для автоматической генерации аватаров для персонажей без изображений.",
|
||||||
|
"template.settingsModal.display.showRandomizedPlot": "Показывать переключатель случайного развития сюжета",
|
||||||
|
"template.settingsModal.display.showRandomizedPlotNote": "Отображать кнопку для генерации ИИ случайных подсказок для развития сюжета.",
|
||||||
|
"template.settingsModal.display.showNaturalPlot": "Показывать переключатель естественного развития сюжета",
|
||||||
|
"template.settingsModal.display.showNaturalPlotNote": "Отображать кнопку для контекстно-зависимых подсказок продолжения повествования.",
|
||||||
|
"template.settingsModal.display.showStartEncounter": "Показывать переключатель начала встречи",
|
||||||
|
"template.settingsModal.display.showStartEncounterNote": "Отображать кнопку для начала интерактивных боевых столкновений.",
|
||||||
|
"template.settingsModal.display.showDiceDisplay": "Показывать отображение броска кубиков",
|
||||||
|
"template.settingsModal.display.showDiceDisplayNote": "Отображать индикатор \"Последний бросок\" на панели.",
|
||||||
|
"template.mainPanel.autoAvatars": "Авто-аватары",
|
||||||
|
"template.settingsModal.advancedTitle": "Дополнительно",
|
||||||
|
"template.settingsModal.advanced.encounterHistoryDepth": "Глубина истории чата для боя:",
|
||||||
|
"template.settingsModal.advanced.encounterHistoryDepthNote": "Количество последних сообщений, включаемых при инициализации боя.",
|
||||||
|
"template.settingsModal.advanced.autoSaveCombatLogs": "Автосохранение журналов боя",
|
||||||
|
"template.settingsModal.advanced.autoSaveCombatLogsNote": "Сохранять подробные журналы боя в файл для будущего использования и анализа.",
|
||||||
|
"template.settingsModal.advanced.clearCacheNote": "Очищает сохраненные и отображаемые данные трекеров для текущего активного чата.",
|
||||||
|
"template.settingsModal.advanced.generationMode": "Режим генерации:",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.together": "Вместе с основной генерацией",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.separate": "Отдельная генерация",
|
||||||
|
"template.settingsModal.advanced.generationModeNote": "Вместе: добавляет RPG-трекинг к основному ответу. Отдельно: генерирует RPG-данные отдельно (вручную или автоматически). Внешний: подключается напрямую к OpenAI-совместимому эндпоинту.",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.external": "Внешний API",
|
||||||
|
"template.settingsModal.advanced.externalApi.title": "Настройки внешнего API",
|
||||||
|
"template.settingsModal.advanced.externalApi.baseUrl": "Базовый URL API",
|
||||||
|
"template.settingsModal.advanced.externalApi.baseUrlNote": "OpenAI-совместимый эндпоинт (например, OpenAI, OpenRouter, локальный сервер LLM).",
|
||||||
|
"template.settingsModal.advanced.externalApi.apiKey": "API-ключ",
|
||||||
|
"template.settingsModal.advanced.externalApi.apiKeyNote": "Ваш API-ключ для внешнего сервиса.",
|
||||||
|
"template.settingsModal.advanced.externalApi.model": "Модель",
|
||||||
|
"template.settingsModal.advanced.externalApi.modelNote": "Идентификатор модели (например, gpt-4o-mini, claude-3-haiku, mistral-7b).",
|
||||||
|
"template.settingsModal.advanced.externalApi.maxTokens": "Максимальное количество токенов",
|
||||||
|
"template.settingsModal.advanced.externalApi.temperature": "Температура",
|
||||||
|
"template.settingsModal.advanced.externalApi.testConnection": "Тестировать соединение",
|
||||||
|
"template.settingsModal.advanced.contextMessages": "Контекстные сообщения:",
|
||||||
|
"template.settingsModal.advanced.contextMessagesNote": "Количество последних сообщений, включаемых в контекст.",
|
||||||
|
"template.settingsModal.advanced.useSeparatePreset": "Использовать модель, подключенную к пресету RPG Companion Trackers",
|
||||||
|
"template.settingsModal.advanced.useSeparatePresetNote": "При включении генерация трекеров будет использовать модель из пресета \"RPG Companion Trackers\" вместо основной модели API. Пресет будет автоматически переключаться во время генерации и восстанавливаться после нее. Выберите желаемую модель в этом пресете и убедитесь, что переключатель \"Bind presets to API connections\" включен (рядом с кнопками импорта/экспорта пресетов).",
|
||||||
|
"template.settingsModal.advanced.skipInjections": "Пропускать инъекции во время управляемых генераций:",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.none": "Никогда не пропускать",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.impersonation": "Только при запросах олицетворения",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.guided": "Всегда для управляемых или тихих подсказок",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsNote": "При установке расширение не будет внедрять подсказки трекеров, примеры или HTML-инструкции в соответствии с выбранным режимом при обнаружении управляемой генерации (через `instruct` или `quiet_prompt`). Полезно при использовании GuidedGenerations или аналогичных расширений.",
|
||||||
|
"template.settingsModal.advanced.customHtmlPromptTitle": "Пользовательская HTML-подсказка:",
|
||||||
|
"template.settingsModal.advanced.restoreDefaultHtmlPrompt": "Восстановить по умолчанию",
|
||||||
|
"template.settingsModal.advanced.customHtmlPromptNote": "Настройте HTML-подсказку, которая внедряется при включенной опции \"Enable Immersive HTML\". Подсказка по умолчанию показана выше - вы можете редактировать ее напрямую или полностью заменить. Нажмите \"Восстановить по умолчанию\" для сброса. Это влияет на все режимы генерации (together, separate и plot progression).",
|
||||||
|
"template.settingsModal.advanced.clearCache": "Очистить кэш расширения",
|
||||||
|
"template.settingsModal.advanced.resetFabPositions": "Сбросить позиции кнопок",
|
||||||
|
"template.settingsModal.advanced.resetFabPositionsNote": "Сбрасывает все плавающие кнопки действий (переключение, обновление, отладка) в позиции по умолчанию (сверху слева). Полезно, если кнопки находятся за пределами экрана.",
|
||||||
|
"template.trackerEditorModal.title": "Редактировать трекеры",
|
||||||
|
"template.trackerEditorModal.tabs.userStats": "Характеристики пользователя",
|
||||||
|
"template.trackerEditorModal.tabs.infoBox": "Инфо-панель",
|
||||||
|
"template.trackerEditorModal.tabs.presentCharacters": "Присутствующие персонажи",
|
||||||
|
"template.trackerEditorModal.buttons.reset": "Сбросить",
|
||||||
|
"template.trackerEditorModal.buttons.cancel": "Отмена",
|
||||||
|
"template.trackerEditorModal.buttons.save": "Сохранить и применить",
|
||||||
|
"template.trackerEditorModal.buttons.export": "Экспорт",
|
||||||
|
"template.trackerEditorModal.buttons.import": "Импорт",
|
||||||
|
"template.trackerEditorModal.messages.exportSuccess": "Шаблон трекеров успешно экспортирован!",
|
||||||
|
"template.trackerEditorModal.messages.exportError": "Не удалось экспортировать шаблон трекеров. Проверьте консоль для получения подробностей.",
|
||||||
|
"template.trackerEditorModal.messages.importSuccess": "Шаблон трекеров успешно импортирован!",
|
||||||
|
"template.trackerEditorModal.messages.importError": "Не удалось импортировать шаблон трекеров",
|
||||||
|
"template.trackerEditorModal.messages.importConfirm": "Это заменит текущую конфигурацию трекеров. Продолжить?",
|
||||||
|
"template.trackerEditorModal.userStatsTab.customStatsTitle": "Пользовательские характеристики",
|
||||||
|
"template.trackerEditorModal.userStatsTab.addCustomStatButton": "Добавить пользовательскую характеристику",
|
||||||
|
"template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG-атрибуты",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableRpgAttributes": "Включить раздел RPG-атрибутов",
|
||||||
|
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "Всегда включать атрибуты в подсказку",
|
||||||
|
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "Если отключено, атрибуты отправляются только при активном броске кубиков.",
|
||||||
|
"template.trackerEditorModal.userStatsTab.addAttributeButton": "Добавить атрибут",
|
||||||
|
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "Раздел статуса",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableStatusSection": "Включить раздел статуса",
|
||||||
|
"template.trackerEditorModal.userStatsTab.showMoodEmoji": "Показывать эмодзи настроения",
|
||||||
|
"template.trackerEditorModal.userStatsTab.statusFieldsLabel": "Поля статуса (через запятую):",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsSectionTitle": "Раздел навыков",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableSkillsSection": "Включить раздел навыков",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsLabelLabel": "Метка навыков:",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsListLabel": "Список навыков (через запятую):",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.widgetsTitle": "Виджеты",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.dateWidget": "Дата",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.weatherWidget": "Погода",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.temperatureWidget": "Температура",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.timeWidget": "Время",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.locationWidget": "Местоположение",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.recentEventsWidget": "Недавние события",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle": "Поля статуса отношений",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.enableRelationshipStatus": "Включить поля статуса отношений",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.relationshipStatusHint": "Определите типы отношений с соответствующими эмодзи, отображаемыми на портретах персонажей.",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.newRelationshipButton": "Новое отношение",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle": "Поля внешности/поведения",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint": "Поля, отображаемые под именем персонажа.",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.addCustomFieldButton": "Добавить пользовательское поле",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle": "Настройки мыслей",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.enableCharacterThoughts": "Включить мысли персонажей",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.thoughtsLabelLabel": "Метка мыслей:",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "Инструкция для ИИ:",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Характеристики персонажей",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Отслеживать характеристики персонажей",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Создавайте характеристики для отслеживания для каждого персонажа (отображаются в виде цветных полос).",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Добавить характеристику персонажа",
|
||||||
|
"template.mainPanel.title": "RPG Companion",
|
||||||
|
"template.mainPanel.lastRoll": "Последний бросок:",
|
||||||
|
"template.mainPanel.clearLastRoll": "Очистить последний бросок",
|
||||||
|
"template.mainPanel.immersiveHtml": "Immersive HTML",
|
||||||
|
"template.mainPanel.coloredDialogues": "Цветные диалоги",
|
||||||
|
"template.mainPanel.spotifyMusic": "Музыка Spotify",
|
||||||
|
"template.mainPanel.snowflakesEffect": "Эффект снежинок",
|
||||||
|
"template.mainPanel.dynamicWeatherEffects": "Динамическая погода",
|
||||||
|
"template.mainPanel.narratorMode": "Режим рассказчика",
|
||||||
|
"template.mainPanel.refreshRpgInfo": "Обновить RPG-информацию",
|
||||||
|
"template.mainPanel.updating": "Обновление...",
|
||||||
|
"template.mainPanel.editTrackersButton": "Редактировать трекеры",
|
||||||
|
"template.mainPanel.settingsButton": "Настройки",
|
||||||
|
"global.none": "Нет",
|
||||||
|
"global.add": "Добавить",
|
||||||
|
"global.cancel": "Отмена",
|
||||||
|
"global.listView": "Вид списка",
|
||||||
|
"global.gridView": "Вид сетки",
|
||||||
|
"global.save": "Сохранить",
|
||||||
|
"global.status": "Статус",
|
||||||
|
"global.inventory": "Инвентарь",
|
||||||
|
"global.quests": "Задания",
|
||||||
|
"global.info": "Информация",
|
||||||
|
"infobox.noData.title": "Данных пока нет",
|
||||||
|
"infobox.noData.instruction": "Сгенерируйте новый ответ в ролевой игре или переключитесь на \"Отдельную генерацию\" в Настройках, чтобы получить доступ и нажать кнопку \"Обновить RPG-информацию\"",
|
||||||
|
"infobox.recentEvents.title": "Недавние события",
|
||||||
|
"infobox.recentEvents.addEventPlaceholder": "Добавить событие...",
|
||||||
|
"inventory.section.onPerson": "При себе",
|
||||||
|
"inventory.section.clothing": "Одежда",
|
||||||
|
"inventory.section.stored": "Хранимое",
|
||||||
|
"inventory.section.assets": "Активы",
|
||||||
|
"inventory.onPerson.empty": "Нет переносимых предметов",
|
||||||
|
"inventory.onPerson.title": "Предметы, которые сейчас в инвентаре",
|
||||||
|
"inventory.onPerson.addItemButton": "Добавить предмет",
|
||||||
|
"inventory.onPerson.addItemPlaceholder": "Введите название предмета...",
|
||||||
|
"inventory.clothing.empty": "Ничего не надето",
|
||||||
|
"inventory.clothing.title": "Одежда и броня",
|
||||||
|
"inventory.clothing.addItemButton": "Добавить одежду",
|
||||||
|
"inventory.clothing.addItemPlaceholder": "Введите элемент одежды...",
|
||||||
|
"inventory.stored.title": "Места хранения",
|
||||||
|
"inventory.stored.addLocationButton": "Добавить место",
|
||||||
|
"inventory.stored.addLocationPlaceholder": "Введите название места...",
|
||||||
|
"inventory.stored.saveButton": "Сохранить",
|
||||||
|
"inventory.stored.empty": "Пока нет мест хранения. Нажмите \"Добавить место\", чтобы создать.",
|
||||||
|
"inventory.stored.noItems": "Здесь нет хранимых предметов",
|
||||||
|
"inventory.stored.addItemToLocationPlaceholder": "Введите название предмета...",
|
||||||
|
"inventory.stored.addItemButton": "Добавить предмет",
|
||||||
|
"inventory.stored.confirmRemoveLocationMessage": "Удалить \"${location}\"? Это удалит все предметы, хранящиеся там.",
|
||||||
|
"inventory.stored.confirmRemoveLocationConfirmButton": "Подтвердить",
|
||||||
|
"inventory.assets.empty": "Нет активов",
|
||||||
|
"inventory.assets.title": "Транспорт, недвижимость и крупные владения",
|
||||||
|
"inventory.assets.addAssetModalTitle": "Добавить актив",
|
||||||
|
"inventory.assets.addAssetButton": "Добавить актив",
|
||||||
|
"inventory.assets.addAssetPlaceholder": "Введите название актива...",
|
||||||
|
"inventory.assets.description": "Активы включают транспортные средства (автомобили, мотоциклы), недвижимость (дома, квартиры) и крупное оборудование (инструменты для мастерской, специальные предметы).",
|
||||||
|
"quests.section.main": "Основное задание",
|
||||||
|
"quests.section.optional": "Дополнительные задания",
|
||||||
|
"quests.main.title": "Основные задания",
|
||||||
|
"quests.main.addQuestButton": "Добавить задание",
|
||||||
|
"quests.main.addQuestPlaceholder": "Введите название основного задания...",
|
||||||
|
"quests.main.empty": "Нет активных основных заданий",
|
||||||
|
"quests.main.hint": "Основное задание представляет вашу главную цель в истории.",
|
||||||
|
"quests.optional.title": "Дополнительные задания",
|
||||||
|
"quests.optional.addQuestButton": "Добавить задание",
|
||||||
|
"quests.optional.addQuestPlaceholder": "Введите название дополнительного задания...",
|
||||||
|
"quests.optional.empty": "Нет активных дополнительных заданий",
|
||||||
|
"quests.optional.hint": "Дополнительные задания - это побочные цели, которые дополняют основную историю.",
|
||||||
|
"checkpoint.setChapterStart": "Установить начало главы",
|
||||||
|
"checkpoint.clearChapterStart": "Очистить начало главы",
|
||||||
|
"checkpoint.indicator": "Начало главы",
|
||||||
|
"checkpoint.tooltip": "Сообщения до этой точки исключаются из контекста",
|
||||||
|
"musicPlayer.title": "Музыка сцены",
|
||||||
|
"musicPlayer.noMusic": "ИИ будет предлагать музыку, когда это уместно для сцены",
|
||||||
|
"errors.parsingError": "Ошибка парсинга RPG Companion Trackers! Модель вернула неправильный формат. Если проблема сохраняется, рассмотрите возможность смены модели для генераций.",
|
||||||
|
"settings.recommendedModels.title": "Рекомендуемые модели",
|
||||||
|
"settings.recommendedModels.description": "Для правильной работы расширения **не рекомендуется использовать модели с базой обчучения ниже 20B, особенно если они старые.** Оно лучше всего работает с современными моделями, такими как Deepseek, Claude, GPT или Gemini."
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"settings.language.label": "語言",
|
"settings.language.label": "語言",
|
||||||
"settings.language.option.en": "English",
|
"settings.language.option.en": "English",
|
||||||
"settings.language.option.zh-tw": "繁體中文",
|
"settings.language.option.zh-tw": "繁體中文",
|
||||||
|
"settings.language.option.ru": "Русский",
|
||||||
"settings.extensionEnabled": "啟用 RPG Companion",
|
"settings.extensionEnabled": "啟用 RPG Companion",
|
||||||
"settings.note": "切換開關以啟用/停用 RPG Companion。其他設定可在面板內配置。",
|
"settings.note": "切換開關以啟用/停用 RPG Companion。其他設定可在面板內配置。",
|
||||||
"template.settingsTitle": "RPG Companion 設定",
|
"template.settingsTitle": "RPG Companion 設定",
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ 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();
|
||||||
@@ -33,6 +37,10 @@ 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();
|
||||||
|
|||||||
@@ -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.6 settings.');
|
console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.3 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.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
|||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { generateAvatarsForCharacters } from '../features/avatarGenerator.js';
|
import { generateAvatarsForCharacters } from '../features/avatarGenerator.js';
|
||||||
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
||||||
|
import { updateStripWidgets } from '../ui/desktop.js';
|
||||||
|
|
||||||
// Store the original preset name to restore after tracker generation
|
// Store the original preset name to restore after tracker generation
|
||||||
let originalPresetName = null;
|
let originalPresetName = null;
|
||||||
@@ -240,8 +241,10 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
|
|
||||||
// Update button to show "Updating..." state
|
// Update button to show "Updating..." state
|
||||||
const $updateBtn = $('#rpg-manual-update');
|
const $updateBtn = $('#rpg-manual-update');
|
||||||
|
const $stripRefreshBtn = $('#rpg-strip-refresh');
|
||||||
const updatingText = i18n.getTranslation('template.mainPanel.updating') || 'Updating...';
|
const updatingText = i18n.getTranslation('template.mainPanel.updating') || 'Updating...';
|
||||||
$updateBtn.html(`<i class="fa-solid fa-spinner fa-spin"></i> ${updatingText}`).prop('disabled', true);
|
$updateBtn.html(`<i class="fa-solid fa-spinner fa-spin"></i> ${updatingText}`).prop('disabled', true);
|
||||||
|
$stripRefreshBtn.html('<i class="fa-solid fa-spinner fa-spin"></i>').prop('disabled', true);
|
||||||
|
|
||||||
const prompt = await generateSeparateUpdatePrompt();
|
const prompt = await generateSeparateUpdatePrompt();
|
||||||
|
|
||||||
@@ -380,11 +383,14 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
setFabLoadingState(false); // Stop spinning FAB on mobile
|
setFabLoadingState(false); // Stop spinning FAB on mobile
|
||||||
updateFabWidgets(); // Update FAB widgets with new data
|
updateFabWidgets(); // Update FAB widgets with new data
|
||||||
|
updateStripWidgets(); // Update strip widgets with new data
|
||||||
|
|
||||||
// Restore button to original state
|
// Restore button to original state
|
||||||
const $updateBtn = $('#rpg-manual-update');
|
const $updateBtn = $('#rpg-manual-update');
|
||||||
|
const $stripRefreshBtn = $('#rpg-strip-refresh');
|
||||||
const refreshText = i18n.getTranslation('template.mainPanel.refreshRpgInfo') || 'Refresh RPG Info';
|
const refreshText = i18n.getTranslation('template.mainPanel.refreshRpgInfo') || 'Refresh RPG Info';
|
||||||
$updateBtn.html(`<i class="fa-solid fa-sync"></i> ${refreshText}`).prop('disabled', false);
|
$updateBtn.html(`<i class="fa-solid fa-sync"></i> ${refreshText}`).prop('disabled', false);
|
||||||
|
$stripRefreshBtn.html('<i class="fa-solid fa-sync"></i>').prop('disabled', false);
|
||||||
|
|
||||||
// Reset the flag after tracker generation completes
|
// Reset the flag after tracker generation completes
|
||||||
// This ensures the flag persists through both main generation AND tracker generation
|
// This ensures the flag persists through both main generation AND tracker generation
|
||||||
@@ -405,6 +411,26 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
function parseCharactersFromThoughts(characterThoughtsData) {
|
function parseCharactersFromThoughts(characterThoughtsData) {
|
||||||
if (!characterThoughtsData) return [];
|
if (!characterThoughtsData) return [];
|
||||||
|
|
||||||
|
// Try parsing as JSON first (current format)
|
||||||
|
try {
|
||||||
|
const parsed = typeof characterThoughtsData === 'string'
|
||||||
|
? JSON.parse(characterThoughtsData)
|
||||||
|
: characterThoughtsData;
|
||||||
|
|
||||||
|
// Handle both {characters: [...]} and direct array formats
|
||||||
|
const charactersArray = Array.isArray(parsed) ? parsed : (parsed.characters || []);
|
||||||
|
|
||||||
|
if (charactersArray.length > 0) {
|
||||||
|
// Extract names from JSON character objects
|
||||||
|
return charactersArray
|
||||||
|
.map(char => char.name)
|
||||||
|
.filter(name => name && name.toLowerCase() !== 'unavailable');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON, fall back to text parsing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Parse text format (legacy)
|
||||||
const lines = characterThoughtsData.split('\n');
|
const lines = characterThoughtsData.split('\n');
|
||||||
const characters = [];
|
const characters = [];
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export async function buildEncounterInitPrompt() {
|
|||||||
|
|
||||||
// console.log('[RPG Companion] World info result:', { worldInfoString, length: worldInfoString?.length });
|
// console.log('[RPG Companion] World info result:', { worldInfoString, length: worldInfoString?.length });
|
||||||
|
|
||||||
if (worldInfoString && worldInfoString.trim()) {
|
if (worldInfoString && typeof worldInfoString === 'string' && worldInfoString.trim()) {
|
||||||
systemMessage += worldInfoString.trim();
|
systemMessage += worldInfoString.trim();
|
||||||
worldInfoAdded = true;
|
worldInfoAdded = true;
|
||||||
// console.log('[RPG Companion] ✅ Added world info from getWorldInfoPrompt');
|
// console.log('[RPG Companion] ✅ Added world info from getWorldInfoPrompt');
|
||||||
@@ -258,6 +258,7 @@ export async function buildEncounterInitPrompt() {
|
|||||||
|
|
||||||
initInstruction += `The combat starts now.\n\n`;
|
initInstruction += `The combat starts now.\n\n`;
|
||||||
initInstruction += `Based on everything above, generate the initial combat state. Analyze who is in the party fighting alongside ${userName} (if anyone), and who the enemies are. Replace placeholders in [brackets] and X with actual values. Return ONLY a JSON object with the following structure:\n\n`;
|
initInstruction += `Based on everything above, generate the initial combat state. Analyze who is in the party fighting alongside ${userName} (if anyone), and who the enemies are. Replace placeholders in [brackets] and X with actual values. Return ONLY a JSON object with the following structure:\n\n`;
|
||||||
|
initInstruction += `FORMAT:\n`;
|
||||||
initInstruction += `{\n`;
|
initInstruction += `{\n`;
|
||||||
initInstruction += ` "party": [\n`;
|
initInstruction += ` "party": [\n`;
|
||||||
initInstruction += ` {\n`;
|
initInstruction += ` {\n`;
|
||||||
@@ -268,7 +269,7 @@ export async function buildEncounterInitPrompt() {
|
|||||||
initInstruction += ` {"name": "Attack", "type": "single-target|AoE|both"},\n`;
|
initInstruction += ` {"name": "Attack", "type": "single-target|AoE|both"},\n`;
|
||||||
initInstruction += ` {"name": "Skill1", "type": "single-target|AoE|both"}\n`;
|
initInstruction += ` {"name": "Skill1", "type": "single-target|AoE|both"}\n`;
|
||||||
initInstruction += ` ],\n`;
|
initInstruction += ` ],\n`;
|
||||||
initInstruction += ` "items": ["Item1", "Item2"],\n`;
|
initInstruction += ` "items": ["Item Name x3", "Another Item x1"],\n`;
|
||||||
initInstruction += ` "statuses": [],\n`;
|
initInstruction += ` "statuses": [],\n`;
|
||||||
initInstruction += ` "isPlayer": true\n`;
|
initInstruction += ` "isPlayer": true\n`;
|
||||||
initInstruction += ` }\n`;
|
initInstruction += ` }\n`;
|
||||||
@@ -302,11 +303,14 @@ export async function buildEncounterInitPrompt() {
|
|||||||
initInstruction += ` - "single-target": Can only target one character (enemy or ally)\n`;
|
initInstruction += ` - "single-target": Can only target one character (enemy or ally)\n`;
|
||||||
initInstruction += ` - "AoE": Area of Effect - targets all enemies, but some AoE attacks (like storms, explosions) can also harm allies if the attack is indiscriminate\n`;
|
initInstruction += ` - "AoE": Area of Effect - targets all enemies, but some AoE attacks (like storms, explosions) can also harm allies if the attack is indiscriminate\n`;
|
||||||
initInstruction += ` - "both": Player can choose to target a single enemy OR use as AoE\n`;
|
initInstruction += ` - "both": Player can choose to target a single enemy OR use as AoE\n`;
|
||||||
|
initInstruction += `- For items array: Include quantities using format "Item Name xN" (e.g., "Health Potion x3", "Bomb x1")\n`;
|
||||||
|
initInstruction += ` - If only one item exists, you can use "Item Name x1" or just "Item Name"\n`;
|
||||||
|
initInstruction += ` - Items will be consumed when used - the quantity will decrease in future turns\n`;
|
||||||
initInstruction += `- Statuses array: May start empty, but don't have to if characters applied them before the combat\n`;
|
initInstruction += `- Statuses array: May start empty, but don't have to if characters applied them before the combat\n`;
|
||||||
initInstruction += ` - Each status has a format: {"name": "Status Name", "emoji": "💀", "duration": X}\n`;
|
initInstruction += ` - Each status has a format: {"name": "Status Name", "emoji": "💀", "duration": X}\n`;
|
||||||
initInstruction += ` - Examples: Poisoned (🧪), Burning (🔥), Blessed (✨), Stunned (💫), Weakened (⬇️), Strengthened (⬆️)\n\n`;
|
initInstruction += ` - Examples: Poisoned (🧪), Burning (🔥), Blessed (✨), Stunned (💫), Weakened (⬇️), Strengthened (⬆️)\n\n`;
|
||||||
initInstruction += `The styleNotes object will be used to visually style the combat window - choose ONE value from each category that best fits the environment described in the chat history.\n\n`;
|
initInstruction += `The styleNotes object will be used to visually style the combat window - choose ONE value from each category that best fits the environment described in the chat history.\n\n`;
|
||||||
initInstruction += `Use the user's current stats, inventory, and skills to populate the party data. For ${userName}'s attacks array, include their available skills. For items, include usable items from their inventory. Set HP based on their current Health stat if available.\n\n`;
|
initInstruction += `Use the user's current stats, inventory, and skills to populate the party data. For ${userName}'s attacks array, include their available skills. For items, include usable items from their inventory WITH QUANTITIES (e.g., "Health Potion x3"). Set HP based on their current Health stat if available.\n\n`;
|
||||||
initInstruction += `Ensure all party members and enemies have realistic HP values based on the setting and their descriptions. Return ONLY the JSON object, no other text.`;
|
initInstruction += `Ensure all party members and enemies have realistic HP values based on the setting and their descriptions. Return ONLY the JSON object, no other text.`;
|
||||||
|
|
||||||
// Only add the instruction if it has meaningful content
|
// Only add the instruction if it has meaningful content
|
||||||
@@ -364,7 +368,7 @@ export async function buildCombatActionPrompt(action, combatStats) {
|
|||||||
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
||||||
const worldInfoString = result?.worldInfoString || result;
|
const worldInfoString = result?.worldInfoString || result;
|
||||||
|
|
||||||
if (worldInfoString && worldInfoString.trim()) {
|
if (worldInfoString && typeof worldInfoString === 'string' && worldInfoString.trim()) {
|
||||||
systemMessage += worldInfoString.trim();
|
systemMessage += worldInfoString.trim();
|
||||||
worldInfoAdded = true;
|
worldInfoAdded = true;
|
||||||
}
|
}
|
||||||
@@ -483,12 +487,25 @@ export async function buildCombatActionPrompt(action, combatStats) {
|
|||||||
stateMessage += `Party Members:\n`;
|
stateMessage += `Party Members:\n`;
|
||||||
combatStats.party.forEach(member => {
|
combatStats.party.forEach(member => {
|
||||||
stateMessage += `- ${member.name}${member.isPlayer ? ' (Player)' : ''}: ${member.hp}/${member.maxHp} HP\n`;
|
stateMessage += `- ${member.name}${member.isPlayer ? ' (Player)' : ''}: ${member.hp}/${member.maxHp} HP\n`;
|
||||||
if (member.attacks && member.attacks.length > 0) {
|
|
||||||
stateMessage += ` Attacks: ${member.attacks.map(a => typeof a === 'string' ? a : a.name).join(', ')}\n`;
|
// For the player, use playerActions if available, otherwise fall back to member data
|
||||||
}
|
if (member.isPlayer && currentEncounter.playerActions) {
|
||||||
if (member.items && member.items.length > 0) {
|
if (currentEncounter.playerActions.attacks && currentEncounter.playerActions.attacks.length > 0) {
|
||||||
stateMessage += ` Items: ${member.items.join(', ')}\n`;
|
stateMessage += ` Attacks: ${currentEncounter.playerActions.attacks.map(a => typeof a === 'string' ? a : a.name).join(', ')}\n`;
|
||||||
|
}
|
||||||
|
if (currentEncounter.playerActions.items && currentEncounter.playerActions.items.length > 0) {
|
||||||
|
stateMessage += ` Items: ${currentEncounter.playerActions.items.join(', ')}\n`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-player party members, use their own data
|
||||||
|
if (member.attacks && member.attacks.length > 0) {
|
||||||
|
stateMessage += ` Attacks: ${member.attacks.map(a => typeof a === 'string' ? a : a.name).join(', ')}\n`;
|
||||||
|
}
|
||||||
|
if (member.items && member.items.length > 0) {
|
||||||
|
stateMessage += ` Items: ${member.items.join(', ')}\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member.statuses && member.statuses.length > 0) {
|
if (member.statuses && member.statuses.length > 0) {
|
||||||
const validStatuses = member.statuses.filter(s => s && (s.emoji || s.name));
|
const validStatuses = member.statuses.filter(s => s && (s.emoji || s.name));
|
||||||
if (validStatuses.length > 0) {
|
if (validStatuses.length > 0) {
|
||||||
@@ -515,11 +532,39 @@ export async function buildCombatActionPrompt(action, combatStats) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
stateMessage += `\n${userName}'s Action: ${action}\n\n`;
|
stateMessage += `\n${userName}'s Action: ${action}\n\n`;
|
||||||
stateMessage += `Respond with the exact JSON object as below, containing ONLY these specified values. Remember to consider the user's party and their moves. DO NOT regenerate character descriptions, sprites, or environment:\n`;
|
stateMessage += `Respond with the exact JSON object as below, containing ONLY these specified values. Remember to consider the user's party and their moves. DO NOT regenerate character descriptions, sprites, or environment.\n\n`;
|
||||||
|
stateMessage += `IMPORTANT - Update ${userName}'s attacks and items arrays based on what happens in combat:\n`;
|
||||||
|
stateMessage += `- ${userName}'s action is already specified above - do NOT regenerate it. Only update ${userName}'s attacks/items arrays if their action consumed resources (used item, lost ability, etc.).\n`;
|
||||||
|
stateMessage += `- If they use an item, decrement its quantity ("Health Potion x3" becomes "Health Potion x2"). If quantity reaches 0, remove the item entirely.\n`;
|
||||||
|
stateMessage += `- If they gain or lose an ability due to status effects, add or remove it from their attacks array.\n`;
|
||||||
|
stateMessage += ` Examples: Disarmed → remove weapon attacks. Bound → remove all attacks or set to []. Freed → restore attacks.\n`;
|
||||||
|
stateMessage += `- If they pick up a weapon/item during combat, add it to their items or attacks array.\n`;
|
||||||
|
stateMessage += `- If environmental changes enable new actions (near water → "Splash Attack"), add them. If they disable actions (fire goes out → remove "Ignite"), remove them.\n`;
|
||||||
|
stateMessage += `- Status effects should persist and decrease duration each turn. Remove statuses when duration reaches 0.\n\n`;
|
||||||
|
stateMessage += `FORMAT:\n`;
|
||||||
stateMessage += `{\n`;
|
stateMessage += `{\n`;
|
||||||
stateMessage += ` "combatStats": {\n`;
|
stateMessage += ` "combatStats": {\n`;
|
||||||
stateMessage += ` "party": [{ "name": "Name", "hp": X, "maxHp": X, "statuses": [...] }],\n`;
|
stateMessage += ` "party": [\n`;
|
||||||
stateMessage += ` "enemies": [{ "name": "Name", "hp": X, "maxHp": X, "statuses": [...] }]\n`;
|
stateMessage += ` {\n`;
|
||||||
|
stateMessage += ` "name": "Name",\n`;
|
||||||
|
stateMessage += ` "hp": X,\n`;
|
||||||
|
stateMessage += ` "maxHp": X,\n`;
|
||||||
|
stateMessage += ` "statuses": [{"name": "Status", "emoji": "💀", "duration": X}],\n`;
|
||||||
|
stateMessage += ` "isPlayer": true|false\n`;
|
||||||
|
stateMessage += ` }\n`;
|
||||||
|
stateMessage += ` ],\n`;
|
||||||
|
stateMessage += ` "enemies": [\n`;
|
||||||
|
stateMessage += ` {\n`;
|
||||||
|
stateMessage += ` "name": "Name",\n`;
|
||||||
|
stateMessage += ` "hp": X,\n`;
|
||||||
|
stateMessage += ` "maxHp": X,\n`;
|
||||||
|
stateMessage += ` "statuses": [{"name": "Status", "emoji": "💀", "duration": X}]\n`;
|
||||||
|
stateMessage += ` }\n`;
|
||||||
|
stateMessage += ` ]\n`;
|
||||||
|
stateMessage += ` },\n`;
|
||||||
|
stateMessage += ` "playerActions": {\n`;
|
||||||
|
stateMessage += ` "attacks": [{"name": "Attack", "type": "single-target|AoE|both"}],\n`;
|
||||||
|
stateMessage += ` "items": ["Item Name x3", "Another Item x1"]\n`;
|
||||||
stateMessage += ` },\n`;
|
stateMessage += ` },\n`;
|
||||||
stateMessage += ` "enemyActions": [{ "enemyName": "Name", "action": "what they do", "target": "target" }],\n`;
|
stateMessage += ` "enemyActions": [{ "enemyName": "Name", "action": "what they do", "target": "target" }],\n`;
|
||||||
stateMessage += ` "partyActions": [{ "memberName": "Name", "action": "what they do", "target": "target" }],\n`;
|
stateMessage += ` "partyActions": [{ "memberName": "Name", "action": "what they do", "target": "target" }],\n`;
|
||||||
@@ -587,7 +632,7 @@ export async function buildCombatSummaryPrompt(combatLog, result) {
|
|||||||
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
||||||
const worldInfoString = result?.worldInfoString || result;
|
const worldInfoString = result?.worldInfoString || result;
|
||||||
|
|
||||||
if (worldInfoString && worldInfoString.trim()) {
|
if (worldInfoString && typeof worldInfoString === 'string' && worldInfoString.trim()) {
|
||||||
systemMessage += worldInfoString.trim();
|
systemMessage += worldInfoString.trim();
|
||||||
worldInfoAdded = true;
|
worldInfoAdded = true;
|
||||||
}
|
}
|
||||||
@@ -659,7 +704,9 @@ export async function buildCombatSummaryPrompt(combatLog, result) {
|
|||||||
summaryMessage += `\n\nProvide a narrative summary of the entire fight in a way that fits the style from the chat history. Start with [FIGHT CONCLUDED] on the first line, then provide the description.\n\n`;
|
summaryMessage += `\n\nProvide a narrative summary of the entire fight in a way that fits the style from the chat history. Start with [FIGHT CONCLUDED] on the first line, then provide the description.\n\n`;
|
||||||
summaryMessage += `Write with intent in ${tense} tense ${person}-person ${narration} from ${pov}'s point of view.\n`;
|
summaryMessage += `Write with intent in ${tense} tense ${person}-person ${narration} from ${pov}'s point of view.\n`;
|
||||||
summaryMessage += `Build novel prose. Break patterns from your previous responses by varying sentence structures, rhythms, formats, and openings. If you last started with a narration, begin this one with dialogue; if with an action, switch to an internal thought. Track whether a sentence shape, cadence, sensory detail, or a descriptor appeared recently, and replace it with a different one or skip it entirely. If you mentioned a physical feature, mannerism, or expression once, move on. Don't fixate on the same traits every turn. Well-placed humor, unconventional similes, and subverted turns of phrase are encouraged. Avoid GPTisms, purple prose, and anaphoras, especially the negatives with "no/not/wasn't/didn't." Refrain from describing actions or emotions via negation; focus on what does happen, not what doesn't. Minimize asterisks, ellipses, and em-dashes. Explicit content is allowed. Never end on handover cues; finish naturally.\n\n`;
|
summaryMessage += `Build novel prose. Break patterns from your previous responses by varying sentence structures, rhythms, formats, and openings. If you last started with a narration, begin this one with dialogue; if with an action, switch to an internal thought. Track whether a sentence shape, cadence, sensory detail, or a descriptor appeared recently, and replace it with a different one or skip it entirely. If you mentioned a physical feature, mannerism, or expression once, move on. Don't fixate on the same traits every turn. Well-placed humor, unconventional similes, and subverted turns of phrase are encouraged. Avoid GPTisms, purple prose, and anaphoras, especially the negatives with "no/not/wasn't/didn't." Refrain from describing actions or emotions via negation; focus on what does happen, not what doesn't. Minimize asterisks, ellipses, and em-dashes. Explicit content is allowed. Never end on handover cues; finish naturally.\n\n`;
|
||||||
summaryMessage += `Express ${userName}'s actions and dialogue using indirect speech (e.g., "${userName} swung their sword" or "${userName} asked for help"). The summary should be 2-4 paragraphs and capture the essence of the battle.\n\n`;
|
summaryMessage += `Dialogue Guidelines:\n`;
|
||||||
|
summaryMessage += `- Include ALL dialogue lines spoken by enemies and NPC party members during the encounter in direct quotes.\n`;
|
||||||
|
summaryMessage += `- Never quote ${userName} directly. Express their actions and dialogue using ONLY indirect speech (e.g., "${userName} swung their sword" or "${userName} asked for help").\n\n`;
|
||||||
|
|
||||||
// If in Together mode and trackers are enabled, add tracker update instructions
|
// If in Together mode and trackers are enabled, add tracker update instructions
|
||||||
if (extensionSettings.generationMode === 'together' && (extensionSettings.showUserStats || extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts)) {
|
if (extensionSettings.generationMode === 'together' && (extensionSettings.showUserStats || extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts)) {
|
||||||
@@ -721,6 +768,12 @@ export async function buildCombatSummaryPrompt(combatLog, result) {
|
|||||||
*/
|
*/
|
||||||
export function parseEncounterJSON(response) {
|
export function parseEncounterJSON(response) {
|
||||||
try {
|
try {
|
||||||
|
// Ensure response is a string
|
||||||
|
if (!response || typeof response !== 'string') {
|
||||||
|
console.error('[RPG Companion] parseEncounterJSON received non-string input:', typeof response);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove code blocks if present
|
// Remove code blocks if present
|
||||||
let cleaned = response.trim();
|
let cleaned = response.trim();
|
||||||
|
|
||||||
@@ -736,6 +789,9 @@ export function parseEncounterJSON(response) {
|
|||||||
|
|
||||||
if (firstBrace !== -1 && lastBrace !== -1) {
|
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||||
cleaned = cleaned.substring(firstBrace, lastBrace + 1);
|
cleaned = cleaned.substring(firstBrace, lastBrace + 1);
|
||||||
|
} else {
|
||||||
|
console.error('[RPG Companion] No JSON object found in response');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse directly first
|
// Try to parse directly first
|
||||||
|
|||||||
@@ -4,15 +4,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { setExtensionPrompt, extension_prompt_types, extension_prompt_roles, eventSource, event_types } from '../../../../../../../script.js';
|
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, eventSource, event_types } from '../../../../../../../script.js';
|
||||||
import {
|
import {
|
||||||
extensionSettings,
|
extensionSettings,
|
||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
lastGeneratedData,
|
lastGeneratedData,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
lastActionWasSwipe,
|
lastActionWasSwipe
|
||||||
setLastActionWasSwipe,
|
|
||||||
setIsGenerating
|
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { evaluateSuppression } from './suppression.js';
|
import { evaluateSuppression } from './suppression.js';
|
||||||
import { parseUserStats } from './parser.js';
|
import { parseUserStats } from './parser.js';
|
||||||
@@ -24,8 +22,11 @@ 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';
|
||||||
@@ -39,9 +40,11 @@ let currentSuppressionState = false;
|
|||||||
// Track last chat length we committed at to prevent duplicate commits from streaming
|
// Track last chat length we committed at to prevent duplicate commits from streaming
|
||||||
let lastCommittedChatLength = -1;
|
let lastCommittedChatLength = -1;
|
||||||
|
|
||||||
// Store original message content for restoration after generation
|
// Store context map for prompt injection (used by event handlers)
|
||||||
// Map of message index -> original mes content
|
let pendingContextMap = new Map();
|
||||||
let originalMessageContent = new Map();
|
|
||||||
|
// Flag to track if injection already happened in BEFORE_COMBINE
|
||||||
|
let historyInjectionDone = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a map of historical context data from ST chat messages with rpg_companion_swipes data.
|
* Builds a map of historical context data from ST chat messages with rpg_companion_swipes data.
|
||||||
@@ -72,7 +75,8 @@ function buildHistoricalContextMap() {
|
|||||||
const maxMessages = messageCount === 0 ? chat.length : Math.min(messageCount, chat.length);
|
const maxMessages = messageCount === 0 ? chat.length : Math.min(messageCount, chat.length);
|
||||||
|
|
||||||
// Find the last assistant message - this is the one that gets current context via setExtensionPrompt
|
// Find the last assistant message - this is the one that gets current context via setExtensionPrompt
|
||||||
// We should NOT add historical context to it
|
// We should NOT add historical context to it when injecting into assistant messages
|
||||||
|
// But when injecting into user messages, we DO need to process it to get context for the preceding user message
|
||||||
let lastAssistantIndex = -1;
|
let lastAssistantIndex = -1;
|
||||||
for (let i = chat.length - 1; i >= 0; i--) {
|
for (let i = chat.length - 1; i >= 0; i--) {
|
||||||
if (!chat[i].is_user && !chat[i].is_system) {
|
if (!chat[i].is_user && !chat[i].is_system) {
|
||||||
@@ -82,9 +86,12 @@ function buildHistoricalContextMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Iterate through messages to find those with tracker data
|
// Iterate through messages to find those with tracker data
|
||||||
// Start from before the last assistant message
|
// For user_message_end: start from the last assistant message (we need its context for the preceding user message)
|
||||||
|
// For assistant_message_end: start from before the last assistant message (it gets current context via setExtensionPrompt)
|
||||||
let processedCount = 0;
|
let processedCount = 0;
|
||||||
const startIndex = lastAssistantIndex > 0 ? lastAssistantIndex - 1 : chat.length - 2;
|
const startIndex = position === 'user_message_end'
|
||||||
|
? lastAssistantIndex
|
||||||
|
: (lastAssistantIndex > 0 ? lastAssistantIndex - 1 : chat.length - 2);
|
||||||
|
|
||||||
for (let i = startIndex; i >= 0 && (messageCount === 0 || processedCount < maxMessages); i--) {
|
for (let i = startIndex; i >= 0 && (messageCount === 0 || processedCount < maxMessages); i--) {
|
||||||
const message = chat[i];
|
const message = chat[i];
|
||||||
@@ -134,14 +141,15 @@ function buildHistoricalContextMap() {
|
|||||||
let targetIndex = i; // Default: the assistant message itself
|
let targetIndex = i; // Default: the assistant message itself
|
||||||
|
|
||||||
if (position === 'user_message_end') {
|
if (position === 'user_message_end') {
|
||||||
// Find the next user message after this assistant message
|
// Find the preceding user message before this assistant message
|
||||||
for (let j = i + 1; j < chat.length; j++) {
|
// This is the user message that prompted this assistant response
|
||||||
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
if (chat[j].is_user && !chat[j].is_system) {
|
if (chat[j].is_user && !chat[j].is_system) {
|
||||||
targetIndex = j;
|
targetIndex = j;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If no user message found after, skip this one
|
// If no user message found before, skip this one
|
||||||
if (targetIndex === i) {
|
if (targetIndex === i) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -164,81 +172,378 @@ function buildHistoricalContextMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injects historical context into chat messages by modifying them in-place.
|
* Prepares historical context for injection into prompts.
|
||||||
* Stores original content for restoration after generation.
|
* This builds the context map and stores it for use by prompt event handlers.
|
||||||
* This approach works for ALL API types (text completion and chat completion).
|
* Does NOT modify the original chat messages.
|
||||||
*/
|
*/
|
||||||
function injectHistoricalContextIntoChat() {
|
function prepareHistoricalContextInjection() {
|
||||||
const historyPersistence = extensionSettings.historyPersistence;
|
const historyPersistence = extensionSettings.historyPersistence;
|
||||||
if (!historyPersistence || !historyPersistence.enabled) {
|
if (!historyPersistence || !historyPersistence.enabled) {
|
||||||
// console.log('[RPG Companion] History persistence not enabled, skipping injection');
|
pendingContextMap = new Map();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSuppressionState || !extensionSettings.enabled) {
|
if (currentSuppressionState || !extensionSettings.enabled) {
|
||||||
// console.log('[RPG Companion] Skipping history injection: suppressed or disabled');
|
pendingContextMap = new Map();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const chat = context.chat;
|
const chat = context.chat;
|
||||||
if (!chat || chat.length < 2) {
|
if (!chat || chat.length < 2) {
|
||||||
// console.log('[RPG Companion] Chat too short, skipping history injection');
|
pendingContextMap = new Map();
|
||||||
|
historyInjectionDone = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the context map
|
// Build and store the context map for use by prompt handlers
|
||||||
const contextMap = buildHistoricalContextMap();
|
pendingContextMap = buildHistoricalContextMap();
|
||||||
if (contextMap.size === 0) {
|
historyInjectionDone = false; // Reset flag for new generation
|
||||||
// console.log('[RPG Companion] No historical context to inject');
|
}
|
||||||
return;
|
|
||||||
|
/**
|
||||||
|
* Finds the best match position for message content in the prompt.
|
||||||
|
* Tries full content first, then progressively smaller suffixes.
|
||||||
|
*
|
||||||
|
* @param {string} prompt - The prompt to search in
|
||||||
|
* @param {string} messageContent - The message content to find
|
||||||
|
* @returns {{start: number, end: number}|null} - Position info or null if not found
|
||||||
|
*/
|
||||||
|
function findMessageInPrompt(prompt, messageContent) {
|
||||||
|
if (!messageContent || !prompt) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(`[RPG Companion] Injecting historical context into ${contextMap.size} messages`);
|
// Try to find the full content first
|
||||||
|
let searchIndex = prompt.lastIndexOf(messageContent);
|
||||||
|
|
||||||
// Clear any previous stored content
|
if (searchIndex !== -1) {
|
||||||
originalMessageContent.clear();
|
return { start: searchIndex, end: searchIndex + messageContent.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If full content not found, try last N characters with progressively smaller chunks
|
||||||
|
// This handles cases where messages are truncated in the prompt
|
||||||
|
const searchLengths = [500, 300, 200, 100, 50];
|
||||||
|
|
||||||
|
for (const len of searchLengths) {
|
||||||
|
if (messageContent.length <= len) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchContent = messageContent.slice(-len);
|
||||||
|
searchIndex = prompt.lastIndexOf(searchContent);
|
||||||
|
|
||||||
|
if (searchIndex !== -1) {
|
||||||
|
return { start: searchIndex, end: searchIndex + searchContent.length };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects historical context into a text completion prompt string.
|
||||||
|
* Searches for message content in the prompt and appends context after matches.
|
||||||
|
*
|
||||||
|
* @param {string} prompt - The text completion prompt
|
||||||
|
* @returns {string} - The modified prompt with injected context
|
||||||
|
*/
|
||||||
|
function injectContextIntoTextPrompt(prompt) {
|
||||||
|
if (pendingContextMap.size === 0) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
let modifiedPrompt = prompt;
|
||||||
let injectedCount = 0;
|
let injectedCount = 0;
|
||||||
for (const [msgIdx, ctxContent] of contextMap) {
|
|
||||||
|
// Sort by message index descending so we inject from end to start
|
||||||
|
// This prevents position shifts from affecting earlier injections
|
||||||
|
const sortedEntries = Array.from(pendingContextMap.entries()).sort((a, b) => b[0] - a[0]);
|
||||||
|
|
||||||
|
// Process each message that needs context injection
|
||||||
|
for (const [msgIdx, ctxContent] of sortedEntries) {
|
||||||
const message = chat[msgIdx];
|
const message = chat[msgIdx];
|
||||||
if (!message || typeof message.mes !== 'string') {
|
if (!message || typeof message.mes !== 'string') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store original content for restoration
|
// Find the message content in the prompt
|
||||||
originalMessageContent.set(msgIdx, message.mes);
|
const position = findMessageInPrompt(modifiedPrompt, message.mes);
|
||||||
|
|
||||||
// Modify the message in-place
|
if (!position) {
|
||||||
message.mes = message.mes + ctxContent;
|
// Message not found in prompt (might be truncated or not included)
|
||||||
|
console.debug(`[RPG Companion] Could not find message ${msgIdx} in prompt for context injection`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the context after the message content
|
||||||
|
modifiedPrompt = modifiedPrompt.slice(0, position.end) + ctxContent + modifiedPrompt.slice(position.end);
|
||||||
injectedCount++;
|
injectedCount++;
|
||||||
// console.log(`[RPG Companion] Injected context into message ${msgIdx}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(`[RPG Companion] Successfully injected historical context into ${injectedCount} messages`);
|
if (injectedCount > 0) {
|
||||||
|
console.log(`[RPG Companion] Injected historical context into ${injectedCount} positions in text prompt`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifiedPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores original message content after generation completes.
|
* Injects historical context into a chat completion message array.
|
||||||
* This ensures the injected context doesn't persist in the actual chat data.
|
* Modifies the content of messages in the array directly.
|
||||||
|
*
|
||||||
|
* @param {Array} chatMessages - The chat completion message array
|
||||||
|
* @returns {Array} - The modified message array with injected context
|
||||||
*/
|
*/
|
||||||
function restoreOriginalMessageContent() {
|
function injectContextIntoChatPrompt(chatMessages) {
|
||||||
if (originalMessageContent.size === 0) {
|
if (pendingContextMap.size === 0 || !Array.isArray(chatMessages)) {
|
||||||
return;
|
return chatMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const chat = context.chat;
|
const chat = context.chat;
|
||||||
|
let injectedCount = 0;
|
||||||
|
|
||||||
// console.log(`[RPG Companion] Restoring ${originalMessageContent.size} messages to original content`);
|
// Process each message that needs context injection
|
||||||
|
for (const [msgIdx, ctxContent] of pendingContextMap) {
|
||||||
|
const originalMessage = chat[msgIdx];
|
||||||
|
if (!originalMessage || typeof originalMessage.mes !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (const [msgIdx, originalContent] of originalMessageContent) {
|
const messageContent = originalMessage.mes;
|
||||||
if (chat[msgIdx]) {
|
|
||||||
chat[msgIdx].mes = originalContent;
|
// Find this message in the chat completion array by matching content
|
||||||
|
// Try full content first, then progressively smaller suffixes
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
for (const promptMsg of chatMessages) {
|
||||||
|
if (!promptMsg.content || typeof promptMsg.content !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try full content match
|
||||||
|
if (promptMsg.content.includes(messageContent)) {
|
||||||
|
promptMsg.content = promptMsg.content + ctxContent;
|
||||||
|
injectedCount++;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try suffix matches for truncated messages
|
||||||
|
const searchLengths = [500, 300, 200, 100, 50];
|
||||||
|
for (const len of searchLengths) {
|
||||||
|
if (messageContent.length <= len) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchContent = messageContent.slice(-len);
|
||||||
|
if (promptMsg.content.includes(searchContent)) {
|
||||||
|
promptMsg.content = promptMsg.content + ctxContent;
|
||||||
|
injectedCount++;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
console.debug(`[RPG Companion] Could not find message ${msgIdx} in chat prompt for context injection`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
originalMessageContent.clear();
|
if (injectedCount > 0) {
|
||||||
|
console.log(`[RPG Companion] Injected historical context into ${injectedCount} messages in chat prompt`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects historical context into finalMesSend message array (text completion).
|
||||||
|
* Iterates through chat and finalMesSend in order, matching by content to skip injected messages.
|
||||||
|
*
|
||||||
|
* @param {Array} finalMesSend - The array of message objects {message: string, extensionPrompts: []}
|
||||||
|
* @returns {number} - Number of injections made
|
||||||
|
*/
|
||||||
|
function injectContextIntoFinalMesSend(finalMesSend) {
|
||||||
|
if (pendingContextMap.size === 0 || !Array.isArray(finalMesSend) || finalMesSend.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
if (!chat || chat.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let injectedCount = 0;
|
||||||
|
|
||||||
|
// Build a map from chat index to finalMesSend index by matching content in order
|
||||||
|
// This handles injected messages (author's note, OOC, etc.) that exist in finalMesSend but not in chat
|
||||||
|
const chatToMesSendMap = new Map();
|
||||||
|
let mesSendIdx = 0;
|
||||||
|
|
||||||
|
for (let chatIdx = 0; chatIdx < chat.length && mesSendIdx < finalMesSend.length; chatIdx++) {
|
||||||
|
const chatMsg = chat[chatIdx];
|
||||||
|
if (!chatMsg || chatMsg.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatContent = chatMsg.mes || '';
|
||||||
|
|
||||||
|
// Look for this chat message in finalMesSend starting from current position
|
||||||
|
// Skip any finalMesSend entries that don't match (they're injected content)
|
||||||
|
while (mesSendIdx < finalMesSend.length) {
|
||||||
|
const mesSendObj = finalMesSend[mesSendIdx];
|
||||||
|
if (!mesSendObj || !mesSendObj.message) {
|
||||||
|
mesSendIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this finalMesSend message contains the chat content
|
||||||
|
// Use a substring match since instruct formatting adds prefixes/suffixes
|
||||||
|
// Match with sufficient content (first 50 chars or full message if shorter)
|
||||||
|
const matchContent = chatContent.length > 50
|
||||||
|
? chatContent.substring(0, 50)
|
||||||
|
: chatContent;
|
||||||
|
|
||||||
|
if (matchContent && mesSendObj.message.includes(matchContent)) {
|
||||||
|
// Found a match - record the mapping
|
||||||
|
chatToMesSendMap.set(chatIdx, mesSendIdx);
|
||||||
|
mesSendIdx++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This finalMesSend entry doesn't match - it's injected content, skip it
|
||||||
|
mesSendIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now inject context using the map
|
||||||
|
for (const [chatIdx, ctxContent] of pendingContextMap) {
|
||||||
|
const targetMesSendIdx = chatToMesSendMap.get(chatIdx);
|
||||||
|
|
||||||
|
if (targetMesSendIdx === undefined) {
|
||||||
|
console.debug(`[RPG Companion] Chat message ${chatIdx} not found in finalMesSend mapping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mesSendObj = finalMesSend[targetMesSendIdx];
|
||||||
|
if (!mesSendObj || !mesSendObj.message) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append context to this message
|
||||||
|
mesSendObj.message = mesSendObj.message + ctxContent;
|
||||||
|
injectedCount++;
|
||||||
|
console.debug(`[RPG Companion] Injected context for chat[${chatIdx}] into finalMesSend[${targetMesSendIdx}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return injectedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for GENERATE_BEFORE_COMBINE_PROMPTS (text completion).
|
||||||
|
* Injects historical context into the finalMesSend array before prompt combination.
|
||||||
|
* This is more reliable than post-combine string searching.
|
||||||
|
*
|
||||||
|
* @param {Object} eventData - Event data with finalMesSend and other properties
|
||||||
|
*/
|
||||||
|
function onGenerateBeforeCombinePrompts(eventData) {
|
||||||
|
if (!eventData || !Array.isArray(eventData.finalMesSend)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip for OpenAI (uses chat completion)
|
||||||
|
if (eventData.api === 'openai') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only inject if we have pending context
|
||||||
|
if (pendingContextMap.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const injectedCount = injectContextIntoFinalMesSend(eventData.finalMesSend);
|
||||||
|
if (injectedCount > 0) {
|
||||||
|
console.log(`[RPG Companion] Injected historical context into ${injectedCount} messages in finalMesSend`);
|
||||||
|
historyInjectionDone = true; // Mark as done to prevent double injection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion).
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
function onGenerateAfterCombinePrompts(eventData) {
|
||||||
|
if (!eventData || typeof eventData.prompt !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.dryRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let didInjectHistory = false;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
console.log('[RPG Companion] Using fallback string-based injection (AFTER_COMBINE)');
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Remove extra newlines after last_message opening and closing tags
|
||||||
|
// Match exactly the double newline pattern
|
||||||
|
eventData.prompt = eventData.prompt.replace(/<last_message>\n\n/g, '<last_message>\n');
|
||||||
|
eventData.prompt = eventData.prompt.replace(/\n\n<\/last_message>/g, '\n</last_message>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for CHAT_COMPLETION_PROMPT_READY.
|
||||||
|
* Injects historical context into the chat message array.
|
||||||
|
* Also fixes newline spacing around <context> tags.
|
||||||
|
*
|
||||||
|
* @param {Object} eventData - Event data with chat property
|
||||||
|
*/
|
||||||
|
function onChatCompletionPromptReady(eventData) {
|
||||||
|
if (!eventData || !Array.isArray(eventData.chat)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.dryRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject historical context if we have pending context
|
||||||
|
if (pendingContextMap.size > 0) {
|
||||||
|
eventData.chat = injectContextIntoChatPrompt(eventData.chat);
|
||||||
|
// DON'T clear pendingContextMap here - let it persist for other generations
|
||||||
|
// (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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -307,7 +612,6 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
await restoreCheckpointOnLoad();
|
await restoreCheckpointOnLoad();
|
||||||
|
|
||||||
const currentChatLength = chat ? chat.length : 0;
|
const currentChatLength = chat ? chat.length : 0;
|
||||||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
|
||||||
|
|
||||||
// For TOGETHER mode: Commit when user sends message (before first generation)
|
// For TOGETHER mode: Commit when user sends message (before first generation)
|
||||||
if (extensionSettings.generationMode === 'together') {
|
if (extensionSettings.generationMode === 'together') {
|
||||||
@@ -504,6 +808,19 @@ 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
|
||||||
@@ -535,12 +852,14 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
const contextSummary = generateContextualSummary();
|
const contextSummary = generateContextualSummary();
|
||||||
|
|
||||||
if (contextSummary) {
|
if (contextSummary) {
|
||||||
const wrappedContext = `\nHere is context information about the current scene, and what follows is the last message in the chat history:
|
// Use custom context instructions prompt if set, otherwise use default
|
||||||
|
const contextInstructionsText = extensionSettings.customContextInstructionsPrompt || DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT;
|
||||||
|
|
||||||
|
const wrappedContext = `
|
||||||
<context>
|
<context>
|
||||||
${contextSummary}
|
${contextSummary}
|
||||||
|
${contextInstructionsText}
|
||||||
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>`;
|
||||||
</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
|
||||||
@@ -592,6 +911,19 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
|||||||
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
|
||||||
@@ -629,6 +961,7 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -636,22 +969,30 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
|||||||
// Set suppression state for the historical context injection
|
// Set suppression state for the historical context injection
|
||||||
currentSuppressionState = shouldSuppress;
|
currentSuppressionState = shouldSuppress;
|
||||||
|
|
||||||
// Inject historical context directly into chat messages
|
// Prepare historical context for injection into prompts
|
||||||
// This temporarily modifies messages and will be restored after generation
|
// This builds the context map but does NOT modify original chat messages
|
||||||
injectHistoricalContextIntoChat();
|
// The persistent event listeners will inject it into all prompts until cleared
|
||||||
|
prepareHistoricalContextInjection();
|
||||||
// Register a one-time listener to restore messages after prompt is built
|
|
||||||
// Using .once() so it auto-removes after firing
|
|
||||||
eventSource.once(event_types.GENERATE_AFTER_COMBINE_PROMPTS, () => {
|
|
||||||
restoreOriginalMessageContent();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when generation ends to restore original message content.
|
* Initialize the history injection event listeners.
|
||||||
* This should be called from the GENERATION_ENDED event handler.
|
* These are persistent listeners that inject context into ALL generations
|
||||||
|
* while pendingContextMap has data. Should be called once at extension init.
|
||||||
*/
|
*/
|
||||||
export function onGenerationEndedCleanup() {
|
export function initHistoryInjectionListeners() {
|
||||||
restoreOriginalMessageContent();
|
// Register persistent listeners for prompt injection
|
||||||
|
// These check pendingContextMap and only inject if there's data
|
||||||
|
|
||||||
|
// Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects)
|
||||||
|
eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
|
||||||
|
|
||||||
|
// Fallback: AFTER_COMBINE for text completion (string-based injection)
|
||||||
|
eventSource.on(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
|
||||||
|
|
||||||
|
// Chat completion (OpenAI, etc.)
|
||||||
|
eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
|
||||||
|
|
||||||
|
console.log('[RPG Companion] History injection listeners initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function buildUserStatsJSONInstruction() {
|
|||||||
const trackerConfig = extensionSettings.trackerConfig;
|
const trackerConfig = extensionSettings.trackerConfig;
|
||||||
const userStatsConfig = trackerConfig?.userStats;
|
const userStatsConfig = trackerConfig?.userStats;
|
||||||
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||||
|
const displayMode = userStatsConfig?.statsDisplayMode || 'percentage';
|
||||||
|
|
||||||
let instruction = '{\n';
|
let instruction = '{\n';
|
||||||
instruction += ' "stats": [\n';
|
instruction += ' "stats": [\n';
|
||||||
@@ -36,7 +37,12 @@ export function buildUserStatsJSONInstruction() {
|
|||||||
for (let i = 0; i < enabledStats.length; i++) {
|
for (let i = 0; i < enabledStats.length; i++) {
|
||||||
const stat = enabledStats[i];
|
const stat = enabledStats[i];
|
||||||
const comma = i < enabledStats.length - 1 ? ',' : '';
|
const comma = i < enabledStats.length - 1 ? ',' : '';
|
||||||
instruction += ` {"id": "${stat.id}", "name": "${stat.name}", "value": X}${comma}\n`;
|
if (displayMode === 'number') {
|
||||||
|
const maxValue = stat.maxValue || 100;
|
||||||
|
instruction += ` {"id": "${stat.id}", "name": "${stat.name}", "value": X}${comma} // 0 to ${maxValue}\n`;
|
||||||
|
} else {
|
||||||
|
instruction += ` {"id": "${stat.id}", "name": "${stat.name}", "value": X}${comma} // 0 to 100 (percentage)\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
instruction += ' ],\n';
|
instruction += ' ],\n';
|
||||||
@@ -45,9 +51,24 @@ export function buildUserStatsJSONInstruction() {
|
|||||||
if (userStatsConfig?.statusSection?.enabled) {
|
if (userStatsConfig?.statusSection?.enabled) {
|
||||||
instruction += ' "status": {\n';
|
instruction += ' "status": {\n';
|
||||||
if (userStatsConfig.statusSection.showMoodEmoji) {
|
if (userStatsConfig.statusSection.showMoodEmoji) {
|
||||||
instruction += ' "mood": "Mood Emoji",\n';
|
instruction += ' "mood": "Mood Emoji"';
|
||||||
|
}
|
||||||
|
// Add all custom status fields
|
||||||
|
const customFields = userStatsConfig.statusSection.customFields || [];
|
||||||
|
if (customFields.length > 0) {
|
||||||
|
for (let i = 0; i < customFields.length; i++) {
|
||||||
|
const fieldName = customFields[i].toLowerCase();
|
||||||
|
const fieldKey = toSnakeCase(fieldName);
|
||||||
|
const comma = (i === customFields.length - 1 && !userStatsConfig.statusSection.showMoodEmoji) ? '' : (userStatsConfig.statusSection.showMoodEmoji || i < customFields.length - 1 ? ',\n' : '\n');
|
||||||
|
if (i === 0 && userStatsConfig.statusSection.showMoodEmoji) {
|
||||||
|
instruction += ',\n';
|
||||||
|
}
|
||||||
|
instruction += ` "${fieldKey}": "[${fieldName}1, ${fieldName}2]"${comma}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!userStatsConfig.statusSection.showMoodEmoji && customFields.length > 0) {
|
||||||
|
instruction += '\n';
|
||||||
}
|
}
|
||||||
instruction += ' "conditions": "[Condition1, Condition2]"\n';
|
|
||||||
instruction += ' },\n';
|
instruction += ' },\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +126,8 @@ export function buildInfoBoxJSONInstruction() {
|
|||||||
let hasFields = false;
|
let hasFields = false;
|
||||||
|
|
||||||
if (widgets.date?.enabled) {
|
if (widgets.date?.enabled) {
|
||||||
instruction += ' "date": {"value": "Weekday, Month, Year"}';
|
const dateFormat = widgets.date.format || 'Weekday, Month, Year';
|
||||||
|
instruction += ` "date": {"value": "${dateFormat}"}`;
|
||||||
hasFields = true;
|
hasFields = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -198,7 +198,9 @@ export function parseResponse(responseText) {
|
|||||||
if (depth === 0) {
|
if (depth === 0) {
|
||||||
// Found complete JSON object
|
// Found complete JSON object
|
||||||
const jsonContent = cleanedResponse.substring(i, j).trim();
|
const jsonContent = cleanedResponse.substring(i, j).trim();
|
||||||
extractedObjects.push(jsonContent);
|
if (jsonContent) {
|
||||||
|
extractedObjects.push(jsonContent);
|
||||||
|
}
|
||||||
i = j;
|
i = j;
|
||||||
} else {
|
} else {
|
||||||
i++;
|
i++;
|
||||||
@@ -307,6 +309,9 @@ export function parseResponse(responseText) {
|
|||||||
for (let idx = 0; idx < jsonMatches.length; idx++) {
|
for (let idx = 0; idx < jsonMatches.length; idx++) {
|
||||||
const match = jsonMatches[idx];
|
const match = jsonMatches[idx];
|
||||||
const jsonContent = match[1].trim();
|
const jsonContent = match[1].trim();
|
||||||
|
|
||||||
|
if (!jsonContent) continue;
|
||||||
|
|
||||||
// console.log(`[RPG Parser] Parsing JSON block ${idx + 1}:`, jsonContent.substring(0, 100) + '...');
|
// console.log(`[RPG Parser] Parsing JSON block ${idx + 1}:`, jsonContent.substring(0, 100) + '...');
|
||||||
|
|
||||||
const parsed = repairJSON(jsonContent);
|
const parsed = repairJSON(jsonContent);
|
||||||
@@ -363,6 +368,9 @@ export function parseResponse(responseText) {
|
|||||||
debugLog('[RPG Parser] Found JSON blocks within XML tags');
|
debugLog('[RPG Parser] Found JSON blocks within XML tags');
|
||||||
for (const match of xmlJsonMatches) {
|
for (const match of xmlJsonMatches) {
|
||||||
const jsonContent = match[1].trim();
|
const jsonContent = match[1].trim();
|
||||||
|
|
||||||
|
if (!jsonContent) continue;
|
||||||
|
|
||||||
const parsed = repairJSON(jsonContent);
|
const parsed = repairJSON(jsonContent);
|
||||||
|
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
@@ -524,7 +532,7 @@ export function parseUserStats(statsText) {
|
|||||||
// Check if this is v3 JSON format - try to parse it first
|
// Check if this is v3 JSON format - try to parse it first
|
||||||
let statsData = null;
|
let statsData = null;
|
||||||
const trimmed = statsText.trim();
|
const trimmed = statsText.trim();
|
||||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
if (trimmed && (trimmed.startsWith('{') || trimmed.startsWith('['))) {
|
||||||
statsData = repairJSON(statsText);
|
statsData = repairJSON(statsText);
|
||||||
if (statsData) {
|
if (statsData) {
|
||||||
debugLog('[RPG Parser] ✓ Parsed as v3 JSON format');
|
debugLog('[RPG Parser] ✓ Parsed as v3 JSON format');
|
||||||
@@ -547,9 +555,15 @@ export function parseUserStats(statsText) {
|
|||||||
extensionSettings.userStats.mood = statsData.status.mood;
|
extensionSettings.userStats.mood = statsData.status.mood;
|
||||||
// console.log('[RPG Parser] ✓ Set mood =', statsData.status.mood);
|
// console.log('[RPG Parser] ✓ Set mood =', statsData.status.mood);
|
||||||
}
|
}
|
||||||
if (statsData.status.conditions) {
|
// Extract all custom status fields
|
||||||
extensionSettings.userStats.conditions = statsData.status.conditions;
|
const trackerConfig = extensionSettings.trackerConfig;
|
||||||
// console.log('[RPG Parser] ✓ Set conditions =', statsData.status.conditions);
|
const customFields = trackerConfig?.userStats?.statusSection?.customFields || [];
|
||||||
|
for (const fieldName of customFields) {
|
||||||
|
const fieldKey = fieldName.toLowerCase();
|
||||||
|
if (statsData.status[fieldKey]) {
|
||||||
|
extensionSettings.userStats[fieldKey] = statsData.status[fieldKey];
|
||||||
|
// console.log(`[RPG Parser] ✓ Set ${fieldKey} =`, statsData.status[fieldKey]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,6 +617,13 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -679,6 +700,7 @@ export function parseUserStats(statsText) {
|
|||||||
const statusConfig = trackerConfig?.userStats?.statusSection;
|
const statusConfig = trackerConfig?.userStats?.statusSection;
|
||||||
if (statusConfig?.enabled) {
|
if (statusConfig?.enabled) {
|
||||||
let moodMatch = null;
|
let moodMatch = null;
|
||||||
|
const customFields = statusConfig.customFields || [];
|
||||||
|
|
||||||
// Try Status: format
|
// Try Status: format
|
||||||
const statusMatch = statsText.match(/Status:\s*(.+)/i);
|
const statusMatch = statsText.match(/Status:\s*(.+)/i);
|
||||||
@@ -691,14 +713,30 @@ export function parseUserStats(statsText) {
|
|||||||
if (emoji) {
|
if (emoji) {
|
||||||
extensionSettings.userStats.mood = emoji;
|
extensionSettings.userStats.mood = emoji;
|
||||||
// Remaining text contains custom status fields
|
// Remaining text contains custom status fields
|
||||||
if (text) {
|
if (text && customFields.length > 0) {
|
||||||
extensionSettings.userStats.conditions = text;
|
// For first custom field, use the remaining text
|
||||||
|
const firstFieldKey = customFields[0].toLowerCase();
|
||||||
|
extensionSettings.userStats[firstFieldKey] = text;
|
||||||
}
|
}
|
||||||
moodMatch = true;
|
moodMatch = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No mood emoji, whole status is conditions
|
// No mood emoji, whole status goes to first custom field
|
||||||
extensionSettings.userStats.conditions = statusContent;
|
if (customFields.length > 0) {
|
||||||
|
const firstFieldKey = customFields[0].toLowerCase();
|
||||||
|
extensionSettings.userStats[firstFieldKey] = statusContent;
|
||||||
|
}
|
||||||
|
moodMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract individual custom status fields by name
|
||||||
|
for (const fieldName of customFields) {
|
||||||
|
const fieldKey = fieldName.toLowerCase();
|
||||||
|
const fieldRegex = new RegExp(`${fieldName}:\\s*(.+?)(?:,|$)`, 'i');
|
||||||
|
const fieldMatch = statsText.match(fieldRegex);
|
||||||
|
if (fieldMatch) {
|
||||||
|
extensionSettings.userStats[fieldKey] = fieldMatch[1].trim();
|
||||||
moodMatch = true;
|
moodMatch = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,7 +744,10 @@ export function parseUserStats(statsText) {
|
|||||||
debugLog('[RPG Parser] Status match:', {
|
debugLog('[RPG Parser] Status match:', {
|
||||||
found: !!moodMatch,
|
found: !!moodMatch,
|
||||||
mood: extensionSettings.userStats.mood,
|
mood: extensionSettings.userStats.mood,
|
||||||
conditions: extensionSettings.userStats.conditions
|
customFields: customFields.map(f => ({
|
||||||
|
name: f,
|
||||||
|
value: extensionSettings.userStats[f.toLowerCase()]
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,16 @@ export const DEFAULT_DIALOGUE_COLORING_PROMPT = `Wrap all character/NPC "dialogu
|
|||||||
/**
|
/**
|
||||||
* Default Deception System prompt text
|
* Default Deception System prompt text
|
||||||
*/
|
*/
|
||||||
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 brackets). This will be hidden from the user's view, but not to you, making it useful for future consequences: <lie>[Character] is [lying/deceiving/omitting], the truth is [truth]. Reason: [reason].</lie>`;
|
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
|
||||||
@@ -53,6 +62,11 @@ 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
|
||||||
@@ -392,21 +406,30 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
// Include attributes based on settings (only if includeAttributes is true)
|
// Include attributes based on settings (only if includeAttributes is true)
|
||||||
if (includeAttributes) {
|
if (includeAttributes) {
|
||||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||||
|
const shouldSendAttributes = alwaysSendAttributes && showRPGAttributes;
|
||||||
|
|
||||||
if (shouldSendAttributes) {
|
if (shouldSendAttributes) {
|
||||||
const attributesString = buildAttributesString();
|
const attributesString = buildAttributesString();
|
||||||
instructions += `${userName}'s attributes: ${attributesString}\n`;
|
instructions += `${userName}'s attributes: ${attributesString}\n`;
|
||||||
|
|
||||||
// Add dice roll context if there was one
|
|
||||||
if (extensionSettings.lastDiceRoll) {
|
|
||||||
const roll = extensionSettings.lastDiceRoll;
|
|
||||||
instructions += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
|
|
||||||
} else {
|
|
||||||
instructions += `\n`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add dice roll context if there was one (independent of attributes)
|
||||||
|
if (extensionSettings.lastDiceRoll) {
|
||||||
|
const roll = extensionSettings.lastDiceRoll;
|
||||||
|
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||||
|
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||||
|
const hasAttributes = includeAttributes && (alwaysSendAttributes && showRPGAttributes);
|
||||||
|
|
||||||
|
if (hasAttributes) {
|
||||||
|
instructions += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
|
||||||
|
} else {
|
||||||
|
instructions += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Decide whether they succeeded or failed the action they attempted.\n\n`;
|
||||||
|
}
|
||||||
|
} else if (includeAttributes && trackerConfig?.userStats?.alwaysSendAttributes && trackerConfig?.userStats?.showRPGAttributes !== false) {
|
||||||
|
instructions += `\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append HTML prompt if enabled AND includeHtmlPrompt is true
|
// Append HTML prompt if enabled AND includeHtmlPrompt is true
|
||||||
@@ -476,11 +499,22 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
|||||||
|
|
||||||
// Handle common object formats
|
// Handle common object formats
|
||||||
if (field && typeof field === 'object') {
|
if (field && typeof field === 'object') {
|
||||||
// Status object: {mood, conditions}
|
// Status object: {mood, [customFields...]}
|
||||||
if ('mood' in field && 'conditions' in field) {
|
if ('mood' in field) {
|
||||||
|
const statusParts = [];
|
||||||
const mood = getValue(field.mood);
|
const mood = getValue(field.mood);
|
||||||
const conditions = getValue(field.conditions);
|
if (mood) statusParts.push(mood);
|
||||||
return `${mood} - ${conditions}`;
|
|
||||||
|
// Add all other status fields (custom fields)
|
||||||
|
for (const [key, value] of Object.entries(field)) {
|
||||||
|
if (key !== 'mood') {
|
||||||
|
const fieldValue = getValue(value);
|
||||||
|
if (fieldValue && fieldValue !== 'None') {
|
||||||
|
statusParts.push(fieldValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return statusParts.join(' - ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skill/item/quest objects: {name}, {title}, {name, quantity}
|
// Skill/item/quest objects: {name}, {title}, {name, quantity}
|
||||||
@@ -526,12 +560,33 @@ 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');
|
||||||
formatted += `${statName}: ${stat.value}\n`;
|
const statId = stat.id || statName.toLowerCase();
|
||||||
|
formatted += `${statName}: ${formatStatValue(stat.value, statId)}\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -544,7 +599,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}: ${value}\n`;
|
formatted += `${displayName}: ${formatStatValue(value, statName)}\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -553,7 +608,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}: ${getValue(value)}\n`;
|
formatted += `${displayName}: ${formatStatValue(getValue(value), key)}\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -731,19 +786,37 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats historical tracker data from a message's rpg_companion_swipes data.
|
* Formats historical tracker data from a message's rpg_companion_swipes data.
|
||||||
* Only includes tracker fields that have persistInHistory enabled in trackerConfig.
|
* Only includes tracker fields that have persistInHistory enabled in trackerConfig,
|
||||||
|
* unless useAllEnabled is true, in which case it includes all enabled fields.
|
||||||
* Uses the same formatting as formatTrackerDataForContext but filtered by persistence settings.
|
* Uses the same formatting as formatTrackerDataForContext but filtered by persistence settings.
|
||||||
*
|
*
|
||||||
* @param {Object} trackerData - The tracker data from message.extra.rpg_companion_swipes[swipeId]
|
* @param {Object} trackerData - The tracker data from message.extra.rpg_companion_swipes[swipeId]
|
||||||
* @param {Object} trackerConfig - The tracker configuration from extensionSettings.trackerConfig
|
* @param {Object} trackerConfig - The tracker configuration from extensionSettings.trackerConfig
|
||||||
* @param {string} userName - The user's name for personalization
|
* @param {string} userName - The user's name for personalization
|
||||||
|
* @param {boolean} [useAllEnabled=false] - If true, include all enabled fields instead of only persistInHistory fields
|
||||||
* @returns {string} Formatted historical context or empty string if nothing to include
|
* @returns {string} Formatted historical context or empty string if nothing to include
|
||||||
*/
|
*/
|
||||||
export function formatHistoricalTrackerData(trackerData, trackerConfig, userName) {
|
export function formatHistoricalTrackerData(trackerData, trackerConfig, userName, useAllEnabled = false) {
|
||||||
if (!trackerData || !trackerConfig) {
|
if (!trackerData || !trackerConfig) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to check if a field should be included
|
||||||
|
const shouldInclude = (config) => {
|
||||||
|
if (useAllEnabled) {
|
||||||
|
return config?.enabled !== false; // Include if enabled (default true for most fields)
|
||||||
|
}
|
||||||
|
return config?.persistInHistory === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to check if a stat/attribute should be included
|
||||||
|
const shouldIncludeStat = (configStat) => {
|
||||||
|
if (useAllEnabled) {
|
||||||
|
return configStat?.enabled !== false;
|
||||||
|
}
|
||||||
|
return configStat?.persistInHistory === true;
|
||||||
|
};
|
||||||
|
|
||||||
let formatted = '';
|
let formatted = '';
|
||||||
|
|
||||||
// Helper to safely get values
|
// Helper to safely get values
|
||||||
@@ -789,11 +862,11 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
|||||||
|
|
||||||
let statsFormatted = '';
|
let statsFormatted = '';
|
||||||
|
|
||||||
// Custom stats with persistInHistory enabled
|
// Custom stats with persistInHistory enabled (or enabled if useAllEnabled)
|
||||||
if (userStatsData.stats && Array.isArray(userStatsData.stats) && userStatsConfig.customStats) {
|
if (userStatsData.stats && Array.isArray(userStatsData.stats) && userStatsConfig.customStats) {
|
||||||
for (const stat of userStatsData.stats) {
|
for (const stat of userStatsData.stats) {
|
||||||
const configStat = userStatsConfig.customStats.find(s => s.id === stat.id);
|
const configStat = userStatsConfig.customStats.find(s => s.id === stat.id);
|
||||||
if (configStat?.persistInHistory && stat.value !== undefined) {
|
if (shouldIncludeStat(configStat) && stat.value !== undefined) {
|
||||||
const statName = stat.name || configStat.name || stat.id;
|
const statName = stat.name || configStat.name || stat.id;
|
||||||
statsFormatted += `${statName}: ${stat.value}, `;
|
statsFormatted += `${statName}: ${stat.value}, `;
|
||||||
}
|
}
|
||||||
@@ -801,15 +874,23 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Status section
|
// Status section
|
||||||
if (userStatsConfig.statusSection?.persistInHistory && userStatsData.status) {
|
if (shouldInclude(userStatsConfig.statusSection) && userStatsData.status) {
|
||||||
const mood = getValue(userStatsData.status.mood || userStatsData.status);
|
const mood = getValue(userStatsData.status.mood || userStatsData.status);
|
||||||
const conditions = getValue(userStatsData.status.conditions);
|
if (mood && userStatsConfig.statusSection.showMoodEmoji) statsFormatted += `Mood: ${mood}, `;
|
||||||
if (mood) statsFormatted += `Mood: ${mood}, `;
|
|
||||||
if (conditions && conditions !== 'None') statsFormatted += `Conditions: ${conditions}, `;
|
// Add all custom status fields
|
||||||
|
const customFields = userStatsConfig.statusSection.customFields || [];
|
||||||
|
for (const fieldName of customFields) {
|
||||||
|
const fieldKey = fieldName.toLowerCase();
|
||||||
|
const fieldValue = getValue(userStatsData.status[fieldKey]);
|
||||||
|
if (fieldValue && fieldValue !== 'None') {
|
||||||
|
statsFormatted += `${fieldName}: ${fieldValue}, `;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skills section
|
// Skills section
|
||||||
if (userStatsConfig.skillsSection?.persistInHistory && userStatsData.skills) {
|
if (shouldInclude(userStatsConfig.skillsSection) && userStatsData.skills) {
|
||||||
const skillsList = Array.isArray(userStatsData.skills)
|
const skillsList = Array.isArray(userStatsData.skills)
|
||||||
? userStatsData.skills.map(s => getValue(s)).filter(s => s).join(', ')
|
? userStatsData.skills.map(s => getValue(s)).filter(s => s).join(', ')
|
||||||
: getValue(userStatsData.skills);
|
: getValue(userStatsData.skills);
|
||||||
@@ -817,7 +898,8 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inventory
|
// Inventory
|
||||||
if (userStatsConfig.inventoryPersistInHistory && userStatsData.inventory) {
|
const shouldIncludeInventory = useAllEnabled || userStatsConfig.inventoryPersistInHistory;
|
||||||
|
if (shouldIncludeInventory && userStatsData.inventory) {
|
||||||
const inv = userStatsData.inventory;
|
const inv = userStatsData.inventory;
|
||||||
if (inv.onPerson && Array.isArray(inv.onPerson) && inv.onPerson.length > 0) {
|
if (inv.onPerson && Array.isArray(inv.onPerson) && inv.onPerson.length > 0) {
|
||||||
const items = inv.onPerson.map(i => getValue(i)).filter(i => i);
|
const items = inv.onPerson.map(i => getValue(i)).filter(i => i);
|
||||||
@@ -830,7 +912,8 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Quests
|
// Quests
|
||||||
if (userStatsConfig.questsPersistInHistory && userStatsData.quests) {
|
const shouldIncludeQuests = useAllEnabled || userStatsConfig.questsPersistInHistory;
|
||||||
|
if (shouldIncludeQuests && userStatsData.quests) {
|
||||||
const quests = userStatsData.quests;
|
const quests = userStatsData.quests;
|
||||||
if (quests.main) {
|
if (quests.main) {
|
||||||
const mainQuest = getValue(quests.main);
|
const mainQuest = getValue(quests.main);
|
||||||
@@ -853,37 +936,37 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
|||||||
let infoFormatted = '';
|
let infoFormatted = '';
|
||||||
|
|
||||||
// Date
|
// Date
|
||||||
if (infoBoxConfig.widgets.date?.persistInHistory && infoBoxData.date) {
|
if (shouldInclude(infoBoxConfig.widgets.date) && infoBoxData.date) {
|
||||||
const date = getValue(infoBoxData.date);
|
const date = getValue(infoBoxData.date);
|
||||||
if (date) infoFormatted += `Date: ${date}, `;
|
if (date) infoFormatted += `Date: ${date}, `;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time
|
// Time
|
||||||
if (infoBoxConfig.widgets.time?.persistInHistory && infoBoxData.time) {
|
if (shouldInclude(infoBoxConfig.widgets.time) && infoBoxData.time) {
|
||||||
const time = getValue(infoBoxData.time);
|
const time = getValue(infoBoxData.time);
|
||||||
if (time) infoFormatted += `Time: ${time}, `;
|
if (time) infoFormatted += `Time: ${time}, `;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Weather
|
// Weather
|
||||||
if (infoBoxConfig.widgets.weather?.persistInHistory && infoBoxData.weather) {
|
if (shouldInclude(infoBoxConfig.widgets.weather) && infoBoxData.weather) {
|
||||||
const weather = getValue(infoBoxData.weather);
|
const weather = getValue(infoBoxData.weather);
|
||||||
if (weather) infoFormatted += `Weather: ${weather}, `;
|
if (weather) infoFormatted += `Weather: ${weather}, `;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature
|
// Temperature
|
||||||
if (infoBoxConfig.widgets.temperature?.persistInHistory && infoBoxData.temperature) {
|
if (shouldInclude(infoBoxConfig.widgets.temperature) && infoBoxData.temperature) {
|
||||||
const temp = getValue(infoBoxData.temperature);
|
const temp = getValue(infoBoxData.temperature);
|
||||||
if (temp) infoFormatted += `Temp: ${temp}, `;
|
if (temp) infoFormatted += `Temp: ${temp}, `;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
if (infoBoxConfig.widgets.location?.persistInHistory && infoBoxData.location) {
|
if (shouldInclude(infoBoxConfig.widgets.location) && infoBoxData.location) {
|
||||||
const location = getValue(infoBoxData.location);
|
const location = getValue(infoBoxData.location);
|
||||||
if (location) infoFormatted += `Location: ${location}, `;
|
if (location) infoFormatted += `Location: ${location}, `;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recent Events
|
// Recent Events
|
||||||
if (infoBoxConfig.widgets.recentEvents?.persistInHistory && infoBoxData.recentEvents) {
|
if (shouldInclude(infoBoxConfig.widgets.recentEvents) && infoBoxData.recentEvents) {
|
||||||
const events = getValue(infoBoxData.recentEvents);
|
const events = getValue(infoBoxData.recentEvents);
|
||||||
if (events) infoFormatted += `Events: ${events}, `;
|
if (events) infoFormatted += `Events: ${events}, `;
|
||||||
}
|
}
|
||||||
@@ -911,7 +994,7 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
|||||||
// Custom fields (appearance, demeanor, etc.)
|
// Custom fields (appearance, demeanor, etc.)
|
||||||
if (char.details && typeof char.details === 'object') {
|
if (char.details && typeof char.details === 'object') {
|
||||||
for (const field of charsConfig.customFields) {
|
for (const field of charsConfig.customFields) {
|
||||||
if (field.persistInHistory && char.details[field.id]) {
|
if (shouldIncludeStat(field) && char.details[field.id]) {
|
||||||
const value = getValue(char.details[field.id]);
|
const value = getValue(char.details[field.id]);
|
||||||
if (value) charFormatted += `${field.name}: ${value}, `;
|
if (value) charFormatted += `${field.name}: ${value}, `;
|
||||||
}
|
}
|
||||||
@@ -919,7 +1002,7 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Thoughts
|
// Thoughts
|
||||||
if (charsConfig.thoughts?.persistInHistory && char.thoughts) {
|
if (shouldInclude(charsConfig.thoughts) && char.thoughts) {
|
||||||
const thoughts = typeof char.thoughts === 'object' && char.thoughts.content
|
const thoughts = typeof char.thoughts === 'object' && char.thoughts.content
|
||||||
? getValue(char.thoughts.content)
|
? getValue(char.thoughts.content)
|
||||||
: getValue(char.thoughts);
|
: getValue(char.thoughts);
|
||||||
@@ -990,19 +1073,25 @@ export function generateContextualSummary() {
|
|||||||
|
|
||||||
// Include attributes based on settings
|
// Include attributes based on settings
|
||||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||||
|
const shouldSendAttributes = alwaysSendAttributes && showRPGAttributes;
|
||||||
|
|
||||||
if (shouldSendAttributes) {
|
if (shouldSendAttributes) {
|
||||||
const attributesString = buildAttributesString();
|
const attributesString = buildAttributesString();
|
||||||
summary += `${userName}'s attributes: ${attributesString}\n`;
|
summary += `${userName}'s attributes: ${attributesString}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
// Add dice roll context if there was one
|
// Add dice roll context if there was one (independent of attributes)
|
||||||
if (extensionSettings.lastDiceRoll) {
|
if (extensionSettings.lastDiceRoll) {
|
||||||
const roll = extensionSettings.lastDiceRoll;
|
const roll = extensionSettings.lastDiceRoll;
|
||||||
|
|
||||||
|
if (shouldSendAttributes) {
|
||||||
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
|
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
|
||||||
} else {
|
} else {
|
||||||
summary += `\n`;
|
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Decide whether they succeeded or failed the action they attempted.\n\n`;
|
||||||
}
|
}
|
||||||
|
} else if (shouldSendAttributes) {
|
||||||
|
summary += `\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return summary.trim();
|
return summary.trim();
|
||||||
@@ -1055,16 +1144,34 @@ export function generateRPGPromptText() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
// Include Present Characters data if it exists, regardless of current showCharacterThoughts setting
|
||||||
|
// This ensures existing character data is preserved in context even if the setting is toggled off
|
||||||
|
if (committedTrackerData.characterThoughts) {
|
||||||
try {
|
try {
|
||||||
// Try to parse as JSON - apply locks before adding to previous
|
let parsed;
|
||||||
const lockedData = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
// Check if it's already a JavaScript object/array (not a JSON string)
|
||||||
const parsed = JSON.parse(lockedData);
|
if (typeof committedTrackerData.characterThoughts === 'object') {
|
||||||
unifiedPrevious.characters = parsed;
|
// Already parsed - apply locks and use directly
|
||||||
} catch {
|
parsed = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
||||||
|
} else {
|
||||||
|
// It's a JSON string - apply locks and parse
|
||||||
|
const lockedData = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
||||||
|
parsed = JSON.parse(lockedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include if there's actual character data (non-empty array or object with content)
|
||||||
|
if (parsed && ((Array.isArray(parsed) && parsed.length > 0) ||
|
||||||
|
(parsed.characters && Array.isArray(parsed.characters) && parsed.characters.length > 0))) {
|
||||||
|
unifiedPrevious.characters = parsed;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// console.warn('[RPG Companion] Failed to process characters for previous section:', e);
|
||||||
// Old text format - show it separately for backward compat
|
// Old text format - show it separately for backward compat
|
||||||
if (!unifiedPrevious.userStats && !unifiedPrevious.infoBox) {
|
if (!unifiedPrevious.userStats && !unifiedPrevious.infoBox) {
|
||||||
promptText += `${committedTrackerData.characterThoughts}\n`;
|
const charText = typeof committedTrackerData.characterThoughts === 'string'
|
||||||
|
? committedTrackerData.characterThoughts
|
||||||
|
: JSON.stringify(committedTrackerData.characterThoughts, null, 2);
|
||||||
|
promptText += `${charText}\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1154,18 +1261,31 @@ export async function generateSeparateUpdatePrompt() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const swipeData = message.extra?.rpg_companion_swipes;
|
// Get the rpg_companion_swipes data for current swipe
|
||||||
|
// Data can be in two places:
|
||||||
|
// 1. message.extra.rpg_companion_swipes (current session, before save)
|
||||||
|
// 2. message.swipe_info[swipeId].extra.rpg_companion_swipes (loaded from file)
|
||||||
|
const currentSwipeId = message.swipe_id || 0;
|
||||||
|
let swipeData = message.extra?.rpg_companion_swipes;
|
||||||
|
|
||||||
|
// If not in message.extra, check swipe_info
|
||||||
|
if (!swipeData && message.swipe_info && message.swipe_info[currentSwipeId]) {
|
||||||
|
swipeData = message.swipe_info[currentSwipeId].extra?.rpg_companion_swipes;
|
||||||
|
}
|
||||||
|
|
||||||
if (!swipeData) {
|
if (!swipeData) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = message.swipe_id || 0;
|
|
||||||
const trackerData = swipeData[currentSwipeId];
|
const trackerData = swipeData[currentSwipeId];
|
||||||
if (!trackerData) {
|
if (!trackerData) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedContext = formatHistoricalTrackerData(trackerData, trackerConfig, userName);
|
// For Refresh RPG Info, use sendAllEnabledOnRefresh setting
|
||||||
|
// When true, include all enabled stats from preset instead of only persistInHistory stats
|
||||||
|
const useAllEnabled = historyPersistence.sendAllEnabledOnRefresh === true;
|
||||||
|
const formattedContext = formatHistoricalTrackerData(trackerData, trackerConfig, userName, useAllEnabled);
|
||||||
if (!formattedContext) {
|
if (!formattedContext) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1177,14 +1297,15 @@ export async function generateSeparateUpdatePrompt() {
|
|||||||
let targetIdx = i;
|
let targetIdx = i;
|
||||||
|
|
||||||
if (position === 'user_message_end') {
|
if (position === 'user_message_end') {
|
||||||
// Find next user message after this assistant message
|
// Find the preceding user message before this assistant message
|
||||||
for (let j = i + 1; j < recentMessages.length; j++) {
|
// This is the user message that prompted this assistant response
|
||||||
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
if (recentMessages[j].is_user && !recentMessages[j].is_system) {
|
if (recentMessages[j].is_user && !recentMessages[j].is_system) {
|
||||||
targetIdx = j;
|
targetIdx = j;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If no user message found, skip
|
// If no user message found before, skip
|
||||||
if (targetIdx === i) {
|
if (targetIdx === i) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { parseResponse, parseUserStats } from '../generation/parser.js';
|
|||||||
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
|
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
|
||||||
import { updateRPGData } from '../generation/apiClient.js';
|
import { updateRPGData } from '../generation/apiClient.js';
|
||||||
import { removeLocks } from '../generation/lockManager.js';
|
import { removeLocks } from '../generation/lockManager.js';
|
||||||
import { onGenerationStarted, onGenerationEndedCleanup } from '../generation/injector.js';
|
import { onGenerationStarted, initHistoryInjectionListeners } from '../generation/injector.js';
|
||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
import { renderUserStats } from '../rendering/userStats.js';
|
import { renderUserStats } from '../rendering/userStats.js';
|
||||||
@@ -45,6 +45,7 @@ import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
|||||||
|
|
||||||
// UI
|
// UI
|
||||||
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
||||||
|
import { updateStripWidgets } from '../ui/desktop.js';
|
||||||
|
|
||||||
// Chapter checkpoint
|
// Chapter checkpoint
|
||||||
import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js';
|
import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js';
|
||||||
@@ -114,10 +115,9 @@ export function onMessageSent() {
|
|||||||
// This allows auto-update to distinguish between new generations and loading chat history
|
// This allows auto-update to distinguish between new generations and loading chat history
|
||||||
setIsAwaitingNewMessage(true);
|
setIsAwaitingNewMessage(true);
|
||||||
|
|
||||||
// Show FAB loading state for together mode (starts spinning)
|
// Note: FAB spinning is NOT shown for together mode since no extra API request is made
|
||||||
if (extensionSettings.generationMode === 'together') {
|
// The RPG data comes embedded in the main response
|
||||||
setFabLoadingState(true);
|
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
|
||||||
}
|
|
||||||
|
|
||||||
// For separate mode with auto-update disabled, commit displayed tracker
|
// For separate mode with auto-update disabled, commit displayed tracker
|
||||||
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
|
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
|
||||||
@@ -233,6 +233,10 @@ export async function onMessageReceived(data) {
|
|||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
|
||||||
|
// Update FAB widgets and strip widgets with newly parsed data
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
|
||||||
// Then update the DOM to reflect the cleaned message
|
// Then update the DOM to reflect the cleaned message
|
||||||
// Using updateMessageBlock to perform macro substitutions + regex formatting
|
// Using updateMessageBlock to perform macro substitutions + regex formatting
|
||||||
const messageId = chat.length - 1;
|
const messageId = chat.length - 1;
|
||||||
@@ -264,9 +268,10 @@ export async function onMessageReceived(data) {
|
|||||||
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
|
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||||
// Update FAB widgets after separate/external mode update completes
|
// Update FAB widgets and strip widgets after separate/external mode update completes
|
||||||
setFabLoadingState(false);
|
setFabLoadingState(false);
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,6 +297,7 @@ export async function onMessageReceived(data) {
|
|||||||
// Stop FAB loading state and update widgets
|
// Stop FAB loading state and update widgets
|
||||||
setFabLoadingState(false);
|
setFabLoadingState(false);
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
|
||||||
// Re-apply checkpoint in case SillyTavern unhid messages during generation
|
// Re-apply checkpoint in case SillyTavern unhid messages during generation
|
||||||
await restoreCheckpointOnLoad();
|
await restoreCheckpointOnLoad();
|
||||||
@@ -330,8 +336,9 @@ export function onCharacterChanged() {
|
|||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
|
||||||
// Update FAB widgets with loaded data
|
// Update FAB widgets and strip widgets with loaded data
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
|
||||||
// Update chat thought overlays
|
// Update chat thought overlays
|
||||||
updateChatThoughts();
|
updateChatThoughts();
|
||||||
@@ -379,7 +386,7 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
|
|
||||||
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
||||||
|
|
||||||
// Load RPG data for this swipe
|
// IMPORTANT: onMessageSwiped is for DISPLAY only!
|
||||||
// lastGeneratedData is for DISPLAY, committedTrackerData is for GENERATION
|
// 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
|
// 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]) {
|
||||||
@@ -467,9 +474,6 @@ export function clearExtensionPrompts() {
|
|||||||
export async function onGenerationEnded() {
|
export async function onGenerationEnded() {
|
||||||
// console.log('[RPG Companion] 🏁 onGenerationEnded called');
|
// console.log('[RPG Companion] 🏁 onGenerationEnded called');
|
||||||
|
|
||||||
// Restore original message content that was modified for historical context injection
|
|
||||||
onGenerationEndedCleanup();
|
|
||||||
|
|
||||||
// Note: isGenerating flag is cleared in onMessageReceived after parsing (together mode)
|
// Note: isGenerating flag is cleared in onMessageReceived after parsing (together mode)
|
||||||
// or in apiClient.js after separate generation completes (separate mode)
|
// or in apiClient.js after separate generation completes (separate mode)
|
||||||
|
|
||||||
@@ -477,3 +481,11 @@ export async function onGenerationEnded() {
|
|||||||
// Re-apply checkpoint if one exists
|
// Re-apply checkpoint if one exists
|
||||||
await restoreCheckpointOnLoad();
|
await restoreCheckpointOnLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize history injection event listeners.
|
||||||
|
* Should be called once during extension initialization.
|
||||||
|
*/
|
||||||
|
export function initHistoryInjection() {
|
||||||
|
initHistoryInjectionListeners();
|
||||||
|
}
|
||||||
|
|||||||
@@ -436,14 +436,12 @@ export function renderInfoBox() {
|
|||||||
|
|
||||||
// Time widget - show if enabled
|
// Time widget - show if enabled
|
||||||
if (config?.widgets?.time?.enabled) {
|
if (config?.widgets?.time?.enabled) {
|
||||||
// Determine which time value to display and edit
|
// Get both start and end times
|
||||||
const hasTimeEnd = Boolean(data.timeEnd);
|
const timeStartDisplay = data.timeStart || '12:00';
|
||||||
const hasTimeStart = Boolean(data.timeStart);
|
const timeEndDisplay = data.timeEnd || data.timeStart || '12:00';
|
||||||
const timeDisplay = data.timeEnd || data.timeStart || '12:00';
|
|
||||||
const timeField = hasTimeEnd ? 'timeEnd' : 'timeStart';
|
|
||||||
|
|
||||||
// Parse time for clock hands
|
// Parse end time for clock hands (use end time for visual display)
|
||||||
const timeMatch = timeDisplay.match(/(\d+):(\d+)/);
|
const timeMatch = timeEndDisplay.match(/(\d+):(\d+)/);
|
||||||
let hourAngle = 0;
|
let hourAngle = 0;
|
||||||
let minuteAngle = 0;
|
let minuteAngle = 0;
|
||||||
if (timeMatch) {
|
if (timeMatch) {
|
||||||
@@ -465,7 +463,11 @@ export function renderInfoBox() {
|
|||||||
<div class="rpg-clock-center"></div>
|
<div class="rpg-clock-center"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="${timeField}" title="Click to edit">${timeDisplay}</div>
|
<div class="rpg-time-range">
|
||||||
|
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeStart" title="Click to edit start time">${timeStartDisplay}</div>
|
||||||
|
<span class="rpg-time-separator">→</span>
|
||||||
|
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeEnd" title="Click to edit end time">${timeEndDisplay}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,8 +212,12 @@ 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
|
// Get quests data - extract value if it's a locked object
|
||||||
const mainQuest = extensionSettings.quests.main || 'None';
|
let 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
|
||||||
|
|||||||
@@ -50,9 +50,11 @@ 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')
|
||||||
* @returns {string} Interpolated hex color
|
* @param {number} lowOpacity - Opacity for low values (0-100)
|
||||||
|
* @param {number} highOpacity - Opacity for high values (0-100)
|
||||||
|
* @returns {string} Interpolated rgba color
|
||||||
*/
|
*/
|
||||||
function getStatColor(percentage, lowColor, highColor) {
|
function getStatColor(percentage, lowColor, highColor, lowOpacity = 100, highOpacity = 100) {
|
||||||
// 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;
|
||||||
|
|
||||||
@@ -73,10 +75,9 @@ function getStatColor(percentage, lowColor, highColor) {
|
|||||||
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;
|
||||||
|
|
||||||
// Convert back to hex
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
const toHex = (n) => n.toString(16).padStart(2, '0');
|
|
||||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -539,7 +540,13 @@ export function renderThoughts() {
|
|||||||
<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(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh);
|
const statColor = getStatColor(
|
||||||
|
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>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the user stats text string using custom stat names
|
* Builds the user stats text string using custom stat names
|
||||||
@@ -105,7 +106,8 @@ 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 excludeFields = new Set(['mood', 'conditions', 'inventory', 'skills', 'level']);
|
const customFields = config.statusSection?.customFields || [];
|
||||||
|
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({
|
||||||
@@ -118,12 +120,17 @@ function updateUserStatsData() {
|
|||||||
|
|
||||||
jsonData.stats = statsArray;
|
jsonData.stats = statsArray;
|
||||||
|
|
||||||
// Update status
|
// Update status - include all custom status fields
|
||||||
jsonData.status = {
|
jsonData.status = {
|
||||||
mood: stats.mood || '😐',
|
mood: stats.mood || '😐'
|
||||||
conditions: stats.conditions || 'None'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add all custom status fields
|
||||||
|
for (const fieldName of customFields) {
|
||||||
|
const fieldKey = fieldName.toLowerCase();
|
||||||
|
jsonData.status[fieldKey] = stats[fieldKey] || 'None';
|
||||||
|
}
|
||||||
|
|
||||||
// Update inventory (convert to v3 format)
|
// Update inventory (convert to v3 format)
|
||||||
const convertToV3Items = (itemString) => {
|
const convertToV3Items = (itemString) => {
|
||||||
if (!itemString) return [];
|
if (!itemString) return [];
|
||||||
@@ -245,8 +252,9 @@ export function renderUserStats() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create gradient from low to high color
|
// Create gradient from low to high color with opacity
|
||||||
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
|
const colors = getStatBarColors();
|
||||||
|
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');
|
||||||
@@ -276,16 +284,33 @@ export function renderUserStats() {
|
|||||||
}
|
}
|
||||||
html += '<div class="rpg-stats-grid">';
|
html += '<div class="rpg-stats-grid">';
|
||||||
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
|
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
|
||||||
|
const displayMode = config.statsDisplayMode || 'percentage';
|
||||||
|
|
||||||
for (const stat of enabledStats) {
|
for (const stat of enabledStats) {
|
||||||
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
|
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
|
||||||
|
const maxValue = stat.maxValue || 100;
|
||||||
|
|
||||||
|
// Calculate percentage for bar fill
|
||||||
|
let percentage;
|
||||||
|
let displayValue;
|
||||||
|
|
||||||
|
if (displayMode === 'number') {
|
||||||
|
// In number mode, value is already the number (0 to maxValue)
|
||||||
|
percentage = maxValue > 0 ? (value / maxValue) * 100 : 100;
|
||||||
|
displayValue = `${value}/${maxValue}`;
|
||||||
|
} else {
|
||||||
|
// In percentage mode, value is 0-100
|
||||||
|
percentage = value;
|
||||||
|
displayValue = `${value}%`;
|
||||||
|
}
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-stat-row">
|
<div class="rpg-stat-row">
|
||||||
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="${stat.id}" title="Click to edit stat name">${stat.name}:</span>
|
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="${stat.id}" title="Click to edit stat name">${stat.name}:</span>
|
||||||
<div class="rpg-stat-bar" style="background: ${gradient}">
|
<div class="rpg-stat-bar" style="background: ${gradient}">
|
||||||
<div class="rpg-stat-fill" style="width: ${100 - value}%"></div>
|
<div class="rpg-stat-fill" style="width: ${100 - percentage}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="${stat.id}" title="Click to edit">${value}%</span>
|
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="${stat.id}" data-max="${maxValue}" data-mode="${displayMode}" title="Click to edit">${displayValue}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -308,13 +333,15 @@ 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 now, use first field as "conditions" for backward compatibility
|
for (const fieldName of config.statusSection.customFields) {
|
||||||
let conditionsValue = stats.conditions || 'None';
|
const fieldKey = fieldName.toLowerCase();
|
||||||
// Strip brackets if present (from JSON array format)
|
let fieldValue = stats[fieldKey] || 'None';
|
||||||
if (typeof conditionsValue === 'string') {
|
// Strip brackets if present (from JSON array format)
|
||||||
conditionsValue = conditionsValue.replace(/^\[|\]$/g, '').trim();
|
if (typeof fieldValue === 'string') {
|
||||||
|
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="conditions" title="Click to edit conditions">${conditionsValue}</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
@@ -406,14 +433,31 @@ export function renderUserStats() {
|
|||||||
// Add event listeners for editable stat values
|
// Add event listeners for editable stat values
|
||||||
$('.rpg-editable-stat').on('blur', function() {
|
$('.rpg-editable-stat').on('blur', function() {
|
||||||
const field = $(this).data('field');
|
const field = $(this).data('field');
|
||||||
const textValue = $(this).text().replace('%', '').trim();
|
const mode = $(this).data('mode');
|
||||||
let value = parseInt(textValue);
|
const maxValue = parseInt($(this).data('max')) || 100;
|
||||||
|
const textValue = $(this).text().trim();
|
||||||
|
let value;
|
||||||
|
|
||||||
// Validate and clamp value between 0 and 100
|
if (mode === 'number') {
|
||||||
if (isNaN(value)) {
|
// In number mode, parse "X/MAX" or just "X"
|
||||||
value = 0;
|
const parts = textValue.split('/');
|
||||||
|
value = parseInt(parts[0]);
|
||||||
|
|
||||||
|
// Validate and clamp value between 0 and maxValue
|
||||||
|
if (isNaN(value)) {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
value = Math.max(0, Math.min(maxValue, value));
|
||||||
|
} else {
|
||||||
|
// In percentage mode, parse "X%" or just "X"
|
||||||
|
value = parseInt(textValue.replace('%', ''));
|
||||||
|
|
||||||
|
// Validate and clamp value between 0 and 100
|
||||||
|
if (isNaN(value)) {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
value = Math.max(0, Math.min(100, value));
|
||||||
}
|
}
|
||||||
value = Math.max(0, Math.min(100, value));
|
|
||||||
|
|
||||||
// Update the setting
|
// Update the setting
|
||||||
extensionSettings.userStats[field] = value;
|
extensionSettings.userStats[field] = value;
|
||||||
@@ -445,7 +489,8 @@ export function renderUserStats() {
|
|||||||
|
|
||||||
$('.rpg-mood-conditions.rpg-editable').on('blur', function() {
|
$('.rpg-mood-conditions.rpg-editable').on('blur', function() {
|
||||||
const value = $(this).text().trim();
|
const value = $(this).text().trim();
|
||||||
extensionSettings.userStats.conditions = value || 'None';
|
const fieldKey = $(this).data('field');
|
||||||
|
extensionSettings.userStats[fieldKey] = value || 'None';
|
||||||
|
|
||||||
// Update userStats data (maintains JSON or text format)
|
// Update userStats data (maintains JSON or text format)
|
||||||
updateUserStatsData();
|
updateUserStatsData();
|
||||||
|
|||||||
+269
-2
@@ -1,10 +1,277 @@
|
|||||||
/**
|
/**
|
||||||
* Desktop UI Module
|
* Desktop UI Module
|
||||||
* Handles desktop-specific UI functionality: tab navigation
|
* Handles desktop-specific UI functionality: tab navigation and strip widgets
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { extensionSettings } 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
|
||||||
|
*/
|
||||||
|
function parseTimeForClock(timeStr) {
|
||||||
|
const timeMatch = timeStr.match(/(\d+):(\d+)/);
|
||||||
|
if (timeMatch) {
|
||||||
|
const hours = parseInt(timeMatch[1]);
|
||||||
|
const minutes = parseInt(timeMatch[2]);
|
||||||
|
const hourAngle = (hours % 12) * 30 + minutes * 0.5; // 30° per hour + 0.5° per minute
|
||||||
|
const minuteAngle = minutes * 6; // 6° per minute
|
||||||
|
return { hourAngle, minuteAngle };
|
||||||
|
}
|
||||||
|
return { hourAngle: 0, minuteAngle: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the desktop strip widgets display based on current tracker data and settings.
|
||||||
|
* Strip widgets are shown vertically in the collapsed panel strip.
|
||||||
|
*/
|
||||||
|
export function updateStripWidgets() {
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
const $container = $('#rpg-strip-widget-container');
|
||||||
|
|
||||||
|
if ($panel.length === 0 || $container.length === 0) return;
|
||||||
|
|
||||||
|
// Check if strip widgets are enabled
|
||||||
|
const widgetSettings = extensionSettings.desktopStripWidgets;
|
||||||
|
if (!widgetSettings || !widgetSettings.enabled) {
|
||||||
|
$panel.removeClass('rpg-strip-widgets-enabled');
|
||||||
|
$container.find('.rpg-strip-widget').removeClass('rpg-strip-widget-visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add enabled class to panel for CSS styling (wider collapsed width)
|
||||||
|
$panel.addClass('rpg-strip-widgets-enabled');
|
||||||
|
|
||||||
|
// Get tracker data - use imported state directly
|
||||||
|
const infoBox = lastGeneratedData?.infoBox || committedTrackerData?.infoBox;
|
||||||
|
|
||||||
|
// Parse infoBox if it's a string
|
||||||
|
let infoData = null;
|
||||||
|
if (infoBox) {
|
||||||
|
try {
|
||||||
|
infoData = typeof infoBox === 'string' ? JSON.parse(infoBox) : infoBox;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Strip Widgets] Failed to parse infoBox:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather Icon Widget (with description)
|
||||||
|
const $weatherWidget = $container.find('.rpg-strip-widget-weather');
|
||||||
|
if (widgetSettings.weatherIcon?.enabled && infoData?.weather?.emoji) {
|
||||||
|
$weatherWidget.find('.rpg-strip-widget-icon').text(infoData.weather.emoji);
|
||||||
|
// Show weather description truncated
|
||||||
|
const forecast = infoData.weather.forecast || '';
|
||||||
|
const displayForecast = forecast.length > 12 ? forecast.substring(0, 10) + '…' : forecast;
|
||||||
|
$weatherWidget.find('.rpg-strip-widget-desc').text(displayForecast);
|
||||||
|
$weatherWidget.attr('title', forecast || 'Weather');
|
||||||
|
$weatherWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$weatherWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clock Widget with animated face
|
||||||
|
const $clockWidget = $container.find('.rpg-strip-widget-clock');
|
||||||
|
if (widgetSettings.clock?.enabled && infoData?.time) {
|
||||||
|
const timeStr = infoData.time.end || infoData.time.value || infoData.time.start || '';
|
||||||
|
if (timeStr) {
|
||||||
|
// Update clock hands
|
||||||
|
const { hourAngle, minuteAngle } = parseTimeForClock(timeStr);
|
||||||
|
$clockWidget.find('.rpg-strip-clock-hour').css('transform', `rotate(${hourAngle}deg)`);
|
||||||
|
$clockWidget.find('.rpg-strip-clock-minute').css('transform', `rotate(${minuteAngle}deg)`);
|
||||||
|
$clockWidget.find('.rpg-strip-widget-value').text(timeStr);
|
||||||
|
$clockWidget.attr('title', `Time: ${timeStr}`);
|
||||||
|
$clockWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$clockWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$clockWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date Widget
|
||||||
|
const $dateWidget = $container.find('.rpg-strip-widget-date');
|
||||||
|
if (widgetSettings.date?.enabled && infoData?.date?.value) {
|
||||||
|
const dateVal = infoData.date.value;
|
||||||
|
// Truncate long dates for display
|
||||||
|
const displayDate = dateVal.length > 20 ? dateVal.substring(0, 18) + '…' : dateVal;
|
||||||
|
$dateWidget.find('.rpg-strip-widget-value').text(displayDate);
|
||||||
|
$dateWidget.attr('title', dateVal);
|
||||||
|
$dateWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$dateWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location Widget
|
||||||
|
const $locationWidget = $container.find('.rpg-strip-widget-location');
|
||||||
|
if (widgetSettings.location?.enabled && infoData?.location?.value) {
|
||||||
|
const loc = infoData.location.value;
|
||||||
|
// Truncate long locations for display
|
||||||
|
const displayLoc = loc.length > 15 ? loc.substring(0, 13) + '…' : loc;
|
||||||
|
$locationWidget.find('.rpg-strip-widget-value').text(displayLoc);
|
||||||
|
$locationWidget.attr('title', loc);
|
||||||
|
$locationWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$locationWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats Widget - get from lastGeneratedData or committedTrackerData first, fallback to extensionSettings
|
||||||
|
const $statsWidget = $container.find('.rpg-strip-widget-stats');
|
||||||
|
if (widgetSettings.stats?.enabled) {
|
||||||
|
let allStats = [];
|
||||||
|
|
||||||
|
// Try to get stats from tracker data first (most current)
|
||||||
|
const userStatsData = lastGeneratedData?.userStats || committedTrackerData?.userStats;
|
||||||
|
if (userStatsData) {
|
||||||
|
try {
|
||||||
|
const parsedStats = typeof userStatsData === 'string' ? JSON.parse(userStatsData) : userStatsData;
|
||||||
|
if (parsedStats?.stats) {
|
||||||
|
allStats = parsedStats.stats;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Strip Widgets] Failed to parse tracker userStats:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to extensionSettings.userStats
|
||||||
|
if (allStats.length === 0 && extensionSettings.userStats) {
|
||||||
|
try {
|
||||||
|
const userStatsJson = extensionSettings.userStats;
|
||||||
|
const parsedUserStats = typeof userStatsJson === 'string' ? JSON.parse(userStatsJson) : userStatsJson;
|
||||||
|
if (parsedUserStats?.stats) {
|
||||||
|
allStats = parsedUserStats.stats;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Strip Widgets] Failed to parse extensionSettings.userStats:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allStats.length > 0) {
|
||||||
|
// Get enabled stats from trackerConfig
|
||||||
|
const configuredStats = extensionSettings.trackerConfig?.userStats?.customStats || [];
|
||||||
|
const enabledStatMap = new Map();
|
||||||
|
configuredStats.forEach(s => {
|
||||||
|
if (s.enabled !== false) {
|
||||||
|
enabledStatMap.set(s.id?.toLowerCase(), true);
|
||||||
|
enabledStatMap.set(s.name?.toLowerCase(), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const $statsList = $statsWidget.find('.rpg-strip-stats-list');
|
||||||
|
$statsList.empty();
|
||||||
|
|
||||||
|
allStats.forEach(stat => {
|
||||||
|
// Filter by config if available - but if no config, show all
|
||||||
|
if (configuredStats.length > 0) {
|
||||||
|
const statId = stat.id?.toLowerCase();
|
||||||
|
const statName = stat.name?.toLowerCase();
|
||||||
|
if (!enabledStatMap.has(statId) && !enabledStatMap.has(statName)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = typeof stat.value === 'number' ? stat.value : parseInt(stat.value) || 0;
|
||||||
|
const color = getStatColor(value);
|
||||||
|
const abbr = stat.name.substring(0, 3).toUpperCase();
|
||||||
|
|
||||||
|
const $item = $(`<div class="rpg-strip-stat-item" title="${stat.name}: ${value}">
|
||||||
|
<span class="rpg-strip-stat-name">${abbr}</span>
|
||||||
|
<span class="rpg-strip-stat-value" style="color: ${color};">${value}</span>
|
||||||
|
</div>`);
|
||||||
|
$statsList.append($item);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($statsList.children().length > 0) {
|
||||||
|
$statsWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$statsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$statsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$statsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes Widget
|
||||||
|
const $attrsWidget = $container.find('.rpg-strip-widget-attributes');
|
||||||
|
if (widgetSettings.attributes?.enabled) {
|
||||||
|
const showRPGAttributes = extensionSettings.trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||||
|
|
||||||
|
if (showRPGAttributes && extensionSettings.classicStats) {
|
||||||
|
// Get enabled attributes from trackerConfig
|
||||||
|
const configuredAttrs = extensionSettings.trackerConfig?.userStats?.rpgAttributes || [];
|
||||||
|
const enabledAttrIds = configuredAttrs.filter(a => a.enabled !== false).map(a => a.id);
|
||||||
|
|
||||||
|
const attrs = extensionSettings.classicStats;
|
||||||
|
const $attrsGrid = $attrsWidget.find('.rpg-strip-attributes-grid');
|
||||||
|
$attrsGrid.empty();
|
||||||
|
|
||||||
|
Object.entries(attrs).forEach(([key, value]) => {
|
||||||
|
// Filter by config if available
|
||||||
|
if (enabledAttrIds.length > 0 && !enabledAttrIds.includes(key.toLowerCase())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $item = $(`<div class="rpg-strip-attr-item" title="${key.toUpperCase()}: ${value}">
|
||||||
|
<span class="rpg-strip-attr-name">${key.toUpperCase()}</span>
|
||||||
|
<span class="rpg-strip-attr-value">${value}</span>
|
||||||
|
</div>`);
|
||||||
|
$attrsGrid.append($item);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($attrsGrid.children().length > 0) {
|
||||||
|
$attrsWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$attrsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$attrsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$attrsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a color interpolated between low and high based on stat value (0-100).
|
||||||
|
* @param {number} value - The stat value (0-100)
|
||||||
|
* @returns {string} CSS color value
|
||||||
|
*/
|
||||||
|
function getStatColor(value) {
|
||||||
|
const lowColor = extensionSettings.statBarColorLow || '#cc3333';
|
||||||
|
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
|
||||||
|
const highColor = extensionSettings.statBarColorHigh || '#33cc66';
|
||||||
|
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
|
||||||
|
|
||||||
|
// Simple linear interpolation between low and high colors
|
||||||
|
const percent = Math.min(100, Math.max(0, value)) / 100;
|
||||||
|
|
||||||
|
// Parse colors
|
||||||
|
const lowRGB = hexToRgb(lowColor);
|
||||||
|
const highRGB = hexToRgb(highColor);
|
||||||
|
|
||||||
|
if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity);
|
||||||
|
|
||||||
|
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
|
||||||
|
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * 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})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a hex color to RGB object.
|
||||||
|
* @param {string} hex - Hex color string (e.g., "#cc3333")
|
||||||
|
* @returns {{r: number, g: number, b: number}|null}
|
||||||
|
*/
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
return result ? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16)
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up desktop tab navigation for organizing content.
|
* Sets up desktop tab navigation for organizing content.
|
||||||
|
|||||||
+117
-41
@@ -397,7 +397,7 @@ export class EncounterModal {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Player Controls -->
|
<!-- Player Controls -->
|
||||||
${this.renderPlayerControls(combatData.party)}
|
${this.renderPlayerControls(combatData.party, currentEncounter.playerActions)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -599,7 +599,7 @@ export class EncounterModal {
|
|||||||
if (member.isPlayer && user_avatar) {
|
if (member.isPlayer && user_avatar) {
|
||||||
avatarIcon = `<img src="${getSafeThumbnailUrl('persona', user_avatar)}" alt="${member.name}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;">`;
|
avatarIcon = `<img src="${getSafeThumbnailUrl('persona', user_avatar)}" alt="${member.name}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;">`;
|
||||||
} else {
|
} else {
|
||||||
const avatarUrl = this.getPartyMemberAvatar(member.name);
|
const avatarUrl = this.getCharacterAvatar(member.name);
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
avatarIcon = `<img src="${avatarUrl}" alt="${member.name}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;">`;
|
avatarIcon = `<img src="${avatarUrl}" alt="${member.name}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;">`;
|
||||||
}
|
}
|
||||||
@@ -657,12 +657,16 @@ export class EncounterModal {
|
|||||||
* @param {Array} party - Party data
|
* @param {Array} party - Party data
|
||||||
* @returns {string} HTML for controls
|
* @returns {string} HTML for controls
|
||||||
*/
|
*/
|
||||||
renderPlayerControls(party) {
|
renderPlayerControls(party, playerActions = null) {
|
||||||
const player = party.find(m => m.isPlayer);
|
const player = party.find(m => m.isPlayer);
|
||||||
if (!player || player.hp <= 0) {
|
if (!player || player.hp <= 0) {
|
||||||
return '<div class="rpg-encounter-controls"><p class="rpg-encounter-defeated">You have been defeated...</p></div>';
|
return '<div class="rpg-encounter-controls"><p class="rpg-encounter-defeated">You have been defeated...</p></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use playerActions if provided, otherwise fall back to player data
|
||||||
|
const attacks = playerActions?.attacks || player.attacks || [];
|
||||||
|
const items = playerActions?.items || player.items || [];
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="rpg-encounter-controls">
|
<div class="rpg-encounter-controls">
|
||||||
<h3><i class="fa-solid fa-hand-fist"></i> Your Actions</h3>
|
<h3><i class="fa-solid fa-hand-fist"></i> Your Actions</h3>
|
||||||
@@ -670,7 +674,7 @@ export class EncounterModal {
|
|||||||
<div class="rpg-encounter-action-buttons">
|
<div class="rpg-encounter-action-buttons">
|
||||||
<div class="rpg-encounter-button-group">
|
<div class="rpg-encounter-button-group">
|
||||||
<h4>Attacks</h4>
|
<h4>Attacks</h4>
|
||||||
${player.attacks.map(attack => {
|
${attacks.map(attack => {
|
||||||
// Support both old string format and new object format
|
// Support both old string format and new object format
|
||||||
const attackName = typeof attack === 'string' ? attack : attack.name;
|
const attackName = typeof attack === 'string' ? attack : attack.name;
|
||||||
const attackType = typeof attack === 'string' ? 'single-target' : (attack.type || 'single-target');
|
const attackType = typeof attack === 'string' ? 'single-target' : (attack.type || 'single-target');
|
||||||
@@ -688,10 +692,10 @@ export class EncounterModal {
|
|||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${player.items && player.items.length > 0 ? `
|
${items && items.length > 0 ? `
|
||||||
<div class="rpg-encounter-button-group">
|
<div class="rpg-encounter-button-group">
|
||||||
<h4>Items</h4>
|
<h4>Items</h4>
|
||||||
${player.items.map(item => `
|
${items.map(item => `
|
||||||
<button class="rpg-encounter-action-btn rpg-encounter-item-btn" data-action="item" data-value="${item}">
|
<button class="rpg-encounter-action-btn rpg-encounter-item-btn" data-action="item" data-value="${item}">
|
||||||
<i class="fa-solid fa-flask"></i> ${item}
|
<i class="fa-solid fa-flask"></i> ${item}
|
||||||
</button>
|
</button>
|
||||||
@@ -718,21 +722,27 @@ export class EncounterModal {
|
|||||||
* @param {Array} party - Party data for reference
|
* @param {Array} party - Party data for reference
|
||||||
*/
|
*/
|
||||||
attachControlListeners(party) {
|
attachControlListeners(party) {
|
||||||
// Attack and item buttons
|
// Only attach once - event delegation on the modal means listeners persist
|
||||||
this.modal.querySelectorAll('.rpg-encounter-action-btn').forEach(btn => {
|
if (this._listenersAttached) {
|
||||||
btn.addEventListener('click', async (e) => {
|
return;
|
||||||
const actionType = e.currentTarget.dataset.action;
|
}
|
||||||
const value = e.currentTarget.dataset.value;
|
|
||||||
const attackType = e.currentTarget.dataset.attackType;
|
// Store handlers as instance properties so we can remove them if needed
|
||||||
|
this._actionHandler = async (e) => {
|
||||||
|
// Handle action buttons (attack/item)
|
||||||
|
const actionBtn = e.target.closest('.rpg-encounter-action-btn');
|
||||||
|
if (actionBtn && !actionBtn.disabled && !this.isProcessing) {
|
||||||
|
const actionType = actionBtn.dataset.action;
|
||||||
|
const value = actionBtn.dataset.value;
|
||||||
|
const attackType = actionBtn.dataset.attackType;
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const userName = context.name1;
|
const userName = context.name1;
|
||||||
|
|
||||||
let actionText = '';
|
let actionText = '';
|
||||||
|
|
||||||
if (actionType === 'attack') {
|
if (actionType === 'attack') {
|
||||||
// Show target selection for attacks
|
|
||||||
const target = await this.showTargetSelection(attackType, currentEncounter.combatStats);
|
const target = await this.showTargetSelection(attackType, currentEncounter.combatStats);
|
||||||
if (!target) return; // User cancelled
|
if (!target) return;
|
||||||
|
|
||||||
if (target === 'all-enemies') {
|
if (target === 'all-enemies') {
|
||||||
actionText = `${userName} uses ${value} targeting all enemies!`;
|
actionText = `${userName} uses ${value} targeting all enemies!`;
|
||||||
@@ -740,40 +750,46 @@ export class EncounterModal {
|
|||||||
actionText = `${userName} uses ${value} on ${target}!`;
|
actionText = `${userName} uses ${value} on ${target}!`;
|
||||||
}
|
}
|
||||||
} else if (actionType === 'item') {
|
} else if (actionType === 'item') {
|
||||||
// Show target selection for items (default to single-target)
|
|
||||||
const target = await this.showTargetSelection('single-target', currentEncounter.combatStats);
|
const target = await this.showTargetSelection('single-target', currentEncounter.combatStats);
|
||||||
if (!target) return; // User cancelled
|
if (!target) return;
|
||||||
|
|
||||||
actionText = `${userName} uses ${value} on ${target}!`;
|
actionText = `${userName} uses ${value} on ${target}!`;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.processCombatAction(actionText);
|
await this.processCombatAction(actionText);
|
||||||
});
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
// Custom action submit
|
// Handle custom submit button
|
||||||
const customInput = this.modal.querySelector('#rpg-encounter-custom-input');
|
const submitBtn = e.target.closest('#rpg-encounter-custom-submit');
|
||||||
const customSubmit = this.modal.querySelector('#rpg-encounter-custom-submit');
|
if (submitBtn && !submitBtn.disabled && !this.isProcessing) {
|
||||||
|
const input = this.modal.querySelector('#rpg-encounter-custom-input');
|
||||||
const submitCustomAction = async () => {
|
if (input) {
|
||||||
const action = customInput.value.trim();
|
const action = input.value.trim();
|
||||||
if (!action) return;
|
if (action) {
|
||||||
|
await this.processCombatAction(action);
|
||||||
await this.processCombatAction(action);
|
input.value = '';
|
||||||
customInput.value = '';
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (customSubmit) {
|
this._keypressHandler = async (e) => {
|
||||||
customSubmit.addEventListener('click', submitCustomAction);
|
const input = e.target.closest('#rpg-encounter-custom-input');
|
||||||
}
|
if (input && e.key === 'Enter' && !this.isProcessing) {
|
||||||
|
const action = input.value.trim();
|
||||||
if (customInput) {
|
if (action) {
|
||||||
customInput.addEventListener('keypress', (e) => {
|
await this.processCombatAction(action);
|
||||||
if (e.key === 'Enter') {
|
input.value = '';
|
||||||
submitCustomAction();
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Attach to the modal itself (which never gets replaced)
|
||||||
|
this.modal.addEventListener('click', this._actionHandler);
|
||||||
|
this.modal.addEventListener('keypress', this._keypressHandler);
|
||||||
|
|
||||||
|
this._listenersAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -820,7 +836,8 @@ export class EncounterModal {
|
|||||||
|
|
||||||
// Update encounter state
|
// Update encounter state
|
||||||
updateCurrentEncounter({
|
updateCurrentEncounter({
|
||||||
combatStats: result.combatStats
|
combatStats: result.combatStats,
|
||||||
|
playerActions: result.playerActions
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collect log entries in order: enemy actions, party actions, then narration
|
// Collect log entries in order: enemy actions, party actions, then narration
|
||||||
@@ -935,16 +952,75 @@ export class EncounterModal {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-render controls if player died
|
// Re-render controls if player died OR if player's actions changed
|
||||||
const player = combatStats.party.find(m => m.isPlayer);
|
const player = combatStats.party.find(m => m.isPlayer);
|
||||||
|
const controlsContainer = this.modal.querySelector('.rpg-encounter-controls');
|
||||||
|
|
||||||
if (player && player.hp <= 0) {
|
if (player && player.hp <= 0) {
|
||||||
const controlsContainer = this.modal.querySelector('.rpg-encounter-controls');
|
|
||||||
if (controlsContainer) {
|
if (controlsContainer) {
|
||||||
controlsContainer.innerHTML = '<p class="rpg-encounter-defeated">You have been defeated...</p>';
|
controlsContainer.innerHTML = '<p class="rpg-encounter-defeated">You have been defeated...</p>';
|
||||||
}
|
}
|
||||||
|
} else if (currentEncounter.playerActions && controlsContainer) {
|
||||||
|
// Check if actions have changed by comparing with previous state
|
||||||
|
const actionsChanged = this.haveActionsChanged(currentEncounter.playerActions);
|
||||||
|
|
||||||
|
if (actionsChanged) {
|
||||||
|
// Store the new actions for next comparison
|
||||||
|
this._previousPlayerActions = {
|
||||||
|
attacks: currentEncounter.playerActions.attacks ? JSON.parse(JSON.stringify(currentEncounter.playerActions.attacks)) : [],
|
||||||
|
items: currentEncounter.playerActions.items ? [...currentEncounter.playerActions.items] : []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-render the entire controls section with new actions
|
||||||
|
const newControlsHTML = this.renderPlayerControls(combatStats.party, currentEncounter.playerActions);
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = newControlsHTML;
|
||||||
|
const newControls = tempDiv.firstElementChild;
|
||||||
|
|
||||||
|
if (newControls) {
|
||||||
|
controlsContainer.replaceWith(newControls);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if player's available actions have changed
|
||||||
|
* @param {Object} playerActions - Current player actions data with attacks and items
|
||||||
|
* @returns {boolean} True if actions changed
|
||||||
|
*/
|
||||||
|
haveActionsChanged(playerActions) {
|
||||||
|
if (!this._previousPlayerActions) {
|
||||||
|
// First time - store initial actions
|
||||||
|
this._previousPlayerActions = {
|
||||||
|
attacks: playerActions.attacks ? JSON.parse(JSON.stringify(playerActions.attacks)) : [],
|
||||||
|
items: playerActions.items ? [...playerActions.items] : []
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentAttacks = playerActions.attacks || [];
|
||||||
|
const currentItems = playerActions.items || [];
|
||||||
|
const prevAttacks = this._previousPlayerActions.attacks || [];
|
||||||
|
const prevItems = this._previousPlayerActions.items || [];
|
||||||
|
|
||||||
|
// Check if attacks changed
|
||||||
|
if (currentAttacks.length !== prevAttacks.length) return true;
|
||||||
|
for (let i = 0; i < currentAttacks.length; i++) {
|
||||||
|
const curr = typeof currentAttacks[i] === 'string' ? currentAttacks[i] : currentAttacks[i].name;
|
||||||
|
const prev = typeof prevAttacks[i] === 'string' ? prevAttacks[i] : prevAttacks[i].name;
|
||||||
|
if (curr !== prev) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if items changed
|
||||||
|
if (currentItems.length !== prevItems.length) return true;
|
||||||
|
for (let i = 0; i < currentItems.length; i++) {
|
||||||
|
if (currentItems[i] !== prevItems[i]) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds multiple log entries sequentially with animation
|
* Adds multiple log entries sequentially with animation
|
||||||
* @param {Array} entries - Array of {message, type} objects
|
* @param {Array} entries - Array of {message, type} objects
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { setupMobileTabs, removeMobileTabs } from './mobile.js';
|
import { setupMobileTabs, removeMobileTabs } from './mobile.js';
|
||||||
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
|
import { setupDesktopTabs, removeDesktopTabs, updateStripWidgets } from './desktop.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the visibility of plot buttons based on settings.
|
* Toggles the visibility of plot buttons based on settings.
|
||||||
@@ -243,6 +243,9 @@ export function setupCollapseToggle() {
|
|||||||
} else if ($panel.hasClass('rpg-position-left')) {
|
} else if ($panel.hasClass('rpg-position-left')) {
|
||||||
$icon.removeClass('fa-chevron-left').addClass('fa-chevron-right');
|
$icon.removeClass('fa-chevron-left').addClass('fa-chevron-right');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update strip widgets when collapsing (they show in collapsed state)
|
||||||
|
updateStripWidgets();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -431,6 +434,7 @@ export function updateGenerationModeUI() {
|
|||||||
if (extensionSettings.generationMode === 'together') {
|
if (extensionSettings.generationMode === 'together') {
|
||||||
// In "together" mode, manual update button is hidden
|
// In "together" mode, manual update button is hidden
|
||||||
$('#rpg-manual-update').hide();
|
$('#rpg-manual-update').hide();
|
||||||
|
$('#rpg-strip-refresh').hide();
|
||||||
$('#rpg-external-api-settings').slideUp(200);
|
$('#rpg-external-api-settings').slideUp(200);
|
||||||
$('#rpg-separate-mode-settings').slideUp(200);
|
$('#rpg-separate-mode-settings').slideUp(200);
|
||||||
// Hide auto-update toggle (not applicable in together mode)
|
// Hide auto-update toggle (not applicable in together mode)
|
||||||
@@ -438,6 +442,7 @@ export function updateGenerationModeUI() {
|
|||||||
} else if (extensionSettings.generationMode === 'separate') {
|
} else if (extensionSettings.generationMode === 'separate') {
|
||||||
// In "separate" mode, manual update button is visible
|
// In "separate" mode, manual update button is visible
|
||||||
$('#rpg-manual-update').show();
|
$('#rpg-manual-update').show();
|
||||||
|
$('#rpg-strip-refresh').show();
|
||||||
$('#rpg-external-api-settings').slideUp(200);
|
$('#rpg-external-api-settings').slideUp(200);
|
||||||
$('#rpg-separate-mode-settings').slideDown(200);
|
$('#rpg-separate-mode-settings').slideDown(200);
|
||||||
// Show auto-update toggle
|
// Show auto-update toggle
|
||||||
@@ -445,6 +450,7 @@ export function updateGenerationModeUI() {
|
|||||||
} else if (extensionSettings.generationMode === 'external') {
|
} else if (extensionSettings.generationMode === 'external') {
|
||||||
// In "external" mode, manual update button is visible AND both settings are shown
|
// In "external" mode, manual update button is visible AND both settings are shown
|
||||||
$('#rpg-manual-update').show();
|
$('#rpg-manual-update').show();
|
||||||
|
$('#rpg-strip-refresh').show();
|
||||||
$('#rpg-external-api-settings').slideDown(200);
|
$('#rpg-external-api-settings').slideDown(200);
|
||||||
$('#rpg-separate-mode-settings').slideDown(200);
|
$('#rpg-separate-mode-settings').slideDown(200);
|
||||||
// Show auto-update toggle for external mode too
|
// Show auto-update toggle for external mode too
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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.
|
||||||
@@ -1451,7 +1452,7 @@ export function updateFabWidgets() {
|
|||||||
if (widgetSettings.attributes?.enabled) {
|
if (widgetSettings.attributes?.enabled) {
|
||||||
// Check if RPG attributes are enabled in trackerConfig
|
// Check if RPG attributes are enabled in trackerConfig
|
||||||
const showRPGAttributes = extensionSettings.trackerConfig?.userStats?.showRPGAttributes !== false;
|
const showRPGAttributes = extensionSettings.trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||||
|
|
||||||
if (showRPGAttributes && extensionSettings.classicStats) {
|
if (showRPGAttributes && extensionSettings.classicStats) {
|
||||||
// Get enabled attributes from trackerConfig
|
// Get enabled attributes from trackerConfig
|
||||||
const configuredAttrs = extensionSettings.trackerConfig?.userStats?.rpgAttributes || [];
|
const configuredAttrs = extensionSettings.trackerConfig?.userStats?.rpgAttributes || [];
|
||||||
@@ -1541,10 +1542,10 @@ export function updateFabWidgets() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const $this = $(this);
|
const $this = $(this);
|
||||||
const wasExpanded = $this.hasClass('expanded');
|
const wasExpanded = $this.hasClass('expanded');
|
||||||
|
|
||||||
// Collapse all other expanded widgets
|
// Collapse all other expanded widgets
|
||||||
$container.find('.rpg-fab-widget.expanded').removeClass('expanded');
|
$container.find('.rpg-fab-widget.expanded').removeClass('expanded');
|
||||||
|
|
||||||
// Toggle this one
|
// Toggle this one
|
||||||
if (!wasExpanded) {
|
if (!wasExpanded) {
|
||||||
$this.addClass('expanded');
|
$this.addClass('expanded');
|
||||||
@@ -1567,7 +1568,9 @@ 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;
|
||||||
@@ -1576,13 +1579,14 @@ 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 ? highColor : lowColor;
|
if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity);
|
||||||
|
|
||||||
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 `rgb(${r}, ${g}, ${b})`;
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT } from '../generation/promptBuilder.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';
|
||||||
|
|
||||||
let $editorModal = null;
|
let $editorModal = null;
|
||||||
let tempPrompts = null; // Temporary prompts for cancel functionality
|
let tempPrompts = null; // Temporary prompts for cancel functionality
|
||||||
@@ -14,9 +14,11 @@ 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.
|
||||||
@@ -96,9 +98,11 @@ 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 || '',
|
||||||
@@ -111,9 +115,11 @@ 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);
|
||||||
@@ -150,9 +156,11 @@ 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();
|
||||||
@@ -182,6 +190,9 @@ 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;
|
||||||
@@ -191,6 +202,9 @@ 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;
|
||||||
@@ -221,9 +235,11 @@ 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);
|
||||||
@@ -235,9 +251,11 @@ 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 = '';
|
||||||
|
|||||||
+55
-12
@@ -5,6 +5,37 @@
|
|||||||
|
|
||||||
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.
|
||||||
*/
|
*/
|
||||||
@@ -75,24 +106,33 @@ 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': colors.bg,
|
'--rpg-bg': bgColor,
|
||||||
'--rpg-accent': colors.accent,
|
'--rpg-accent': accentColor,
|
||||||
'--rpg-text': colors.text,
|
'--rpg-text': textColor,
|
||||||
'--rpg-highlight': colors.highlight,
|
'--rpg-highlight': highlightColor,
|
||||||
'--rpg-border': colors.highlight,
|
'--rpg-border': highlightColor,
|
||||||
'--rpg-shadow': `${colors.highlight}80` // Add alpha for shadow
|
'--rpg-shadow': shadowColor
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply custom colors to mobile toggle and thought elements
|
// Apply custom colors to mobile toggle and thought elements
|
||||||
const customStyles = {
|
const customStyles = {
|
||||||
'--rpg-bg': colors.bg,
|
'--rpg-bg': bgColor,
|
||||||
'--rpg-accent': colors.accent,
|
'--rpg-accent': accentColor,
|
||||||
'--rpg-text': colors.text,
|
'--rpg-text': textColor,
|
||||||
'--rpg-highlight': colors.highlight,
|
'--rpg-highlight': highlightColor,
|
||||||
'--rpg-border': colors.highlight,
|
'--rpg-border': highlightColor,
|
||||||
'--rpg-shadow': `${colors.highlight}80`
|
'--rpg-shadow': shadowColor
|
||||||
};
|
};
|
||||||
|
|
||||||
const $mobileToggle = $('#rpg-mobile-toggle');
|
const $mobileToggle = $('#rpg-mobile-toggle');
|
||||||
@@ -139,6 +179,7 @@ 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');
|
||||||
|
|
||||||
@@ -150,6 +191,7 @@ 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);
|
||||||
|
|
||||||
@@ -161,6 +203,7 @@ 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 ||
|
||||||
|
|||||||
+122
-13
@@ -21,6 +21,8 @@ import {
|
|||||||
removePresetAssociationForCurrentEntity,
|
removePresetAssociationForCurrentEntity,
|
||||||
getPresetForCurrentEntity,
|
getPresetForCurrentEntity,
|
||||||
hasPresetAssociation,
|
hasPresetAssociation,
|
||||||
|
isAssociatedWithCurrentPreset,
|
||||||
|
getCurrentEntityKey,
|
||||||
getCurrentEntityName,
|
getCurrentEntityName,
|
||||||
exportPresets,
|
exportPresets,
|
||||||
importPresets
|
importPresets
|
||||||
@@ -33,6 +35,8 @@ import { updateFabWidgets } from './mobile.js';
|
|||||||
let $editorModal = null;
|
let $editorModal = null;
|
||||||
let activeTab = 'userStats';
|
let activeTab = 'userStats';
|
||||||
let tempConfig = null; // Temporary config for cancel functionality
|
let tempConfig = null; // Temporary config for cancel functionality
|
||||||
|
let tempAssociation = null; // Temporary association state: { presetId: string|null, entityKey: string|null }
|
||||||
|
let originalAssociation = null; // Original association when editor opened
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the tracker editor modal
|
* Initialize the tracker editor modal
|
||||||
@@ -104,6 +108,12 @@ export function initTrackerEditor() {
|
|||||||
$(document).on('change', '#rpg-preset-select', function() {
|
$(document).on('change', '#rpg-preset-select', function() {
|
||||||
const presetId = $(this).val();
|
const presetId = $(this).val();
|
||||||
if (presetId && presetId !== getActivePresetId()) {
|
if (presetId && presetId !== getActivePresetId()) {
|
||||||
|
// Check if the current character had an association (either original or pending)
|
||||||
|
const entityKey = getCurrentEntityKey();
|
||||||
|
const wasAssociated = tempAssociation
|
||||||
|
? tempAssociation.presetId !== null
|
||||||
|
: hasPresetAssociation();
|
||||||
|
|
||||||
// Save current changes to the old preset before switching
|
// Save current changes to the old preset before switching
|
||||||
const currentPresetId = getActivePresetId();
|
const currentPresetId = getActivePresetId();
|
||||||
if (currentPresetId) {
|
if (currentPresetId) {
|
||||||
@@ -113,8 +123,17 @@ export function initTrackerEditor() {
|
|||||||
if (loadPreset(presetId)) {
|
if (loadPreset(presetId)) {
|
||||||
tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig));
|
tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig));
|
||||||
renderEditorUI();
|
renderEditorUI();
|
||||||
|
|
||||||
|
// If the character was associated with a preset, update temp association to new preset
|
||||||
|
if (wasAssociated && entityKey) {
|
||||||
|
tempAssociation = { presetId: presetId, entityKey: entityKey };
|
||||||
|
const preset = getPreset(presetId);
|
||||||
|
toastr.info(`"${preset?.name || 'Unknown'}" will be associated with ${getCurrentEntityName()} when saved.`);
|
||||||
|
} else {
|
||||||
|
toastr.success(`Switched to preset "${getPreset(presetId)?.name || 'Unknown'}".`);
|
||||||
|
}
|
||||||
|
|
||||||
updatePresetUI();
|
updatePresetUI();
|
||||||
toastr.success(`Switched to preset "${getPreset(presetId)?.name || 'Unknown'}".`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -162,12 +181,25 @@ export function initTrackerEditor() {
|
|||||||
|
|
||||||
// Associate preset checkbox
|
// Associate preset checkbox
|
||||||
$(document).on('change', '#rpg-preset-associate', function() {
|
$(document).on('change', '#rpg-preset-associate', function() {
|
||||||
|
const activePresetId = getActivePresetId();
|
||||||
|
const preset = getPreset(activePresetId);
|
||||||
|
const entityName = getCurrentEntityName();
|
||||||
|
const entityKey = getCurrentEntityKey();
|
||||||
|
|
||||||
if ($(this).is(':checked')) {
|
if ($(this).is(':checked')) {
|
||||||
associatePresetWithCurrentEntity();
|
// Store pending association (don't save yet)
|
||||||
toastr.info(`This preset will be used for ${getCurrentEntityName()}.`);
|
tempAssociation = { presetId: activePresetId, entityKey: entityKey };
|
||||||
|
toastr.info(`"${preset?.name || 'Unknown'}" will be associated with ${entityName} when saved.`);
|
||||||
} else {
|
} else {
|
||||||
removePresetAssociationForCurrentEntity();
|
// Store pending removal (don't save yet)
|
||||||
toastr.info(`Preset association removed for ${getCurrentEntityName()}.`);
|
tempAssociation = { presetId: null, entityKey: entityKey };
|
||||||
|
const defaultPresetId = getDefaultPresetId();
|
||||||
|
const defaultPreset = getPreset(defaultPresetId);
|
||||||
|
if (defaultPreset && defaultPresetId !== activePresetId) {
|
||||||
|
toastr.info(`Association will be removed when saved. Default preset "${defaultPreset.name}" will apply on next character switch.`);
|
||||||
|
} else {
|
||||||
|
toastr.info(`Association will be removed for ${entityName} when saved.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -203,7 +235,15 @@ function updatePresetUI() {
|
|||||||
$('#rpg-preset-entity-name').text(entityName);
|
$('#rpg-preset-entity-name').text(entityName);
|
||||||
|
|
||||||
// Update the association checkbox
|
// Update the association checkbox
|
||||||
const isAssociated = hasPresetAssociation();
|
// Use temp state if available, otherwise check actual association with CURRENT preset
|
||||||
|
let isAssociated;
|
||||||
|
if (tempAssociation !== null) {
|
||||||
|
// Use pending state: checked if pending preset matches active preset
|
||||||
|
isAssociated = tempAssociation.presetId === activePresetId;
|
||||||
|
} else {
|
||||||
|
// No pending changes, check actual state
|
||||||
|
isAssociated = isAssociatedWithCurrentPreset();
|
||||||
|
}
|
||||||
$('#rpg-preset-associate').prop('checked', isAssociated);
|
$('#rpg-preset-associate').prop('checked', isAssociated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +254,12 @@ function openTrackerEditor() {
|
|||||||
// Create temporary copy for cancel functionality
|
// Create temporary copy for cancel functionality
|
||||||
tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig));
|
tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig));
|
||||||
|
|
||||||
|
// Store original association state for cancel functionality
|
||||||
|
const entityKey = getCurrentEntityKey();
|
||||||
|
const currentAssociatedPreset = getPresetForCurrentEntity();
|
||||||
|
originalAssociation = { presetId: currentAssociatedPreset, entityKey: entityKey };
|
||||||
|
tempAssociation = null; // Reset pending changes
|
||||||
|
|
||||||
// Set theme to match current extension theme
|
// Set theme to match current extension theme
|
||||||
const theme = extensionSettings.theme || 'modern';
|
const theme = extensionSettings.theme || 'modern';
|
||||||
$editorModal.attr('data-theme', theme);
|
$editorModal.attr('data-theme', theme);
|
||||||
@@ -235,6 +281,10 @@ function closeTrackerEditor() {
|
|||||||
tempConfig = null;
|
tempConfig = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Discard pending association changes (cancel = no save)
|
||||||
|
tempAssociation = null;
|
||||||
|
originalAssociation = null;
|
||||||
|
|
||||||
$editorModal.removeClass('is-open').addClass('is-closing');
|
$editorModal.removeClass('is-open').addClass('is-closing');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$editorModal.removeClass('is-closing').hide();
|
$editorModal.removeClass('is-closing').hide();
|
||||||
@@ -247,6 +297,21 @@ function closeTrackerEditor() {
|
|||||||
function applyTrackerConfig() {
|
function applyTrackerConfig() {
|
||||||
tempConfig = null; // Clear temp config
|
tempConfig = null; // Clear temp config
|
||||||
|
|
||||||
|
// Apply pending association changes
|
||||||
|
if (tempAssociation) {
|
||||||
|
if (tempAssociation.presetId !== null) {
|
||||||
|
// Associate with the pending preset
|
||||||
|
associatePresetWithCurrentEntity();
|
||||||
|
const preset = getPreset(tempAssociation.presetId);
|
||||||
|
toastr.success(`"${preset?.name || 'Unknown'}" is now associated with ${getCurrentEntityName()}.`);
|
||||||
|
} else {
|
||||||
|
// Remove association
|
||||||
|
removePresetAssociationForCurrentEntity();
|
||||||
|
}
|
||||||
|
tempAssociation = null;
|
||||||
|
}
|
||||||
|
originalAssociation = null;
|
||||||
|
|
||||||
// Save to the current preset
|
// Save to the current preset
|
||||||
const currentPresetId = getActivePresetId();
|
const currentPresetId = getActivePresetId();
|
||||||
if (currentPresetId) {
|
if (currentPresetId) {
|
||||||
@@ -354,7 +419,8 @@ function resetToDefaults() {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
messageCount: 5,
|
messageCount: 5,
|
||||||
injectionPosition: 'assistant_message_end',
|
injectionPosition: 'assistant_message_end',
|
||||||
contextPreamble: ''
|
contextPreamble: '',
|
||||||
|
sendAllEnabledOnRefresh: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,13 +729,27 @@ function renderUserStatsTab() {
|
|||||||
|
|
||||||
// Custom Stats section
|
// Custom Stats section
|
||||||
html += `<h4><i class="fa-solid fa-heart-pulse"></i> ${i18n.getTranslation('template.trackerEditorModal.userStatsTab.customStatsTitle')}</h4>`;
|
html += `<h4><i class="fa-solid fa-heart-pulse"></i> ${i18n.getTranslation('template.trackerEditorModal.userStatsTab.customStatsTitle')}</h4>`;
|
||||||
|
|
||||||
|
// Stats display mode toggle
|
||||||
|
const statsDisplayMode = config.statsDisplayMode || 'percentage';
|
||||||
|
html += '<div class="rpg-editor-toggle-row">';
|
||||||
|
html += '<label>Display Mode:</label>';
|
||||||
|
html += '<div class="rpg-radio-group">';
|
||||||
|
html += `<label><input type="radio" name="stats-display-mode" value="percentage" ${statsDisplayMode === 'percentage' ? 'checked' : ''}> Percentage</label>`;
|
||||||
|
html += `<label><input type="radio" name="stats-display-mode" value="number" ${statsDisplayMode === 'number' ? 'checked' : ''}> Number</label>`;
|
||||||
|
html += '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
html += '<div class="rpg-editor-stats-list" id="rpg-editor-stats-list">';
|
html += '<div class="rpg-editor-stats-list" id="rpg-editor-stats-list">';
|
||||||
|
|
||||||
config.customStats.forEach((stat, index) => {
|
config.customStats.forEach((stat, index) => {
|
||||||
|
const showMaxValue = statsDisplayMode === 'number';
|
||||||
|
const maxValue = stat.maxValue || 100;
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-editor-stat-item" data-index="${index}">
|
<div class="rpg-editor-stat-item" data-index="${index}">
|
||||||
<input type="checkbox" ${stat.enabled ? 'checked' : ''} class="rpg-stat-toggle" data-index="${index}">
|
<input type="checkbox" ${stat.enabled ? 'checked' : ''} class="rpg-stat-toggle" data-index="${index}">
|
||||||
<input type="text" value="${stat.name}" class="rpg-stat-name" data-index="${index}" placeholder="Stat Name">
|
<input type="text" value="${stat.name}" class="rpg-stat-name" data-index="${index}" placeholder="Stat Name">
|
||||||
|
<input type="number" value="${maxValue}" class="rpg-stat-max ${showMaxValue ? '' : 'rpg-hidden'}" data-index="${index}" placeholder="Max" min="1" step="1" title="Maximum value">
|
||||||
<button class="rpg-stat-remove" data-index="${index}" title="Remove stat"><i class="fa-solid fa-trash"></i></button>
|
<button class="rpg-stat-remove" data-index="${index}" title="Remove stat"><i class="fa-solid fa-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -779,7 +859,8 @@ function setupUserStatsListeners() {
|
|||||||
extensionSettings.trackerConfig.userStats.customStats.push({
|
extensionSettings.trackerConfig.userStats.customStats.push({
|
||||||
id: newId,
|
id: newId,
|
||||||
name: 'New Stat',
|
name: 'New Stat',
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
maxValue: 100
|
||||||
});
|
});
|
||||||
// Initialize value if doesn't exist
|
// Initialize value if doesn't exist
|
||||||
if (extensionSettings.userStats[newId] === undefined) {
|
if (extensionSettings.userStats[newId] === undefined) {
|
||||||
@@ -807,6 +888,19 @@ function setupUserStatsListeners() {
|
|||||||
extensionSettings.trackerConfig.userStats.customStats[index].name = $(this).val();
|
extensionSettings.trackerConfig.userStats.customStats[index].name = $(this).val();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Change stat max value
|
||||||
|
$('.rpg-stat-max').off('blur').on('blur', function() {
|
||||||
|
const index = $(this).data('index');
|
||||||
|
const value = parseInt($(this).val()) || 100;
|
||||||
|
extensionSettings.trackerConfig.userStats.customStats[index].maxValue = Math.max(1, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stats display mode toggle
|
||||||
|
$('input[name="stats-display-mode"]').off('change').on('change', function() {
|
||||||
|
extensionSettings.trackerConfig.userStats.statsDisplayMode = $(this).val();
|
||||||
|
renderUserStatsTab(); // Re-render to show/hide max value fields
|
||||||
|
});
|
||||||
|
|
||||||
// Add attribute
|
// Add attribute
|
||||||
$('#rpg-add-attr').off('click').on('click', function() {
|
$('#rpg-add-attr').off('click').on('click', function() {
|
||||||
// Ensure rpgAttributes array exists with defaults if needed
|
// Ensure rpgAttributes array exists with defaults if needed
|
||||||
@@ -913,9 +1007,7 @@ function renderInfoBoxTab() {
|
|||||||
html += `<label for="rpg-widget-date">${i18n.getTranslation('template.trackerEditorModal.infoBoxTab.dateWidget')}</label>`;
|
html += `<label for="rpg-widget-date">${i18n.getTranslation('template.trackerEditorModal.infoBoxTab.dateWidget')}</label>`;
|
||||||
html += '<select id="rpg-date-format" class="rpg-select-mini">';
|
html += '<select id="rpg-date-format" class="rpg-select-mini">';
|
||||||
html += `<option value="Weekday, Month, Year" ${config.widgets.date.format === 'Weekday, Month, Year' ? 'selected' : ''}>Weekday, Month, Year</option>`;
|
html += `<option value="Weekday, Month, Year" ${config.widgets.date.format === 'Weekday, Month, Year' ? 'selected' : ''}>Weekday, Month, Year</option>`;
|
||||||
html += `<option value="dd/mm/yyyy" ${config.widgets.date.format === 'dd/mm/yyyy' ? 'selected' : ''}>dd/mm/yyyy</option>`;
|
html += `<option value="Day (Numerical), Month, Year" ${config.widgets.date.format === 'Day (Numerical), Month, Year' ? 'selected' : ''}>Day (Numerical), Month, Year</option>`;
|
||||||
html += `<option value="mm/dd/yyyy" ${config.widgets.date.format === 'mm/dd/yyyy' ? 'selected' : ''}>mm/dd/yyyy</option>`;
|
|
||||||
html += `<option value="yyyy-mm-dd" ${config.widgets.date.format === 'yyyy-mm-dd' ? 'selected' : ''}>yyyy-mm-dd</option>`;
|
|
||||||
html += '</select>';
|
html += '</select>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
@@ -1364,11 +1456,13 @@ function renderHistoryPersistenceTab() {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
messageCount: 5,
|
messageCount: 5,
|
||||||
injectionPosition: 'assistant_message_end',
|
injectionPosition: 'assistant_message_end',
|
||||||
contextPreamble: ''
|
contextPreamble: '',
|
||||||
|
sendAllEnabledOnRefresh: false
|
||||||
};
|
};
|
||||||
const userStatsConfig = extensionSettings.trackerConfig.userStats;
|
const userStatsConfig = extensionSettings.trackerConfig.userStats;
|
||||||
const infoBoxConfig = extensionSettings.trackerConfig.infoBox;
|
const infoBoxConfig = extensionSettings.trackerConfig.infoBox;
|
||||||
const presentCharsConfig = extensionSettings.trackerConfig.presentCharacters;
|
const presentCharsConfig = extensionSettings.trackerConfig.presentCharacters;
|
||||||
|
const generationMode = extensionSettings.generationMode || 'together';
|
||||||
|
|
||||||
let html = '<div class="rpg-editor-section">';
|
let html = '<div class="rpg-editor-section">';
|
||||||
|
|
||||||
@@ -1382,6 +1476,15 @@ function renderHistoryPersistenceTab() {
|
|||||||
html += `<label for="rpg-history-persistence-enabled">Enable History Persistence</label>`;
|
html += `<label for="rpg-history-persistence-enabled">Enable History Persistence</label>`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
|
// External API Only toggle - only show for separate/external modes
|
||||||
|
if (generationMode === 'separate' || generationMode === 'external') {
|
||||||
|
html += '<div class="rpg-editor-toggle-row" style="margin-top: 8px;">';
|
||||||
|
html += `<input type="checkbox" id="rpg-history-send-all-enabled" ${historyPersistence.sendAllEnabledOnRefresh ? 'checked' : ''}>`;
|
||||||
|
html += `<label for="rpg-history-send-all-enabled">Send All Enabled Stats on Refresh</label>`;
|
||||||
|
html += '</div>';
|
||||||
|
html += `<p class="rpg-editor-hint" style="margin-top: 4px; margin-left: 24px;">When enabled, Refresh RPG Info will include all enabled stats from the preset in history context, ignoring the individual selections below.</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Message count
|
// Message count
|
||||||
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
||||||
html += `<label for="rpg-history-message-count">Number of messages to include (0 = all available):</label>`;
|
html += `<label for="rpg-history-message-count">Number of messages to include (0 = all available):</label>`;
|
||||||
@@ -1528,7 +1631,8 @@ function setupHistoryPersistenceListeners() {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
messageCount: 5,
|
messageCount: 5,
|
||||||
injectionPosition: 'assistant_message_end',
|
injectionPosition: 'assistant_message_end',
|
||||||
contextPreamble: ''
|
contextPreamble: '',
|
||||||
|
externalApiOnly: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1537,6 +1641,11 @@ function setupHistoryPersistenceListeners() {
|
|||||||
extensionSettings.historyPersistence.enabled = $(this).is(':checked');
|
extensionSettings.historyPersistence.enabled = $(this).is(':checked');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Send All Enabled on Refresh toggle
|
||||||
|
$('#rpg-history-send-all-enabled').off('change').on('change', function() {
|
||||||
|
extensionSettings.historyPersistence.sendAllEnabledOnRefresh = $(this).is(':checked');
|
||||||
|
});
|
||||||
|
|
||||||
// Message count
|
// Message count
|
||||||
$('#rpg-history-message-count').off('change').on('change', function() {
|
$('#rpg-history-message-count').off('change').on('change', function() {
|
||||||
extensionSettings.historyPersistence.messageCount = parseInt($(this).val()) || 0;
|
extensionSettings.historyPersistence.messageCount = parseInt($(this).val()) || 0;
|
||||||
|
|||||||
@@ -4,9 +4,106 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
||||||
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
|
|
||||||
let weatherContainer = null;
|
let weatherContainer = null;
|
||||||
let currentWeatherType = null;
|
let currentWeatherType = null;
|
||||||
|
let currentTimeOfDay = null;
|
||||||
|
let currentHour = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse time string to extract hour (24-hour format)
|
||||||
|
* Supports formats like "3:00 PM", "15:00", "3 PM", "Evening", etc.
|
||||||
|
*/
|
||||||
|
function parseHourFromTime(timeStr) {
|
||||||
|
if (!timeStr) return null;
|
||||||
|
|
||||||
|
const text = timeStr.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Check for descriptive time words first
|
||||||
|
if (text.includes('dawn') || text.includes('sunrise')) return 6;
|
||||||
|
if (text.includes('early morning')) return 7;
|
||||||
|
if (text.includes('morning')) return 9;
|
||||||
|
if (text.includes('midday') || text.includes('noon') || text.includes('mid-day')) return 12;
|
||||||
|
if (text.includes('afternoon')) return 14;
|
||||||
|
if (text.includes('late afternoon')) return 16;
|
||||||
|
if (text.includes('evening') || text.includes('dusk') || text.includes('sunset')) return 19;
|
||||||
|
if (text.includes('twilight')) return 20;
|
||||||
|
if (text.includes('night') || text.includes('nighttime')) return 22;
|
||||||
|
if (text.includes('midnight')) return 0;
|
||||||
|
if (text.includes('late night')) return 2;
|
||||||
|
|
||||||
|
// Try to parse numeric time formats
|
||||||
|
// Format: "3:00 PM" or "3:00PM" or "3 PM"
|
||||||
|
const ampmMatch = text.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i);
|
||||||
|
if (ampmMatch) {
|
||||||
|
let hour = parseInt(ampmMatch[1], 10);
|
||||||
|
const isPM = ampmMatch[3].toLowerCase() === 'pm';
|
||||||
|
if (isPM && hour !== 12) hour += 12;
|
||||||
|
if (!isPM && hour === 12) hour = 0;
|
||||||
|
return hour;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format: "15:00" (24-hour)
|
||||||
|
const militaryMatch = text.match(/(\d{1,2}):(\d{2})/);
|
||||||
|
if (militaryMatch) {
|
||||||
|
return parseInt(militaryMatch[1], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine time of day based on hour
|
||||||
|
*/
|
||||||
|
function getTimeOfDay(hour) {
|
||||||
|
if (hour === null) return 'unknown';
|
||||||
|
|
||||||
|
// Night: 8 PM (20:00) to 5 AM (05:00)
|
||||||
|
if (hour >= 20 || hour < 5) return 'night';
|
||||||
|
|
||||||
|
// Dawn/Dusk: 5 AM - 7 AM and 6 PM - 8 PM
|
||||||
|
if (hour >= 5 && hour < 7) return 'dawn';
|
||||||
|
if (hour >= 18 && hour < 20) return 'dusk';
|
||||||
|
|
||||||
|
// Day: 7 AM to 6 PM
|
||||||
|
return 'day';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract time from Info Box data
|
||||||
|
*/
|
||||||
|
function getCurrentTime() {
|
||||||
|
const infoBoxData = lastGeneratedData.infoBox || committedTrackerData.infoBox || '';
|
||||||
|
|
||||||
|
// Try to parse as JSON first (new format)
|
||||||
|
try {
|
||||||
|
const parsed = typeof infoBoxData === 'string' ? repairJSON(infoBoxData) : infoBoxData;
|
||||||
|
if (parsed && parsed.time) {
|
||||||
|
// Use the end time if available (current time), otherwise start time
|
||||||
|
return parsed.time.end || parsed.time.start || null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON, try old text format
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Parse the old text format to find Time field
|
||||||
|
const lines = infoBoxData.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed.startsWith('Time:')) {
|
||||||
|
const timeStr = trimmed.substring('Time:'.length).trim();
|
||||||
|
// If it contains →, take the end time (after arrow)
|
||||||
|
if (timeStr.includes('→')) {
|
||||||
|
const parts = timeStr.split('→');
|
||||||
|
return parts[1]?.trim() || parts[0]?.trim();
|
||||||
|
}
|
||||||
|
return timeStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse weather text to determine effect type
|
* Parse weather text to determine effect type
|
||||||
@@ -136,22 +233,299 @@ function createMist() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create sunshine rays effect
|
* Calculate sun position based on hour (arc across sky)
|
||||||
|
* Returns { left: vw%, top: dvh% }
|
||||||
*/
|
*/
|
||||||
function createSunshine() {
|
function calculateSunPosition(hour) {
|
||||||
const container = document.createElement('div');
|
// Daytime is roughly 5 AM to 8 PM (5-20)
|
||||||
container.className = 'rpg-weather-particles';
|
// Map hour to position along an arc
|
||||||
|
// 5 AM = far left, low | 12 PM = center, high | 8 PM = far right, low
|
||||||
|
|
||||||
|
if (hour === null) hour = 12; // Default to noon if unknown
|
||||||
|
|
||||||
|
// Clamp to daytime hours
|
||||||
|
const clampedHour = Math.max(5, Math.min(20, hour));
|
||||||
|
|
||||||
|
// Normalize to 0-1 range (5 AM = 0, 20 PM = 1)
|
||||||
|
const progress = (clampedHour - 5) / 15;
|
||||||
|
|
||||||
|
// Horizontal position: 3% to 92% (left to right, wider range)
|
||||||
|
const left = 3 + progress * 89;
|
||||||
|
|
||||||
|
// Vertical position: parabolic arc (high at noon, low at dawn/dusk)
|
||||||
|
// At progress 0.5 (noon), top should be ~8% (high)
|
||||||
|
// At progress 0 or 1, top should be ~40% (low, near horizon)
|
||||||
|
const normalizedProgress = (progress - 0.5) * 2; // -1 to 1
|
||||||
|
const top = 8 + 32 * (normalizedProgress * normalizedProgress);
|
||||||
|
|
||||||
|
return { left, top };
|
||||||
|
}
|
||||||
|
|
||||||
// Create 8 sun rays
|
/**
|
||||||
for (let i = 0; i < 8; i++) {
|
* Create clear/sunny weather effect with floating particles and warm glow
|
||||||
const ray = document.createElement('div');
|
*/
|
||||||
ray.className = 'rpg-weather-particle rpg-sunray';
|
function createSunshine(hour) {
|
||||||
ray.style.left = `${10 + i * 12}%`;
|
const container = document.createElement('div');
|
||||||
ray.style.animationDelay = `${i * 0.5}s`;
|
container.className = 'rpg-weather-particles rpg-clear-weather';
|
||||||
ray.style.animationDuration = `${8 + Math.random() * 4}s`;
|
|
||||||
container.appendChild(ray);
|
// Create the sun based on current hour
|
||||||
|
const sunPos = calculateSunPosition(hour);
|
||||||
|
|
||||||
|
const sun = document.createElement('div');
|
||||||
|
sun.className = 'rpg-weather-particle rpg-clear-sun';
|
||||||
|
sun.style.left = `${sunPos.left}vw`;
|
||||||
|
sun.style.top = `${sunPos.top}dvh`;
|
||||||
|
container.appendChild(sun);
|
||||||
|
|
||||||
|
// Create sun glow
|
||||||
|
const sunGlow = document.createElement('div');
|
||||||
|
sunGlow.className = 'rpg-weather-particle rpg-clear-sun-glow';
|
||||||
|
sunGlow.style.left = `${sunPos.left}vw`;
|
||||||
|
sunGlow.style.top = `${sunPos.top}dvh`;
|
||||||
|
container.appendChild(sunGlow);
|
||||||
|
|
||||||
|
// Create warm ambient glow overlay
|
||||||
|
const ambientGlow = document.createElement('div');
|
||||||
|
ambientGlow.className = 'rpg-weather-particle rpg-clear-ambient-glow';
|
||||||
|
container.appendChild(ambientGlow);
|
||||||
|
|
||||||
|
// Create floating dust motes / pollen particles (golden sparkles)
|
||||||
|
for (let i = 0; i < 25; 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`;
|
||||||
|
// Vary the size slightly
|
||||||
|
const size = 2 + Math.random() * 4;
|
||||||
|
particle.style.width = `${size}px`;
|
||||||
|
particle.style.height = `${size}px`;
|
||||||
|
container.appendChild(particle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create soft light orbs that drift gently
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const orb = document.createElement('div');
|
||||||
|
orb.className = 'rpg-weather-particle rpg-clear-light-orb';
|
||||||
|
orb.style.left = `${10 + Math.random() * 80}vw`;
|
||||||
|
orb.style.top = `${10 + Math.random() * 80}dvh`;
|
||||||
|
orb.style.animationDelay = `${i * 2}s`;
|
||||||
|
orb.style.animationDuration = `${20 + Math.random() * 10}s`;
|
||||||
|
// Vary the size
|
||||||
|
const size = 80 + Math.random() * 120;
|
||||||
|
orb.style.width = `${size}px`;
|
||||||
|
orb.style.height = `${size}px`;
|
||||||
|
container.appendChild(orb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create lens flare effect in corner
|
||||||
|
const lensFlare = document.createElement('div');
|
||||||
|
lensFlare.className = 'rpg-weather-particle rpg-clear-lens-flare';
|
||||||
|
container.appendChild(lensFlare);
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
function createNighttime(hour) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'rpg-weather-particles rpg-night-weather';
|
||||||
|
|
||||||
|
// Create dark blue ambient overlay
|
||||||
|
const nightOverlay = document.createElement('div');
|
||||||
|
nightOverlay.className = 'rpg-weather-particle rpg-night-overlay';
|
||||||
|
container.appendChild(nightOverlay);
|
||||||
|
|
||||||
|
// Calculate moon position based on hour
|
||||||
|
const moonPos = calculateMoonPosition(hour);
|
||||||
|
|
||||||
|
// Create the moon
|
||||||
|
const moon = document.createElement('div');
|
||||||
|
moon.className = 'rpg-weather-particle rpg-night-moon';
|
||||||
|
moon.style.left = `${moonPos.left}vw`;
|
||||||
|
moon.style.top = `${moonPos.top}dvh`;
|
||||||
|
container.appendChild(moon);
|
||||||
|
|
||||||
|
// Create moon glow
|
||||||
|
const moonGlow = document.createElement('div');
|
||||||
|
moonGlow.className = 'rpg-weather-particle rpg-night-moon-glow';
|
||||||
|
moonGlow.style.left = `${moonPos.left - 3}vw`;
|
||||||
|
moonGlow.style.top = `${moonPos.top - 3}dvh`;
|
||||||
|
container.appendChild(moonGlow);
|
||||||
|
|
||||||
|
// Create twinkling stars
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
const star = document.createElement('div');
|
||||||
|
star.className = 'rpg-weather-particle rpg-night-star';
|
||||||
|
star.style.left = `${Math.random() * 100}vw`;
|
||||||
|
star.style.top = `${Math.random() * 60}dvh`; // Stars mostly in upper portion
|
||||||
|
star.style.animationDelay = `${Math.random() * 5}s`;
|
||||||
|
star.style.animationDuration = `${2 + Math.random() * 3}s`;
|
||||||
|
// Vary the size
|
||||||
|
const size = 1 + Math.random() * 2;
|
||||||
|
star.style.width = `${size}px`;
|
||||||
|
star.style.height = `${size}px`;
|
||||||
|
container.appendChild(star);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a few brighter stars
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const brightStar = document.createElement('div');
|
||||||
|
brightStar.className = 'rpg-weather-particle rpg-night-star rpg-night-star-bright';
|
||||||
|
brightStar.style.left = `${Math.random() * 100}vw`;
|
||||||
|
brightStar.style.top = `${Math.random() * 50}dvh`;
|
||||||
|
brightStar.style.animationDelay = `${Math.random() * 4}s`;
|
||||||
|
brightStar.style.animationDuration = `${3 + Math.random() * 2}s`;
|
||||||
|
container.appendChild(brightStar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fireflies / floating light particles
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
const firefly = document.createElement('div');
|
||||||
|
firefly.className = 'rpg-weather-particle rpg-night-firefly';
|
||||||
|
firefly.style.left = `${Math.random() * 100}vw`;
|
||||||
|
firefly.style.top = `${40 + Math.random() * 55}dvh`; // Fireflies in lower portion
|
||||||
|
firefly.style.animationDelay = `${Math.random() * 10}s`;
|
||||||
|
firefly.style.animationDuration = `${8 + Math.random() * 7}s`;
|
||||||
|
container.appendChild(firefly);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subtle shooting star occasionally
|
||||||
|
const shootingStar = document.createElement('div');
|
||||||
|
shootingStar.className = 'rpg-weather-particle rpg-night-shooting-star';
|
||||||
|
container.appendChild(shootingStar);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +564,75 @@ function createWind() {
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate moon position based on hour (arc across sky at night)
|
||||||
|
* Returns { left: vw%, top: dvh% }
|
||||||
|
*/
|
||||||
|
function calculateMoonPosition(hour) {
|
||||||
|
// Nighttime is roughly 8 PM to 5 AM (20-5)
|
||||||
|
// Map hour to position along an arc
|
||||||
|
// 8 PM = far left, low | midnight = center-left, high | 5 AM = far right, low
|
||||||
|
|
||||||
|
if (hour === null) hour = 0; // Default to midnight if unknown
|
||||||
|
|
||||||
|
// Normalize night hours to 0-1 range
|
||||||
|
// 20 (8 PM) = 0, 0 (midnight) = ~0.44, 5 (5 AM) = 1
|
||||||
|
let progress;
|
||||||
|
if (hour >= 20) {
|
||||||
|
// 8 PM to midnight: 20-24 maps to 0-0.44
|
||||||
|
progress = (hour - 20) / 9;
|
||||||
|
} else {
|
||||||
|
// Midnight to 5 AM: 0-5 maps to 0.44-1
|
||||||
|
progress = (hour + 4) / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal position: 10% to 80% (left to right)
|
||||||
|
const left = 10 + progress * 70;
|
||||||
|
|
||||||
|
// Vertical position: parabolic arc (high at ~2 AM, low at dusk/dawn)
|
||||||
|
// Peak should be around progress 0.67 (~2 AM)
|
||||||
|
const peakProgress = 0.5;
|
||||||
|
const normalizedProgress = (progress - peakProgress) * 2; // -1 to 1
|
||||||
|
const top = 8 + 25 * (normalizedProgress * normalizedProgress);
|
||||||
|
|
||||||
|
return { left, top };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update sun/moon position without recreating the whole effect
|
||||||
|
*/
|
||||||
|
function updateCelestialPosition(hour) {
|
||||||
|
if (!weatherContainer) return false;
|
||||||
|
|
||||||
|
// Update sun position if it exists
|
||||||
|
const sun = weatherContainer.querySelector('.rpg-clear-sun');
|
||||||
|
const sunGlow = weatherContainer.querySelector('.rpg-clear-sun-glow');
|
||||||
|
|
||||||
|
if (sun && sunGlow) {
|
||||||
|
const sunPos = calculateSunPosition(hour);
|
||||||
|
sun.style.left = `${sunPos.left}vw`;
|
||||||
|
sun.style.top = `${sunPos.top}dvh`;
|
||||||
|
sunGlow.style.left = `${sunPos.left}vw`;
|
||||||
|
sunGlow.style.top = `${sunPos.top}dvh`;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update moon position if it exists
|
||||||
|
const moon = weatherContainer.querySelector('.rpg-night-moon');
|
||||||
|
const moonGlow = weatherContainer.querySelector('.rpg-night-moon-glow');
|
||||||
|
|
||||||
|
if (moon && moonGlow) {
|
||||||
|
const moonPos = calculateMoonPosition(hour);
|
||||||
|
moon.style.left = `${moonPos.left}vw`;
|
||||||
|
moon.style.top = `${moonPos.top}dvh`;
|
||||||
|
moonGlow.style.left = `${moonPos.left - 3}vw`;
|
||||||
|
moonGlow.style.top = `${moonPos.top - 3}dvh`;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove current weather effect
|
* Remove current weather effect
|
||||||
*/
|
*/
|
||||||
@@ -198,11 +641,13 @@ function removeWeatherEffect() {
|
|||||||
weatherContainer.remove();
|
weatherContainer.remove();
|
||||||
weatherContainer = null;
|
weatherContainer = null;
|
||||||
currentWeatherType = null;
|
currentWeatherType = null;
|
||||||
|
currentTimeOfDay = null;
|
||||||
|
currentHour = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update weather effect based on current weather
|
* Update weather effect based on current weather and time
|
||||||
*/
|
*/
|
||||||
export function updateWeatherEffect() {
|
export function updateWeatherEffect() {
|
||||||
// Check if dynamic weather is enabled
|
// Check if dynamic weather is enabled
|
||||||
@@ -214,8 +659,21 @@ export function updateWeatherEffect() {
|
|||||||
const weather = getCurrentWeather();
|
const weather = getCurrentWeather();
|
||||||
const weatherType = parseWeatherType(weather);
|
const weatherType = parseWeatherType(weather);
|
||||||
|
|
||||||
// Don't recreate if weather hasn't changed
|
// Get current time of day
|
||||||
if (weatherType === currentWeatherType) {
|
const timeStr = getCurrentTime();
|
||||||
|
const hour = parseHourFromTime(timeStr);
|
||||||
|
const timeOfDay = getTimeOfDay(hour);
|
||||||
|
|
||||||
|
// If only the hour changed (same weather and time of day), just update celestial position
|
||||||
|
if (weatherType === currentWeatherType && timeOfDay === currentTimeOfDay && hour !== currentHour) {
|
||||||
|
if (updateCelestialPosition(hour)) {
|
||||||
|
currentHour = hour;
|
||||||
|
return; // Successfully updated position without recreating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't recreate if nothing has changed
|
||||||
|
if (weatherType === currentWeatherType && timeOfDay === currentTimeOfDay && hour === currentHour) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +686,8 @@ export function updateWeatherEffect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentWeatherType = weatherType;
|
currentWeatherType = weatherType;
|
||||||
|
currentTimeOfDay = timeOfDay;
|
||||||
|
currentHour = hour;
|
||||||
|
|
||||||
switch (weatherType) {
|
switch (weatherType) {
|
||||||
case 'snow':
|
case 'snow':
|
||||||
@@ -240,7 +700,16 @@ export function updateWeatherEffect() {
|
|||||||
weatherContainer = createMist();
|
weatherContainer = createMist();
|
||||||
break;
|
break;
|
||||||
case 'sunny':
|
case 'sunny':
|
||||||
weatherContainer = createSunshine();
|
// Use appropriate effect based on time of day
|
||||||
|
if (timeOfDay === 'night') {
|
||||||
|
weatherContainer = createNighttime(hour);
|
||||||
|
} else if (timeOfDay === 'dawn') {
|
||||||
|
weatherContainer = createSunrise(hour);
|
||||||
|
} else if (timeOfDay === 'dusk') {
|
||||||
|
weatherContainer = createSunset(hour);
|
||||||
|
} else {
|
||||||
|
weatherContainer = createSunshine(hour);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'wind':
|
case 'wind':
|
||||||
weatherContainer = createWind();
|
weatherContainer = createWind();
|
||||||
@@ -270,6 +739,18 @@ export function updateWeatherEffect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (weatherContainer) {
|
if (weatherContainer) {
|
||||||
|
// Apply z-index based on background/foreground settings
|
||||||
|
if (extensionSettings.weatherForeground) {
|
||||||
|
weatherContainer.style.zIndex = '9998'; // In front of chat
|
||||||
|
weatherContainer.classList.add('rpg-weather-foreground');
|
||||||
|
} else if (extensionSettings.weatherBackground) {
|
||||||
|
weatherContainer.style.zIndex = '1'; // Behind chat (default)
|
||||||
|
weatherContainer.classList.remove('rpg-weather-foreground');
|
||||||
|
} else {
|
||||||
|
// Both disabled - don't show weather
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.body.appendChild(weatherContainer);
|
document.body.appendChild(weatherContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-6
@@ -11,13 +11,17 @@
|
|||||||
* @returns {object|null} Repaired JSON object or null if repair fails
|
* @returns {object|null} Repaired JSON object or null if repair fails
|
||||||
*/
|
*/
|
||||||
export function repairJSON(jsonString) {
|
export function repairJSON(jsonString) {
|
||||||
if (!jsonString || typeof jsonString !== 'string') {
|
if (typeof jsonString !== 'string') {
|
||||||
console.warn('[RPG JSON Repair] Invalid input:', typeof jsonString);
|
console.warn('[RPG JSON Repair] Invalid input type:', typeof jsonString);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cleaned = jsonString.trim();
|
let cleaned = jsonString.trim();
|
||||||
|
|
||||||
|
if (!cleaned) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove markdown code fences
|
// Remove markdown code fences
|
||||||
cleaned = cleaned.replace(/```json\s*/gi, '');
|
cleaned = cleaned.replace(/```json\s*/gi, '');
|
||||||
cleaned = cleaned.replace(/```\s*/g, '');
|
cleaned = cleaned.replace(/```\s*/g, '');
|
||||||
@@ -147,7 +151,8 @@ export function extractJSONFromText(text) {
|
|||||||
// Try to extract from ```json code fence
|
// Try to extract from ```json code fence
|
||||||
const fenceMatch = text.match(/```json\s*([\s\S]*?)```/i);
|
const fenceMatch = text.match(/```json\s*([\s\S]*?)```/i);
|
||||||
if (fenceMatch && fenceMatch[1]) {
|
if (fenceMatch && fenceMatch[1]) {
|
||||||
return fenceMatch[1].trim();
|
const trimmed = fenceMatch[1].trim();
|
||||||
|
if (trimmed) return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to extract from ``` code fence (without json label)
|
// Try to extract from ``` code fence (without json label)
|
||||||
@@ -155,20 +160,20 @@ export function extractJSONFromText(text) {
|
|||||||
if (genericFenceMatch && genericFenceMatch[1]) {
|
if (genericFenceMatch && genericFenceMatch[1]) {
|
||||||
const content = genericFenceMatch[1].trim();
|
const content = genericFenceMatch[1].trim();
|
||||||
// Check if it looks like JSON (starts with { or [)
|
// Check if it looks like JSON (starts with { or [)
|
||||||
if (content.startsWith('{') || content.startsWith('[')) {
|
if (content && (content.startsWith('{') || content.startsWith('['))) {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find standalone JSON object
|
// Try to find standalone JSON object
|
||||||
const objectMatch = text.match(/\{[\s\S]*\}/);
|
const objectMatch = text.match(/\{[\s\S]*\}/);
|
||||||
if (objectMatch) {
|
if (objectMatch && objectMatch[0].trim()) {
|
||||||
return objectMatch[0];
|
return objectMatch[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find standalone JSON array
|
// Try to find standalone JSON array
|
||||||
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
||||||
if (arrayMatch) {
|
if (arrayMatch && arrayMatch[0].trim()) {
|
||||||
return arrayMatch[0];
|
return arrayMatch[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+185
-9
@@ -4,6 +4,46 @@
|
|||||||
<i class="fa-solid fa-chevron-right"></i>
|
<i class="fa-solid fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Strip Widget Container (shown when collapsed with strip widgets enabled) -->
|
||||||
|
<div id="rpg-strip-widget-container" class="rpg-strip-widget-container">
|
||||||
|
<!-- Weather Icon Widget -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-weather" data-widget="weatherIcon">
|
||||||
|
<span class="rpg-strip-widget-icon"></span>
|
||||||
|
<span class="rpg-strip-widget-desc"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Clock Widget with animated face -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-clock" data-widget="clock">
|
||||||
|
<div class="rpg-strip-clock-face">
|
||||||
|
<div class="rpg-strip-clock-hour"></div>
|
||||||
|
<div class="rpg-strip-clock-minute"></div>
|
||||||
|
<div class="rpg-strip-clock-center"></div>
|
||||||
|
</div>
|
||||||
|
<span class="rpg-strip-widget-value"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Date Widget -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-date" data-widget="date">
|
||||||
|
<i class="fa-solid fa-calendar"></i>
|
||||||
|
<span class="rpg-strip-widget-value"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Location Widget -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-location" data-widget="location">
|
||||||
|
<i class="fa-solid fa-location-dot"></i>
|
||||||
|
<span class="rpg-strip-widget-value"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Stats Widget -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-stats" data-widget="stats">
|
||||||
|
<div class="rpg-strip-stats-list"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Attributes Widget -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-attributes" data-widget="attributes">
|
||||||
|
<div class="rpg-strip-attributes-grid"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Refresh Button (bottom) -->
|
||||||
|
<button id="rpg-strip-refresh" class="rpg-strip-refresh-btn" title="Refresh RPG Info">
|
||||||
|
<i class="fa-solid fa-sync"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main Game Panel -->
|
<!-- Main Game Panel -->
|
||||||
<div class="rpg-game-container">
|
<div class="rpg-game-container">
|
||||||
<!-- Header with Controls -->
|
<!-- Header with Controls -->
|
||||||
@@ -99,6 +139,15 @@
|
|||||||
</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">
|
||||||
@@ -201,29 +250,49 @@
|
|||||||
<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>
|
||||||
<input type="color" id="rpg-custom-bg" value="#1a1a2e" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<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>
|
||||||
<input type="color" id="rpg-custom-accent" value="#16213e" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<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>
|
||||||
<input type="color" id="rpg-custom-text" value="#eaeaea" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<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>
|
||||||
<input type="color" id="rpg-custom-highlight" value="#e94560" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<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>
|
||||||
<input type="color" id="rpg-stat-bar-color-low" value="#cc3333" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<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>
|
||||||
@@ -231,7 +300,11 @@
|
|||||||
<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>
|
||||||
<input type="color" id="rpg-stat-bar-color-high" value="#33cc66" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<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>
|
||||||
@@ -348,6 +421,15 @@
|
|||||||
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>
|
||||||
@@ -375,6 +457,25 @@
|
|||||||
Display a toggle button to enable/disable animated weather effects.
|
Display a toggle button to enable/disable animated weather effects.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
<!-- Weather sub-options (shown when dynamic weather is enabled) -->
|
||||||
|
<div id="rpg-weather-suboptions" style="margin-left: 24px; margin-top: 8px;">
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="radio" name="rpg-weather-position" id="rpg-toggle-weather-background" />
|
||||||
|
<span>Show in Background</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;">
|
||||||
|
Display weather effects behind the chat (standard behavior).
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="radio" name="rpg-weather-position" id="rpg-toggle-weather-foreground" />
|
||||||
|
<span>Show in Foreground</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;">
|
||||||
|
Display weather effects in front of the chat (experimental).
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="checkbox" id="rpg-toggle-show-narrator-mode" />
|
<input type="checkbox" id="rpg-toggle-show-narrator-mode" />
|
||||||
<span data-i18n-key="template.settingsModal.display.showNarratorMode">Show Narrator Mode</span>
|
<span data-i18n-key="template.settingsModal.display.showNarratorMode">Show Narrator Mode</span>
|
||||||
@@ -486,6 +587,56 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Strip Widgets Section -->
|
||||||
|
<div class="rpg-settings-group">
|
||||||
|
<h4 data-i18n-key="template.settingsModal.desktopStripTitle">Desktop Collapsed Strip Widgets</h4>
|
||||||
|
<small class="notes" style="display: block; margin-bottom: 10px;"
|
||||||
|
data-i18n-key="template.settingsModal.desktopStripNote">
|
||||||
|
Show compact info widgets in the collapsed panel strip on desktop. Displays stats vertically without needing to expand the panel.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-widgets-enabled" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.enabled">Enable Strip Widgets</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.desktopStrip.enabledNote">
|
||||||
|
Shows widgets in the collapsed panel strip for quick access to stats.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<div id="rpg-strip-widget-options" style="margin-left: 10px; border-left: 2px solid var(--SmartThemeBorderColor); padding-left: 10px; margin-top: 8px; display: none;">
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-weather-icon" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.weatherIcon">Weather Icon</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-clock" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.clock">Time/Clock</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-date" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.date">Date</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-location" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.location">Location</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-stats" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.stats">Stats (Health, Energy, etc.)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-attributes" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.attributes">RPG Attributes (STR, DEX, etc.)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rpg-settings-group">
|
<div class="rpg-settings-group">
|
||||||
<h4 data-i18n-key="template.settingsModal.advancedTitle"><i class="fa-solid fa-sliders"
|
<h4 data-i18n-key="template.settingsModal.advancedTitle"><i class="fa-solid fa-sliders"
|
||||||
aria-hidden="true"></i> Advanced</h4>
|
aria-hidden="true"></i> Advanced</h4>
|
||||||
@@ -887,6 +1038,20 @@
|
|||||||
</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> 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;">
|
||||||
@@ -929,6 +1094,20 @@
|
|||||||
</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> 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;">
|
||||||
@@ -1015,9 +1194,6 @@
|
|||||||
</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">
|
||||||
|
|||||||
Reference in New Issue
Block a user