feat: rpg stats improvements
This commit is contained in:
@@ -235,15 +235,15 @@ export function onGenerationStarted(type, data) {
|
||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
} else if (extensionSettings.generationMode === 'separate') {
|
||||
// In SEPARATE mode, inject the contextual summary for main roleplay generation
|
||||
const contextSummary = generateContextualSummary();
|
||||
// In SEPARATE mode, inject the current state as JSON for main roleplay generation
|
||||
const currentStateJSON = generateContextualSummary();
|
||||
|
||||
if (contextSummary) {
|
||||
const wrappedContext = `\nHere is context information about the current scene, and what follows is the last message in the chat history:
|
||||
if (currentStateJSON) {
|
||||
const wrappedContext = `\nHere is {{user}}'s current state in JSON format. This is merely informative, it's not your job to update it:
|
||||
<context>
|
||||
${contextSummary}
|
||||
|
||||
Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses.
|
||||
\`\`\`json
|
||||
${currentStateJSON}
|
||||
\`\`\`
|
||||
</context>\n\n`;
|
||||
|
||||
// Inject context at depth 1 (before last user message) as SYSTEM
|
||||
@@ -251,7 +251,7 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
||||
if (!shouldSuppress) {
|
||||
setExtensionPrompt('rpg-companion-context', wrappedContext, extension_prompt_types.IN_CHAT, 1, false);
|
||||
}
|
||||
// console.log('[RPG Companion] Injected contextual summary for separate mode:', contextSummary);
|
||||
// console.log('[RPG Companion] Injected current state JSON for separate mode:', currentStateJSON);
|
||||
} else {
|
||||
// Clear if no data yet
|
||||
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
||||
|
||||
@@ -217,6 +217,43 @@ export function parseJSONTrackerData(jsonData) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse attributes (RPG attributes like STR, DEX, etc.)
|
||||
// Only parse if allowAIUpdateAttributes is enabled
|
||||
const allowAIUpdateAttributes = trackerConfig?.userStats?.allowAIUpdateAttributes !== false; // Default to true for backwards compatibility
|
||||
if (jsonData.attributes && typeof jsonData.attributes === 'object' && allowAIUpdateAttributes) {
|
||||
debugLog('[RPG Parser] Parsing attributes:', Object.keys(jsonData.attributes));
|
||||
const rpgAttributes = trackerConfig?.userStats?.rpgAttributes || [
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
];
|
||||
|
||||
for (const [attrName, value] of Object.entries(jsonData.attributes)) {
|
||||
// Find matching attribute in config (case-insensitive)
|
||||
const attrConfig = rpgAttributes.find(a =>
|
||||
a && a.name && a.name.toLowerCase() === attrName.toLowerCase()
|
||||
);
|
||||
if (attrConfig && typeof value === 'number') {
|
||||
// Store in classicStats using the attribute id
|
||||
extensionSettings.classicStats[attrConfig.id] = Math.max(1, value);
|
||||
debugLog(`[RPG Parser] Attribute ${attrConfig.name}: ${value}`);
|
||||
}
|
||||
}
|
||||
} else if (jsonData.attributes && !allowAIUpdateAttributes) {
|
||||
debugLog('[RPG Parser] Attributes found in response but allowAIUpdateAttributes is disabled - skipping update');
|
||||
}
|
||||
|
||||
// Parse level (only if allowAIUpdateAttributes is enabled)
|
||||
if (jsonData.level !== undefined && typeof jsonData.level === 'number' && allowAIUpdateAttributes) {
|
||||
extensionSettings.level = Math.max(1, jsonData.level);
|
||||
debugLog(`[RPG Parser] Level: ${extensionSettings.level}`);
|
||||
} else if (jsonData.level !== undefined && !allowAIUpdateAttributes) {
|
||||
debugLog('[RPG Parser] Level found in response but allowAIUpdateAttributes is disabled - skipping update');
|
||||
}
|
||||
|
||||
// Parse status
|
||||
if (jsonData.status) {
|
||||
if (jsonData.status.mood) {
|
||||
|
||||
@@ -242,6 +242,38 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
}
|
||||
}
|
||||
|
||||
// Attributes section (if RPG attributes are enabled and should be included)
|
||||
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes;
|
||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
||||
|
||||
if (showRPGAttributes && shouldSendAttributes) {
|
||||
const rpgAttributes = trackerConfig?.userStats?.rpgAttributes || [
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
];
|
||||
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
|
||||
|
||||
if (enabledAttributes.length > 0) {
|
||||
let attrsJson = ' "attributes": {\n';
|
||||
const attrParts = enabledAttributes.map(attr => {
|
||||
const value = extensionSettings.classicStats?.[attr.id] ?? 10;
|
||||
return ` "${attr.name}": ${value}`;
|
||||
});
|
||||
attrsJson += attrParts.join(',\n');
|
||||
attrsJson += '\n }';
|
||||
sections.push(attrsJson);
|
||||
|
||||
// Add level
|
||||
const currentLevel = extensionSettings.level ?? 1;
|
||||
sections.push(` "level": ${currentLevel}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Info Box section
|
||||
if (showInfoBox) {
|
||||
const widgets = trackerConfig?.infoBox?.widgets || {};
|
||||
@@ -362,6 +394,12 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
instructions += '- Output ONLY valid JSON inside the code fence\n';
|
||||
instructions += '- Use actual values, not placeholders like [Location]\n';
|
||||
instructions += '- Stats are percentages (0-100)\n';
|
||||
|
||||
if (showRPGAttributes && shouldSendAttributes) {
|
||||
instructions += '- Attributes are numeric values (typically 1-20, but can be higher)\n';
|
||||
instructions += '- Level is a numeric value (typically 1+, represents character progression)\n';
|
||||
}
|
||||
|
||||
instructions += '- Empty arrays [] for sections with no items\n';
|
||||
instructions += '- null for main quest if none active\n';
|
||||
|
||||
@@ -447,104 +485,175 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a formatted contextual summary for SEPARATE mode injection.
|
||||
* Includes the full tracker data in original format (without code fences and separators).
|
||||
* Generates the current tracker state as a JSON string for SEPARATE mode injection.
|
||||
* Uses COMMITTED data (not displayed data) for generation context.
|
||||
* Similar to how <previous> is formatted, but for the current state.
|
||||
*
|
||||
* @returns {string} Formatted contextual summary
|
||||
* @returns {string} JSON string of current state, or empty string if no data
|
||||
*/
|
||||
export function generateContextualSummary() {
|
||||
// Use COMMITTED data for generation context, not displayed data
|
||||
const userName = getContext().name1;
|
||||
// Build current state as JSON (similar to previousState in generateRPGPromptText)
|
||||
const currentState = {};
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
let summary = '';
|
||||
|
||||
// Helper function to clean tracker data (remove code fences and separator lines)
|
||||
const cleanTrackerData = (data) => {
|
||||
if (!data) return '';
|
||||
return data
|
||||
.split('\n')
|
||||
.filter(line => {
|
||||
const trimmed = line.trim();
|
||||
return trimmed &&
|
||||
!trimmed.startsWith('```') &&
|
||||
trimmed !== '---';
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
// Add User Stats tracker data if enabled
|
||||
if (extensionSettings.showUserStats && committedTrackerData.userStats) {
|
||||
const cleanedStats = cleanTrackerData(committedTrackerData.userStats);
|
||||
if (cleanedStats) {
|
||||
summary += cleanedStats + '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Add Info Box tracker data if enabled
|
||||
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
|
||||
const cleanedInfoBox = cleanTrackerData(committedTrackerData.infoBox);
|
||||
if (cleanedInfoBox) {
|
||||
summary += cleanedInfoBox + '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Add Present Characters tracker data if enabled
|
||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
||||
const cleanedThoughts = cleanTrackerData(committedTrackerData.characterThoughts);
|
||||
if (cleanedThoughts) {
|
||||
summary += cleanedThoughts + '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Add inventory context if enabled (only if not already present in cleaned stats)
|
||||
if (extensionSettings.showInventory && extensionSettings.userStats?.inventory) {
|
||||
const inventorySummary = buildInventorySummary(extensionSettings.userStats.inventory);
|
||||
if (inventorySummary && inventorySummary !== 'None') {
|
||||
// Check if inventory is already in the summary (case-insensitive)
|
||||
const summaryLower = summary.toLowerCase();
|
||||
if (!summaryLower.includes('inventory:') && !summaryLower.includes('on person:')) {
|
||||
summary += inventorySummary + '\n\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add quests context if enabled (only if not already present in cleaned stats)
|
||||
if (extensionSettings.showQuests && extensionSettings.quests) {
|
||||
const summaryLower = summary.toLowerCase();
|
||||
// Only add if not already present
|
||||
if (!summaryLower.includes('main quest') && !summaryLower.includes('optional quest')) {
|
||||
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
|
||||
summary += `Main Quests: ${extensionSettings.quests.main}\n`;
|
||||
}
|
||||
if (extensionSettings.quests.optional && extensionSettings.quests.optional.length > 0) {
|
||||
const optionalQuests = extensionSettings.quests.optional.filter(q => q && q !== 'None').join(', ');
|
||||
if (optionalQuests) {
|
||||
summary += `Optional Quests: ${optionalQuests}\n`;
|
||||
const descriptions = {};
|
||||
|
||||
// Stats
|
||||
if (extensionSettings.showUserStats) {
|
||||
const customStats = trackerConfig?.userStats?.customStats?.filter(s => s?.enabled) || [];
|
||||
if (customStats.length > 0) {
|
||||
currentState.stats = {};
|
||||
descriptions.stats = {};
|
||||
for (const stat of customStats) {
|
||||
currentState.stats[stat.name] = extensionSettings.userStats[stat.id] ?? 100;
|
||||
if (stat.description) {
|
||||
descriptions.stats[stat.name] = stat.description;
|
||||
}
|
||||
}
|
||||
summary += '\n';
|
||||
}
|
||||
|
||||
// Status
|
||||
const statusConfig = trackerConfig?.userStats?.statusSection;
|
||||
if (statusConfig?.enabled) {
|
||||
currentState.status = {
|
||||
mood: extensionSettings.userStats.mood || '😐',
|
||||
fields: {}
|
||||
};
|
||||
const customFields = statusConfig.customFields || [];
|
||||
for (const field of customFields) {
|
||||
currentState.status.fields[field] = extensionSettings.userStats.conditions || 'None';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include attributes based on settings
|
||||
|
||||
// InfoBox
|
||||
if (extensionSettings.showInfoBox && extensionSettings.infoBoxData) {
|
||||
currentState.infoBox = extensionSettings.infoBoxData;
|
||||
}
|
||||
|
||||
// Characters - format to match schema
|
||||
if (extensionSettings.showCharacterThoughts && extensionSettings.charactersData?.length > 0) {
|
||||
// Ensure characters match the expected schema format
|
||||
currentState.characters = extensionSettings.charactersData.map(char => {
|
||||
const formatted = { name: char.name };
|
||||
if (char.relationship) formatted.relationship = char.relationship;
|
||||
if (char.emoji) formatted.emoji = char.emoji;
|
||||
if (char.fields && Object.keys(char.fields).length > 0) formatted.fields = char.fields;
|
||||
if (char.stats && Object.keys(char.stats).length > 0) formatted.stats = char.stats;
|
||||
if (char.thoughts) formatted.thoughts = char.thoughts;
|
||||
return formatted;
|
||||
});
|
||||
|
||||
// Add character field descriptions
|
||||
const charConfig = trackerConfig?.presentCharacters;
|
||||
if (charConfig?.customFields?.length > 0) {
|
||||
descriptions.characterFields = {};
|
||||
for (const field of charConfig.customFields) {
|
||||
if (field.enabled && field.description) {
|
||||
descriptions.characterFields[field.name] = field.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add character stats descriptions
|
||||
const charStatsConfig = charConfig?.characterStats;
|
||||
if (charStatsConfig?.enabled && charStatsConfig?.customStats?.length > 0) {
|
||||
if (!descriptions.characterStats) {
|
||||
descriptions.characterStats = {};
|
||||
}
|
||||
for (const stat of charStatsConfig.customStats) {
|
||||
if (stat.enabled && stat.description) {
|
||||
descriptions.characterStats[stat.name] = stat.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inventory - format to match schema (use "items" for simplified mode)
|
||||
if (extensionSettings.showInventory && extensionSettings.inventoryV3) {
|
||||
const inv = extensionSettings.inventoryV3;
|
||||
if (extensionSettings.useSimplifiedInventory) {
|
||||
// Simplified mode uses "items" key
|
||||
const items = inv.simplified || inv.onPerson || [];
|
||||
if (items.length > 0) {
|
||||
currentState.inventory = { items };
|
||||
}
|
||||
} else {
|
||||
// Full categorized mode
|
||||
if (inv.onPerson?.length > 0 || Object.keys(inv.stored || {}).length > 0 || inv.assets?.length > 0) {
|
||||
currentState.inventory = {
|
||||
onPerson: inv.onPerson || [],
|
||||
stored: inv.stored || {},
|
||||
assets: inv.assets || []
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (extensionSettings.showSkills && extensionSettings.skillsV2) {
|
||||
currentState.skills = extensionSettings.skillsV2;
|
||||
|
||||
// Add skill category descriptions
|
||||
const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
const categoriesWithDesc = skillCategories.filter(cat =>
|
||||
typeof cat === 'object' && cat.enabled !== false && cat.description
|
||||
);
|
||||
if (categoriesWithDesc.length > 0) {
|
||||
descriptions.skillCategories = {};
|
||||
for (const cat of categoriesWithDesc) {
|
||||
descriptions.skillCategories[cat.name] = cat.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quests
|
||||
if (extensionSettings.showQuests && extensionSettings.questsV2) {
|
||||
currentState.quests = extensionSettings.questsV2;
|
||||
}
|
||||
|
||||
// Attributes and level (if RPG attributes are enabled and should be included)
|
||||
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes;
|
||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
||||
|
||||
if (shouldSendAttributes) {
|
||||
const attributesString = buildAttributesString();
|
||||
summary += `${userName}'s attributes: ${attributesString}\n`;
|
||||
|
||||
// Add dice roll context if there was one
|
||||
if (extensionSettings.lastDiceRoll) {
|
||||
const roll = extensionSettings.lastDiceRoll;
|
||||
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
|
||||
} else {
|
||||
summary += `\n`;
|
||||
|
||||
if (showRPGAttributes && shouldSendAttributes) {
|
||||
const rpgAttributes = trackerConfig?.userStats?.rpgAttributes || [
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
];
|
||||
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
|
||||
|
||||
if (enabledAttributes.length > 0) {
|
||||
currentState.attributes = {};
|
||||
descriptions.attributes = {};
|
||||
for (const attr of enabledAttributes) {
|
||||
const value = extensionSettings.classicStats?.[attr.id] ?? 10;
|
||||
currentState.attributes[attr.name] = value;
|
||||
if (attr.description) {
|
||||
descriptions.attributes[attr.name] = attr.description;
|
||||
}
|
||||
}
|
||||
|
||||
// Add level
|
||||
currentState.level = extensionSettings.level ?? 1;
|
||||
}
|
||||
}
|
||||
|
||||
return summary.trim();
|
||||
|
||||
// Add descriptions metadata if any exist
|
||||
if (Object.keys(descriptions).length > 0) {
|
||||
currentState._descriptions = descriptions;
|
||||
}
|
||||
|
||||
// Return JSON string if we have any data, otherwise empty string
|
||||
if (Object.keys(currentState).length > 0) {
|
||||
return JSON.stringify(currentState, null, 2);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -638,6 +747,35 @@ export function generateRPGPromptText() {
|
||||
previousState.quests = extensionSettings.questsV2;
|
||||
}
|
||||
|
||||
// Attributes and level (if RPG attributes are enabled and should be included)
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes;
|
||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
||||
|
||||
if (showRPGAttributes && shouldSendAttributes) {
|
||||
const rpgAttributes = trackerConfig?.userStats?.rpgAttributes || [
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
];
|
||||
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
|
||||
|
||||
if (enabledAttributes.length > 0) {
|
||||
previousState.attributes = {};
|
||||
for (const attr of enabledAttributes) {
|
||||
const value = extensionSettings.classicStats?.[attr.id] ?? 10;
|
||||
previousState.attributes[attr.name] = value;
|
||||
}
|
||||
|
||||
// Add level
|
||||
previousState.level = extensionSettings.level ?? 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Output as JSON if we have any data, otherwise indicate first update
|
||||
if (Object.keys(previousState).length > 0) {
|
||||
promptText += '```json\n';
|
||||
@@ -649,8 +787,9 @@ export function generateRPGPromptText() {
|
||||
|
||||
promptText += `</previous>\n`;
|
||||
|
||||
// Add JSON format instructions
|
||||
promptText += generateJSONTrackerInstructions(false, false, false);
|
||||
// Add JSON format instructions - include attributes if alwaysSendAttributes is enabled
|
||||
const includeAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
||||
promptText += generateJSONTrackerInstructions(false, false, includeAttributes);
|
||||
|
||||
return promptText;
|
||||
}
|
||||
@@ -708,3 +847,4 @@ export async function generateSeparateUpdatePrompt() {
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user