diff --git a/README.md b/README.md
index 6fd067e..b5f7e34 100644
--- a/README.md
+++ b/README.md
@@ -7,12 +7,9 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New
-### v3.7.1
+### v3.7.2
-- Improved instructions for the model to generate dynamic weather.
-- Small fixes and updates to descriptions.
-- Fixed a scroll/viewport bug that moved everything up after you edited fields.
-- Changed the display of avatars for present characters.
+- Minor bug fixes
**Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein!
diff --git a/manifest.json b/manifest.json
index 26ae263..ff16a2a 100644
--- a/manifest.json
+++ b/manifest.json
@@ -6,6 +6,6 @@
"js": "index.js",
"css": "style.css",
"author": "Marinara",
- "version": "3.7.1",
+ "version": "3.7.2",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
}
diff --git a/settings.html b/settings.html
index 4acebed..6226b02 100644
--- a/settings.html
+++ b/settings.html
@@ -49,7 +49,7 @@
- v3.7.1
+ v3.7.2
diff --git a/src/systems/generation/jsonPromptHelpers.js b/src/systems/generation/jsonPromptHelpers.js
index 26478cf..2d66deb 100644
--- a/src/systems/generation/jsonPromptHelpers.js
+++ b/src/systems/generation/jsonPromptHelpers.js
@@ -21,6 +21,19 @@ function toSnakeCase(name) {
.replace(/^_+|_+$/g, '');
}
+/**
+ * Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
+ * Parenthetical content is treated as a description/hint, not part of the key.
+ * Example: "Conditions (up to 5 traits)" -> "conditions"
+ * Example: "Status Effects" -> "status_effects"
+ * @param {string} name - Field name, possibly with parenthetical description
+ * @returns {string} snake_case key from the base name only
+ */
+function toFieldKey(name) {
+ const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
+ return toSnakeCase(baseName);
+}
+
/**
* Builds User Stats JSON format instruction
* @returns {string} JSON format instruction for user stats
@@ -60,12 +73,12 @@ export function buildUserStatsJSONInstruction() {
if (customFields.length > 0) {
for (let i = 0; i < customFields.length; i++) {
const fieldName = customFields[i].toLowerCase();
- const fieldKey = toSnakeCase(fieldName);
+ const fieldKey = toFieldKey(fieldName);
const comma = (i === customFields.length - 1 && !userStatsConfig.statusSection.showMoodEmoji) ? '' : (userStatsConfig.statusSection.showMoodEmoji || i < customFields.length - 1 ? ',\n' : '\n');
if (i === 0 && userStatsConfig.statusSection.showMoodEmoji) {
instruction += ',\n';
}
- instruction += ` "${fieldKey}": "[${fieldName}1, ${fieldName}2]"${comma}`;
+ instruction += ` "${fieldKey}": "[${fieldName}]"${comma}`;
}
}
if (!userStatsConfig.statusSection.showMoodEmoji && customFields.length > 0) {
diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js
index 8bb0da0..7a5eaab 100644
--- a/src/systems/generation/parser.js
+++ b/src/systems/generation/parser.js
@@ -9,6 +9,20 @@ import { saveSettings } from '../../core/persistence.js';
import { extractInventory } from './inventoryParser.js';
import { repairJSON } from '../../utils/jsonRepair.js';
+/**
+ * Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
+ * Example: "Conditions (up to 5 traits)" -> "conditions"
+ * @param {string} name - Field name, possibly with parenthetical description
+ * @returns {string} snake_case key from the base name only
+ */
+function toFieldKey(name) {
+ const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
+ return baseName
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '_')
+ .replace(/^_+|_+$/g, '');
+}
+
/**
* Helper to separate emoji from text in a string
* Handles cases where there's no comma or space after emoji
@@ -559,10 +573,12 @@ export function parseUserStats(statsText) {
const trackerConfig = extensionSettings.trackerConfig;
const customFields = trackerConfig?.userStats?.statusSection?.customFields || [];
for (const fieldName of customFields) {
- const fieldKey = fieldName.toLowerCase();
- if (statsData.status[fieldKey]) {
- extensionSettings.userStats[fieldKey] = statsData.status[fieldKey];
- // console.log(`[RPG Parser] ✓ Set ${fieldKey} =`, statsData.status[fieldKey]);
+ const fieldKey = toFieldKey(fieldName);
+ // Try the base key first (e.g., "conditions"), then fall back to full lowercase name
+ const value = statsData.status[fieldKey] || statsData.status[fieldName.toLowerCase()];
+ if (value) {
+ extensionSettings.userStats[fieldKey] = value;
+ // console.log(`[RPG Parser] ✓ Set ${fieldKey} =`, value);
}
}
}
diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js
index 0876c78..5cc4a93 100644
--- a/src/systems/rendering/thoughts.js
+++ b/src/systems/rendering/thoughts.js
@@ -153,11 +153,20 @@ function namesMatch(cardName, aiName) {
* Displays character cards with avatars, relationship badges, and traits.
* Includes event listeners for editable character fields.
*/
-export function renderThoughts() {
+export function renderThoughts({ preserveScroll = false } = {}) {
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
return;
}
+ // Save scroll position before re-render if requested
+ let savedContentScroll = 0;
+ if (preserveScroll) {
+ const $content = $thoughtsContainer.find('.rpg-thoughts-content');
+ if ($content.length) {
+ savedContentScroll = $content[0].scrollTop;
+ }
+ }
+
// Don't render if no data exists (e.g., after cache clear)
const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts;
if (!thoughtsData) {
@@ -714,6 +723,14 @@ export function renderThoughts() {
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
}
+ // Restore scroll position after re-render
+ if (preserveScroll) {
+ const $content = $thoughtsContainer.find('.rpg-thoughts-content');
+ if ($content.length) {
+ $content[0].scrollTop = savedContentScroll;
+ }
+ }
+
// Update chat overlay if enabled
if (extensionSettings.showThoughtsInChat) {
updateChatThoughts();
@@ -1147,8 +1164,8 @@ export function updateCharacterField(characterName, field, value) {
// console.log('[RPG Companion] JSON format updated successfully');
// console.log('[RPG Companion] Updated data:', lastGeneratedData.characterThoughts);
- // Re-render the thoughts panel to show updated value
- renderThoughts();
+ // Re-render the thoughts panel to show updated value (preserve scroll position)
+ renderThoughts({ preserveScroll: true });
// Update chat thought overlays if editing thoughts
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
diff --git a/src/systems/rendering/userStats.js b/src/systems/rendering/userStats.js
index fa9ddaa..7706f7a 100644
--- a/src/systems/rendering/userStats.js
+++ b/src/systems/rendering/userStats.js
@@ -23,6 +23,20 @@ import { isItemLocked, setItemLock } from '../generation/lockManager.js';
import { updateFabWidgets } from '../ui/mobile.js';
import { getStatBarColors } from '../ui/theme.js';
+/**
+ * Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
+ * Example: "Conditions (up to 5 traits)" -> "conditions"
+ * @param {string} name - Field name, possibly with parenthetical description
+ * @returns {string} snake_case key from the base name only
+ */
+function toFieldKey(name) {
+ const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
+ return baseName
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '_')
+ .replace(/^_+|_+$/g, '');
+}
+
/**
* Builds the user stats text string using custom stat names
* @returns {string} Formatted stats text for tracker
@@ -107,7 +121,7 @@ function updateUserStatsData() {
// Then, add any other numeric stats from extensionSettings that aren't in config
// (these could be custom stats the AI added or disabled stats)
const customFields = config.statusSection?.customFields || [];
- const excludeFields = new Set(['mood', ...customFields.map(f => f.toLowerCase()), 'inventory', 'skills', 'level']);
+ const excludeFields = new Set(['mood', ...customFields.map(f => toFieldKey(f)), 'inventory', 'skills', 'level']);
Object.entries(stats).forEach(([key, value]) => {
if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') {
statsArray.push({
@@ -127,7 +141,7 @@ function updateUserStatsData() {
// Add all custom status fields
for (const fieldName of customFields) {
- const fieldKey = fieldName.toLowerCase();
+ const fieldKey = toFieldKey(fieldName);
jsonData.status[fieldKey] = stats[fieldKey] || 'None';
}
@@ -334,7 +348,7 @@ export function renderUserStats() {
// Render custom status fields
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
for (const fieldName of config.statusSection.customFields) {
- const fieldKey = fieldName.toLowerCase();
+ const fieldKey = toFieldKey(fieldName);
let fieldValue = stats[fieldKey] || 'None';
// Handle array format (from JSON)
if (Array.isArray(fieldValue)) {