feat(inventory): add v2 parsing and generation support
Add full AI integration for inventory v2 format:
**Parsing (NEW: inventoryParser.js, 125 lines):**
- extractInventoryData() - Parse multi-line v2 format from AI responses
- Extracts "On Person: ..." section
- Extracts multiple "Stored - [Location]: ..." sections
- Extracts "Assets: ..." section
- Returns InventoryV2 object
- extractLegacyInventory() - Fallback parser for old v1 format
- extractInventory() - Main function that tries v2 first, falls back to v1
**Parsing Integration (parser.js):**
- Import extractInventory() from inventoryParser
- Replace old single-line regex with v2-aware extraction
- Use feature flag to switch between v1/v2 parsing modes
- Maintains backward compatibility with FEATURE_FLAGS.useNewInventory
**Generation (promptBuilder.js, 60 lines changed):**
- NEW: buildInventorySummary() - Converts v2 object to multi-line text
- Formats "On Person: ..." line
- Formats multiple "Stored - [Location]: ..." lines
- Formats "Assets: ..." line
- Handles legacy v1 string format for backward compat
- Update generateTrackerInstructions() with v2 format spec:
- Shows AI how to format inventory in multi-line v2 structure
- Includes note about multiple storage locations
- Falls back to v1 format when feature flag disabled
- Update generateContextualSummary() to use buildInventorySummary()
- Converts v2 inventory to readable context for separate mode
**Format Examples:**
AI Output (v2 format):
```
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)
```
Parsed Result:
```js
{
version: 2,
onPerson: "Sword (equipped), 3x Health Potions, Leather Armor",
stored: {
"Home": "Spare clothes, Tools, 50 gold coins",
"Bank": "Family heirloom, Important documents"
},
assets: "Motorcycle (garage), Downtown apartment (owned)"
}
```
Changes:
- NEW: src/systems/generation/inventoryParser.js (125 lines)
- MODIFIED: src/systems/generation/parser.js (+14 lines)
- MODIFIED: src/systems/generation/promptBuilder.js (+60 lines)
Part of inventory system v2 implementation
Dependencies: v2 types, migration utility, persistence integration
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
// 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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user