Compare commits

..

1 Commits

Author SHA1 Message Date
Spicy_Marinara fd8afba7f2 v3.6.1: Dynamic combat actions and bug fixes
- Added dynamic action updates: AI can now modify available attacks/items based on combat state
- Items decrease when used, abilities change based on status effects
- Fixed event delegation for encounter buttons to work reliably on mobile
- Fixed multiple JSON parsing validation errors
- Added proper dialogue handling in combat summaries
- UI now re-renders action buttons when actions change
- Improved prompt instructions for item quantities and dynamic actions
2026-01-13 19:21:49 +01:00
10 changed files with 348 additions and 808 deletions
+4 -3
View File
@@ -7,10 +7,11 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New ## 🆕 What's New
### v3.6.2 ### v3.6.1
- Various bug fixes. - Fixed the bugs in the encounter system where you couldn't use the buttons after performing any custom action.
- Added the ability to add present characters manually. - Improved combat actions and made them dynamic, depending on the current situation.
- Added Russian as a supported language.
**Special thanks to all the other contributors for this project:** **Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610. Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
+2
View File
@@ -151,6 +151,7 @@ import {
onMessageReceived, onMessageReceived,
onCharacterChanged, onCharacterChanged,
onMessageSwiped, onMessageSwiped,
onMessageDeleted,
updatePersonaAvatar, updatePersonaAvatar,
clearExtensionPrompts, clearExtensionPrompts,
onGenerationEnded, onGenerationEnded,
@@ -1254,6 +1255,7 @@ jQuery(async () => {
[event_types.GENERATION_ENDED]: onGenerationEnded, [event_types.GENERATION_ENDED]: onGenerationEnded,
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts], [event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts],
[event_types.MESSAGE_SWIPED]: onMessageSwiped, [event_types.MESSAGE_SWIPED]: onMessageSwiped,
[event_types.MESSAGE_DELETED]: onMessageDeleted,
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar, [event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar [event_types.SETTINGS_UPDATED]: updatePersonaAvatar
}); });
+1 -1
View File
@@ -6,6 +6,6 @@
"js": "index.js", "js": "index.js",
"css": "style.css", "css": "style.css",
"author": "Marinara", "author": "Marinara",
"version": "3.6.3", "version": "3.6.1",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
} }
+1 -2
View File
@@ -15,7 +15,6 @@
<select id="rpg-companion-language-select" class="text_pole"> <select id="rpg-companion-language-select" class="text_pole">
<option value="en" data-i18n-key="settings.language.option.en">English</option> <option value="en" data-i18n-key="settings.language.option.en">English</option>
<option value="zh-tw" data-i18n-key="settings.language.option.zh-tw">繁體中文</option> <option value="zh-tw" data-i18n-key="settings.language.option.zh-tw">繁體中文</option>
<option value="ru" data-i18n-key="settings.language.option.ru">Русский</option>
</select> </select>
</div> </div>
@@ -49,7 +48,7 @@
</div> </div>
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;"> <div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
v3.6.2 v3.6.1
</div> </div>
</div> </div>
</div> </div>
+8 -30
View File
@@ -546,33 +546,12 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
if (trackerType === 'userStats') { if (trackerType === 'userStats') {
formatted += `${userName}'s Stats:\n`; formatted += `${userName}'s Stats:\n`;
// Get display mode and custom stats config for maxValue lookup
const userStatsConfig = extensionSettings.trackerConfig?.userStats;
const displayMode = userStatsConfig?.statsDisplayMode || 'percentage';
const customStats = userStatsConfig?.customStats || [];
// Helper to get maxValue for a stat by id
const getMaxValue = (statId) => {
const statConfig = customStats.find(s => s.id === statId);
return statConfig?.maxValue || 100;
};
// Helper to format stat value based on display mode
const formatStatValue = (value, statId) => {
if (displayMode === 'number') {
const maxValue = getMaxValue(statId);
return `${value}/${maxValue}`;
}
return value;
};
// Handle stats array format: [{id, name, value}, ...] // Handle stats array format: [{id, name, value}, ...]
if (data.stats && Array.isArray(data.stats)) { if (data.stats && Array.isArray(data.stats)) {
for (const stat of data.stats) { for (const stat of data.stats) {
if (stat && stat.value !== undefined) { if (stat && stat.value !== undefined) {
const statName = stat.name || (stat.id ? stat.id.charAt(0).toUpperCase() + stat.id.slice(1) : 'Unknown'); const statName = stat.name || (stat.id ? stat.id.charAt(0).toUpperCase() + stat.id.slice(1) : 'Unknown');
const statId = stat.id || statName.toLowerCase(); formatted += `${statName}: ${stat.value}\n`;
formatted += `${statName}: ${formatStatValue(stat.value, statId)}\n`;
} }
} }
} else { } else {
@@ -585,7 +564,7 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
const value = getValue(data[statName]); const value = getValue(data[statName]);
if (value) { if (value) {
const displayName = statName.charAt(0).toUpperCase() + statName.slice(1); const displayName = statName.charAt(0).toUpperCase() + statName.slice(1);
formatted += `${displayName}: ${formatStatValue(value, statName)}\n`; formatted += `${displayName}: ${value}\n`;
} }
} }
} }
@@ -594,7 +573,7 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
if (!statFieldOrder.includes(key) && !specialFields.includes(key) && typeof value === 'number') { if (!statFieldOrder.includes(key) && !specialFields.includes(key) && typeof value === 'number') {
const displayName = key.charAt(0).toUpperCase() + key.slice(1); const displayName = key.charAt(0).toUpperCase() + key.slice(1);
formatted += `${displayName}: ${formatStatValue(getValue(value), key)}\n`; formatted += `${displayName}: ${getValue(value)}\n`;
} }
} }
} }
@@ -725,14 +704,13 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
} }
} }
// Relationship - check both Relationship (new format) and relationship (old format) // Relationship
const relationshipValue = char.Relationship || char.relationship; if (char.relationship) {
if (relationshipValue) {
let relValue; let relValue;
if (typeof relationshipValue === 'object' && !Array.isArray(relationshipValue) && 'status' in relationshipValue) { if (typeof char.relationship === 'object' && !Array.isArray(char.relationship) && 'status' in char.relationship) {
relValue = getValue(relationshipValue.status); relValue = getValue(char.relationship.status);
} else { } else {
relValue = getValue(relationshipValue); relValue = getValue(char.relationship);
} }
if (relValue) formatted += ` Relationship: ${relValue}\n`; if (relValue) formatted += ` Relationship: ${relValue}\n`;
} }
+202 -19
View File
@@ -359,7 +359,8 @@ export function onMessageSwiped(messageIndex) {
// console.log('[RPG Companion] 🔵 EVENT: onMessageSwiped at index:', messageIndex); // console.log('[RPG Companion] 🔵 EVENT: onMessageSwiped at index:', messageIndex);
// Get the message that was swiped // Get the message that was swiped
const message = chat[messageIndex]; const currentChat = getContext().chat;
const message = currentChat[messageIndex];
if (!message || message.is_user) { if (!message || message.is_user) {
// console.log('[RPG Companion] 🔵 Ignoring swipe - message is user or undefined'); // console.log('[RPG Companion] 🔵 Ignoring swipe - message is user or undefined');
return; return;
@@ -379,40 +380,80 @@ export function onMessageSwiped(messageIndex) {
setLastActionWasSwipe(true); setLastActionWasSwipe(true);
setIsAwaitingNewMessage(true); setIsAwaitingNewMessage(true);
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true'); // console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
// CRITICAL: For new swipes, commit data from the PREVIOUS assistant message
// This ensures the LLM gets context from BEFORE the message being regenerated,
// not the message itself (which would cause time/story to advance incorrectly)
for (let i = messageIndex - 1; i >= 0; i--) {
const prevMessage = currentChat[i];
if (!prevMessage.is_user && prevMessage.extra?.rpg_companion_swipes) {
const prevSwipeId = prevMessage.swipe_id || 0;
const prevSwipeData = prevMessage.extra.rpg_companion_swipes[prevSwipeId];
if (prevSwipeData) {
// console.log('[RPG Companion] 🔵 Committing tracker data from PREVIOUS message at index', i);
committedTrackerData.userStats = prevSwipeData.userStats || null;
committedTrackerData.infoBox = prevSwipeData.infoBox || null;
committedTrackerData.characterThoughts = prevSwipeData.characterThoughts || null;
} else {
// Previous message has no swipe data - clear committed data
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
}
break;
}
// If we hit index 0 without finding a previous assistant message, clear committed data
if (i === 0) {
// console.log('[RPG Companion] 🔵 No previous assistant message found - clearing committed data');
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
}
}
// Edge case: if messageIndex is 0 (first message being swiped), clear committed data
if (messageIndex === 0) {
// console.log('[RPG Companion] 🔵 Swiping first message - clearing committed data');
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
}
// For new swipes, also update lastGeneratedData to match committed data
// This ensures the UI shows the "before" state while waiting for the new response
lastGeneratedData.userStats = committedTrackerData.userStats;
lastGeneratedData.infoBox = committedTrackerData.infoBox;
lastGeneratedData.characterThoughts = committedTrackerData.characterThoughts;
// Parse user stats for display if available
if (committedTrackerData.userStats) {
parseUserStats(committedTrackerData.userStats);
}
} else { } else {
// This is navigating to an EXISTING swipe - don't change the flag // This is navigating to an EXISTING swipe - don't change the flag
// console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe); // console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe);
}
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId); // Load RPG data for this existing swipe for DISPLAY purposes
// IMPORTANT: onMessageSwiped is for DISPLAY only!
// lastGeneratedData is for DISPLAY, committedTrackerData is for GENERATION
// It's safe to load swipe data into lastGeneratedData - it won't be committed due to !lastActionWasSwipe check
if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) { if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) {
const swipeData = message.extra.rpg_companion_swipes[currentSwipeId]; const swipeData = message.extra.rpg_companion_swipes[currentSwipeId];
// Load swipe data into lastGeneratedData for display (both modes) // Load swipe data into lastGeneratedData for display
lastGeneratedData.userStats = swipeData.userStats || null; lastGeneratedData.userStats = swipeData.userStats || null;
lastGeneratedData.infoBox = swipeData.infoBox || null; lastGeneratedData.infoBox = swipeData.infoBox || null;
// Normalize characterThoughts to string format (for backward compatibility with old object format)
if (swipeData.characterThoughts && typeof swipeData.characterThoughts === 'object') {
lastGeneratedData.characterThoughts = JSON.stringify(swipeData.characterThoughts, null, 2);
} else {
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null; lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
}
// DON'T parse user stats when loading swipe data // Parse user stats if available
// This would overwrite manually edited fields (like Conditions) with old swipe data if (swipeData.userStats) {
// The lastGeneratedData is loaded for display purposes only parseUserStats(swipeData.userStats);
// parseUserStats() updates extensionSettings.userStats which should only be modified }
// by new generations or manual edits, not by swipe navigation
// console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId); // console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId);
} else { } else {
// console.log('[RPG Companion] ️ No stored data for swipe:', currentSwipeId); // console.log('[RPG Companion] ️ No stored data for swipe:', currentSwipeId);
} }
}
// Re-render the panels // Re-render the panels
renderUserStats(); renderUserStats();
@@ -426,6 +467,148 @@ export function onMessageSwiped(messageIndex) {
updateChatThoughts(); updateChatThoughts();
} }
/**
* Event handler for when a message is deleted.
* Restores RPG state from the last assistant message with RPG data,
* or clears state if no messages remain.
*/
export function onMessageDeleted(messageIndex) {
if (!extensionSettings.enabled) {
return;
}
// console.log('[RPG Companion] 🗑️ EVENT: onMessageDeleted at index:', messageIndex);
const context = getContext();
const currentChat = context.chat;
// If chat is empty, clear all RPG state
if (!currentChat || currentChat.length === 0) {
// console.log('[RPG Companion] 🗑️ Chat is empty - clearing RPG state');
lastGeneratedData.userStats = null;
lastGeneratedData.infoBox = null;
lastGeneratedData.characterThoughts = null;
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
// Clear parsed stats from extensionSettings
if (extensionSettings.userStats) {
extensionSettings.userStats = null;
}
// Re-render empty panels
renderUserStats();
renderInfoBox();
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update FAB widgets and strip widgets
updateFabWidgets();
updateStripWidgets();
// Update chat thought overlays (removes any remaining)
updateChatThoughts();
// Save the cleared state
saveChatData();
return;
}
// Find the last assistant message with RPG data
for (let i = currentChat.length - 1; i >= 0; i--) {
const message = currentChat[i];
if (!message.is_user && message.extra?.rpg_companion_swipes) {
const swipeId = message.swipe_id || 0;
const swipeData = message.extra.rpg_companion_swipes[swipeId];
if (swipeData) {
// Check if this is the same data we already have displayed
const sameUserStats = lastGeneratedData.userStats === swipeData.userStats;
const sameInfoBox = lastGeneratedData.infoBox === swipeData.infoBox;
const sameThoughts = lastGeneratedData.characterThoughts === swipeData.characterThoughts;
if (sameUserStats && sameInfoBox && sameThoughts) {
// console.log('[RPG Companion] 🗑️ RPG state already matches last message - no restore needed');
return;
}
// console.log('[RPG Companion] 🗑️ Restoring RPG state from message index', i, 'swipe', swipeId);
// Restore state from this message
lastGeneratedData.userStats = swipeData.userStats || null;
lastGeneratedData.infoBox = swipeData.infoBox || null;
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
// Also update committed data so next generation uses correct context
committedTrackerData.userStats = swipeData.userStats || null;
committedTrackerData.infoBox = swipeData.infoBox || null;
committedTrackerData.characterThoughts = swipeData.characterThoughts || null;
// Parse user stats if available
if (swipeData.userStats) {
parseUserStats(swipeData.userStats);
}
// Re-render panels with restored data
renderUserStats();
renderInfoBox();
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update FAB widgets and strip widgets
updateFabWidgets();
updateStripWidgets();
// Update chat thought overlays
updateChatThoughts();
// Save the restored state
saveChatData();
return;
}
}
}
// No assistant message with RPG data found - clear state
// console.log('[RPG Companion] 🗑️ No assistant message with RPG data found - clearing state');
lastGeneratedData.userStats = null;
lastGeneratedData.infoBox = null;
lastGeneratedData.characterThoughts = null;
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
// Clear parsed stats
if (extensionSettings.userStats) {
extensionSettings.userStats = null;
}
// Re-render empty panels
renderUserStats();
renderInfoBox();
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update FAB widgets and strip widgets
updateFabWidgets();
updateStripWidgets();
// Update chat thought overlays
updateChatThoughts();
// Save the cleared state
saveChatData();
}
/** /**
* Update the persona avatar image when user switches personas * Update the persona avatar image when user switches personas
*/ */
+36 -252
View File
@@ -512,20 +512,17 @@ export function renderThoughts() {
const fieldNameLower = field.name.toLowerCase(); const fieldNameLower = field.name.toLowerCase();
// Skip lock icons for thoughts field // Skip lock icons for thoughts field
const showLock = !fieldNameLower.includes('thought'); const showLock = !fieldNameLower.includes('thought');
// Add placeholder for empty fields
const placeholder = fieldValue ? '' : `data-placeholder="${field.name}"`;
const emptyClass = fieldValue ? '' : ' rpg-empty-field';
if (showLock) { if (showLock) {
const lockIconHtml = getLockIconHtml('characters', `${char.name}.${field.name}`); const lockIconHtml = getLockIconHtml('characters', `${char.name}.${field.name}`);
html += ` html += `
<div class="rpg-character-field rpg-character-${fieldId}" style="position: relative;"> <div class="rpg-character-field rpg-character-${fieldId}" style="position: relative;">
${lockIconHtml} ${lockIconHtml}
<span class="rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}" ${placeholder}>${fieldValue}</span> <span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</span>
</div> </div>
`; `;
} else { } else {
html += ` html += `
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}" ${placeholder}>${fieldValue}</div> <div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</div>
`; `;
} }
} }
@@ -567,16 +564,6 @@ export function renderThoughts() {
} }
debugLog('[RPG Thoughts] Finished building all character cards'); debugLog('[RPG Thoughts] Finished building all character cards');
// Add "Add Character" button if data exists (inside rpg-thoughts-content)
if (presentCharacters.length > 0) {
html += `
<button class="rpg-add-character-btn" title="Add a new character">
<i class="fa-solid fa-plus"></i> Add Character
</button>
`;
}
html += '</div>'; html += '</div>';
} }
@@ -675,31 +662,6 @@ export function renderThoughts() {
fileInput.trigger('click'); fileInput.trigger('click');
}); });
// Add event listener for "Add Character" button (support both click and touch for mobile)
$thoughtsContainer.find('.rpg-add-character-btn').on('click touchend', function(e) {
e.preventDefault();
e.stopPropagation();
addNewCharacter();
});
// Handle empty field focus - remove placeholder styling on focus
$thoughtsContainer.find('.rpg-editable.rpg-empty-field').on('focus', function() {
$(this).removeClass('rpg-empty-field');
$(this).removeAttr('data-placeholder');
});
// Restore placeholder if field becomes empty on blur (after the main blur handler)
$thoughtsContainer.find('.rpg-editable').on('blur', function() {
const $this = $(this);
if (!$this.text().trim()) {
const field = $this.data('field');
if (field) {
$this.addClass('rpg-empty-field');
$this.attr('data-placeholder', field);
}
}
});
// Remove updating class after animation // Remove updating class after animation
if (extensionSettings.enableAnimations) { if (extensionSettings.enableAnimations) {
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600); setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
@@ -826,136 +788,6 @@ export function removeCharacter(characterName) {
renderThoughts(); renderThoughts();
} }
/**
* Adds a new blank character to Present Characters data.
* Creates a character with empty fields based on the tracker template.
*/
export function addNewCharacter() {
const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters;
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
const characterStats = presentCharsConfig?.characterStats;
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
const hasRelationship = presentCharsConfig?.relationshipFields?.length > 0;
// Check if data is in JSON format
let isJSON = false;
let parsedData = null;
try {
parsedData = typeof lastGeneratedData.characterThoughts === 'string'
? JSON.parse(lastGeneratedData.characterThoughts)
: lastGeneratedData.characterThoughts;
if (Array.isArray(parsedData) || (parsedData && parsedData.characters)) {
isJSON = true;
}
} catch (e) {
// Not JSON, treat as text format
}
if (isJSON) {
// JSON format - add new character object
const charactersArray = Array.isArray(parsedData) ? parsedData : (parsedData.characters || []);
const newCharacter = {
name: 'New Character',
emoji: '👤',
details: {}
};
// Add all enabled custom fields as empty
for (const field of enabledFields) {
newCharacter.details[field.name] = '';
}
// Add relationship if enabled
if (hasRelationship) {
newCharacter.relationship = 'Neutral';
}
// Add stats if enabled
if (enabledCharStats.length > 0) {
newCharacter.stats = {};
for (const stat of enabledCharStats) {
newCharacter.stats[stat.name] = 100;
}
}
charactersArray.push(newCharacter);
// Save back as JSON string
lastGeneratedData.characterThoughts = JSON.stringify(
Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray },
null,
2
);
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
} else {
// Text format - add new character block
const lines = lastGeneratedData.characterThoughts.split('\n');
const dividerIndex = lines.findIndex(line => line.includes('---'));
if (dividerIndex >= 0) {
const newCharacterLines = ['- New Character'];
// Add custom detail fields as standalone lines
for (const customField of enabledFields) {
newCharacterLines.push(` ${customField.name}: `);
}
// Add Relationship field if enabled
if (hasRelationship) {
newCharacterLines.push(` Relationship: Neutral`);
}
// Add Stats if enabled
if (enabledCharStats.length > 0) {
const statsParts = enabledCharStats.map(s => `${s.name}: 100%`);
newCharacterLines.push(` Stats: ${statsParts.join(' | ')}`);
}
// Find the last character and add after it, or after divider if no characters
let insertIndex = dividerIndex + 1;
for (let i = lines.length - 1; i > dividerIndex; i--) {
if (lines[i].trim().startsWith('- ')) {
// Find the end of this character block
insertIndex = i + 1;
while (insertIndex < lines.length && lines[insertIndex].trim() && !lines[insertIndex].trim().startsWith('- ')) {
insertIndex++;
}
break;
}
}
lines.splice(insertIndex, 0, ...newCharacterLines);
lastGeneratedData.characterThoughts = lines.join('\n');
committedTrackerData.characterThoughts = lines.join('\n');
}
}
// Update message swipe data
const chat = getContext().chat;
if (chat && chat.length > 0) {
for (let i = chat.length - 1; i >= 0; i--) {
const message = chat[i];
if (!message.is_user) {
if (message.extra && message.extra.rpg_companion_swipes) {
const swipeId = message.swipe_id || 0;
if (message.extra.rpg_companion_swipes[swipeId]) {
message.extra.rpg_companion_swipes[swipeId].characterThoughts = lastGeneratedData.characterThoughts;
}
}
break;
}
}
}
saveChatData();
// Re-render to show new character
renderThoughts();
}
/** /**
* Updates a specific character field in Present Characters data and re-renders. * Updates a specific character field in Present Characters data and re-renders.
* Works with the new multi-line format. * Works with the new multi-line format.
@@ -1023,27 +855,18 @@ export function updateCharacterField(characterName, field, value) {
} else if (field === 'emoji') { } else if (field === 'emoji') {
char.emoji = value; char.emoji = value;
} else if (field === 'Relationship') { } else if (field === 'Relationship') {
// Store relationship in the correct nested format // Store relationship as text, converting emoji if needed
// Remove old flat format if it exists
if (char.Relationship) {
delete char.Relationship;
}
// First check if it's an emoji → convert to text // First check if it's an emoji → convert to text
let relationshipValue;
if (emojiToRelationship[value]) { if (emojiToRelationship[value]) {
relationshipValue = emojiToRelationship[value]; char.Relationship = emojiToRelationship[value];
} else { } else {
// It's text - find matching relationship name (case-insensitive) // It's text - find matching relationship name (case-insensitive)
const matchingRelationship = Object.keys(relationshipEmojis).find( const matchingRelationship = Object.keys(relationshipEmojis).find(
name => name.toLowerCase() === value.toLowerCase() name => name.toLowerCase() === value.toLowerCase()
); );
relationshipValue = matchingRelationship || value; char.Relationship = matchingRelationship || value;
} }
// console.log('[RPG Companion] After update - char.Relationship:', char.Relationship);
// Store in the correct nested format
char.relationship = { status: relationshipValue };
// console.log('[RPG Companion] After update - char.relationship:', char.relationship);
// console.log('[RPG Companion] relationshipEmojis:', relationshipEmojis); // console.log('[RPG Companion] relationshipEmojis:', relationshipEmojis);
// console.log('[RPG Companion] emojiToRelationship:', emojiToRelationship); // console.log('[RPG Companion] emojiToRelationship:', emojiToRelationship);
} else if (field.toLowerCase() === 'thoughts' || field === (presentCharsConfig?.thoughts?.name || 'Thoughts')) { } else if (field.toLowerCase() === 'thoughts' || field === (presentCharsConfig?.thoughts?.name || 'Thoughts')) {
@@ -1059,44 +882,15 @@ export function updateCharacterField(characterName, field, value) {
numValue = Math.max(0, Math.min(100, numValue)); numValue = Math.max(0, Math.min(100, numValue));
char.stats[field] = numValue; char.stats[field] = numValue;
} else { } else {
// It's a custom detail field - store in details object // It's a custom detail field
if (!char.details) char.details = {}; if (!char.details) char.details = {};
char.details[field] = value; char.details[field] = value;
// Clean up snake_case version if it exists (from AI generation)
const fieldKey = toSnakeCase(field);
if (fieldKey !== field && char.details[fieldKey] !== undefined) {
delete char.details[fieldKey];
}
// Clean up old root-level field if it exists (from v2 format)
if (char[field] !== undefined && field !== 'name' && field !== 'emoji') {
delete char[field];
}
if (char[fieldKey] !== undefined && fieldKey !== 'name' && fieldKey !== 'emoji') {
delete char[fieldKey];
} }
} }
} }
// Clean up ALL duplicate snake_case fields in details (not just the edited field) // Save back to lastGeneratedData
// This prevents duplicates from AI-generated data lastGeneratedData.characterThoughts = Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray };
if (char.details) {
for (const customField of enabledFields) {
const fieldName = customField.name;
const snakeCaseKey = toSnakeCase(fieldName);
// If both versions exist, keep the properly-cased one and remove snake_case
if (snakeCaseKey !== fieldName &&
char.details[fieldName] !== undefined &&
char.details[snakeCaseKey] !== undefined) {
delete char.details[snakeCaseKey];
}
}
}
}
// Save back to lastGeneratedData as JSON string (consistent with infoBox and userStats)
lastGeneratedData.characterThoughts = JSON.stringify(Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray }, null, 2);
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts; committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
// console.log('[RPG Companion] Saved to lastGeneratedData.characterThoughts:', JSON.stringify(lastGeneratedData.characterThoughts)); // console.log('[RPG Companion] Saved to lastGeneratedData.characterThoughts:', JSON.stringify(lastGeneratedData.characterThoughts));
@@ -1177,9 +971,6 @@ export function updateCharacterField(characterName, field, value) {
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts'; const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
const isThoughtsField = field.toLowerCase() === 'thoughts' || field === thoughtsFieldName; const isThoughtsField = field.toLowerCase() === 'thoughts' || field === thoughtsFieldName;
// Track if field was found and updated
let fieldUpdated = false;
// First pass: check if Stats line exists and update other fields // First pass: check if Stats line exists and update other fields
for (let i = characterStartIndex; i < characterEndIndex; i++) { for (let i = characterStartIndex; i < characterEndIndex; i++) {
const line = lines[i].trim(); const line = lines[i].trim();
@@ -1187,37 +978,35 @@ export function updateCharacterField(characterName, field, value) {
if (line.startsWith('Stats:')) { if (line.startsWith('Stats:')) {
statsLineExists = true; statsLineExists = true;
statsLineIndex = i; statsLineIndex = i;
continue; // Skip to next line
} }
// Check for name update
if (field === 'name' && line.startsWith('- ')) { if (field === 'name' && line.startsWith('- ')) {
lines[i] = `- ${value}`; lines[i] = `- ${value}`;
fieldUpdated = true;
continue;
} }
else if (field === 'emoji' && line.startsWith('Details:')) {
// Check for Relationship field const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim());
if (field === 'Relationship' && line.startsWith('Relationship:')) { parts[0] = value;
lines[i] = `Details: ${parts.join(' | ')}`;
}
else if (line.startsWith('Details:')) {
const fieldIndex = enabledFields.findIndex(f => f.name === field);
if (fieldIndex !== -1) {
const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim());
if (parts.length > fieldIndex + 1) {
parts[fieldIndex + 1] = value;
lines[i] = `Details: ${parts.join(' | ')}`;
}
}
}
else if (field === 'Relationship' && line.startsWith('Relationship:')) {
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' }; const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
const relationshipValue = emojiToRelationship[value] || value; const relationshipValue = emojiToRelationship[value] || value;
lines[i] = `Relationship: ${relationshipValue}`; lines[i] = `Relationship: ${relationshipValue}`;
fieldUpdated = true;
continue;
} }
else if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
// Check for Thoughts field // Update thoughts field
if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) { lines[i] = `${thoughtsFieldName}: ${value}`;
lines[i] = ` ${thoughtsFieldName}: ${value}`; // console.log('[RPG Companion] Updated thoughts:', lines[i]);
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)
} }
} }
@@ -1284,28 +1073,23 @@ export function updateCharacterField(characterName, field, value) {
} }
} }
} else { } else {
// Create new character block (v3 text format only) // Create new character block
const dividerIndex = lines.findIndex(line => line.includes('---')); const dividerIndex = lines.findIndex(line => line.includes('---'));
if (dividerIndex >= 0) { if (dividerIndex >= 0) {
const newCharacterLines = [`- ${characterName}`]; const newCharacterLines = [`- ${characterName}`];
// Add custom detail fields as standalone lines let detailsParts = [field === 'emoji' ? value : '😊'];
for (const customField of enabledFields) { for (let i = 0; i < enabledFields.length; i++) {
if (field === customField.name) { detailsParts.push(field === enabledFields[i].name ? value : '');
newCharacterLines.push(` ${customField.name}: ${value}`);
} else {
newCharacterLines.push(` ${customField.name}: `);
}
} }
newCharacterLines.push(`Details: ${detailsParts.join(' | ')}`);
// Add Relationship field if enabled
if (presentCharsConfig?.relationshipFields?.length > 0) { if (presentCharsConfig?.relationshipFields?.length > 0) {
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' }; const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
const relationshipValue = field === 'Relationship' ? (emojiToRelationship[value] || value) : 'Neutral'; const relationshipValue = field === 'Relationship' ? (emojiToRelationship[value] || value) : 'Neutral';
newCharacterLines.push(` Relationship: ${relationshipValue}`); newCharacterLines.push(`Relationship: ${relationshipValue}`);
} }
// Add Stats if enabled
if (enabledCharStats.length > 0) { if (enabledCharStats.length > 0) {
const statsParts = enabledCharStats.map(s => { const statsParts = enabledCharStats.map(s => {
if (field === s.name) { if (field === s.name) {
@@ -1320,7 +1104,7 @@ export function updateCharacterField(characterName, field, value) {
} }
return `${s.name}: 0%`; return `${s.name}: 0%`;
}); });
newCharacterLines.push(` Stats: ${statsParts.join(' | ')}`); newCharacterLines.push(`Stats: ${statsParts.join(' | ')}`);
} }
lines.splice(dividerIndex + 1, 0, ...newCharacterLines); lines.splice(dividerIndex + 1, 0, ...newCharacterLines);
+31 -170
View File
@@ -105,48 +105,41 @@ function getCurrentTime() {
return null; 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 * Parse weather text to determine effect type
*/ */
function parseWeatherType(weatherText) { function parseWeatherType(weatherText) {
if (!weatherText) return "none"; if (!weatherText) return 'none';
const text = weatherText.toLowerCase(); const text = weatherText.toLowerCase();
for (const language of Object.values(WEATHER_PATTERNS_BY_LANGUAGE)) { // Check for specific weather conditions (order matters - check combined effects first)
for (const { id, patterns } of language) { if (text.includes('blizzard')) {
if (patterns.some(p => text.includes(p))) { return 'blizzard'; // Snow + Wind
return id;
} }
if (text.includes('storm') || text.includes('thunder') || text.includes('lightning')) {
return 'storm'; // Rain + Lightning
} }
if (text.includes('wind') || text.includes('breeze') || text.includes('gust') || text.includes('gale')) {
return 'wind';
}
if (text.includes('snow') || text.includes('flurries')) {
return 'snow';
}
if (text.includes('rain') || text.includes('drizzle') || text.includes('shower')) {
return 'rain';
}
if (text.includes('mist') || text.includes('fog') || text.includes('haze')) {
return 'mist';
}
if (text.includes('sunny') || text.includes('clear') || text.includes('bright')) {
return 'sunny';
}
if (text.includes('cloud') || text.includes('overcast') || text.includes('indoor') || text.includes('inside')) {
return 'none';
} }
return "none"; return 'none';
} }
/** /**
@@ -244,9 +237,9 @@ function createMist() {
* Returns { left: vw%, top: dvh% } * Returns { left: vw%, top: dvh% }
*/ */
function calculateSunPosition(hour) { function calculateSunPosition(hour) {
// Daytime is roughly 5 AM to 8 PM (5-20) // Daytime is roughly 6 AM to 8 PM (6-20)
// Map hour to position along an arc // Map hour to position along an arc
// 5 AM = far left, low | 12 PM = center, high | 8 PM = far right, low // 6 AM = far left, low | 12 PM = center, high | 6 PM = far right, low
if (hour === null) hour = 12; // Default to noon if unknown if (hour === null) hour = 12; // Default to noon if unknown
@@ -256,14 +249,14 @@ function calculateSunPosition(hour) {
// Normalize to 0-1 range (5 AM = 0, 20 PM = 1) // Normalize to 0-1 range (5 AM = 0, 20 PM = 1)
const progress = (clampedHour - 5) / 15; const progress = (clampedHour - 5) / 15;
// Horizontal position: 3% to 92% (left to right, wider range) // Horizontal position: 5% to 85% (left to right)
const left = 3 + progress * 89; const left = 5 + progress * 80;
// Vertical position: parabolic arc (high at noon, low at dawn/dusk) // Vertical position: parabolic arc (high at noon, low at dawn/dusk)
// At progress 0.5 (noon), top should be ~8% (high) // At progress 0.5 (noon), top should be ~8% (high)
// At progress 0 or 1, top should be ~40% (low, near horizon) // At progress 0 or 1, top should be ~35% (low, near horizon)
const normalizedProgress = (progress - 0.5) * 2; // -1 to 1 const normalizedProgress = (progress - 0.5) * 2; // -1 to 1
const top = 8 + 32 * (normalizedProgress * normalizedProgress); const top = 8 + 27 * (normalizedProgress * normalizedProgress);
return { left, top }; return { left, top };
} }
@@ -334,134 +327,6 @@ function createSunshine(hour) {
return container; return container;
} }
/**
* Create sunrise effect (dawn - warm orange/pink sky gradient with low sun)
*/
function createSunrise(hour) {
const container = document.createElement('div');
container.className = 'rpg-weather-particles rpg-sunrise-weather';
// Create sunrise gradient overlay
const sunriseOverlay = document.createElement('div');
sunriseOverlay.className = 'rpg-weather-particle rpg-sunrise-overlay';
container.appendChild(sunriseOverlay);
// Calculate sun position (rising from left horizon)
const sunPos = calculateSunPosition(hour);
// Create the rising sun
const sun = document.createElement('div');
sun.className = 'rpg-weather-particle rpg-clear-sun rpg-sunrise-sun';
sun.style.left = `${sunPos.left}vw`;
sun.style.top = `${sunPos.top}dvh`;
container.appendChild(sun);
// Create sun glow (more orange during sunrise)
const sunGlow = document.createElement('div');
sunGlow.className = 'rpg-weather-particle rpg-clear-sun-glow rpg-sunrise-glow';
sunGlow.style.left = `${sunPos.left}vw`;
sunGlow.style.top = `${sunPos.top}dvh`;
container.appendChild(sunGlow);
// Create horizon glow
const horizonGlow = document.createElement('div');
horizonGlow.className = 'rpg-weather-particle rpg-sunrise-horizon-glow';
container.appendChild(horizonGlow);
// Add some fading stars (still visible at dawn)
for (let i = 0; i < 15; i++) {
const star = document.createElement('div');
star.className = 'rpg-weather-particle rpg-night-star rpg-sunrise-fading-star';
star.style.left = `${Math.random() * 100}vw`;
star.style.top = `${Math.random() * 40}dvh`;
star.style.animationDelay = `${Math.random() * 3}s`;
const size = 1 + Math.random() * 1.5;
star.style.width = `${size}px`;
star.style.height = `${size}px`;
container.appendChild(star);
}
// Add some golden dust motes
for (let i = 0; i < 12; i++) {
const particle = document.createElement('div');
particle.className = 'rpg-weather-particle rpg-clear-dust-mote';
particle.style.left = `${Math.random() * 100}vw`;
particle.style.top = `${Math.random() * 100}dvh`;
particle.style.animationDelay = `${Math.random() * 15}s`;
particle.style.animationDuration = `${12 + Math.random() * 8}s`;
const size = 2 + Math.random() * 3;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
container.appendChild(particle);
}
return container;
}
/**
* Create sunset effect (dusk - warm red/purple sky gradient with low sun)
*/
function createSunset(hour) {
const container = document.createElement('div');
container.className = 'rpg-weather-particles rpg-sunset-weather';
// Create sunset gradient overlay
const sunsetOverlay = document.createElement('div');
sunsetOverlay.className = 'rpg-weather-particle rpg-sunset-overlay';
container.appendChild(sunsetOverlay);
// Calculate sun position (setting on right horizon)
const sunPos = calculateSunPosition(hour);
// Create the setting sun
const sun = document.createElement('div');
sun.className = 'rpg-weather-particle rpg-clear-sun rpg-sunset-sun';
sun.style.left = `${sunPos.left}vw`;
sun.style.top = `${sunPos.top}dvh`;
container.appendChild(sun);
// Create sun glow (more red during sunset)
const sunGlow = document.createElement('div');
sunGlow.className = 'rpg-weather-particle rpg-clear-sun-glow rpg-sunset-glow';
sunGlow.style.left = `${sunPos.left}vw`;
sunGlow.style.top = `${sunPos.top}dvh`;
container.appendChild(sunGlow);
// Create horizon glow
const horizonGlow = document.createElement('div');
horizonGlow.className = 'rpg-weather-particle rpg-sunset-horizon-glow';
container.appendChild(horizonGlow);
// Add some early stars (appearing at dusk)
for (let i = 0; i < 20; i++) {
const star = document.createElement('div');
star.className = 'rpg-weather-particle rpg-night-star rpg-sunset-emerging-star';
star.style.left = `${Math.random() * 100}vw`;
star.style.top = `${Math.random() * 50}dvh`;
star.style.animationDelay = `${Math.random() * 5}s`;
const size = 1 + Math.random() * 1.5;
star.style.width = `${size}px`;
star.style.height = `${size}px`;
container.appendChild(star);
}
// Add some golden/pink dust motes
for (let i = 0; i < 12; i++) {
const particle = document.createElement('div');
particle.className = 'rpg-weather-particle rpg-clear-dust-mote rpg-sunset-dust';
particle.style.left = `${Math.random() * 100}vw`;
particle.style.top = `${Math.random() * 100}dvh`;
particle.style.animationDelay = `${Math.random() * 15}s`;
particle.style.animationDuration = `${12 + Math.random() * 8}s`;
const size = 2 + Math.random() * 3;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
container.appendChild(particle);
}
return container;
}
/** /**
* Create clear nighttime weather effect with moon, stars, and fireflies * Create clear nighttime weather effect with moon, stars, and fireflies
*/ */
@@ -707,13 +572,9 @@ export function updateWeatherEffect() {
weatherContainer = createMist(); weatherContainer = createMist();
break; break;
case 'sunny': case 'sunny':
// Use appropriate effect based on time of day // Use nighttime effect for clear weather at night
if (timeOfDay === 'night') { if (timeOfDay === 'night') {
weatherContainer = createNighttime(hour); weatherContainer = createNighttime(hour);
} else if (timeOfDay === 'dawn') {
weatherContainer = createSunrise(hour);
} else if (timeOfDay === 'dusk') {
weatherContainer = createSunset(hour);
} else { } else {
weatherContainer = createSunshine(hour); weatherContainer = createSunshine(hour);
} }
+4 -272
View File
@@ -2329,40 +2329,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
transform: scale(0.95); transform: scale(0.95);
} }
/* Add Character Button (inside rpg-thoughts-content, after last character) */
.rpg-add-character-btn {
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
color: var(--rpg-text);
padding: 0;
margin: 0.5rem auto 0;
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: 500;
cursor: pointer;
border-radius: 3px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.25rem;
opacity: 0.6;
width: auto;
}
.rpg-add-character-btn:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
color: var(--rpg-bg);
opacity: 1;
}
.rpg-add-character-btn:active {
transform: scale(0.95);
}
.rpg-add-character-btn i {
font-size: 0.875em;
}
/* Character traits/status line and custom fields */ /* Character traits/status line and custom fields */
.rpg-character-traits, .rpg-character-traits,
.rpg-character-field { .rpg-character-field {
@@ -2374,20 +2340,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
word-wrap: break-word; word-wrap: break-word;
} }
/* Empty field placeholder using data-placeholder attribute */ /* Placeholder for empty editable character fields */
.rpg-editable.rpg-empty-field:empty::before { .rpg-character-field.rpg-editable:empty::before {
content: attr(data-placeholder); content: 'Click to edit...';
color: var(--rpg-highlight); color: var(--rpg-highlight);
opacity: 0.4; opacity: 0.5;
font-style: italic; font-style: italic;
pointer-events: none;
}
/* Ensure empty fields have minimum height for clickability */
.rpg-editable.rpg-empty-field {
min-height: 1.2em;
display: inline-block;
min-width: 3em;
} }
/* Character stats display */ /* Character stats display */
@@ -9940,200 +9898,6 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
} }
} }
/* ===== Sunrise Effects (Dawn) ===== */
.rpg-sunrise-weather {
overflow: hidden;
}
/* Sunrise sky gradient overlay */
.rpg-sunrise-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100dvh;
background: linear-gradient(to bottom,
rgba(40, 30, 80, 0.1) 0%,
rgba(120, 60, 120, 0.08) 15%,
rgba(200, 100, 100, 0.1) 35%,
rgba(255, 140, 100, 0.12) 55%,
rgba(255, 180, 120, 0.1) 75%,
rgba(255, 200, 150, 0.08) 100%);
animation: rpg-sunrise-sky-transition 30s ease-in-out infinite alternate;
pointer-events: none;
}
@keyframes rpg-sunrise-sky-transition {
0% {
opacity: 0.8;
}
100% {
opacity: 1;
}
}
/* Sunrise sun - more orange/red */
.rpg-sunrise-sun {
background: radial-gradient(circle at 40% 40%,
rgba(255, 255, 220, 1) 0%,
rgba(255, 220, 150, 1) 30%,
rgba(255, 160, 80, 0.9) 60%,
rgba(255, 100, 50, 0.6) 80%,
rgba(255, 80, 30, 0) 100%) !important;
box-shadow:
0 0 40px 15px rgba(255, 180, 100, 0.6),
0 0 80px 30px rgba(255, 140, 80, 0.4),
0 0 120px 50px rgba(255, 100, 50, 0.2) !important;
}
/* Sunrise sun glow - warm orange */
.rpg-sunrise-glow {
background: radial-gradient(circle at center,
rgba(255, 200, 150, 0.35) 0%,
rgba(255, 160, 100, 0.2) 30%,
rgba(255, 120, 80, 0.1) 50%,
transparent 70%) !important;
}
/* Horizon glow for sunrise */
.rpg-sunrise-horizon-glow {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 40dvh;
background: linear-gradient(to top,
rgba(255, 160, 100, 0.15) 0%,
rgba(255, 140, 90, 0.1) 20%,
rgba(255, 120, 80, 0.05) 50%,
rgba(255, 100, 70, 0.02) 75%,
transparent 100%);
animation: rpg-horizon-glow-pulse 8s ease-in-out infinite;
pointer-events: none;
}
@keyframes rpg-horizon-glow-pulse {
0%, 100% {
opacity: 0.7;
}
50% {
opacity: 1;
}
}
/* Fading stars at sunrise */
.rpg-sunrise-fading-star {
opacity: 0.3 !important;
animation: rpg-star-fade-out 4s ease-in-out infinite !important;
}
@keyframes rpg-star-fade-out {
0%, 100% {
opacity: 0.2;
}
50% {
opacity: 0.4;
}
}
/* ===== Sunset Effects (Dusk) ===== */
.rpg-sunset-weather {
overflow: hidden;
}
/* Sunset sky gradient overlay */
.rpg-sunset-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100dvh;
background: linear-gradient(to bottom,
rgba(30, 20, 60, 0.12) 0%,
rgba(80, 40, 100, 0.1) 15%,
rgba(150, 60, 90, 0.12) 30%,
rgba(220, 80, 70, 0.12) 50%,
rgba(255, 120, 80, 0.12) 70%,
rgba(255, 160, 100, 0.1) 85%,
rgba(255, 180, 120, 0.06) 100%);
animation: rpg-sunset-sky-transition 30s ease-in-out infinite alternate;
pointer-events: none;
}
@keyframes rpg-sunset-sky-transition {
0% {
opacity: 1;
}
100% {
opacity: 0.8;
}
}
/* Sunset sun - more red/deep orange */
.rpg-sunset-sun {
background: radial-gradient(circle at 40% 40%,
rgba(255, 240, 200, 1) 0%,
rgba(255, 180, 100, 1) 30%,
rgba(255, 120, 60, 0.9) 60%,
rgba(255, 80, 40, 0.6) 80%,
rgba(200, 50, 30, 0) 100%) !important;
box-shadow:
0 0 40px 15px rgba(255, 140, 80, 0.6),
0 0 80px 30px rgba(255, 100, 60, 0.4),
0 0 120px 50px rgba(200, 60, 40, 0.2) !important;
}
/* Sunset sun glow - deep orange/red */
.rpg-sunset-glow {
background: radial-gradient(circle at center,
rgba(255, 160, 120, 0.35) 0%,
rgba(255, 120, 80, 0.2) 30%,
rgba(200, 80, 60, 0.1) 50%,
transparent 70%) !important;
}
/* Horizon glow for sunset */
.rpg-sunset-horizon-glow {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 45dvh;
background: linear-gradient(to top,
rgba(255, 120, 60, 0.18) 0%,
rgba(255, 100, 50, 0.12) 20%,
rgba(220, 70, 50, 0.06) 45%,
rgba(150, 50, 60, 0.02) 70%,
transparent 100%);
animation: rpg-horizon-glow-pulse 8s ease-in-out infinite;
pointer-events: none;
}
/* Emerging stars at sunset */
.rpg-sunset-emerging-star {
animation: rpg-star-emerge 5s ease-in-out infinite !important;
}
@keyframes rpg-star-emerge {
0%, 100% {
opacity: 0.3;
}
50% {
opacity: 0.7;
}
}
/* Sunset dust motes - pinkish tint */
.rpg-sunset-dust {
background: radial-gradient(circle at 30% 30%,
rgba(255, 200, 180, 0.9) 0%,
rgba(255, 180, 160, 0.6) 50%,
rgba(255, 160, 140, 0) 100%) !important;
box-shadow: 0 0 6px 2px rgba(255, 180, 160, 0.4) !important;
}
/* Lens flare effect */ /* Lens flare effect */
.rpg-clear-lens-flare { .rpg-clear-lens-flare {
position: fixed; position: fixed;
@@ -10508,12 +10272,6 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
.rpg-night-shooting-star { .rpg-night-shooting-star {
animation-duration: 18s; animation-duration: 18s;
} }
/* Sunrise/Sunset mobile optimizations */
.rpg-sunrise-horizon-glow,
.rpg-sunset-horizon-glow {
height: 35%;
}
} }
/* Foreground mode - reduced opacity for celestial bodies to not obstruct content */ /* Foreground mode - reduced opacity for celestial bodies to not obstruct content */
@@ -10561,32 +10319,6 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
opacity: 0.6; opacity: 0.6;
} }
/* Sunrise/Sunset foreground mode */
.rpg-weather-foreground .rpg-sunrise-overlay,
.rpg-weather-foreground .rpg-sunset-overlay {
opacity: 0.4;
}
.rpg-weather-foreground .rpg-sunrise-horizon-glow,
.rpg-weather-foreground .rpg-sunset-horizon-glow {
opacity: 0.3;
}
.rpg-weather-foreground .rpg-sunrise-sun,
.rpg-weather-foreground .rpg-sunset-sun {
opacity: 0.5 !important;
}
.rpg-weather-foreground .rpg-sunrise-glow,
.rpg-weather-foreground .rpg-sunset-glow {
opacity: 0.3 !important;
}
.rpg-weather-foreground .rpg-sunrise-fading-star,
.rpg-weather-foreground .rpg-sunset-emerging-star {
opacity: 0.2 !important;
}
/* Lightning flash effect */ /* Lightning flash effect */
.rpg-lightning { .rpg-lightning {
position: fixed; position: fixed;