From 7b4ebb8d762e04f6a0365befa9fc766a17de19aa Mon Sep 17 00:00:00 2001 From: IDeathByte <41571062+IDeathByte@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:23:52 +0500 Subject: [PATCH 01/10] internalization weatherEffects.js update for russian support --- src/systems/ui/weatherEffects.js | 101 +++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 26 deletions(-) diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 6620321..e50443a 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -113,33 +113,82 @@ function parseWeatherType(weatherText) { 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'; - } + const weather_en = new Map([ + ["blizzard", "blizzard"], + ["storm", "storm,thunder,lightning"], + ["wind", "wind,breeze,gust,gale"], + ["snow", "snow,flurries"], + ["rain", "rain,drizzle,shower"], + ["mist", "mist,fog,haze"], + ["sunny", "sunny,clear,bright"], + ["none", "cloud,overcast,indoor,inside"], + ]); - return 'none'; + 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"; } /** From 0e988b201c5b414b9f1551980c9f09bdd0581399 Mon Sep 17 00:00:00 2001 From: IDeathByte <41571062+IDeathByte@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:38:26 +0500 Subject: [PATCH 02/10] Update weatherEffects.js syntax fix --- src/systems/ui/weatherEffects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index e50443a..673b37b 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -129,7 +129,7 @@ function parseWeatherType(weatherText) { ["storm", "гроза,буря,шторм"], [ "wind", - "ветер,ветренно,ветерок,бриз,легкий бриз,слегка ветренно,легкий ветер,шквол,буря", + "ветер,ветрено,ветерок,бриз,легкий бриз,слегка ветрено,легкий ветер,шквал,буря", ], ["snow", "снег,снегопад"], ["rain", "дождь,морось,ливень"], From f4324a5d19aca6fa82e23acafcfbd4e2f938f5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olari=20T=C5=A1ernobrovkin?= Date: Thu, 15 Jan 2026 20:30:48 +0200 Subject: [PATCH 03/10] Fix weather pattern matching regression --- src/systems/ui/weatherEffects.js | 136 +++++++++++-------------------- 1 file changed, 47 insertions(+), 89 deletions(-) diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 673b37b..0adab11 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -105,90 +105,48 @@ function getCurrentTime() { return null; } +// Patterns for specific weather conditions (order matters - combined effects first) +// Grouped by languages for easy editing +const weatherPatternsByLanguage = { + 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(); - const weather_en = new Map([ - ["blizzard", "blizzard"], - ["storm", "storm,thunder,lightning"], - ["wind", "wind,breeze,gust,gale"], - ["snow", "snow,flurries"], - ["rain", "rain,drizzle,shower"], - ["mist", "mist,fog,haze"], - ["sunny", "sunny,clear,bright"], - ["none", "cloud,overcast,indoor,inside"], - ]); + for (const language of Object.values(weatherPatternsByLanguage)) { + for (const { id, patterns } of language) { + if (patterns.some(p => text.includes(p))) { + return id; + } + } + } - 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"; } /** @@ -289,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 }; } @@ -319,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`; @@ -621,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; @@ -634,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 }; } @@ -656,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`; @@ -669,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`; From 5ddc380dac4b39a00da3ef9b3273dd89d0057948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olari=20T=C5=A1ernobrovkin?= Date: Sat, 17 Jan 2026 20:03:34 +0200 Subject: [PATCH 04/10] Make constant's variable name consistent with the codebase --- src/systems/ui/weatherEffects.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 0adab11..0bef359 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -107,7 +107,7 @@ function getCurrentTime() { // Patterns for specific weather conditions (order matters - combined effects first) // Grouped by languages for easy editing -const weatherPatternsByLanguage = { +const WEATHER_PATTERNS_BY_LANGUAGE = { en: [ { id: "blizzard", patterns: [ "blizzard" ] }, // Snow + Wind { id: "storm", patterns: [ "storm", "thunder", "lightning" ] }, // Rain + Lightning @@ -138,7 +138,7 @@ function parseWeatherType(weatherText) { const text = weatherText.toLowerCase(); - for (const language of Object.values(weatherPatternsByLanguage)) { + 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; From d0dd8950a658ce1be0f2461c52795ef09ab37a83 Mon Sep 17 00:00:00 2001 From: Spicy Marinara Date: Sat, 17 Jan 2026 21:13:28 +0100 Subject: [PATCH 05/10] Revert "internalization weatherEffects.js" --- src/systems/ui/weatherEffects.js | 99 ++++++++------------------------ 1 file changed, 25 insertions(+), 74 deletions(-) diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 673b37b..6620321 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -113,82 +113,33 @@ function parseWeatherType(weatherText) { const text = weatherText.toLowerCase(); - const weather_en = new Map([ - ["blizzard", "blizzard"], - ["storm", "storm,thunder,lightning"], - ["wind", "wind,breeze,gust,gale"], - ["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"; - } + 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'; + } - return "none"; + return 'none'; } /** From 03f21ef1efb76c948539667b5377b0c4ff0e918e Mon Sep 17 00:00:00 2001 From: Spicy Marinara Date: Sat, 17 Jan 2026 21:14:44 +0100 Subject: [PATCH 06/10] Revert "Revert "internalization weatherEffects.js"" --- src/systems/ui/weatherEffects.js | 101 +++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 26 deletions(-) diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 6620321..673b37b 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -113,33 +113,82 @@ function parseWeatherType(weatherText) { 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'; - } + const weather_en = new Map([ + ["blizzard", "blizzard"], + ["storm", "storm,thunder,lightning"], + ["wind", "wind,breeze,gust,gale"], + ["snow", "snow,flurries"], + ["rain", "rain,drizzle,shower"], + ["mist", "mist,fog,haze"], + ["sunny", "sunny,clear,bright"], + ["none", "cloud,overcast,indoor,inside"], + ]); - return 'none'; + 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"; } /** From 2a48c308082cdc7932a92c8a33a27702dad01926 Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Sat, 17 Jan 2026 21:34:53 +0100 Subject: [PATCH 07/10] Update sillytavern.js --- src/systems/integration/sillytavern.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/systems/integration/sillytavern.js b/src/systems/integration/sillytavern.js index 811829e..1398ec1 100644 --- a/src/systems/integration/sillytavern.js +++ b/src/systems/integration/sillytavern.js @@ -397,10 +397,11 @@ export function onMessageSwiped(messageIndex) { lastGeneratedData.infoBox = swipeData.infoBox || null; lastGeneratedData.characterThoughts = swipeData.characterThoughts || null; - // Parse user stats if available - if (swipeData.userStats) { - parseUserStats(swipeData.userStats); - } + // 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 { From f78c8a1b783f61fe6f84e07a56d3d2d6c0ce0a40 Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Sun, 18 Jan 2026 19:15:30 +0100 Subject: [PATCH 08/10] v3.6.2: Fix relationship field in context for manually added characters, add empty field placeholders and mobile support --- README.md | 7 +- manifest.json | 2 +- settings.html | 2 +- src/systems/generation/injector.js | 70 +++--- src/systems/generation/promptBuilder.js | 11 +- src/systems/integration/sillytavern.js | 8 +- src/systems/rendering/thoughts.js | 271 +++++++++++++++++++++--- style.css | 50 ++++- 8 files changed, 338 insertions(+), 83 deletions(-) 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 */ From e82918004e45dc1273a817c40c7995586a34e983 Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Tue, 20 Jan 2026 21:51:41 +0100 Subject: [PATCH 09/10] v3.6.3: Fix relationship field to use correct nested format (relationship.status) --- manifest.json | 2 +- src/systems/rendering/thoughts.js | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/manifest.json b/manifest.json index 60823b8..88c9cbe 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Marinara", - "version": "3.6.2", + "version": "3.6.3", "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" } diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index e60b40a..7576d5a 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -1023,18 +1023,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')) { From 6fc35e50a1ef523ded692dba3f73735c3d5d4fe5 Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Fri, 23 Jan 2026 09:17:40 +0100 Subject: [PATCH 10/10] Refactor inventory lock logic to use item names Updated inventory lock management and rendering to match items by name instead of index, improving reliability and consistency. Also adjusted quest rendering and parsing to handle locked quest objects with a value property. --- src/systems/generation/lockManager.js | 25 ++++++++++++++----------- src/systems/generation/parser.js | 2 ++ src/systems/rendering/inventory.js | 16 ++++++++-------- src/systems/rendering/quests.js | 7 +++++-- 4 files changed, 29 insertions(+), 21 deletions(-) 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/parser.js b/src/systems/generation/parser.js index cf75328..83b7400 100644 --- a/src/systems/generation/parser.js +++ b/src/systems/generation/parser.js @@ -617,6 +617,8 @@ export function parseUserStats(statsText) { if (!quest) return ''; if (typeof quest === 'string') return quest; if (typeof quest === 'object') { + // Check for locked format: {value, locked} + if (quest.value !== undefined) return String(quest.value); // v3 format: {title, description, status} return quest.title || quest.description || JSON.stringify(quest); } 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/quests.js b/src/systems/rendering/quests.js index a404a28..dceb783 100644 --- a/src/systems/rendering/quests.js +++ b/src/systems/rendering/quests.js @@ -212,8 +212,11 @@ export function renderQuests() { // Get current sub-tab from container or default to 'main' const activeSubTab = $questsContainer.data('active-subtab') || 'main'; - // Get quests data - const mainQuest = extensionSettings.quests.main || 'None'; + // Get quests data - extract value if it's a locked object + let mainQuest = extensionSettings.quests.main || 'None'; + if (typeof mainQuest === 'object' && mainQuest.value !== undefined) { + mainQuest = mainQuest.value; + } const optionalQuests = extensionSettings.quests.optional || []; // Build HTML