diff --git a/src/systems/generation/lockManager.js b/src/systems/generation/lockManager.js index 713bd6c..e8f467c 100644 --- a/src/systems/generation/lockManager.js +++ b/src/systems/generation/lockManager.js @@ -98,16 +98,19 @@ function applyUserStatsLocks(data, lockedItems) { } } - // Lock inventory items - handle bracket notation paths like "inventory.onPerson[0]" + // Lock inventory items - match by item name instead of index if (data.inventory && lockedItems.inventory) { - // Helper function to parse bracket notation and apply lock + // Helper function to apply locks based on item name const applyInventoryLocks = (items, category) => { if (!Array.isArray(items)) return items; + if (!lockedItems.inventory[category]) return items; - return items.map((item, index) => { - // Check if this specific item is locked using bracket notation with inventory prefix - const bracketPath = `${category}[${index}]`; - if (lockedItems.inventory[bracketPath]) { + return items.map((item) => { + // Get item name (handle both string and object formats) + const itemName = typeof item === 'string' ? item : (item.item || item.name || ''); + + // Check if this specific item name is locked + if (lockedItems.inventory[category][itemName]) { return typeof item === 'string' ? { item, locked: true } : { ...item, locked: true }; @@ -131,13 +134,13 @@ function applyUserStatsLocks(data, lockedItems) { data.inventory.assets = applyInventoryLocks(data.inventory.assets, 'assets'); } - // Apply locks to stored items (nested structure with inventory.stored.location[index]) + // Apply locks to stored items - match by item name if (data.inventory.stored && lockedItems.inventory.stored) { for (const location in data.inventory.stored) { - if (Array.isArray(data.inventory.stored[location])) { - data.inventory.stored[location] = data.inventory.stored[location].map((item, index) => { - const bracketPath = `${location}[${index}]`; - if (lockedItems.inventory.stored[bracketPath]) { + if (Array.isArray(data.inventory.stored[location]) && lockedItems.inventory.stored[location]) { + data.inventory.stored[location] = data.inventory.stored[location].map((item) => { + const itemName = typeof item === 'string' ? item : (item.item || item.name || ''); + if (lockedItems.inventory.stored[location][itemName]) { return typeof item === 'string' ? { item, locked: true } : { ...item, locked: true }; diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index abfd6e6..743c120 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -739,13 +739,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 811829e..3af43a8 100644 --- a/src/systems/integration/sillytavern.js +++ b/src/systems/integration/sillytavern.js @@ -395,13 +395,20 @@ 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; - // Parse user stats if available - if (swipeData.userStats) { - parseUserStats(swipeData.userStats); + // 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 + // 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); } else { // console.log('[RPG Companion] ℹ️ No stored data for swipe:', currentSwipeId); diff --git a/src/systems/rendering/inventory.js b/src/systems/rendering/inventory.js index 6eba950..0c914c3 100644 --- a/src/systems/rendering/inventory.js +++ b/src/systems/rendering/inventory.js @@ -81,7 +81,7 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') { if (viewMode === 'grid') { // Grid view: card-style items itemsHtml = items.map((item, index) => { - const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson[${index}]`); + const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson.${item}`); return `
${lockIconHtml} @@ -94,7 +94,7 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') { } else { // List view: full-width rows itemsHtml = items.map((item, index) => { - const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson[${index}]`); + const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson.${item}`); return `
${lockIconHtml} @@ -163,7 +163,7 @@ export function renderClothingView(clothingItems, viewMode = 'list') { if (viewMode === 'grid') { // Grid view: card-style items itemsHtml = items.map((item, index) => { - const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing[${index}]`); + const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing.${item}`); return `
${lockIconHtml} @@ -176,7 +176,7 @@ export function renderClothingView(clothingItems, viewMode = 'list') { } else { // List view: full-width rows itemsHtml = items.map((item, index) => { - const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing[${index}]`); + const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing.${item}`); return `
${lockIconHtml} @@ -291,7 +291,7 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li if (viewMode === 'grid') { // Grid view: card-style items itemsHtml = items.map((item, index) => { - const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}[${index}]`); + const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}.${item}`); return `
${lockIconHtml} @@ -304,7 +304,7 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li } else { // List view: full-width rows itemsHtml = items.map((item, index) => { - const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}[${index}]`); + const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}.${item}`); return `
${lockIconHtml} @@ -393,7 +393,7 @@ export function renderAssetsView(assets, viewMode = 'list') { if (viewMode === 'grid') { // Grid view: card-style items itemsHtml = items.map((item, index) => { - const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`); + const lockIconHtml = getLockIconHtml('userStats', `inventory.assets.${item}`); return `
${lockIconHtml} @@ -406,7 +406,7 @@ export function renderAssetsView(assets, viewMode = 'list') { } else { // List view: full-width rows itemsHtml = items.map((item, index) => { - const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`); + const lockIconHtml = getLockIconHtml('userStats', `inventory.assets.${item}`); return `
${lockIconHtml} diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index b5db03a..f6d5552 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -513,17 +513,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}
`; } } @@ -571,6 +574,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 += '
'; } @@ -669,6 +682,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); @@ -795,6 +833,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. @@ -862,18 +1030,27 @@ export function updateCharacterField(characterName, field, value) { } else if (field === 'emoji') { char.emoji = value; } 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 + let relationshipValue; if (emojiToRelationship[value]) { - char.Relationship = emojiToRelationship[value]; + relationshipValue = emojiToRelationship[value]; } else { // It's text - find matching relationship name (case-insensitive) const matchingRelationship = Object.keys(relationshipEmojis).find( 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] emojiToRelationship:', emojiToRelationship); } else if (field.toLowerCase() === 'thoughts' || field === (presentCharsConfig?.thoughts?.name || 'Thoughts')) { @@ -889,15 +1066,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)); @@ -978,6 +1184,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(); @@ -985,35 +1194,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) } } @@ -1080,23 +1291,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) { @@ -1111,7 +1327,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/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 6620321..0bef359 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -105,41 +105,48 @@ function getCurrentTime() { 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 */ function parseWeatherType(weatherText) { - if (!weatherText) return 'none'; + if (!weatherText) return "none"; const text = weatherText.toLowerCase(); - // Check for specific weather conditions (order matters - check combined effects first) - if (text.includes('blizzard')) { - return 'blizzard'; // Snow + Wind - } - if (text.includes('storm') || text.includes('thunder') || text.includes('lightning')) { - return 'storm'; // Rain + Lightning - } - if (text.includes('wind') || text.includes('breeze') || text.includes('gust') || text.includes('gale')) { - return 'wind'; - } - if (text.includes('snow') || text.includes('flurries')) { - return 'snow'; - } - if (text.includes('rain') || text.includes('drizzle') || text.includes('shower')) { - return 'rain'; - } - if (text.includes('mist') || text.includes('fog') || text.includes('haze')) { - return 'mist'; - } - if (text.includes('sunny') || text.includes('clear') || text.includes('bright')) { - return 'sunny'; - } - if (text.includes('cloud') || text.includes('overcast') || text.includes('indoor') || text.includes('inside')) { - return 'none'; + for (const language of Object.values(WEATHER_PATTERNS_BY_LANGUAGE)) { + for (const { id, patterns } of language) { + if (patterns.some(p => text.includes(p))) { + return id; + } + } } - return 'none'; + return "none"; } /** @@ -240,24 +247,24 @@ function calculateSunPosition(hour) { // Daytime is roughly 5 AM to 8 PM (5-20) // Map hour to position along an arc // 5 AM = far left, low | 12 PM = center, high | 8 PM = far right, low - + if (hour === null) hour = 12; // Default to noon if unknown - + // Clamp to daytime hours const clampedHour = Math.max(5, Math.min(20, hour)); - + // Normalize to 0-1 range (5 AM = 0, 20 PM = 1) const progress = (clampedHour - 5) / 15; - + // Horizontal position: 3% to 92% (left to right, wider range) const left = 3 + progress * 89; - + // Vertical position: parabolic arc (high at noon, low at dawn/dusk) // At progress 0.5 (noon), top should be ~8% (high) // At progress 0 or 1, top should be ~40% (low, near horizon) const normalizedProgress = (progress - 0.5) * 2; // -1 to 1 const top = 8 + 32 * (normalizedProgress * normalizedProgress); - + return { left, top }; } @@ -270,7 +277,7 @@ function createSunshine(hour) { // Create the sun based on current hour const sunPos = calculateSunPosition(hour); - + const sun = document.createElement('div'); sun.className = 'rpg-weather-particle rpg-clear-sun'; sun.style.left = `${sunPos.left}vw`; @@ -572,9 +579,9 @@ function calculateMoonPosition(hour) { // Nighttime is roughly 8 PM to 5 AM (20-5) // Map hour to position along an arc // 8 PM = far left, low | midnight = center-left, high | 5 AM = far right, low - + if (hour === null) hour = 0; // Default to midnight if unknown - + // Normalize night hours to 0-1 range // 20 (8 PM) = 0, 0 (midnight) = ~0.44, 5 (5 AM) = 1 let progress; @@ -585,16 +592,16 @@ function calculateMoonPosition(hour) { // Midnight to 5 AM: 0-5 maps to 0.44-1 progress = (hour + 4) / 9; } - + // Horizontal position: 10% to 80% (left to right) const left = 10 + progress * 70; - + // Vertical position: parabolic arc (high at ~2 AM, low at dusk/dawn) // Peak should be around progress 0.67 (~2 AM) const peakProgress = 0.5; const normalizedProgress = (progress - peakProgress) * 2; // -1 to 1 const top = 8 + 25 * (normalizedProgress * normalizedProgress); - + return { left, top }; } @@ -607,7 +614,7 @@ function updateCelestialPosition(hour) { // Update sun position if it exists const sun = weatherContainer.querySelector('.rpg-clear-sun'); const sunGlow = weatherContainer.querySelector('.rpg-clear-sun-glow'); - + if (sun && sunGlow) { const sunPos = calculateSunPosition(hour); sun.style.left = `${sunPos.left}vw`; @@ -620,7 +627,7 @@ function updateCelestialPosition(hour) { // Update moon position if it exists const moon = weatherContainer.querySelector('.rpg-night-moon'); const moonGlow = weatherContainer.querySelector('.rpg-night-moon-glow'); - + if (moon && moonGlow) { const moonPos = calculateMoonPosition(hour); moon.style.left = `${moonPos.left}vw`; 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 */