feat: rpg stats improvements

This commit is contained in:
Subarashimo
2025-12-04 20:40:02 +01:00
parent b5f5f6d9c5
commit 9f6c44745b
9 changed files with 310 additions and 98 deletions
+2
View File
@@ -434,6 +434,8 @@ function migrateToTrackerConfig() {
userStats: {
customStats: [],
showRPGAttributes: true,
alwaysSendAttributes: false,
allowAIUpdateAttributes: true,
rpgAttributes: [
{ id: 'str', name: 'STR', description: '', enabled: true },
{ id: 'dex', name: 'DEX', description: '', enabled: true },
+1
View File
@@ -120,6 +120,7 @@ export let extensionSettings = {
// RPG Attributes (customizable D&D-style attributes)
showRPGAttributes: true,
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
allowAIUpdateAttributes: true, // If true, allow AI to update attributes from JSON response; if false, attributes are read-only
rpgAttributes: [
{ id: 'str', name: 'STR', description: '', enabled: true },
{ id: 'dex', name: 'DEX', description: '', enabled: true },
+2
View File
@@ -87,6 +87,8 @@
"template.trackerEditorModal.userStatsTab.enableRpgAttributes": "Enable RPG Attributes Section",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "Always Include Attributes in Prompt",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "If disabled, attributes are only sent when a dice roll is active.",
"template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes": "Allow AI to Update RPG Attributes",
"template.trackerEditorModal.userStatsTab.allowAIUpdateAttributesNote": "If enabled, the AI can update attribute values and level from its responses. If disabled, attributes are read-only and can only be changed manually.",
"template.trackerEditorModal.userStatsTab.addAttributeButton": "Add Attribute",
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "Status Section",
"template.trackerEditorModal.userStatsTab.enableStatusSection": "Enable Status Section",
+2
View File
@@ -87,6 +87,8 @@
"template.trackerEditorModal.userStatsTab.enableRpgAttributes": "啟用 RPG 屬性",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "始終發送屬性(prompt",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "將 RPG 屬性始終包含在提示詞中,即使它們未顯示在面板上也一樣。",
"template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes": "允許 AI 更新 RPG 屬性",
"template.trackerEditorModal.userStatsTab.allowAIUpdateAttributesNote": "如果啟用,AI 可以從其 JSON 回應中更新屬性值和等級。如果禁用,屬性為唯讀,只能手動更改。",
"template.trackerEditorModal.userStatsTab.addAttributeButton": "添加屬性",
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "狀態欄",
"template.trackerEditorModal.userStatsTab.enableStatusSection": "啟用狀態欄",
+8 -8
View File
@@ -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);
+37
View File
@@ -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) {
+227 -87
View File
@@ -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;
}
+15 -3
View File
@@ -242,6 +242,14 @@ function renderUserStatsTab() {
html += '</div>';
html += `<small class="rpg-editor-note">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote')}</small>`;
// Allow AI to update attributes toggle
const allowAIUpdateAttributes = config.allowAIUpdateAttributes !== undefined ? config.allowAIUpdateAttributes : true;
html += '<div class="rpg-editor-toggle-row">';
html += `<input type="checkbox" id="rpg-allow-ai-update-attrs" ${allowAIUpdateAttributes ? 'checked' : ''}>`;
html += `<label for="rpg-allow-ai-update-attrs">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes')}</label>`;
html += '</div>';
html += `<small class="rpg-editor-note">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.allowAIUpdateAttributesNote')}</small>`;
html += '<div class="rpg-editor-stats-list" id="rpg-editor-attrs-list">';
// Ensure rpgAttributes exists in the actual config (not just local fallback)
@@ -444,9 +452,13 @@ function setupUserStatsListeners() {
});
// Always send attributes toggle
$('#rpg-always-send-attrs').off('change').on('change', function() {
extensionSettings.trackerConfig.userStats.alwaysSendAttributes = $(this).is(':checked');
});
$('#rpg-always-send-attrs').off('change').on('change', function() {
extensionSettings.trackerConfig.userStats.alwaysSendAttributes = $(this).is(':checked');
});
$('#rpg-allow-ai-update-attrs').off('change').on('change', function() {
extensionSettings.trackerConfig.userStats.allowAIUpdateAttributes = $(this).is(':checked');
});
// Status section toggles
$('#rpg-status-enabled').off('change').on('change', function() {
+16
View File
@@ -43,6 +43,12 @@
* Example: { "Health": 85, "Energy": 70, "Custom Stat": 50 }
*/
/**
* @typedef {Object.<string, number>} TrackerAttributes
* Dynamic attributes object - keys are attribute names from config (e.g., STR, DEX), values are numeric
* Example: { "STR": 15, "DEX": 12, "INT": 18 }
*/
/**
* @typedef {Object} TrackerStatus
* @property {string} [mood] - Mood emoji (if enabled)
@@ -84,6 +90,8 @@
* @typedef {Object} TrackerData
* @property {TrackerStats} [stats] - Numeric stats (based on config)
* @property {TrackerStatus} [status] - Status info (mood, custom fields)
* @property {TrackerAttributes} [attributes] - RPG attributes (STR, DEX, etc.)
* @property {number} [level] - Character level
* @property {TrackerInfoBox} [infoBox] - Scene information (based on enabled widgets)
* @property {TrackerCharacter[]} [characters] - Present characters
* @property {TrackerInventory} [inventory] - Player inventory
@@ -431,6 +439,14 @@ export function mergeTrackerData(existing, newData) {
};
}
if (newData.attributes) {
merged.attributes = { ...merged.attributes, ...newData.attributes };
}
if (newData.level !== undefined) {
merged.level = newData.level;
}
if (newData.infoBox) {
merged.infoBox = { ...merged.infoBox, ...newData.infoBox };
}