-
`;
// Render custom fields dynamically
@@ -1060,11 +1062,25 @@ export function updateCharacterField(characterName, field, value) {
// Check if it's a character stat
const isStatField = enabledCharStats.findIndex(s => s.name === field) !== -1;
if (isStatField) {
- if (!char.stats) char.stats = {};
let numValue = parseInt(value.replace('%', '').trim());
if (isNaN(numValue)) numValue = 0;
numValue = Math.max(0, Math.min(100, numValue));
- char.stats[field] = numValue;
+
+ // Handle both array format (from LLM) and object format
+ if (Array.isArray(char.stats)) {
+ // Array format: [{name: "Health", value: 80}]
+ const statIndex = char.stats.findIndex(s => s.name === field);
+ if (statIndex !== -1) {
+ char.stats[statIndex].value = numValue;
+ } else {
+ // Stat not found in array - add it
+ char.stats.push({ name: field, value: numValue });
+ }
+ } else {
+ // Object format: {Health: 80} or undefined
+ if (!char.stats) char.stats = {};
+ char.stats[field] = numValue;
+ }
} else {
// It's a custom detail field - store in details object
if (!char.details) char.details = {};
diff --git a/src/systems/ui/mobile.js b/src/systems/ui/mobile.js
index 9a537d3..f018cf8 100644
--- a/src/systems/ui/mobile.js
+++ b/src/systems/ui/mobile.js
@@ -794,12 +794,17 @@ export function setupMobileKeyboardHandling() {
/**
* Handles focus on contenteditable fields to ensure they're visible when keyboard appears.
* Uses smooth scrolling to bring focused field into view with proper padding.
+ * Only applies on mobile viewports where virtual keyboard can obscure content.
*/
export function setupContentEditableScrolling() {
const $panel = $('#rpg-companion-panel');
// Use event delegation for all contenteditable fields
$panel.on('focusin', '[contenteditable="true"]', function(e) {
+ // Only apply scrolling behavior on mobile (where virtual keyboard appears)
+ const isMobile = window.innerWidth <= 1000;
+ if (!isMobile) return;
+
const $field = $(this);
// Small delay to let keyboard animate in
diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js
index 0bef359..21e3139 100644
--- a/src/systems/ui/weatherEffects.js
+++ b/src/systems/ui/weatherEffects.js
@@ -107,7 +107,8 @@ function getCurrentTime() {
// Patterns for specific weather conditions (order matters - combined effects first)
// Grouped by languages for easy editing
-const WEATHER_PATTERNS_BY_LANGUAGE = {
+// EXPORTED: Used by jsonPromptHelpers.js to provide valid weather keywords to LLM
+export const WEATHER_PATTERNS_BY_LANGUAGE = {
en: [
{ id: "blizzard", patterns: [ "blizzard" ] }, // Snow + Wind
{ id: "storm", patterns: [ "storm", "thunder", "lightning" ] }, // Rain + Lightning
@@ -130,6 +131,63 @@ const WEATHER_PATTERNS_BY_LANGUAGE = {
],
}
+/**
+ * Get valid weather keywords for LLM prompt injection.
+ * Returns weather patterns for specified language or all languages.
+ * This ensures LLM generates responses that exactly match our expected patterns.
+ *
+ * @param {string} [language] - Language code (e.g., 'en', 'ru'). If not specified, returns all languages.
+ * @returns {Object} Object with weather type IDs as keys and arrays of valid keywords as values
+ * @example
+ * // Returns: { blizzard: ["blizzard"], storm: ["storm", "thunder", "lightning"], ... }
+ * getWeatherKeywordsForPrompt('en');
+ */
+export function getWeatherKeywordsForPrompt(language) {
+ const result = {};
+
+ // Get patterns for specified language or merge all languages
+ const languagesToProcess = language && WEATHER_PATTERNS_BY_LANGUAGE[language]
+ ? { [language]: WEATHER_PATTERNS_BY_LANGUAGE[language] }
+ : WEATHER_PATTERNS_BY_LANGUAGE;
+
+ for (const [lang, patterns] of Object.entries(languagesToProcess)) {
+ for (const { id, patterns: keywords } of patterns) {
+ if (!result[id]) {
+ result[id] = [];
+ }
+ // Add keywords, avoiding duplicates
+ for (const keyword of keywords) {
+ if (!result[id].includes(keyword)) {
+ result[id].push(keyword);
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Get weather keywords as a formatted string for LLM instructions.
+ * Provides a clear template showing valid weather forecast values.
+ *
+ * @param {string} [language] - Language code. If not specified, uses all available patterns.
+ * @returns {string} Formatted string for prompt injection
+ * @example
+ * // Returns: 'Valid forecast values: "blizzard", "storm", "thunder", "lightning", "wind", ...'
+ * getWeatherKeywordsAsPromptString('en');
+ */
+export function getWeatherKeywordsAsPromptString(language) {
+ const keywords = getWeatherKeywordsForPrompt(language);
+ const allKeywords = [];
+
+ for (const patterns of Object.values(keywords)) {
+ allKeywords.push(...patterns);
+ }
+
+ return `Valid forecast values (use one of these exactly): ${allKeywords.map(k => `"${k}"`).join(', ')}`;
+}
+
/**
* Parse weather text to determine effect type
*/
diff --git a/style.css b/style.css
index 4029010..68b01ef 100644
--- a/style.css
+++ b/style.css
@@ -2118,8 +2118,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
/* Present Characters - Character Cards */
.rpg-character-card {
display: flex;
- align-items: flex-start;
- gap: clamp(8px, 1vw, 12px);
+ flex-direction: column;
+ gap: clamp(6px, 0.8vh, 10px);
padding: clamp(6px, 1vh, 8px);
background: rgba(0, 0, 0, 0.3);
border-radius: clamp(4px, 0.5vh, 6px);
@@ -2157,6 +2157,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
border-color: var(--rpg-highlight);
}
+/* Header row with avatar and name */
+.rpg-character-header-row {
+ display: flex;
+ align-items: center;
+ gap: clamp(8px, 1vw, 12px);
+ width: 100%;
+}
+
/* Character avatar container with relationship badge */
.rpg-character-avatar {
position: relative;
@@ -2164,8 +2172,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
}
.rpg-character-avatar img {
- width: clamp(35px, 6vh, 45px);
- height: clamp(35px, 6vh, 45px);
+ width: clamp(30px, 5vh, 40px);
+ height: clamp(30px, 5vh, 40px);
border-radius: 50%;
border: 2px solid var(--rpg-highlight);
object-fit: cover;
@@ -2232,13 +2240,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
-/* Character info section */
+/* Character info section - now takes full width below header row */
.rpg-character-content {
- flex: 1;
- min-width: 0;
+ width: 100%;
display: flex;
flex-direction: column;
- gap: 0;
+ gap: clamp(3px, 0.5vh, 5px);
overflow: hidden;
}
@@ -2271,13 +2278,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
background: var(--rpg-highlight);
}
-/* Character header with emoji and name */
+/* Character header with emoji and name - now inside header row */
.rpg-character-header {
display: flex;
align-items: center;
gap: clamp(4px, 0.5vw, 6px);
flex-wrap: nowrap; /* Prevent wrapping */
- position: relative;
+ flex: 1;
+ min-width: 0;
}
.rpg-character-emoji {