diff --git a/index.js b/index.js index e945b96..8316b38 100644 --- a/index.js +++ b/index.js @@ -70,6 +70,7 @@ import { renderInventory } from './src/systems/rendering/inventory.js'; import { renderQuests } from './src/systems/rendering/quests.js'; import { renderMusicPlayer } from './src/systems/rendering/musicPlayer.js'; import { toggleSnowflakes, initSnowflakes } from './src/systems/ui/snowflakes.js'; +import { toggleDynamicWeather, initWeatherEffects, updateWeatherEffect } from './src/systems/ui/weatherEffects.js'; // Interaction modules import { initInventoryEventListeners } from './src/systems/interaction/inventoryActions.js'; @@ -397,6 +398,12 @@ async function initUI() { toggleSnowflakes(extensionSettings.enableSnowflakes); }); + $('#rpg-toggle-dynamic-weather').on('change', function() { + extensionSettings.enableDynamicWeather = $(this).prop('checked'); + saveSettings(); + toggleDynamicWeather(extensionSettings.enableDynamicWeather); + }); + $('#rpg-dismiss-promo').on('click', function() { extensionSettings.dismissedHolidayPromo = true; saveSettings(); @@ -561,6 +568,24 @@ async function initUI() { updateFeatureTogglesVisibility(); }); + $('#rpg-toggle-show-dynamic-weather-toggle').on('change', function() { + extensionSettings.showDynamicWeatherToggle = $(this).prop('checked'); + // Also disable the feature when hiding the toggle + if (!extensionSettings.showDynamicWeatherToggle) { + extensionSettings.enableDynamicWeather = false; + $('#rpg-toggle-dynamic-weather').prop('checked', false); + toggleDynamicWeather(false); + } + saveSettings(); + updateFeatureTogglesVisibility(); + }); + + $('#rpg-toggle-show-snowflakes-toggle').on('change', function() { + extensionSettings.showSnowflakesToggle = $(this).prop('checked'); + saveSettings(); + updateFeatureTogglesVisibility(); + }); + // Auto avatar generation settings $('#rpg-toggle-auto-avatars').on('change', function() { extensionSettings.autoGenerateAvatars = $(this).prop('checked'); @@ -757,11 +782,14 @@ async function initUI() { $('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt); $('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic); $('#rpg-toggle-snowflakes').prop('checked', extensionSettings.enableSnowflakes); + $('#rpg-toggle-dynamic-weather').prop('checked', extensionSettings.enableDynamicWeather); // Feature toggle visibility settings $('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true); $('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true); $('#rpg-toggle-show-snowflakes-toggle').prop('checked', extensionSettings.showSnowflakesToggle ?? true); + $('#rpg-toggle-show-dynamic-weather-toggle').prop('checked', extensionSettings.showDynamicWeatherToggle ?? true); + $('#rpg-toggle-show-snowflakes-toggle').prop('checked', extensionSettings.showSnowflakesToggle ?? true); // Hide holiday promo if previously dismissed if (extensionSettings.dismissedHolidayPromo) { @@ -825,6 +853,7 @@ async function initUI() { toggleAnimations(); updateFeatureTogglesVisibility(); togglePlotButtons(); // Initialize plot buttons and encounter button visibility + initWeatherEffects(); // Initialize dynamic weather effects // Setup mobile toggle button setupMobileToggle(); @@ -865,6 +894,12 @@ async function initUI() { // Initialize Lorebook Limiter initLorebookLimiter(); + + // Expose weather effect functions globally for cross-module access + if (!window.RPGCompanion) { + window.RPGCompanion = {}; + } + window.RPGCompanion.updateWeatherEffect = updateWeatherEffect; } diff --git a/manifest.json b/manifest.json index 69552ea..8dc7765 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Marysia", - "version": "2.0.0", + "version": "2.1.0", "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" } diff --git a/settings.html b/settings.html index 198a4b6..8f4aa18 100644 --- a/settings.html +++ b/settings.html @@ -30,7 +30,7 @@
- RPG Companion v2.0 + RPG Companion v2.1
diff --git a/src/core/persistence.js b/src/core/persistence.js index 33a645c..120e933 100644 --- a/src/core/persistence.js +++ b/src/core/persistence.js @@ -78,6 +78,24 @@ export function loadSettings() { } updateExtensionSettings(savedSettings); + + // Perform settings migrations based on version + const currentVersion = extensionSettings.settingsVersion || 1; + let settingsChanged = false; + + // Migration to version 2: Enable dynamic weather for existing users + if (currentVersion < 2) { + console.log('[RPG Companion] Migrating settings to version 2 (enabling dynamic weather)'); + extensionSettings.enableDynamicWeather = true; + extensionSettings.settingsVersion = 2; + settingsChanged = true; + } + + // Save migrated settings + if (settingsChanged) { + saveSettings(); + } + // console.log('[RPG Companion] Settings loaded:', extensionSettings); } else { // console.log('[RPG Companion] No saved settings found, using defaults'); diff --git a/src/core/state.js b/src/core/state.js index d57b174..c8b58d1 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -10,6 +10,7 @@ * Extension settings - persisted to SillyTavern settings */ export let extensionSettings = { + settingsVersion: 2, // Version number for settings migrations enabled: true, autoUpdate: true, updateDepth: 4, // How many messages to include in the context @@ -27,10 +28,12 @@ export let extensionSettings = { enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs) customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default) enableSnowflakes: false, // Enable festive snowflakes effect + enableDynamicWeather: true, // Enable dynamic weather effects based on Info Box weather field (v2: enabled by default) dismissedHolidayPromo: false, // User dismissed the holiday promotion banner showHtmlToggle: true, // Show Immersive HTML toggle in main panel showSpotifyToggle: true, // Show Spotify Music toggle in main panel showSnowflakesToggle: true, // Show Snowflakes Effect toggle in main panel + showDynamicWeatherToggle: true, // Show Dynamic Weather Effects toggle in main panel skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility) enablePlotButtons: true, // Show plot progression buttons above chat input saveTrackerHistory: false, // Save tracker data in chat history for each message diff --git a/src/i18n/en.json b/src/i18n/en.json index cce66ce..09cecec 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -42,6 +42,7 @@ "template.settingsModal.display.showImmersiveHtmlToggle": "Show Immersive HTML", "template.settingsModal.display.showSpotifyMusicToggle": "Show Spotify Music", "template.settingsModal.display.showSnowflakesToggle": "Show Snowflakes Effect", + "template.settingsModal.display.showDynamicWeatherToggle": "Show Dynamic Weather Effects", "template.settingsModal.display.showPlotProgressionButtons": "Show Plot Progression Buttons", "template.settingsModal.display.showPlotProgressionButtonsNote": "Display buttons above chat input for plot progression prompts", "template.settingsModal.display.showDiceDisplay": "Show Dice Roll Display", @@ -90,6 +91,13 @@ "template.trackerEditorModal.buttons.reset": "Reset to Defaults", "template.trackerEditorModal.buttons.cancel": "Cancel", "template.trackerEditorModal.buttons.save": "Save & Apply", + "template.trackerEditorModal.buttons.export": "Export Preset", + "template.trackerEditorModal.buttons.import": "Import Preset", + "template.trackerEditorModal.messages.exportSuccess": "Tracker preset exported successfully!", + "template.trackerEditorModal.messages.exportError": "Failed to export tracker preset. Check console for details.", + "template.trackerEditorModal.messages.importSuccess": "Tracker preset imported successfully!", + "template.trackerEditorModal.messages.importError": "Failed to import tracker preset", + "template.trackerEditorModal.messages.importConfirm": "This will replace your current tracker configuration. Continue?", "template.trackerEditorModal.userStatsTab.customStatsTitle": "Custom Stats", "template.trackerEditorModal.userStatsTab.addCustomStatButton": "Add Custom Stat", "template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG Attributes", @@ -132,6 +140,7 @@ "template.mainPanel.immersiveHtml": "Immersive HTML", "template.mainPanel.spotifyMusic": "Spotify Music", "template.mainPanel.snowflakesEffect": "Snowflakes Effect", + "template.mainPanel.dynamicWeatherEffects": "Dynamic Weather", "template.mainPanel.refreshRpgInfo": "Refresh RPG Info", "template.mainPanel.updating": "Updating...", "template.mainPanel.editTrackersButton": "Edit Trackers", @@ -151,12 +160,17 @@ "infobox.recentEvents.title": "Recent Events", "infobox.recentEvents.addEventPlaceholder": "Add event...", "inventory.section.onPerson": "On Person", + "inventory.section.clothing": "Clothing", "inventory.section.stored": "Stored", "inventory.section.assets": "Assets", "inventory.onPerson.empty": "No items carried", "inventory.onPerson.title": "Items Currently Carried", "inventory.onPerson.addItemButton": "Add Item", "inventory.onPerson.addItemPlaceholder": "Enter item name...", + "inventory.clothing.empty": "Not wearing anything", + "inventory.clothing.title": "Clothing & Armor", + "inventory.clothing.addItemButton": "Add Clothing", + "inventory.clothing.addItemPlaceholder": "Enter clothing item...", "inventory.stored.title": "Storage Locations", "inventory.stored.addLocationButton": "Add Location", "inventory.stored.addLocationPlaceholder": "Enter location name...", diff --git a/src/i18n/zh-tw.json b/src/i18n/zh-tw.json index 205c105..80a3e14 100644 --- a/src/i18n/zh-tw.json +++ b/src/i18n/zh-tw.json @@ -38,8 +38,7 @@ "template.settingsModal.display.enableAnimationsNote": "屬性、內容更新和擲骰的動畫效果", "template.settingsModal.display.showImmersiveHtmlToggle": "顯示沉浸式 HTML", "template.settingsModal.display.showSpotifyMusicToggle": "顯示 Spotify 音樂", - "template.settingsModal.display.showSnowflakesToggle": "顯示雪花效果", - "template.settingsModal.display.showPlotProgressionButtons": "顯示劇情推進按鈕(QR)", + "template.settingsModal.display.showSnowflakesToggle": "顯示雪花效果", "template.settingsModal.display.showDynamicWeatherToggle": "顯示動態天氣效果", "template.settingsModal.display.showPlotProgressionButtons": "顯示劇情推進按鈕(QR)", "template.settingsModal.display.showPlotProgressionButtonsNote": "在聊天輸入框上方顯示劇情推進提示按鈕(QR)", "template.settingsModal.display.enableDebugMode": "Debug Mode", "template.settingsModal.display.enableDebugModeNote": "UI 面板中顯示日誌,對於故障排除很有用。", @@ -83,6 +82,13 @@ "template.trackerEditorModal.buttons.reset": "重置為預設值", "template.trackerEditorModal.buttons.cancel": "取消", "template.trackerEditorModal.buttons.save": "保存並應用", + "template.trackerEditorModal.buttons.export": "匯出預設", + "template.trackerEditorModal.buttons.import": "匯入預設", + "template.trackerEditorModal.messages.exportSuccess": "追蹤器預設匯出成功!", + "template.trackerEditorModal.messages.exportError": "匯出追蹤器預設失敗。請檢查控制台以獲取詳細資訊。", + "template.trackerEditorModal.messages.importSuccess": "追蹤器預設匯入成功!", + "template.trackerEditorModal.messages.importError": "匯入追蹤器預設失敗", + "template.trackerEditorModal.messages.importConfirm": "這將替換您當前的追蹤器配置。繼續?", "template.trackerEditorModal.userStatsTab.customStatsTitle": "自訂屬性", "template.trackerEditorModal.userStatsTab.addCustomStatButton": "添加自訂屬性", "template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG 屬性", @@ -124,8 +130,7 @@ "template.mainPanel.clearLastRoll": "清除上次擲骰", "template.mainPanel.immersiveHtml": "沉浸式 HTML", "template.mainPanel.spotifyMusic": "Spotify 音樂", - "template.mainPanel.snowflakesEffect": "雪花效果", - "template.mainPanel.refreshRpgInfo": "刷新資訊", + "template.mainPanel.snowflakesEffect": "雪花效果", "template.mainPanel.dynamicWeatherEffects": "動態天氣", "template.mainPanel.refreshRpgInfo": "刷新資訊", "template.mainPanel.updating": "更新中...", "template.mainPanel.editTrackersButton": "追蹤器編輯", "template.mainPanel.settingsButton": "設定", @@ -144,12 +149,17 @@ "infobox.recentEvents.title": "近期事件", "infobox.recentEvents.addEventPlaceholder": "添加事件...", "inventory.section.onPerson": "隨身物品", + "inventory.section.clothing": "服裝", "inventory.section.stored": "倉庫物品", "inventory.section.assets": "資產", "inventory.onPerson.empty": "這裡什麼都沒有 (⚲□⚲)", "inventory.onPerson.title": "攜帶的物品", "inventory.onPerson.addItemButton": "添加物品", "inventory.onPerson.addItemPlaceholder": "輸入物品名稱...", + "inventory.clothing.empty": "未穿著任何服裝 (⚲□⚲)", + "inventory.clothing.title": "服裝與護甲", + "inventory.clothing.addItemButton": "添加服裝", + "inventory.clothing.addItemPlaceholder": "輸入服裝物品...", "inventory.stored.title": "倉庫位置", "inventory.stored.addLocationButton": "添加倉庫", "inventory.stored.addLocationPlaceholder": "輸入倉庫名稱...", diff --git a/src/systems/generation/inventoryParser.js b/src/systems/generation/inventoryParser.js index f56e28d..7fdc90d 100644 --- a/src/systems/generation/inventoryParser.js +++ b/src/systems/generation/inventoryParser.js @@ -28,6 +28,7 @@ export function extractInventoryData(statsText) { const result = { version: 2, onPerson: "None", + clothing: "None", stored: {}, assets: "None" }; @@ -48,6 +49,14 @@ export function extractInventoryData(statsText) { continue; } + // Parse "Clothing: ..." line + const clothingMatch = trimmed.match(/^Clothing:\s*(.+)$/i); + if (clothingMatch) { + result.clothing = clothingMatch[1].trim() || "None"; + foundAnyInventoryData = true; + continue; + } + // Parse "Stored - [Location]: ..." lines const storedMatch = trimmed.match(/^Stored\s*-\s*([^:]+):\s*(.+)$/i); if (storedMatch) { @@ -122,6 +131,7 @@ export function extractInventory(statsText) { return { version: 2, onPerson: v1Data, + clothing: "None", stored: {}, assets: "None" }; diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index 6fbe468..62cfad7 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -137,6 +137,11 @@ export function buildInventorySummary(inventory) { summary += `On Person: ${inventory.onPerson}\n`; } + // Add Clothing section + if (inventory.clothing && inventory.clothing !== 'None') { + summary += `Clothing: ${inventory.clothing}\n`; + } + // Add Stored sections for each location if (inventory.stored && Object.keys(inventory.stored).length > 0) { for (const [location, items] of Object.entries(inventory.stored)) { diff --git a/src/systems/rendering/infoBox.js b/src/systems/rendering/infoBox.js index d13a8ef..aff7cab 100644 --- a/src/systems/rendering/infoBox.js +++ b/src/systems/rendering/infoBox.js @@ -521,6 +521,11 @@ export function renderInfoBox() { if (extensionSettings.enableAnimations) { setTimeout(() => $infoBoxContainer.removeClass('rpg-content-updating'), 500); } + + // Update weather effect after rendering + if (window.RPGCompanion?.updateWeatherEffect) { + window.RPGCompanion.updateWeatherEffect(); + } } /** @@ -878,6 +883,12 @@ function updateRecentEvent(field, value) { saveChatData(); renderInfoBox(); + + // Update weather effect after rendering + if (window.RPGCompanion?.updateWeatherEffect) { + window.RPGCompanion.updateWeatherEffect(); + } + console.log(`[RPG Companion] Updated recent event ${field}:`, value); } } diff --git a/src/systems/rendering/inventory.js b/src/systems/rendering/inventory.js index 00b9e76..d19b461 100644 --- a/src/systems/rendering/inventory.js +++ b/src/systems/rendering/inventory.js @@ -24,8 +24,8 @@ export function getLocationId(locationName) { } /** - * Renders the inventory sub-tab navigation (On Person, Stored, Assets) - * @param {string} activeTab - Currently active sub-tab ('onPerson', 'stored', 'assets') + * Renders the inventory sub-tab navigation (On Person, Clothing, Stored, Assets) + * @param {string} activeTab - Currently active sub-tab ('onPerson', 'clothing', 'stored', 'assets') * @returns {string} HTML for sub-tab navigation */ export function renderInventorySubTabs(activeTab = 'onPerson') { @@ -34,6 +34,9 @@ export function renderInventorySubTabs(activeTab = 'onPerson') { + @@ -120,6 +123,82 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') { `; } +/** + * Renders the "Clothing" inventory view with list or grid display + * @param {string} clothingItems - Current clothing items (comma-separated string) + * @param {string} viewMode - View mode ('list' or 'grid') + * @returns {string} HTML for clothing view with items and add button + */ +export function renderClothingView(clothingItems, viewMode = 'list') { + const items = parseItems(clothingItems); + + let itemsHtml = ''; + if (items.length === 0) { + itemsHtml = `
${i18n.getTranslation('inventory.clothing.empty')}
`; + } else { + if (viewMode === 'grid') { + // Grid view: card-style items + itemsHtml = items.map((item, index) => ` +
+ + ${escapeHtml(item)} +
+ `).join(''); + } else { + // List view: full-width rows + itemsHtml = items.map((item, index) => ` +
+ ${escapeHtml(item)} + +
+ `).join(''); + } + } + + const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view'; + + return ` +
+
+

${i18n.getTranslation('inventory.clothing.title')}

+
+
+ + +
+ +
+
+
+ +
+ ${itemsHtml} +
+
+
+ `; +} + /** * Renders the "Stored" inventory view with collapsible locations and list/grid views * @param {Object.} stored - Stored items by location @@ -372,6 +451,7 @@ function generateInventoryHTML(inventory, options = {}) { v2Inventory = { version: 2, onPerson: 'None', + clothing: 'None', stored: {}, assets: 'None' }; @@ -381,6 +461,9 @@ function generateInventoryHTML(inventory, options = {}) { if (!v2Inventory.onPerson || typeof v2Inventory.onPerson !== 'string') { v2Inventory.onPerson = 'None'; } + if (!v2Inventory.clothing || typeof v2Inventory.clothing !== 'string') { + v2Inventory.clothing = 'None'; + } if (!v2Inventory.stored || typeof v2Inventory.stored !== 'object' || Array.isArray(v2Inventory.stored)) { v2Inventory.stored = {}; } @@ -397,6 +480,7 @@ function generateInventoryHTML(inventory, options = {}) { // Get view modes from settings (default to 'list') const viewModes = extensionSettings.inventoryViewModes || { onPerson: 'list', + clothing: 'list', stored: 'list', assets: 'list' }; @@ -406,6 +490,9 @@ function generateInventoryHTML(inventory, options = {}) { case 'onPerson': html += renderOnPersonView(v2Inventory.onPerson, viewModes.onPerson); break; + case 'clothing': + html += renderClothingView(v2Inventory.clothing, viewModes.clothing); + break; case 'stored': html += renderStoredView(v2Inventory.stored, collapsedLocations, viewModes.stored); break; diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index c9da8de..a36e7a3 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -311,6 +311,10 @@ export function renderThoughts() { debugLog('[RPG Thoughts] showCharacterThoughts setting:', extensionSettings.showCharacterThoughts); debugLog('[RPG Thoughts] Container exists:', !!$thoughtsContainer); + // Save scroll position before re-rendering + const scrollParent = $thoughtsContainer.closest('.rpg-content-box, .rpg-tab-content, .rpg-mobile-tab-content').filter(':visible').first(); + const savedScrollTop = scrollParent.length > 0 ? scrollParent.scrollTop() : 0; + // Add updating class for animation if (extensionSettings.enableAnimations) { $thoughtsContainer.addClass('rpg-content-updating'); @@ -474,8 +478,8 @@ export function renderThoughts() { const escapedDefaultName = escapeHtmlAttr(defaultName); // Determine right-click action text based on auto-generate setting - const defaultAvatarRightClickAction = extensionSettings.autoGenerateAvatars - ? 'Right-click to regenerate avatar' + const defaultAvatarRightClickAction = extensionSettings.autoGenerateAvatars + ? 'Right-click to regenerate avatar' : 'Right-click to delete avatar'; html += '
'; @@ -541,8 +545,8 @@ export function renderThoughts() { const isCurrentlyGenerating = isGenerating(char.name); // Determine right-click action text based on auto-generate setting - const avatarRightClickAction = extensionSettings.autoGenerateAvatars - ? 'Right-click to regenerate avatar' + const avatarRightClickAction = extensionSettings.autoGenerateAvatars + ? 'Right-click to regenerate avatar' : 'Right-click to delete avatar'; html += ` @@ -609,6 +613,11 @@ export function renderThoughts() { $thoughtsContainer.html(html); + // Restore scroll position to prevent UI jumping + if (scrollParent.length > 0 && savedScrollTop > 0) { + scrollParent.scrollTop(savedScrollTop); + } + debugLog('[RPG Thoughts] ✓ HTML rendered to container'); debugLog('[RPG Thoughts] ======================================================='); @@ -666,7 +675,7 @@ export function renderThoughts() { try { // Regenerate the avatar const newUrl = await regenerateAvatar(characterName); - + if (newUrl) { console.log(`[RPG Companion] Successfully regenerated avatar for: ${characterName}`); } else { diff --git a/src/systems/ui/desktop.js b/src/systems/ui/desktop.js index fb35e4f..cca40f7 100644 --- a/src/systems/ui/desktop.js +++ b/src/systems/ui/desktop.js @@ -4,6 +4,7 @@ */ import { i18n } from '../../core/i18n.js'; +import { extensionSettings } from '../../core/state.js'; /** * Sets up desktop tab navigation for organizing content. @@ -31,23 +32,40 @@ export function setupDesktopTabs() { return; } - // Create tab navigation - const $tabNav = $(` -
- + // Build tab navigation dynamically based on enabled settings + const tabButtons = []; + const hasInventory = $inventory.length > 0 && extensionSettings.showInventory; + const hasQuests = $quests.length > 0 && extensionSettings.showQuests; + + // Status tab (always present if any status content exists) + tabButtons.push(` + + `); + + // Inventory tab (only if enabled in settings) + if (hasInventory) { + tabButtons.push(` + `); + } + + // Quests tab (only if enabled in settings) + if (hasQuests) { + tabButtons.push(` -
- `); + `); + } + + const $tabNav = $(`
${tabButtons.join('')}
`); // Create tab content containers const $statusTab = $('
'); @@ -57,23 +75,25 @@ export function setupDesktopTabs() { // Move sections into their respective tabs (detach to preserve event handlers) if ($userStats.length > 0) { $statusTab.append($userStats.detach()); - $userStats.show(); + if (extensionSettings.showUserStats) $userStats.show(); } if ($infoBox.length > 0) { $statusTab.append($infoBox.detach()); - $infoBox.show(); + if (extensionSettings.showInfoBox) $infoBox.show(); } if ($thoughts.length > 0) { $statusTab.append($thoughts.detach()); - $thoughts.show(); + if (extensionSettings.showCharacterThoughts) $thoughts.show(); } if ($inventory.length > 0) { $inventoryTab.append($inventory.detach()); - $inventory.show(); + // Only show if enabled (will be part of tab structure) + if (hasInventory) $inventory.show(); } if ($quests.length > 0) { $questsTab.append($quests.detach()); - $quests.show(); + // Only show if enabled (will be part of tab structure) + if (hasQuests) $quests.show(); } // Hide dividers on desktop tabs (tabs separate content naturally) @@ -83,6 +103,9 @@ export function setupDesktopTabs() { const $tabsContainer = $('
'); $tabsContainer.append($tabNav); $tabsContainer.append($statusTab); + + // Always append inventory and quests tabs to preserve the elements + // But they'll only show if enabled (via tab button visibility) $tabsContainer.append($inventoryTab); $tabsContainer.append($questsTab); @@ -103,7 +126,7 @@ export function setupDesktopTabs() { $(`.rpg-tab-content[data-tab-content="${tabName}"]`).addClass('active'); }); - console.log('[RPG Desktop] Desktop tabs initialized'); + } /** @@ -145,12 +168,11 @@ export function removeDesktopTabs() { $contentBox.append($quests); } - // Show sections and dividers - $userStats.show(); - $infoBox.show(); - $thoughts.show(); - $inventory.show(); + // Show/hide sections based on settings (respect visibility settings) + if (extensionSettings.showUserStats) $userStats.show(); + if (extensionSettings.showInfoBox) $infoBox.show(); + if (extensionSettings.showCharacterThoughts) $thoughts.show(); + if (extensionSettings.showInventory) $inventory.show(); + if (extensionSettings.showQuests) $quests.show(); $('.rpg-divider').show(); - - console.log('[RPG Desktop] Desktop tabs removed'); } diff --git a/src/systems/ui/layout.js b/src/systems/ui/layout.js index 07ec973..2b7ad0a 100644 --- a/src/systems/ui/layout.js +++ b/src/systems/ui/layout.js @@ -11,9 +11,13 @@ import { $thoughtsContainer, $inventoryContainer, $questsContainer, - $musicPlayerContainer + $musicPlayerContainer, + setInventoryContainer, + setQuestsContainer } from '../../core/state.js'; import { i18n } from '../../core/i18n.js'; +import { setupMobileTabs, removeMobileTabs } from './mobile.js'; +import { setupDesktopTabs, removeDesktopTabs } from './desktop.js'; /** * Toggles the visibility of plot buttons based on settings. @@ -248,6 +252,10 @@ export function updatePanelVisibility() { * Updates the visibility of individual sections. */ export function updateSectionVisibility() { + // Refresh container references first (in case they were detached during tab operations) + setInventoryContainer($('#rpg-inventory')); + setQuestsContainer($('#rpg-quests')); + // Show/hide sections based on settings // Use explicit .show()/.hide() instead of .toggle() to ensure proper state on reload if (extensionSettings.showUserStats) { @@ -268,20 +276,17 @@ export function updateSectionVisibility() { $thoughtsContainer.hide(); } - if ($inventoryContainer) { - if (extensionSettings.showInventory) { - $inventoryContainer.show(); - } else { - $inventoryContainer.hide(); - } + // Use direct DOM selectors for inventory and quests to avoid stale references + if (extensionSettings.showInventory) { + $('#rpg-inventory').show(); + } else { + $('#rpg-inventory').hide(); } - if ($questsContainer) { - if (extensionSettings.showQuests) { - $questsContainer.show(); - } else { - $questsContainer.hide(); - } + if (extensionSettings.showQuests) { + $('#rpg-quests').show(); + } else { + $('#rpg-quests').hide(); } if ($musicPlayerContainer) { @@ -335,6 +340,37 @@ export function updateSectionVisibility() { } else { $('#rpg-divider-quests').hide(); } + + // Rebuild tabs to reflect visibility changes for inventory and quests + const isMobile = window.innerWidth <= 1000; + const hasMobileTabs = $('.rpg-mobile-container').length > 0; + const hasDesktopTabs = $('.rpg-tabs-nav').length > 0; + + // Only rebuild if tabs currently exist + if (hasMobileTabs || hasDesktopTabs) { + // Remove existing tabs + if (hasMobileTabs) { + removeMobileTabs(); + // Force remove any lingering mobile tab elements (but not the content sections!) + $('.rpg-mobile-container').remove(); + $('.rpg-mobile-tabs').remove(); + } else { + removeDesktopTabs(); + // Force remove any lingering desktop tab structure (but not the content sections!) + // The removeDesktopTabs() function already detached and restored the sections + } + + // Rebuild tabs immediately + if (isMobile) { + setupMobileTabs(); + } else { + setupDesktopTabs(); + } + + // Refresh container references + setInventoryContainer($('#rpg-inventory')); + setQuestsContainer($('#rpg-quests')); + } } /** diff --git a/src/systems/ui/mobile.js b/src/systems/ui/mobile.js index 2a8be3b..b80b0fc 100644 --- a/src/systems/ui/mobile.js +++ b/src/systems/ui/mobile.js @@ -579,8 +579,8 @@ export function setupMobileTabs() { const tabs = []; const hasStats = $userStats.length > 0; const hasInfo = $infoBox.length > 0 || $thoughts.length > 0; - const hasInventory = $inventory.length > 0; - const hasQuests = $quests.length > 0; + const hasInventory = $inventory.length > 0 && extensionSettings.showInventory; + const hasQuests = $quests.length > 0 && extensionSettings.showQuests; // Tab 1: Stats (User Stats only) if (hasStats) { @@ -650,12 +650,12 @@ export function setupMobileTabs() { const $mobileContainer = $('
'); $mobileContainer.append($tabNav); - // Only append tab content wrappers that have content - if (hasStats) $mobileContainer.append($statsTab); - if (hasInfo) $mobileContainer.append($infoTab); - if (hasInventory) $mobileContainer.append($inventoryTab); - if (hasQuests) $mobileContainer.append($questsTab); - if (hasInventory) $mobileContainer.append($inventoryTab); + // Always append all tab content wrappers to preserve elements + // Tab buttons control visibility + $mobileContainer.append($statsTab); + $mobileContainer.append($infoTab); + $mobileContainer.append($inventoryTab); + $mobileContainer.append($questsTab); // Insert mobile tab structure at the beginning of content box $contentBox.prepend($mobileContainer); @@ -712,11 +712,12 @@ export function removeMobileTabs() { $contentBox.prepend($userStats); } - // Show sections and dividers - $userStats.show(); - $infoBox.show(); - $thoughts.show(); - $inventory.show(); + // Show/hide sections based on settings (respect visibility settings) + if (extensionSettings.showUserStats) $userStats.show(); + if (extensionSettings.showInfoBox) $infoBox.show(); + if (extensionSettings.showCharacterThoughts) $thoughts.show(); + if (extensionSettings.showInventory) $inventory.show(); + if (extensionSettings.showQuests) $quests.show(); $('.rpg-divider').show(); } diff --git a/src/systems/ui/theme.js b/src/systems/ui/theme.js index dcb0e6e..b63cce0 100644 --- a/src/systems/ui/theme.js +++ b/src/systems/ui/theme.js @@ -84,16 +84,19 @@ export function updateFeatureTogglesVisibility() { const $htmlToggle = $('#rpg-html-toggle-wrapper'); const $spotifyToggle = $('#rpg-spotify-toggle-wrapper'); const $snowflakesToggle = $('#rpg-snowflakes-toggle-wrapper'); + const $dynamicWeatherToggle = $('#rpg-dynamic-weather-toggle-wrapper'); // Show/hide individual toggles $htmlToggle.toggle(extensionSettings.showHtmlToggle); $spotifyToggle.toggle(extensionSettings.showSpotifyToggle); $snowflakesToggle.toggle(extensionSettings.showSnowflakesToggle); + $dynamicWeatherToggle.toggle(extensionSettings.showDynamicWeatherToggle); // Hide entire row if all toggles are hidden const anyVisible = extensionSettings.showHtmlToggle || extensionSettings.showSpotifyToggle || - extensionSettings.showSnowflakesToggle; + extensionSettings.showSnowflakesToggle || + extensionSettings.showDynamicWeatherToggle; $featuresRow.toggle(anyVisible); } diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js index 0485c73..8713df9 100644 --- a/src/systems/ui/trackerEditor.js +++ b/src/systems/ui/trackerEditor.js @@ -68,6 +68,16 @@ export function initTrackerEditor() { $(document).on('click', '#rpg-open-tracker-editor', function() { openTrackerEditor(); }); + + // Export button + $(document).on('click', '#rpg-editor-export', function() { + exportTrackerPreset(); + }); + + // Import button + $(document).on('click', '#rpg-editor-import', function() { + importTrackerPreset(); + }); } /** @@ -188,6 +198,103 @@ function resetToDefaults() { }; } +/** + * Export current tracker configuration to a JSON file + */ +function exportTrackerPreset() { + try { + // Get the current tracker configuration + const config = extensionSettings.trackerConfig; + + // Create a preset object with metadata + const preset = { + name: 'Custom Tracker Preset', + version: '1.0', + exportDate: new Date().toISOString(), + trackerConfig: JSON.parse(JSON.stringify(config)) // Deep copy + }; + + // Convert to JSON + const jsonString = JSON.stringify(preset, null, 2); + const blob = new Blob([jsonString], { type: 'application/json' }); + + // Create download link + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + + // Generate filename with timestamp + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); + link.download = `rpg-tracker-preset-${timestamp}.json`; + + // Trigger download + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + console.log('[RPG Companion] Tracker preset exported successfully'); + toastr.success(i18n.getTranslation('template.trackerEditorModal.messages.exportSuccess') || 'Tracker preset exported successfully!'); + } catch (error) { + console.error('[RPG Companion] Error exporting tracker preset:', error); + toastr.error(i18n.getTranslation('template.trackerEditorModal.messages.exportError') || 'Failed to export tracker preset. Check console for details.'); + } +} + +/** + * Import tracker configuration from a JSON file + */ +function importTrackerPreset() { + // Create file input + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + + input.onchange = async (e) => { + const file = e.target.files[0]; + if (!file) return; + + try { + const text = await file.text(); + const data = JSON.parse(text); + + // Validate the imported data + if (!data.trackerConfig) { + throw new Error('Invalid preset file: missing trackerConfig'); + } + + // Validate required sections + if (!data.trackerConfig.userStats || !data.trackerConfig.infoBox || !data.trackerConfig.presentCharacters) { + throw new Error('Invalid preset file: missing required configuration sections'); + } + + // Ask for confirmation + const confirmMessage = i18n.getTranslation('template.trackerEditorModal.messages.importConfirm') || + 'This will replace your current tracker configuration. Continue?'; + + if (!confirm(confirmMessage)) { + return; + } + + // Apply the imported configuration + extensionSettings.trackerConfig = JSON.parse(JSON.stringify(data.trackerConfig)); // Deep copy + + // Re-render the editor UI + renderEditorUI(); + + console.log('[RPG Companion] Tracker preset imported successfully'); + toastr.success(i18n.getTranslation('template.trackerEditorModal.messages.importSuccess') || 'Tracker preset imported successfully!'); + } catch (error) { + console.error('[RPG Companion] Error importing tracker preset:', error); + toastr.error(i18n.getTranslation('template.trackerEditorModal.messages.importError') || + `Failed to import tracker preset: ${error.message}`); + } + }; + + // Trigger file selection + input.click(); +} + /** * Render the editor UI based on current config */ diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js new file mode 100644 index 0000000..7181396 --- /dev/null +++ b/src/systems/ui/weatherEffects.js @@ -0,0 +1,289 @@ +/** + * Dynamic Weather Effects Module + * Creates weather effects based on the Info Box weather field + */ + +import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js'; + +let weatherContainer = null; +let currentWeatherType = null; + +/** + * Parse weather text to determine effect type + */ +function parseWeatherType(weatherText) { + 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'; + } + + return 'none'; +} + +/** + * Extract weather from Info Box data + */ +function getCurrentWeather() { + const infoBoxData = lastGeneratedData.infoBox || committedTrackerData.infoBox || ''; + + // Parse the Info Box data to find Weather field + const lines = infoBoxData.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('Weather:')) { + return trimmed.substring('Weather:'.length).trim(); + } + } + + return null; +} + +/** + * Create snowflakes effect + */ +function createSnowflakes() { + const container = document.createElement('div'); + container.className = 'rpg-weather-particles'; + + // Create 50 snowflakes + for (let i = 0; i < 50; i++) { + const snowflake = document.createElement('div'); + snowflake.className = 'rpg-weather-particle rpg-snowflake'; + snowflake.textContent = '❄'; + snowflake.style.left = `${Math.random() * 100}%`; + snowflake.style.animationDelay = `${Math.random() * 10}s`; + snowflake.style.animationDuration = `${10 + Math.random() * 10}s`; + container.appendChild(snowflake); + } + + return container; +} + +/** + * Create rain effect + */ +function createRain() { + const container = document.createElement('div'); + container.className = 'rpg-weather-particles'; + + // Create 100 raindrops for heavier effect + for (let i = 0; i < 100; i++) { + const raindrop = document.createElement('div'); + raindrop.className = 'rpg-weather-particle rpg-raindrop'; + raindrop.style.left = `${Math.random() * 100}%`; + raindrop.style.animationDelay = `${Math.random() * 2}s`; + raindrop.style.animationDuration = `${0.5 + Math.random() * 0.5}s`; + container.appendChild(raindrop); + } + + return container; +} + +/** + * Create mist/fog effect + */ +function createMist() { + const container = document.createElement('div'); + container.className = 'rpg-weather-particles'; + + // Create 5 mist layers + for (let i = 0; i < 5; i++) { + const mist = document.createElement('div'); + mist.className = 'rpg-weather-particle rpg-mist'; + mist.style.animationDelay = `${i * 2}s`; + mist.style.animationDuration = `${15 + i * 2}s`; + mist.style.opacity = `${0.1 + Math.random() * 0.2}`; + container.appendChild(mist); + } + + return container; +} + +/** + * Create sunshine rays effect + */ +function createSunshine() { + const container = document.createElement('div'); + container.className = 'rpg-weather-particles'; + + // Create 8 sun rays + for (let i = 0; i < 8; i++) { + const ray = document.createElement('div'); + ray.className = 'rpg-weather-particle rpg-sunray'; + ray.style.left = `${10 + i * 12}%`; + ray.style.animationDelay = `${i * 0.5}s`; + ray.style.animationDuration = `${8 + Math.random() * 4}s`; + container.appendChild(ray); + } + + return container; +} + +/** + * Create lightning flash effect + */ +function createLightning() { + const container = document.createElement('div'); + container.className = 'rpg-weather-particles'; + + // Create lightning flash overlay + const flash = document.createElement('div'); + flash.className = 'rpg-weather-particle rpg-lightning'; + container.appendChild(flash); + + return container; +} + +/** + * Create wind effect + */ +function createWind() { + const container = document.createElement('div'); + container.className = 'rpg-weather-particles'; + + // Create 30 wind streaks + for (let i = 0; i < 30; i++) { + const streak = document.createElement('div'); + streak.className = 'rpg-weather-particle rpg-wind-streak'; + streak.style.top = `${Math.random() * 100}%`; + streak.style.animationDelay = `${Math.random() * 5}s`; + streak.style.animationDuration = `${1.5 + Math.random() * 1}s`; + container.appendChild(streak); + } + + return container; +} + +/** + * Remove current weather effect + */ +function removeWeatherEffect() { + if (weatherContainer) { + weatherContainer.remove(); + weatherContainer = null; + currentWeatherType = null; + } +} + +/** + * Update weather effect based on current weather + */ +export function updateWeatherEffect() { + // Check if dynamic weather is enabled + if (!extensionSettings.enableDynamicWeather) { + removeWeatherEffect(); + return; + } + + const weather = getCurrentWeather(); + const weatherType = parseWeatherType(weather); + + // Don't recreate if weather hasn't changed + if (weatherType === currentWeatherType) { + return; + } + + // Remove existing effect + removeWeatherEffect(); + + // Create new effect based on weather type + if (weatherType === 'none') { + return; // No effect + } + + currentWeatherType = weatherType; + + switch (weatherType) { + case 'snow': + weatherContainer = createSnowflakes(); + break; + case 'rain': + weatherContainer = createRain(); + break; + case 'mist': + weatherContainer = createMist(); + break; + case 'sunny': + weatherContainer = createSunshine(); + break; + case 'wind': + weatherContainer = createWind(); + break; + case 'storm': { + // Storm = Rain + Lightning (combined effects) + const rainContainer = createRain(); + const lightningContainer = createLightning(); + // Merge both containers + weatherContainer = document.createElement('div'); + weatherContainer.className = 'rpg-weather-particles'; + weatherContainer.appendChild(rainContainer); + weatherContainer.appendChild(lightningContainer); + break; + } + case 'blizzard': { + // Blizzard = Snow + Wind (combined effects) + const snowContainer = createSnowflakes(); + const windContainer = createWind(); + // Merge both containers + weatherContainer = document.createElement('div'); + weatherContainer.className = 'rpg-weather-particles'; + weatherContainer.appendChild(snowContainer); + weatherContainer.appendChild(windContainer); + break; + } + } + + if (weatherContainer) { + document.body.appendChild(weatherContainer); + } +} + +/** + * Initialize weather effects + */ +export function initWeatherEffects() { + updateWeatherEffect(); +} + +/** + * Toggle dynamic weather effects + */ +export function toggleDynamicWeather(enabled) { + if (enabled) { + updateWeatherEffect(); + } else { + removeWeatherEffect(); + } +} + +/** + * Clean up weather effects + */ +export function cleanupWeatherEffects() { + removeWeatherEffect(); +} diff --git a/src/types/inventory.js b/src/types/inventory.js index 3cfebcf..72d6778 100644 --- a/src/types/inventory.js +++ b/src/types/inventory.js @@ -8,6 +8,7 @@ * @typedef {Object} InventoryV2 * @property {number} version - Schema version (always 2) * @property {string} onPerson - Items currently carried/worn by the character (plaintext list) + * @property {string} clothing - Clothing and armor currently worn (plaintext list) * @property {Object.} stored - Items stored at named locations (location name → plaintext list) * @property {string} assets - Character's vehicles, property, and major possessions (plaintext list) */ diff --git a/src/utils/migration.js b/src/utils/migration.js index b5b0f23..ca17e00 100644 --- a/src/utils/migration.js +++ b/src/utils/migration.js @@ -15,6 +15,7 @@ const DEFAULT_INVENTORY_V2 = { version: 2, onPerson: "None", + clothing: "None", stored: {}, assets: "None" }; @@ -29,6 +30,17 @@ const DEFAULT_INVENTORY_V2 = { export function migrateInventory(inventory) { // Case 1: Already v2 format (has version property and is an object) if (inventory && typeof inventory === 'object' && inventory.version === 2) { + // Check if clothing field exists (v2.1 upgrade) + if (!inventory.hasOwnProperty('clothing')) { + // console.log('[RPG Companion Migration] Upgrading v2 inventory to v2.1 (adding clothing field)'); + inventory.clothing = "None"; + return { + inventory: inventory, + migrated: true, + source: 'v2-upgrade' + }; + } + // console.log('[RPG Companion Migration] Inventory already v2, no migration needed'); return { inventory: inventory, @@ -66,6 +78,7 @@ export function migrateInventory(inventory) { inventory: { version: 2, onPerson: inventory, + clothing: "None", stored: {}, assets: "None" }, diff --git a/style.css b/style.css index cb9da62..e7a9d59 100644 --- a/style.css +++ b/style.css @@ -407,6 +407,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld { color: var(--rpg-highlight); text-shadow: 0 0 8px var(--rpg-highlight); letter-spacing: 0.031em; + border-left: none !important; + padding-left: 0 !important; +} + +.rpg-panel-header h3 i { + border-left: none !important; + padding-left: 0 !important; + color: var(--rpg-highlight); } .rpg-btn-icon { @@ -2531,8 +2539,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld { width: 100%; padding: 0.75em 1em; margin-bottom: 10px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border: 2px solid rgba(102, 126, 234, 0.5); + background: linear-gradient(135deg, var(--rpg-border, #4a7ba7) 0%, var(--rpg-highlight, #e94560) 100%); + border: 2px solid var(--rpg-border-transparent, rgba(74, 123, 167, 0.5)); border-radius: 0.5em; color: #ffffff; font-size: 14px; @@ -2546,10 +2554,10 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-memory-recollection-btn:hover { - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); - border-color: rgba(102, 126, 234, 0.8); + background: linear-gradient(135deg, var(--rpg-highlight, #e94560) 0%, var(--rpg-border, #4a7ba7) 100%); + border-color: var(--rpg-border-opaque, rgba(74, 123, 167, 0.8)); transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); + box-shadow: 0 4px 12px var(--rpg-border-transparent, rgba(74, 123, 167, 0.3)); } .rpg-memory-recollection-btn:active { @@ -2580,7 +2588,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* Modal Container */ .rpg-memory-modal { background: var(--SmartThemeBlurTintColor, #1a1a2e); - border: 2px solid var(--SmartThemeBorderColor, #667eea); + border: 2px solid var(--rpg-border, var(--SmartThemeBorderColor, #4a7ba7)); border-radius: 12px; max-width: 500px; width: 90%; @@ -2602,7 +2610,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* Modal Header */ .rpg-memory-modal-header { padding: 1.25em; - border-bottom: 1px solid var(--SmartThemeBorderColor, #667eea); + border-bottom: 1px solid var(--rpg-border, var(--SmartThemeBorderColor, #4a7ba7)); background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); } @@ -2625,10 +2633,10 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-memory-modal-info { - background: rgba(102, 126, 234, 0.1); + background: var(--rpg-border, rgba(102, 126, 234, 0.1)); padding: 1em; border-radius: 8px; - border-left: 4px solid #667eea; + border-left: 4px solid var(--rpg-border, var(--SmartThemeBorderColor, #4a7ba7)); margin-top: 1em; } @@ -2656,7 +2664,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { .rpg-memory-progress-fill { height: 100%; width: 0%; - background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(90deg, var(--rpg-border, #4a7ba7) 0%, var(--rpg-highlight, #e94560) 100%); transition: width 0.3s ease; display: flex; align-items: center; @@ -2680,7 +2688,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* Modal Footer */ .rpg-memory-modal-footer { padding: 1em 1.25em; - border-top: 1px solid var(--SmartThemeBorderColor, #667eea); + border-top: 1px solid var(--rpg-border, var(--SmartThemeBorderColor, #4a7ba7)); display: flex; gap: 0.75em; justify-content: flex-end; @@ -2709,15 +2717,15 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-memory-proceed { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(135deg, var(--rpg-border, #4a7ba7) 0%, var(--rpg-highlight, #e94560) 100%); color: white; - border: 2px solid rgba(102, 126, 234, 0.5); + border: 2px solid var(--rpg-border-transparent, rgba(74, 123, 167, 0.5)); } .rpg-memory-proceed:hover { - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); - border-color: rgba(102, 126, 234, 0.8); - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); + background: linear-gradient(135deg, var(--rpg-highlight, #e94560) 0%, var(--rpg-border, #4a7ba7) 100%); + border-color: var(--rpg-border-opaque, rgba(74, 123, 167, 0.8)); + box-shadow: 0 4px 12px var(--rpg-border-transparent, rgba(74, 123, 167, 0.3)); } /* ============================================ @@ -3741,8 +3749,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-editor-tab-content { - max-height: 60vh; - overflow-y: auto; + max-height: none; + overflow-y: visible; } .rpg-editor-section { @@ -4090,6 +4098,11 @@ body:has(.rpg-panel.rpg-position-left) #sheld { background: var(--rpg-accent); } +.rpg-footer-left { + display: flex; + gap: 0.5em; +} + .rpg-footer-right { display: flex; gap: 0.5em; @@ -4280,6 +4293,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld { display: flex; align-items: center; gap: var(--modal-gap); + border-left: none !important; + padding-left: 0 !important; +} + +.rpg-settings-popup-header h3 i { + border-left: none !important; + padding-left: 0 !important; + color: var(--rpg-highlight); } /* Close button - touch-friendly */ @@ -6775,12 +6796,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld { gap: 8px; padding: 8px 12px; margin-bottom: 12px; - background: linear-gradient(135deg, rgba(74, 158, 255, 0.15) 0%, rgba(74, 158, 255, 0.05) 100%); - border-left: 4px solid #4a9eff; + background: linear-gradient(135deg, var(--rpg-border-transparent, rgba(74, 158, 255, 0.15)) 0%, var(--rpg-border-transparent-fade, rgba(74, 158, 255, 0.05)) 100%); + border-left: 4px solid var(--rpg-border, #4a7ba7); border-radius: 4px; font-size: 13px; font-weight: 600; - color: #4a9eff; + color: var(--rpg-highlight, #4a9eff); animation: checkpoint-fade-in 0.3s ease-out; } @@ -6811,7 +6832,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { top: 0; bottom: 0; width: 3px; - background: linear-gradient(180deg, #4a9eff 0%, transparent 100%); + background: linear-gradient(180deg, var(--rpg-border, #4a7ba7) 0%, transparent 100%); opacity: 0.5; } @@ -8580,6 +8601,234 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { } } +/* ============================================ + DYNAMIC WEATHER EFFECTS + ============================================ */ + +/* Weather particles container */ +.rpg-weather-particles { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; + overflow: hidden; +} + +/* Base particle */ +.rpg-weather-particle { + position: absolute; +} + +/* Rain drops */ +.rpg-raindrop { + width: 2px; + height: 50px; + background: linear-gradient(to bottom, rgba(174, 194, 224, 0), rgba(174, 194, 224, 0.8)); + animation: rpg-rainfall linear infinite; + top: -50px; +} + +@keyframes rpg-rainfall { + 0% { + transform: translateY(0); + opacity: 0.8; + } + 100% { + transform: translateY(100vh); + opacity: 0.3; + } +} + +.rpg-raindrop:nth-child(2n) { + height: 40px; + animation-duration: 0.6s; +} + +.rpg-raindrop:nth-child(3n) { + height: 60px; + animation-duration: 0.8s; +} + +/* Mist/Fog layers */ +.rpg-mist { + width: 100%; + height: 40%; + background: radial-gradient(ellipse at center, rgba(200, 200, 220, 0.3) 0%, transparent 70%); + animation: rpg-mistFloat ease-in-out infinite; + top: 30%; +} + +@keyframes rpg-mistFloat { + 0%, 100% { + transform: translateX(-10%) scale(1); + opacity: 0.15; + } + 50% { + transform: translateX(10%) scale(1.1); + opacity: 0.25; + } +} + +.rpg-mist:nth-child(2) { + top: 10%; + background: radial-gradient(ellipse at center, rgba(180, 180, 200, 0.2) 0%, transparent 70%); +} + +.rpg-mist:nth-child(3) { + top: 50%; + background: radial-gradient(ellipse at center, rgba(220, 220, 240, 0.25) 0%, transparent 70%); +} + +/* Sunshine rays */ +.rpg-sunray { + width: 3px; + height: 100vh; + background: linear-gradient(to bottom, + rgba(255, 250, 200, 0) 0%, + rgba(255, 250, 200, 0.3) 20%, + rgba(255, 250, 200, 0.15) 50%, + rgba(255, 250, 200, 0) 100%); + transform-origin: top; + animation: rpg-sunrayShine ease-in-out infinite; + top: -20%; + filter: blur(2px); +} + +@keyframes rpg-sunrayShine { + 0%, 100% { + opacity: 0.2; + transform: translateY(0) scaleY(1); + } + 50% { + opacity: 0.4; + transform: translateY(5%) scaleY(1.05); + } +} + +.rpg-sunray:nth-child(2n) { + width: 4px; + animation-duration: 10s; +} + +.rpg-sunray:nth-child(3n) { + width: 2px; + animation-duration: 12s; +} + +/* Mobile optimizations */ +@media (max-width: 768px) { + .rpg-raindrop { + animation-duration: 1s !important; + } + + .rpg-mist { + animation-duration: 20s; + } + + .rpg-sunray { + animation-duration: 15s; + } +} + +/* Lightning flash effect */ +.rpg-lightning { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0); + animation: rpg-lightningFlash 6s ease-in-out infinite; + pointer-events: none; +} + +@keyframes rpg-lightningFlash { + 0%, 100% { + background: rgba(255, 255, 255, 0); + } + 10% { + background: rgba(200, 220, 255, 0.4); + } + 10.5% { + background: rgba(255, 255, 255, 0); + } + 11% { + background: rgba(220, 235, 255, 0.6); + } + 11.3% { + background: rgba(255, 255, 255, 0); + } + 45% { + background: rgba(255, 255, 255, 0); + } + 45.2% { + background: rgba(210, 225, 255, 0.5); + } + 45.5% { + background: rgba(255, 255, 255, 0); + } +} + +/* Wind streaks effect */ +.rpg-wind-streak { + position: absolute; + width: 80px; + height: 2px; + background: linear-gradient(to right, rgba(200, 200, 220, 0), rgba(200, 200, 220, 0.5), rgba(200, 200, 220, 0)); + animation: rpg-windBlow linear infinite; + left: -100px; + opacity: 0.6; + transform: skewX(-20deg); +} + +@keyframes rpg-windBlow { + 0% { + transform: translateX(0) skewX(-20deg); + opacity: 0; + } + 10% { + opacity: 0.6; + } + 90% { + opacity: 0.6; + } + 100% { + transform: translateX(calc(100vw + 100px)) skewX(-20deg); + opacity: 0; + } +} + +.rpg-wind-streak:nth-child(2n) { + width: 100px; + animation-duration: 2s; + opacity: 0.4; +} + +.rpg-wind-streak:nth-child(3n) { + width: 60px; + animation-duration: 1.8s; + opacity: 0.7; +} + +.rpg-wind-streak:nth-child(4n) { + height: 1px; + opacity: 0.5; +} + +/* Mobile optimizations for new effects */ +@media (max-width: 768px) { + .rpg-wind-streak { + animation-duration: 2.5s !important; + } + + .rpg-lightning { + animation-duration: 8s; + } +} + /* ============================================ HOLIDAY PROMOTION BANNER ============================================ */ diff --git a/template.html b/template.html index 8ff1c08..0d3b8ac 100644 --- a/template.html +++ b/template.html @@ -98,6 +98,15 @@ Snowflakes Effect
+ + +
+ +
@@ -293,6 +302,11 @@ Show Snowflakes Effect + +