Add holiday promotion, snowflakes effect, and Spotify music widget

- Added holiday promotion banner with 2026WITHMARINARA discount code
- Added dismiss functionality for promotion with persistent state
- Implemented snowflakes animation effect with toggle
- Added Spotify music widget above chat input
- Widget matches extension theme colors and positioning
- Added Display Options toggles to show/hide feature toggles
- Improved responsive design and mobile support
This commit is contained in:
Spicy_Marinara
2025-12-30 20:56:38 +01:00
parent 51535c5fdc
commit 3f58c7ceca
17 changed files with 1170 additions and 199 deletions
+79
View File
@@ -0,0 +1,79 @@
/**
* Music Player Module
* Handles parsing and storing Spotify URLs from AI responses
*/
import { extensionSettings, committedTrackerData } from '../../core/state.js';
/**
* Extracts song suggestion from AI response in <spotify:Song - Artist/> format
* @param {string} responseText - The raw AI response text
* @returns {Object|null} Object with {song, artist, searchQuery} or null if not found
*/
export function extractSpotifyUrl(responseText) {
if (!responseText || !extensionSettings.enableSpotifyMusic) return null;
// Match <spotify:Song Title - Artist Name/> format
const songMatch = responseText.match(/<spotify:([^<>-]+)\s*-\s*([^<>\/]+)\/>/i);
if (songMatch) {
const song = songMatch[1].trim();
const artist = songMatch[2].trim();
const searchQuery = `${song} ${artist}`;
return {
song,
artist,
searchQuery,
displayText: `${song} - ${artist}`
};
}
return null;
}
/**
* Converts song data to Spotify app protocol URL
* @param {Object} songData - Object with {song, artist, searchQuery}
* @returns {string} Spotify app protocol URL
*/
export function convertToEmbedUrl(songData) {
if (!songData || !songData.searchQuery) return '';
// Use Spotify app protocol for direct app opening
const encodedQuery = encodeURIComponent(songData.searchQuery);
return `spotify:search:${encodedQuery}`;
}
/**
* Parses AI response for song suggestion and stores it
* @param {string} responseText - The raw AI response text
* @returns {boolean} True if song was found and stored
*/
export function parseAndStoreSpotifyUrl(responseText) {
if (!extensionSettings.enableSpotifyMusic) return false;
const songData = extractSpotifyUrl(responseText);
console.log('[RPG Companion] Spotify Parser: Found song:', songData);
if (songData) {
// Store in committed tracker data
committedTrackerData.spotifyUrl = songData;
console.log('[RPG Companion] Spotify Parser: Stored song in committedTrackerData:', committedTrackerData.spotifyUrl);
return true;
}
return false;
}
/**
* Gets the current song data from committed tracker data
* @returns {Object|null} Current song data or null
*/
export function getCurrentSpotifyUrl() {
return committedTrackerData.spotifyUrl || null;
}
/**
* Clears the current song data
*/
export function clearSpotifyUrl() {
committedTrackerData.spotifyUrl = null;
}
+9 -3
View File
@@ -12,18 +12,21 @@ import {
isGenerating,
lastActionWasSwipe,
setIsGenerating,
setLastActionWasSwipe
setLastActionWasSwipe,
$musicPlayerContainer
} from '../../core/state.js';
import { saveChatData } from '../../core/persistence.js';
import {
generateSeparateUpdatePrompt
} from './promptBuilder.js';
import { parseResponse, parseUserStats } from './parser.js';
import { parseAndStoreSpotifyUrl } from '../features/musicPlayer.js';
import { renderUserStats } from '../rendering/userStats.js';
import { renderInfoBox } from '../rendering/infoBox.js';
import { renderThoughts } from '../rendering/thoughts.js';
import { renderInventory } from '../rendering/inventory.js';
import { renderQuests } from '../rendering/quests.js';
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
import { i18n } from '../../core/i18n.js';
import { generateAvatarsForCharacters } from '../features/avatarGenerator.js';
@@ -121,7 +124,7 @@ export async function testExternalAPIConnection() {
if (!baseUrl || !apiKey || !model) {
return {
success: false,
message: !apiKey
message: !apiKey
? 'API Key not found. Please re-enter it in settings (keys are stored locally per-browser).'
: 'Please fill in all required fields (Base URL, API Key, and Model)'
};
@@ -267,6 +270,8 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
if (response) {
// console.log('[RPG Companion] Raw AI response:', response);
const parsedData = parseResponse(response);
// Parse and store Spotify URL if feature is enabled
parseAndStoreSpotifyUrl(response);
// console.log('[RPG Companion] Parsed data:', parsedData);
// console.log('[RPG Companion] parsedData.userStats:', parsedData.userStats ? parsedData.userStats.substring(0, 100) + '...' : 'null');
@@ -346,6 +351,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Save to chat metadata
saveChatData();
@@ -356,7 +362,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
const charactersNeedingAvatars = parseCharactersFromThoughts(parsedData.characterThoughts);
if (charactersNeedingAvatars.length > 0) {
console.log('[RPG Companion] Generating avatars for:', charactersNeedingAvatars);
// Generate avatars - this awaits completion
await generateAvatarsForCharacters(charactersNeedingAvatars, (names) => {
// Callback when generation starts - re-render to show loading spinners
+32 -1
View File
@@ -19,7 +19,9 @@ import {
generateTrackerExample,
generateTrackerInstructions,
generateContextualSummary,
DEFAULT_HTML_PROMPT
DEFAULT_HTML_PROMPT,
DEFAULT_SPOTIFY_PROMPT,
SPOTIFY_FORMAT_INSTRUCTION
} from './promptBuilder.js';
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
@@ -67,6 +69,7 @@ export async function onGenerationStarted(type, data) {
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
}
@@ -230,6 +233,19 @@ export async function onGenerationStarted(type, data) {
// Clear HTML prompt if disabled
setExtensionPrompt('rpg-companion-html', '', 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
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
const spotifyPrompt = `\n${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}`;
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for together mode');
} else {
// Clear Spotify prompt if disabled
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
}
} else if (extensionSettings.generationMode === 'separate') {
// In SEPARATE mode, inject the contextual summary for main roleplay generation
const contextSummary = generateContextualSummary();
@@ -266,6 +282,19 @@ Ensure these details naturally reflect and influence the narrative. Character be
setExtensionPrompt('rpg-companion-html', '', 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
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
const spotifyPrompt = `\n${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}`;
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for separate mode');
} else {
// Clear Spotify prompt if disabled
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
}
// Clear together mode injections
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
@@ -274,5 +303,7 @@ Ensure these details naturally reflect and influence the narrative. Character be
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
}
}
+24
View File
@@ -16,6 +16,16 @@ import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../co
*/
export const DEFAULT_HTML_PROMPT = `If appropriate, include inline HTML, CSS, and JS segments whenever they enhance visual storytelling (e.g., for in-world screens, posters, books, letters, signs, crests, labels, etc.). Style them to match the setting's theme (e.g., fantasy, sci-fi), keep the text readable, and embed all assets directly (using inline SVGs only with no external scripts, libraries, or fonts). Use these elements freely and naturally within the narrative as characters would encounter them, including animations, 3D effects, pop-ups, dropdowns, websites, and so on. Do not wrap the HTML/CSS/JS in code fences!`;
/**
* Default Spotify music prompt text (customizable by users)
*/
export const DEFAULT_SPOTIFY_PROMPT = `If appropriate for the current scene's mood and atmosphere, suggest a song that fits the ambiance. Choose music that enhances the emotional tone, setting, or action of the scene.`;
/**
* Spotify format instruction (constant, not editable by users)
*/
export const SPOTIFY_FORMAT_INSTRUCTION = `Include it in this exact format: <spotify:Song Title - Artist Name/>.`;
/**
* Gets character card information for current chat (handles both single and group chats)
* @returns {string} Formatted character information
@@ -445,6 +455,20 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
instructions += htmlPrompt;
}
// Append Spotify music prompt if enabled AND includeHtmlPrompt is true
if (extensionSettings.enableSpotifyMusic && includeHtmlPrompt) {
// Add separator
if (hasAnyTrackers || extensionSettings.enableHtmlPrompt) {
instructions += `\n\n`;
} else {
instructions += `\n`;
}
// Use custom Spotify prompt if set, otherwise use default
const spotifyPrompt = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
instructions += spotifyPrompt + ' ' + SPOTIFY_FORMAT_INSTRUCTION;
}
return instructions;
}
+32 -6
View File
@@ -16,12 +16,14 @@ import {
setLastActionWasSwipe,
setIsPlotProgression,
updateLastGeneratedData,
updateCommittedTrackerData
updateCommittedTrackerData,
$musicPlayerContainer
} from '../../core/state.js';
import { saveChatData, loadChatData } from '../../core/persistence.js';
// Generation & Parsing
import { parseResponse, parseUserStats } from '../generation/parser.js';
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
import { updateRPGData } from '../generation/apiClient.js';
// Rendering
@@ -30,6 +32,7 @@ import { renderInfoBox } from '../rendering/infoBox.js';
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
import { renderInventory } from '../rendering/inventory.js';
import { renderQuests } from '../rendering/quests.js';
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
// Utils
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
@@ -119,6 +122,8 @@ export async function onMessageReceived(data) {
// console.log('[RPG Companion] Parsing together mode response:', responseText);
const parsedData = parseResponse(responseText);
// Parse and store Spotify URL if feature is enabled
parseAndStoreSpotifyUrl(responseText);
// console.log('[RPG Companion] Parsed data:', parsedData);
// Update stored data
@@ -176,6 +181,7 @@ export async function onMessageReceived(data) {
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
}
// Note: <trackers> XML tags are automatically hidden by SillyTavern
// Note: <Song - Artist/> tags are also automatically hidden by SillyTavern
// Update the message in chat history
lastMessage.mes = cleanedMessage.trim();
@@ -191,6 +197,7 @@ export async function onMessageReceived(data) {
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Then update the DOM to reflect the cleaned message
// Using updateMessageBlock to perform macro substitutions + regex formatting
@@ -202,11 +209,28 @@ export async function onMessageReceived(data) {
// Save to chat metadata
saveChatData();
}
} else if (extensionSettings.generationMode === 'separate' && extensionSettings.autoUpdate) {
// In separate mode with auto-update, trigger update after message
setTimeout(async () => {
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
}, 500);
} else if (extensionSettings.generationMode === 'separate') {
// In separate mode, also parse Spotify URLs from the main roleplay response
const lastMessage = chat[chat.length - 1];
if (lastMessage && !lastMessage.is_user) {
const responseText = lastMessage.mes;
// Parse and store Spotify URL
const foundSpotifyUrl = parseAndStoreSpotifyUrl(responseText);
// No need to clean message - SillyTavern auto-hides <Song - Artist/> tags
if (foundSpotifyUrl && extensionSettings.enableSpotifyMusic) {
// Just render the music player
renderMusicPlayer($musicPlayerContainer[0]);
}
}
// Trigger auto-update if enabled
if (extensionSettings.autoUpdate) {
setTimeout(async () => {
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
}, 500);
}
}
// Reset the swipe flag after generation completes
@@ -253,6 +277,7 @@ export function onCharacterChanged() {
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update chat thought overlays
updateChatThoughts();
@@ -328,6 +353,7 @@ export function onMessageSwiped(messageIndex) {
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update chat thought overlays
updateChatThoughts();
+150
View File
@@ -0,0 +1,150 @@
/**
* Music Player Rendering Module
* Handles UI rendering for Spotify music player widget
*/
import { extensionSettings, committedTrackerData } from '../../core/state.js';
import { i18n } from '../../core/i18n.js';
/**
* Creates a Spotify deep link URL that opens the Spotify app
* Uses spotify:search: protocol for app, falls back to web URL
* @param {Object} songData - Object with {song, artist, searchQuery}
* @returns {Object} Object with appUrl and webUrl
*/
function createSpotifyUrls(songData) {
if (!songData || !songData.searchQuery) {
return { appUrl: '', webUrl: '' };
}
const encodedQuery = encodeURIComponent(songData.searchQuery);
return {
// Spotify app protocol - opens directly in Spotify app on desktop/mobile
appUrl: `spotify:search:${encodedQuery}`,
// Web fallback - opens Spotify web player search
webUrl: `https://open.spotify.com/search/${encodedQuery}`
};
}
/**
* Opens Spotify with the given song
* Tries app protocol first, falls back to web
* @param {Object} songData - Song data object
*/
function openInSpotify(songData) {
const urls = createSpotifyUrls(songData);
// Try to open in Spotify app first
// On mobile, this will open the Spotify app if installed
// On desktop, this will open Spotify desktop app if installed
window.location.href = urls.appUrl;
// Fallback: If app doesn't open within 2 seconds, open web version
// This handles cases where Spotify app isn't installed
setTimeout(() => {
// Check if we're still on the same page (app didn't open)
// Note: This is a best-effort fallback
if (document.hasFocus()) {
window.open(urls.webUrl, '_blank');
}
}, 1500);
}
/**
* Renders the Spotify music player as a mini player widget above chat input
* @param {HTMLElement} container - Container element to render into
*/
export function renderMusicPlayer(container) {
console.log('[RPG Companion] Music Player: renderMusicPlayer called');
// Remove old chat-attached player if it exists
$('#rpg-chat-music-player').remove();
console.log('[RPG Companion] Music Player: enableSpotifyMusic =', extensionSettings.enableSpotifyMusic);
if (!extensionSettings.enableSpotifyMusic) {
console.warn('[RPG Companion] Music Player: Spotify music is disabled');
return;
}
const songData = committedTrackerData.spotifyUrl;
console.log('[RPG Companion] Music Player: Rendering with song:', songData);
if (!songData || !songData.displayText) {
// No song - don't show anything
return;
}
// Create the mini music player widget
const musicPlayerHtml = `
<div id="rpg-chat-music-player" class="rpg-music-widget">
<div class="rpg-music-widget-content">
<div class="rpg-music-widget-icon">
<i class="fa-brands fa-spotify"></i>
</div>
<div class="rpg-music-widget-info">
<div class="rpg-music-widget-title" title="${songData.song}">${songData.song}</div>
<div class="rpg-music-widget-artist" title="${songData.artist}">${songData.artist}</div>
</div>
<button class="rpg-music-widget-play" title="Play in Spotify">
<i class="fa-solid fa-play"></i>
</button>
<button class="rpg-music-widget-close" title="Dismiss">
<i class="fa-solid fa-times"></i>
</button>
</div>
</div>
`;
// Find the chat form container and insert widget before (above) it
const $chatForm = $('#send_form');
console.log('[RPG Companion] Music Player: Found #send_form:', $chatForm.length > 0);
if ($chatForm.length === 0) {
console.error('[RPG Companion] Music Player: Could not find #send_form - cannot render widget!');
return;
}
// Insert widget inside (at top of) the chat form
console.log('[RPG Companion] Music Player: Prepending widget to #send_form');
$chatForm.prepend(musicPlayerHtml);
console.log('[RPG Companion] Music Player: Widget inserted, checking if visible...');
const $widget = $('#rpg-chat-music-player');
console.log('[RPG Companion] Music Player: Widget exists:', $widget.length > 0);
if ($widget.length > 0) {
console.log('[RPG Companion] Music Player: Widget position:', $widget.offset());
console.log('[RPG Companion] Music Player: Widget dimensions:', { width: $widget.width(), height: $widget.height() });
console.log('[RPG Companion] Music Player: Widget CSS display:', $widget.css('display'));
console.log('[RPG Companion] Music Player: Widget CSS visibility:', $widget.css('visibility'));
}
// Bind play button click
$('#rpg-chat-music-player .rpg-music-widget-play').on('click', function(e) {
e.stopPropagation();
openInSpotify(songData);
});
// Bind close button click
$('#rpg-chat-music-player .rpg-music-widget-close').on('click', function(e) {
e.stopPropagation();
$('#rpg-chat-music-player').fadeOut(200, function() {
$(this).remove();
});
});
// Clicking anywhere else on the widget also opens Spotify
$('#rpg-chat-music-player .rpg-music-widget-content').on('click', function() {
openInSpotify(songData);
});
}
/**
* Updates the music player display
* @param {HTMLElement} container - Container element
*/
export function updateMusicPlayer(container) {
renderMusicPlayer(container);
}
+22 -5
View File
@@ -10,7 +10,8 @@ import {
$infoBoxContainer,
$thoughtsContainer,
$inventoryContainer,
$questsContainer
$questsContainer,
$musicPlayerContainer
} from '../../core/state.js';
import { i18n } from '../../core/i18n.js';
@@ -283,10 +284,18 @@ export function updateSectionVisibility() {
}
}
if ($musicPlayerContainer) {
if (extensionSettings.enableSpotifyMusic) {
$musicPlayerContainer.show();
} else {
$musicPlayerContainer.hide();
}
}
// Show/hide dividers intelligently
// Divider after User Stats: shown if User Stats is visible AND at least one section after it is visible
const showDividerAfterStats = extensionSettings.showUserStats &&
(extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showQuests);
(extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
if (showDividerAfterStats) {
$('#rpg-divider-stats').show();
} else {
@@ -304,20 +313,28 @@ export function updateSectionVisibility() {
// Divider after Thoughts: shown if Thoughts is visible AND at least one section after it is visible
const showDividerAfterThoughts = extensionSettings.showCharacterThoughts &&
(extensionSettings.showInventory || extensionSettings.showQuests);
(extensionSettings.showInventory || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
if (showDividerAfterThoughts) {
$('#rpg-divider-thoughts').show();
} else {
$('#rpg-divider-thoughts').hide();
}
// Divider after Inventory: shown if Inventory is visible AND Quests is visible
const showDividerAfterInventory = extensionSettings.showInventory && extensionSettings.showQuests;
// Divider after Inventory: shown if Inventory is visible AND (Quests or Music) is visible
const showDividerAfterInventory = extensionSettings.showInventory && (extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
if (showDividerAfterInventory) {
$('#rpg-divider-inventory').show();
} else {
$('#rpg-divider-inventory').hide();
}
// Divider after Quests: shown if Quests is visible AND Music is visible
const showDividerAfterQuests = extensionSettings.showQuests && extensionSettings.enableSpotifyMusic;
if (showDividerAfterQuests) {
$('#rpg-divider-quests').show();
} else {
$('#rpg-divider-quests').hide();
}
}
/**
+10 -1
View File
@@ -4,7 +4,7 @@
*/
import { extensionSettings } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js';
import { DEFAULT_HTML_PROMPT } from '../generation/promptBuilder.js';
import { DEFAULT_HTML_PROMPT, DEFAULT_SPOTIFY_PROMPT } from '../generation/promptBuilder.js';
let $editorModal = null;
let tempPrompts = null; // Temporary prompts for cancel functionality
@@ -12,6 +12,7 @@ let tempPrompts = null; // Temporary prompts for cancel functionality
// Default prompts
const DEFAULT_PROMPTS = {
html: DEFAULT_HTML_PROMPT,
spotify: DEFAULT_SPOTIFY_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, but 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.
@@ -97,6 +98,7 @@ function openPromptsEditor() {
// Create temporary copy for cancel functionality
tempPrompts = {
html: extensionSettings.customHtmlPrompt || '',
spotify: extensionSettings.customSpotifyPrompt || '',
plotRandom: extensionSettings.customPlotRandomPrompt || '',
plotNatural: extensionSettings.customPlotNaturalPrompt || '',
avatar: extensionSettings.avatarLLMCustomInstruction || '',
@@ -107,6 +109,7 @@ function openPromptsEditor() {
// Load current values or defaults
$('#rpg-prompt-html').val(extensionSettings.customHtmlPrompt || DEFAULT_PROMPTS.html);
$('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify);
$('#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);
@@ -141,6 +144,7 @@ function closePromptsEditor() {
*/
function savePrompts() {
extensionSettings.customHtmlPrompt = $('#rpg-prompt-html').val().trim();
extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').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();
@@ -164,6 +168,9 @@ function restorePromptToDefault(promptType) {
case 'html':
extensionSettings.customHtmlPrompt = '';
break;
case 'spotify':
extensionSettings.customSpotifyPrompt = '';
break;
case 'plotRandom':
extensionSettings.customPlotRandomPrompt = '';
break;
@@ -192,6 +199,7 @@ function restorePromptToDefault(promptType) {
*/
function restoreAllToDefaults() {
$('#rpg-prompt-html').val(DEFAULT_PROMPTS.html);
$('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify);
$('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom);
$('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural);
$('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar);
@@ -201,6 +209,7 @@ function restoreAllToDefaults() {
// Clear all custom prompts
extensionSettings.customHtmlPrompt = '';
extensionSettings.customSpotifyPrompt = '';
extensionSettings.customPlotRandomPrompt = '';
extensionSettings.customPlotNaturalPrompt = '';
extensionSettings.avatarLLMCustomInstruction = '';
+78
View File
@@ -0,0 +1,78 @@
/**
* Snowflakes Effect Module
* Creates and manages animated snowflakes overlay
*/
import { extensionSettings } from '../../core/state.js';
let snowflakesContainer = null;
/**
* Create snowflakes container and snowflakes
*/
function createSnowflakes() {
if (snowflakesContainer) return; // Already created
// Create container
snowflakesContainer = document.createElement('div');
snowflakesContainer.className = 'rpg-snowflakes-container';
// Create 50 snowflakes with random positions
for (let i = 0; i < 50; i++) {
const snowflake = document.createElement('div');
snowflake.className = 'rpg-snowflake';
snowflake.textContent = '❄';
// Random horizontal position
snowflake.style.left = `${Math.random() * 100}%`;
// Random animation delay for staggered effect
snowflake.style.animationDelay = `${Math.random() * 10}s`;
// Random animation duration (between 10-20s)
snowflake.style.animationDuration = `${10 + Math.random() * 10}s`;
snowflakesContainer.appendChild(snowflake);
}
document.body.appendChild(snowflakesContainer);
}
/**
* Remove snowflakes container
*/
function removeSnowflakes() {
if (snowflakesContainer) {
snowflakesContainer.remove();
snowflakesContainer = null;
}
}
/**
* Toggle snowflakes effect
*/
export function toggleSnowflakes(enabled) {
if (enabled) {
createSnowflakes();
} else {
removeSnowflakes();
}
}
/**
* Initialize snowflakes based on saved state
*/
export function initSnowflakes() {
const enabled = extensionSettings.enableSnowflakes || false;
if (enabled) {
createSnowflakes();
}
}
/**
* Clean up snowflakes
*/
export function cleanupSnowflakes() {
removeSnowflakes();
}
+21
View File
@@ -76,6 +76,27 @@ export function toggleAnimations() {
}
}
/**
* Updates visibility of feature toggles in main panel based on settings
*/
export function updateFeatureTogglesVisibility() {
const $featuresRow = $('#rpg-features-row');
const $htmlToggle = $('#rpg-html-toggle-wrapper');
const $spotifyToggle = $('#rpg-spotify-toggle-wrapper');
const $snowflakesToggle = $('#rpg-snowflakes-toggle-wrapper');
// Show/hide individual toggles
$htmlToggle.toggle(extensionSettings.showHtmlToggle);
$spotifyToggle.toggle(extensionSettings.showSpotifyToggle);
$snowflakesToggle.toggle(extensionSettings.showSnowflakesToggle);
// Hide entire row if all toggles are hidden
const anyVisible = extensionSettings.showHtmlToggle ||
extensionSettings.showSpotifyToggle ||
extensionSettings.showSnowflakesToggle;
$featuresRow.toggle(anyVisible);
}
/**
* Updates the settings popup theme in real-time.
* Backwards compatible wrapper for SettingsModal class.