Compare commits

...

6 Commits

Author SHA1 Message Date
Spicy_Marinara ddc02d9bbc Release v3.3.2: Fix auto-update on chat switch & restore character removal 2026-01-09 10:04:29 +01:00
Spicy Marinara 659b5bb82b Merge pull request #87 from tomt610/fix/quest-removal-sync
Fix: Sync quest changes to committedTrackerData
2026-01-09 09:29:28 +01:00
tomt610 5f72e6f549 Fix: Sync quest changes to committedTrackerData
When manually adding/editing/removing quests via UI, the changes were
only saved to extensionSettings but not to committedTrackerData.userStats.
This caused the AI to see stale quest data on the next external server
update, resulting in removed quests reappearing.

- Add syncQuestsToCommittedData() function to update JSON quest data
- Call sync and saveChatData() on all quest modification actions
- Imports committedTrackerData, lastGeneratedData, saveChatData
2026-01-09 00:23:23 +00:00
Spicy_Marinara 0d71dcca04 v3.3.1: Fix Recent Events reading from lastGeneratedData and add desktop thought panel collapse 2026-01-08 23:29:18 +01:00
Spicy Marinara 39e2a07829 Merge pull request #85 from tomt610/feature/update-complete-event
Add event emission when tracker update completes
2026-01-08 23:18:10 +01:00
tomt610 dedfead59e Add event emission when tracker update completes
Emits 'rpg_companion_update_complete' event after updateRPGData() finishes.
This allows other extensions (like Context Prewarm) to hook into the
completion of tracker updates and perform actions afterward.

The event is emitted in the finally block, so it fires regardless of
success or failure, after isGenerating is reset.
2026-01-08 22:12:06 +00:00
12 changed files with 268 additions and 74 deletions
+5 -7
View File
@@ -7,15 +7,13 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New ## 🆕 What's New
### v3.3.0 ### v3.3.2
- Small upgrades to the combat system. - Fixed the auto-generation triggering on switching/starting new chats in separate generation mode.
- Regex fix. - Restored the option to remove generated characters from the panel.
- Fixed External API logic.
- Even more minor bug fixes.
**Special thanks to all the other contributors for this project:** **Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, and Amauragis. Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
## 📥 Installation ## 📥 Installation
@@ -268,7 +266,7 @@ If you enjoy this extension, consider supporting development:
## 🙏 Credits ## 🙏 Credits
**Contributors:** **Contributors:**
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, and Amauragis. SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
## 🚀 Planned Features ## 🚀 Planned Features
+1
View File
@@ -64,6 +64,7 @@ import { renderInfoBox, updateInfoBoxField } from './src/systems/rendering/infoB
import { import {
renderThoughts, renderThoughts,
updateCharacterField, updateCharacterField,
removeCharacter,
updateChatThoughts, updateChatThoughts,
createThoughtPanel createThoughtPanel
} from './src/systems/rendering/thoughts.js'; } from './src/systems/rendering/thoughts.js';
+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.3.0", "version": "3.3.2",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
} }
+2 -2
View File
@@ -43,12 +43,12 @@
<i class="fa-solid fa-users"></i> <strong>Contributors:</strong> <i class="fa-solid fa-users"></i> <strong>Contributors:</strong>
</div> </div>
<div style="opacity: 0.8; font-size: 0.9em;"> <div style="opacity: 0.8; font-size: 0.9em;">
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, and Amauragis. SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
</div> </div>
</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.2.6 v3.3.2
</div> </div>
</div> </div>
</div> </div>
+10
View File
@@ -308,6 +308,12 @@ export let isGenerating = false;
*/ */
export let isPlotProgression = false; export let isPlotProgression = false;
/**
* Flag indicating if we're actively expecting a new message from generation
* (as opposed to loading chat history)
*/
export let isAwaitingNewMessage = false;
/** /**
* Temporary storage for pending dice roll (not saved until user clicks "Save Roll") * Temporary storage for pending dice roll (not saved until user clicks "Save Roll")
*/ */
@@ -408,6 +414,10 @@ export function setIsPlotProgression(value) {
isPlotProgression = value; isPlotProgression = value;
} }
export function setIsAwaitingNewMessage(value) {
isAwaitingNewMessage = value;
}
export function setPendingDiceRoll(roll) { export function setPendingDiceRoll(roll) {
pendingDiceRoll = roll; pendingDiceRoll = roll;
} }
+9 -1
View File
@@ -3,8 +3,12 @@
* Handles API calls for RPG tracker generation * Handles API calls for RPG tracker generation
*/ */
import { generateRaw, chat } from '../../../../../../../script.js'; import { generateRaw, chat, eventSource } from '../../../../../../../script.js';
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
// Custom event name for when RPG Companion finishes updating tracker data
// Other extensions can listen for this event to know when RPG Companion is done
export const RPG_COMPANION_UPDATE_COMPLETE = 'rpg_companion_update_complete';
import { import {
extensionSettings, extensionSettings,
lastGeneratedData, lastGeneratedData,
@@ -397,6 +401,10 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
// This ensures the flag persists through both main generation AND tracker generation // This ensures the flag persists through both main generation AND tracker generation
// console.log('[RPG Companion] 🔄 Tracker generation complete - resetting lastActionWasSwipe to false'); // console.log('[RPG Companion] 🔄 Tracker generation complete - resetting lastActionWasSwipe to false');
setLastActionWasSwipe(false); setLastActionWasSwipe(false);
// Emit event for other extensions to know RPG Companion has finished updating
console.debug('[RPG Companion] Emitting RPG_COMPANION_UPDATE_COMPLETE event');
eventSource.emit(RPG_COMPANION_UPDATE_COMPLETE);
} }
} }
+12 -1
View File
@@ -13,9 +13,11 @@ import {
committedTrackerData, committedTrackerData,
lastActionWasSwipe, lastActionWasSwipe,
isPlotProgression, isPlotProgression,
isAwaitingNewMessage,
setLastActionWasSwipe, setLastActionWasSwipe,
setIsPlotProgression, setIsPlotProgression,
setIsGenerating, setIsGenerating,
setIsAwaitingNewMessage,
updateLastGeneratedData, updateLastGeneratedData,
updateCommittedTrackerData, updateCommittedTrackerData,
$musicPlayerContainer $musicPlayerContainer
@@ -105,6 +107,10 @@ export function onMessageSent() {
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent (after placeholder check)'); // console.log('[RPG Companion] 🟢 EVENT: onMessageSent (after placeholder check)');
// console.log('[RPG Companion] 🟢 NOTE: lastActionWasSwipe will be reset in onMessageReceived after generation completes'); // console.log('[RPG Companion] 🟢 NOTE: lastActionWasSwipe will be reset in onMessageReceived after generation completes');
// Set flag to indicate we're expecting a new message from generation
// This allows auto-update to distinguish between new generations and loading chat history
setIsAwaitingNewMessage(true);
// For separate mode with auto-update disabled, commit displayed tracker // For separate mode with auto-update disabled, commit displayed tracker
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) { if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
if (lastGeneratedData.userStats || lastGeneratedData.infoBox || lastGeneratedData.characterThoughts) { if (lastGeneratedData.userStats || lastGeneratedData.infoBox || lastGeneratedData.characterThoughts) {
@@ -250,13 +256,17 @@ export async function onMessageReceived(data) {
} }
// Trigger auto-update if enabled (for both separate and external modes) // Trigger auto-update if enabled (for both separate and external modes)
if (extensionSettings.autoUpdate) { // Only trigger if this is a newly generated message, not loading chat history
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
setTimeout(async () => { setTimeout(async () => {
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
}, 500); }, 500);
} }
} }
// Reset the awaiting flag after processing the message
setIsAwaitingNewMessage(false);
// Reset the swipe flag after generation completes // Reset the swipe flag after generation completes
// This ensures that if the user swiped → auto-reply generated → flag is now cleared // This ensures that if the user swiped → auto-reply generated → flag is now cleared
// so the next user message will be treated as a new message (not a swipe) // so the next user message will be treated as a new message (not a swipe)
@@ -340,6 +350,7 @@ export function onMessageSwiped(messageIndex) {
if (!isExistingSwipe) { if (!isExistingSwipe) {
// This is a NEW swipe that will trigger generation // This is a NEW swipe that will trigger generation
setLastActionWasSwipe(true); setLastActionWasSwipe(true);
setIsAwaitingNewMessage(true);
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true'); // console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
} 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
+5 -5
View File
@@ -498,19 +498,19 @@ export function renderInfoBox() {
if (config?.widgets?.recentEvents?.enabled) { if (config?.widgets?.recentEvents?.enabled) {
// Parse Recent Events from infoBox (supports both JSON and text formats) // Parse Recent Events from infoBox (supports both JSON and text formats)
let recentEvents = []; let recentEvents = [];
if (committedTrackerData.infoBox) { if (infoBoxData) {
// Try JSON format first // Try JSON format first
try { try {
const parsed = typeof committedTrackerData.infoBox === 'string' const parsed = typeof infoBoxData === 'string'
? JSON.parse(committedTrackerData.infoBox) ? JSON.parse(infoBoxData)
: committedTrackerData.infoBox; : infoBoxData;
if (parsed && Array.isArray(parsed.recentEvents)) { if (parsed && Array.isArray(parsed.recentEvents)) {
recentEvents = parsed.recentEvents; recentEvents = parsed.recentEvents;
} }
} catch (e) { } catch (e) {
// Fall back to old text format // Fall back to old text format
const recentEventsLine = committedTrackerData.infoBox.split('\n').find(line => line.startsWith('Recent Events:')); const recentEventsLine = infoBoxData.split('\n').find(line => line.startsWith('Recent Events:'));
if (recentEventsLine) { if (recentEventsLine) {
const eventsString = recentEventsLine.replace('Recent Events:', '').trim(); const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
if (eventsString) { if (eventsString) {
+39 -2
View File
@@ -3,10 +3,35 @@
* Handles UI rendering for quests system (main and optional quests) * Handles UI rendering for quests system (main and optional quests)
*/ */
import { extensionSettings, $questsContainer } from '../../core/state.js'; import { extensionSettings, $questsContainer, committedTrackerData, lastGeneratedData } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js'; import { saveSettings, saveChatData } from '../../core/persistence.js';
import { isItemLocked, setItemLock } from '../generation/lockManager.js'; import { isItemLocked, setItemLock } from '../generation/lockManager.js';
/**
* Syncs the current extensionSettings.quests to committedTrackerData.userStats
* This ensures quest changes made via UI are reflected in the data sent to AI
*/
function syncQuestsToCommittedData() {
const currentData = committedTrackerData.userStats || lastGeneratedData.userStats;
if (!currentData) return;
const trimmed = currentData.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
const jsonData = JSON.parse(currentData);
if (jsonData && typeof jsonData === 'object') {
// Update quests in the JSON data
jsonData.quests = extensionSettings.quests || { main: 'None', optional: [] };
const updatedJSON = JSON.stringify(jsonData, null, 2);
committedTrackerData.userStats = updatedJSON;
lastGeneratedData.userStats = updatedJSON;
}
} catch (e) {
console.warn('[RPG Quests] Failed to sync quests to committed data:', e);
}
}
}
/** /**
* Helper to generate lock icon HTML if setting is enabled * Helper to generate lock icon HTML if setting is enabled
* @param {string} tracker - Tracker name * @param {string} tracker - Tracker name
@@ -250,7 +275,10 @@ function attachQuestEventHandlers() {
} }
extensionSettings.quests.optional.push(questTitle); extensionSettings.quests.optional.push(questTitle);
} }
// Sync quest changes to committedTrackerData so AI sees the addition
syncQuestsToCommittedData();
saveSettings(); saveSettings();
saveChatData();
renderQuests(); renderQuests();
} }
}); });
@@ -278,7 +306,10 @@ function attachQuestEventHandlers() {
if (questTitle) { if (questTitle) {
extensionSettings.quests.main = questTitle; extensionSettings.quests.main = questTitle;
// Sync quest changes to committedTrackerData so AI sees the edit
syncQuestsToCommittedData();
saveSettings(); saveSettings();
saveChatData();
renderQuests(); renderQuests();
} }
}); });
@@ -293,7 +324,10 @@ function attachQuestEventHandlers() {
} else { } else {
extensionSettings.quests.optional.splice(index, 1); extensionSettings.quests.optional.splice(index, 1);
} }
// Sync quest changes to committedTrackerData so AI sees the removal
syncQuestsToCommittedData();
saveSettings(); saveSettings();
saveChatData();
renderQuests(); renderQuests();
}); });
@@ -306,7 +340,10 @@ function attachQuestEventHandlers() {
if (newTitle && field === 'optional' && index !== undefined) { if (newTitle && field === 'optional' && index !== undefined) {
extensionSettings.quests.optional[index] = newTitle; extensionSettings.quests.optional[index] = newTitle;
// Sync quest changes to committedTrackerData so AI sees the edit
syncQuestsToCommittedData();
saveSettings(); saveSettings();
saveChatData();
} }
}); });
+174 -50
View File
@@ -391,50 +391,10 @@ export function renderThoughts() {
debugLog('[RPG Thoughts] ==================== BUILDING HTML ===================='); debugLog('[RPG Thoughts] ==================== BUILDING HTML ====================');
debugLog('[RPG Thoughts] Starting HTML generation for', presentCharacters.length + ' characters'); debugLog('[RPG Thoughts] Starting HTML generation for', presentCharacters.length + ' characters');
// If no characters parsed, show a placeholder editable card // If no characters parsed, show empty state (no placeholder)
if (presentCharacters.length === 0) { if (presentCharacters.length === 0) {
debugLog('[RPG Thoughts] ⚠ No characters parsed - showing placeholder card'); debugLog('[RPG Thoughts] ⚠ No characters parsed - showing empty state');
// Get default character portrait html += '<div class="rpg-thoughts-content"></div>';
let defaultPortrait = FALLBACK_AVATAR_DATA_URI;
let defaultName = 'Character';
if (this_chid !== undefined && characters[this_chid]) {
if (characters[this_chid].avatar && characters[this_chid].avatar !== 'none') {
const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar);
if (thumbnailUrl) {
defaultPortrait = thumbnailUrl;
}
}
defaultName = characters[this_chid].name || 'Character';
}
html += '<div class="rpg-thoughts-content">';
html += `
<div class="rpg-character-card" data-character-name="${defaultName}">
<div class="rpg-character-avatar">
<img src="${defaultPortrait}" alt="${defaultName}" onerror="this.style.opacity='0.5';this.onerror=null;" />
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">⚖️</div>
</div>
<div class="rpg-character-info">
<div class="rpg-character-header">
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="emoji" title="Click to edit emoji">😊</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="name" title="Click to edit name">${defaultName}</span>
</div>
`;
// Add custom fields dynamically
for (const field of enabledFields) {
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
html += `
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="${field.name}" title="Click to edit ${field.name}"></div>
`;
}
html += `
</div>
</div>
`;
html += '</div>';
} else { } else {
html += '<div class="rpg-thoughts-content">'; html += '<div class="rpg-thoughts-content">';
@@ -540,6 +500,7 @@ export function renderThoughts() {
<div class="rpg-character-header"> <div class="rpg-character-header">
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span> <span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span> <span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
<button class="rpg-character-remove" data-character="${char.name}" title="Remove character">×</button>
</div> </div>
`; `;
@@ -650,6 +611,15 @@ export function renderThoughts() {
saveSettings(); saveSettings();
}); });
// Add event listener for character remove button
$thoughtsContainer.find('.rpg-character-remove').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
const characterName = $(this).data('character');
removeCharacter(characterName);
});
// Add event listener for avatar upload clicks // Add event listener for avatar upload clicks
$thoughtsContainer.find('.rpg-avatar-upload').on('click', function(e) { $thoughtsContainer.find('.rpg-avatar-upload').on('click', function(e) {
e.preventDefault(); e.preventDefault();
@@ -703,6 +673,121 @@ export function renderThoughts() {
} }
} }
/**
* Removes a character from Present Characters data and re-renders.
*
* @param {string} characterName - Name of the character to remove
*/
export function removeCharacter(characterName) {
if (!lastGeneratedData.characterThoughts) {
return;
}
// 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 - remove character from array
let characters = Array.isArray(parsedData) ? parsedData : parsedData.characters;
characters = characters.filter(char => char.name !== characterName);
if (Array.isArray(parsedData)) {
parsedData = characters;
} else {
parsedData.characters = characters;
}
const updatedJSON = JSON.stringify(parsedData, null, 2);
lastGeneratedData.characterThoughts = updatedJSON;
committedTrackerData.characterThoughts = updatedJSON;
} else {
// Text format - remove character block
const lines = lastGeneratedData.characterThoughts.split('\n');
const dividerIndex = lines.findIndex(line => line.includes('---'));
if (dividerIndex === -1) return;
// Find the character block to remove
let startLineIndex = -1;
let endLineIndex = -1;
for (let i = dividerIndex + 1; i < lines.length; i++) {
const line = lines[i].trim();
// Check if this is the start of the character block
if (line.startsWith('Name:')) {
const nameMatch = line.match(/^Name:\s*(.+)/);
if (nameMatch && nameMatch[1].trim() === characterName) {
startLineIndex = i;
}
}
// If we found the start, look for the end
if (startLineIndex !== -1 && i > startLineIndex) {
// End of block is either another "Name:" line or end of content
if (line.startsWith('Name:') || i === lines.length - 1) {
endLineIndex = line.startsWith('Name:') ? i - 1 : i;
// Remove empty lines at the end of the block
while (endLineIndex > startLineIndex && !lines[endLineIndex].trim()) {
endLineIndex--;
}
break;
}
}
}
// Remove the character block
if (startLineIndex !== -1 && endLineIndex !== -1) {
lines.splice(startLineIndex, endLineIndex - startLineIndex + 1);
// Remove empty lines after removal to keep formatting clean
let i = startLineIndex;
while (i < lines.length && !lines[i].trim()) {
lines.splice(i, 1);
}
}
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 updated character list
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.
@@ -1847,9 +1932,9 @@ export function createThoughtPanel($message, thoughtsArray) {
}, 100); }, 100);
} }
} else { } else {
// Desktop: show panel, hide icon with class // Desktop: always start with panel expanded on page load/refresh
$thoughtPanel.css('display', 'block'); $thoughtPanel.css('display', 'block');
$thoughtIcon.addClass('rpg-force-hide'); $thoughtIcon.addClass('rpg-force-hide').removeClass('rpg-collapsed-desktop');
} }
// Handle viewport changes between mobile and desktop // Handle viewport changes between mobile and desktop
@@ -1880,24 +1965,62 @@ export function createThoughtPanel($message, thoughtsArray) {
wasMobileView = isMobileNow; wasMobileView = isMobileNow;
}); });
// Close button functionality (mobile only) - support both click and touch // Close button functionality - support both click and touch
$thoughtPanel.find('.rpg-thought-close').on('click touchend', function(e) { $thoughtPanel.find('.rpg-thought-close').on('click touchend', function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// Only hide/show in mobile view
if (window.innerWidth <= 1000) { const isMobileView = window.innerWidth <= 1000;
if (isMobileView) {
// Mobile: hide panel and show icon
$thoughtPanel.fadeOut(200, function() { $thoughtPanel.fadeOut(200, function() {
// Make sure icon is visible and clean state when panel closes (use selector, not variable) // Make sure icon is visible and clean state when panel closes (use selector, not variable)
const $icon = $('#rpg-thought-icon'); const $icon = $('#rpg-thought-icon');
$icon.removeClass('rpg-hidden dragging'); $icon.removeClass('rpg-hidden dragging');
$icon.data('just-dragged', false); $icon.data('just-dragged', false);
}); });
} else {
// Desktop: collapse to icon at panel position
const panelRect = $thoughtPanel[0].getBoundingClientRect();
const $icon = $('#rpg-thought-icon');
// Position icon where the panel is
$icon.css({
top: `${panelRect.top}px`,
left: isRightPanel ? `${panelRect.left}px` : 'auto',
right: isRightPanel ? 'auto' : `${window.innerWidth - panelRect.right}px`
});
// Mark as collapsed desktop state (session only, not persisted)
$icon.addClass('rpg-collapsed-desktop');
// Hide panel and show icon
$thoughtPanel.fadeOut(200, function() {
$icon.removeClass('rpg-hidden rpg-force-hide');
});
} }
}); });
// Icon click/tap to show panel (mobile only) // Icon click/tap to show panel
const handleThoughtIconTap = function(e) { const handleThoughtIconTap = function(e) {
// Skip if we just finished dragging const isMobileView = window.innerWidth <= 1000;
const $icon = $('#rpg-thought-icon');
// Desktop collapsed state: expand panel and hide icon
if (!isMobileView && $icon.hasClass('rpg-collapsed-desktop')) {
e.preventDefault();
e.stopPropagation();
// Remove collapsed state (no need to save, state is session-only)
$icon.addClass('rpg-force-hide').removeClass('rpg-collapsed-desktop');
// Show panel
$('#rpg-thought-panel').fadeIn(200);
return;
}
// Skip if we just finished dragging (mobile only)
if ($thoughtIcon.data('just-dragged')) { if ($thoughtIcon.data('just-dragged')) {
return; return;
} }
@@ -1995,3 +2118,4 @@ export function createThoughtPanel($message, thoughtsArray) {
} }
}); });
} }
+9 -4
View File
@@ -4732,11 +4732,16 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
} }
} }
/* Force hide class for desktop mode - overrides media query */ /* Force hide class for desktop mode - overrides media query (unless collapsed) */
#rpg-thought-icon.rpg-force-hide { #rpg-thought-icon.rpg-force-hide:not(.rpg-collapsed-desktop) {
display: none !important; display: none !important;
} }
/* When collapsed in desktop, show icon */
#rpg-thought-icon.rpg-collapsed-desktop {
display: flex !important;
}
/* Hidden state that allows transitions */ /* Hidden state that allows transitions */
#rpg-thought-icon.rpg-hidden { #rpg-thought-icon.rpg-hidden {
opacity: 0; opacity: 0;
@@ -4780,10 +4785,10 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
color: var(--rpg-highlight, #e94560); color: var(--rpg-highlight, #e94560);
} }
/* Hide close button in desktop view (panel doesn't close) */ /* Show close button in desktop view for collapsing */
@media (min-width: 1001px) { @media (min-width: 1001px) {
.rpg-thought-close { .rpg-thought-close {
display: none !important; display: flex !important;
} }
} }
+1 -1
View File
@@ -948,7 +948,7 @@
<h4 style="margin-top: 20px; margin-bottom: 10px;"><strong>Special thanks to all the other contributors for this project:</strong></h4> <h4 style="margin-top: 20px; margin-bottom: 10px;"><strong>Special thanks to all the other contributors for this project:</strong></h4>
<p style="margin-left: 20px; line-height: 1.6;"> <p style="margin-left: 20px; line-height: 1.6;">
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude (???), IDeathByte, Chungchandev, Joenunezb, and Amauragis. Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude (???), IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
</p> </p>
<div style="margin-top: 20px; text-align: center;"> <div style="margin-top: 20px; text-align: center;">