Add customizable RPG attributes and fix character stats editing

Features:
- Made RPG attributes (STR/DEX/CON/INT/WIS/CHA) fully customizable
- Added enable/disable toggle for entire RPG Attributes section
- Users can add/remove/rename/toggle individual attributes
- Custom attribute names now appear in AI prompts for dice rolls
- Added proper CSS styling for attribute editor fields

Bug Fixes:
- Fixed character stat editing showing 0% on blur but saving correctly
- Character stats now create Stats line if missing from AI response
- Separated stat name from editable percentage value
- Added value sanitization (removes %, validates 0-100 range)
- Stats line now inserts before Thoughts line when created

Technical:
- Added buildAttributesString() helper in promptBuilder.js
- Updated generateTrackerInstructions and generateContextualSummary
- Restructured character stat HTML to prevent nested contenteditable
- Enhanced updateCharacterField to handle missing Stats lines
- Removed legacy default preset/regex import code
This commit is contained in:
Spicy_Marinara
2025-11-03 17:01:53 +01:00
parent f20710f5a3
commit d8707318c8
10 changed files with 218 additions and 143 deletions
+6
View File
@@ -365,6 +365,7 @@ function migrateToTrackerConfig() {
extensionSettings.trackerConfig = {
userStats: {
customStats: [],
showRPGAttributes: true,
rpgAttributes: [
{ id: 'str', name: 'STR', enabled: true },
{ id: 'dex', name: 'DEX', enabled: true },
@@ -457,6 +458,11 @@ function migrateToTrackerConfig() {
];
}
// Ensure showRPGAttributes exists (defaults to true)
if (extensionSettings.trackerConfig.userStats.showRPGAttributes === undefined) {
extensionSettings.trackerConfig.userStats.showRPGAttributes = true;
}
// Ensure all rpgAttributes have corresponding values in classicStats
if (extensionSettings.classicStats) {
for (const attr of extensionSettings.trackerConfig.userStats.rpgAttributes) {
+1
View File
@@ -72,6 +72,7 @@ export let extensionSettings = {
{ id: 'arousal', name: 'Arousal', enabled: true }
],
// RPG Attributes (customizable D&D-style attributes)
showRPGAttributes: true,
rpgAttributes: [
{ id: 'str', name: 'STR', enabled: true },
{ id: 'dex', name: 'DEX', enabled: true },
+39 -3
View File
@@ -56,6 +56,41 @@ export function buildInventorySummary(inventory) {
return 'None';
}
/**
* Builds a dynamic attributes string based on configured RPG attributes.
* Uses custom attribute names and values from classicStats.
*
* @returns {string} Formatted attributes string (e.g., "STR 10, DEX 12, INT 15, LVL 5")
*/
function buildAttributesString() {
const trackerConfig = extensionSettings.trackerConfig;
const classicStats = extensionSettings.classicStats;
const userStatsConfig = trackerConfig?.userStats;
// Get enabled attributes from config
const rpgAttributes = userStatsConfig?.rpgAttributes || [
{ id: 'str', name: 'STR', enabled: true },
{ id: 'dex', name: 'DEX', enabled: true },
{ id: 'con', name: 'CON', enabled: true },
{ id: 'int', name: 'INT', enabled: true },
{ id: 'wis', name: 'WIS', enabled: true },
{ id: 'cha', name: 'CHA', enabled: true }
];
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
// Build attributes string dynamically
const attributeParts = enabledAttributes.map(attr => {
const value = classicStats[attr.id] !== undefined ? classicStats[attr.id] : 10;
return `${attr.name} ${value}`;
});
// Add level at the end
attributeParts.push(`LVL ${extensionSettings.level}`);
return attributeParts.join(', ');
}
/**
* Generates an example block showing current tracker states in markdown code blocks.
* Uses COMMITTED data (not displayed data) for generation context.
@@ -250,7 +285,8 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
// Include attributes and dice roll only if there was a dice roll
if (extensionSettings.lastDiceRoll) {
const roll = extensionSettings.lastDiceRoll;
instructions += `${userName}'s attributes: STR ${classicStats.str}, DEX ${classicStats.dex}, CON ${classicStats.con}, INT ${classicStats.int}, WIS ${classicStats.wis}, CHA ${classicStats.cha}, LVL ${extensionSettings.level}\n`;
const attributesString = buildAttributesString();
instructions += `${userName}'s attributes: ${attributesString}\n`;
instructions += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
}
}
@@ -327,9 +363,9 @@ export function generateContextualSummary() {
// Include attributes and dice roll only if there was a dice roll
if (extensionSettings.lastDiceRoll) {
const classicStats = extensionSettings.classicStats;
const roll = extensionSettings.lastDiceRoll;
summary += `${userName}'s attributes: STR ${classicStats.str}, DEX ${classicStats.dex}, CON ${classicStats.con}, INT ${classicStats.int}, WIS ${classicStats.wis}, CHA ${classicStats.cha}, LVL ${extensionSettings.level}\n`;
const attributesString = buildAttributesString();
summary += `${userName}'s attributes: ${attributesString}\n`;
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
}
+87 -14
View File
@@ -396,7 +396,7 @@ export function renderThoughts() {
const statColor = getStatColor(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh);
html += `
<div class="rpg-character-stat">
<span class="rpg-stat-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" title="Click to edit ${stat.name}">${stat.name}: <span style="color: ${statColor}">${statValue}%</span></span>
<span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" style="color: ${statColor}" title="Click to edit ${stat.name}">${statValue}%</span>
</div>
`;
}
@@ -431,6 +431,7 @@ export function renderThoughts() {
const character = $(this).data('character');
const field = $(this).data('field');
const value = $(this).text().trim();
console.log('[RPG Companion] Character stat edit:', { character, field, value });
updateCharacterField(character, field, value);
});
@@ -492,10 +493,20 @@ export function updateCharacterField(characterName, field, value) {
}
if (characterFound) {
// Update the specific field within the character block
// Check if we're updating a character stat
const isStatField = enabledCharStats.findIndex(s => s.name === field) !== -1;
let statsLineExists = false;
let statsLineIndex = -1;
// First pass: check if Stats line exists and update other fields
for (let i = characterStartIndex; i < characterEndIndex; i++) {
const line = lines[i].trim();
if (line.startsWith('Stats:')) {
statsLineExists = true;
statsLineIndex = i;
}
if (field === 'name' && line.startsWith('- ')) {
lines[i] = `- ${value}`;
}
@@ -519,20 +530,68 @@ export function updateCharacterField(characterName, field, value) {
const relationshipValue = emojiToRelationship[value] || value;
lines[i] = `Relationship: ${relationshipValue}`;
}
else if (line.startsWith('Stats:')) {
const statIndex = enabledCharStats.findIndex(s => s.name === field);
if (statIndex !== -1) {
const statsContent = line.substring(line.indexOf(':') + 1).trim();
const statParts = statsContent.split('|').map(p => p.trim());
}
for (let j = 0; j < statParts.length; j++) {
if (statParts[j].startsWith(field + ':')) {
statParts[j] = `${field}: ${value}%`;
break;
}
// Handle stat updates
if (isStatField) {
// Clean the value: remove % if present, parse as integer, clamp 0-100
let cleanValue = value.replace('%', '').trim();
let numValue = parseInt(cleanValue);
if (isNaN(numValue)) {
numValue = 0;
}
numValue = Math.max(0, Math.min(100, numValue));
console.log('[RPG Companion] Updating stat:', { field, rawValue: value, cleanValue, numValue });
if (statsLineExists) {
// Update existing Stats line
const line = lines[statsLineIndex];
const statsContent = line.substring(line.indexOf(':') + 1).trim();
const statParts = statsContent.split('|').map(p => p.trim());
let statFound = false;
for (let j = 0; j < statParts.length; j++) {
if (statParts[j].startsWith(field + ':')) {
statParts[j] = `${field}: ${numValue}%`;
statFound = true;
console.log('[RPG Companion] Updated stat part:', statParts[j]);
break;
}
lines[i] = `Stats: ${statParts.join(' | ')}`;
}
// If stat wasn't found in existing parts, add it
if (!statFound) {
statParts.push(`${field}: ${numValue}%`);
console.log('[RPG Companion] Added new stat to existing line:', `${field}: ${numValue}%`);
}
lines[statsLineIndex] = `Stats: ${statParts.join(' | ')}`;
console.log('[RPG Companion] Updated stats line:', lines[statsLineIndex]);
} else {
// Create new Stats line with all enabled stats (defaulting to 0% except the one being edited)
const statsParts = enabledCharStats.map(s => {
if (s.name === field) {
return `${s.name}: ${numValue}%`;
}
return `${s.name}: 0%`;
});
const newStatsLine = `Stats: ${statsParts.join(' | ')}`;
// Insert before Thoughts line or at end of character block
let insertIndex = characterEndIndex;
for (let i = characterStartIndex; i < characterEndIndex; i++) {
const line = lines[i].trim();
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
if (line.startsWith(thoughtsFieldName + ':')) {
insertIndex = i;
break;
}
}
lines.splice(insertIndex, 0, newStatsLine);
console.log('[RPG Companion] Created new stats line:', newStatsLine);
characterEndIndex++; // Adjust end index since we inserted a line
}
}
} else {
@@ -554,7 +613,19 @@ export function updateCharacterField(characterName, field, value) {
}
if (enabledCharStats.length > 0) {
const statsParts = enabledCharStats.map(s => `${s.name}: ${field === s.name ? value : '0'}%`);
const statsParts = enabledCharStats.map(s => {
if (field === s.name) {
// Clean the value: remove % if present, parse as integer, clamp 0-100
let cleanValue = value.replace('%', '').trim();
let numValue = parseInt(cleanValue);
if (isNaN(numValue)) {
numValue = 0;
}
numValue = Math.max(0, Math.min(100, numValue));
return `${s.name}: ${numValue}%`;
}
return `${s.name}: 0%`;
});
newCharacterLines.push(`Stats: ${statsParts.join(' | ')}`);
}
@@ -565,6 +636,8 @@ export function updateCharacterField(characterName, field, value) {
lastGeneratedData.characterThoughts = lines.join('\n');
committedTrackerData.characterThoughts = lines.join('\n');
console.log('[RPG Companion] Updated characterThoughts data:', lastGeneratedData.characterThoughts);
const chat = getContext().chat;
if (chat && chat.length > 0) {
for (let i = chat.length - 1; i >= 0; i--) {
+16 -3
View File
@@ -179,10 +179,22 @@ export function renderUserStats() {
html += '</div>'; // Close rpg-stats-left
// RPG Attributes section (dynamically generated from config)
const rpgAttributes = config.rpgAttributes || [];
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
// Check if RPG Attributes section is enabled
const showRPGAttributes = config.showRPGAttributes !== undefined ? config.showRPGAttributes : true;
if (enabledAttributes.length > 0) {
if (showRPGAttributes) {
// Use attributes from config, with fallback to defaults if not configured
const rpgAttributes = (config.rpgAttributes && config.rpgAttributes.length > 0) ? config.rpgAttributes : [
{ id: 'str', name: 'STR', enabled: true },
{ id: 'dex', name: 'DEX', enabled: true },
{ id: 'con', name: 'CON', enabled: true },
{ id: 'int', name: 'INT', enabled: true },
{ id: 'wis', name: 'WIS', enabled: true },
{ id: 'cha', name: 'CHA', enabled: true }
];
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
if (enabledAttributes.length > 0) {
html += `
<div class="rpg-stats-right">
<div class="rpg-classic-stats">
@@ -208,6 +220,7 @@ export function renderUserStats() {
</div>
</div>
`;
}
}
html += '</div>'; // Close rpg-stats-content
+39 -10
View File
@@ -127,6 +127,7 @@ function resetToDefaults() {
{ id: 'hygiene', name: 'Hygiene', enabled: true },
{ id: 'arousal', name: 'Arousal', enabled: true }
],
showRPGAttributes: true,
rpgAttributes: [
{ id: 'str', name: 'STR', enabled: true },
{ id: 'dex', name: 'DEX', enabled: true },
@@ -221,16 +222,31 @@ function renderUserStatsTab() {
// RPG Attributes section
html += '<h4><i class="fa-solid fa-dice-d20"></i> RPG Attributes</h4>';
// Enable/disable toggle for entire RPG Attributes section
const showRPGAttributes = config.showRPGAttributes !== undefined ? config.showRPGAttributes : true;
html += '<div class="rpg-editor-toggle-row">';
html += `<input type="checkbox" id="rpg-show-rpg-attrs" ${showRPGAttributes ? 'checked' : ''}>`;
html += '<label for="rpg-show-rpg-attrs">Enable RPG Attributes Section</label>';
html += '</div>';
html += '<div class="rpg-editor-stats-list" id="rpg-editor-attrs-list">';
const rpgAttributes = config.rpgAttributes || [
{ id: 'str', name: 'STR', enabled: true },
{ id: 'dex', name: 'DEX', enabled: true },
{ id: 'con', name: 'CON', enabled: true },
{ id: 'int', name: 'INT', enabled: true },
{ id: 'wis', name: 'WIS', enabled: true },
{ id: 'cha', name: 'CHA', enabled: true }
];
// Ensure rpgAttributes exists in the actual config (not just local fallback)
if (!config.rpgAttributes || config.rpgAttributes.length === 0) {
config.rpgAttributes = [
{ id: 'str', name: 'STR', enabled: true },
{ id: 'dex', name: 'DEX', enabled: true },
{ id: 'con', name: 'CON', enabled: true },
{ id: 'int', name: 'INT', enabled: true },
{ id: 'wis', name: 'WIS', enabled: true },
{ id: 'cha', name: 'CHA', enabled: true }
];
// Save the defaults back to the actual config
extensionSettings.trackerConfig.userStats.rpgAttributes = config.rpgAttributes;
}
const rpgAttributes = config.rpgAttributes;
rpgAttributes.forEach((attr, index) => {
html += `
@@ -316,8 +332,16 @@ function setupUserStatsListeners() {
// Add attribute
$('#rpg-add-attr').off('click').on('click', function() {
if (!extensionSettings.trackerConfig.userStats.rpgAttributes) {
extensionSettings.trackerConfig.userStats.rpgAttributes = [];
// Ensure rpgAttributes array exists with defaults if needed
if (!extensionSettings.trackerConfig.userStats.rpgAttributes || extensionSettings.trackerConfig.userStats.rpgAttributes.length === 0) {
extensionSettings.trackerConfig.userStats.rpgAttributes = [
{ id: 'str', name: 'STR', enabled: true },
{ id: 'dex', name: 'DEX', enabled: true },
{ id: 'con', name: 'CON', enabled: true },
{ id: 'int', name: 'INT', enabled: true },
{ id: 'wis', name: 'WIS', enabled: true },
{ id: 'cha', name: 'CHA', enabled: true }
];
}
const newId = 'attr_' + Date.now();
extensionSettings.trackerConfig.userStats.rpgAttributes.push({
@@ -351,6 +375,11 @@ function setupUserStatsListeners() {
extensionSettings.trackerConfig.userStats.rpgAttributes[index].name = $(this).val();
});
// Enable/disable RPG Attributes section toggle
$('#rpg-show-rpg-attrs').off('change').on('change', function() {
extensionSettings.trackerConfig.userStats.showRPGAttributes = $(this).is(':checked');
});
// Status section toggles
$('#rpg-status-enabled').off('change').on('change', function() {
extensionSettings.trackerConfig.userStats.statusSection.enabled = $(this).is(':checked');