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:
Lucas 'Paperboy' Rose-Winters
2025-10-17 15:10:31 +11:00
parent 39ec36f105
commit b00bae905f
3 changed files with 216 additions and 10 deletions
+132
View File
@@ -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;
}