Combat encounters: Add pre-encounter config modal, targeting fixes, and tracker integration
- Add pre-encounter narrative configuration modal with combat/summary style settings - Change POV fields to text inputs (default: narrator) for custom character names - Fix targeting system for enemies with spaces in names (e.g., 'Gilded Thrall 1') - Display character-specific sprites/avatars in targeting modal instead of generic emojis - Add combat difficulty scaling guidance to prevent trivial god defeats or endless wolf battles - Integrate tracker updates in combat summary generation (together mode) - Update auto-save logs description to clarify file storage vs chat history - Apply extension theming to Close Combat Window button
This commit is contained in:
@@ -128,6 +128,7 @@ import { ensureHtmlCleaningRegex, detectConflictingRegexScripts } from './src/sy
|
||||
import { setupMemoryRecollectionButton, updateMemoryRecollectionButton } from './src/systems/features/memoryRecollection.js';
|
||||
import { initLorebookLimiter } from './src/systems/features/lorebookLimiter.js';
|
||||
import { DEFAULT_HTML_PROMPT } from './src/systems/generation/promptBuilder.js';
|
||||
import { openEncounterModal } from './src/systems/ui/encounterUI.js';
|
||||
|
||||
// Integration modules
|
||||
import {
|
||||
@@ -402,6 +403,122 @@ async function initUI() {
|
||||
togglePlotButtons();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-encounters').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = { enabled: true, historyDepth: 8, autoSaveLogs: true };
|
||||
}
|
||||
extensionSettings.encounterSettings.enabled = $(this).prop('checked');
|
||||
saveSettings();
|
||||
togglePlotButtons(); // This also controls encounter button visibility
|
||||
});
|
||||
|
||||
$('#rpg-encounter-history-depth').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = { enabled: true, historyDepth: 8, autoSaveLogs: true };
|
||||
}
|
||||
const value = $(this).val();
|
||||
extensionSettings.encounterSettings.historyDepth = parseInt(String(value));
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-autosave-logs').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = { enabled: true, historyDepth: 8, autoSaveLogs: true };
|
||||
}
|
||||
extensionSettings.encounterSettings.autoSaveLogs = $(this).prop('checked');
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
// Combat narrative style settings
|
||||
$('#rpg-combat-tense').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = {};
|
||||
}
|
||||
if (!extensionSettings.encounterSettings.combatNarrative) {
|
||||
extensionSettings.encounterSettings.combatNarrative = {};
|
||||
}
|
||||
extensionSettings.encounterSettings.combatNarrative.tense = $(this).val();
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-combat-person').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = {};
|
||||
}
|
||||
if (!extensionSettings.encounterSettings.combatNarrative) {
|
||||
extensionSettings.encounterSettings.combatNarrative = {};
|
||||
}
|
||||
extensionSettings.encounterSettings.combatNarrative.person = $(this).val();
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-combat-narration').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = {};
|
||||
}
|
||||
if (!extensionSettings.encounterSettings.combatNarrative) {
|
||||
extensionSettings.encounterSettings.combatNarrative = {};
|
||||
}
|
||||
extensionSettings.encounterSettings.combatNarrative.narration = $(this).val();
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-combat-pov').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = {};
|
||||
}
|
||||
if (!extensionSettings.encounterSettings.combatNarrative) {
|
||||
extensionSettings.encounterSettings.combatNarrative = {};
|
||||
}
|
||||
extensionSettings.encounterSettings.combatNarrative.pov = $(this).val();
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
// Summary narrative style settings
|
||||
$('#rpg-summary-tense').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = {};
|
||||
}
|
||||
if (!extensionSettings.encounterSettings.summaryNarrative) {
|
||||
extensionSettings.encounterSettings.summaryNarrative = {};
|
||||
}
|
||||
extensionSettings.encounterSettings.summaryNarrative.tense = $(this).val();
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-summary-person').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = {};
|
||||
}
|
||||
if (!extensionSettings.encounterSettings.summaryNarrative) {
|
||||
extensionSettings.encounterSettings.summaryNarrative = {};
|
||||
}
|
||||
extensionSettings.encounterSettings.summaryNarrative.person = $(this).val();
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-summary-narration').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = {};
|
||||
}
|
||||
if (!extensionSettings.encounterSettings.summaryNarrative) {
|
||||
extensionSettings.encounterSettings.summaryNarrative = {};
|
||||
}
|
||||
extensionSettings.encounterSettings.summaryNarrative.narration = $(this).val();
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-summary-pov').on('change', function() {
|
||||
if (!extensionSettings.encounterSettings) {
|
||||
extensionSettings.encounterSettings = {};
|
||||
}
|
||||
if (!extensionSettings.encounterSettings.summaryNarrative) {
|
||||
extensionSettings.encounterSettings.summaryNarrative = {};
|
||||
}
|
||||
extensionSettings.encounterSettings.summaryNarrative.pov = $(this).val();
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-animations').on('change', function() {
|
||||
extensionSettings.enableAnimations = $(this).prop('checked');
|
||||
saveSettings();
|
||||
@@ -524,6 +641,22 @@ async function initUI() {
|
||||
$('#rpg-custom-html-prompt').val(extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT);
|
||||
|
||||
$('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons);
|
||||
$('#rpg-toggle-encounters').prop('checked', extensionSettings.encounterSettings?.enabled ?? true);
|
||||
$('#rpg-encounter-history-depth').val(extensionSettings.encounterSettings?.historyDepth ?? 8);
|
||||
$('#rpg-toggle-autosave-logs').prop('checked', extensionSettings.encounterSettings?.autoSaveLogs ?? true);
|
||||
|
||||
// Combat narrative style
|
||||
$('#rpg-combat-tense').val(extensionSettings.encounterSettings?.combatNarrative?.tense ?? 'present');
|
||||
$('#rpg-combat-person').val(extensionSettings.encounterSettings?.combatNarrative?.person ?? 'third');
|
||||
$('#rpg-combat-narration').val(extensionSettings.encounterSettings?.combatNarrative?.narration ?? 'omniscient');
|
||||
$('#rpg-combat-pov').val(extensionSettings.encounterSettings?.combatNarrative?.pov ?? 'narrator');
|
||||
|
||||
// Summary narrative style
|
||||
$('#rpg-summary-tense').val(extensionSettings.encounterSettings?.summaryNarrative?.tense ?? 'past');
|
||||
$('#rpg-summary-person').val(extensionSettings.encounterSettings?.summaryNarrative?.person ?? 'third');
|
||||
$('#rpg-summary-narration').val(extensionSettings.encounterSettings?.summaryNarrative?.narration ?? 'omniscient');
|
||||
$('#rpg-summary-pov').val(extensionSettings.encounterSettings?.summaryNarrative?.pov ?? 'narrator');
|
||||
|
||||
$('#rpg-toggle-animations').prop('checked', extensionSettings.enableAnimations);
|
||||
|
||||
// Initialize avatar options
|
||||
@@ -580,7 +713,7 @@ async function initUI() {
|
||||
setupSettingsPopup();
|
||||
initTrackerEditor();
|
||||
addDiceQuickReply();
|
||||
setupPlotButtons(sendPlotProgression);
|
||||
setupPlotButtons(sendPlotProgression, openEncounterModal);
|
||||
setupMobileKeyboardHandling();
|
||||
setupContentEditableScrolling();
|
||||
initInventoryEventListeners();
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Encounter State Module
|
||||
* Manages combat encounter state and history
|
||||
*/
|
||||
|
||||
/**
|
||||
* Current encounter state
|
||||
*/
|
||||
export let currentEncounter = {
|
||||
active: false,
|
||||
initialized: false,
|
||||
combatHistory: [], // Array of {role: 'user'|'assistant'|'system', content: string}
|
||||
combatStats: null, // Current combat stats (HP, party, enemies, etc.)
|
||||
preEncounterContext: [], // Messages from before the encounter started
|
||||
encounterStartMessage: '', // The message that triggered the encounter
|
||||
encounterLog: [] // Full log of combat actions for final summary
|
||||
};
|
||||
|
||||
/**
|
||||
* Encounter logs storage (per chat)
|
||||
*/
|
||||
export let encounterLogs = {
|
||||
// chatId: [
|
||||
// {
|
||||
// timestamp: Date,
|
||||
// log: [],
|
||||
// summary: string,
|
||||
// result: 'victory'|'defeat'|'fled'
|
||||
// }
|
||||
// ]
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the current encounter state
|
||||
* @param {object} encounter - The encounter state object
|
||||
*/
|
||||
export function setCurrentEncounter(encounter) {
|
||||
currentEncounter = encounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates current encounter state with partial data
|
||||
* @param {object} updates - Partial encounter state to merge
|
||||
*/
|
||||
export function updateCurrentEncounter(updates) {
|
||||
Object.assign(currentEncounter, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the encounter state
|
||||
*/
|
||||
export function resetEncounter() {
|
||||
currentEncounter = {
|
||||
active: false,
|
||||
initialized: false,
|
||||
combatHistory: [],
|
||||
combatStats: null,
|
||||
preEncounterContext: [],
|
||||
encounterStartMessage: '',
|
||||
encounterLog: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to combat history
|
||||
* @param {string} role - Message role ('user', 'assistant', or 'system')
|
||||
* @param {string} content - Message content
|
||||
*/
|
||||
export function addCombatMessage(role, content) {
|
||||
currentEncounter.combatHistory.push({ role, content });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an entry to the encounter log
|
||||
* @param {string} action - The action taken
|
||||
* @param {string} result - The result of the action
|
||||
*/
|
||||
export function addEncounterLogEntry(action, result) {
|
||||
currentEncounter.encounterLog.push({
|
||||
timestamp: Date.now(),
|
||||
action,
|
||||
result
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an encounter log for a specific chat
|
||||
* @param {string} chatId - The chat identifier
|
||||
* @param {object} logData - The encounter log data
|
||||
*/
|
||||
export function saveEncounterLog(chatId, logData) {
|
||||
if (!encounterLogs[chatId]) {
|
||||
encounterLogs[chatId] = [];
|
||||
}
|
||||
encounterLogs[chatId].push({
|
||||
timestamp: new Date(),
|
||||
log: logData.log || [],
|
||||
summary: logData.summary || '',
|
||||
result: logData.result || 'unknown'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets encounter logs for a specific chat
|
||||
* @param {string} chatId - The chat identifier
|
||||
* @returns {Array} Array of encounter logs
|
||||
*/
|
||||
export function getEncounterLogs(chatId) {
|
||||
return encounterLogs[chatId] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all encounter logs for a specific chat
|
||||
* @param {string} chatId - The chat identifier
|
||||
*/
|
||||
export function clearEncounterLogs(chatId) {
|
||||
if (encounterLogs[chatId]) {
|
||||
delete encounterLogs[chatId];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports encounter logs as JSON
|
||||
* @param {string} chatId - The chat identifier
|
||||
* @returns {string} JSON string of encounter logs
|
||||
*/
|
||||
export function exportEncounterLogs(chatId) {
|
||||
const logs = getEncounterLogs(chatId);
|
||||
return JSON.stringify(logs, null, 2);
|
||||
}
|
||||
@@ -11,8 +11,9 @@ import { Generate } from '../../../../../../../script.js';
|
||||
/**
|
||||
* Sets up the plot progression buttons inside the send form area.
|
||||
* @param {Function} handlePlotClick - Callback function to handle plot button clicks
|
||||
* @param {Function} handleEncounterClick - Callback function to handle encounter button click
|
||||
*/
|
||||
export function setupPlotButtons(handlePlotClick) {
|
||||
export function setupPlotButtons(handlePlotClick, handleEncounterClick) {
|
||||
// Remove existing buttons if any
|
||||
$('#rpg-plot-buttons').remove();
|
||||
|
||||
@@ -50,6 +51,19 @@ export function setupPlotButtons(handlePlotClick) {
|
||||
" tabindex="0" role="button">
|
||||
<i class="fa-solid fa-forward"></i> Natural Plot
|
||||
</button>
|
||||
<button id="rpg-encounter-button" class="menu_button interactable" style="
|
||||
background-color: #cc3333;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
margin: 0 4px;
|
||||
display: inline-block;
|
||||
" tabindex="0" role="button" title="Enter combat encounter">
|
||||
<i class="fa-solid fa-fire"></i> Enter Encounter
|
||||
</button>
|
||||
</span>
|
||||
`;
|
||||
|
||||
@@ -59,6 +73,7 @@ export function setupPlotButtons(handlePlotClick) {
|
||||
// Add event handlers for buttons
|
||||
$('#rpg-plot-random').on('click', () => handlePlotClick('random'));
|
||||
$('#rpg-plot-natural').on('click', () => handlePlotClick('natural'));
|
||||
$('#rpg-encounter-button').on('click', () => handleEncounterClick());
|
||||
|
||||
// Show/hide based on setting
|
||||
togglePlotButtons();
|
||||
|
||||
@@ -0,0 +1,714 @@
|
||||
/**
|
||||
* Encounter Prompt Builder Module
|
||||
* Handles all AI prompt generation for combat encounters
|
||||
*/
|
||||
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
import { chat, characters, this_chid, substituteParams } from '../../../../../../../script.js';
|
||||
import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js';
|
||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
||||
import { currentEncounter } from '../features/encounterState.js';
|
||||
import { buildInventorySummary, generateTrackerInstructions, generateTrackerExample } from './promptBuilder.js';
|
||||
|
||||
/**
|
||||
* Gets character information from the current chat
|
||||
* @returns {Promise<string>} Formatted character information
|
||||
*/
|
||||
async function getCharactersInfo() {
|
||||
let characterInfo = '';
|
||||
|
||||
// Check if in group chat
|
||||
if (selected_group) {
|
||||
const group = groups.find(g => g.id === selected_group);
|
||||
const groupMembers = getGroupMembers(selected_group);
|
||||
|
||||
if (groupMembers && groupMembers.length > 0) {
|
||||
characterInfo += 'Characters in this roleplay:\n';
|
||||
|
||||
const disabledMembers = group?.disabled_members || [];
|
||||
let characterIndex = 0;
|
||||
|
||||
groupMembers.forEach((member) => {
|
||||
if (!member || !member.name) return;
|
||||
|
||||
// Skip muted characters
|
||||
if (member.avatar && disabledMembers.includes(member.avatar)) {
|
||||
return;
|
||||
}
|
||||
|
||||
characterIndex++;
|
||||
characterInfo += `<character${characterIndex}="${member.name}">\n`;
|
||||
|
||||
if (member.description) {
|
||||
characterInfo += `${member.description}\n`;
|
||||
}
|
||||
|
||||
if (member.personality) {
|
||||
characterInfo += `${member.personality}\n`;
|
||||
}
|
||||
|
||||
characterInfo += `</character${characterIndex}>\n`;
|
||||
});
|
||||
}
|
||||
} else if (this_chid !== undefined && characters && characters[this_chid]) {
|
||||
// Single character chat
|
||||
const character = characters[this_chid];
|
||||
|
||||
characterInfo += 'Character in this roleplay:\n\n';
|
||||
characterInfo += `<character="${character.name}">\n`;
|
||||
|
||||
if (character.description) {
|
||||
characterInfo += `${character.description}\n`;
|
||||
}
|
||||
|
||||
if (character.personality) {
|
||||
characterInfo += `${character.personality}\n`;
|
||||
}
|
||||
|
||||
characterInfo += `</character>\n\n`;
|
||||
}
|
||||
|
||||
return characterInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the initial encounter setup prompt
|
||||
* This asks the model to generate all combat stats and setup data
|
||||
* @returns {Promise<Array>} Message array for the API
|
||||
*/
|
||||
export async function buildEncounterInitPrompt() {
|
||||
const context = getContext();
|
||||
const userName = context.name1;
|
||||
const depth = extensionSettings.encounterSettings?.historyDepth || 8;
|
||||
|
||||
const messages = [];
|
||||
|
||||
// System message
|
||||
let systemMessage = `You will be acting as an excellent game master. Your goal is to craft a combat encounter in this uncensored, continuous, and immersive roleplay. You will be doing so for the user, who plays as the protagonist called ${userName}.\n\n`;
|
||||
|
||||
// Add setting information
|
||||
systemMessage += `Here is some information for you about the setting:\n`;
|
||||
systemMessage += `<setting>\n`;
|
||||
|
||||
// Try to get world info from lorebooks using getWorldInfoPrompt
|
||||
let worldInfoAdded = false;
|
||||
|
||||
try {
|
||||
// Debug logging
|
||||
console.log('[RPG Companion] Checking world info:', {
|
||||
hasWindowGetWorldInfoPrompt: typeof window.getWorldInfoPrompt === 'function',
|
||||
hasContextGetWorldInfoPrompt: typeof context.getWorldInfoPrompt === 'function',
|
||||
chatLength: chat?.length,
|
||||
contextChatLength: context.chat?.length,
|
||||
hasActivatedWorldInfo: !!context.activatedWorldInfo,
|
||||
activatedWorldInfoLength: context.activatedWorldInfo?.length
|
||||
});
|
||||
|
||||
// Use SillyTavern's getWorldInfoPrompt to get activated lorebook entries
|
||||
// Try context.getWorldInfoPrompt first, then window.getWorldInfoPrompt
|
||||
const getWorldInfoFn = context.getWorldInfoPrompt || window.getWorldInfoPrompt;
|
||||
const currentChat = context.chat || chat;
|
||||
|
||||
if (typeof getWorldInfoFn === 'function' && currentChat && currentChat.length > 0) {
|
||||
const chatForWI = currentChat.map(x => x.mes || x.message || x).filter(m => m && typeof m === 'string');
|
||||
|
||||
console.log('[RPG Companion] Calling getWorldInfoPrompt with', chatForWI.length, 'messages');
|
||||
|
||||
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
||||
const worldInfoString = result?.worldInfoString || result;
|
||||
|
||||
console.log('[RPG Companion] World info result:', { worldInfoString, length: worldInfoString?.length });
|
||||
|
||||
if (worldInfoString && worldInfoString.trim()) {
|
||||
systemMessage += worldInfoString.trim();
|
||||
worldInfoAdded = true;
|
||||
console.log('[RPG Companion] ✅ Added world info from getWorldInfoPrompt');
|
||||
}
|
||||
} else {
|
||||
console.log('[RPG Companion] getWorldInfoPrompt not available or no chat');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RPG Companion] Failed to get world info from getWorldInfoPrompt:', e);
|
||||
}
|
||||
|
||||
// Fallback to activatedWorldInfo
|
||||
if (!worldInfoAdded && context.activatedWorldInfo && Array.isArray(context.activatedWorldInfo) && context.activatedWorldInfo.length > 0) {
|
||||
console.log('[RPG Companion] Using fallback activatedWorldInfo:', context.activatedWorldInfo.length, 'entries');
|
||||
context.activatedWorldInfo.forEach((entry) => {
|
||||
if (entry && entry.content) {
|
||||
systemMessage += `${entry.content}\n\n`;
|
||||
worldInfoAdded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!worldInfoAdded) {
|
||||
console.warn('[RPG Companion] ⚠️ No world information available');
|
||||
systemMessage += 'No world information available.';
|
||||
}
|
||||
|
||||
systemMessage += `\n</setting>\n\n`;
|
||||
|
||||
// Add character information
|
||||
const charactersInfo = await getCharactersInfo();
|
||||
if (charactersInfo) {
|
||||
systemMessage += `Here is the information available to you about the characters participating in the fight:\n`;
|
||||
systemMessage += `<characters>\n${charactersInfo}</characters>\n\n`;
|
||||
}
|
||||
|
||||
// Add persona information
|
||||
systemMessage += `Here are details about the user's ${userName}:\n`;
|
||||
systemMessage += `<persona>\n`;
|
||||
|
||||
try {
|
||||
const personaText = substituteParams('{{persona}}');
|
||||
if (personaText && personaText !== '{{persona}}') {
|
||||
systemMessage += personaText;
|
||||
} else {
|
||||
systemMessage += 'No persona information available.';
|
||||
}
|
||||
} catch (e) {
|
||||
systemMessage += 'No persona information available.';
|
||||
}
|
||||
|
||||
systemMessage += `\n</persona>\n\n`;
|
||||
|
||||
// Add chat history from before the encounter
|
||||
systemMessage += `Here is the chat history from before the encounter started between the user and the assistant:\n`;
|
||||
systemMessage += `<history>\n`;
|
||||
|
||||
messages.push({
|
||||
role: 'system',
|
||||
content: systemMessage
|
||||
});
|
||||
|
||||
// Add recent chat history (last X messages before encounter)
|
||||
if (chat && chat.length > 0) {
|
||||
const recentMessages = chat.slice(-depth - 1, -1); // Exclude the last message (encounter trigger)
|
||||
|
||||
for (const message of recentMessages) {
|
||||
const content = message.mes?.trim();
|
||||
// Skip empty messages
|
||||
if (content) {
|
||||
messages.push({
|
||||
role: message.is_user ? 'user' : 'assistant',
|
||||
content: content
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add the encounter trigger message
|
||||
const lastMessage = chat[chat.length - 1];
|
||||
if (lastMessage && lastMessage.mes?.trim()) {
|
||||
currentEncounter.encounterStartMessage = lastMessage.mes;
|
||||
messages.push({
|
||||
role: lastMessage.is_user ? 'user' : 'assistant',
|
||||
content: lastMessage.mes.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build user's current stats
|
||||
let userStatsInfo = '';
|
||||
|
||||
// Add HP and other stats from committed tracker data
|
||||
if (committedTrackerData.userStats) {
|
||||
userStatsInfo += `${userName}'s Current Stats:\n${committedTrackerData.userStats}\n\n`;
|
||||
}
|
||||
|
||||
// Add skills if available
|
||||
const skillsSection = extensionSettings.trackerConfig?.userStats?.skillsSection;
|
||||
if (skillsSection?.enabled && skillsSection.customFields && skillsSection.customFields.length > 0) {
|
||||
userStatsInfo += `${userName}'s Skills: ${skillsSection.customFields.join(', ')}\n`;
|
||||
}
|
||||
|
||||
// Add inventory
|
||||
const inventory = extensionSettings.userStats?.inventory;
|
||||
if (inventory) {
|
||||
const inventorySummary = buildInventorySummary(inventory);
|
||||
userStatsInfo += `${userName}'s Inventory:\n${inventorySummary}\n\n`;
|
||||
}
|
||||
|
||||
// Add classic stats/attributes
|
||||
if (extensionSettings.classicStats) {
|
||||
const stats = extensionSettings.classicStats;
|
||||
userStatsInfo += `${userName}'s Attributes: `;
|
||||
userStatsInfo += `STR ${stats.str}, DEX ${stats.dex}, CON ${stats.con}, INT ${stats.int}, WIS ${stats.wis}, CHA ${stats.cha}, LVL ${extensionSettings.level}\n\n`;
|
||||
}
|
||||
|
||||
// Add present characters info for party members
|
||||
let partyInfo = '';
|
||||
if (committedTrackerData.characterThoughts) {
|
||||
partyInfo += `Present Characters (potential party members):\n${committedTrackerData.characterThoughts}\n\n`;
|
||||
}
|
||||
|
||||
// Close history and add combat initialization instruction
|
||||
let initInstruction = `</history>\n\n`;
|
||||
|
||||
// Wrap RPG Companion panel data in context tags
|
||||
initInstruction += `Here is some additional tracked context for the scene:\n`;
|
||||
initInstruction += `<context>\n`;
|
||||
initInstruction += userStatsInfo;
|
||||
initInstruction += partyInfo;
|
||||
initInstruction += `</context>\n\n`;
|
||||
|
||||
initInstruction += `The combat starts now.\n\n`;
|
||||
initInstruction += `Based on everything above, generate the initial combat state. Analyze who is in the party fighting alongside ${userName} (if anyone), and who the enemies are. Replace placeholders in [brackets] and X with actual values. Return ONLY a JSON object with the following structure:\n\n`;
|
||||
initInstruction += `{\n`;
|
||||
initInstruction += ` "party": [\n`;
|
||||
initInstruction += ` {\n`;
|
||||
initInstruction += ` "name": "${userName}",\n`;
|
||||
initInstruction += ` "hp": X,\n`;
|
||||
initInstruction += ` "maxHp": X,\n`;
|
||||
initInstruction += ` "attacks": [\n`;
|
||||
initInstruction += ` {"name": "Attack", "type": "single-target|AoE|both"},\n`;
|
||||
initInstruction += ` {"name": "Skill1", "type": "single-target|AoE|both"}\n`;
|
||||
initInstruction += ` ],\n`;
|
||||
initInstruction += ` "items": ["Item1", "Item2"],\n`;
|
||||
initInstruction += ` "statuses": [],\n`;
|
||||
initInstruction += ` "isPlayer": true\n`;
|
||||
initInstruction += ` }\n`;
|
||||
initInstruction += ` // Add other party members here if they exist in the context, changing isPlayer to false for them.\n`;
|
||||
initInstruction += ` ],\n`;
|
||||
initInstruction += ` "enemies": [\n`;
|
||||
initInstruction += ` {\n`;
|
||||
initInstruction += ` "name": "Enemy Name",\n`;
|
||||
initInstruction += ` "hp": X,\n`;
|
||||
initInstruction += ` "maxHp": X,\n`;
|
||||
initInstruction += ` "attacks": [\n`;
|
||||
initInstruction += ` {"name": "Attack1", "type": "single-target|AoE|both"},\n`;
|
||||
initInstruction += ` {"name": "Attack2", "type": "single-target|AoE|both"}\n`;
|
||||
initInstruction += ` ],\n`;
|
||||
initInstruction += ` "statuses": [],\n`;
|
||||
initInstruction += ` "description": "Brief enemy description",\n`;
|
||||
initInstruction += ` "sprite": "emoji or brief visual description"\n`;
|
||||
initInstruction += ` }\n`;
|
||||
initInstruction += ` // Add all enemies participating in this combat\n`;
|
||||
initInstruction += ` ],\n`;
|
||||
initInstruction += ` "environment": "Brief description of the combat environment",\n`;
|
||||
initInstruction += ` "styleNotes": {\n`;
|
||||
initInstruction += ` "environmentType": "forest|dungeon|desert|cave|city|ruins|snow|water|castle|wasteland|plains|mountains|swamp|volcanic",\n`;
|
||||
initInstruction += ` "atmosphere": "bright|dark|foggy|stormy|calm|eerie|chaotic|peaceful",\n`;
|
||||
initInstruction += ` "timeOfDay": "dawn|day|dusk|night|twilight",\n`;
|
||||
initInstruction += ` "weather": "clear|rainy|snowy|windy|stormy|overcast"\n`;
|
||||
initInstruction += ` }\n`;
|
||||
initInstruction += `}\n\n`;
|
||||
initInstruction += `IMPORTANT NOTES:\n`;
|
||||
initInstruction += `- For attacks array: Each attack must be an object with "name" and "type" properties\n`;
|
||||
initInstruction += ` - "single-target": Can only target one character (enemy or ally)\n`;
|
||||
initInstruction += ` - "AoE": Area of Effect - targets all enemies, but some AoE attacks (like storms, explosions) can also harm allies if the attack is indiscriminate\n`;
|
||||
initInstruction += ` - "both": Player can choose to target a single enemy OR use as AoE\n`;
|
||||
initInstruction += `- Statuses array: May start empty, but don't have to if characters applied them before the combat\n`;
|
||||
initInstruction += ` - Each status has a format: {"name": "Status Name", "emoji": "💀", "duration": X}\n`;
|
||||
initInstruction += ` - Examples: Poisoned (🧪), Burning (🔥), Blessed (✨), Stunned (💫), Weakened (⬇️), Strengthened (⬆️)\n\n`;
|
||||
initInstruction += `The styleNotes object will be used to visually style the combat window - choose ONE value from each category that best fits the environment described in the chat history.\n\n`;
|
||||
initInstruction += `Use the user's current stats, inventory, and skills to populate the party data. For ${userName}'s attacks array, include their available skills. For items, include usable items from their inventory. Set HP based on their current Health stat if available.\n\n`;
|
||||
initInstruction += `Ensure all party members and enemies have realistic HP values based on the setting and their descriptions. Return ONLY the JSON object, no other text.`;
|
||||
|
||||
// Only add the instruction if it has meaningful content
|
||||
if (initInstruction.trim()) {
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: initInstruction.trim()
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that we have at least one message with content
|
||||
if (messages.length === 0 || messages.every(m => !m.content || !m.content.trim())) {
|
||||
throw new Error('Unable to build encounter prompt - no valid content available');
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a combat action prompt
|
||||
* This is sent when the user takes an action in combat
|
||||
* @param {string} action - The action taken by the user
|
||||
* @param {object} combatStats - Current combat statistics
|
||||
* @returns {Array} Message array for the API
|
||||
*/
|
||||
export async function buildCombatActionPrompt(action, combatStats) {
|
||||
const context = getContext();
|
||||
const userName = context.name1;
|
||||
const depth = extensionSettings.encounterSettings?.historyDepth || 8;
|
||||
|
||||
// Get narrative style from settings
|
||||
const narrativeStyle = extensionSettings.encounterSettings?.combatNarrative || {};
|
||||
const tense = narrativeStyle.tense || 'present';
|
||||
const person = narrativeStyle.person || 'third';
|
||||
const narration = narrativeStyle.narration || 'omniscient';
|
||||
const pov = narrativeStyle.pov || 'narrator';
|
||||
|
||||
const messages = [];
|
||||
|
||||
// Build system message with setting info
|
||||
let systemMessage = `You are the game master managing this combat encounter. You must not play as ${userName} - only describe what happens as a result of their actions/dialogues and control NPCs/enemies.\n\n`;
|
||||
|
||||
// Add setting information
|
||||
systemMessage += `Here is some information for you about the setting:\n`;
|
||||
systemMessage += `<setting>\n`;
|
||||
|
||||
// Get world info
|
||||
let worldInfoAdded = false;
|
||||
try {
|
||||
const getWorldInfoFn = context.getWorldInfoPrompt || window.getWorldInfoPrompt;
|
||||
const currentChat = context.chat || chat;
|
||||
|
||||
if (typeof getWorldInfoFn === 'function' && currentChat && currentChat.length > 0) {
|
||||
const chatForWI = currentChat.map(x => x.mes || x.message || x).filter(m => m && typeof m === 'string');
|
||||
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
||||
const worldInfoString = result?.worldInfoString || result;
|
||||
|
||||
if (worldInfoString && worldInfoString.trim()) {
|
||||
systemMessage += worldInfoString.trim();
|
||||
worldInfoAdded = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RPG Companion] Failed to get world info for combat action:', e);
|
||||
}
|
||||
|
||||
if (!worldInfoAdded && context.activatedWorldInfo && Array.isArray(context.activatedWorldInfo) && context.activatedWorldInfo.length > 0) {
|
||||
context.activatedWorldInfo.forEach((entry) => {
|
||||
if (entry && entry.content) {
|
||||
systemMessage += `${entry.content}\n\n`;
|
||||
worldInfoAdded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!worldInfoAdded) {
|
||||
systemMessage += 'No world information available.';
|
||||
}
|
||||
|
||||
systemMessage += `\n</setting>\n\n`;
|
||||
|
||||
// Add character information
|
||||
const charactersInfo = await getCharactersInfo();
|
||||
if (charactersInfo) {
|
||||
systemMessage += `Here is the information available to you about the characters:\n`;
|
||||
systemMessage += `<characters>\n${charactersInfo}</characters>\n\n`;
|
||||
}
|
||||
|
||||
// Add persona info
|
||||
if (context.name1) {
|
||||
systemMessage += `The protagonist is:\n`;
|
||||
systemMessage += `<persona>\n`;
|
||||
|
||||
// Use substituteParams to get {{persona}} like in initial encounter
|
||||
try {
|
||||
const personaText = substituteParams('{{persona}}');
|
||||
if (personaText && personaText !== '{{persona}}') {
|
||||
systemMessage += personaText;
|
||||
} else {
|
||||
systemMessage += `Name: ${context.name1}\n`;
|
||||
if (extensionSettings.userStats?.personaDescription) {
|
||||
systemMessage += `${extensionSettings.userStats.personaDescription}\n`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
systemMessage += `Name: ${context.name1}\n`;
|
||||
if (extensionSettings.userStats?.personaDescription) {
|
||||
systemMessage += `${extensionSettings.userStats.personaDescription}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add ONLY classic stats/attributes if enabled
|
||||
if (extensionSettings.classicStats) {
|
||||
const stats = extensionSettings.classicStats;
|
||||
systemMessage += `\nAttributes: STR ${stats.str}, DEX ${stats.dex}, CON ${stats.con}, INT ${stats.int}, WIS ${stats.wis}, CHA ${stats.cha}, LVL ${extensionSettings.level}\n`;
|
||||
}
|
||||
|
||||
systemMessage += `</persona>\n\n`;
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'system',
|
||||
content: systemMessage
|
||||
});
|
||||
|
||||
// Add recent chat history for context - append as user/assistant messages like initial encounter
|
||||
const currentChat = context.chat || chat;
|
||||
if (currentChat && currentChat.length > 0) {
|
||||
const recentMessages = currentChat.slice(-depth);
|
||||
|
||||
for (const message of recentMessages) {
|
||||
const content = message.mes?.trim();
|
||||
// Skip empty messages
|
||||
if (content) {
|
||||
messages.push({
|
||||
role: message.is_user ? 'user' : 'assistant',
|
||||
content: content
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add combat log as plain text (previous actions)
|
||||
if (currentEncounter.encounterLog && currentEncounter.encounterLog.length > 0) {
|
||||
let combatHistory = 'Previous Combat Actions:\n';
|
||||
currentEncounter.encounterLog.forEach(entry => {
|
||||
combatHistory += `- ${entry.action}\n`;
|
||||
if (entry.result) {
|
||||
combatHistory += ` ${entry.result}\n`;
|
||||
}
|
||||
});
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: combatHistory
|
||||
});
|
||||
}
|
||||
|
||||
// Add current combat state with FULL information (but tell AI not to regenerate static parts)
|
||||
let stateMessage = `Current Combat State:\n`;
|
||||
stateMessage += `Environment: ${combatStats.environment || 'Unknown location'}\n\n`;
|
||||
|
||||
stateMessage += `Party Members:\n`;
|
||||
combatStats.party.forEach(member => {
|
||||
stateMessage += `- ${member.name}${member.isPlayer ? ' (Player)' : ''}: ${member.hp}/${member.maxHp} HP\n`;
|
||||
if (member.attacks && member.attacks.length > 0) {
|
||||
stateMessage += ` Attacks: ${member.attacks.map(a => typeof a === 'string' ? a : a.name).join(', ')}\n`;
|
||||
}
|
||||
if (member.items && member.items.length > 0) {
|
||||
stateMessage += ` Items: ${member.items.join(', ')}\n`;
|
||||
}
|
||||
if (member.statuses && member.statuses.length > 0) {
|
||||
const validStatuses = member.statuses.filter(s => s && (s.emoji || s.name));
|
||||
if (validStatuses.length > 0) {
|
||||
stateMessage += ` Status Effects: ${validStatuses.map(s => `${s.emoji || ''} ${s.name || ''}`.trim()).join(', ')}\n`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
stateMessage += `\nEnemies:\n`;
|
||||
combatStats.enemies.forEach(enemy => {
|
||||
stateMessage += `- ${enemy.name} (${enemy.sprite || ''}): ${enemy.hp}/${enemy.maxHp} HP\n`;
|
||||
if (enemy.description) {
|
||||
stateMessage += ` ${enemy.description}\n`;
|
||||
}
|
||||
if (enemy.attacks && enemy.attacks.length > 0) {
|
||||
stateMessage += ` Attacks: ${enemy.attacks.map(a => typeof a === 'string' ? a : a.name).join(', ')}\n`;
|
||||
}
|
||||
if (enemy.statuses && enemy.statuses.length > 0) {
|
||||
const validStatuses = enemy.statuses.filter(s => s && (s.emoji || s.name));
|
||||
if (validStatuses.length > 0) {
|
||||
stateMessage += ` Status Effects: ${validStatuses.map(s => `${s.emoji || ''} ${s.name || ''}`.trim()).join(', ')}\n`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
stateMessage += `\n${userName}'s Action: ${action}\n\n`;
|
||||
stateMessage += `Respond with a JSON object containing ONLY updated HP values and new status effects. DO NOT regenerate character descriptions, sprites, or environment:\n`;
|
||||
stateMessage += `{\n`;
|
||||
stateMessage += ` "combatStats": {\n`;
|
||||
stateMessage += ` "party": [{ "name": "Name", "hp": X, "maxHp": X, "statuses": [...] }],\n`;
|
||||
stateMessage += ` "enemies": [{ "name": "Name", "hp": X, "maxHp": X, "statuses": [...] }]\n`;
|
||||
stateMessage += ` },\n`;
|
||||
stateMessage += ` "enemyActions": [{ "enemyName": "Name", "action": "what they do", "target": "target" }],\n`;
|
||||
stateMessage += ` "partyActions": [{ "memberName": "Name", "action": "what they do", "target": "target" }],\n`;
|
||||
stateMessage += ` "narrative": "The roleplay description of what happens"\n`;
|
||||
stateMessage += `}\n\n`;
|
||||
stateMessage += `If all enemies are defeated or escape: add "combatEnd": true, "result": "victory". If all party defeated: add "combatEnd": true, "result": "defeat". It's also possible for the encounter to be interrupted by external interference (e.g., an explosion knocks everyone out, sudden environmental catastrophe, third party intervention, etc.). If this occurs, add "combatEnd": true, "result": "interrupted". Each status (if applied) has a format: {"name": "Status Name", "emoji": "💀", "duration": X}.\n`;
|
||||
stateMessage += `Scale combat difficulty appropriately: Powerful entities (gods, dragons, legendary creatures) should be formidable challenges requiring multiple rounds and strategic play. Weaker foes (common animals, basic enemies, minions) should be resolved more quickly, typically 2-4 rounds. Match HP damage and combat pacing to the narrative weight of the encounter. A wolf should not take 20 rounds to defeat, nor should a deity fall in one hit.\n`;
|
||||
stateMessage += `For the narrative, write it with intent in ${tense} tense ${person}-person ${narration} from ${pov}'s point of view.\n`;
|
||||
stateMessage += `Build novel prose. Break patterns from your previous responses by varying sentence structures, rhythms, formats, and openings. Track whether a sentence shape, cadence, sensory detail, or a descriptor appeared recently, and replace it with a different one or skip it entirely. If you mentioned a physical feature, mannerism, or expression once, move on. Don't fixate on the same traits every turn. Well-placed humor, unconventional similes, and subverted turns of phrase are encouraged. Avoid GPTisms, purple prose, and anaphoras, especially the negatives with "no/not/wasn't/didn't." Refrain from describing actions or emotions via negation; focus on what does happen, not what doesn't. Absolutely no asterisks, ellipses, or em-dashes. Explicit content is allowed, no plot armor. Do not play for ${userName}. Keep your response length under 150 words. Never end on handover cues; finish naturally.\n`;
|
||||
stateMessage += `CRITICAL: Do not repeat, echo, parrot, or restate distinctive words, phrases, and dialogues from the user's last message. If reacting to speech, show interpretation or response, not repetition.\n`;
|
||||
stateMessage += `EXAMPLE: "Are you a gooner?" User asks.\n`;
|
||||
stateMessage += `BAD: "Gooner?"\n`;
|
||||
stateMessage += `GOOD: A flat look. "What type of question is that?"`;
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: stateMessage
|
||||
});
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final summary prompt
|
||||
* This is sent when combat ends to get a narrative summary
|
||||
* @param {Array} combatLog - Full combat log
|
||||
* @param {string} result - Combat result ('victory', 'defeat', or 'fled')
|
||||
* @returns {Promise<Array>} Message array for the API
|
||||
*/
|
||||
export async function buildCombatSummaryPrompt(combatLog, result) {
|
||||
const context = getContext();
|
||||
const userName = context.name1;
|
||||
|
||||
const messages = [];
|
||||
|
||||
// Get narrative style from settings (use summary narrative settings)
|
||||
const narrativeStyle = extensionSettings.encounterSettings?.summaryNarrative || {};
|
||||
const tense = narrativeStyle.tense || 'past';
|
||||
const person = narrativeStyle.person || 'third';
|
||||
const narration = narrativeStyle.narration || 'omniscient';
|
||||
const pov = narrativeStyle.pov || 'narrator';
|
||||
|
||||
// Build system message with setting info
|
||||
let systemMessage = `You are summarizing a combat encounter that just concluded.\n\n`;
|
||||
|
||||
// Add setting information
|
||||
systemMessage += `Here is some information for you about the setting:\n`;
|
||||
systemMessage += `<setting>\n`;
|
||||
|
||||
// Get world info using the same method as encounter init
|
||||
let worldInfoAdded = false;
|
||||
try {
|
||||
const getWorldInfoFn = context.getWorldInfoPrompt || window.getWorldInfoPrompt;
|
||||
const currentChat = context.chat || chat;
|
||||
|
||||
if (typeof getWorldInfoFn === 'function' && currentChat && currentChat.length > 0) {
|
||||
const chatForWI = currentChat.map(x => x.mes || x.message || x).filter(m => m && typeof m === 'string');
|
||||
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
||||
const worldInfoString = result?.worldInfoString || result;
|
||||
|
||||
if (worldInfoString && worldInfoString.trim()) {
|
||||
systemMessage += worldInfoString.trim();
|
||||
worldInfoAdded = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RPG Companion] Failed to get world info for summary:', e);
|
||||
}
|
||||
|
||||
// Fallback to activatedWorldInfo
|
||||
if (!worldInfoAdded && context.activatedWorldInfo && Array.isArray(context.activatedWorldInfo) && context.activatedWorldInfo.length > 0) {
|
||||
context.activatedWorldInfo.forEach((entry) => {
|
||||
if (entry && entry.content) {
|
||||
systemMessage += `${entry.content}\n\n`;
|
||||
worldInfoAdded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!worldInfoAdded) {
|
||||
systemMessage += 'No world information available.';
|
||||
}
|
||||
|
||||
systemMessage += `\n</setting>\n\n`;
|
||||
|
||||
// Add character information
|
||||
const charactersInfo = await getCharactersInfo();
|
||||
if (charactersInfo) {
|
||||
systemMessage += `Here is the information available to you about the characters:\n`;
|
||||
systemMessage += `<characters>\n${charactersInfo}</characters>\n\n`;
|
||||
}
|
||||
|
||||
// Add persona information
|
||||
systemMessage += `Here are details about ${userName}:\n`;
|
||||
systemMessage += `<persona>\n`;
|
||||
|
||||
try {
|
||||
const personaText = substituteParams('{{persona}}');
|
||||
if (personaText && personaText !== '{{persona}}') {
|
||||
systemMessage += personaText;
|
||||
} else {
|
||||
systemMessage += 'No persona information available.';
|
||||
}
|
||||
} catch (e) {
|
||||
systemMessage += 'No persona information available.';
|
||||
}
|
||||
|
||||
systemMessage += `\n</persona>\n\n`;
|
||||
|
||||
// Add the message that triggered the encounter
|
||||
if (currentEncounter.encounterStartMessage) {
|
||||
systemMessage += `Here is the last message before combat started:\n`;
|
||||
systemMessage += `<trigger>\n${currentEncounter.encounterStartMessage}\n</trigger>\n\n`;
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'system',
|
||||
content: systemMessage
|
||||
});
|
||||
|
||||
let summaryMessage = `Combat has ended with result: ${result}\n\n`;
|
||||
summaryMessage += `Full Combat Log:\n`;
|
||||
|
||||
combatLog.forEach((entry, index) => {
|
||||
summaryMessage += `\nRound ${index + 1}:\n`;
|
||||
summaryMessage += `${entry.action}\n`;
|
||||
summaryMessage += `${entry.result}\n`;
|
||||
});
|
||||
|
||||
summaryMessage += `\n\nProvide a narrative summary of the entire fight in a way that fits the style from the chat history. Start with [FIGHT CONCLUDED] on the first line, then provide the description.\n\n`;
|
||||
summaryMessage += `Write with intent in ${tense} tense ${person}-person ${narration} from ${pov}'s point of view.\n`;
|
||||
summaryMessage += `Build novel prose. Break patterns from your previous responses by varying sentence structures, rhythms, formats, and openings. If you last started with a narration, begin this one with dialogue; if with an action, switch to an internal thought. Track whether a sentence shape, cadence, sensory detail, or a descriptor appeared recently, and replace it with a different one or skip it entirely. If you mentioned a physical feature, mannerism, or expression once, move on. Don't fixate on the same traits every turn. Well-placed humor, unconventional similes, and subverted turns of phrase are encouraged. Avoid GPTisms, purple prose, and anaphoras, especially the negatives with "no/not/wasn't/didn't." Refrain from describing actions or emotions via negation; focus on what does happen, not what doesn't. Minimize asterisks, ellipses, and em-dashes. Explicit content is allowed. Never end on handover cues; finish naturally.\n\n`;
|
||||
summaryMessage += `Express ${userName}'s actions and dialogue using indirect speech (e.g., "${userName} swung their sword" or "${userName} asked for help"). The summary should be 2-4 paragraphs and capture the essence of the battle.\n\n`;
|
||||
|
||||
// If in Together mode and trackers are enabled, add tracker update instructions
|
||||
if (extensionSettings.generationMode === 'together' && (extensionSettings.showUserStats || extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts)) {
|
||||
summaryMessage += `\n--- TRACKER UPDATE ---\n\n`;
|
||||
summaryMessage += `After the [FIGHT CONCLUDED] summary, update the RPG trackers to reflect ${userName}'s state AFTER the combat encounter. `;
|
||||
summaryMessage += `Account for any injuries sustained, resources used, emotional state changes, or other consequences of the battle.\n\n`;
|
||||
|
||||
// Include pre-combat tracker state if available
|
||||
if (committedTrackerData.userStats || committedTrackerData.infoBox || committedTrackerData.characterThoughts) {
|
||||
summaryMessage += `Pre-combat tracker state:\n`;
|
||||
summaryMessage += `<previous>\n`;
|
||||
|
||||
if (committedTrackerData.userStats) {
|
||||
summaryMessage += `${userName}'s Stats:\n${committedTrackerData.userStats}\n\n`;
|
||||
}
|
||||
|
||||
if (committedTrackerData.infoBox) {
|
||||
summaryMessage += `Info Box:\n${committedTrackerData.infoBox}\n\n`;
|
||||
}
|
||||
|
||||
if (committedTrackerData.characterThoughts) {
|
||||
summaryMessage += `Present Characters:\n${committedTrackerData.characterThoughts}\n\n`;
|
||||
}
|
||||
|
||||
summaryMessage += `</previous>\n\n`;
|
||||
}
|
||||
|
||||
// Add tracker instructions and example
|
||||
const trackerInstructions = generateTrackerInstructions(false, false, true);
|
||||
summaryMessage += trackerInstructions;
|
||||
|
||||
const trackerExample = generateTrackerExample();
|
||||
if (trackerExample) {
|
||||
summaryMessage += `\n${trackerExample}`;
|
||||
}
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: summaryMessage
|
||||
});
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses JSON response from the AI, handling code blocks
|
||||
* @param {string} response - The AI response
|
||||
* @returns {object|null} Parsed JSON object or null if parsing fails
|
||||
*/
|
||||
export function parseEncounterJSON(response) {
|
||||
try {
|
||||
// Remove code blocks if present
|
||||
let cleaned = response.trim();
|
||||
|
||||
// Remove ```json and ``` markers
|
||||
cleaned = cleaned.replace(/```json\s*/gi, '');
|
||||
cleaned = cleaned.replace(/```\s*/g, '');
|
||||
|
||||
// Find the first { and last }
|
||||
const firstBrace = cleaned.indexOf('{');
|
||||
const lastBrace = cleaned.lastIndexOf('}');
|
||||
|
||||
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||
cleaned = cleaned.substring(firstBrace, lastBrace + 1);
|
||||
}
|
||||
|
||||
return JSON.parse(cleaned);
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] Failed to parse encounter JSON:', error);
|
||||
console.error('[RPG Companion] Response was:', response);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -205,10 +205,10 @@ export function renderInfoBox() {
|
||||
data.weatherEmoji = emoji;
|
||||
data.weatherForecast = text;
|
||||
} else if (weatherStr.includes(',')) {
|
||||
// Fallback to comma split if emoji detection failed
|
||||
const weatherParts = weatherStr.split(',').map(p => p.trim());
|
||||
data.weatherEmoji = weatherParts[0] || '';
|
||||
data.weatherForecast = weatherParts[1] || '';
|
||||
// Fallback to comma split if emoji detection failed - split only on FIRST comma
|
||||
const firstCommaIndex = weatherStr.indexOf(',');
|
||||
data.weatherEmoji = weatherStr.substring(0, firstCommaIndex).trim();
|
||||
data.weatherForecast = weatherStr.substring(firstCommaIndex + 1).trim();
|
||||
} else {
|
||||
// No clear separation - assume it's all forecast text
|
||||
data.weatherEmoji = '🌤️'; // Default emoji
|
||||
@@ -608,14 +608,16 @@ export function updateInfoBoxField(field, value) {
|
||||
if (line.startsWith('Weather:')) {
|
||||
// New format: Weather: emoji, forecast
|
||||
const weatherContent = line.replace('Weather:', '').trim();
|
||||
const parts = weatherContent.split(',').map(p => p.trim());
|
||||
const forecast = parts[1] || 'Weather';
|
||||
// Split only on first comma to get emoji and rest
|
||||
const firstCommaIndex = weatherContent.indexOf(',');
|
||||
const forecast = firstCommaIndex > 0 ? weatherContent.substring(firstCommaIndex + 1).trim() : 'Weather';
|
||||
return `Weather: ${value}, ${forecast}`;
|
||||
} else {
|
||||
// Legacy format: emoji: forecast
|
||||
const parts = line.split(':');
|
||||
if (parts.length >= 2) {
|
||||
return `${value}: ${parts.slice(1).join(':').trim()}`;
|
||||
const firstColonIndex = line.indexOf(':');
|
||||
if (firstColonIndex >= 0) {
|
||||
const forecast = line.substring(firstColonIndex + 1).trim();
|
||||
return `${value}: ${forecast}`;
|
||||
}
|
||||
}
|
||||
} else if (field === 'weatherForecast' && index === weatherLineIndex) {
|
||||
@@ -623,14 +625,16 @@ export function updateInfoBoxField(field, value) {
|
||||
if (line.startsWith('Weather:')) {
|
||||
// New format: Weather: emoji, forecast
|
||||
const weatherContent = line.replace('Weather:', '').trim();
|
||||
const parts = weatherContent.split(',').map(p => p.trim());
|
||||
const emoji = parts[0] || '🌤️';
|
||||
// Split only on first comma to get emoji and rest
|
||||
const firstCommaIndex = weatherContent.indexOf(',');
|
||||
const emoji = firstCommaIndex > 0 ? weatherContent.substring(0, firstCommaIndex).trim() : '🌤️';
|
||||
return `Weather: ${emoji}, ${value}`;
|
||||
} else {
|
||||
// Legacy format: emoji: forecast
|
||||
const parts = line.split(':');
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0].trim()}: ${value}`;
|
||||
const firstColonIndex = line.indexOf(':');
|
||||
if (firstColonIndex >= 0) {
|
||||
const emoji = line.substring(0, firstColonIndex).trim();
|
||||
return `${emoji}: ${value}`;
|
||||
}
|
||||
}
|
||||
} else if (field === 'temperature' && (line.includes('🌡️:') || line.startsWith('Temperature:'))) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,13 @@ import { i18n } from '../../core/i18n.js';
|
||||
export function togglePlotButtons() {
|
||||
if (extensionSettings.enablePlotButtons && extensionSettings.enabled) {
|
||||
$('#rpg-plot-buttons').show();
|
||||
|
||||
// Show/hide encounter button based on encounter settings
|
||||
if (extensionSettings.encounterSettings?.enabled) {
|
||||
$('#rpg-encounter-button').show();
|
||||
} else {
|
||||
$('#rpg-encounter-button').hide();
|
||||
}
|
||||
} else {
|
||||
$('#rpg-plot-buttons').hide();
|
||||
}
|
||||
|
||||
@@ -268,6 +268,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpg-settings-group">
|
||||
<h4><i class="fa-solid fa-swords" aria-hidden="true"></i> Combat Encounters</h4>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-encounters" />
|
||||
<span>Enable Combat Encounters</span>
|
||||
</label>
|
||||
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;">
|
||||
Show the "Start Encounter" button above chat input for interactive combat
|
||||
</small>
|
||||
|
||||
<div class="rpg-setting-row" style="margin-top: 12px;">
|
||||
<label for="rpg-encounter-history-depth">Chat History Depth:</label>
|
||||
<input type="number" id="rpg-encounter-history-depth" min="1" max="20" value="8" class="rpg-input" />
|
||||
<small>Number of recent messages to include in combat initialization</small>
|
||||
</div>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-autosave-logs" />
|
||||
<span>Auto-save Combat Logs</span>
|
||||
</label>
|
||||
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;">
|
||||
Save detailed combat logs to file for future reference and analysis
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="rpg-settings-group">
|
||||
<h4 data-i18n-key="template.settingsModal.advancedTitle"><i class="fa-solid fa-sliders" aria-hidden="true"></i> Advanced</h4>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user