diff --git a/README.md b/README.md
index b0d1938..d9c1d3f 100644
--- a/README.md
+++ b/README.md
@@ -7,14 +7,15 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New
-### v3.5.0
+### v3.6.0
-- Various fixes and upgrades to the existing systems.
-- Repaired Auto-generate Avatars.
-- Fixed Dynami Weather on mobiles.
-- Added an option to decide where to display the weather effects (foreground or background).
-- Unified CSS.
-- Dice rolls are now sent with the prompt even if you don't have Attributes toggled on.
+- You can now choose whether stats are displayed as percentages or numbers.
+- Added collapsed strip widgets for desktop.
+- Added new effects for the dynamic weather.
+- Changed the displayed clock format in the Info Box.
+- Fixed customized status field to work.
+- Fixed date format toggles.
+- Minor CSS and bug fixes.
**Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
diff --git a/manifest.json b/manifest.json
index 85769dd..c8ae75e 100644
--- a/manifest.json
+++ b/manifest.json
@@ -6,6 +6,6 @@
"js": "index.js",
"css": "style.css",
"author": "Marinara",
- "version": "3.4.1",
+ "version": "3.6.0",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
}
diff --git a/settings.html b/settings.html
index dae88f6..79fc70f 100644
--- a/settings.html
+++ b/settings.html
@@ -48,7 +48,7 @@
- v3.5.0
+ v3.6.0
diff --git a/src/core/persistence.js b/src/core/persistence.js
index 2434f96..15b6165 100644
--- a/src/core/persistence.js
+++ b/src/core/persistence.js
@@ -147,6 +147,12 @@ export function loadSettings() {
// Migrate to preset manager system if presets don't exist
migrateToPresetManager();
+
+ // Initialize custom status fields
+ initializeCustomStatusFields();
+
+ // Ensure all stats have maxValue (for number display mode)
+ ensureStatsHaveMaxValue();
} catch (error) {
console.error('[RPG Companion] Error loading settings:', error);
console.error('[RPG Companion] Error details:', error.message, error.stack);
@@ -694,6 +700,45 @@ export function migrateToPresetManager() {
}
}
+/**
+ * Initializes custom status fields in userStats based on trackerConfig
+ * Ensures all defined custom status fields have a value in the userStats object
+ */
+function initializeCustomStatusFields() {
+ const customFields = extensionSettings.trackerConfig?.userStats?.statusSection?.customFields || [];
+
+ // Initialize each custom field if it doesn't exist
+ for (const fieldName of customFields) {
+ const fieldKey = fieldName.toLowerCase();
+ if (extensionSettings.userStats[fieldKey] === undefined) {
+ extensionSettings.userStats[fieldKey] = 'None';
+ // console.log(`[RPG Companion] Initialized custom status field: ${fieldKey}`);
+ }
+ }
+}
+
+/**
+ * Ensures all custom stats have a maxValue property
+ * This migration supports the number display mode feature
+ */
+function ensureStatsHaveMaxValue() {
+ const customStats = extensionSettings.trackerConfig?.userStats?.customStats || [];
+
+ for (const stat of customStats) {
+ if (stat && stat.maxValue === undefined) {
+ stat.maxValue = 100; // Default to 100 for backward compatibility
+ // console.log(`[RPG Companion] Added maxValue to stat: ${stat.id || stat.name}`);
+ }
+ }
+
+ // Ensure statsDisplayMode is set (default to percentage)
+ if (extensionSettings.trackerConfig?.userStats &&
+ extensionSettings.trackerConfig.userStats.statsDisplayMode === undefined) {
+ extensionSettings.trackerConfig.userStats.statsDisplayMode = 'percentage';
+ // console.log('[RPG Companion] Initialized statsDisplayMode to percentage');
+ }
+}
+
/**
* Gets all available presets
* @returns {Object} Map of preset ID to preset data
diff --git a/src/core/state.js b/src/core/state.js
index 178d213..acc3b13 100644
--- a/src/core/state.js
+++ b/src/core/state.js
@@ -86,7 +86,7 @@ export let extensionSettings = {
},
// Desktop strip widget display options (shown in collapsed panel strip)
desktopStripWidgets: {
- enabled: false, // Master toggle for strip widgets (disabled by default)
+ enabled: true, // Master toggle for strip widgets (enabled by default)
weatherIcon: { enabled: true }, // Weather emoji (☀️, 🌧️, etc.)
clock: { enabled: true }, // Current time display
date: { enabled: true }, // Date display
@@ -125,13 +125,15 @@ export let extensionSettings = {
// Tracker customization configuration
trackerConfig: {
userStats: {
+ // Stats display mode: 'percentage' or 'number'
+ statsDisplayMode: 'percentage',
// Array of custom stats (allows add/remove/rename)
customStats: [
- { id: 'health', name: 'Health', enabled: true, persistInHistory: false },
- { id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false },
- { id: 'energy', name: 'Energy', enabled: true, persistInHistory: false },
- { id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false },
- { id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false }
+ { id: 'health', name: 'Health', enabled: true, persistInHistory: false, maxValue: 100 },
+ { id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false, maxValue: 100 },
+ { id: 'energy', name: 'Energy', enabled: true, persistInHistory: false, maxValue: 100 },
+ { id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false, maxValue: 100 },
+ { id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false, maxValue: 100 }
],
// RPG Attributes (customizable D&D-style attributes)
showRPGAttributes: true,
diff --git a/src/systems/generation/jsonPromptHelpers.js b/src/systems/generation/jsonPromptHelpers.js
index 21fbad8..61c9d30 100644
--- a/src/systems/generation/jsonPromptHelpers.js
+++ b/src/systems/generation/jsonPromptHelpers.js
@@ -28,6 +28,7 @@ export function buildUserStatsJSONInstruction() {
const trackerConfig = extensionSettings.trackerConfig;
const userStatsConfig = trackerConfig?.userStats;
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
+ const displayMode = userStatsConfig?.statsDisplayMode || 'percentage';
let instruction = '{\n';
instruction += ' "stats": [\n';
@@ -36,7 +37,12 @@ export function buildUserStatsJSONInstruction() {
for (let i = 0; i < enabledStats.length; i++) {
const stat = enabledStats[i];
const comma = i < enabledStats.length - 1 ? ',' : '';
- instruction += ` {"id": "${stat.id}", "name": "${stat.name}", "value": X}${comma}\n`;
+ if (displayMode === 'number') {
+ const maxValue = stat.maxValue || 100;
+ instruction += ` {"id": "${stat.id}", "name": "${stat.name}", "value": X}${comma} // 0 to ${maxValue}\n`;
+ } else {
+ instruction += ` {"id": "${stat.id}", "name": "${stat.name}", "value": X}${comma} // 0 to 100 (percentage)\n`;
+ }
}
instruction += ' ],\n';
@@ -45,9 +51,24 @@ export function buildUserStatsJSONInstruction() {
if (userStatsConfig?.statusSection?.enabled) {
instruction += ' "status": {\n';
if (userStatsConfig.statusSection.showMoodEmoji) {
- instruction += ' "mood": "Mood Emoji",\n';
+ instruction += ' "mood": "Mood Emoji"';
+ }
+ // Add all custom status fields
+ const customFields = userStatsConfig.statusSection.customFields || [];
+ if (customFields.length > 0) {
+ for (let i = 0; i < customFields.length; i++) {
+ const fieldName = customFields[i].toLowerCase();
+ const fieldKey = toSnakeCase(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}`;
+ }
+ }
+ if (!userStatsConfig.statusSection.showMoodEmoji && customFields.length > 0) {
+ instruction += '\n';
}
- instruction += ' "conditions": "[Condition1, Condition2]"\n';
instruction += ' },\n';
}
@@ -105,7 +126,8 @@ export function buildInfoBoxJSONInstruction() {
let hasFields = false;
if (widgets.date?.enabled) {
- instruction += ' "date": {"value": "Weekday, Month, Year"}';
+ const dateFormat = widgets.date.format || 'Weekday, Month, Year';
+ instruction += ` "date": {"value": "${dateFormat}"}`;
hasFields = true;
}
diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js
index f0bdd05..d2feb7b 100644
--- a/src/systems/generation/parser.js
+++ b/src/systems/generation/parser.js
@@ -547,9 +547,15 @@ export function parseUserStats(statsText) {
extensionSettings.userStats.mood = statsData.status.mood;
// console.log('[RPG Parser] ✓ Set mood =', statsData.status.mood);
}
- if (statsData.status.conditions) {
- extensionSettings.userStats.conditions = statsData.status.conditions;
- // console.log('[RPG Parser] ✓ Set conditions =', statsData.status.conditions);
+ // Extract all custom status fields
+ 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]);
+ }
}
}
@@ -679,6 +685,7 @@ export function parseUserStats(statsText) {
const statusConfig = trackerConfig?.userStats?.statusSection;
if (statusConfig?.enabled) {
let moodMatch = null;
+ const customFields = statusConfig.customFields || [];
// Try Status: format
const statusMatch = statsText.match(/Status:\s*(.+)/i);
@@ -691,14 +698,30 @@ export function parseUserStats(statsText) {
if (emoji) {
extensionSettings.userStats.mood = emoji;
// Remaining text contains custom status fields
- if (text) {
- extensionSettings.userStats.conditions = text;
+ if (text && customFields.length > 0) {
+ // For first custom field, use the remaining text
+ const firstFieldKey = customFields[0].toLowerCase();
+ extensionSettings.userStats[firstFieldKey] = text;
}
moodMatch = true;
}
} else {
- // No mood emoji, whole status is conditions
- extensionSettings.userStats.conditions = statusContent;
+ // No mood emoji, whole status goes to first custom field
+ if (customFields.length > 0) {
+ const firstFieldKey = customFields[0].toLowerCase();
+ extensionSettings.userStats[firstFieldKey] = statusContent;
+ }
+ moodMatch = true;
+ }
+ }
+
+ // Try to extract individual custom status fields by name
+ for (const fieldName of customFields) {
+ const fieldKey = fieldName.toLowerCase();
+ const fieldRegex = new RegExp(`${fieldName}:\\s*(.+?)(?:,|$)`, 'i');
+ const fieldMatch = statsText.match(fieldRegex);
+ if (fieldMatch) {
+ extensionSettings.userStats[fieldKey] = fieldMatch[1].trim();
moodMatch = true;
}
}
@@ -706,7 +729,10 @@ export function parseUserStats(statsText) {
debugLog('[RPG Parser] Status match:', {
found: !!moodMatch,
mood: extensionSettings.userStats.mood,
- conditions: extensionSettings.userStats.conditions
+ customFields: customFields.map(f => ({
+ name: f,
+ value: extensionSettings.userStats[f.toLowerCase()]
+ }))
});
}
diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js
index f74b152..0e43360 100644
--- a/src/systems/generation/promptBuilder.js
+++ b/src/systems/generation/promptBuilder.js
@@ -485,11 +485,22 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
// Handle common object formats
if (field && typeof field === 'object') {
- // Status object: {mood, conditions}
- if ('mood' in field && 'conditions' in field) {
+ // Status object: {mood, [customFields...]}
+ if ('mood' in field) {
+ const statusParts = [];
const mood = getValue(field.mood);
- const conditions = getValue(field.conditions);
- return `${mood} - ${conditions}`;
+ if (mood) statusParts.push(mood);
+
+ // Add all other status fields (custom fields)
+ for (const [key, value] of Object.entries(field)) {
+ if (key !== 'mood') {
+ const fieldValue = getValue(value);
+ if (fieldValue && fieldValue !== 'None') {
+ statusParts.push(fieldValue);
+ }
+ }
+ }
+ return statusParts.join(' - ');
}
// Skill/item/quest objects: {name}, {title}, {name, quantity}
@@ -830,9 +841,17 @@ export function formatHistoricalTrackerData(trackerData, trackerConfig, userName
// Status section
if (shouldInclude(userStatsConfig.statusSection) && userStatsData.status) {
const mood = getValue(userStatsData.status.mood || userStatsData.status);
- const conditions = getValue(userStatsData.status.conditions);
- if (mood) statsFormatted += `Mood: ${mood}, `;
- if (conditions && conditions !== 'None') statsFormatted += `Conditions: ${conditions}, `;
+ if (mood && userStatsConfig.statusSection.showMoodEmoji) statsFormatted += `Mood: ${mood}, `;
+
+ // Add all custom status fields
+ const customFields = userStatsConfig.statusSection.customFields || [];
+ for (const fieldName of customFields) {
+ const fieldKey = fieldName.toLowerCase();
+ const fieldValue = getValue(userStatsData.status[fieldKey]);
+ if (fieldValue && fieldValue !== 'None') {
+ statsFormatted += `${fieldName}: ${fieldValue}, `;
+ }
+ }
}
// Skills section
diff --git a/src/systems/rendering/userStats.js b/src/systems/rendering/userStats.js
index 88eed94..31cbe16 100644
--- a/src/systems/rendering/userStats.js
+++ b/src/systems/rendering/userStats.js
@@ -105,7 +105,8 @@ 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 excludeFields = new Set(['mood', 'conditions', 'inventory', 'skills', 'level']);
+ const customFields = config.statusSection?.customFields || [];
+ const excludeFields = new Set(['mood', ...customFields.map(f => f.toLowerCase()), 'inventory', 'skills', 'level']);
Object.entries(stats).forEach(([key, value]) => {
if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') {
statsArray.push({
@@ -118,12 +119,17 @@ function updateUserStatsData() {
jsonData.stats = statsArray;
- // Update status
+ // Update status - include all custom status fields
jsonData.status = {
- mood: stats.mood || '😐',
- conditions: stats.conditions || 'None'
+ mood: stats.mood || '😐'
};
+ // Add all custom status fields
+ for (const fieldName of customFields) {
+ const fieldKey = fieldName.toLowerCase();
+ jsonData.status[fieldKey] = stats[fieldKey] || 'None';
+ }
+
// Update inventory (convert to v3 format)
const convertToV3Items = (itemString) => {
if (!itemString) return [];
@@ -276,16 +282,33 @@ export function renderUserStats() {
}
html += '
';
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
+ const displayMode = config.statsDisplayMode || 'percentage';
for (const stat of enabledStats) {
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
+ const maxValue = stat.maxValue || 100;
+
+ // Calculate percentage for bar fill
+ let percentage;
+ let displayValue;
+
+ if (displayMode === 'number') {
+ // In number mode, value is already the number (0 to maxValue)
+ percentage = maxValue > 0 ? (value / maxValue) * 100 : 100;
+ displayValue = `${value}/${maxValue}`;
+ } else {
+ // In percentage mode, value is 0-100
+ percentage = value;
+ displayValue = `${value}%`;
+ }
+
html += `
${stat.name}:
-
+
- ${value}%
+ ${displayValue}
`;
}
@@ -308,13 +331,15 @@ export function renderUserStats() {
// Render custom status fields
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
- // For now, use first field as "conditions" for backward compatibility
- let conditionsValue = stats.conditions || 'None';
- // Strip brackets if present (from JSON array format)
- if (typeof conditionsValue === 'string') {
- conditionsValue = conditionsValue.replace(/^\[|\]$/g, '').trim();
+ for (const fieldName of config.statusSection.customFields) {
+ const fieldKey = fieldName.toLowerCase();
+ let fieldValue = stats[fieldKey] || 'None';
+ // Strip brackets if present (from JSON array format)
+ if (typeof fieldValue === 'string') {
+ fieldValue = fieldValue.replace(/^\[|\]$/g, '').trim();
+ }
+ html += `
${fieldValue}
`;
}
- html += `
${conditionsValue}
`;
}
html += '
';
@@ -406,14 +431,31 @@ export function renderUserStats() {
// Add event listeners for editable stat values
$('.rpg-editable-stat').on('blur', function() {
const field = $(this).data('field');
- const textValue = $(this).text().replace('%', '').trim();
- let value = parseInt(textValue);
+ const mode = $(this).data('mode');
+ const maxValue = parseInt($(this).data('max')) || 100;
+ const textValue = $(this).text().trim();
+ let value;
- // Validate and clamp value between 0 and 100
- if (isNaN(value)) {
- value = 0;
+ if (mode === 'number') {
+ // In number mode, parse "X/MAX" or just "X"
+ const parts = textValue.split('/');
+ value = parseInt(parts[0]);
+
+ // Validate and clamp value between 0 and maxValue
+ if (isNaN(value)) {
+ value = 0;
+ }
+ value = Math.max(0, Math.min(maxValue, value));
+ } else {
+ // In percentage mode, parse "X%" or just "X"
+ value = parseInt(textValue.replace('%', ''));
+
+ // Validate and clamp value between 0 and 100
+ if (isNaN(value)) {
+ value = 0;
+ }
+ value = Math.max(0, Math.min(100, value));
}
- value = Math.max(0, Math.min(100, value));
// Update the setting
extensionSettings.userStats[field] = value;
@@ -445,7 +487,8 @@ export function renderUserStats() {
$('.rpg-mood-conditions.rpg-editable').on('blur', function() {
const value = $(this).text().trim();
- extensionSettings.userStats.conditions = value || 'None';
+ const fieldKey = $(this).data('field');
+ extensionSettings.userStats[fieldKey] = value || 'None';
// Update userStats data (maintains JSON or text format)
updateUserStatsData();
diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js
index 57c8463..30b7cdc 100644
--- a/src/systems/ui/trackerEditor.js
+++ b/src/systems/ui/trackerEditor.js
@@ -729,13 +729,27 @@ function renderUserStatsTab() {
// Custom Stats section
html += `