';
@@ -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
+
+
+