Files
rpg-companion-sillytavern/src/systems/rendering/characterStateRenderer.js
T

374 lines
13 KiB
JavaScript

/**
* Character State Rendering Module
* Displays character state information in the UI
*/
import { getCharacterState } from '../../core/characterState.js';
/**
* Renders the character's emotional state section
* @param {Object} $container - jQuery container element
*/
export function renderEmotionalState($container) {
if (!$container || !$container.length) return;
const charState = getCharacterState();
const charName = charState.characterName || 'Character';
let html = `<div class="rpg-character-emotions">`;
html += `<h4>${charName}'s Emotional State</h4>`;
// Get active emotional states (>10 intensity)
const activeEmotions = Object.entries(charState.secondaryStates)
.filter(([key, value]) => value > 10)
.sort((a, b) => b[1] - a[1]) // Sort by intensity
.slice(0, 8); // Show top 8
if (activeEmotions.length > 0) {
html += `<div class="rpg-emotion-list">`;
for (const [emotion, value] of activeEmotions) {
const emotionLabel = formatEmotionName(emotion);
const emotionColor = getEmotionColor(emotion, value);
const barWidth = value;
html += `<div class="rpg-emotion-item">`;
html += `<span class="rpg-emotion-label">${emotionLabel}</span>`;
html += `<div class="rpg-stat-bar-container">`;
html += `<div class="rpg-stat-bar" style="width: ${barWidth}%; background-color: ${emotionColor};"></div>`;
html += `</div>`;
html += `<span class="rpg-emotion-value">${value}</span>`;
html += `</div>`;
}
html += `</div>`;
} else {
html += `<p class="rpg-neutral-state">Emotionally neutral</p>`;
}
html += `</div>`;
$container.html(html);
}
/**
* Renders the character's physical condition section
* @param {Object} $container - jQuery container element
*/
export function renderPhysicalCondition($container) {
if (!$container || !$container.length) return;
const charState = getCharacterState();
const stats = charState.physicalStats;
let html = `<div class="rpg-physical-condition">`;
html += `<h4>Physical Condition</h4>`;
html += `<div class="rpg-physical-stats">`;
const displayStats = [
{ key: 'health', label: 'Health', icon: '❤️' },
{ key: 'energy', label: 'Energy', icon: '⚡' },
{ key: 'hunger', label: 'Hunger', icon: '🍽️' },
{ key: 'arousal', label: 'Arousal', icon: '🔥' }
];
for (const stat of displayStats) {
const value = stats[stat.key] !== undefined ? stats[stat.key] : 50;
const color = getStatColor(stat.key, value);
html += `<div class="rpg-physical-stat-item">`;
html += `<span class="rpg-stat-icon">${stat.icon}</span>`;
html += `<span class="rpg-stat-label">${stat.label}</span>`;
html += `<div class="rpg-stat-bar-container">`;
html += `<div class="rpg-stat-bar" style="width: ${value}%; background-color: ${color};"></div>`;
html += `</div>`;
html += `<span class="rpg-stat-value">${value}%</span>`;
html += `</div>`;
}
html += `</div>`;
html += `</div>`;
$container.html(html);
}
/**
* Renders the character's relationships section
* @param {Object} $container - jQuery container element
*/
export function renderRelationships($container) {
if (!$container || !$container.length) return;
const charState = getCharacterState();
const charName = charState.characterName || 'Character';
const relationships = charState.relationships;
let html = `<div class="rpg-relationships">`;
html += `<h4>${charName}'s Relationships</h4>`;
const relationshipEntries = Object.entries(relationships);
if (relationshipEntries.length > 0) {
html += `<div class="rpg-relationship-list">`;
for (const [npcName, relData] of relationshipEntries) {
// Only show relationships with some significance
if (relData.trust < 20 && relData.love < 10 && relData.attraction < 10) {
continue;
}
html += `<div class="rpg-relationship-card">`;
html += `<div class="rpg-relationship-header">`;
html += `<strong>${npcName}</strong>`;
html += `<span class="rpg-relationship-status">${relData.relationshipStatus || 'Acquaintance'}</span>`;
html += `</div>`;
// Show key stats
html += `<div class="rpg-relationship-stats">`;
if (relData.trust > 20) {
html += `<span class="rpg-rel-stat">Trust: ${relData.trust}</span>`;
}
if (relData.love > 10) {
html += `<span class="rpg-rel-stat">Love: ${relData.love}❤️</span>`;
}
if (relData.attraction > 10) {
html += `<span class="rpg-rel-stat">Attraction: ${relData.attraction}✨</span>`;
}
html += `</div>`;
// Show current thoughts
if (relData.currentThoughts) {
html += `<div class="rpg-relationship-thoughts">`;
html += `<em>"${relData.currentThoughts}"</em>`;
html += `</div>`;
}
html += `</div>`;
}
html += `</div>`;
} else {
html += `<p class="rpg-no-relationships">No significant relationships yet</p>`;
}
html += `</div>`;
$container.html(html);
}
/**
* Renders the character's internal thoughts section
* @param {Object} $container - jQuery container element
*/
export function renderInternalThoughts($container) {
if (!$container || !$container.length) return;
const charState = getCharacterState();
const charName = charState.characterName || 'Character';
const thoughts = charState.thoughts;
let html = `<div class="rpg-internal-thoughts">`;
html += `<h4>${charName}'s Thoughts</h4>`;
if (thoughts.internalMonologue) {
html += `<div class="rpg-thought-bubble">`;
html += `<p>"${thoughts.internalMonologue}"</p>`;
html += `</div>`;
} else {
html += `<p class="rpg-no-thoughts"><em>No current thoughts</em></p>`;
}
html += `</div>`;
$container.html(html);
}
/**
* Renders the character's current context (location, time, etc.)
* @param {Object} $container - jQuery container element
*/
export function renderContext($container) {
if (!$container || !$container.length) return;
const charState = getCharacterState();
const context = charState.contextInfo;
let html = `<div class="rpg-context">`;
html += `<h4>Current Scene</h4>`;
html += `<div class="rpg-context-info">`;
if (context.location) {
html += `<div class="rpg-context-item">`;
html += `<span class="rpg-context-icon">📍</span>`;
html += `<span class="rpg-context-label">Location:</span>`;
html += `<span class="rpg-context-value">${context.location}</span>`;
html += `</div>`;
}
if (context.timeOfDay) {
html += `<div class="rpg-context-item">`;
html += `<span class="rpg-context-icon">🕐</span>`;
html += `<span class="rpg-context-label">Time:</span>`;
html += `<span class="rpg-context-value">${context.timeOfDay}</span>`;
html += `</div>`;
}
if (context.presentCharacters && context.presentCharacters.length > 0) {
html += `<div class="rpg-context-item">`;
html += `<span class="rpg-context-icon">👥</span>`;
html += `<span class="rpg-context-label">Present:</span>`;
html += `<span class="rpg-context-value">${context.presentCharacters.join(', ')}</span>`;
html += `</div>`;
}
html += `</div>`;
html += `</div>`;
$container.html(html);
}
/**
* Renders a comprehensive character state overview
* @param {Object} $container - jQuery container element
*/
export function renderCharacterStateOverview($container) {
if (!$container || !$container.length) return;
const charState = getCharacterState();
const charName = charState.characterName || 'Character';
let html = `<div class="rpg-character-overview">`;
html += `<h3>📊 ${charName}'s State</h3>`;
// Create tabbed sections
html += `<div class="rpg-character-tabs">`;
html += `<button class="rpg-tab-btn active" data-tab="emotions">Emotions</button>`;
html += `<button class="rpg-tab-btn" data-tab="physical">Physical</button>`;
html += `<button class="rpg-tab-btn" data-tab="relationships">Relationships</button>`;
html += `<button class="rpg-tab-btn" data-tab="thoughts">Thoughts</button>`;
html += `<button class="rpg-tab-btn" data-tab="context">Context</button>`;
html += `</div>`;
// Tab contents
html += `<div class="rpg-tab-content">`;
html += `<div id="rpg-tab-emotions" class="rpg-tab-pane active"></div>`;
html += `<div id="rpg-tab-physical" class="rpg-tab-pane"></div>`;
html += `<div id="rpg-tab-relationships" class="rpg-tab-pane"></div>`;
html += `<div id="rpg-tab-thoughts" class="rpg-tab-pane"></div>`;
html += `<div id="rpg-tab-context" class="rpg-tab-pane"></div>`;
html += `</div>`;
html += `</div>`;
$container.html(html);
// Render individual sections
renderEmotionalState($('#rpg-tab-emotions'));
renderPhysicalCondition($('#rpg-tab-physical'));
renderRelationships($('#rpg-tab-relationships'));
renderInternalThoughts($('#rpg-tab-thoughts'));
renderContext($('#rpg-tab-context'));
// Set up tab switching
setupTabs();
}
/**
* Sets up tab switching functionality
*/
function setupTabs() {
$('.rpg-tab-btn').off('click').on('click', function() {
const tabName = $(this).data('tab');
// Update active button
$('.rpg-tab-btn').removeClass('active');
$(this).addClass('active');
// Update active pane
$('.rpg-tab-pane').removeClass('active');
$(`#rpg-tab-${tabName}`).addClass('active');
});
}
/**
* Helper function to format emotion names for display
* @param {string} emotion - Raw emotion key
* @returns {string} Formatted emotion name
*/
function formatEmotionName(emotion) {
// Convert camelCase to Title Case
return emotion
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase())
.trim();
}
/**
* Helper function to get color for an emotion based on its type and intensity
* @param {string} emotion - Emotion type
* @param {number} value - Emotion intensity (0-100)
* @returns {string} CSS color
*/
function getEmotionColor(emotion, value) {
const intensity = value / 100;
// Color mappings for different emotions
const emotionColors = {
happy: `rgba(76, 175, 80, ${0.5 + intensity * 0.5})`, // Green
sad: `rgba(96, 125, 139, ${0.5 + intensity * 0.5})`, // Blue-grey
angry: `rgba(244, 67, 54, ${0.5 + intensity * 0.5})`, // Red
anxious: `rgba(255, 152, 0, ${0.5 + intensity * 0.5})`, // Orange
horny: `rgba(233, 30, 99, ${0.5 + intensity * 0.5})`, // Pink
confident: `rgba(63, 81, 181, ${0.5 + intensity * 0.5})`, // Indigo
scared: `rgba(121, 85, 72, ${0.5 + intensity * 0.5})`, // Brown
playful: `rgba(255, 193, 7, ${0.5 + intensity * 0.5})` // Amber
};
return emotionColors[emotion] || `rgba(158, 158, 158, ${0.5 + intensity * 0.5})`;
}
/**
* Helper function to get color for a physical stat
* @param {string} statKey - Stat key
* @param {number} value - Stat value (0-100)
* @returns {string} CSS color
*/
function getStatColor(statKey, value) {
// For most stats, green is high, red is low
// For hunger and arousal, yellow/orange might be more appropriate
if (statKey === 'hunger') {
if (value < 30) return '#4CAF50'; // Green (not hungry)
if (value < 60) return '#FFC107'; // Yellow (getting hungry)
return '#F44336'; // Red (very hungry)
}
if (statKey === 'arousal') {
if (value < 30) return '#9E9E9E'; // Grey (low)
if (value < 70) return '#E91E63'; // Pink (moderate)
return '#880E4F'; // Dark pink (high)
}
// Default: green for high, red for low
if (value > 70) return '#4CAF50'; // Green
if (value > 40) return '#FFC107'; // Yellow
return '#F44336'; // Red
}
/**
* Updates character state display
* Call this after parsing an LLM response to update the UI
*/
export function updateCharacterStateDisplay() {
console.log('[Character State Renderer] 🎭 updateCharacterStateDisplay called');
// Find the main container
const $mainContainer = $('#rpg-character-state-container');
console.log('[Character State Renderer] Container found:', $mainContainer && $mainContainer.length > 0);
if ($mainContainer && $mainContainer.length) {
console.log('[Character State Renderer] ✅ Rendering character state overview');
renderCharacterStateOverview($mainContainer);
} else {
console.warn('[Character State Renderer] ❌ Container #rpg-character-state-container not found in DOM');
}
}