Compare commits

..

7 Commits

23 changed files with 516 additions and 431 deletions
+5 -2
View File
@@ -7,10 +7,13 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New
### v3.6.2
### v3.7.0
- Added omniscience filter.
- Added opacity to the color selector.
- Overwritten SillyTavern's dumb-ahh trim logic when joining prompts.
- Fixed custom attributes not allowing value increase/decrease.
- Various bug fixes.
- Added the ability to add present characters manually.
**Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
+100
View File
@@ -384,6 +384,11 @@ async function initUI() {
saveSettings();
});
$('#rpg-toggle-omniscience').on('change', function() {
extensionSettings.enableOmniscienceFilter = $(this).prop('checked');
saveSettings();
});
$('#rpg-toggle-cyoa').on('change', function() {
extensionSettings.enableCYOA = $(this).prop('checked');
saveSettings();
@@ -572,6 +577,12 @@ async function initUI() {
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-omniscience-toggle').on('change', function() {
extensionSettings.showOmniscienceToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-cyoa-toggle').on('change', function() {
extensionSettings.showCYOAToggle = $(this).prop('checked');
saveSettings();
@@ -805,12 +816,30 @@ async function initUI() {
renderUserStats(); // Re-render with new colors
});
$('#rpg-stat-bar-color-low-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.statBarColorLowOpacity = opacity;
$('#rpg-stat-bar-color-low-opacity-value').text(opacity + '%');
renderUserStats();
}).on('change', function() {
saveSettings();
});
$('#rpg-stat-bar-color-high').on('change', function() {
extensionSettings.statBarColorHigh = String($(this).val());
saveSettings();
renderUserStats(); // Re-render with new colors
});
$('#rpg-stat-bar-color-high-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.statBarColorHighOpacity = opacity;
$('#rpg-stat-bar-color-high-opacity-value').text(opacity + '%');
renderUserStats();
}).on('change', function() {
saveSettings();
});
// Theme selection
$('#rpg-theme-select').on('change', function() {
extensionSettings.theme = String($(this).val());
@@ -832,6 +861,19 @@ async function initUI() {
}
});
$('#rpg-custom-bg-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.bgOpacity = opacity;
$('#rpg-custom-bg-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-accent').on('change', function() {
extensionSettings.customColors.accent = String($(this).val());
saveSettings();
@@ -842,6 +884,19 @@ async function initUI() {
}
});
$('#rpg-custom-accent-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.accentOpacity = opacity;
$('#rpg-custom-accent-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-text').on('change', function() {
extensionSettings.customColors.text = String($(this).val());
saveSettings();
@@ -852,6 +907,19 @@ async function initUI() {
}
});
$('#rpg-custom-text-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.textOpacity = opacity;
$('#rpg-custom-text-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-highlight').on('change', function() {
extensionSettings.customColors.highlight = String($(this).val());
saveSettings();
@@ -862,6 +930,19 @@ async function initUI() {
}
});
$('#rpg-custom-highlight-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.highlightOpacity = opacity;
$('#rpg-custom-highlight-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
// External API settings event handlers
$('#rpg-external-base-url').on('change', function() {
if (!extensionSettings.externalApiSettings) {
@@ -969,6 +1050,7 @@ async function initUI() {
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
$('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring);
$('#rpg-toggle-deception').prop('checked', extensionSettings.enableDeceptionSystem ?? false);
$('#rpg-toggle-omniscience').prop('checked', extensionSettings.enableOmniscienceFilter ?? false);
$('#rpg-toggle-cyoa').prop('checked', extensionSettings.enableCYOA ?? false);
$('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic);
@@ -979,6 +1061,7 @@ async function initUI() {
$('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true);
$('#rpg-toggle-show-dialogue-coloring-toggle').prop('checked', extensionSettings.showDialogueColoringToggle ?? true);
$('#rpg-toggle-show-deception-toggle').prop('checked', extensionSettings.showDeceptionToggle ?? true);
$('#rpg-toggle-show-omniscience-toggle').prop('checked', extensionSettings.showOmniscienceToggle ?? true);
$('#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);
@@ -1041,12 +1124,29 @@ async function initUI() {
$('#rpg-strip-widget-options').toggle(stripWidgets.enabled || false);
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
$('#rpg-stat-bar-color-low-opacity').val(extensionSettings.statBarColorLowOpacity ?? 100);
$('#rpg-stat-bar-color-low-opacity-value').text((extensionSettings.statBarColorLowOpacity ?? 100) + '%');
$('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh);
$('#rpg-stat-bar-color-high-opacity').val(extensionSettings.statBarColorHighOpacity ?? 100);
$('#rpg-stat-bar-color-high-opacity-value').text((extensionSettings.statBarColorHighOpacity ?? 100) + '%');
$('#rpg-theme-select').val(extensionSettings.theme);
$('#rpg-custom-bg').val(extensionSettings.customColors.bg);
$('#rpg-custom-bg-opacity').val(extensionSettings.customColors.bgOpacity ?? 100);
$('#rpg-custom-bg-opacity-value').text((extensionSettings.customColors.bgOpacity ?? 100) + '%');
$('#rpg-custom-accent').val(extensionSettings.customColors.accent);
$('#rpg-custom-accent-opacity').val(extensionSettings.customColors.accentOpacity ?? 100);
$('#rpg-custom-accent-opacity-value').text((extensionSettings.customColors.accentOpacity ?? 100) + '%');
$('#rpg-custom-text').val(extensionSettings.customColors.text);
$('#rpg-custom-text-opacity').val(extensionSettings.customColors.textOpacity ?? 100);
$('#rpg-custom-text-opacity-value').text((extensionSettings.customColors.textOpacity ?? 100) + '%');
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
$('#rpg-custom-highlight-opacity').val(extensionSettings.customColors.highlightOpacity ?? 100);
$('#rpg-custom-highlight-opacity-value').text((extensionSettings.customColors.highlightOpacity ?? 100) + '%');
// Initialize External API settings values
if (extensionSettings.externalApiSettings) {
+1 -1
View File
@@ -6,6 +6,6 @@
"js": "index.js",
"css": "style.css",
"author": "Marinara",
"version": "3.6.3",
"version": "3.7.0",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
}
+1 -1
View File
@@ -49,7 +49,7 @@
</div>
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
v3.6.2
v3.7.0
</div>
</div>
</div>
+16
View File
@@ -118,6 +118,22 @@ export function loadSettings() {
settingsChanged = true;
}
// Migration to version 5: Add opacity properties for all colors
if (currentVersion < 5) {
// console.log('[RPG Companion] Migrating settings to version 5 (adding color opacity)');
if (!extensionSettings.customColors) {
extensionSettings.customColors = {};
}
if (extensionSettings.customColors.bgOpacity === undefined) extensionSettings.customColors.bgOpacity = 100;
if (extensionSettings.customColors.accentOpacity === undefined) extensionSettings.customColors.accentOpacity = 100;
if (extensionSettings.customColors.textOpacity === undefined) extensionSettings.customColors.textOpacity = 100;
if (extensionSettings.customColors.highlightOpacity === undefined) extensionSettings.customColors.highlightOpacity = 100;
if (extensionSettings.statBarColorLowOpacity === undefined) extensionSettings.statBarColorLowOpacity = 100;
if (extensionSettings.statBarColorHighOpacity === undefined) extensionSettings.statBarColorHighOpacity = 100;
extensionSettings.settingsVersion = 5;
settingsChanged = true;
}
// Save migrated settings
if (settingsChanged) {
saveSettings();
+11 -1
View File
@@ -23,12 +23,15 @@ export let extensionSettings = {
showThoughtsInChat: true, // Show thoughts overlay in chat
narratorMode: false, // Use character card as narrator instead of fixed character references
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default)
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
enableDialogueColoring: false, // Enable dialogue coloring prompt injection
customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default)
enableDeceptionSystem: false, // Enable deception tracking with <lie> tags
customDeceptionPrompt: '', // Custom deception prompt text (empty = use default)
enableOmniscienceFilter: false, // Enable omniscience filter with <filter> tags
customOmnisciencePrompt: '', // Custom omniscience filter prompt text (empty = use default)
enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices
customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default)
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
@@ -41,6 +44,7 @@ export let extensionSettings = {
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default)
showDeceptionToggle: true, // Show Deception System toggle in main panel
showOmniscienceToggle: true, // Show Omniscience Filter toggle in main panel
showCYOAToggle: true, // Show CYOA toggle in main panel
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
@@ -62,12 +66,18 @@ export let extensionSettings = {
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
customColors: {
bg: '#1a1a2e',
bgOpacity: 100,
accent: '#16213e',
accentOpacity: 100,
text: '#eaeaea',
highlight: '#e94560'
textOpacity: 100,
highlight: '#e94560',
highlightOpacity: 100
},
statBarColorLow: '#cc3333', // Color for low stat values (red)
statBarColorLowOpacity: 100,
statBarColorHigh: '#33cc66', // Color for high stat values (green)
statBarColorHighOpacity: 100,
enableAnimations: true, // Enable smooth animations for stats and content updates
mobileFabPosition: {
top: 'calc(var(--topBarBlockSize) + 60px)',
+6
View File
@@ -52,6 +52,10 @@
"template.settingsModal.display.showImmersiveHtmlToggleNote": "Display a toggle button to enable/disable HTML formatting in messages.",
"template.settingsModal.display.showDialogueColoringToggle": "Show Colored Dialogues",
"template.settingsModal.display.showDialogueColoringToggleNote": "Display a toggle button to enable/disable colored dialogue formatting.",
"template.settingsModal.display.showDeceptionToggle": "Show Deception System",
"template.settingsModal.display.showDeceptionToggleNote": "Display a toggle button to enable/disable the Deception System for marking lies and deceptions.",
"template.settingsModal.display.showOmniscienceToggle": "Show Omniscience Filter",
"template.settingsModal.display.showOmniscienceToggleNote": "Display a toggle button to enable/disable the Omniscience Filter for filtering hidden events.",
"template.settingsModal.display.showSpotifyMusicToggle": "Show Spotify Music",
"template.settingsModal.display.showSpotifyMusicToggleNote": "Display Spotify music player with AI-suggested scene-appropriate tracks.",
"template.settingsModal.display.showSnowflakesToggle": "Show Snowflakes Effect",
@@ -162,6 +166,8 @@
"template.mainPanel.clearLastRoll": "Clear last roll",
"template.mainPanel.immersiveHtml": "Immersive HTML",
"template.mainPanel.coloredDialogues": "Colored Dialogues",
"template.mainPanel.deceptionSystem": "Deception System",
"template.mainPanel.omniscienceFilter": "Omniscience Filter",
"template.mainPanel.spotifyMusic": "Spotify Music",
"template.mainPanel.snowflakesEffect": "Snowflakes Effect",
"template.mainPanel.dynamicWeatherEffects": "Dynamic Weather",
+8
View File
@@ -20,6 +20,10 @@ export function setupClassicStatsButtons() {
// Delegated event listener for increase buttons
$userStatsContainer.on('click', '.rpg-stat-increase', function() {
const stat = $(this).data('stat');
// Initialize custom attributes if they don't exist
if (extensionSettings.classicStats[stat] === undefined) {
extensionSettings.classicStats[stat] = 10;
}
if (extensionSettings.classicStats[stat] < 999) {
extensionSettings.classicStats[stat]++;
saveSettings();
@@ -33,6 +37,10 @@ export function setupClassicStatsButtons() {
// Delegated event listener for decrease buttons
$userStatsContainer.on('click', '.rpg-stat-decrease', function() {
const stat = $(this).data('stat');
// Initialize custom attributes if they don't exist
if (extensionSettings.classicStats[stat] === undefined) {
extensionSettings.classicStats[stat] = 10;
}
if (extensionSettings.classicStats[stat] > 1) {
extensionSettings.classicStats[stat]--;
saveSettings();
+1 -1
View File
@@ -76,7 +76,7 @@ export async function ensureJsonCleaningRegex(st_extension_settings, saveSetting
}
// Small delay to ensure save completes
await new Promise(resolve => setTimeout(resolve, 100));
console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.6 settings.');
console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.3 settings.');
} else {
console.log('[RPG Companion] JSON Cleaning Regex is up to date.');
}
+66 -21
View File
@@ -4,7 +4,7 @@
*/
import { getContext } from '../../../../../../extensions.js';
import { setExtensionPrompt, extension_prompt_types, extension_prompt_roles, eventSource, event_types } from '../../../../../../../script.js';
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, eventSource, event_types } from '../../../../../../../script.js';
import {
extensionSettings,
committedTrackerData,
@@ -22,8 +22,11 @@ import {
DEFAULT_HTML_PROMPT,
DEFAULT_DIALOGUE_COLORING_PROMPT,
DEFAULT_DECEPTION_PROMPT,
DEFAULT_OMNISCIENCE_FILTER_PROMPT,
DEFAULT_CYOA_PROMPT,
DEFAULT_SPOTIFY_PROMPT,
DEFAULT_NARRATOR_PROMPT,
DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT,
SPOTIFY_FORMAT_INSTRUCTION
} from './promptBuilder.js';
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
@@ -478,6 +481,7 @@ function onGenerateBeforeCombinePrompts(eventData) {
/**
* Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion).
* This is now a backup/fallback - primary injection happens in BEFORE_COMBINE.
* Also fixes newline spacing after </context> tag.
*
* @param {Object} eventData - Event data with prompt property
*/
@@ -490,24 +494,30 @@ function onGenerateAfterCombinePrompts(eventData) {
return;
}
// Skip if injection already happened in BEFORE_COMBINE
if (historyInjectionDone) {
return;
let didInjectHistory = false;
// Inject historical context if available and not already done
if (!historyInjectionDone && pendingContextMap.size > 0) {
// Fallback injection for edge cases where BEFORE_COMBINE didn't work
console.log('[RPG Companion] Using fallback string-based injection (AFTER_COMBINE)');
eventData.prompt = injectContextIntoTextPrompt(eventData.prompt);
didInjectHistory = true;
}
// Only inject if we have pending context
if (pendingContextMap.size === 0) {
return;
}
// Always fix newlines around context tags (whether we just injected or not)
eventData.prompt = eventData.prompt.replace(/<context>/g, '\n<context>');
eventData.prompt = eventData.prompt.replace(/<\/context>/g, '</context>\n');
// 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);
// Remove extra newlines after last_message opening and closing tags
// Match exactly the double newline pattern
eventData.prompt = eventData.prompt.replace(/<last_message>\n\n/g, '<last_message>\n');
eventData.prompt = eventData.prompt.replace(/\n\n<\/last_message>/g, '\n</last_message>');
}
/**
* Event handler for CHAT_COMPLETION_PROMPT_READY.
* Injects historical context into the chat message array.
* Also fixes newline spacing around <context> tags.
*
* @param {Object} eventData - Event data with chat property
*/
@@ -520,14 +530,20 @@ function onChatCompletionPromptReady(eventData) {
return;
}
// Only inject if we have pending context
if (pendingContextMap.size === 0) {
return;
// Inject historical context if we have pending context
if (pendingContextMap.size > 0) {
eventData.chat = injectContextIntoChatPrompt(eventData.chat);
// DON'T clear pendingContextMap here - let it persist for other generations
// (e.g., prewarm extensions). It will be cleared on GENERATION_ENDED.
}
eventData.chat = injectContextIntoChatPrompt(eventData.chat);
// DON'T clear pendingContextMap here - let it persist for other generations
// (e.g., prewarm extensions). It will be cleared on GENERATION_ENDED.
// Fix newlines around context tags for all messages
for (const message of eventData.chat) {
if (message.content && typeof message.content === 'string') {
message.content = message.content.replace(/<context>/g, '\n<context>');
message.content = message.content.replace(/<\/context>/g, '</context>\n');
}
}
}
/**
@@ -792,6 +808,19 @@ export async function onGenerationStarted(type, data, dryRun) {
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
}
// Inject Omniscience Filter prompt separately at depth 0 if enabled
if (extensionSettings.enableOmniscienceFilter && !shouldSuppress) {
// Use custom Omniscience Filter prompt if set, otherwise use default
const omnisciencePromptText = extensionSettings.customOmnisciencePrompt || DEFAULT_OMNISCIENCE_FILTER_PROMPT;
const omnisciencePrompt = `\n${omnisciencePromptText}\n`;
setExtensionPrompt('rpg-companion-omniscience', omnisciencePrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected Omniscience Filter prompt at depth 0 for together mode');
} else {
// Clear Omniscience Filter prompt if disabled
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
}
// Inject Spotify prompt separately at depth 0 if enabled
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
// Use custom Spotify prompt if set, otherwise use default
@@ -823,12 +852,14 @@ export async function onGenerationStarted(type, data, dryRun) {
const contextSummary = generateContextualSummary();
if (contextSummary) {
const wrappedContext = `\nHere is context information about the current scene, and what follows is the last message in the chat history:
// Use custom context instructions prompt if set, otherwise use default
const contextInstructionsText = extensionSettings.customContextInstructionsPrompt || DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT;
const wrappedContext = `
<context>
${contextSummary}
Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses.
</context>\n\n`;
${contextInstructionsText}
</context>`;
// Inject context at depth 1 (before last user message) as SYSTEM
// Skip when a guided generation injection is present to avoid conflicting instructions
@@ -880,6 +911,19 @@ Ensure these details naturally reflect and influence the narrative. Character be
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
}
// Inject Omniscience Filter prompt separately at depth 0 if enabled
if (extensionSettings.enableOmniscienceFilter && !shouldSuppress) {
// Use custom Omniscience Filter prompt if set, otherwise use default
const omnisciencePromptText = extensionSettings.customOmnisciencePrompt || DEFAULT_OMNISCIENCE_FILTER_PROMPT;
const omnisciencePrompt = `\n${omnisciencePromptText}\n`;
setExtensionPrompt('rpg-companion-omniscience', omnisciencePrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected Omniscience Filter prompt at depth 0 for separate/external mode');
} else {
// Clear Omniscience Filter prompt if disabled
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
}
// Inject Spotify prompt separately at depth 0 if enabled
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
// Use custom Spotify prompt if set, otherwise use default
@@ -917,6 +961,7 @@ Ensure these details naturally reflect and influence the narrative. Character be
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-zzz-cyoa', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
}
+7
View File
@@ -617,6 +617,13 @@ export function parseUserStats(statsText) {
if (!quest) return '';
if (typeof quest === 'string') return quest;
if (typeof quest === 'object') {
// Check for locked format: {value, locked}
// Recursively extract value if it's nested
let extracted = quest;
while (typeof extracted === 'object' && extracted.value !== undefined) {
extracted = extracted.value;
}
if (typeof extracted === 'string') return extracted;
// v3 format: {title, description, status}
return quest.title || quest.description || JSON.stringify(quest);
}
+19 -6
View File
@@ -33,6 +33,15 @@ export const DEFAULT_DIALOGUE_COLORING_PROMPT = `Wrap all character/NPC "dialogu
*/
export const DEFAULT_DECEPTION_PROMPT = `When a character is lying or deceiving, you should follow up that line with the <lie> tag, containing a brief description of the truth and the lie's reason, using the template below (replace placeholders in quotation marks). This will be hidden from the user's view, but not to you, making it useful for future consequences: <lie character="name" type="lying/deceiving/omitting" truth="truth" reason="reason"/>.`;
/**
* Default Omniscience Filter prompt text
* This instructs the AI to separate information the player character cannot perceive
*/
export const DEFAULT_OMNISCIENCE_FILTER_PROMPT = `You must strictly separate what the player can directly perceive from what they cannot. They should only read limited narrative content that their persona can actually see, hear, smell, touch, or otherwise directly sense. Before writing any narrative content that involves events, actions, or details the player directly cannot perceive (because they're not looking, too far away, behind them, in another room, happening silently, include NPCs' internal thoughts, etc.), you absolutely must output that hidden information inside a <filter> tag using this exact format:
<filter event="[Brief description of what is happening that the player cannot perceive]" reason="[Why the player character cannot perceive this - e.g., 'behind them', 'in another room', 'too quiet to hear', 'focused elsewhere']"/>
Example: <filter event="Zandik quietly takes the key from the table and slips out the back door" reason="Zandik is behind Mari, who is absorbed in reading, and he moves silently"/> You hear a faint click from somewhere behind you, but when you glance up from your newspaper, the room seems unchanged.`;
/**
* Default CYOA prompt text
*/
@@ -53,6 +62,11 @@ export const SPOTIFY_FORMAT_INSTRUCTION = `Include it in this exact format: <spo
*/
export const DEFAULT_NARRATOR_PROMPT = `Infer the identity and details of characters present in each scene from the story context below. Do not use fixed character references; instead, identify characters naturally based on their actions, dialogue, and descriptions in the narrative.`;
/**
* Default Context Instructions prompt text (customizable by users)
*/
export const DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT = `The context above is information about the current scene, and what follows is the last message in the chat history. Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses.`;
/**
* Gets character card information for current chat (handles both single and group chats)
* @returns {string} Formatted character information
@@ -725,14 +739,13 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
}
}
// Relationship - check both Relationship (new format) and relationship (old format)
const relationshipValue = char.Relationship || char.relationship;
if (relationshipValue) {
// Relationship
if (char.relationship) {
let relValue;
if (typeof relationshipValue === 'object' && !Array.isArray(relationshipValue) && 'status' in relationshipValue) {
relValue = getValue(relationshipValue.status);
if (typeof char.relationship === 'object' && !Array.isArray(char.relationship) && 'status' in char.relationship) {
relValue = getValue(char.relationship.status);
} else {
relValue = getValue(relationshipValue);
relValue = getValue(char.relationship);
}
if (relValue) formatted += ` Relationship: ${relValue}\n`;
}
+4 -11
View File
@@ -395,20 +395,13 @@ export function onMessageSwiped(messageIndex) {
// Load swipe data into lastGeneratedData for display (both modes)
lastGeneratedData.userStats = swipeData.userStats || null;
lastGeneratedData.infoBox = swipeData.infoBox || null;
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
// Normalize characterThoughts to string format (for backward compatibility with old object format)
if (swipeData.characterThoughts && typeof swipeData.characterThoughts === 'object') {
lastGeneratedData.characterThoughts = JSON.stringify(swipeData.characterThoughts, null, 2);
} else {
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
// Parse user stats if available
if (swipeData.userStats) {
parseUserStats(swipeData.userStats);
}
// DON'T parse user stats when loading swipe data
// This would overwrite manually edited fields (like Conditions) with old swipe data
// The lastGeneratedData is loaded for display purposes only
// parseUserStats() updates extensionSettings.userStats which should only be modified
// by new generations or manual edits, not by swipe navigation
// console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId);
} else {
// console.log('[RPG Companion] ️ No stored data for swipe:', currentSwipeId);
+6 -2
View File
@@ -212,8 +212,12 @@ export function renderQuests() {
// Get current sub-tab from container or default to 'main'
const activeSubTab = $questsContainer.data('active-subtab') || 'main';
// Get quests data
const mainQuest = extensionSettings.quests.main || 'None';
// Get quests data - extract value if it's a locked object
let mainQuest = extensionSettings.quests.main || 'None';
// Recursively extract value if it's nested objects
while (typeof mainQuest === 'object' && mainQuest.value !== undefined) {
mainQuest = mainQuest.value;
}
const optionalQuests = extensionSettings.quests.optional || [];
// Build HTML
+49 -258
View File
@@ -50,9 +50,11 @@ function debugLog(message, data = null) {
* @param {number} percentage - Value from 0-100
* @param {string} lowColor - Hex color for low values (e.g., '#ff0000')
* @param {string} highColor - Hex color for high values (e.g., '#00ff00')
* @returns {string} Interpolated hex color
* @param {number} lowOpacity - Opacity for low values (0-100)
* @param {number} highOpacity - Opacity for high values (0-100)
* @returns {string} Interpolated rgba color
*/
function getStatColor(percentage, lowColor, highColor) {
function getStatColor(percentage, lowColor, highColor, lowOpacity = 100, highOpacity = 100) {
// Clamp percentage to 0-100
const percent = Math.max(0, Math.min(100, percentage)) / 100;
@@ -73,10 +75,9 @@ function getStatColor(percentage, lowColor, highColor) {
const r = Math.round(low.r + (high.r - low.r) * percent);
const g = Math.round(low.g + (high.g - low.g) * percent);
const b = Math.round(low.b + (high.b - low.b) * percent);
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
// Convert back to hex
const toHex = (n) => n.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
/**
@@ -512,20 +513,17 @@ export function renderThoughts() {
const fieldNameLower = field.name.toLowerCase();
// Skip lock icons for thoughts field
const showLock = !fieldNameLower.includes('thought');
// Add placeholder for empty fields
const placeholder = fieldValue ? '' : `data-placeholder="${field.name}"`;
const emptyClass = fieldValue ? '' : ' rpg-empty-field';
if (showLock) {
const lockIconHtml = getLockIconHtml('characters', `${char.name}.${field.name}`);
html += `
<div class="rpg-character-field rpg-character-${fieldId}" style="position: relative;">
${lockIconHtml}
<span class="rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}" ${placeholder}>${fieldValue}</span>
<span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</span>
</div>
`;
} else {
html += `
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}" ${placeholder}>${fieldValue}</div>
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</div>
`;
}
}
@@ -542,7 +540,13 @@ export function renderThoughts() {
<div class="rpg-character-stats-inner">`;
for (const stat of enabledCharStats) {
const statValue = char[stat.name] || 0;
const statColor = getStatColor(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh);
const statColor = getStatColor(
statValue,
extensionSettings.statBarColorLow,
extensionSettings.statBarColorHigh,
extensionSettings.statBarColorLowOpacity ?? 100,
extensionSettings.statBarColorHighOpacity ?? 100
);
html += `
<div class="rpg-character-stat">
<span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" style="color: ${statColor}" title="Click to edit ${stat.name}">${statValue}%</span>
@@ -567,16 +571,6 @@ export function renderThoughts() {
}
debugLog('[RPG Thoughts] Finished building all character cards');
// Add "Add Character" button if data exists (inside rpg-thoughts-content)
if (presentCharacters.length > 0) {
html += `
<button class="rpg-add-character-btn" title="Add a new character">
<i class="fa-solid fa-plus"></i> Add Character
</button>
`;
}
html += '</div>';
}
@@ -675,31 +669,6 @@ export function renderThoughts() {
fileInput.trigger('click');
});
// Add event listener for "Add Character" button (support both click and touch for mobile)
$thoughtsContainer.find('.rpg-add-character-btn').on('click touchend', function(e) {
e.preventDefault();
e.stopPropagation();
addNewCharacter();
});
// Handle empty field focus - remove placeholder styling on focus
$thoughtsContainer.find('.rpg-editable.rpg-empty-field').on('focus', function() {
$(this).removeClass('rpg-empty-field');
$(this).removeAttr('data-placeholder');
});
// Restore placeholder if field becomes empty on blur (after the main blur handler)
$thoughtsContainer.find('.rpg-editable').on('blur', function() {
const $this = $(this);
if (!$this.text().trim()) {
const field = $this.data('field');
if (field) {
$this.addClass('rpg-empty-field');
$this.attr('data-placeholder', field);
}
}
});
// Remove updating class after animation
if (extensionSettings.enableAnimations) {
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
@@ -826,136 +795,6 @@ export function removeCharacter(characterName) {
renderThoughts();
}
/**
* Adds a new blank character to Present Characters data.
* Creates a character with empty fields based on the tracker template.
*/
export function addNewCharacter() {
const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters;
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
const characterStats = presentCharsConfig?.characterStats;
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
const hasRelationship = presentCharsConfig?.relationshipFields?.length > 0;
// Check if data is in JSON format
let isJSON = false;
let parsedData = null;
try {
parsedData = typeof lastGeneratedData.characterThoughts === 'string'
? JSON.parse(lastGeneratedData.characterThoughts)
: lastGeneratedData.characterThoughts;
if (Array.isArray(parsedData) || (parsedData && parsedData.characters)) {
isJSON = true;
}
} catch (e) {
// Not JSON, treat as text format
}
if (isJSON) {
// JSON format - add new character object
const charactersArray = Array.isArray(parsedData) ? parsedData : (parsedData.characters || []);
const newCharacter = {
name: 'New Character',
emoji: '👤',
details: {}
};
// Add all enabled custom fields as empty
for (const field of enabledFields) {
newCharacter.details[field.name] = '';
}
// Add relationship if enabled
if (hasRelationship) {
newCharacter.relationship = 'Neutral';
}
// Add stats if enabled
if (enabledCharStats.length > 0) {
newCharacter.stats = {};
for (const stat of enabledCharStats) {
newCharacter.stats[stat.name] = 100;
}
}
charactersArray.push(newCharacter);
// Save back as JSON string
lastGeneratedData.characterThoughts = JSON.stringify(
Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray },
null,
2
);
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
} else {
// Text format - add new character block
const lines = lastGeneratedData.characterThoughts.split('\n');
const dividerIndex = lines.findIndex(line => line.includes('---'));
if (dividerIndex >= 0) {
const newCharacterLines = ['- New Character'];
// Add custom detail fields as standalone lines
for (const customField of enabledFields) {
newCharacterLines.push(` ${customField.name}: `);
}
// Add Relationship field if enabled
if (hasRelationship) {
newCharacterLines.push(` Relationship: Neutral`);
}
// Add Stats if enabled
if (enabledCharStats.length > 0) {
const statsParts = enabledCharStats.map(s => `${s.name}: 100%`);
newCharacterLines.push(` Stats: ${statsParts.join(' | ')}`);
}
// Find the last character and add after it, or after divider if no characters
let insertIndex = dividerIndex + 1;
for (let i = lines.length - 1; i > dividerIndex; i--) {
if (lines[i].trim().startsWith('- ')) {
// Find the end of this character block
insertIndex = i + 1;
while (insertIndex < lines.length && lines[insertIndex].trim() && !lines[insertIndex].trim().startsWith('- ')) {
insertIndex++;
}
break;
}
}
lines.splice(insertIndex, 0, ...newCharacterLines);
lastGeneratedData.characterThoughts = lines.join('\n');
committedTrackerData.characterThoughts = lines.join('\n');
}
}
// Update message swipe data
const chat = getContext().chat;
if (chat && chat.length > 0) {
for (let i = chat.length - 1; i >= 0; i--) {
const message = chat[i];
if (!message.is_user) {
if (message.extra && message.extra.rpg_companion_swipes) {
const swipeId = message.swipe_id || 0;
if (message.extra.rpg_companion_swipes[swipeId]) {
message.extra.rpg_companion_swipes[swipeId].characterThoughts = lastGeneratedData.characterThoughts;
}
}
break;
}
}
}
saveChatData();
// Re-render to show new character
renderThoughts();
}
/**
* Updates a specific character field in Present Characters data and re-renders.
* Works with the new multi-line format.
@@ -1023,27 +862,18 @@ export function updateCharacterField(characterName, field, value) {
} else if (field === 'emoji') {
char.emoji = value;
} else if (field === 'Relationship') {
// Store relationship in the correct nested format
// Remove old flat format if it exists
if (char.Relationship) {
delete char.Relationship;
}
// Store relationship as text, converting emoji if needed
// First check if it's an emoji → convert to text
let relationshipValue;
if (emojiToRelationship[value]) {
relationshipValue = emojiToRelationship[value];
char.Relationship = emojiToRelationship[value];
} else {
// It's text - find matching relationship name (case-insensitive)
const matchingRelationship = Object.keys(relationshipEmojis).find(
name => name.toLowerCase() === value.toLowerCase()
);
relationshipValue = matchingRelationship || value;
char.Relationship = matchingRelationship || value;
}
// Store in the correct nested format
char.relationship = { status: relationshipValue };
// console.log('[RPG Companion] After update - char.relationship:', char.relationship);
// console.log('[RPG Companion] After update - char.Relationship:', char.Relationship);
// console.log('[RPG Companion] relationshipEmojis:', relationshipEmojis);
// console.log('[RPG Companion] emojiToRelationship:', emojiToRelationship);
} else if (field.toLowerCase() === 'thoughts' || field === (presentCharsConfig?.thoughts?.name || 'Thoughts')) {
@@ -1059,44 +889,15 @@ export function updateCharacterField(characterName, field, value) {
numValue = Math.max(0, Math.min(100, numValue));
char.stats[field] = numValue;
} else {
// It's a custom detail field - store in details object
// It's a custom detail field
if (!char.details) char.details = {};
char.details[field] = value;
// Clean up snake_case version if it exists (from AI generation)
const fieldKey = toSnakeCase(field);
if (fieldKey !== field && char.details[fieldKey] !== undefined) {
delete char.details[fieldKey];
}
// Clean up old root-level field if it exists (from v2 format)
if (char[field] !== undefined && field !== 'name' && field !== 'emoji') {
delete char[field];
}
if (char[fieldKey] !== undefined && fieldKey !== 'name' && fieldKey !== 'emoji') {
delete char[fieldKey];
}
}
}
// Clean up ALL duplicate snake_case fields in details (not just the edited field)
// This prevents duplicates from AI-generated data
if (char.details) {
for (const customField of enabledFields) {
const fieldName = customField.name;
const snakeCaseKey = toSnakeCase(fieldName);
// If both versions exist, keep the properly-cased one and remove snake_case
if (snakeCaseKey !== fieldName &&
char.details[fieldName] !== undefined &&
char.details[snakeCaseKey] !== undefined) {
delete char.details[snakeCaseKey];
}
}
}
}
// Save back to lastGeneratedData as JSON string (consistent with infoBox and userStats)
lastGeneratedData.characterThoughts = JSON.stringify(Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray }, null, 2);
// Save back to lastGeneratedData
lastGeneratedData.characterThoughts = Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray };
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
// console.log('[RPG Companion] Saved to lastGeneratedData.characterThoughts:', JSON.stringify(lastGeneratedData.characterThoughts));
@@ -1177,9 +978,6 @@ export function updateCharacterField(characterName, field, value) {
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
const isThoughtsField = field.toLowerCase() === 'thoughts' || field === thoughtsFieldName;
// Track if field was found and updated
let fieldUpdated = false;
// First pass: check if Stats line exists and update other fields
for (let i = characterStartIndex; i < characterEndIndex; i++) {
const line = lines[i].trim();
@@ -1187,37 +985,35 @@ export function updateCharacterField(characterName, field, value) {
if (line.startsWith('Stats:')) {
statsLineExists = true;
statsLineIndex = i;
continue; // Skip to next line
}
// Check for name update
if (field === 'name' && line.startsWith('- ')) {
lines[i] = `- ${value}`;
fieldUpdated = true;
continue;
}
// Check for Relationship field
if (field === 'Relationship' && line.startsWith('Relationship:')) {
else if (field === 'emoji' && line.startsWith('Details:')) {
const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim());
parts[0] = value;
lines[i] = `Details: ${parts.join(' | ')}`;
}
else if (line.startsWith('Details:')) {
const fieldIndex = enabledFields.findIndex(f => f.name === field);
if (fieldIndex !== -1) {
const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim());
if (parts.length > fieldIndex + 1) {
parts[fieldIndex + 1] = value;
lines[i] = `Details: ${parts.join(' | ')}`;
}
}
}
else if (field === 'Relationship' && line.startsWith('Relationship:')) {
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
const relationshipValue = emojiToRelationship[value] || value;
lines[i] = `Relationship: ${relationshipValue}`;
fieldUpdated = true;
continue;
}
// Check for Thoughts field
if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
lines[i] = ` ${thoughtsFieldName}: ${value}`;
fieldUpdated = true;
continue;
}
// Check for v3 text format standalone field lines (e.g., "Appearance: ...", "Demeanor: ...")
if (line.startsWith(field + ':')) {
lines[i] = ` ${field}: ${value}`;
fieldUpdated = true;
// Don't break - update ALL instances of this field (in case of duplicates from previous bugs)
else if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
// Update thoughts field
lines[i] = `${thoughtsFieldName}: ${value}`;
// console.log('[RPG Companion] Updated thoughts:', lines[i]);
}
}
@@ -1284,28 +1080,23 @@ export function updateCharacterField(characterName, field, value) {
}
}
} else {
// Create new character block (v3 text format only)
// Create new character block
const dividerIndex = lines.findIndex(line => line.includes('---'));
if (dividerIndex >= 0) {
const newCharacterLines = [`- ${characterName}`];
// Add custom detail fields as standalone lines
for (const customField of enabledFields) {
if (field === customField.name) {
newCharacterLines.push(` ${customField.name}: ${value}`);
} else {
newCharacterLines.push(` ${customField.name}: `);
}
let detailsParts = [field === 'emoji' ? value : '😊'];
for (let i = 0; i < enabledFields.length; i++) {
detailsParts.push(field === enabledFields[i].name ? value : '');
}
newCharacterLines.push(`Details: ${detailsParts.join(' | ')}`);
// Add Relationship field if enabled
if (presentCharsConfig?.relationshipFields?.length > 0) {
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
const relationshipValue = field === 'Relationship' ? (emojiToRelationship[value] || value) : 'Neutral';
newCharacterLines.push(` Relationship: ${relationshipValue}`);
newCharacterLines.push(`Relationship: ${relationshipValue}`);
}
// Add Stats if enabled
if (enabledCharStats.length > 0) {
const statsParts = enabledCharStats.map(s => {
if (field === s.name) {
@@ -1320,7 +1111,7 @@ export function updateCharacterField(characterName, field, value) {
}
return `${s.name}: 0%`;
});
newCharacterLines.push(` Stats: ${statsParts.join(' | ')}`);
newCharacterLines.push(`Stats: ${statsParts.join(' | ')}`);
}
lines.splice(dividerIndex + 1, 0, ...newCharacterLines);
+4 -2
View File
@@ -21,6 +21,7 @@ import { getSafeThumbnailUrl } from '../../utils/avatars.js';
import { buildInventorySummary } from '../generation/promptBuilder.js';
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
import { updateFabWidgets } from '../ui/mobile.js';
import { getStatBarColors } from '../ui/theme.js';
/**
* Builds the user stats text string using custom stat names
@@ -251,8 +252,9 @@ export function renderUserStats() {
}
}
// Create gradient from low to high color
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
// Create gradient from low to high color with opacity
const colors = getStatBarColors();
const gradient = `linear-gradient(to right, ${colors.low}, ${colors.high})`;
// Check if stats bars section is locked
const isStatsLocked = isItemLocked('userStats', 'stats');
+9 -5
View File
@@ -5,6 +5,7 @@
import { i18n } from '../../core/i18n.js';
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
import { hexToRgba } from './theme.js';
/**
* Helper to parse time string and calculate clock hand angles
@@ -28,7 +29,7 @@ function parseTimeForClock(timeStr) {
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
@@ -118,7 +119,7 @@ export function updateStripWidgets() {
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) {
@@ -131,7 +132,7 @@ export function updateStripWidgets() {
console.warn('[RPG Strip Widgets] Failed to parse tracker userStats:', e);
}
}
// Fallback to extensionSettings.userStats
if (allStats.length === 0 && extensionSettings.userStats) {
try {
@@ -237,7 +238,9 @@ export function updateStripWidgets() {
*/
function getStatColor(value) {
const lowColor = extensionSettings.statBarColorLow || '#cc3333';
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
const highColor = extensionSettings.statBarColorHigh || '#33cc66';
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
// Simple linear interpolation between low and high colors
const percent = Math.min(100, Math.max(0, value)) / 100;
@@ -246,13 +249,14 @@ function getStatColor(value) {
const lowRGB = hexToRgb(lowColor);
const highRGB = hexToRgb(highColor);
if (!lowRGB || !highRGB) return value > 50 ? highColor : lowColor;
if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity);
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
return `rgb(${r}, ${g}, ${b})`;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
/**
+9 -5
View File
@@ -8,6 +8,7 @@ import { saveSettings } from '../../core/persistence.js';
import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js';
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
import { i18n } from '../../core/i18n.js';
import { hexToRgba } from './theme.js';
/**
* Updates the text labels of the mobile navigation tabs based on the current language.
@@ -1451,7 +1452,7 @@ export function updateFabWidgets() {
if (widgetSettings.attributes?.enabled) {
// Check if RPG attributes are enabled in trackerConfig
const showRPGAttributes = extensionSettings.trackerConfig?.userStats?.showRPGAttributes !== false;
if (showRPGAttributes && extensionSettings.classicStats) {
// Get enabled attributes from trackerConfig
const configuredAttrs = extensionSettings.trackerConfig?.userStats?.rpgAttributes || [];
@@ -1541,10 +1542,10 @@ export function updateFabWidgets() {
e.stopPropagation();
const $this = $(this);
const wasExpanded = $this.hasClass('expanded');
// Collapse all other expanded widgets
$container.find('.rpg-fab-widget.expanded').removeClass('expanded');
// Toggle this one
if (!wasExpanded) {
$this.addClass('expanded');
@@ -1567,7 +1568,9 @@ export function updateFabWidgets() {
*/
function getStatColor(value) {
const lowColor = extensionSettings.statBarColorLow || '#cc3333';
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
const highColor = extensionSettings.statBarColorHigh || '#33cc66';
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
// Simple linear interpolation between low and high colors
const percent = Math.min(100, Math.max(0, value)) / 100;
@@ -1576,13 +1579,14 @@ function getStatColor(value) {
const lowRGB = hexToRgb(lowColor);
const highRGB = hexToRgb(highColor);
if (!lowRGB || !highRGB) return value > 50 ? highColor : lowColor;
if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity);
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
return `rgb(${r}, ${g}, ${b})`;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
/**
+19 -1
View File
@@ -4,7 +4,7 @@
*/
import { extensionSettings } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js';
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT } from '../generation/promptBuilder.js';
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_OMNISCIENCE_FILTER_PROMPT, DEFAULT_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT, DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT } from '../generation/promptBuilder.js';
let $editorModal = null;
let tempPrompts = null; // Temporary prompts for cancel functionality
@@ -14,9 +14,11 @@ const DEFAULT_PROMPTS = {
html: DEFAULT_HTML_PROMPT,
dialogueColoring: DEFAULT_DIALOGUE_COLORING_PROMPT,
deception: DEFAULT_DECEPTION_PROMPT,
omniscience: DEFAULT_OMNISCIENCE_FILTER_PROMPT,
cyoa: DEFAULT_CYOA_PROMPT,
spotify: DEFAULT_SPOTIFY_PROMPT,
narrator: DEFAULT_NARRATOR_PROMPT,
contextInstructions: DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT,
plotRandom: 'Actually, the scene is getting stale. Introduce {{random::stakes::a plot twist::a new character::a cataclysm::a fourth-wall-breaking joke::a sudden atmospheric phenomenon::a plot hook::a running gag::an ecchi scenario::Death from Discworld::a new stake::a drama::a conflict::an angered entity::a god::a vision::a prophetic dream::Il Dottore from Genshin Impact::a new development::a civilian in need::an emotional bit::a threat::a villain::an important memory recollection::a marriage proposal::a date idea::an angry horde of villagers with pitchforks::a talking animal::an enemy::a cliffhanger::a short omniscient POV shift to a completely different character::a quest::an unexpected revelation::a scandal::an evil clone::death of an important character::harm to an important character::a romantic setup::a gossip::a messenger::a plot point from the past::a plot hole::a tragedy::a ghost::an otherworldly occurrence::a plot device::a curse::a magic device::a rival::an unexpected pregnancy::a brothel::a prostitute::a new location::a past lover::a completely random thing::a what-if scenario::a significant choice::war::love::a monster::lewd undertones::Professor Mari::a travelling troupe::a secret::a fortune-teller::something completely different::a killer::a murder mystery::a mystery::a skill check::a deus ex machina::three raccoons in a trench coat::a pet::a slave::an orphan::a psycho::tentacles::"there is only one bed" trope::accidental marriage::a fun twist::a boss battle::sexy corn::an eldritch horror::a character getting hungry, thirsty, or exhausted::horniness::a need for a bathroom break need::someone fainting::an assassination attempt::a meta narration of this all being an out of hand DND session::a dungeon::a friend in need::an old friend::a small time skip::a scene shift::Aurora Borealis, at this time of year, at this time of day, at this part of the country::a grand ball::a surprise party::zombies::foreshadowing::a Spanish Inquisition (nobody expects it)::a natural plot progression}} to make things more interesting! Be creative, but stay grounded in the setting.',
plotNatural: 'Actually, the scene is getting stale. Progress it, to make things more interesting! Reintroduce an unresolved plot point from the past, or push the story further towards the current main goal. Be creative, but stay grounded in the setting.',
avatar: `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons; however, your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable.
@@ -96,9 +98,11 @@ function openPromptsEditor() {
html: extensionSettings.customHtmlPrompt || '',
dialogueColoring: extensionSettings.customDialogueColoringPrompt || '',
deception: extensionSettings.customDeceptionPrompt || '',
omniscience: extensionSettings.customOmnisciencePrompt || '',
cyoa: extensionSettings.customCYOAPrompt || '',
spotify: extensionSettings.customSpotifyPrompt || '',
narrator: extensionSettings.customNarratorPrompt || '',
contextInstructions: extensionSettings.customContextInstructionsPrompt || '',
plotRandom: extensionSettings.customPlotRandomPrompt || '',
plotNatural: extensionSettings.customPlotNaturalPrompt || '',
avatar: extensionSettings.avatarLLMCustomInstruction || '',
@@ -111,9 +115,11 @@ function openPromptsEditor() {
$('#rpg-prompt-html').val(extensionSettings.customHtmlPrompt || DEFAULT_PROMPTS.html);
$('#rpg-prompt-dialogue-coloring').val(extensionSettings.customDialogueColoringPrompt || DEFAULT_PROMPTS.dialogueColoring);
$('#rpg-prompt-deception').val(extensionSettings.customDeceptionPrompt || DEFAULT_PROMPTS.deception);
$('#rpg-prompt-omniscience').val(extensionSettings.customOmnisciencePrompt || DEFAULT_PROMPTS.omniscience);
$('#rpg-prompt-cyoa').val(extensionSettings.customCYOAPrompt || DEFAULT_PROMPTS.cyoa);
$('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify);
$('#rpg-prompt-narrator').val(extensionSettings.customNarratorPrompt || DEFAULT_PROMPTS.narrator);
$('#rpg-prompt-context-instructions').val(extensionSettings.customContextInstructionsPrompt || DEFAULT_PROMPTS.contextInstructions);
$('#rpg-prompt-plot-random').val(extensionSettings.customPlotRandomPrompt || DEFAULT_PROMPTS.plotRandom);
$('#rpg-prompt-plot-natural').val(extensionSettings.customPlotNaturalPrompt || DEFAULT_PROMPTS.plotNatural);
$('#rpg-prompt-avatar').val(extensionSettings.avatarLLMCustomInstruction || DEFAULT_PROMPTS.avatar);
@@ -150,9 +156,11 @@ function savePrompts() {
extensionSettings.customHtmlPrompt = $('#rpg-prompt-html').val().trim();
extensionSettings.customDialogueColoringPrompt = $('#rpg-prompt-dialogue-coloring').val().trim();
extensionSettings.customDeceptionPrompt = $('#rpg-prompt-deception').val().trim();
extensionSettings.customOmnisciencePrompt = $('#rpg-prompt-omniscience').val().trim();
extensionSettings.customCYOAPrompt = $('#rpg-prompt-cyoa').val().trim();
extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').val().trim();
extensionSettings.customNarratorPrompt = $('#rpg-prompt-narrator').val().trim();
extensionSettings.customContextInstructionsPrompt = $('#rpg-prompt-context-instructions').val().trim();
extensionSettings.customPlotRandomPrompt = $('#rpg-prompt-plot-random').val().trim();
extensionSettings.customPlotNaturalPrompt = $('#rpg-prompt-plot-natural').val().trim();
extensionSettings.avatarLLMCustomInstruction = $('#rpg-prompt-avatar').val().trim();
@@ -182,6 +190,9 @@ function restorePromptToDefault(promptType) {
case 'deception':
extensionSettings.customDeceptionPrompt = '';
break;
case 'omniscience':
extensionSettings.customOmnisciencePrompt = '';
break;
case 'cyoa':
extensionSettings.customCYOAPrompt = '';
break;
@@ -191,6 +202,9 @@ function restorePromptToDefault(promptType) {
case 'narrator':
extensionSettings.customNarratorPrompt = '';
break;
case 'contextInstructions':
extensionSettings.customContextInstructionsPrompt = '';
break;
case 'plotRandom':
extensionSettings.customPlotRandomPrompt = '';
break;
@@ -221,9 +235,11 @@ function restoreAllToDefaults() {
$('#rpg-prompt-html').val(DEFAULT_PROMPTS.html);
$('#rpg-prompt-dialogue-coloring').val(DEFAULT_PROMPTS.dialogueColoring);
$('#rpg-prompt-deception').val(DEFAULT_PROMPTS.deception);
$('#rpg-prompt-omniscience').val(DEFAULT_PROMPTS.omniscience);
$('#rpg-prompt-cyoa').val(DEFAULT_PROMPTS.cyoa);
$('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify);
$('#rpg-prompt-narrator').val(DEFAULT_PROMPTS.narrator);
$('#rpg-prompt-context-instructions').val(DEFAULT_PROMPTS.contextInstructions);
$('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom);
$('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural);
$('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar);
@@ -235,9 +251,11 @@ function restoreAllToDefaults() {
extensionSettings.customHtmlPrompt = '';
extensionSettings.customDialogueColoringPrompt = '';
extensionSettings.customDeceptionPrompt = '';
extensionSettings.customOmnisciencePrompt = '';
extensionSettings.customCYOAPrompt = '';
extensionSettings.customSpotifyPrompt = '';
extensionSettings.customNarratorPrompt = '';
extensionSettings.customContextInstructionsPrompt = '';
extensionSettings.customPlotRandomPrompt = '';
extensionSettings.customPlotNaturalPrompt = '';
extensionSettings.avatarLLMCustomInstruction = '';
+55 -12
View File
@@ -5,6 +5,37 @@
import { extensionSettings, $panelContainer } from '../../core/state.js';
/**
* Converts hex color and opacity percentage to rgba string
* @param {string} hex - Hex color (e.g., '#ff0000')
* @param {number} opacity - Opacity percentage (0-100)
* @returns {string} - RGBA color string
*/
export function hexToRgba(hex, opacity = 100) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const a = opacity / 100;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
/**
* Gets stat bar colors with opacity applied
* @returns {{low: string, high: string}} RGBA color strings for stat bars
*/
export function getStatBarColors() {
return {
low: hexToRgba(
extensionSettings.statBarColorLow || '#cc3333',
extensionSettings.statBarColorLowOpacity ?? 100
),
high: hexToRgba(
extensionSettings.statBarColorHigh || '#33cc66',
extensionSettings.statBarColorHighOpacity ?? 100
)
};
}
/**
* Applies the selected theme to the panel.
*/
@@ -75,24 +106,33 @@ export function applyCustomTheme() {
const colors = extensionSettings.customColors;
// Convert hex colors with opacity to rgba
const bgColor = hexToRgba(colors.bg, colors.bgOpacity ?? 100);
const accentColor = hexToRgba(colors.accent, colors.accentOpacity ?? 100);
const textColor = hexToRgba(colors.text, colors.textOpacity ?? 100);
const highlightColor = hexToRgba(colors.highlight, colors.highlightOpacity ?? 100);
// Create shadow with 50% opacity of highlight color
const shadowColor = hexToRgba(colors.highlight, (colors.highlightOpacity ?? 100) * 0.5);
// Apply custom CSS variables as inline styles to main panel
$panelContainer.css({
'--rpg-bg': colors.bg,
'--rpg-accent': colors.accent,
'--rpg-text': colors.text,
'--rpg-highlight': colors.highlight,
'--rpg-border': colors.highlight,
'--rpg-shadow': `${colors.highlight}80` // Add alpha for shadow
'--rpg-bg': bgColor,
'--rpg-accent': accentColor,
'--rpg-text': textColor,
'--rpg-highlight': highlightColor,
'--rpg-border': highlightColor,
'--rpg-shadow': shadowColor
});
// Apply custom colors to mobile toggle and thought elements
const customStyles = {
'--rpg-bg': colors.bg,
'--rpg-accent': colors.accent,
'--rpg-text': colors.text,
'--rpg-highlight': colors.highlight,
'--rpg-border': colors.highlight,
'--rpg-shadow': `${colors.highlight}80`
'--rpg-bg': bgColor,
'--rpg-accent': accentColor,
'--rpg-text': textColor,
'--rpg-highlight': highlightColor,
'--rpg-border': highlightColor,
'--rpg-shadow': shadowColor
};
const $mobileToggle = $('#rpg-mobile-toggle');
@@ -139,6 +179,7 @@ export function updateFeatureTogglesVisibility() {
const $htmlToggle = $('#rpg-html-toggle-wrapper');
const $dialogueColoringToggle = $('#rpg-dialogue-coloring-toggle-wrapper');
const $deceptionToggle = $('#rpg-deception-toggle-wrapper');
const $omniscienceToggle = $('#rpg-omniscience-toggle-wrapper');
const $cyoaToggle = $('#rpg-cyoa-toggle-wrapper');
const $spotifyToggle = $('#rpg-spotify-toggle-wrapper');
@@ -150,6 +191,7 @@ export function updateFeatureTogglesVisibility() {
$htmlToggle.toggle(extensionSettings.showHtmlToggle);
$dialogueColoringToggle.toggle(extensionSettings.showDialogueColoringToggle);
$deceptionToggle.toggle(extensionSettings.showDeceptionToggle ?? true);
$omniscienceToggle.toggle(extensionSettings.showOmniscienceToggle ?? true);
$cyoaToggle.toggle(extensionSettings.showCYOAToggle ?? true);
$spotifyToggle.toggle(extensionSettings.showSpotifyToggle);
@@ -161,6 +203,7 @@ export function updateFeatureTogglesVisibility() {
const anyVisible = extensionSettings.showHtmlToggle ||
extensionSettings.showDialogueColoringToggle ||
(extensionSettings.showDeceptionToggle ?? true) ||
(extensionSettings.showOmniscienceToggle ?? true) ||
(extensionSettings.showCYOAToggle ?? true) ||
extensionSettings.showSpotifyToggle ||
extensionSettings.showDynamicWeatherToggle ||
+40 -47
View File
@@ -105,48 +105,41 @@ function getCurrentTime() {
return null;
}
// Patterns for specific weather conditions (order matters - combined effects first)
// Grouped by languages for easy editing
const WEATHER_PATTERNS_BY_LANGUAGE = {
en: [
{ id: "blizzard", patterns: [ "blizzard" ] }, // Snow + Wind
{ id: "storm", patterns: [ "storm", "thunder", "lightning" ] }, // Rain + Lightning
{ id: "wind", patterns: [ "wind", "breeze", "gust", "gale" ] },
{ id: "snow", patterns: [ "snow", "flurries" ] },
{ id: "rain", patterns: [ "rain", "drizzle", "shower" ] },
{ id: "mist", patterns: [ "mist", "fog", "haze" ] },
{ id: "sunny", patterns: [ "sunny", "clear", "bright" ] },
{ id: "none", patterns: [ "cloud", "overcast", "indoor", "inside" ] },
],
ru: [
{ id: "blizzard", patterns: [ "метель" ] },
{ id: "storm", patterns: [ "гроза", "буря", "шторм" ] },
{ id: "wind", patterns: [ "ветер", "ветрено", "ветерок", "бриз", "легкий бриз", "слегка ветрено", "легкий ветер", "шквал,буря" ] },
{ id: "snow", patterns: [ "снег", "снегопад" ] },
{ id: "rain", patterns: [ "дождь", "морось", "ливень" ] },
{ id: "mist", patterns: [ "мгла", "туман", "туманно" ] },
{ id: "sunny", patterns: [ "солнечно", "ясно", "ярко", "ясное утро", "ясный день" ] },
{ id: "none", patterns: [ "облачно", "пасмурно", "в помещении", "внутри" ] },
],
}
/**
* Parse weather text to determine effect type
*/
function parseWeatherType(weatherText) {
if (!weatherText) return "none";
if (!weatherText) return 'none';
const text = weatherText.toLowerCase();
for (const language of Object.values(WEATHER_PATTERNS_BY_LANGUAGE)) {
for (const { id, patterns } of language) {
if (patterns.some(p => text.includes(p))) {
return id;
}
}
// Check for specific weather conditions (order matters - check combined effects first)
if (text.includes('blizzard')) {
return 'blizzard'; // Snow + Wind
}
if (text.includes('storm') || text.includes('thunder') || text.includes('lightning')) {
return 'storm'; // Rain + Lightning
}
if (text.includes('wind') || text.includes('breeze') || text.includes('gust') || text.includes('gale')) {
return 'wind';
}
if (text.includes('snow') || text.includes('flurries')) {
return 'snow';
}
if (text.includes('rain') || text.includes('drizzle') || text.includes('shower')) {
return 'rain';
}
if (text.includes('mist') || text.includes('fog') || text.includes('haze')) {
return 'mist';
}
if (text.includes('sunny') || text.includes('clear') || text.includes('bright')) {
return 'sunny';
}
if (text.includes('cloud') || text.includes('overcast') || text.includes('indoor') || text.includes('inside')) {
return 'none';
}
return "none";
return 'none';
}
/**
@@ -247,24 +240,24 @@ function calculateSunPosition(hour) {
// Daytime is roughly 5 AM to 8 PM (5-20)
// Map hour to position along an arc
// 5 AM = far left, low | 12 PM = center, high | 8 PM = far right, low
if (hour === null) hour = 12; // Default to noon if unknown
// Clamp to daytime hours
const clampedHour = Math.max(5, Math.min(20, hour));
// Normalize to 0-1 range (5 AM = 0, 20 PM = 1)
const progress = (clampedHour - 5) / 15;
// Horizontal position: 3% to 92% (left to right, wider range)
const left = 3 + progress * 89;
// Vertical position: parabolic arc (high at noon, low at dawn/dusk)
// At progress 0.5 (noon), top should be ~8% (high)
// At progress 0 or 1, top should be ~40% (low, near horizon)
const normalizedProgress = (progress - 0.5) * 2; // -1 to 1
const top = 8 + 32 * (normalizedProgress * normalizedProgress);
return { left, top };
}
@@ -277,7 +270,7 @@ function createSunshine(hour) {
// 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`;
@@ -579,9 +572,9 @@ 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;
@@ -592,16 +585,16 @@ function calculateMoonPosition(hour) {
// 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 };
}
@@ -614,7 +607,7 @@ function updateCelestialPosition(hour) {
// 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`;
@@ -627,7 +620,7 @@ function updateCelestialPosition(hour) {
// 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`;
+4 -46
View File
@@ -2329,40 +2329,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
transform: scale(0.95);
}
/* Add Character Button (inside rpg-thoughts-content, after last character) */
.rpg-add-character-btn {
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
color: var(--rpg-text);
padding: 0;
margin: 0.5rem auto 0;
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: 500;
cursor: pointer;
border-radius: 3px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.25rem;
opacity: 0.6;
width: auto;
}
.rpg-add-character-btn:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
color: var(--rpg-bg);
opacity: 1;
}
.rpg-add-character-btn:active {
transform: scale(0.95);
}
.rpg-add-character-btn i {
font-size: 0.875em;
}
/* Character traits/status line and custom fields */
.rpg-character-traits,
.rpg-character-field {
@@ -2374,20 +2340,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
word-wrap: break-word;
}
/* Empty field placeholder using data-placeholder attribute */
.rpg-editable.rpg-empty-field:empty::before {
content: attr(data-placeholder);
/* Placeholder for empty editable character fields */
.rpg-character-field.rpg-editable:empty::before {
content: 'Click to edit...';
color: var(--rpg-highlight);
opacity: 0.4;
opacity: 0.5;
font-style: italic;
pointer-events: none;
}
/* Ensure empty fields have minimum height for clickability */
.rpg-editable.rpg-empty-field {
min-height: 1.2em;
display: inline-block;
min-width: 3em;
}
/* Character stats display */
+76 -9
View File
@@ -139,6 +139,15 @@
</label>
</div>
<!-- Omniscience Filter Toggle -->
<div class="rpg-toggle-container rpg-feature-col" id="rpg-omniscience-toggle-wrapper">
<label class="rpg-toggle-label" title="Omniscience Filter">
<input type="checkbox" id="rpg-toggle-omniscience">
<i class="fa-solid fa-eye-slash"></i>
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.omniscienceFilter">Omniscience Filter</span>
</label>
</div>
<!-- CYOA Toggle -->
<div class="rpg-toggle-container rpg-feature-col" id="rpg-cyoa-toggle-wrapper">
<label class="rpg-toggle-label" title="CYOA">
@@ -241,29 +250,49 @@
<div class="rpg-setting-row">
<label for="rpg-custom-bg"
data-i18n-key="template.settingsModal.themeOptions.custom.background">Background:</label>
<input type="color" id="rpg-custom-bg" value="#1a1a2e" />
<div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-custom-bg" value="#1a1a2e" style="width: 60px;" />
<input type="range" id="rpg-custom-bg-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-bg-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div>
<div class="rpg-setting-row">
<label for="rpg-custom-accent"
data-i18n-key="template.settingsModal.themeOptions.custom.accent">Accent:</label>
<input type="color" id="rpg-custom-accent" value="#16213e" />
<div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-custom-accent" value="#16213e" style="width: 60px;" />
<input type="range" id="rpg-custom-accent-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-accent-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div>
<div class="rpg-setting-row">
<label for="rpg-custom-text"
data-i18n-key="template.settingsModal.themeOptions.custom.text">Text:</label>
<input type="color" id="rpg-custom-text" value="#eaeaea" />
<div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-custom-text" value="#eaeaea" style="width: 60px;" />
<input type="range" id="rpg-custom-text-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-text-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div>
<div class="rpg-setting-row">
<label for="rpg-custom-highlight"
data-i18n-key="template.settingsModal.themeOptions.custom.highlight">Highlight:</label>
<input type="color" id="rpg-custom-highlight" value="#e94560" />
<div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-custom-highlight" value="#e94560" style="width: 60px;" />
<input type="range" id="rpg-custom-highlight-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-highlight-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div>
</div>
<div class="rpg-setting-row">
<label for="rpg-stat-bar-color-low" data-i18n-key="template.settingsModal.theme.statBarLow">Stat Bar
Color (Low):</label>
<input type="color" id="rpg-stat-bar-color-low" value="#cc3333" />
<div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-stat-bar-color-low" value="#cc3333" style="width: 60px;" />
<input type="range" id="rpg-stat-bar-color-low-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-stat-bar-color-low-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
<small data-i18n-key="template.settingsModal.theme.statBarLowNote">Color when stats are at
0%.</small>
</div>
@@ -271,7 +300,11 @@
<div class="rpg-setting-row">
<label for="rpg-stat-bar-color-high" data-i18n-key="template.settingsModal.theme.statBarHigh">Stat
Bar Color (High):</label>
<input type="color" id="rpg-stat-bar-color-high" value="#33cc66" />
<div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-stat-bar-color-high" value="#33cc66" style="width: 60px;" />
<input type="range" id="rpg-stat-bar-color-high-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-stat-bar-color-high-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
<small data-i18n-key="template.settingsModal.theme.statBarHighNote">Color when stats are at
100%.</small>
</div>
@@ -388,6 +421,15 @@
Display a toggle button to enable/disable special formatting of lies and deceptions crafted by the model, allowing it to easily track whenever one was committed, without showing it to the user.
</small>
<label class="checkbox_label">
<input type="checkbox" id="rpg-toggle-show-omniscience-toggle" />
<span data-i18n-key="template.settingsModal.display.showOmniscienceToggle">Show Omniscience Filter</span>
</label>
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
data-i18n-key="template.settingsModal.display.showOmniscienceToggleNote">
Display a toggle button to enable/disable the omniscience filter, which instructs the AI to hide information the player character cannot perceive (events behind them, in other rooms, etc.) in special tags.
</small>
<label class="checkbox_label">
<input type="checkbox" id="rpg-toggle-show-cyoa-toggle" />
<span data-i18n-key="template.settingsModal.display.showCYOAToggle">Show CYOA</span>
@@ -996,6 +1038,20 @@
</button>
</div>
<!-- Omniscience Filter Prompt -->
<div class="rpg-prompt-editor-section">
<label for="rpg-prompt-omniscience" style="display: block; margin-bottom: 8px; font-weight: 600;">
<i class="fa-solid fa-eye-slash"></i> Omniscience Filter Prompt
</label>
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
Injected when "Enable Omniscience Filter" is enabled. Instructs AI to separate information the player character cannot perceive into hidden filter tags.
</small>
<textarea id="rpg-prompt-omniscience" class="rpg-prompt-textarea" rows="6"></textarea>
<button class="menu_button rpg-restore-prompt-btn" data-prompt="omniscience" style="margin-top: 8px;">
<i class="fa-solid fa-rotate-left"></i>&nbsp;Restore Default
</button>
</div>
<!-- CYOA Prompt -->
<div class="rpg-prompt-editor-section">
<label for="rpg-prompt-cyoa" style="display: block; margin-bottom: 8px; font-weight: 600;">
@@ -1038,6 +1094,20 @@
</button>
</div>
<!-- Context Instructions Prompt -->
<div class="rpg-prompt-editor-section">
<label for="rpg-prompt-context-instructions" style="display: block; margin-bottom: 8px; font-weight: 600;">
<i class="fa-solid fa-comment-dots"></i> Context Instructions Prompt
</label>
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
Injected in Separate/External mode after the context summary. Tells the AI how to use the context.
</small>
<textarea id="rpg-prompt-context-instructions" class="rpg-prompt-textarea" rows="4"></textarea>
<button class="menu_button rpg-restore-prompt-btn" data-prompt="contextInstructions" style="margin-top: 8px;">
<i class="fa-solid fa-rotate-left"></i>&nbsp;Restore Default
</button>
</div>
<!-- Random Plot Progression Prompt -->
<div class="rpg-prompt-editor-section">
<label for="rpg-prompt-plot-random" style="display: block; margin-bottom: 8px; font-weight: 600;">
@@ -1124,9 +1194,6 @@
</div>
<footer class="rpg-settings-popup-footer">
<button id="rpg-prompts-restore-all" class="rpg-btn-secondary" type="button">
<i class="fa-solid fa-rotate-left"></i> Restore All To Default
</button>
<div class="rpg-footer-right">
<button id="rpg-prompts-cancel" class="rpg-btn-secondary" type="button">Cancel</button>
<button id="rpg-prompts-save" class="rpg-btn-primary" type="button">