diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js index 3a6d020..7fec28a 100644 --- a/src/systems/generation/parser.js +++ b/src/systems/generation/parser.js @@ -133,6 +133,88 @@ function debugLog(message, data = null) { } } +/** + * Extract structured skills data from stats text + * Parses format: + * Skills: + * CategoryName: + * - SkillName (Lv X) + * - SkillName (Lv X) + * Uncategorized: + * - SkillName (Lv X) + * + * @param {string} statsText - Stats section text containing skills + * @returns {Object|null} Structured skills data or null if not found + */ +function extractSkills(statsText) { + if (!statsText) return null; + + // Find the Skills section + const skillsMatch = statsText.match(/Skills:([\s\S]*?)(?=\n\n|On Person:|Stored|Assets:|Main Quest|Optional Quest|$)/i); + if (!skillsMatch) { + // Fallback: try simple format "Skills: skill1, skill2" + const simpleMatch = statsText.match(/Skills:\s*(.+)/i); + if (simpleMatch) { + const skillsText = simpleMatch[1].trim(); + if (skillsText && skillsText !== 'None') { + // Return as string for backward compatibility + return skillsText; + } + } + return null; + } + + const skillsSection = skillsMatch[1]; + const skillsData = { + version: 1, + categories: {}, + uncategorized: [] + }; + + // Split into lines and process + const lines = skillsSection.split('\n').map(line => line.trim()).filter(line => line); + + let currentCategory = null; + + for (const line of lines) { + // Check if this is a category header (ends with colon, no dash) + if (line.endsWith(':') && !line.startsWith('-')) { + currentCategory = line.slice(0, -1).trim(); + if (currentCategory !== 'Uncategorized' && !skillsData.categories[currentCategory]) { + skillsData.categories[currentCategory] = []; + } + continue; + } + + // Check if this is a skill line (starts with -, has level info) + const skillMatch = line.match(/^-\s*(.+?)\s*\(Lv\s*(\d+)\)/i); + if (skillMatch) { + const skillName = skillMatch[1].trim(); + const level = parseInt(skillMatch[2], 10) || 1; + + const skill = { + name: skillName, + level: level, + xp: 0, + maxXP: 100 + }; + + if (currentCategory === 'Uncategorized' || currentCategory === null) { + skillsData.uncategorized.push(skill); + } else if (currentCategory && skillsData.categories[currentCategory]) { + skillsData.categories[currentCategory].push(skill); + } + } + } + + // Return null if no skills were found + if (Object.keys(skillsData.categories).length === 0 && skillsData.uncategorized.length === 0) { + return null; + } + + return skillsData; +} + /** * Parses the model response to extract the different data sections. * Extracts tracker data from markdown code blocks in the AI response. @@ -351,10 +433,12 @@ export function parseUserStats(statsText) { // Parse skills section if enabled const skillsConfig = trackerConfig?.userStats?.skillsSection; if (skillsConfig?.enabled) { - const skillsMatch = statsText.match(/Skills:\s*(.+)/i); - if (skillsMatch) { - extensionSettings.userStats.skills = skillsMatch[1].trim(); - debugLog('[RPG Parser] Skills extracted:', skillsMatch[1].trim()); + const skillsData = extractSkills(statsText); + if (skillsData) { + extensionSettings.userStats.skills = skillsData; + debugLog('[RPG Parser] Skills extracted:', skillsData); + } else { + debugLog('[RPG Parser] Skills extraction failed or none found'); } } diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index 62be66c..a1dbf42 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -10,6 +10,53 @@ import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../co // Type imports /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ +/** + * Builds a formatted skills summary for AI context injection. + * Converts structured skills data to multi-line plaintext format organized by category. + * + * @param {Object|string} skills - Current skills (structured or legacy string) + * @returns {string} Formatted skills summary for prompt injection + * @example + * // Structured input: { version: 1, categories: { Combat: [{name: 'Swordsmanship', level: 5}] }, uncategorized: [] } + * // Returns: "Skills:\nCombat:\n- Swordsmanship (Lv 5)" + */ +export function buildSkillsSummary(skills) { + // Handle legacy string format + if (typeof skills === 'string') { + return `Skills: ${skills}`; + } + + // Handle structured format + if (skills && typeof skills === 'object' && skills.version) { + let summary = 'Skills:'; + const categories = skills.categories || {}; + const uncategorized = skills.uncategorized || []; + + // Add categorized skills + for (const [categoryName, skillsList] of Object.entries(categories)) { + if (skillsList && skillsList.length > 0) { + summary += `\n${categoryName}:`; + for (const skill of skillsList) { + summary += `\n- ${skill.name} (Lv ${skill.level})`; + } + } + } + + // Add uncategorized skills + if (uncategorized.length > 0) { + summary += '\nUncategorized:'; + for (const skill of uncategorized) { + summary += `\n- ${skill.name} (Lv ${skill.level})`; + } + } + + return summary; + } + + // Empty or invalid + return 'Skills: None'; +} + /** * Builds a formatted inventory summary for AI context injection. * Converts v2 inventory structure to multi-line plaintext format. @@ -166,9 +213,13 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon // Add skills section if enabled if (userStatsConfig?.skillsSection?.enabled) { - const skillFields = userStatsConfig.skillsSection.customFields || []; - const skillFieldsText = skillFields.map(f => `[${f}]`).join(', '); - instructions += `Skills: [${skillFieldsText || 'Skill1, Skill2, etc.'}]\n`; + instructions += `Skills:\n`; + instructions += `[Category Name]:\n`; + instructions += `- [Skill Name] (Lv [1-100])\n`; + instructions += `- [Another Skill] (Lv [1-100])\n`; + instructions += `Uncategorized:\n`; + instructions += `- [Uncategorized Skill] (Lv [1-100])\n`; + instructions += `(Organize skills by logical categories like Combat, Magic, Social, Crafting, etc. Include level as integer 1-100. Skills without a clear category go in Uncategorized.)\n`; } // Add inventory format based on feature flag