Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8edc42164 | |||
| 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 |
@@ -7,13 +7,15 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
|
||||
|
||||
## 🆕 What's New
|
||||
|
||||
### v3.4.0
|
||||
|
||||
- Added History Persistance in Edit Trackers that allows you to control how many past trackers in the chat history to include.
|
||||
- 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 CYOA toggle.
|
||||
- Added Deception System toggle.
|
||||
- The trackers are no longer sent together with an image generation request.
|
||||
### v3.6.0
|
||||
|
||||
- You can now choose whether stats are displayed as percentages or numbers.
|
||||
- Added collapsed strip widgets for desktop.
|
||||
- Added new effects for the dynamic weather.
|
||||
- Changed the displayed clock format in the Info Box.
|
||||
- Fixed customized status field to work.
|
||||
- Fixed date format toggles.
|
||||
- Minor CSS and bug fixes.
|
||||
|
||||
**Special thanks to all the other contributors for this project:**
|
||||
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
||||
|
||||
@@ -131,7 +131,8 @@ import {
|
||||
} from './src/systems/ui/mobile.js';
|
||||
import {
|
||||
setupDesktopTabs,
|
||||
removeDesktopTabs
|
||||
removeDesktopTabs,
|
||||
updateStripWidgets
|
||||
} from './src/systems/ui/desktop.js';
|
||||
|
||||
// Feature modules
|
||||
@@ -150,6 +151,7 @@ import {
|
||||
onMessageReceived,
|
||||
onCharacterChanged,
|
||||
onMessageSwiped,
|
||||
onMessageDeleted,
|
||||
updatePersonaAvatar,
|
||||
clearExtensionPrompts,
|
||||
onGenerationEnded,
|
||||
@@ -593,6 +595,34 @@ async function initUI() {
|
||||
}
|
||||
saveSettings();
|
||||
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() {
|
||||
@@ -697,6 +727,63 @@ async function initUI() {
|
||||
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() {
|
||||
if (!extensionSettings.enabled) {
|
||||
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
|
||||
@@ -705,6 +792,14 @@ async function initUI() {
|
||||
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() {
|
||||
extensionSettings.statBarColorLow = String($(this).val());
|
||||
saveSettings();
|
||||
@@ -888,6 +983,8 @@ async function initUI() {
|
||||
$('#rpg-toggle-show-cyoa-toggle').prop('checked', extensionSettings.showCYOAToggle ?? true);
|
||||
$('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? 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-auto-avatars').prop('checked', extensionSettings.showAutoAvatars ?? true);
|
||||
|
||||
@@ -932,6 +1029,18 @@ async function initUI() {
|
||||
// Toggle visibility of widget options based on master toggle
|
||||
$('#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-high').val(extensionSettings.statBarColorHigh);
|
||||
$('#rpg-theme-select').val(extensionSettings.theme);
|
||||
@@ -1075,8 +1184,9 @@ jQuery(async () => {
|
||||
// Load chat-specific data for current chat
|
||||
try {
|
||||
loadChatData();
|
||||
// Initialize FAB widgets with any loaded data
|
||||
// Initialize FAB widgets and strip widgets with any loaded data
|
||||
updateFabWidgets();
|
||||
updateStripWidgets();
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] Chat data load failed, using defaults:', error);
|
||||
}
|
||||
@@ -1145,6 +1255,7 @@ jQuery(async () => {
|
||||
[event_types.GENERATION_ENDED]: onGenerationEnded,
|
||||
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts],
|
||||
[event_types.MESSAGE_SWIPED]: onMessageSwiped,
|
||||
[event_types.MESSAGE_DELETED]: onMessageDeleted,
|
||||
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
||||
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
||||
});
|
||||
@@ -1185,3 +1296,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",
|
||||
"css": "style.css",
|
||||
"author": "Marinara",
|
||||
"version": "3.4.1",
|
||||
"version": "3.6.0",
|
||||
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
||||
}
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
|
||||
v3.4.0
|
||||
v3.6.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -147,6 +147,12 @@ export function loadSettings() {
|
||||
|
||||
// Migrate to preset manager system if presets don't exist
|
||||
migrateToPresetManager();
|
||||
|
||||
// Initialize custom status fields
|
||||
initializeCustomStatusFields();
|
||||
|
||||
// Ensure all stats have maxValue (for number display mode)
|
||||
ensureStatsHaveMaxValue();
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] Error loading settings:', error);
|
||||
console.error('[RPG Companion] Error details:', error.message, error.stack);
|
||||
@@ -694,6 +700,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
|
||||
* @returns {Object} Map of preset ID to preset data
|
||||
@@ -915,6 +960,17 @@ export function hasPresetAssociation() {
|
||||
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
|
||||
* Called when character changes. Falls back to default preset if no association.
|
||||
|
||||
+21
-6
@@ -35,6 +35,8 @@ export let extensionSettings = {
|
||||
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use 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
|
||||
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
|
||||
showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default)
|
||||
@@ -53,7 +55,8 @@ export let extensionSettings = {
|
||||
enabled: false, // Master toggle for history persistence feature
|
||||
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'
|
||||
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'
|
||||
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
|
||||
@@ -81,6 +84,16 @@ export let extensionSettings = {
|
||||
stats: { enabled: true, position: 5 }, // All stats as compact numbers
|
||||
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({
|
||||
stats: [
|
||||
{ id: 'health', name: 'Health', value: 100 },
|
||||
@@ -112,13 +125,15 @@ export let extensionSettings = {
|
||||
// Tracker customization configuration
|
||||
trackerConfig: {
|
||||
userStats: {
|
||||
// Stats display mode: 'percentage' or 'number'
|
||||
statsDisplayMode: 'percentage',
|
||||
// Array of custom stats (allows add/remove/rename)
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false },
|
||||
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false }
|
||||
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false, maxValue: 100 }
|
||||
],
|
||||
// RPG Attributes (customizable D&D-style attributes)
|
||||
showRPGAttributes: true,
|
||||
|
||||
@@ -35,6 +35,7 @@ import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { generateAvatarsForCharacters } from '../features/avatarGenerator.js';
|
||||
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
||||
import { updateStripWidgets } from '../ui/desktop.js';
|
||||
|
||||
// Store the original preset name to restore after tracker generation
|
||||
let originalPresetName = null;
|
||||
@@ -240,8 +241,10 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
|
||||
// Update button to show "Updating..." state
|
||||
const $updateBtn = $('#rpg-manual-update');
|
||||
const $stripRefreshBtn = $('#rpg-strip-refresh');
|
||||
const updatingText = i18n.getTranslation('template.mainPanel.updating') || 'Updating...';
|
||||
$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();
|
||||
|
||||
@@ -380,11 +383,14 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
setIsGenerating(false);
|
||||
setFabLoadingState(false); // Stop spinning FAB on mobile
|
||||
updateFabWidgets(); // Update FAB widgets with new data
|
||||
updateStripWidgets(); // Update strip widgets with new data
|
||||
|
||||
// Restore button to original state
|
||||
const $updateBtn = $('#rpg-manual-update');
|
||||
const $stripRefreshBtn = $('#rpg-strip-refresh');
|
||||
const refreshText = i18n.getTranslation('template.mainPanel.refreshRpgInfo') || 'Refresh RPG Info';
|
||||
$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
|
||||
// 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) {
|
||||
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 characters = [];
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
committedTrackerData,
|
||||
lastGeneratedData,
|
||||
isGenerating,
|
||||
lastActionWasSwipe,
|
||||
setLastActionWasSwipe,
|
||||
setIsGenerating
|
||||
lastActionWasSwipe
|
||||
} from '../../core/state.js';
|
||||
import { evaluateSuppression } from './suppression.js';
|
||||
import { parseUserStats } from './parser.js';
|
||||
@@ -42,6 +40,9 @@ let lastCommittedChatLength = -1;
|
||||
// Store context map for prompt injection (used by event handlers)
|
||||
let pendingContextMap = 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.
|
||||
* Returns a map keyed by message index with formatted context strings.
|
||||
@@ -71,7 +72,8 @@ function buildHistoricalContextMap() {
|
||||
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
|
||||
// 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;
|
||||
for (let i = chat.length - 1; i >= 0; i--) {
|
||||
if (!chat[i].is_user && !chat[i].is_system) {
|
||||
@@ -81,9 +83,12 @@ function buildHistoricalContextMap() {
|
||||
}
|
||||
|
||||
// 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;
|
||||
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--) {
|
||||
const message = chat[i];
|
||||
@@ -133,14 +138,15 @@ function buildHistoricalContextMap() {
|
||||
let targetIndex = i; // Default: the assistant message itself
|
||||
|
||||
if (position === 'user_message_end') {
|
||||
// Find the next user message after this assistant message
|
||||
for (let j = i + 1; j < chat.length; j++) {
|
||||
// Find the preceding user message before this assistant message
|
||||
// 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) {
|
||||
targetIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If no user message found after, skip this one
|
||||
// If no user message found before, skip this one
|
||||
if (targetIndex === i) {
|
||||
continue;
|
||||
}
|
||||
@@ -183,11 +189,53 @@ function prepareHistoricalContextInjection() {
|
||||
const chat = context.chat;
|
||||
if (!chat || chat.length < 2) {
|
||||
pendingContextMap = new Map();
|
||||
historyInjectionDone = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Build and store the context map for use by prompt handlers
|
||||
pendingContextMap = buildHistoricalContextMap();
|
||||
historyInjectionDone = false; // Reset flag for new generation
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// Try to find the full content first
|
||||
let searchIndex = prompt.lastIndexOf(messageContent);
|
||||
|
||||
if (searchIndex !== -1) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,30 +255,28 @@ function injectContextIntoTextPrompt(prompt) {
|
||||
let modifiedPrompt = prompt;
|
||||
let injectedCount = 0;
|
||||
|
||||
// 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 pendingContextMap) {
|
||||
for (const [msgIdx, ctxContent] of sortedEntries) {
|
||||
const message = chat[msgIdx];
|
||||
if (!message || typeof message.mes !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the message content in the prompt
|
||||
// Use a portion of the message to find it (last 100 chars should be unique enough)
|
||||
const searchContent = message.mes.length > 100
|
||||
? message.mes.slice(-100)
|
||||
: message.mes;
|
||||
const position = findMessageInPrompt(modifiedPrompt, message.mes);
|
||||
|
||||
const searchIndex = modifiedPrompt.lastIndexOf(searchContent);
|
||||
if (searchIndex === -1) {
|
||||
// Message not found in prompt (might be truncated)
|
||||
if (!position) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Find the end of this message content in the prompt
|
||||
const insertPosition = searchIndex + searchContent.length;
|
||||
|
||||
// Insert the context after the message
|
||||
modifiedPrompt = modifiedPrompt.slice(0, insertPosition) + ctxContent + modifiedPrompt.slice(insertPosition);
|
||||
// Insert the context after the message content
|
||||
modifiedPrompt = modifiedPrompt.slice(0, position.end) + ctxContent + modifiedPrompt.slice(position.end);
|
||||
injectedCount++;
|
||||
}
|
||||
|
||||
@@ -264,20 +310,48 @@ function injectContextIntoChatPrompt(chatMessages) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find this message in the chat completion array by matching content
|
||||
// Use a portion of the message to find it
|
||||
const searchContent = originalMessage.mes.length > 100
|
||||
? originalMessage.mes.slice(-100)
|
||||
: originalMessage.mes;
|
||||
const messageContent = originalMessage.mes;
|
||||
|
||||
// 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' &&
|
||||
promptMsg.content.includes(searchContent)) {
|
||||
// Found the message - append context
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,9 +362,122 @@ function injectContextIntoChatPrompt(chatMessages) {
|
||||
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).
|
||||
* Injects historical context into the prompt string.
|
||||
* This is now a backup/fallback - primary injection happens in BEFORE_COMBINE.
|
||||
*
|
||||
* @param {Object} eventData - Event data with prompt property
|
||||
*/
|
||||
@@ -303,14 +490,19 @@ function onGenerateAfterCombinePrompts(eventData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if injection already happened in BEFORE_COMBINE
|
||||
if (historyInjectionDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only inject if we have pending context
|
||||
if (pendingContextMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
// DON'T clear pendingContextMap here - let it persist for other generations
|
||||
// (e.g., prewarm extensions). It will be cleared on GENERATION_ENDED.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -404,7 +596,6 @@ export async function onGenerationStarted(type, data, dryRun) {
|
||||
await restoreCheckpointOnLoad();
|
||||
|
||||
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)
|
||||
if (extensionSettings.generationMode === 'together') {
|
||||
@@ -747,8 +938,16 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
||||
export function initHistoryInjectionListeners() {
|
||||
// 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 userStatsConfig = trackerConfig?.userStats;
|
||||
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
const displayMode = userStatsConfig?.statsDisplayMode || 'percentage';
|
||||
|
||||
let instruction = '{\n';
|
||||
instruction += ' "stats": [\n';
|
||||
@@ -36,7 +37,12 @@ export function buildUserStatsJSONInstruction() {
|
||||
for (let i = 0; i < enabledStats.length; i++) {
|
||||
const stat = enabledStats[i];
|
||||
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';
|
||||
@@ -45,9 +51,24 @@ export function buildUserStatsJSONInstruction() {
|
||||
if (userStatsConfig?.statusSection?.enabled) {
|
||||
instruction += ' "status": {\n';
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -105,7 +126,8 @@ export function buildInfoBoxJSONInstruction() {
|
||||
let hasFields = false;
|
||||
|
||||
if (widgets.date?.enabled) {
|
||||
instruction += ' "date": {"value": "Weekday, Month, Year"}';
|
||||
const dateFormat = widgets.date.format || 'Weekday, Month, Year';
|
||||
instruction += ` "date": {"value": "${dateFormat}"}`;
|
||||
hasFields = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -547,9 +547,15 @@ export function parseUserStats(statsText) {
|
||||
extensionSettings.userStats.mood = statsData.status.mood;
|
||||
// console.log('[RPG Parser] ✓ Set mood =', statsData.status.mood);
|
||||
}
|
||||
if (statsData.status.conditions) {
|
||||
extensionSettings.userStats.conditions = statsData.status.conditions;
|
||||
// console.log('[RPG Parser] ✓ Set conditions =', statsData.status.conditions);
|
||||
// Extract all custom status fields
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,6 +685,7 @@ export function parseUserStats(statsText) {
|
||||
const statusConfig = trackerConfig?.userStats?.statusSection;
|
||||
if (statusConfig?.enabled) {
|
||||
let moodMatch = null;
|
||||
const customFields = statusConfig.customFields || [];
|
||||
|
||||
// Try Status: format
|
||||
const statusMatch = statsText.match(/Status:\s*(.+)/i);
|
||||
@@ -691,14 +698,30 @@ export function parseUserStats(statsText) {
|
||||
if (emoji) {
|
||||
extensionSettings.userStats.mood = emoji;
|
||||
// Remaining text contains custom status fields
|
||||
if (text) {
|
||||
extensionSettings.userStats.conditions = text;
|
||||
if (text && customFields.length > 0) {
|
||||
// For first custom field, use the remaining text
|
||||
const firstFieldKey = customFields[0].toLowerCase();
|
||||
extensionSettings.userStats[firstFieldKey] = text;
|
||||
}
|
||||
moodMatch = true;
|
||||
}
|
||||
} else {
|
||||
// No mood emoji, whole status is conditions
|
||||
extensionSettings.userStats.conditions = statusContent;
|
||||
// No mood emoji, whole status goes to first custom field
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -706,7 +729,10 @@ export function parseUserStats(statsText) {
|
||||
debugLog('[RPG Parser] Status match:', {
|
||||
found: !!moodMatch,
|
||||
mood: extensionSettings.userStats.mood,
|
||||
conditions: extensionSettings.userStats.conditions
|
||||
customFields: customFields.map(f => ({
|
||||
name: f,
|
||||
value: extensionSettings.userStats[f.toLowerCase()]
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -392,21 +392,30 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
// Include attributes based on settings (only if includeAttributes is true)
|
||||
if (includeAttributes) {
|
||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
||||
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||
const shouldSendAttributes = alwaysSendAttributes && showRPGAttributes;
|
||||
|
||||
if (shouldSendAttributes) {
|
||||
const attributesString = buildAttributesString();
|
||||
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
|
||||
@@ -476,11 +485,22 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
||||
|
||||
// Handle common object formats
|
||||
if (field && typeof field === 'object') {
|
||||
// Status object: {mood, conditions}
|
||||
if ('mood' in field && 'conditions' in field) {
|
||||
// Status object: {mood, [customFields...]}
|
||||
if ('mood' in field) {
|
||||
const statusParts = [];
|
||||
const mood = getValue(field.mood);
|
||||
const conditions = getValue(field.conditions);
|
||||
return `${mood} - ${conditions}`;
|
||||
if (mood) statusParts.push(mood);
|
||||
|
||||
// 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}
|
||||
@@ -731,19 +751,37 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {Object} trackerData - The tracker data from message.extra.rpg_companion_swipes[swipeId]
|
||||
* @param {Object} trackerConfig - The tracker configuration from extensionSettings.trackerConfig
|
||||
* @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
|
||||
*/
|
||||
export function formatHistoricalTrackerData(trackerData, trackerConfig, userName) {
|
||||
export function formatHistoricalTrackerData(trackerData, trackerConfig, userName, useAllEnabled = false) {
|
||||
if (!trackerData || !trackerConfig) {
|
||||
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 = '';
|
||||
|
||||
// Helper to safely get values
|
||||
@@ -789,11 +827,11 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
||||
|
||||
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) {
|
||||
for (const stat of userStatsData.stats) {
|
||||
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;
|
||||
statsFormatted += `${statName}: ${stat.value}, `;
|
||||
}
|
||||
@@ -801,15 +839,23 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
||||
}
|
||||
|
||||
// Status section
|
||||
if (userStatsConfig.statusSection?.persistInHistory && userStatsData.status) {
|
||||
if (shouldInclude(userStatsConfig.statusSection) && userStatsData.status) {
|
||||
const mood = getValue(userStatsData.status.mood || userStatsData.status);
|
||||
const conditions = getValue(userStatsData.status.conditions);
|
||||
if (mood) statsFormatted += `Mood: ${mood}, `;
|
||||
if (conditions && conditions !== 'None') statsFormatted += `Conditions: ${conditions}, `;
|
||||
if (mood && userStatsConfig.statusSection.showMoodEmoji) statsFormatted += `Mood: ${mood}, `;
|
||||
|
||||
// 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
|
||||
if (userStatsConfig.skillsSection?.persistInHistory && userStatsData.skills) {
|
||||
if (shouldInclude(userStatsConfig.skillsSection) && userStatsData.skills) {
|
||||
const skillsList = Array.isArray(userStatsData.skills)
|
||||
? userStatsData.skills.map(s => getValue(s)).filter(s => s).join(', ')
|
||||
: getValue(userStatsData.skills);
|
||||
@@ -817,7 +863,8 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
||||
}
|
||||
|
||||
// Inventory
|
||||
if (userStatsConfig.inventoryPersistInHistory && userStatsData.inventory) {
|
||||
const shouldIncludeInventory = useAllEnabled || userStatsConfig.inventoryPersistInHistory;
|
||||
if (shouldIncludeInventory && userStatsData.inventory) {
|
||||
const inv = userStatsData.inventory;
|
||||
if (inv.onPerson && Array.isArray(inv.onPerson) && inv.onPerson.length > 0) {
|
||||
const items = inv.onPerson.map(i => getValue(i)).filter(i => i);
|
||||
@@ -830,7 +877,8 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
||||
}
|
||||
|
||||
// Quests
|
||||
if (userStatsConfig.questsPersistInHistory && userStatsData.quests) {
|
||||
const shouldIncludeQuests = useAllEnabled || userStatsConfig.questsPersistInHistory;
|
||||
if (shouldIncludeQuests && userStatsData.quests) {
|
||||
const quests = userStatsData.quests;
|
||||
if (quests.main) {
|
||||
const mainQuest = getValue(quests.main);
|
||||
@@ -853,37 +901,37 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
||||
let infoFormatted = '';
|
||||
|
||||
// Date
|
||||
if (infoBoxConfig.widgets.date?.persistInHistory && infoBoxData.date) {
|
||||
if (shouldInclude(infoBoxConfig.widgets.date) && infoBoxData.date) {
|
||||
const date = getValue(infoBoxData.date);
|
||||
if (date) infoFormatted += `Date: ${date}, `;
|
||||
}
|
||||
|
||||
// Time
|
||||
if (infoBoxConfig.widgets.time?.persistInHistory && infoBoxData.time) {
|
||||
if (shouldInclude(infoBoxConfig.widgets.time) && infoBoxData.time) {
|
||||
const time = getValue(infoBoxData.time);
|
||||
if (time) infoFormatted += `Time: ${time}, `;
|
||||
}
|
||||
|
||||
// Weather
|
||||
if (infoBoxConfig.widgets.weather?.persistInHistory && infoBoxData.weather) {
|
||||
if (shouldInclude(infoBoxConfig.widgets.weather) && infoBoxData.weather) {
|
||||
const weather = getValue(infoBoxData.weather);
|
||||
if (weather) infoFormatted += `Weather: ${weather}, `;
|
||||
}
|
||||
|
||||
// Temperature
|
||||
if (infoBoxConfig.widgets.temperature?.persistInHistory && infoBoxData.temperature) {
|
||||
if (shouldInclude(infoBoxConfig.widgets.temperature) && infoBoxData.temperature) {
|
||||
const temp = getValue(infoBoxData.temperature);
|
||||
if (temp) infoFormatted += `Temp: ${temp}, `;
|
||||
}
|
||||
|
||||
// Location
|
||||
if (infoBoxConfig.widgets.location?.persistInHistory && infoBoxData.location) {
|
||||
if (shouldInclude(infoBoxConfig.widgets.location) && infoBoxData.location) {
|
||||
const location = getValue(infoBoxData.location);
|
||||
if (location) infoFormatted += `Location: ${location}, `;
|
||||
}
|
||||
|
||||
// Recent Events
|
||||
if (infoBoxConfig.widgets.recentEvents?.persistInHistory && infoBoxData.recentEvents) {
|
||||
if (shouldInclude(infoBoxConfig.widgets.recentEvents) && infoBoxData.recentEvents) {
|
||||
const events = getValue(infoBoxData.recentEvents);
|
||||
if (events) infoFormatted += `Events: ${events}, `;
|
||||
}
|
||||
@@ -911,7 +959,7 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
||||
// Custom fields (appearance, demeanor, etc.)
|
||||
if (char.details && typeof char.details === 'object') {
|
||||
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]);
|
||||
if (value) charFormatted += `${field.name}: ${value}, `;
|
||||
}
|
||||
@@ -919,7 +967,7 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
|
||||
}
|
||||
|
||||
// Thoughts
|
||||
if (charsConfig.thoughts?.persistInHistory && char.thoughts) {
|
||||
if (shouldInclude(charsConfig.thoughts) && char.thoughts) {
|
||||
const thoughts = typeof char.thoughts === 'object' && char.thoughts.content
|
||||
? getValue(char.thoughts.content)
|
||||
: getValue(char.thoughts);
|
||||
@@ -990,19 +1038,25 @@ export function generateContextualSummary() {
|
||||
|
||||
// Include attributes based on settings
|
||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
||||
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||
const shouldSendAttributes = alwaysSendAttributes && showRPGAttributes;
|
||||
|
||||
if (shouldSendAttributes) {
|
||||
const attributesString = buildAttributesString();
|
||||
summary += `${userName}'s attributes: ${attributesString}\n`;
|
||||
}
|
||||
|
||||
// Add dice roll context if there was one
|
||||
if (extensionSettings.lastDiceRoll) {
|
||||
const roll = extensionSettings.lastDiceRoll;
|
||||
// Add dice roll context if there was one (independent of attributes)
|
||||
if (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`;
|
||||
} 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();
|
||||
@@ -1172,18 +1226,31 @@ export async function generateSeparateUpdatePrompt() {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentSwipeId = message.swipe_id || 0;
|
||||
const trackerData = swipeData[currentSwipeId];
|
||||
if (!trackerData) {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
@@ -1195,14 +1262,15 @@ export async function generateSeparateUpdatePrompt() {
|
||||
let targetIdx = i;
|
||||
|
||||
if (position === 'user_message_end') {
|
||||
// Find next user message after this assistant message
|
||||
for (let j = i + 1; j < recentMessages.length; j++) {
|
||||
// Find the preceding user message before this assistant message
|
||||
// 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) {
|
||||
targetIdx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If no user message found, skip
|
||||
// If no user message found before, skip
|
||||
if (targetIdx === i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||
|
||||
// UI
|
||||
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
||||
import { updateStripWidgets } from '../ui/desktop.js';
|
||||
|
||||
// Chapter checkpoint
|
||||
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
|
||||
setIsAwaitingNewMessage(true);
|
||||
|
||||
// Show FAB loading state for together mode (starts spinning)
|
||||
if (extensionSettings.generationMode === 'together') {
|
||||
setFabLoadingState(true);
|
||||
}
|
||||
// Note: FAB spinning is NOT shown for together mode since no extra API request is made
|
||||
// The RPG data comes embedded in the main response
|
||||
// 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
|
||||
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
|
||||
@@ -233,6 +233,10 @@ export async function onMessageReceived(data) {
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
|
||||
// Update FAB widgets and strip widgets with newly parsed data
|
||||
updateFabWidgets();
|
||||
updateStripWidgets();
|
||||
|
||||
// Then update the DOM to reflect the cleaned message
|
||||
// Using updateMessageBlock to perform macro substitutions + regex formatting
|
||||
const messageId = chat.length - 1;
|
||||
@@ -264,9 +268,10 @@ export async function onMessageReceived(data) {
|
||||
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
|
||||
setTimeout(async () => {
|
||||
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);
|
||||
updateFabWidgets();
|
||||
updateStripWidgets();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
@@ -292,6 +297,7 @@ export async function onMessageReceived(data) {
|
||||
// Stop FAB loading state and update widgets
|
||||
setFabLoadingState(false);
|
||||
updateFabWidgets();
|
||||
updateStripWidgets();
|
||||
|
||||
// Re-apply checkpoint in case SillyTavern unhid messages during generation
|
||||
await restoreCheckpointOnLoad();
|
||||
@@ -330,8 +336,9 @@ export function onCharacterChanged() {
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
|
||||
// Update FAB widgets with loaded data
|
||||
// Update FAB widgets and strip widgets with loaded data
|
||||
updateFabWidgets();
|
||||
updateStripWidgets();
|
||||
|
||||
// Update chat thought overlays
|
||||
updateChatThoughts();
|
||||
@@ -352,7 +359,8 @@ export function onMessageSwiped(messageIndex) {
|
||||
// console.log('[RPG Companion] 🔵 EVENT: onMessageSwiped at index:', messageIndex);
|
||||
|
||||
// Get the message that was swiped
|
||||
const message = chat[messageIndex];
|
||||
const currentChat = getContext().chat;
|
||||
const message = currentChat[messageIndex];
|
||||
if (!message || message.is_user) {
|
||||
// console.log('[RPG Companion] 🔵 Ignoring swipe - message is user or undefined');
|
||||
return;
|
||||
@@ -372,32 +380,79 @@ export function onMessageSwiped(messageIndex) {
|
||||
setLastActionWasSwipe(true);
|
||||
setIsAwaitingNewMessage(true);
|
||||
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
|
||||
|
||||
// CRITICAL: For new swipes, commit data from the PREVIOUS assistant message
|
||||
// This ensures the LLM gets context from BEFORE the message being regenerated,
|
||||
// not the message itself (which would cause time/story to advance incorrectly)
|
||||
for (let i = messageIndex - 1; i >= 0; i--) {
|
||||
const prevMessage = currentChat[i];
|
||||
if (!prevMessage.is_user && prevMessage.extra?.rpg_companion_swipes) {
|
||||
const prevSwipeId = prevMessage.swipe_id || 0;
|
||||
const prevSwipeData = prevMessage.extra.rpg_companion_swipes[prevSwipeId];
|
||||
|
||||
if (prevSwipeData) {
|
||||
// console.log('[RPG Companion] 🔵 Committing tracker data from PREVIOUS message at index', i);
|
||||
committedTrackerData.userStats = prevSwipeData.userStats || null;
|
||||
committedTrackerData.infoBox = prevSwipeData.infoBox || null;
|
||||
committedTrackerData.characterThoughts = prevSwipeData.characterThoughts || null;
|
||||
} else {
|
||||
// Previous message has no swipe data - clear committed data
|
||||
committedTrackerData.userStats = null;
|
||||
committedTrackerData.infoBox = null;
|
||||
committedTrackerData.characterThoughts = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If we hit index 0 without finding a previous assistant message, clear committed data
|
||||
if (i === 0) {
|
||||
// console.log('[RPG Companion] 🔵 No previous assistant message found - clearing committed data');
|
||||
committedTrackerData.userStats = null;
|
||||
committedTrackerData.infoBox = null;
|
||||
committedTrackerData.characterThoughts = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Edge case: if messageIndex is 0 (first message being swiped), clear committed data
|
||||
if (messageIndex === 0) {
|
||||
// console.log('[RPG Companion] 🔵 Swiping first message - clearing committed data');
|
||||
committedTrackerData.userStats = null;
|
||||
committedTrackerData.infoBox = null;
|
||||
committedTrackerData.characterThoughts = null;
|
||||
}
|
||||
|
||||
// For new swipes, also update lastGeneratedData to match committed data
|
||||
// This ensures the UI shows the "before" state while waiting for the new response
|
||||
lastGeneratedData.userStats = committedTrackerData.userStats;
|
||||
lastGeneratedData.infoBox = committedTrackerData.infoBox;
|
||||
lastGeneratedData.characterThoughts = committedTrackerData.characterThoughts;
|
||||
|
||||
// Parse user stats for display if available
|
||||
if (committedTrackerData.userStats) {
|
||||
parseUserStats(committedTrackerData.userStats);
|
||||
}
|
||||
} else {
|
||||
// This is navigating to an EXISTING swipe - don't change the flag
|
||||
// console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe);
|
||||
}
|
||||
|
||||
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
||||
// Load RPG data for this existing swipe for DISPLAY purposes
|
||||
if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) {
|
||||
const swipeData = message.extra.rpg_companion_swipes[currentSwipeId];
|
||||
|
||||
// Load RPG data for this swipe
|
||||
// lastGeneratedData is for DISPLAY, committedTrackerData is for GENERATION
|
||||
// It's safe to load swipe data into lastGeneratedData - it won't be committed due to !lastActionWasSwipe check
|
||||
if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) {
|
||||
const swipeData = message.extra.rpg_companion_swipes[currentSwipeId];
|
||||
// Load swipe data into lastGeneratedData for display
|
||||
lastGeneratedData.userStats = swipeData.userStats || null;
|
||||
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
||||
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
||||
|
||||
// Load swipe data into lastGeneratedData for display (both modes)
|
||||
lastGeneratedData.userStats = swipeData.userStats || null;
|
||||
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
||||
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
||||
// Parse user stats if available
|
||||
if (swipeData.userStats) {
|
||||
parseUserStats(swipeData.userStats);
|
||||
}
|
||||
|
||||
// Parse user stats if available
|
||||
if (swipeData.userStats) {
|
||||
parseUserStats(swipeData.userStats);
|
||||
// console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId);
|
||||
} else {
|
||||
// console.log('[RPG Companion] ℹ️ No stored data for swipe:', currentSwipeId);
|
||||
}
|
||||
|
||||
// console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId);
|
||||
} else {
|
||||
// console.log('[RPG Companion] ℹ️ No stored data for swipe:', currentSwipeId);
|
||||
}
|
||||
|
||||
// Re-render the panels
|
||||
@@ -412,6 +467,148 @@ export function onMessageSwiped(messageIndex) {
|
||||
updateChatThoughts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for when a message is deleted.
|
||||
* Restores RPG state from the last assistant message with RPG data,
|
||||
* or clears state if no messages remain.
|
||||
*/
|
||||
export function onMessageDeleted(messageIndex) {
|
||||
if (!extensionSettings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('[RPG Companion] 🗑️ EVENT: onMessageDeleted at index:', messageIndex);
|
||||
|
||||
const context = getContext();
|
||||
const currentChat = context.chat;
|
||||
|
||||
// If chat is empty, clear all RPG state
|
||||
if (!currentChat || currentChat.length === 0) {
|
||||
// console.log('[RPG Companion] 🗑️ Chat is empty - clearing RPG state');
|
||||
lastGeneratedData.userStats = null;
|
||||
lastGeneratedData.infoBox = null;
|
||||
lastGeneratedData.characterThoughts = null;
|
||||
|
||||
committedTrackerData.userStats = null;
|
||||
committedTrackerData.infoBox = null;
|
||||
committedTrackerData.characterThoughts = null;
|
||||
|
||||
// Clear parsed stats from extensionSettings
|
||||
if (extensionSettings.userStats) {
|
||||
extensionSettings.userStats = null;
|
||||
}
|
||||
|
||||
// Re-render empty panels
|
||||
renderUserStats();
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
|
||||
// Update FAB widgets and strip widgets
|
||||
updateFabWidgets();
|
||||
updateStripWidgets();
|
||||
|
||||
// Update chat thought overlays (removes any remaining)
|
||||
updateChatThoughts();
|
||||
|
||||
// Save the cleared state
|
||||
saveChatData();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the last assistant message with RPG data
|
||||
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||
const message = currentChat[i];
|
||||
if (!message.is_user && message.extra?.rpg_companion_swipes) {
|
||||
const swipeId = message.swipe_id || 0;
|
||||
const swipeData = message.extra.rpg_companion_swipes[swipeId];
|
||||
|
||||
if (swipeData) {
|
||||
// Check if this is the same data we already have displayed
|
||||
const sameUserStats = lastGeneratedData.userStats === swipeData.userStats;
|
||||
const sameInfoBox = lastGeneratedData.infoBox === swipeData.infoBox;
|
||||
const sameThoughts = lastGeneratedData.characterThoughts === swipeData.characterThoughts;
|
||||
|
||||
if (sameUserStats && sameInfoBox && sameThoughts) {
|
||||
// console.log('[RPG Companion] 🗑️ RPG state already matches last message - no restore needed');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('[RPG Companion] 🗑️ Restoring RPG state from message index', i, 'swipe', swipeId);
|
||||
|
||||
// Restore state from this message
|
||||
lastGeneratedData.userStats = swipeData.userStats || null;
|
||||
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
||||
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
||||
|
||||
// Also update committed data so next generation uses correct context
|
||||
committedTrackerData.userStats = swipeData.userStats || null;
|
||||
committedTrackerData.infoBox = swipeData.infoBox || null;
|
||||
committedTrackerData.characterThoughts = swipeData.characterThoughts || null;
|
||||
|
||||
// Parse user stats if available
|
||||
if (swipeData.userStats) {
|
||||
parseUserStats(swipeData.userStats);
|
||||
}
|
||||
|
||||
// Re-render panels with restored data
|
||||
renderUserStats();
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
|
||||
// Update FAB widgets and strip widgets
|
||||
updateFabWidgets();
|
||||
updateStripWidgets();
|
||||
|
||||
// Update chat thought overlays
|
||||
updateChatThoughts();
|
||||
|
||||
// Save the restored state
|
||||
saveChatData();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No assistant message with RPG data found - clear state
|
||||
// console.log('[RPG Companion] 🗑️ No assistant message with RPG data found - clearing state');
|
||||
lastGeneratedData.userStats = null;
|
||||
lastGeneratedData.infoBox = null;
|
||||
lastGeneratedData.characterThoughts = null;
|
||||
|
||||
committedTrackerData.userStats = null;
|
||||
committedTrackerData.infoBox = null;
|
||||
committedTrackerData.characterThoughts = null;
|
||||
|
||||
// Clear parsed stats
|
||||
if (extensionSettings.userStats) {
|
||||
extensionSettings.userStats = null;
|
||||
}
|
||||
|
||||
// Re-render empty panels
|
||||
renderUserStats();
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
|
||||
// Update FAB widgets and strip widgets
|
||||
updateFabWidgets();
|
||||
updateStripWidgets();
|
||||
|
||||
// Update chat thought overlays
|
||||
updateChatThoughts();
|
||||
|
||||
// Save the cleared state
|
||||
saveChatData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the persona avatar image when user switches personas
|
||||
*/
|
||||
|
||||
@@ -436,14 +436,12 @@ export function renderInfoBox() {
|
||||
|
||||
// Time widget - show if enabled
|
||||
if (config?.widgets?.time?.enabled) {
|
||||
// Determine which time value to display and edit
|
||||
const hasTimeEnd = Boolean(data.timeEnd);
|
||||
const hasTimeStart = Boolean(data.timeStart);
|
||||
const timeDisplay = data.timeEnd || data.timeStart || '12:00';
|
||||
const timeField = hasTimeEnd ? 'timeEnd' : 'timeStart';
|
||||
// Get both start and end times
|
||||
const timeStartDisplay = data.timeStart || '12:00';
|
||||
const timeEndDisplay = data.timeEnd || data.timeStart || '12:00';
|
||||
|
||||
// Parse time for clock hands
|
||||
const timeMatch = timeDisplay.match(/(\d+):(\d+)/);
|
||||
// Parse end time for clock hands (use end time for visual display)
|
||||
const timeMatch = timeEndDisplay.match(/(\d+):(\d+)/);
|
||||
let hourAngle = 0;
|
||||
let minuteAngle = 0;
|
||||
if (timeMatch) {
|
||||
@@ -465,7 +463,11 @@ export function renderInfoBox() {
|
||||
<div class="rpg-clock-center"></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>
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@ function updateUserStatsData() {
|
||||
|
||||
// Then, add any other numeric stats from extensionSettings that aren't in config
|
||||
// (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]) => {
|
||||
if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') {
|
||||
statsArray.push({
|
||||
@@ -118,12 +119,17 @@ function updateUserStatsData() {
|
||||
|
||||
jsonData.stats = statsArray;
|
||||
|
||||
// Update status
|
||||
// Update status - include all custom status fields
|
||||
jsonData.status = {
|
||||
mood: stats.mood || '😐',
|
||||
conditions: stats.conditions || 'None'
|
||||
mood: stats.mood || '😐'
|
||||
};
|
||||
|
||||
// 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)
|
||||
const convertToV3Items = (itemString) => {
|
||||
if (!itemString) return [];
|
||||
@@ -276,16 +282,33 @@ export function renderUserStats() {
|
||||
}
|
||||
html += '<div class="rpg-stats-grid">';
|
||||
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
|
||||
const displayMode = config.statsDisplayMode || 'percentage';
|
||||
|
||||
for (const stat of enabledStats) {
|
||||
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 += `
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
@@ -308,13 +331,15 @@ export function renderUserStats() {
|
||||
|
||||
// Render custom status fields
|
||||
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
|
||||
// For now, use first field as "conditions" for backward compatibility
|
||||
let conditionsValue = stats.conditions || 'None';
|
||||
// Strip brackets if present (from JSON array format)
|
||||
if (typeof conditionsValue === 'string') {
|
||||
conditionsValue = conditionsValue.replace(/^\[|\]$/g, '').trim();
|
||||
for (const fieldName of config.statusSection.customFields) {
|
||||
const fieldKey = fieldName.toLowerCase();
|
||||
let fieldValue = stats[fieldKey] || 'None';
|
||||
// Strip brackets if present (from JSON array format)
|
||||
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>';
|
||||
@@ -406,14 +431,31 @@ export function renderUserStats() {
|
||||
// Add event listeners for editable stat values
|
||||
$('.rpg-editable-stat').on('blur', function() {
|
||||
const field = $(this).data('field');
|
||||
const textValue = $(this).text().replace('%', '').trim();
|
||||
let value = parseInt(textValue);
|
||||
const mode = $(this).data('mode');
|
||||
const maxValue = parseInt($(this).data('max')) || 100;
|
||||
const textValue = $(this).text().trim();
|
||||
let value;
|
||||
|
||||
// Validate and clamp value between 0 and 100
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
if (mode === 'number') {
|
||||
// In number mode, parse "X/MAX" or just "X"
|
||||
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
|
||||
extensionSettings.userStats[field] = value;
|
||||
@@ -445,7 +487,8 @@ export function renderUserStats() {
|
||||
|
||||
$('.rpg-mood-conditions.rpg-editable').on('blur', function() {
|
||||
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)
|
||||
updateUserStatsData();
|
||||
|
||||
+265
-2
@@ -1,10 +1,273 @@
|
||||
/**
|
||||
* 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 { extensionSettings } from '../../core/state.js';
|
||||
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.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 highColor = extensionSettings.statBarColorHigh || '#33cc66';
|
||||
|
||||
// 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 ? highColor : lowColor;
|
||||
|
||||
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);
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from '../../core/state.js';
|
||||
import { i18n } from '../../core/i18n.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.
|
||||
@@ -243,6 +243,9 @@ export function setupCollapseToggle() {
|
||||
} else if ($panel.hasClass('rpg-position-left')) {
|
||||
$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') {
|
||||
// In "together" mode, manual update button is hidden
|
||||
$('#rpg-manual-update').hide();
|
||||
$('#rpg-strip-refresh').hide();
|
||||
$('#rpg-external-api-settings').slideUp(200);
|
||||
$('#rpg-separate-mode-settings').slideUp(200);
|
||||
// Hide auto-update toggle (not applicable in together mode)
|
||||
@@ -438,6 +442,7 @@ export function updateGenerationModeUI() {
|
||||
} else if (extensionSettings.generationMode === 'separate') {
|
||||
// In "separate" mode, manual update button is visible
|
||||
$('#rpg-manual-update').show();
|
||||
$('#rpg-strip-refresh').show();
|
||||
$('#rpg-external-api-settings').slideUp(200);
|
||||
$('#rpg-separate-mode-settings').slideDown(200);
|
||||
// Show auto-update toggle
|
||||
@@ -445,6 +450,7 @@ export function updateGenerationModeUI() {
|
||||
} else if (extensionSettings.generationMode === 'external') {
|
||||
// In "external" mode, manual update button is visible AND both settings are shown
|
||||
$('#rpg-manual-update').show();
|
||||
$('#rpg-strip-refresh').show();
|
||||
$('#rpg-external-api-settings').slideDown(200);
|
||||
$('#rpg-separate-mode-settings').slideDown(200);
|
||||
// Show auto-update toggle for external mode too
|
||||
|
||||
+122
-13
@@ -21,6 +21,8 @@ import {
|
||||
removePresetAssociationForCurrentEntity,
|
||||
getPresetForCurrentEntity,
|
||||
hasPresetAssociation,
|
||||
isAssociatedWithCurrentPreset,
|
||||
getCurrentEntityKey,
|
||||
getCurrentEntityName,
|
||||
exportPresets,
|
||||
importPresets
|
||||
@@ -33,6 +35,8 @@ import { updateFabWidgets } from './mobile.js';
|
||||
let $editorModal = null;
|
||||
let activeTab = 'userStats';
|
||||
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
|
||||
@@ -104,6 +108,12 @@ export function initTrackerEditor() {
|
||||
$(document).on('change', '#rpg-preset-select', function() {
|
||||
const presetId = $(this).val();
|
||||
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
|
||||
const currentPresetId = getActivePresetId();
|
||||
if (currentPresetId) {
|
||||
@@ -113,8 +123,17 @@ export function initTrackerEditor() {
|
||||
if (loadPreset(presetId)) {
|
||||
tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig));
|
||||
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();
|
||||
toastr.success(`Switched to preset "${getPreset(presetId)?.name || 'Unknown'}".`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -162,12 +181,25 @@ export function initTrackerEditor() {
|
||||
|
||||
// Associate preset checkbox
|
||||
$(document).on('change', '#rpg-preset-associate', function() {
|
||||
const activePresetId = getActivePresetId();
|
||||
const preset = getPreset(activePresetId);
|
||||
const entityName = getCurrentEntityName();
|
||||
const entityKey = getCurrentEntityKey();
|
||||
|
||||
if ($(this).is(':checked')) {
|
||||
associatePresetWithCurrentEntity();
|
||||
toastr.info(`This preset will be used for ${getCurrentEntityName()}.`);
|
||||
// Store pending association (don't save yet)
|
||||
tempAssociation = { presetId: activePresetId, entityKey: entityKey };
|
||||
toastr.info(`"${preset?.name || 'Unknown'}" will be associated with ${entityName} when saved.`);
|
||||
} else {
|
||||
removePresetAssociationForCurrentEntity();
|
||||
toastr.info(`Preset association removed for ${getCurrentEntityName()}.`);
|
||||
// Store pending removal (don't save yet)
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -214,6 +254,12 @@ function openTrackerEditor() {
|
||||
// Create temporary copy for cancel functionality
|
||||
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
|
||||
const theme = extensionSettings.theme || 'modern';
|
||||
$editorModal.attr('data-theme', theme);
|
||||
@@ -235,6 +281,10 @@ function closeTrackerEditor() {
|
||||
tempConfig = null;
|
||||
}
|
||||
|
||||
// Discard pending association changes (cancel = no save)
|
||||
tempAssociation = null;
|
||||
originalAssociation = null;
|
||||
|
||||
$editorModal.removeClass('is-open').addClass('is-closing');
|
||||
setTimeout(() => {
|
||||
$editorModal.removeClass('is-closing').hide();
|
||||
@@ -247,6 +297,21 @@ function closeTrackerEditor() {
|
||||
function applyTrackerConfig() {
|
||||
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
|
||||
const currentPresetId = getActivePresetId();
|
||||
if (currentPresetId) {
|
||||
@@ -354,7 +419,8 @@ function resetToDefaults() {
|
||||
enabled: false,
|
||||
messageCount: 5,
|
||||
injectionPosition: 'assistant_message_end',
|
||||
contextPreamble: ''
|
||||
contextPreamble: '',
|
||||
sendAllEnabledOnRefresh: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -663,13 +729,27 @@ function renderUserStatsTab() {
|
||||
|
||||
// Custom Stats section
|
||||
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">';
|
||||
|
||||
config.customStats.forEach((stat, index) => {
|
||||
const showMaxValue = statsDisplayMode === 'number';
|
||||
const maxValue = stat.maxValue || 100;
|
||||
html += `
|
||||
<div class="rpg-editor-stat-item" 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="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>
|
||||
</div>
|
||||
`;
|
||||
@@ -779,7 +859,8 @@ function setupUserStatsListeners() {
|
||||
extensionSettings.trackerConfig.userStats.customStats.push({
|
||||
id: newId,
|
||||
name: 'New Stat',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
maxValue: 100
|
||||
});
|
||||
// Initialize value if doesn't exist
|
||||
if (extensionSettings.userStats[newId] === undefined) {
|
||||
@@ -807,6 +888,19 @@ function setupUserStatsListeners() {
|
||||
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
|
||||
$('#rpg-add-attr').off('click').on('click', function() {
|
||||
// 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 += '<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="dd/mm/yyyy" ${config.widgets.date.format === 'dd/mm/yyyy' ? 'selected' : ''}>dd/mm/yyyy</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 += `<option value="Day (Numerical), Month, Year" ${config.widgets.date.format === 'Day (Numerical), Month, Year' ? 'selected' : ''}>Day (Numerical), Month, Year</option>`;
|
||||
html += '</select>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -1364,11 +1456,13 @@ function renderHistoryPersistenceTab() {
|
||||
enabled: false,
|
||||
messageCount: 5,
|
||||
injectionPosition: 'assistant_message_end',
|
||||
contextPreamble: ''
|
||||
contextPreamble: '',
|
||||
sendAllEnabledOnRefresh: false
|
||||
};
|
||||
const userStatsConfig = extensionSettings.trackerConfig.userStats;
|
||||
const infoBoxConfig = extensionSettings.trackerConfig.infoBox;
|
||||
const presentCharsConfig = extensionSettings.trackerConfig.presentCharacters;
|
||||
const generationMode = extensionSettings.generationMode || 'together';
|
||||
|
||||
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 += '</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
|
||||
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>`;
|
||||
@@ -1528,7 +1631,8 @@ function setupHistoryPersistenceListeners() {
|
||||
enabled: false,
|
||||
messageCount: 5,
|
||||
injectionPosition: 'assistant_message_end',
|
||||
contextPreamble: ''
|
||||
contextPreamble: '',
|
||||
externalApiOnly: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1537,6 +1641,11 @@ function setupHistoryPersistenceListeners() {
|
||||
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
|
||||
$('#rpg-history-message-count').off('change').on('change', function() {
|
||||
extensionSettings.historyPersistence.messageCount = parseInt($(this).val()) || 0;
|
||||
|
||||
@@ -4,9 +4,106 @@
|
||||
*/
|
||||
|
||||
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||
|
||||
let weatherContainer = 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
|
||||
@@ -136,22 +233,171 @@ function createMist() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sunshine rays effect
|
||||
* Calculate sun position based on hour (arc across sky)
|
||||
* Returns { left: vw%, top: dvh% }
|
||||
*/
|
||||
function createSunshine() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'rpg-weather-particles';
|
||||
function calculateSunPosition(hour) {
|
||||
// Daytime is roughly 6 AM to 8 PM (6-20)
|
||||
// Map hour to position along an arc
|
||||
// 6 AM = far left, low | 12 PM = center, high | 6 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: 5% to 85% (left to right)
|
||||
const left = 5 + progress * 80;
|
||||
|
||||
// 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 ~35% (low, near horizon)
|
||||
const normalizedProgress = (progress - 0.5) * 2; // -1 to 1
|
||||
const top = 8 + 27 * (normalizedProgress * normalizedProgress);
|
||||
|
||||
return { left, top };
|
||||
}
|
||||
|
||||
// Create 8 sun rays
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const ray = document.createElement('div');
|
||||
ray.className = 'rpg-weather-particle rpg-sunray';
|
||||
ray.style.left = `${10 + i * 12}%`;
|
||||
ray.style.animationDelay = `${i * 0.5}s`;
|
||||
ray.style.animationDuration = `${8 + Math.random() * 4}s`;
|
||||
container.appendChild(ray);
|
||||
/**
|
||||
* Create clear/sunny weather effect with floating particles and warm glow
|
||||
*/
|
||||
function createSunshine(hour) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'rpg-weather-particles rpg-clear-weather';
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
@@ -190,6 +436,75 @@ function createWind() {
|
||||
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
|
||||
*/
|
||||
@@ -198,11 +513,13 @@ function removeWeatherEffect() {
|
||||
weatherContainer.remove();
|
||||
weatherContainer = 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() {
|
||||
// Check if dynamic weather is enabled
|
||||
@@ -214,8 +531,21 @@ export function updateWeatherEffect() {
|
||||
const weather = getCurrentWeather();
|
||||
const weatherType = parseWeatherType(weather);
|
||||
|
||||
// Don't recreate if weather hasn't changed
|
||||
if (weatherType === currentWeatherType) {
|
||||
// Get current time of day
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -228,6 +558,8 @@ export function updateWeatherEffect() {
|
||||
}
|
||||
|
||||
currentWeatherType = weatherType;
|
||||
currentTimeOfDay = timeOfDay;
|
||||
currentHour = hour;
|
||||
|
||||
switch (weatherType) {
|
||||
case 'snow':
|
||||
@@ -240,7 +572,12 @@ export function updateWeatherEffect() {
|
||||
weatherContainer = createMist();
|
||||
break;
|
||||
case 'sunny':
|
||||
weatherContainer = createSunshine();
|
||||
// Use nighttime effect for clear weather at night
|
||||
if (timeOfDay === 'night') {
|
||||
weatherContainer = createNighttime(hour);
|
||||
} else {
|
||||
weatherContainer = createSunshine(hour);
|
||||
}
|
||||
break;
|
||||
case 'wind':
|
||||
weatherContainer = createWind();
|
||||
@@ -270,6 +607,18 @@ export function updateWeatherEffect() {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+109
@@ -4,6 +4,46 @@
|
||||
<i class="fa-solid fa-chevron-right"></i>
|
||||
</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 -->
|
||||
<div class="rpg-game-container">
|
||||
<!-- Header with Controls -->
|
||||
@@ -375,6 +415,25 @@
|
||||
Display a toggle button to enable/disable animated weather effects.
|
||||
</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">
|
||||
<input type="checkbox" id="rpg-toggle-show-narrator-mode" />
|
||||
<span data-i18n-key="template.settingsModal.display.showNarratorMode">Show Narrator Mode</span>
|
||||
@@ -486,6 +545,56 @@
|
||||
</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">
|
||||
<h4 data-i18n-key="template.settingsModal.advancedTitle"><i class="fa-solid fa-sliders"
|
||||
aria-hidden="true"></i> Advanced</h4>
|
||||
|
||||
Reference in New Issue
Block a user