diff --git a/README.md b/README.md
index db88ca9..fde2e10 100644
--- a/README.md
+++ b/README.md
@@ -7,11 +7,10 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New
-### v3.6.1
+### v3.6.2
-- Fixed the bugs in the encounter system where you couldn't use the buttons after performing any custom action.
-- Improved combat actions and made them dynamic, depending on the current situation.
-- Added Russian as a supported language.
+- Various bug fixes.
+- Added the ability to add present characters manually.
**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 348307d..60823b8 100644
--- a/manifest.json
+++ b/manifest.json
@@ -6,6 +6,6 @@
"js": "index.js",
"css": "style.css",
"author": "Marinara",
- "version": "3.6.1",
+ "version": "3.6.2",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
}
diff --git a/settings.html b/settings.html
index 0de3497..4910b03 100644
--- a/settings.html
+++ b/settings.html
@@ -49,7 +49,7 @@
- v3.6.1
+ v3.6.2
diff --git a/src/systems/generation/injector.js b/src/systems/generation/injector.js
index bf7471a..52e9b2f 100644
--- a/src/systems/generation/injector.js
+++ b/src/systems/generation/injector.js
@@ -86,8 +86,8 @@ function buildHistoricalContextMap() {
// For user_message_end: start from the last assistant message (we need its context for the preceding user message)
// For assistant_message_end: start from before the last assistant message (it gets current context via setExtensionPrompt)
let processedCount = 0;
- const startIndex = position === 'user_message_end'
- ? lastAssistantIndex
+ const startIndex = position === 'user_message_end'
+ ? lastAssistantIndex
: (lastAssistantIndex > 0 ? lastAssistantIndex - 1 : chat.length - 2);
for (let i = startIndex; i >= 0 && (messageCount === 0 || processedCount < maxMessages); i--) {
@@ -201,7 +201,7 @@ function prepareHistoricalContextInjection() {
/**
* Finds the best match position for message content in the prompt.
* Tries full content first, then progressively smaller suffixes.
- *
+ *
* @param {string} prompt - The prompt to search in
* @param {string} messageContent - The message content to find
* @returns {{start: number, end: number}|null} - Position info or null if not found
@@ -213,7 +213,7 @@ function findMessageInPrompt(prompt, messageContent) {
// Try to find the full content first
let searchIndex = prompt.lastIndexOf(messageContent);
-
+
if (searchIndex !== -1) {
return { start: searchIndex, end: searchIndex + messageContent.length };
}
@@ -221,15 +221,15 @@ function findMessageInPrompt(prompt, messageContent) {
// If full content not found, try last N characters with progressively smaller chunks
// This handles cases where messages are truncated in the prompt
const searchLengths = [500, 300, 200, 100, 50];
-
+
for (const len of searchLengths) {
if (messageContent.length <= len) {
continue;
}
-
+
const searchContent = messageContent.slice(-len);
searchIndex = prompt.lastIndexOf(searchContent);
-
+
if (searchIndex !== -1) {
return { start: searchIndex, end: searchIndex + searchContent.length };
}
@@ -241,7 +241,7 @@ function findMessageInPrompt(prompt, messageContent) {
/**
* Injects historical context into a text completion prompt string.
* Searches for message content in the prompt and appends context after matches.
- *
+ *
* @param {string} prompt - The text completion prompt
* @returns {string} - The modified prompt with injected context
*/
@@ -268,7 +268,7 @@ function injectContextIntoTextPrompt(prompt) {
// Find the message content in the prompt
const position = findMessageInPrompt(modifiedPrompt, message.mes);
-
+
if (!position) {
// Message not found in prompt (might be truncated or not included)
console.debug(`[RPG Companion] Could not find message ${msgIdx} in prompt for context injection`);
@@ -290,7 +290,7 @@ function injectContextIntoTextPrompt(prompt) {
/**
* Injects historical context into a chat completion message array.
* Modifies the content of messages in the array directly.
- *
+ *
* @param {Array} chatMessages - The chat completion message array
* @returns {Array} - The modified message array with injected context
*/
@@ -315,7 +315,7 @@ function injectContextIntoChatPrompt(chatMessages) {
// Find this message in the chat completion array by matching content
// Try full content first, then progressively smaller suffixes
let found = false;
-
+
for (const promptMsg of chatMessages) {
if (!promptMsg.content || typeof promptMsg.content !== 'string') {
continue;
@@ -335,7 +335,7 @@ function injectContextIntoChatPrompt(chatMessages) {
if (messageContent.length <= len) {
continue;
}
-
+
const searchContent = messageContent.slice(-len);
if (promptMsg.content.includes(searchContent)) {
promptMsg.content = promptMsg.content + ctxContent;
@@ -344,12 +344,12 @@ function injectContextIntoChatPrompt(chatMessages) {
break;
}
}
-
+
if (found) {
break;
}
}
-
+
if (!found) {
console.debug(`[RPG Companion] Could not find message ${msgIdx} in chat prompt for context injection`);
}
@@ -365,7 +365,7 @@ function injectContextIntoChatPrompt(chatMessages) {
/**
* Injects historical context into finalMesSend message array (text completion).
* Iterates through chat and finalMesSend in order, matching by content to skip injected messages.
- *
+ *
* @param {Array} finalMesSend - The array of message objects {message: string, extensionPrompts: []}
* @returns {number} - Number of injections made
*/
@@ -381,20 +381,20 @@ function injectContextIntoFinalMesSend(finalMesSend) {
}
let injectedCount = 0;
-
+
// Build a map from chat index to finalMesSend index by matching content in order
// This handles injected messages (author's note, OOC, etc.) that exist in finalMesSend but not in chat
const chatToMesSendMap = new Map();
let mesSendIdx = 0;
-
+
for (let chatIdx = 0; chatIdx < chat.length && mesSendIdx < finalMesSend.length; chatIdx++) {
const chatMsg = chat[chatIdx];
if (!chatMsg || chatMsg.is_system) {
continue;
}
-
+
const chatContent = chatMsg.mes || '';
-
+
// Look for this chat message in finalMesSend starting from current position
// Skip any finalMesSend entries that don't match (they're injected content)
while (mesSendIdx < finalMesSend.length) {
@@ -403,40 +403,40 @@ function injectContextIntoFinalMesSend(finalMesSend) {
mesSendIdx++;
continue;
}
-
+
// Check if this finalMesSend message contains the chat content
// Use a substring match since instruct formatting adds prefixes/suffixes
// Match with sufficient content (first 50 chars or full message if shorter)
- const matchContent = chatContent.length > 50
- ? chatContent.substring(0, 50)
+ const matchContent = chatContent.length > 50
+ ? chatContent.substring(0, 50)
: chatContent;
-
+
if (matchContent && mesSendObj.message.includes(matchContent)) {
// Found a match - record the mapping
chatToMesSendMap.set(chatIdx, mesSendIdx);
mesSendIdx++;
break;
}
-
+
// This finalMesSend entry doesn't match - it's injected content, skip it
mesSendIdx++;
}
}
-
+
// Now inject context using the map
for (const [chatIdx, ctxContent] of pendingContextMap) {
const targetMesSendIdx = chatToMesSendMap.get(chatIdx);
-
+
if (targetMesSendIdx === undefined) {
console.debug(`[RPG Companion] Chat message ${chatIdx} not found in finalMesSend mapping`);
continue;
}
-
+
const mesSendObj = finalMesSend[targetMesSendIdx];
if (!mesSendObj || !mesSendObj.message) {
continue;
}
-
+
// Append context to this message
mesSendObj.message = mesSendObj.message + ctxContent;
injectedCount++;
@@ -450,7 +450,7 @@ function injectContextIntoFinalMesSend(finalMesSend) {
* Event handler for GENERATE_BEFORE_COMBINE_PROMPTS (text completion).
* Injects historical context into the finalMesSend array before prompt combination.
* This is more reliable than post-combine string searching.
- *
+ *
* @param {Object} eventData - Event data with finalMesSend and other properties
*/
function onGenerateBeforeCombinePrompts(eventData) {
@@ -478,7 +478,7 @@ function onGenerateBeforeCombinePrompts(eventData) {
/**
* Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion).
* This is now a backup/fallback - primary injection happens in BEFORE_COMBINE.
- *
+ *
* @param {Object} eventData - Event data with prompt property
*/
function onGenerateAfterCombinePrompts(eventData) {
@@ -508,7 +508,7 @@ function onGenerateAfterCombinePrompts(eventData) {
/**
* Event handler for CHAT_COMPLETION_PROMPT_READY.
* Injects historical context into the chat message array.
- *
+ *
* @param {Object} eventData - Event data with chat property
*/
function onChatCompletionPromptReady(eventData) {
@@ -938,16 +938,16 @@ Ensure these details naturally reflect and influence the narrative. Character be
export function initHistoryInjectionListeners() {
// Register persistent listeners for prompt injection
// These check pendingContextMap and only inject if there's data
-
+
// Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects)
eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
-
+
// Fallback: AFTER_COMBINE for text completion (string-based injection)
eventSource.on(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
-
+
// Chat completion (OpenAI, etc.)
eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
-
+
console.log('[RPG Companion] History injection listeners initialized');
}
diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js
index 8c7b69b..a1f496e 100644
--- a/src/systems/generation/promptBuilder.js
+++ b/src/systems/generation/promptBuilder.js
@@ -725,13 +725,14 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
}
}
- // Relationship
- if (char.relationship) {
+ // Relationship - check both Relationship (new format) and relationship (old format)
+ const relationshipValue = char.Relationship || char.relationship;
+ if (relationshipValue) {
let relValue;
- if (typeof char.relationship === 'object' && !Array.isArray(char.relationship) && 'status' in char.relationship) {
- relValue = getValue(char.relationship.status);
+ if (typeof relationshipValue === 'object' && !Array.isArray(relationshipValue) && 'status' in relationshipValue) {
+ relValue = getValue(relationshipValue.status);
} else {
- relValue = getValue(char.relationship);
+ relValue = getValue(relationshipValue);
}
if (relValue) formatted += ` Relationship: ${relValue}\n`;
}
diff --git a/src/systems/integration/sillytavern.js b/src/systems/integration/sillytavern.js
index 1398ec1..3af43a8 100644
--- a/src/systems/integration/sillytavern.js
+++ b/src/systems/integration/sillytavern.js
@@ -395,7 +395,13 @@ export function onMessageSwiped(messageIndex) {
// Load swipe data into lastGeneratedData for display (both modes)
lastGeneratedData.userStats = swipeData.userStats || null;
lastGeneratedData.infoBox = swipeData.infoBox || null;
- lastGeneratedData.characterThoughts = swipeData.characterThoughts || 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;
+ }
// DON'T parse user stats when loading swipe data
// This would overwrite manually edited fields (like Conditions) with old swipe data
diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js
index 5df493d..e60b40a 100644
--- a/src/systems/rendering/thoughts.js
+++ b/src/systems/rendering/thoughts.js
@@ -512,17 +512,20 @@ export function renderThoughts() {
const fieldNameLower = field.name.toLowerCase();
// Skip lock icons for thoughts field
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) {
const lockIconHtml = getLockIconHtml('characters', `${char.name}.${field.name}`);
html += `
${lockIconHtml}
- ${fieldValue}
+ ${fieldValue}
`;
} else {
html += `
-
${fieldValue}
+
${fieldValue}
`;
}
}
@@ -564,6 +567,16 @@ export function renderThoughts() {
}
debugLog('[RPG Thoughts] Finished building all character cards');
+
+ // Add "Add Character" button if data exists (inside rpg-thoughts-content)
+ if (presentCharacters.length > 0) {
+ html += `
+
+ `;
+ }
+
html += '';
}
@@ -662,6 +675,31 @@ export function renderThoughts() {
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
if (extensionSettings.enableAnimations) {
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
@@ -788,6 +826,136 @@ export function removeCharacter(characterName) {
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.
* Works with the new multi-line format.
@@ -882,15 +1050,44 @@ export function updateCharacterField(characterName, field, value) {
numValue = Math.max(0, Math.min(100, numValue));
char.stats[field] = numValue;
} else {
- // It's a custom detail field
+ // It's a custom detail field - store in details object
if (!char.details) char.details = {};
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)
+ // This prevents duplicates from AI-generated data
+ 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
- lastGeneratedData.characterThoughts = Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray };
+ // 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;
// console.log('[RPG Companion] Saved to lastGeneratedData.characterThoughts:', JSON.stringify(lastGeneratedData.characterThoughts));
@@ -971,6 +1168,9 @@ export function updateCharacterField(characterName, field, value) {
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
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
for (let i = characterStartIndex; i < characterEndIndex; i++) {
const line = lines[i].trim();
@@ -978,35 +1178,37 @@ export function updateCharacterField(characterName, field, value) {
if (line.startsWith('Stats:')) {
statsLineExists = true;
statsLineIndex = i;
+ continue; // Skip to next line
}
+ // Check for name update
if (field === 'name' && line.startsWith('- ')) {
lines[i] = `- ${value}`;
+ fieldUpdated = true;
+ continue;
}
- else if (field === 'emoji' && line.startsWith('Details:')) {
- const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim());
- 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:')) {
+
+ // Check for Relationship field
+ if (field === 'Relationship' && line.startsWith('Relationship:')) {
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
const relationshipValue = emojiToRelationship[value] || value;
lines[i] = `Relationship: ${relationshipValue}`;
+ fieldUpdated = true;
+ continue;
}
- else if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
- // Update thoughts field
- lines[i] = `${thoughtsFieldName}: ${value}`;
- // console.log('[RPG Companion] Updated thoughts:', lines[i]);
+
+ // Check for Thoughts field
+ if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
+ lines[i] = ` ${thoughtsFieldName}: ${value}`;
+ 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)
}
}
@@ -1073,23 +1275,28 @@ export function updateCharacterField(characterName, field, value) {
}
}
} else {
- // Create new character block
+ // Create new character block (v3 text format only)
const dividerIndex = lines.findIndex(line => line.includes('---'));
if (dividerIndex >= 0) {
const newCharacterLines = [`- ${characterName}`];
- let detailsParts = [field === 'emoji' ? value : '😊'];
- for (let i = 0; i < enabledFields.length; i++) {
- detailsParts.push(field === enabledFields[i].name ? value : '');
+ // Add custom detail fields as standalone lines
+ for (const customField of enabledFields) {
+ if (field === customField.name) {
+ 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) {
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
const relationshipValue = field === 'Relationship' ? (emojiToRelationship[value] || value) : 'Neutral';
- newCharacterLines.push(`Relationship: ${relationshipValue}`);
+ newCharacterLines.push(` Relationship: ${relationshipValue}`);
}
+ // Add Stats if enabled
if (enabledCharStats.length > 0) {
const statsParts = enabledCharStats.map(s => {
if (field === s.name) {
@@ -1104,7 +1311,7 @@ export function updateCharacterField(characterName, field, value) {
}
return `${s.name}: 0%`;
});
- newCharacterLines.push(`Stats: ${statsParts.join(' | ')}`);
+ newCharacterLines.push(` Stats: ${statsParts.join(' | ')}`);
}
lines.splice(dividerIndex + 1, 0, ...newCharacterLines);
diff --git a/style.css b/style.css
index 69eb7dc..4029010 100644
--- a/style.css
+++ b/style.css
@@ -2329,6 +2329,40 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
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 */
.rpg-character-traits,
.rpg-character-field {
@@ -2340,12 +2374,20 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
word-wrap: break-word;
}
-/* Placeholder for empty editable character fields */
-.rpg-character-field.rpg-editable:empty::before {
- content: 'Click to edit...';
+/* Empty field placeholder using data-placeholder attribute */
+.rpg-editable.rpg-empty-field:empty::before {
+ content: attr(data-placeholder);
color: var(--rpg-highlight);
- opacity: 0.5;
+ opacity: 0.4;
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 */