Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e82918004e | |||
| f78c8a1b78 | |||
| 2a48c30808 | |||
| c5a9c8631f | |||
| 2623df4050 | |||
| 5ddc380dac | |||
| f4324a5d19 |
@@ -7,11 +7,10 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
|
|||||||
|
|
||||||
## 🆕 What's New
|
## 🆕 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.
|
- Various bug fixes.
|
||||||
- Improved combat actions and made them dynamic, depending on the current situation.
|
- Added the ability to add present characters manually.
|
||||||
- Added Russian as a supported language.
|
|
||||||
|
|
||||||
**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, Amauragis, and Tomt610.
|
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"js": "index.js",
|
"js": "index.js",
|
||||||
"css": "style.css",
|
"css": "style.css",
|
||||||
"author": "Marinara",
|
"author": "Marinara",
|
||||||
"version": "3.6.1",
|
"version": "3.6.3",
|
||||||
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@
|
|||||||
</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.6.1
|
v3.6.2
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 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)
|
// For assistant_message_end: start from before the last assistant message (it gets current context via setExtensionPrompt)
|
||||||
let processedCount = 0;
|
let processedCount = 0;
|
||||||
const startIndex = position === 'user_message_end'
|
const startIndex = position === 'user_message_end'
|
||||||
? lastAssistantIndex
|
? lastAssistantIndex
|
||||||
: (lastAssistantIndex > 0 ? lastAssistantIndex - 1 : chat.length - 2);
|
: (lastAssistantIndex > 0 ? lastAssistantIndex - 1 : chat.length - 2);
|
||||||
|
|
||||||
for (let i = startIndex; i >= 0 && (messageCount === 0 || processedCount < maxMessages); i--) {
|
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.
|
* Finds the best match position for message content in the prompt.
|
||||||
* Tries full content first, then progressively smaller suffixes.
|
* Tries full content first, then progressively smaller suffixes.
|
||||||
*
|
*
|
||||||
* @param {string} prompt - The prompt to search in
|
* @param {string} prompt - The prompt to search in
|
||||||
* @param {string} messageContent - The message content to find
|
* @param {string} messageContent - The message content to find
|
||||||
* @returns {{start: number, end: number}|null} - Position info or null if not found
|
* @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
|
// Try to find the full content first
|
||||||
let searchIndex = prompt.lastIndexOf(messageContent);
|
let searchIndex = prompt.lastIndexOf(messageContent);
|
||||||
|
|
||||||
if (searchIndex !== -1) {
|
if (searchIndex !== -1) {
|
||||||
return { start: searchIndex, end: searchIndex + messageContent.length };
|
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
|
// If full content not found, try last N characters with progressively smaller chunks
|
||||||
// This handles cases where messages are truncated in the prompt
|
// This handles cases where messages are truncated in the prompt
|
||||||
const searchLengths = [500, 300, 200, 100, 50];
|
const searchLengths = [500, 300, 200, 100, 50];
|
||||||
|
|
||||||
for (const len of searchLengths) {
|
for (const len of searchLengths) {
|
||||||
if (messageContent.length <= len) {
|
if (messageContent.length <= len) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchContent = messageContent.slice(-len);
|
const searchContent = messageContent.slice(-len);
|
||||||
searchIndex = prompt.lastIndexOf(searchContent);
|
searchIndex = prompt.lastIndexOf(searchContent);
|
||||||
|
|
||||||
if (searchIndex !== -1) {
|
if (searchIndex !== -1) {
|
||||||
return { start: searchIndex, end: searchIndex + searchContent.length };
|
return { start: searchIndex, end: searchIndex + searchContent.length };
|
||||||
}
|
}
|
||||||
@@ -241,7 +241,7 @@ function findMessageInPrompt(prompt, messageContent) {
|
|||||||
/**
|
/**
|
||||||
* Injects historical context into a text completion prompt string.
|
* Injects historical context into a text completion prompt string.
|
||||||
* Searches for message content in the prompt and appends context after matches.
|
* Searches for message content in the prompt and appends context after matches.
|
||||||
*
|
*
|
||||||
* @param {string} prompt - The text completion prompt
|
* @param {string} prompt - The text completion prompt
|
||||||
* @returns {string} - The modified prompt with injected context
|
* @returns {string} - The modified prompt with injected context
|
||||||
*/
|
*/
|
||||||
@@ -268,7 +268,7 @@ function injectContextIntoTextPrompt(prompt) {
|
|||||||
|
|
||||||
// Find the message content in the prompt
|
// Find the message content in the prompt
|
||||||
const position = findMessageInPrompt(modifiedPrompt, message.mes);
|
const position = findMessageInPrompt(modifiedPrompt, message.mes);
|
||||||
|
|
||||||
if (!position) {
|
if (!position) {
|
||||||
// Message not found in prompt (might be truncated or not included)
|
// 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`);
|
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.
|
* Injects historical context into a chat completion message array.
|
||||||
* Modifies the content of messages in the array directly.
|
* Modifies the content of messages in the array directly.
|
||||||
*
|
*
|
||||||
* @param {Array} chatMessages - The chat completion message array
|
* @param {Array} chatMessages - The chat completion message array
|
||||||
* @returns {Array} - The modified message array with injected context
|
* @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
|
// Find this message in the chat completion array by matching content
|
||||||
// Try full content first, then progressively smaller suffixes
|
// Try full content first, then progressively smaller suffixes
|
||||||
let found = false;
|
let found = false;
|
||||||
|
|
||||||
for (const promptMsg of chatMessages) {
|
for (const promptMsg of chatMessages) {
|
||||||
if (!promptMsg.content || typeof promptMsg.content !== 'string') {
|
if (!promptMsg.content || typeof promptMsg.content !== 'string') {
|
||||||
continue;
|
continue;
|
||||||
@@ -335,7 +335,7 @@ function injectContextIntoChatPrompt(chatMessages) {
|
|||||||
if (messageContent.length <= len) {
|
if (messageContent.length <= len) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchContent = messageContent.slice(-len);
|
const searchContent = messageContent.slice(-len);
|
||||||
if (promptMsg.content.includes(searchContent)) {
|
if (promptMsg.content.includes(searchContent)) {
|
||||||
promptMsg.content = promptMsg.content + ctxContent;
|
promptMsg.content = promptMsg.content + ctxContent;
|
||||||
@@ -344,12 +344,12 @@ function injectContextIntoChatPrompt(chatMessages) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found) {
|
if (found) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
console.debug(`[RPG Companion] Could not find message ${msgIdx} in chat prompt for context injection`);
|
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).
|
* Injects historical context into finalMesSend message array (text completion).
|
||||||
* Iterates through chat and finalMesSend in order, matching by content to skip injected messages.
|
* 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: []}
|
* @param {Array} finalMesSend - The array of message objects {message: string, extensionPrompts: []}
|
||||||
* @returns {number} - Number of injections made
|
* @returns {number} - Number of injections made
|
||||||
*/
|
*/
|
||||||
@@ -381,20 +381,20 @@ function injectContextIntoFinalMesSend(finalMesSend) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let injectedCount = 0;
|
let injectedCount = 0;
|
||||||
|
|
||||||
// Build a map from chat index to finalMesSend index by matching content in order
|
// 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
|
// This handles injected messages (author's note, OOC, etc.) that exist in finalMesSend but not in chat
|
||||||
const chatToMesSendMap = new Map();
|
const chatToMesSendMap = new Map();
|
||||||
let mesSendIdx = 0;
|
let mesSendIdx = 0;
|
||||||
|
|
||||||
for (let chatIdx = 0; chatIdx < chat.length && mesSendIdx < finalMesSend.length; chatIdx++) {
|
for (let chatIdx = 0; chatIdx < chat.length && mesSendIdx < finalMesSend.length; chatIdx++) {
|
||||||
const chatMsg = chat[chatIdx];
|
const chatMsg = chat[chatIdx];
|
||||||
if (!chatMsg || chatMsg.is_system) {
|
if (!chatMsg || chatMsg.is_system) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatContent = chatMsg.mes || '';
|
const chatContent = chatMsg.mes || '';
|
||||||
|
|
||||||
// Look for this chat message in finalMesSend starting from current position
|
// Look for this chat message in finalMesSend starting from current position
|
||||||
// Skip any finalMesSend entries that don't match (they're injected content)
|
// Skip any finalMesSend entries that don't match (they're injected content)
|
||||||
while (mesSendIdx < finalMesSend.length) {
|
while (mesSendIdx < finalMesSend.length) {
|
||||||
@@ -403,40 +403,40 @@ function injectContextIntoFinalMesSend(finalMesSend) {
|
|||||||
mesSendIdx++;
|
mesSendIdx++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this finalMesSend message contains the chat content
|
// Check if this finalMesSend message contains the chat content
|
||||||
// Use a substring match since instruct formatting adds prefixes/suffixes
|
// Use a substring match since instruct formatting adds prefixes/suffixes
|
||||||
// Match with sufficient content (first 50 chars or full message if shorter)
|
// Match with sufficient content (first 50 chars or full message if shorter)
|
||||||
const matchContent = chatContent.length > 50
|
const matchContent = chatContent.length > 50
|
||||||
? chatContent.substring(0, 50)
|
? chatContent.substring(0, 50)
|
||||||
: chatContent;
|
: chatContent;
|
||||||
|
|
||||||
if (matchContent && mesSendObj.message.includes(matchContent)) {
|
if (matchContent && mesSendObj.message.includes(matchContent)) {
|
||||||
// Found a match - record the mapping
|
// Found a match - record the mapping
|
||||||
chatToMesSendMap.set(chatIdx, mesSendIdx);
|
chatToMesSendMap.set(chatIdx, mesSendIdx);
|
||||||
mesSendIdx++;
|
mesSendIdx++;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This finalMesSend entry doesn't match - it's injected content, skip it
|
// This finalMesSend entry doesn't match - it's injected content, skip it
|
||||||
mesSendIdx++;
|
mesSendIdx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now inject context using the map
|
// Now inject context using the map
|
||||||
for (const [chatIdx, ctxContent] of pendingContextMap) {
|
for (const [chatIdx, ctxContent] of pendingContextMap) {
|
||||||
const targetMesSendIdx = chatToMesSendMap.get(chatIdx);
|
const targetMesSendIdx = chatToMesSendMap.get(chatIdx);
|
||||||
|
|
||||||
if (targetMesSendIdx === undefined) {
|
if (targetMesSendIdx === undefined) {
|
||||||
console.debug(`[RPG Companion] Chat message ${chatIdx} not found in finalMesSend mapping`);
|
console.debug(`[RPG Companion] Chat message ${chatIdx} not found in finalMesSend mapping`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mesSendObj = finalMesSend[targetMesSendIdx];
|
const mesSendObj = finalMesSend[targetMesSendIdx];
|
||||||
if (!mesSendObj || !mesSendObj.message) {
|
if (!mesSendObj || !mesSendObj.message) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append context to this message
|
// Append context to this message
|
||||||
mesSendObj.message = mesSendObj.message + ctxContent;
|
mesSendObj.message = mesSendObj.message + ctxContent;
|
||||||
injectedCount++;
|
injectedCount++;
|
||||||
@@ -450,7 +450,7 @@ function injectContextIntoFinalMesSend(finalMesSend) {
|
|||||||
* Event handler for GENERATE_BEFORE_COMBINE_PROMPTS (text completion).
|
* Event handler for GENERATE_BEFORE_COMBINE_PROMPTS (text completion).
|
||||||
* Injects historical context into the finalMesSend array before prompt combination.
|
* Injects historical context into the finalMesSend array before prompt combination.
|
||||||
* This is more reliable than post-combine string searching.
|
* This is more reliable than post-combine string searching.
|
||||||
*
|
*
|
||||||
* @param {Object} eventData - Event data with finalMesSend and other properties
|
* @param {Object} eventData - Event data with finalMesSend and other properties
|
||||||
*/
|
*/
|
||||||
function onGenerateBeforeCombinePrompts(eventData) {
|
function onGenerateBeforeCombinePrompts(eventData) {
|
||||||
@@ -478,7 +478,7 @@ function onGenerateBeforeCombinePrompts(eventData) {
|
|||||||
/**
|
/**
|
||||||
* Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion).
|
* Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion).
|
||||||
* This is now a backup/fallback - primary injection happens in BEFORE_COMBINE.
|
* This is now a backup/fallback - primary injection happens in BEFORE_COMBINE.
|
||||||
*
|
*
|
||||||
* @param {Object} eventData - Event data with prompt property
|
* @param {Object} eventData - Event data with prompt property
|
||||||
*/
|
*/
|
||||||
function onGenerateAfterCombinePrompts(eventData) {
|
function onGenerateAfterCombinePrompts(eventData) {
|
||||||
@@ -508,7 +508,7 @@ function onGenerateAfterCombinePrompts(eventData) {
|
|||||||
/**
|
/**
|
||||||
* Event handler for CHAT_COMPLETION_PROMPT_READY.
|
* Event handler for CHAT_COMPLETION_PROMPT_READY.
|
||||||
* Injects historical context into the chat message array.
|
* Injects historical context into the chat message array.
|
||||||
*
|
*
|
||||||
* @param {Object} eventData - Event data with chat property
|
* @param {Object} eventData - Event data with chat property
|
||||||
*/
|
*/
|
||||||
function onChatCompletionPromptReady(eventData) {
|
function onChatCompletionPromptReady(eventData) {
|
||||||
@@ -938,16 +938,16 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
|||||||
export function initHistoryInjectionListeners() {
|
export function initHistoryInjectionListeners() {
|
||||||
// Register persistent listeners for prompt injection
|
// Register persistent listeners for prompt injection
|
||||||
// These check pendingContextMap and only inject if there's data
|
// These check pendingContextMap and only inject if there's data
|
||||||
|
|
||||||
// Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects)
|
// Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects)
|
||||||
eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
|
eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
|
||||||
|
|
||||||
// Fallback: AFTER_COMBINE for text completion (string-based injection)
|
// Fallback: AFTER_COMBINE for text completion (string-based injection)
|
||||||
eventSource.on(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
|
eventSource.on(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
|
||||||
|
|
||||||
// Chat completion (OpenAI, etc.)
|
// Chat completion (OpenAI, etc.)
|
||||||
eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
|
eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
|
||||||
|
|
||||||
console.log('[RPG Companion] History injection listeners initialized');
|
console.log('[RPG Companion] History injection listeners initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -725,13 +725,14 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relationship
|
// Relationship - check both Relationship (new format) and relationship (old format)
|
||||||
if (char.relationship) {
|
const relationshipValue = char.Relationship || char.relationship;
|
||||||
|
if (relationshipValue) {
|
||||||
let relValue;
|
let relValue;
|
||||||
if (typeof char.relationship === 'object' && !Array.isArray(char.relationship) && 'status' in char.relationship) {
|
if (typeof relationshipValue === 'object' && !Array.isArray(relationshipValue) && 'status' in relationshipValue) {
|
||||||
relValue = getValue(char.relationship.status);
|
relValue = getValue(relationshipValue.status);
|
||||||
} else {
|
} else {
|
||||||
relValue = getValue(char.relationship);
|
relValue = getValue(relationshipValue);
|
||||||
}
|
}
|
||||||
if (relValue) formatted += ` Relationship: ${relValue}\n`;
|
if (relValue) formatted += ` Relationship: ${relValue}\n`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,13 +395,20 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
// Load swipe data into lastGeneratedData for display (both modes)
|
// Load swipe data into lastGeneratedData for display (both modes)
|
||||||
lastGeneratedData.userStats = swipeData.userStats || null;
|
lastGeneratedData.userStats = swipeData.userStats || null;
|
||||||
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
||||||
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
|
||||||
|
|
||||||
// Parse user stats if available
|
// Normalize characterThoughts to string format (for backward compatibility with old object format)
|
||||||
if (swipeData.userStats) {
|
if (swipeData.characterThoughts && typeof swipeData.characterThoughts === 'object') {
|
||||||
parseUserStats(swipeData.userStats);
|
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
|
||||||
|
// The lastGeneratedData is loaded for display purposes only
|
||||||
|
// parseUserStats() updates extensionSettings.userStats which should only be modified
|
||||||
|
// by new generations or manual edits, not by swipe navigation
|
||||||
|
|
||||||
// console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId);
|
// console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId);
|
||||||
} else {
|
} else {
|
||||||
// console.log('[RPG Companion] ℹ️ No stored data for swipe:', currentSwipeId);
|
// console.log('[RPG Companion] ℹ️ No stored data for swipe:', currentSwipeId);
|
||||||
|
|||||||
@@ -512,17 +512,20 @@ export function renderThoughts() {
|
|||||||
const fieldNameLower = field.name.toLowerCase();
|
const fieldNameLower = field.name.toLowerCase();
|
||||||
// Skip lock icons for thoughts field
|
// Skip lock icons for thoughts field
|
||||||
const showLock = !fieldNameLower.includes('thought');
|
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) {
|
if (showLock) {
|
||||||
const lockIconHtml = getLockIconHtml('characters', `${char.name}.${field.name}`);
|
const lockIconHtml = getLockIconHtml('characters', `${char.name}.${field.name}`);
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-character-field rpg-character-${fieldId}" style="position: relative;">
|
<div class="rpg-character-field rpg-character-${fieldId}" style="position: relative;">
|
||||||
${lockIconHtml}
|
${lockIconHtml}
|
||||||
<span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</span>
|
<span class="rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}" ${placeholder}>${fieldValue}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</div>
|
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}" ${placeholder}>${fieldValue}</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,6 +567,16 @@ export function renderThoughts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debugLog('[RPG Thoughts] Finished building all character cards');
|
debugLog('[RPG Thoughts] Finished building all character cards');
|
||||||
|
|
||||||
|
// Add "Add Character" button if data exists (inside rpg-thoughts-content)
|
||||||
|
if (presentCharacters.length > 0) {
|
||||||
|
html += `
|
||||||
|
<button class="rpg-add-character-btn" title="Add a new character">
|
||||||
|
<i class="fa-solid fa-plus"></i> Add Character
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,6 +675,31 @@ export function renderThoughts() {
|
|||||||
fileInput.trigger('click');
|
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
|
// Remove updating class after animation
|
||||||
if (extensionSettings.enableAnimations) {
|
if (extensionSettings.enableAnimations) {
|
||||||
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
|
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
|
||||||
@@ -788,6 +826,136 @@ export function removeCharacter(characterName) {
|
|||||||
renderThoughts();
|
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.
|
* 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.
|
||||||
@@ -855,18 +1023,27 @@ export function updateCharacterField(characterName, field, value) {
|
|||||||
} else if (field === 'emoji') {
|
} else if (field === 'emoji') {
|
||||||
char.emoji = value;
|
char.emoji = value;
|
||||||
} else if (field === 'Relationship') {
|
} else if (field === 'Relationship') {
|
||||||
// Store relationship as text, converting emoji if needed
|
// Store relationship in the correct nested format
|
||||||
|
// Remove old flat format if it exists
|
||||||
|
if (char.Relationship) {
|
||||||
|
delete char.Relationship;
|
||||||
|
}
|
||||||
|
|
||||||
// First check if it's an emoji → convert to text
|
// First check if it's an emoji → convert to text
|
||||||
|
let relationshipValue;
|
||||||
if (emojiToRelationship[value]) {
|
if (emojiToRelationship[value]) {
|
||||||
char.Relationship = emojiToRelationship[value];
|
relationshipValue = emojiToRelationship[value];
|
||||||
} else {
|
} else {
|
||||||
// It's text - find matching relationship name (case-insensitive)
|
// It's text - find matching relationship name (case-insensitive)
|
||||||
const matchingRelationship = Object.keys(relationshipEmojis).find(
|
const matchingRelationship = Object.keys(relationshipEmojis).find(
|
||||||
name => name.toLowerCase() === value.toLowerCase()
|
name => name.toLowerCase() === value.toLowerCase()
|
||||||
);
|
);
|
||||||
char.Relationship = matchingRelationship || value;
|
relationshipValue = matchingRelationship || value;
|
||||||
}
|
}
|
||||||
// console.log('[RPG Companion] After update - char.Relationship:', char.Relationship);
|
|
||||||
|
// Store in the correct nested format
|
||||||
|
char.relationship = { status: relationshipValue };
|
||||||
|
// console.log('[RPG Companion] After update - char.relationship:', char.relationship);
|
||||||
// console.log('[RPG Companion] relationshipEmojis:', relationshipEmojis);
|
// console.log('[RPG Companion] relationshipEmojis:', relationshipEmojis);
|
||||||
// console.log('[RPG Companion] emojiToRelationship:', emojiToRelationship);
|
// console.log('[RPG Companion] emojiToRelationship:', emojiToRelationship);
|
||||||
} else if (field.toLowerCase() === 'thoughts' || field === (presentCharsConfig?.thoughts?.name || 'Thoughts')) {
|
} else if (field.toLowerCase() === 'thoughts' || field === (presentCharsConfig?.thoughts?.name || 'Thoughts')) {
|
||||||
@@ -882,15 +1059,44 @@ export function updateCharacterField(characterName, field, value) {
|
|||||||
numValue = Math.max(0, Math.min(100, numValue));
|
numValue = Math.max(0, Math.min(100, numValue));
|
||||||
char.stats[field] = numValue;
|
char.stats[field] = numValue;
|
||||||
} else {
|
} else {
|
||||||
// It's a custom detail field
|
// It's a custom detail field - store in details object
|
||||||
if (!char.details) char.details = {};
|
if (!char.details) char.details = {};
|
||||||
char.details[field] = value;
|
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
|
// Save back to lastGeneratedData as JSON string (consistent with infoBox and userStats)
|
||||||
lastGeneratedData.characterThoughts = Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray };
|
lastGeneratedData.characterThoughts = JSON.stringify(Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray }, null, 2);
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
||||||
|
|
||||||
// console.log('[RPG Companion] Saved to lastGeneratedData.characterThoughts:', JSON.stringify(lastGeneratedData.characterThoughts));
|
// console.log('[RPG Companion] Saved to lastGeneratedData.characterThoughts:', JSON.stringify(lastGeneratedData.characterThoughts));
|
||||||
@@ -971,6 +1177,9 @@ export function updateCharacterField(characterName, field, value) {
|
|||||||
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
|
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
|
||||||
const isThoughtsField = field.toLowerCase() === 'thoughts' || field === thoughtsFieldName;
|
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
|
// First pass: check if Stats line exists and update other fields
|
||||||
for (let i = characterStartIndex; i < characterEndIndex; i++) {
|
for (let i = characterStartIndex; i < characterEndIndex; i++) {
|
||||||
const line = lines[i].trim();
|
const line = lines[i].trim();
|
||||||
@@ -978,35 +1187,37 @@ export function updateCharacterField(characterName, field, value) {
|
|||||||
if (line.startsWith('Stats:')) {
|
if (line.startsWith('Stats:')) {
|
||||||
statsLineExists = true;
|
statsLineExists = true;
|
||||||
statsLineIndex = i;
|
statsLineIndex = i;
|
||||||
|
continue; // Skip to next line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for name update
|
||||||
if (field === 'name' && line.startsWith('- ')) {
|
if (field === 'name' && line.startsWith('- ')) {
|
||||||
lines[i] = `- ${value}`;
|
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());
|
// Check for Relationship field
|
||||||
parts[0] = value;
|
if (field === 'Relationship' && line.startsWith('Relationship:')) {
|
||||||
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:')) {
|
|
||||||
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
|
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
|
||||||
const relationshipValue = emojiToRelationship[value] || value;
|
const relationshipValue = emojiToRelationship[value] || value;
|
||||||
lines[i] = `Relationship: ${relationshipValue}`;
|
lines[i] = `Relationship: ${relationshipValue}`;
|
||||||
|
fieldUpdated = true;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
else if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
|
|
||||||
// Update thoughts field
|
// Check for Thoughts field
|
||||||
lines[i] = `${thoughtsFieldName}: ${value}`;
|
if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
|
||||||
// console.log('[RPG Companion] Updated thoughts:', lines[i]);
|
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 +1284,28 @@ export function updateCharacterField(characterName, field, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create new character block
|
// Create new character block (v3 text format only)
|
||||||
const dividerIndex = lines.findIndex(line => line.includes('---'));
|
const dividerIndex = lines.findIndex(line => line.includes('---'));
|
||||||
if (dividerIndex >= 0) {
|
if (dividerIndex >= 0) {
|
||||||
const newCharacterLines = [`- ${characterName}`];
|
const newCharacterLines = [`- ${characterName}`];
|
||||||
|
|
||||||
let detailsParts = [field === 'emoji' ? value : '😊'];
|
// Add custom detail fields as standalone lines
|
||||||
for (let i = 0; i < enabledFields.length; i++) {
|
for (const customField of enabledFields) {
|
||||||
detailsParts.push(field === enabledFields[i].name ? value : '');
|
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) {
|
if (presentCharsConfig?.relationshipFields?.length > 0) {
|
||||||
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
|
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
|
||||||
const relationshipValue = field === 'Relationship' ? (emojiToRelationship[value] || value) : 'Neutral';
|
const relationshipValue = field === 'Relationship' ? (emojiToRelationship[value] || value) : 'Neutral';
|
||||||
newCharacterLines.push(`Relationship: ${relationshipValue}`);
|
newCharacterLines.push(` Relationship: ${relationshipValue}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Stats if enabled
|
||||||
if (enabledCharStats.length > 0) {
|
if (enabledCharStats.length > 0) {
|
||||||
const statsParts = enabledCharStats.map(s => {
|
const statsParts = enabledCharStats.map(s => {
|
||||||
if (field === s.name) {
|
if (field === s.name) {
|
||||||
@@ -1104,7 +1320,7 @@ export function updateCharacterField(characterName, field, value) {
|
|||||||
}
|
}
|
||||||
return `${s.name}: 0%`;
|
return `${s.name}: 0%`;
|
||||||
});
|
});
|
||||||
newCharacterLines.push(`Stats: ${statsParts.join(' | ')}`);
|
newCharacterLines.push(` Stats: ${statsParts.join(' | ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.splice(dividerIndex + 1, 0, ...newCharacterLines);
|
lines.splice(dividerIndex + 1, 0, ...newCharacterLines);
|
||||||
|
|||||||
@@ -105,90 +105,48 @@ function getCurrentTime() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Patterns for specific weather conditions (order matters - combined effects first)
|
||||||
|
// Grouped by languages for easy editing
|
||||||
|
const WEATHER_PATTERNS_BY_LANGUAGE = {
|
||||||
|
en: [
|
||||||
|
{ id: "blizzard", patterns: [ "blizzard" ] }, // Snow + Wind
|
||||||
|
{ id: "storm", patterns: [ "storm", "thunder", "lightning" ] }, // Rain + Lightning
|
||||||
|
{ id: "wind", patterns: [ "wind", "breeze", "gust", "gale" ] },
|
||||||
|
{ id: "snow", patterns: [ "snow", "flurries" ] },
|
||||||
|
{ id: "rain", patterns: [ "rain", "drizzle", "shower" ] },
|
||||||
|
{ id: "mist", patterns: [ "mist", "fog", "haze" ] },
|
||||||
|
{ id: "sunny", patterns: [ "sunny", "clear", "bright" ] },
|
||||||
|
{ id: "none", patterns: [ "cloud", "overcast", "indoor", "inside" ] },
|
||||||
|
],
|
||||||
|
ru: [
|
||||||
|
{ id: "blizzard", patterns: [ "метель" ] },
|
||||||
|
{ id: "storm", patterns: [ "гроза", "буря", "шторм" ] },
|
||||||
|
{ id: "wind", patterns: [ "ветер", "ветрено", "ветерок", "бриз", "легкий бриз", "слегка ветрено", "легкий ветер", "шквал,буря" ] },
|
||||||
|
{ id: "snow", patterns: [ "снег", "снегопад" ] },
|
||||||
|
{ id: "rain", patterns: [ "дождь", "морось", "ливень" ] },
|
||||||
|
{ id: "mist", patterns: [ "мгла", "туман", "туманно" ] },
|
||||||
|
{ id: "sunny", patterns: [ "солнечно", "ясно", "ярко", "ясное утро", "ясный день" ] },
|
||||||
|
{ id: "none", patterns: [ "облачно", "пасмурно", "в помещении", "внутри" ] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse weather text to determine effect type
|
* Parse weather text to determine effect type
|
||||||
*/
|
*/
|
||||||
function parseWeatherType(weatherText) {
|
function parseWeatherType(weatherText) {
|
||||||
if (!weatherText) return 'none';
|
if (!weatherText) return "none";
|
||||||
|
|
||||||
const text = weatherText.toLowerCase();
|
const text = weatherText.toLowerCase();
|
||||||
|
|
||||||
const weather_en = new Map([
|
for (const language of Object.values(WEATHER_PATTERNS_BY_LANGUAGE)) {
|
||||||
["blizzard", "blizzard"],
|
for (const { id, patterns } of language) {
|
||||||
["storm", "storm,thunder,lightning"],
|
if (patterns.some(p => text.includes(p))) {
|
||||||
["wind", "wind,breeze,gust,gale"],
|
return id;
|
||||||
["snow", "snow,flurries"],
|
}
|
||||||
["rain", "rain,drizzle,shower"],
|
}
|
||||||
["mist", "mist,fog,haze"],
|
}
|
||||||
["sunny", "sunny,clear,bright"],
|
|
||||||
["none", "cloud,overcast,indoor,inside"],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const weather_ru = new Map([
|
|
||||||
["blizzard", "метель"],
|
|
||||||
["storm", "гроза,буря,шторм"],
|
|
||||||
[
|
|
||||||
"wind",
|
|
||||||
"ветер,ветрено,ветерок,бриз,легкий бриз,слегка ветрено,легкий ветер,шквал,буря",
|
|
||||||
],
|
|
||||||
["snow", "снег,снегопад"],
|
|
||||||
["rain", "дождь,морось,ливень"],
|
|
||||||
["mist", "мгла,туман,туманно"],
|
|
||||||
["sunny", "солнечно,ясно,ярко,ясное утро,ясный день"],
|
|
||||||
["none", "облачно,пасмурно,в помещении,внутри"],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Check for specific weather conditions (order matters - check combined effects first)
|
|
||||||
if (
|
|
||||||
weather_en.get("blizzard").includes(text) ||
|
|
||||||
weather_ru.get("blizzard").includes(text)
|
|
||||||
) {
|
|
||||||
return "blizzard"; // Snow + Wind
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
weather_en.get("storm").includes(text) ||
|
|
||||||
weather_ru.get("storm").includes(text)
|
|
||||||
) {
|
|
||||||
return "storm"; // Rain + Lightning
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
weather_en.get("wind").includes(text) ||
|
|
||||||
weather_ru.get("wind").includes(text)
|
|
||||||
) {
|
|
||||||
return "wind";
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
weather_en.get("snow").includes(text) ||
|
|
||||||
weather_ru.get("snow").includes(text)
|
|
||||||
) {
|
|
||||||
return "snow";
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
weather_en.get("rain").includes(text) ||
|
|
||||||
weather_ru.get("rain").includes(text)
|
|
||||||
) {
|
|
||||||
return "rain";
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
weather_en.get("mist").includes(text) ||
|
|
||||||
weather_ru.get("mist").includes(text)
|
|
||||||
) {
|
|
||||||
return "mist";
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
weather_en.get("sunny").includes(text) ||
|
|
||||||
weather_ru.get("sunny").includes(text)
|
|
||||||
) {
|
|
||||||
return "sunny";
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
weather_en.get("none").includes(text) ||
|
|
||||||
weather_ru.get("none").includes(text)
|
|
||||||
) {
|
|
||||||
return "none";
|
return "none";
|
||||||
}
|
|
||||||
|
|
||||||
return "none";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -289,24 +247,24 @@ function calculateSunPosition(hour) {
|
|||||||
// Daytime is roughly 5 AM to 8 PM (5-20)
|
// Daytime is roughly 5 AM to 8 PM (5-20)
|
||||||
// Map hour to position along an arc
|
// Map hour to position along an arc
|
||||||
// 5 AM = far left, low | 12 PM = center, high | 8 PM = far right, low
|
// 5 AM = far left, low | 12 PM = center, high | 8 PM = far right, low
|
||||||
|
|
||||||
if (hour === null) hour = 12; // Default to noon if unknown
|
if (hour === null) hour = 12; // Default to noon if unknown
|
||||||
|
|
||||||
// Clamp to daytime hours
|
// Clamp to daytime hours
|
||||||
const clampedHour = Math.max(5, Math.min(20, hour));
|
const clampedHour = Math.max(5, Math.min(20, hour));
|
||||||
|
|
||||||
// Normalize to 0-1 range (5 AM = 0, 20 PM = 1)
|
// Normalize to 0-1 range (5 AM = 0, 20 PM = 1)
|
||||||
const progress = (clampedHour - 5) / 15;
|
const progress = (clampedHour - 5) / 15;
|
||||||
|
|
||||||
// Horizontal position: 3% to 92% (left to right, wider range)
|
// Horizontal position: 3% to 92% (left to right, wider range)
|
||||||
const left = 3 + progress * 89;
|
const left = 3 + progress * 89;
|
||||||
|
|
||||||
// Vertical position: parabolic arc (high at noon, low at dawn/dusk)
|
// Vertical position: parabolic arc (high at noon, low at dawn/dusk)
|
||||||
// At progress 0.5 (noon), top should be ~8% (high)
|
// At progress 0.5 (noon), top should be ~8% (high)
|
||||||
// At progress 0 or 1, top should be ~40% (low, near horizon)
|
// At progress 0 or 1, top should be ~40% (low, near horizon)
|
||||||
const normalizedProgress = (progress - 0.5) * 2; // -1 to 1
|
const normalizedProgress = (progress - 0.5) * 2; // -1 to 1
|
||||||
const top = 8 + 32 * (normalizedProgress * normalizedProgress);
|
const top = 8 + 32 * (normalizedProgress * normalizedProgress);
|
||||||
|
|
||||||
return { left, top };
|
return { left, top };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +277,7 @@ function createSunshine(hour) {
|
|||||||
|
|
||||||
// Create the sun based on current hour
|
// Create the sun based on current hour
|
||||||
const sunPos = calculateSunPosition(hour);
|
const sunPos = calculateSunPosition(hour);
|
||||||
|
|
||||||
const sun = document.createElement('div');
|
const sun = document.createElement('div');
|
||||||
sun.className = 'rpg-weather-particle rpg-clear-sun';
|
sun.className = 'rpg-weather-particle rpg-clear-sun';
|
||||||
sun.style.left = `${sunPos.left}vw`;
|
sun.style.left = `${sunPos.left}vw`;
|
||||||
@@ -621,9 +579,9 @@ function calculateMoonPosition(hour) {
|
|||||||
// Nighttime is roughly 8 PM to 5 AM (20-5)
|
// Nighttime is roughly 8 PM to 5 AM (20-5)
|
||||||
// Map hour to position along an arc
|
// Map hour to position along an arc
|
||||||
// 8 PM = far left, low | midnight = center-left, high | 5 AM = far right, low
|
// 8 PM = far left, low | midnight = center-left, high | 5 AM = far right, low
|
||||||
|
|
||||||
if (hour === null) hour = 0; // Default to midnight if unknown
|
if (hour === null) hour = 0; // Default to midnight if unknown
|
||||||
|
|
||||||
// Normalize night hours to 0-1 range
|
// Normalize night hours to 0-1 range
|
||||||
// 20 (8 PM) = 0, 0 (midnight) = ~0.44, 5 (5 AM) = 1
|
// 20 (8 PM) = 0, 0 (midnight) = ~0.44, 5 (5 AM) = 1
|
||||||
let progress;
|
let progress;
|
||||||
@@ -634,16 +592,16 @@ function calculateMoonPosition(hour) {
|
|||||||
// Midnight to 5 AM: 0-5 maps to 0.44-1
|
// Midnight to 5 AM: 0-5 maps to 0.44-1
|
||||||
progress = (hour + 4) / 9;
|
progress = (hour + 4) / 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal position: 10% to 80% (left to right)
|
// Horizontal position: 10% to 80% (left to right)
|
||||||
const left = 10 + progress * 70;
|
const left = 10 + progress * 70;
|
||||||
|
|
||||||
// Vertical position: parabolic arc (high at ~2 AM, low at dusk/dawn)
|
// Vertical position: parabolic arc (high at ~2 AM, low at dusk/dawn)
|
||||||
// Peak should be around progress 0.67 (~2 AM)
|
// Peak should be around progress 0.67 (~2 AM)
|
||||||
const peakProgress = 0.5;
|
const peakProgress = 0.5;
|
||||||
const normalizedProgress = (progress - peakProgress) * 2; // -1 to 1
|
const normalizedProgress = (progress - peakProgress) * 2; // -1 to 1
|
||||||
const top = 8 + 25 * (normalizedProgress * normalizedProgress);
|
const top = 8 + 25 * (normalizedProgress * normalizedProgress);
|
||||||
|
|
||||||
return { left, top };
|
return { left, top };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,7 +614,7 @@ function updateCelestialPosition(hour) {
|
|||||||
// Update sun position if it exists
|
// Update sun position if it exists
|
||||||
const sun = weatherContainer.querySelector('.rpg-clear-sun');
|
const sun = weatherContainer.querySelector('.rpg-clear-sun');
|
||||||
const sunGlow = weatherContainer.querySelector('.rpg-clear-sun-glow');
|
const sunGlow = weatherContainer.querySelector('.rpg-clear-sun-glow');
|
||||||
|
|
||||||
if (sun && sunGlow) {
|
if (sun && sunGlow) {
|
||||||
const sunPos = calculateSunPosition(hour);
|
const sunPos = calculateSunPosition(hour);
|
||||||
sun.style.left = `${sunPos.left}vw`;
|
sun.style.left = `${sunPos.left}vw`;
|
||||||
@@ -669,7 +627,7 @@ function updateCelestialPosition(hour) {
|
|||||||
// Update moon position if it exists
|
// Update moon position if it exists
|
||||||
const moon = weatherContainer.querySelector('.rpg-night-moon');
|
const moon = weatherContainer.querySelector('.rpg-night-moon');
|
||||||
const moonGlow = weatherContainer.querySelector('.rpg-night-moon-glow');
|
const moonGlow = weatherContainer.querySelector('.rpg-night-moon-glow');
|
||||||
|
|
||||||
if (moon && moonGlow) {
|
if (moon && moonGlow) {
|
||||||
const moonPos = calculateMoonPosition(hour);
|
const moonPos = calculateMoonPosition(hour);
|
||||||
moon.style.left = `${moonPos.left}vw`;
|
moon.style.left = `${moonPos.left}vw`;
|
||||||
|
|||||||
@@ -2329,6 +2329,40 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
transform: scale(0.95);
|
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 */
|
/* Character traits/status line and custom fields */
|
||||||
.rpg-character-traits,
|
.rpg-character-traits,
|
||||||
.rpg-character-field {
|
.rpg-character-field {
|
||||||
@@ -2340,12 +2374,20 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Placeholder for empty editable character fields */
|
/* Empty field placeholder using data-placeholder attribute */
|
||||||
.rpg-character-field.rpg-editable:empty::before {
|
.rpg-editable.rpg-empty-field:empty::before {
|
||||||
content: 'Click to edit...';
|
content: attr(data-placeholder);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
opacity: 0.5;
|
opacity: 0.4;
|
||||||
font-style: italic;
|
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 */
|
/* Character stats display */
|
||||||
|
|||||||
Reference in New Issue
Block a user