Initializing combat...
+${combatData.environment || 'Battle Arena'}
+${enemy.description}
` : ''} +You have been defeated...
You have been defeated...
'; + } + } + } + + /** + * Adds multiple log entries sequentially with animation + * @param {Array} entries - Array of {message, type} objects + * @param {number} delay - Delay between entries in ms + */ + async addLogsSequentially(entries, delay = 400) { + for (const entry of entries) { + this.addToLog(entry.message, entry.type); + if (entries.indexOf(entry) < entries.length - 1) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + /** + * Adds an entry to the combat log + * @param {string} message - Log message + * @param {string} type - Log entry type (for styling) + */ + addToLog(message, type = '') { + const logContainer = this.modal.querySelector('#rpg-encounter-log'); + if (!logContainer) return; + + const entry = document.createElement('div'); + entry.className = `rpg-encounter-log-entry ${type}`; + entry.style.whiteSpace = 'pre-wrap'; + entry.textContent = message; + + logContainer.appendChild(entry); + logContainer.scrollTop = logContainer.scrollHeight; + } + + /** + * Concludes the encounter early (user-initiated) + */ + async concludeEncounter() { + if (!currentEncounter.active) { + console.warn('[RPG Companion] No active encounter to conclude'); + return; + } + + // End combat with "interrupted" result + await this.endCombat('interrupted'); + } + + /** + * Ends the combat and generates summary + * @param {string} result - Combat result ('victory', 'defeat', 'fled', 'interrupted') + */ + async endCombat(result) { + try { + // Show combat over screen + this.showCombatOverScreen(result); + + // Generate summary + const summaryPrompt = await buildCombatSummaryPrompt(currentEncounter.encounterLog, result); + + const summaryResponse = await generateRaw({ + prompt: summaryPrompt, + quietToLoud: false + }); + + if (summaryResponse) { + // Extract summary (remove [FIGHT CONCLUDED] tag) + const summary = summaryResponse.replace(/\[FIGHT CONCLUDED\]\s*/i, '').trim(); + + // Determine which character should speak the summary + const speakerName = this.getCombatNarrator(); + + // Use /sendas command to safely add summary to chat + // This handles group chats properly and won't delete chat history + try { + await executeSlashCommandsOnChatInput( + `/sendas name="${speakerName}" ${summary}`, + { clearChatInput: false } + ); + + console.log(`[RPG Companion] Added combat summary to chat as "${speakerName}"`); + + // Update combat over screen + this.updateCombatOverScreen(true, speakerName); + } catch (sendError) { + console.error('[RPG Companion] Error using /sendas command:', sendError); + // Fallback: try appending to last message + if (chat && chat.length > 0) { + const lastMessage = chat[chat.length - 1]; + if (lastMessage) { + lastMessage.mes += '\n\n' + summary; + saveChatDebounced(); + } + } + this.updateCombatOverScreen(true, 'chat'); + } + + // Save encounter log + const context = getContext(); + if (context.chatId) { + saveEncounterLog(context.chatId, { + log: currentEncounter.encounterLog, + summary: summary, + result: result + }); + } + } else { + this.updateCombatOverScreen(false); + } + + } catch (error) { + console.error('[RPG Companion] Error ending combat:', error); + this.updateCombatOverScreen(false); + } + } + + /** + * Determines which character should narrate the combat summary + * Priority: Narrator character > First active group member > Current character + * @returns {string} Character name to use for /sendas + */ + getCombatNarrator() { + // 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) { + const disabledMembers = group?.disabled_members || []; + + // First priority: Look for a character named "Narrator" or "GM" + const narrator = groupMembers.find(member => + member && member.name && + !disabledMembers.includes(member.avatar) && + (member.name.toLowerCase() === 'narrator' || + member.name.toLowerCase() === 'gm' || + member.name.toLowerCase() === 'game master') + ); + + if (narrator) { + return narrator.name; + } + + // Second priority: First active (non-muted) group member + const firstActive = groupMembers.find(member => + member && member.name && + !disabledMembers.includes(member.avatar) + ); + + if (firstActive) { + return firstActive.name; + } + } + } + + // Fallback: Use current character + if (this_chid !== undefined && characters && characters[this_chid]) { + return characters[this_chid].name; + } + + // Last resort: Generic narrator + return 'Narrator'; + } + + /** + * Shows the combat over screen + * @param {string} result - Combat result ('victory', 'defeat', 'fled', 'interrupted') + */ + showCombatOverScreen(result) { + const mainContent = this.modal.querySelector('#rpg-encounter-main'); + if (!mainContent) return; + + const resultIcons = { + victory: 'fa-trophy', + defeat: 'fa-skull-crossbones', + fled: 'fa-person-running', + interrupted: 'fa-flag-checkered' + }; + + const resultColors = { + victory: '#4caf50', + defeat: '#e94560', + fled: '#ff9800', + interrupted: '#888' + }; + + const icon = resultIcons[result] || 'fa-flag-checkered'; + const color = resultColors[result] || '#888'; + + mainContent.innerHTML = ` +Generating combat summary...
+Failed to create summary. You can close this window.
+ + `; + + // Add click handler for close button + const closeBtn = overScreen.querySelector('#rpg-encounter-close-final'); + if (closeBtn) { + closeBtn.addEventListener('click', () => { + this.close(); + }); + } + } + } + + /** + * Shows a loading state + * @param {string} message - Loading message + */ + showLoadingState(message) { + const loadingContent = this.modal.querySelector('#rpg-encounter-loading'); + const mainContent = this.modal.querySelector('#rpg-encounter-main'); + + if (loadingContent) { + loadingContent.querySelector('p').textContent = message; + loadingContent.style.display = 'flex'; + } + + if (mainContent) { + mainContent.style.display = 'none'; + } + } + + /** + * Shows an error message + * @param {string} message - Error message + */ + showError(message) { + const loadingContent = this.modal.querySelector('#rpg-encounter-loading'); + + if (loadingContent) { + loadingContent.innerHTML = ` + +${message}
+ `; + } + } + + /** + * Shows an error message with a regenerate button + * @param {string} message - Error message to display + */ + showErrorWithRegenerate(message) { + const loadingContent = this.modal.querySelector('#rpg-encounter-loading'); + const combatContent = this.modal.querySelector('#rpg-encounter-content'); + + // Hide combat content if visible + if (combatContent) { + combatContent.style.display = 'none'; + } + + // Show error in loading area + if (loadingContent) { + loadingContent.style.display = 'flex'; + loadingContent.innerHTML = ` +${message}
+${message}
+