f09c42ec6e
Fixes critical issue where manual edits (add location, add item, change stats, etc.) were invisible to AI in next generation, causing edits to be immediately overwritten. Root Cause: - Manual edits updated extensionSettings and lastGeneratedData - AI prompt builder used committedTrackerData (NOT extensionSettings) - Manual edits were never synced to committedTrackerData - Result: AI didn't see manual changes, overwrote them Solution - Sync to Both Data Stores: All manual edit points now update BOTH: 1. lastGeneratedData (for display) 2. committedTrackerData (for AI context) Files Modified: 1. **src/systems/interaction/inventoryActions.js** - updateLastGeneratedDataInventory() now sets committedTrackerData.userStats - Affects: add/remove items, add/remove locations 2. **src/systems/rendering/userStats.js** - All 3 edit handlers now set committedTrackerData.userStats - Affects: stat values (health, etc.), mood emoji, conditions - Also fixed: now uses buildInventorySummary() for proper v2 format 3. **src/systems/rendering/infoBox.js** - updateInfoBoxField() now sets committedTrackerData.infoBox - Affects: date, weather, temperature, time, location 4. **src/systems/rendering/thoughts.js** - updateCharacterField() now sets committedTrackerData.characterThoughts - Affects: character emoji, name, traits, thoughts, relationship Impact - Manual Edits Now Persist: Before: - Add location "Home" → Next generation → Location gone ❌ - Add item "Sword" → Next generation → Item gone ❌ - Change health to 25% → AI ignores it ❌ After: - Add location "Home" → Next generation → Location persists ✓ - Add item "Sword" → Next generation → Item included ✓ - Change health to 25% → AI acknowledges low health ✓ Works in Both Modes: - Together mode: AI sees manual edits in injected prompt ✓ - Separate mode: AI sees manual edits in context ✓ User Experience: - "I edited it, so it should stay" - now works as expected - AI builds on manual changes instead of overwriting them - Minimal overhead (just string copies) Fixes: Manual inventory/stats edits being overwritten by AI generation
608 lines
18 KiB
JavaScript
608 lines
18 KiB
JavaScript
/**
|
|
* Inventory Actions Module
|
|
* Handles all user interactions with the inventory v2 system
|
|
*/
|
|
|
|
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
|
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
|
|
import { buildInventorySummary } from '../generation/promptBuilder.js';
|
|
import { renderInventory } from '../rendering/inventory.js';
|
|
import { parseItems, serializeItems } from '../../utils/itemParser.js';
|
|
import { sanitizeLocationName, sanitizeItemName } from '../../utils/security.js';
|
|
|
|
// Type imports
|
|
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
|
|
|
/**
|
|
* Current active sub-tab for inventory UI
|
|
* @type {string}
|
|
*/
|
|
let currentActiveSubTab = 'onPerson';
|
|
|
|
/**
|
|
* Array of collapsed storage location names
|
|
* @type {string[]}
|
|
*/
|
|
let collapsedLocations = [];
|
|
|
|
/**
|
|
* Tracks which inline forms are currently open
|
|
* @type {Object}
|
|
*/
|
|
let openForms = {
|
|
addLocation: false,
|
|
addItemOnPerson: false,
|
|
addItemStored: {}, // { [locationName]: true/false }
|
|
addItemAssets: false
|
|
};
|
|
|
|
/**
|
|
* Updates lastGeneratedData.userStats AND committedTrackerData.userStats to include
|
|
* current inventory in text format.
|
|
* This ensures manual edits are immediately visible to AI in next generation.
|
|
*/
|
|
function updateLastGeneratedDataInventory() {
|
|
const stats = extensionSettings.userStats;
|
|
const inventorySummary = buildInventorySummary(stats.inventory);
|
|
|
|
// Rebuild the userStats text format
|
|
const statsText =
|
|
`Health: ${stats.health}%\n` +
|
|
`Satiety: ${stats.satiety}%\n` +
|
|
`Energy: ${stats.energy}%\n` +
|
|
`Hygiene: ${stats.hygiene}%\n` +
|
|
`Arousal: ${stats.arousal}%\n` +
|
|
`${stats.mood}: ${stats.conditions}\n` +
|
|
`${inventorySummary}`;
|
|
|
|
// Update BOTH lastGeneratedData AND committedTrackerData
|
|
// This makes manual edits immediately visible to AI
|
|
lastGeneratedData.userStats = statsText;
|
|
committedTrackerData.userStats = statsText;
|
|
}
|
|
|
|
/**
|
|
* Shows the inline form for adding a new item.
|
|
* @param {string} field - Field name ('onPerson', 'stored', 'assets')
|
|
* @param {string} [location] - Location name (required for 'stored' field)
|
|
*/
|
|
export function showAddItemForm(field, location) {
|
|
let formId;
|
|
let inputId;
|
|
|
|
if (field === 'stored') {
|
|
const locationId = location.replace(/\s+/g, '-');
|
|
formId = `rpg-add-item-form-stored-${locationId}`;
|
|
inputId = `.rpg-location-item-input[data-location="${location}"]`;
|
|
// Track in state
|
|
if (!openForms.addItemStored) openForms.addItemStored = {};
|
|
openForms.addItemStored[location] = true;
|
|
} else {
|
|
formId = `rpg-add-item-form-${field}`;
|
|
inputId = `#rpg-new-item-${field}`;
|
|
// Track in state
|
|
if (field === 'onPerson') {
|
|
openForms.addItemOnPerson = true;
|
|
} else if (field === 'assets') {
|
|
openForms.addItemAssets = true;
|
|
}
|
|
}
|
|
|
|
const form = $(`#${formId}`);
|
|
const input = $(inputId);
|
|
|
|
form.show();
|
|
input.val('').focus();
|
|
}
|
|
|
|
/**
|
|
* Hides the inline form for adding a new item.
|
|
* @param {string} field - Field name ('onPerson', 'stored', 'assets')
|
|
* @param {string} [location] - Location name (required for 'stored' field)
|
|
*/
|
|
export function hideAddItemForm(field, location) {
|
|
let formId;
|
|
let inputId;
|
|
|
|
if (field === 'stored') {
|
|
const locationId = location.replace(/\s+/g, '-');
|
|
formId = `rpg-add-item-form-stored-${locationId}`;
|
|
inputId = `.rpg-location-item-input[data-location="${location}"]`;
|
|
// Clear from state
|
|
if (openForms.addItemStored && openForms.addItemStored[location]) {
|
|
delete openForms.addItemStored[location];
|
|
}
|
|
} else {
|
|
formId = `rpg-add-item-form-${field}`;
|
|
inputId = `#rpg-new-item-${field}`;
|
|
// Clear from state
|
|
if (field === 'onPerson') {
|
|
openForms.addItemOnPerson = false;
|
|
} else if (field === 'assets') {
|
|
openForms.addItemAssets = false;
|
|
}
|
|
}
|
|
|
|
const form = $(`#${formId}`);
|
|
const input = $(inputId);
|
|
|
|
form.hide();
|
|
input.val('');
|
|
}
|
|
|
|
/**
|
|
* Adds a new item to the inventory.
|
|
* @param {string} field - Field name ('onPerson', 'stored', 'assets')
|
|
* @param {string} [location] - Location name (required for 'stored' field)
|
|
*/
|
|
export function saveAddItem(field, location) {
|
|
const inventory = extensionSettings.userStats.inventory;
|
|
let inputId;
|
|
|
|
if (field === 'stored') {
|
|
inputId = `.rpg-location-item-input[data-location="${location}"]`;
|
|
} else {
|
|
inputId = `#rpg-new-item-${field}`;
|
|
}
|
|
|
|
const input = $(inputId);
|
|
const rawItemName = input.val().trim();
|
|
|
|
if (!rawItemName) {
|
|
hideAddItemForm(field, location);
|
|
return;
|
|
}
|
|
|
|
// Security: Validate and sanitize item name
|
|
const itemName = sanitizeItemName(rawItemName);
|
|
if (!itemName) {
|
|
alert('Invalid item name.');
|
|
hideAddItemForm(field, location);
|
|
return;
|
|
}
|
|
|
|
// Get current items, add new one, serialize back
|
|
let currentString;
|
|
if (field === 'stored') {
|
|
currentString = inventory.stored[location] || 'None';
|
|
} else {
|
|
currentString = inventory[field] || 'None';
|
|
}
|
|
|
|
const items = parseItems(currentString);
|
|
items.push(itemName);
|
|
const newString = serializeItems(items);
|
|
|
|
// Save back to inventory
|
|
if (field === 'stored') {
|
|
inventory.stored[location] = newString;
|
|
} else {
|
|
inventory[field] = newString;
|
|
}
|
|
|
|
updateLastGeneratedDataInventory();
|
|
saveSettings();
|
|
saveChatData();
|
|
updateMessageSwipeData();
|
|
|
|
// Hide form and re-render
|
|
hideAddItemForm(field, location);
|
|
renderInventory();
|
|
}
|
|
|
|
/**
|
|
* Removes an item from the inventory.
|
|
* @param {string} field - Field name ('onPerson', 'stored', 'assets')
|
|
* @param {number} itemIndex - Index of item to remove
|
|
* @param {string} [location] - Location name (required for 'stored' field)
|
|
*/
|
|
export function removeItem(field, itemIndex, location) {
|
|
const inventory = extensionSettings.userStats.inventory;
|
|
|
|
// Get current items, remove the one at index, serialize back
|
|
let currentString;
|
|
if (field === 'stored') {
|
|
currentString = inventory.stored[location] || 'None';
|
|
} else {
|
|
currentString = inventory[field] || 'None';
|
|
}
|
|
|
|
const items = parseItems(currentString);
|
|
items.splice(itemIndex, 1); // Remove item at index
|
|
const newString = serializeItems(items);
|
|
|
|
// Save back to inventory
|
|
if (field === 'stored') {
|
|
inventory.stored[location] = newString;
|
|
} else {
|
|
inventory[field] = newString;
|
|
}
|
|
|
|
updateLastGeneratedDataInventory();
|
|
saveSettings();
|
|
saveChatData();
|
|
updateMessageSwipeData();
|
|
|
|
// Re-render
|
|
renderInventory();
|
|
}
|
|
|
|
/**
|
|
* Shows the inline form for adding a new storage location.
|
|
*/
|
|
export function showAddLocationForm() {
|
|
const form = $('#rpg-add-location-form');
|
|
const input = $('#rpg-new-location-name');
|
|
|
|
// Track in state
|
|
openForms.addLocation = true;
|
|
|
|
form.show();
|
|
input.val('').focus();
|
|
}
|
|
|
|
/**
|
|
* Hides the inline form for adding a new storage location.
|
|
*/
|
|
export function hideAddLocationForm() {
|
|
const form = $('#rpg-add-location-form');
|
|
const input = $('#rpg-new-location-name');
|
|
|
|
// Clear from state
|
|
openForms.addLocation = false;
|
|
|
|
form.hide();
|
|
input.val('');
|
|
}
|
|
|
|
/**
|
|
* Saves a new storage location from the inline form.
|
|
*/
|
|
export function saveAddLocation() {
|
|
const inventory = extensionSettings.userStats.inventory;
|
|
const input = $('#rpg-new-location-name');
|
|
const rawLocationName = input.val().trim();
|
|
|
|
if (!rawLocationName) {
|
|
hideAddLocationForm();
|
|
return;
|
|
}
|
|
|
|
// Security: Validate and sanitize location name
|
|
const locationName = sanitizeLocationName(rawLocationName);
|
|
if (!locationName) {
|
|
alert('Invalid location name. Avoid special names like "__proto__" or "constructor".');
|
|
hideAddLocationForm();
|
|
return;
|
|
}
|
|
|
|
// Check for duplicate
|
|
if (inventory.stored[locationName]) {
|
|
alert(`Storage location "${locationName}" already exists.`);
|
|
return;
|
|
}
|
|
|
|
// Create new location with default "None"
|
|
inventory.stored[locationName] = 'None';
|
|
|
|
updateLastGeneratedDataInventory();
|
|
saveSettings();
|
|
saveChatData();
|
|
updateMessageSwipeData();
|
|
|
|
// Hide form and re-render
|
|
hideAddLocationForm();
|
|
renderInventory();
|
|
}
|
|
|
|
/**
|
|
* Shows the inline confirmation UI for removing a storage location.
|
|
* @param {string} locationName - Name of location to remove
|
|
*/
|
|
export function showRemoveConfirmation(locationName) {
|
|
const confirmId = `rpg-remove-confirm-${locationName.replace(/\s+/g, '-')}`;
|
|
const confirmUI = $(`#${confirmId}`);
|
|
|
|
if (confirmUI.length > 0) {
|
|
confirmUI.show();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hides the inline confirmation UI for removing a storage location.
|
|
* @param {string} locationName - Name of location
|
|
*/
|
|
export function hideRemoveConfirmation(locationName) {
|
|
const confirmId = `rpg-remove-confirm-${locationName.replace(/\s+/g, '-')}`;
|
|
const confirmUI = $(`#${confirmId}`);
|
|
|
|
if (confirmUI.length > 0) {
|
|
confirmUI.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Confirms and removes a storage location from the inventory.
|
|
* @param {string} locationName - Name of location to remove
|
|
*/
|
|
export function confirmRemoveLocation(locationName) {
|
|
const inventory = extensionSettings.userStats.inventory;
|
|
delete inventory.stored[locationName];
|
|
|
|
// Remove from collapsed list if present
|
|
const index = collapsedLocations.indexOf(locationName);
|
|
if (index > -1) {
|
|
collapsedLocations.splice(index, 1);
|
|
}
|
|
|
|
updateLastGeneratedDataInventory();
|
|
saveSettings();
|
|
saveChatData();
|
|
updateMessageSwipeData();
|
|
|
|
// Re-render inventory UI
|
|
renderInventory();
|
|
}
|
|
|
|
/**
|
|
* Toggles the collapsed state of a storage location section.
|
|
* @param {string} locationName - Name of location to toggle
|
|
*/
|
|
export function toggleLocationCollapse(locationName) {
|
|
const index = collapsedLocations.indexOf(locationName);
|
|
|
|
if (index > -1) {
|
|
// Currently collapsed, expand it
|
|
collapsedLocations.splice(index, 1);
|
|
} else {
|
|
// Currently expanded, collapse it
|
|
collapsedLocations.push(locationName);
|
|
}
|
|
|
|
// Save collapsed state to settings
|
|
extensionSettings.collapsedInventoryLocations = collapsedLocations;
|
|
saveSettings();
|
|
|
|
// Re-render inventory UI
|
|
renderInventory();
|
|
}
|
|
|
|
/**
|
|
* Switches the active inventory sub-tab.
|
|
* @param {string} tabName - Name of the tab ('onPerson', 'stored', 'assets')
|
|
*/
|
|
export function switchInventoryTab(tabName) {
|
|
currentActiveSubTab = tabName;
|
|
|
|
// Re-render inventory UI
|
|
renderInventory();
|
|
}
|
|
|
|
/**
|
|
* Switches the view mode for an inventory section.
|
|
* @param {string} field - Field name ('onPerson', 'stored', 'assets')
|
|
* @param {string} mode - View mode ('list' or 'grid')
|
|
*/
|
|
export function switchViewMode(field, mode) {
|
|
// Ensure inventoryViewModes exists
|
|
if (!extensionSettings.inventoryViewModes) {
|
|
extensionSettings.inventoryViewModes = {
|
|
onPerson: 'list',
|
|
stored: 'list',
|
|
assets: 'list'
|
|
};
|
|
}
|
|
|
|
// Update view mode
|
|
extensionSettings.inventoryViewModes[field] = mode;
|
|
|
|
// Save settings
|
|
saveSettings();
|
|
|
|
// Re-render inventory UI
|
|
renderInventory();
|
|
}
|
|
|
|
/**
|
|
* Initializes all event listeners for inventory interactions.
|
|
* Uses event delegation to handle dynamically created elements.
|
|
*/
|
|
export function initInventoryEventListeners() {
|
|
// Load collapsed state from settings
|
|
if (extensionSettings.collapsedInventoryLocations) {
|
|
collapsedLocations = extensionSettings.collapsedInventoryLocations;
|
|
}
|
|
|
|
// Add item button - shows inline form
|
|
$(document).on('click', '.rpg-inventory-add-btn[data-action="add-item"]', function(e) {
|
|
e.preventDefault();
|
|
const field = $(this).data('field');
|
|
const location = $(this).data('location');
|
|
showAddItemForm(field, location);
|
|
});
|
|
|
|
// Add item inline form - save button
|
|
$(document).on('click', '.rpg-inline-btn[data-action="save-add-item"]', function(e) {
|
|
e.preventDefault();
|
|
const field = $(this).data('field');
|
|
const location = $(this).data('location');
|
|
saveAddItem(field, location);
|
|
});
|
|
|
|
// Add item inline form - cancel button
|
|
$(document).on('click', '.rpg-inline-btn[data-action="cancel-add-item"]', function(e) {
|
|
e.preventDefault();
|
|
const field = $(this).data('field');
|
|
const location = $(this).data('location');
|
|
hideAddItemForm(field, location);
|
|
});
|
|
|
|
// Add item inline form - enter key to save
|
|
$(document).on('keypress', '.rpg-inline-input', function(e) {
|
|
if (e.which === 13) { // Enter key
|
|
e.preventDefault();
|
|
const $btn = $(this).closest('.rpg-inline-form').find('[data-action="save-add-item"]');
|
|
if ($btn.length > 0) {
|
|
const field = $btn.data('field');
|
|
const location = $btn.data('location');
|
|
saveAddItem(field, location);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Remove item button
|
|
$(document).on('click', '.rpg-item-remove[data-action="remove-item"]', function(e) {
|
|
e.preventDefault();
|
|
const field = $(this).data('field');
|
|
const itemIndex = parseInt($(this).data('index'));
|
|
const location = $(this).data('location');
|
|
removeItem(field, itemIndex, location);
|
|
});
|
|
|
|
// Add location button - shows inline form
|
|
$(document).on('click', '.rpg-inventory-add-btn[data-action="add-location"]', function(e) {
|
|
e.preventDefault();
|
|
showAddLocationForm();
|
|
});
|
|
|
|
// Add location inline form - save button
|
|
$(document).on('click', '.rpg-inline-btn[data-action="save-add-location"]', function(e) {
|
|
e.preventDefault();
|
|
saveAddLocation();
|
|
});
|
|
|
|
// Add location inline form - cancel button
|
|
$(document).on('click', '.rpg-inline-btn[data-action="cancel-add-location"]', function(e) {
|
|
e.preventDefault();
|
|
hideAddLocationForm();
|
|
});
|
|
|
|
// Add location inline form - enter key to save
|
|
$(document).on('keypress', '#rpg-new-location-name', function(e) {
|
|
if (e.which === 13) { // Enter key
|
|
e.preventDefault();
|
|
saveAddLocation();
|
|
}
|
|
});
|
|
|
|
// Remove location button - shows inline confirmation
|
|
$(document).on('click', '.rpg-inventory-remove-btn[data-action="remove-location"]', function(e) {
|
|
e.preventDefault();
|
|
const location = $(this).data('location');
|
|
showRemoveConfirmation(location);
|
|
});
|
|
|
|
// Remove location inline confirmation - confirm button
|
|
$(document).on('click', '.rpg-inline-btn[data-action="confirm-remove-location"]', function(e) {
|
|
e.preventDefault();
|
|
const location = $(this).data('location');
|
|
confirmRemoveLocation(location);
|
|
});
|
|
|
|
// Remove location inline confirmation - cancel button
|
|
$(document).on('click', '.rpg-inline-btn[data-action="cancel-remove-location"]', function(e) {
|
|
e.preventDefault();
|
|
const location = $(this).data('location');
|
|
hideRemoveConfirmation(location);
|
|
});
|
|
|
|
// Collapse toggle buttons
|
|
$(document).on('click', '.rpg-storage-toggle', function(e) {
|
|
e.preventDefault();
|
|
const location = $(this).data('location');
|
|
toggleLocationCollapse(location);
|
|
});
|
|
|
|
// Sub-tab switching
|
|
$(document).on('click', '.rpg-inventory-subtab', function(e) {
|
|
e.preventDefault();
|
|
const tab = $(this).data('tab');
|
|
switchInventoryTab(tab);
|
|
});
|
|
|
|
// View mode switching
|
|
$(document).on('click', '.rpg-view-btn[data-action="switch-view"]', function(e) {
|
|
e.preventDefault();
|
|
const field = $(this).data('field');
|
|
const view = $(this).data('view');
|
|
switchViewMode(field, view);
|
|
});
|
|
|
|
console.log('[RPG Companion] Inventory event listeners initialized');
|
|
}
|
|
|
|
/**
|
|
* Gets the current inventory rendering options.
|
|
* @returns {Object} Options object with activeSubTab and collapsedLocations
|
|
*/
|
|
export function getInventoryRenderOptions() {
|
|
return {
|
|
activeSubTab: currentActiveSubTab,
|
|
collapsedLocations
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Restores the state of inline forms after re-rendering.
|
|
* This ensures forms that were open before re-render are shown again.
|
|
* Also cleans up orphaned form states for deleted locations (Bug #3 fix).
|
|
*/
|
|
export function restoreFormStates() {
|
|
// Restore add location form
|
|
if (openForms.addLocation) {
|
|
const form = $('#rpg-add-location-form');
|
|
const input = $('#rpg-new-location-name');
|
|
if (form.length > 0) {
|
|
form.show();
|
|
// Don't refocus to avoid disrupting user interaction
|
|
}
|
|
}
|
|
|
|
// Restore add item on person form
|
|
if (openForms.addItemOnPerson) {
|
|
const form = $('#rpg-add-item-form-onPerson');
|
|
const input = $('#rpg-new-item-onPerson');
|
|
if (form.length > 0) {
|
|
form.show();
|
|
}
|
|
}
|
|
|
|
// Restore add item assets form
|
|
if (openForms.addItemAssets) {
|
|
const form = $('#rpg-add-item-form-assets');
|
|
const input = $('#rpg-new-item-assets');
|
|
if (form.length > 0) {
|
|
form.show();
|
|
}
|
|
}
|
|
|
|
// Restore add item stored forms (for each location)
|
|
// Clean up orphaned states for deleted locations (Bug #3 fix)
|
|
if (openForms.addItemStored && typeof openForms.addItemStored === 'object') {
|
|
const inventory = extensionSettings.userStats.inventory;
|
|
const locationsToDelete = [];
|
|
|
|
for (const location in openForms.addItemStored) {
|
|
if (openForms.addItemStored[location]) {
|
|
// Check if location still exists in inventory
|
|
if (inventory?.stored && inventory.stored.hasOwnProperty(location)) {
|
|
// Location exists, restore form
|
|
const locationId = location.replace(/\s+/g, '-');
|
|
const form = $(`#rpg-add-item-form-stored-${locationId}`);
|
|
if (form.length > 0) {
|
|
form.show();
|
|
}
|
|
} else {
|
|
// Location was deleted, mark for cleanup
|
|
locationsToDelete.push(location);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up orphaned form states
|
|
for (const location of locationsToDelete) {
|
|
delete openForms.addItemStored[location];
|
|
}
|
|
}
|
|
}
|