diff --git a/src/systems/generation/inventoryParser.js b/src/systems/generation/inventoryParser.js new file mode 100644 index 0000000..f56e28d --- /dev/null +++ b/src/systems/generation/inventoryParser.js @@ -0,0 +1,132 @@ +/** + * Inventory Parser Module + * Extracts v2 inventory data from AI-generated text + */ + +// Type imports +/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ + +/** + * Extracts inventory data from AI-generated stats text in v2 multi-line format. + * + * Expected format from AI: + * ``` + * On Person: Sword (equipped), 3x Health Potions, Leather Armor + * Stored - Home: Spare clothes, Tools, 50 gold coins + * Stored - Bank: Family heirloom, Important documents + * Assets: Motorcycle (garage), Downtown apartment (owned) + * ``` + * + * @param {string} statsText - Raw stats text from AI response + * @returns {InventoryV2|null} Parsed inventory v2 object or null if not found + */ +export function extractInventoryData(statsText) { + if (!statsText || typeof statsText !== 'string') { + return null; + } + + const result = { + version: 2, + onPerson: "None", + stored: {}, + assets: "None" + }; + + let foundAnyInventoryData = false; + + // Split into lines for parsing + const lines = statsText.split('\n'); + + for (const line of lines) { + const trimmed = line.trim(); + + // Parse "On Person: ..." line + const onPersonMatch = trimmed.match(/^On Person:\s*(.+)$/i); + if (onPersonMatch) { + result.onPerson = onPersonMatch[1].trim() || "None"; + foundAnyInventoryData = true; + continue; + } + + // Parse "Stored - [Location]: ..." lines + const storedMatch = trimmed.match(/^Stored\s*-\s*([^:]+):\s*(.+)$/i); + if (storedMatch) { + const locationName = storedMatch[1].trim(); + const items = storedMatch[2].trim(); + if (locationName && items) { + result.stored[locationName] = items; + foundAnyInventoryData = true; + } + continue; + } + + // Parse "Assets: ..." line + const assetsMatch = trimmed.match(/^Assets:\s*(.+)$/i); + if (assetsMatch) { + result.assets = assetsMatch[1].trim() || "None"; + foundAnyInventoryData = true; + continue; + } + } + + // Return null if we didn't find any inventory data + return foundAnyInventoryData ? result : null; +} + +/** + * Attempts to parse legacy v1 inventory format (single line). + * Fallback for old AI responses that haven't been updated to v2 format. + * + * Expected format: "Inventory: Sword, Shield, 3x Potions, Gold coins" + * + * @param {string} text - Text that may contain legacy inventory + * @returns {string|null} Legacy inventory string or null + */ +export function extractLegacyInventory(text) { + if (!text || typeof text !== 'string') { + return null; + } + + // Match old single-line format: "Inventory: ..." + const match = text.match(/Inventory:\s*(.+?)(?:\n|$)/i); + if (match && match[1]) { + const inventoryText = match[1].trim(); + // Return null for empty values like "None" or "" + if (!inventoryText || inventoryText.toLowerCase() === 'none') { + return null; + } + return inventoryText; + } + + return null; +} + +/** + * Main inventory extraction function that tries v2 format first, then falls back to v1. + * Converts v1 format to v2 automatically if found. + * + * @param {string} statsText - Raw stats text from AI response + * @returns {InventoryV2|null} Parsed inventory in v2 format or null + */ +export function extractInventory(statsText) { + // Try v2 format first + const v2Data = extractInventoryData(statsText); + if (v2Data) { + return v2Data; + } + + // Fallback to v1 format and convert to v2 + const v1Data = extractLegacyInventory(statsText); + if (v1Data) { + // Convert v1 string to v2 format (place in onPerson) + return { + version: 2, + onPerson: v1Data, + stored: {}, + assets: "None" + }; + } + + // No inventory data found + return null; +} diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js index af8713e..60c6b70 100644 --- a/src/systems/generation/parser.js +++ b/src/systems/generation/parser.js @@ -3,8 +3,9 @@ * Handles parsing of AI responses to extract tracker data */ -import { extensionSettings } from '../../core/state.js'; +import { extensionSettings, FEATURE_FLAGS } from '../../core/state.js'; import { saveSettings } from '../../core/persistence.js'; +import { extractInventory } from './inventoryParser.js'; /** * Parses the model response to extract the different data sections. @@ -91,8 +92,19 @@ export function parseUserStats(statsText) { } } - // Extract inventory - const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i); + // Extract inventory - use v2 parser if feature flag enabled, otherwise fallback to v1 + if (FEATURE_FLAGS.useNewInventory) { + const inventoryData = extractInventory(statsText); + if (inventoryData) { + extensionSettings.userStats.inventory = inventoryData; + } + } else { + // Legacy v1 parsing for backward compatibility + const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i); + if (inventoryMatch) { + extensionSettings.userStats.inventory = inventoryMatch[1].trim(); + } + } if (healthMatch) extensionSettings.userStats.health = parseInt(healthMatch[1]); if (satietyMatch) extensionSettings.userStats.satiety = parseInt(satietyMatch[1]); @@ -103,9 +115,6 @@ export function parseUserStats(statsText) { extensionSettings.userStats.mood = moodMatch[1].trim(); // Emoji extensionSettings.userStats.conditions = moodMatch[2].trim(); // Conditions } - if (inventoryMatch) { - extensionSettings.userStats.inventory = inventoryMatch[1].trim(); - } saveSettings(); } catch (error) { diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index 9e7a5c2..a24135b 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -5,7 +5,56 @@ import { getContext } from '../../../../../../extensions.js'; import { chat } from '../../../../../../../script.js'; -import { extensionSettings, committedTrackerData } from '../../core/state.js'; +import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js'; + +// Type imports +/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ + +/** + * Builds a formatted inventory summary for AI context injection. + * Converts v2 inventory structure to multi-line plaintext format. + * + * @param {InventoryV2|string} inventory - Current inventory (v2 or legacy string) + * @returns {string} Formatted inventory summary for prompt injection + * @example + * // v2 input: { onPerson: "Sword", stored: { Home: "Gold" }, assets: "Horse", version: 2 } + * // Returns: "On Person: Sword\nStored - Home: Gold\nAssets: Horse" + */ +export function buildInventorySummary(inventory) { + // Handle legacy v1 string format + if (typeof inventory === 'string') { + return inventory; + } + + // Handle v2 object format + if (inventory && typeof inventory === 'object' && inventory.version === 2) { + let summary = ''; + + // Add On Person section + if (inventory.onPerson && inventory.onPerson !== 'None') { + summary += `On Person: ${inventory.onPerson}\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)) { + if (items && items !== 'None') { + summary += `Stored - ${location}: ${items}\n`; + } + } + } + + // Add Assets section + if (inventory.assets && inventory.assets !== 'None') { + summary += `Assets: ${inventory.assets}`; + } + + return summary.trim(); + } + + // Fallback for unknown format + return 'None'; +} /** * Generates an example block showing current tracker states in markdown code blocks. @@ -64,7 +113,18 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon instructions += '- Hygiene: X%\n'; instructions += '- Arousal: X%\n'; instructions += '[Mood Emoji]: [Conditions (up to three traits)]\n'; - instructions += 'Inventory: [Clothing/Armor, Inventory Items (list of important items/none)]\n'; + + // Add inventory format based on feature flag + if (FEATURE_FLAGS.useNewInventory) { + instructions += 'On Person: [Items currently carried/worn, or "None"]\n'; + instructions += 'Stored - [Location Name]: [Items stored at this location]\n'; + instructions += '(Add multiple "Stored - [Location]:" lines as needed for different storage locations)\n'; + instructions += 'Assets: [Vehicles, property, major possessions, or "None"]\n'; + } else { + // Legacy v1 format + instructions += 'Inventory: [Clothing/Armor, Inventory Items (list of important items/none)]\n'; + } + instructions += '```\n\n'; } @@ -143,8 +203,13 @@ export function generateContextualSummary() { // console.log('[RPG Companion] Building stats summary with:', stats); summary += `${userName}'s Stats:\n`; summary += `Condition: Health ${stats.health}%, Satiety ${stats.satiety}%, Energy ${stats.energy}%, Hygiene ${stats.hygiene}%, Arousal ${stats.arousal}% | ${stats.mood} ${stats.conditions}\n`; - if (stats.inventory && stats.inventory !== 'None') { - summary += `Inventory: ${stats.inventory}\n`; + + // Add inventory summary using v2-aware builder + if (stats.inventory) { + const inventorySummary = buildInventorySummary(stats.inventory); + if (inventorySummary && inventorySummary !== 'None') { + summary += `Inventory:\n${inventorySummary}\n`; + } } // Include classic stats (attributes) and dice roll only if there was a dice roll if (extensionSettings.lastDiceRoll) {