v2.1: Add dynamic weather effects, clothing inventory, and bug fixes

Features:
- Add dynamic weather effects system (snow, rain, mist, sunshine, storm, wind, blizzard)
- Add separate Clothing tab in inventory system
- Weather effects auto-update based on Info Box weather field
- Combined effects for storm (rain+lightning) and blizzard (snow+wind)

Improvements:
- Settings migration system for automatic feature enablement
- Weather effects positioned behind chat interface (z-index: 1)
- Dynamic weather enabled by default for new users

Bug Fixes:
- Fix tab visibility issues (disabled tabs now properly hide)
- Fix theme-aware borders (remove hardcoded blue colors)
- Fix double scrollbar in Edit Trackers window
- Fix scroll position jumping when editing Present Characters
- Fix dynamic weather toggle hiding issue

Technical:
- Update inventory schema to v2.1 with clothing field
- Add automatic migration for existing v2 inventories
- Update parsers and prompts to handle clothing separately
- Add translations (EN/ZH-TW) for new features
This commit is contained in:
Spicy_Marinara
2026-01-02 13:58:43 +01:00
parent ddd59d124e
commit 62ed7ffb18
22 changed files with 1035 additions and 88 deletions
+11
View File
@@ -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);
}
}
+89 -2
View File
@@ -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') {
<button class="rpg-inventory-subtab ${activeTab === 'onPerson' ? 'active' : ''}" data-tab="onPerson" data-i18n-key="inventory.section.onPerson">
${i18n.getTranslation('inventory.section.onPerson')}
</button>
<button class="rpg-inventory-subtab ${activeTab === 'clothing' ? 'active' : ''}" data-tab="clothing" data-i18n-key="inventory.section.clothing">
${i18n.getTranslation('inventory.section.clothing')}
</button>
<button class="rpg-inventory-subtab ${activeTab === 'stored' ? 'active' : ''}" data-tab="stored" data-i18n-key="inventory.section.stored">
${i18n.getTranslation('inventory.section.stored')}
</button>
@@ -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 = `<div class="rpg-inventory-empty" data-i18n-key="inventory.clothing.empty">${i18n.getTranslation('inventory.clothing.empty')}</div>`;
} else {
if (viewMode === 'grid') {
// Grid view: card-style items
itemsHtml = items.map((item, index) => `
<div class="rpg-item-card" data-field="clothing" data-index="${index}">
<button class="rpg-item-remove" data-action="remove-item" data-field="clothing" data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="clothing" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
</div>
`).join('');
} else {
// List view: full-width rows
itemsHtml = items.map((item, index) => `
<div class="rpg-item-row" data-field="clothing" data-index="${index}">
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="clothing" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
<button class="rpg-item-remove" data-action="remove-item" data-field="clothing" data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
</div>
`).join('');
}
}
const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view';
return `
<div class="rpg-inventory-section" data-section="clothing">
<div class="rpg-inventory-header">
<h4 data-i18n-key="inventory.clothing.title">${i18n.getTranslation('inventory.clothing.title')}</h4>
<div class="rpg-inventory-header-actions">
<div class="rpg-view-toggle">
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="clothing" data-view="list" title="${i18n.getTranslation('global.listView')}">
<i class="fa-solid fa-list"></i>
</button>
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="clothing" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
<i class="fa-solid fa-th"></i>
</button>
</div>
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="clothing" title="Add new item">
<i class="fa-solid fa-plus"></i> <span data-i18n-key="inventory.clothing.addItemButton">${i18n.getTranslation('inventory.clothing.addItemButton')}</span>
</button>
</div>
</div>
<div class="rpg-inventory-content">
<div class="rpg-inline-form" id="rpg-add-item-form-clothing" style="display: none;">
<input type="text" class="rpg-inline-input" id="rpg-new-item-clothing" placeholder="${i18n.getTranslation('inventory.clothing.addItemPlaceholder')}" data-i18n-placeholder-key="inventory.clothing.addItemPlaceholder" />
<div class="rpg-inline-buttons">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="clothing">
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
</button>
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="clothing">
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
</button>
</div>
</div>
<div class="rpg-item-list ${listViewClass}">
${itemsHtml}
</div>
</div>
</div>
`;
}
/**
* Renders the "Stored" inventory view with collapsible locations and list/grid views
* @param {Object.<string, string>} 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;
+14 -5
View File
@@ -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 += '<div class="rpg-thoughts-content">';
@@ -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 {