Revert "Merge pull request #57 from devsorcer/claude/add-character-state-tracking-01AC3zt7Z6eEYLfZXoZCgut4"

This reverts commit 8905db3e44, reversing
changes made to 628d8ee7a4.
This commit is contained in:
Spicy_Marinara
2025-12-06 00:04:32 +01:00
parent cc1dd8dc11
commit 8645bbde98
12 changed files with 5 additions and 5881 deletions
-433
View File
@@ -1,433 +0,0 @@
/**
* Character State Management Module
* Tracks comprehensive character states based on Katherine RPG system
*/
/**
* Complete character state structure
* This represents the {{char}}'s current state across all systems
*/
export let characterState = {
// Basic info
characterName: null,
// PRIMARY TRAITS (The DNA Layer) - Permanent personality traits (0-100 scale)
primaryTraits: {
// Core Disposition
dominance: 50, // 0=Pure submissive, 50=Switch, 100=Pure dominant
introversion: 50, // 0=Extreme introvert, 100=Extreme extrovert
openness: 50, // How curious and adaptable
emotionalStability: 50, // 0=Volatile, 100=Stable
conscientiousness: 50, // How organized and reliable
agreeableness: 50, // How cooperative vs competitive
neuroticism: 50, // Baseline anxiety level
riskTaking: 50, // 0=Cautious, 100=Reckless
// Sexual Personality
perversion: 50, // Comfort with taboo sexuality
exhibitionism: 50, // Desire to be seen/watched
voyeurism: 50, // Desire to watch others
sadism: 50, // Pleasure from giving pain
masochism: 50, // Pleasure from receiving pain
sexualAggression: 50, // Intensity in sex
romanticOrientation: 50, // Need for emotional connection with sex
loyalty: 50, // Monogamous vs polyamorous tendency
sexualCreativity: 50, // Imagination in sexual scenarios
modesty: 50, // 0=Shameless, 100=Modest
fertilityInstinct: 50, // Biological drive toward reproduction
sexualInitiative: 50, // How often initiates vs waits
// Moral Core
honesty: 50, // 0=Pathological liar, 100=Brutally honest
empathy: 50, // Ability to feel others' emotions
selfishness: 50, // 0=Pure altruism, 100=Pure selfishness
kindness: 50, // 0=Cruel, 100=Kind
justice: 50, // 0=Always merciful, 100=Strict justice
moralLoyalty: 50, // Devotion to person/group
integrity: 50, // 0=Pragmatic, 100=Principled
corruption: 50, // Moral degradation level
shameSensitivity: 50, // How much shame affects them
authorityRespect: 50, // Deference to hierarchy
vengefulness: 50, // Holds grudges and seeks revenge
materialismSpiritualism: 50, // 0=Pure materialism, 100=Pure spiritualism
// Intellectual Traits
intelligence: 50, // General cognitive ability
wisdom: 50, // Practical judgment
creativity: 50, // Original thinking
logicIntuition: 50, // 0=Pure intuition, 100=Pure logic
analyticalThinking: 50, // Breaking problems into components
memory: 50, // Recall ability
perception: 50, // Noticing details
curiosity: 50 // Drive to learn and explore
},
// SECONDARY STATES (The Weather Layer) - Temporary emotional states (0-100 intensity)
secondaryStates: {
// Core Emotions
happy: 50,
sad: 0,
angry: 0,
anxious: 0,
stressed: 0,
scared: 0,
disgusted: 0,
surprised: 0,
ashamed: 0,
guilty: 0,
proud: 0,
jealous: 0,
// Arousal & Sexual States
horny: 0,
sexuallyFrustrated: 0,
arousedNonSexual: 0,
cravingTouch: 0,
sensuallyStimulated: 0,
seductive: 0,
submissiveSexual: 0,
dominantSexual: 0,
// Social States
seekingValidation: 0,
lonely: 0,
needy: 0,
confident: 50,
insecure: 0,
defensive: 0,
vulnerable: 0,
aggressive: 0,
playful: 0,
curious: 50,
competitive: 0,
grateful: 0,
// Energy & Altered States
drunk: 0,
high: 0,
exhausted: 0,
energized: 50,
overstimulated: 0,
dissociating: 0,
manic: 0,
melancholic: 0,
euphoric: 0,
numb: 0
},
// BELIEFS & WORLDVIEW (The Filter Layer)
beliefs: [
// Example format:
// {
// belief: "Loyalty matters more than truth",
// strength: 85,
// stability: 75,
// category: "moral"
// }
],
// PHYSICAL STATS (The Body's Needs)
physicalStats: {
// Survival Needs
bladder: 20, // 0-100 urge to urinate
hunger: 40, // 0-100 need to eat
thirst: 30, // 0-100 need to drink
energy: 70, // 0-100 physical energy level
sleepNeed: 20, // 0-100 tiredness
// Physical Condition
health: 100, // 0-100 overall wellbeing
pain: 0, // 0-100 current pain level
arousal: 0, // 0-100 sexual arousal (detailed below)
temperatureComfort: 50, // 0=Freezing, 50=Perfect, 100=Overheating
cleanliness: 80, // 0-100 how clean they feel
// Physical Attributes (rarely change)
strength: 50,
stamina: 50,
agility: 50,
coordination: 50,
flexibility: 50
},
// SEXUAL BIOLOGY (Detailed Arousal System)
sexualBiology: {
arousalLevel: 0, // 0-100 current arousal
refractoryPeriod: false, // Currently in refractory period?
refractoryUntil: null, // Timestamp when refractory ends
ovulationDay: null, // Day of cycle (for female chars)
menstrualPhase: null, // 'menstruation', 'follicular', 'ovulation', 'luteal'
dayOfCycle: 1, // 1-28 day of menstrual cycle
lastOrgasm: null, // Timestamp of last orgasm
orgasmIntensity: 0, // 0-100 intensity of last orgasm
deprivationDays: 0 // Days since last sexual release
},
// OUTFIT/CLOTHING SYSTEM (Dynamic tracking)
clothing: {
underwear: {
bra: { worn: true, type: 'Regular bra', description: '', status: 'Worn normally', coverage: 15 },
panties: { worn: true, type: 'Regular panties', description: '', status: 'Worn normally', coverage: 10 }
},
upperBody: {
shirt: { worn: true, type: 'Blouse', description: '', status: 'Worn properly', coverage: 30 }
},
lowerBody: {
pants: { worn: true, type: 'Jeans', description: '', status: 'Worn properly', coverage: 30 }
},
outerwear: {
jacket: { worn: false, type: '', description: '', status: '', coverage: 0 }
},
footwear: {
shoes: { worn: true, type: 'Sneakers', description: '', status: 'On', coverage: 5 },
socks: { worn: true, type: 'Regular socks', description: '', status: 'On', coverage: 2 }
},
accessories: [],
totalCoverage: 92, // Sum of all coverage percentages
lastChange: null // Timestamp of last clothing change
},
// PHYSICAL STATE (Sweat, Temperature, Cleanliness)
physicalState: {
bodyTemperature: 37.0, // Celsius
heartRate: 70, // BPM
breathingRate: 14, // breaths per minute
sweatLevel: 10, // 0-100
hairCondition: 'Clean, styled',
makeupState: 'Fresh',
skinCondition: 'Soft, smooth',
marks: [], // Hickeys, bruises, scratches
scent: 'Natural (clean)'
},
// RELATIONSHIP TRACKING (Per-NPC detailed stats)
relationships: {
// Example format:
// "NPC_Name": {
// // Core Metrics
// trust: 50,
// love: 0,
// loyalty: null, // null until unlocked
// attraction: 0,
// respect: 50,
// fear: 0,
//
// // Social Dynamics
// closeness: 20,
// openness: 20,
// comfort: 50,
// dependency: 0,
//
// // Attraction Breakdown
// physicalAttraction: 0,
// emotionalAttraction: 0,
// intellectualAttraction: 0,
//
// // Sexual Dynamics
// flirtiness: 0,
// sexualCompatibility: 50,
// sexualSatisfaction: 50,
//
// // Power Dynamics
// dominanceOverThem: 50, // How dominant char is over them
// submissivenessToThem: 0, // How submissive char is to them
// possessivenessToward: 0,
//
// // Negative Feelings
// jealousyOf: 0,
// resentment: 0,
//
// // Thoughts & Notes
// currentThoughts: '', // What char is thinking about this person
// relationshipStatus: 'Acquaintance',
// lastInteraction: null
// }
},
// CONTEXTUAL INFO (Extracted from scene)
contextInfo: {
location: '',
timeOfDay: '',
weather: '',
presentCharacters: [], // List of characters currently present
recentEvents: '',
currentActivity: ''
},
// INTERNAL THOUGHTS (Character's current thoughts)
thoughts: {
internalMonologue: '', // What they're thinking right now
desires: '', // What they want in this moment
fears: '', // What they're afraid of
plans: '' // What they're planning to do
}
};
/**
* Initialize a new relationship entry for an NPC
* @param {string} npcName - Name of the NPC
* @returns {Object} Default relationship data
*/
export function initializeRelationship(npcName) {
return {
// Core Metrics
trust: 50,
love: 0,
loyalty: null,
attraction: 0,
respect: 50,
fear: 0,
// Social Dynamics
closeness: 20,
openness: 20,
comfort: 50,
dependency: 0,
// Attraction Breakdown
physicalAttraction: 0,
emotionalAttraction: 0,
intellectualAttraction: 0,
// Sexual Dynamics
flirtiness: 0,
sexualCompatibility: 50,
sexualSatisfaction: 50,
// Power Dynamics
dominanceOverThem: 50,
submissivenessToThem: 0,
possessivenessToward: 0,
// Negative Feelings
jealousyOf: 0,
resentment: 0,
// Thoughts & Notes
currentThoughts: '',
relationshipStatus: 'Stranger',
lastInteraction: new Date().toISOString()
};
}
/**
* Get or create relationship data for an NPC
* @param {string} npcName - Name of the NPC
* @returns {Object} Relationship data
*/
export function getRelationship(npcName) {
if (!characterState.relationships[npcName]) {
characterState.relationships[npcName] = initializeRelationship(npcName);
}
return characterState.relationships[npcName];
}
/**
* Update relationship data for an NPC
* @param {string} npcName - Name of the NPC
* @param {Object} updates - Partial relationship data to update
*/
export function updateRelationship(npcName, updates) {
const relationship = getRelationship(npcName);
Object.assign(relationship, updates);
relationship.lastInteraction = new Date().toISOString();
}
/**
* Set the entire character state
* @param {Object} newState - New character state object
*/
export function setCharacterState(newState) {
characterState = newState;
}
/**
* Update specific parts of character state
* @param {Object} updates - Partial character state to update
*/
export function updateCharacterState(updates) {
// Deep merge for nested objects
if (updates.primaryTraits) {
Object.assign(characterState.primaryTraits, updates.primaryTraits);
}
if (updates.secondaryStates) {
Object.assign(characterState.secondaryStates, updates.secondaryStates);
}
if (updates.physicalStats) {
Object.assign(characterState.physicalStats, updates.physicalStats);
}
if (updates.sexualBiology) {
Object.assign(characterState.sexualBiology, updates.sexualBiology);
}
if (updates.clothing) {
Object.assign(characterState.clothing, updates.clothing);
}
if (updates.physicalState) {
Object.assign(characterState.physicalState, updates.physicalState);
}
if (updates.contextInfo) {
Object.assign(characterState.contextInfo, updates.contextInfo);
}
if (updates.thoughts) {
Object.assign(characterState.thoughts, updates.thoughts);
}
if (updates.beliefs !== undefined) {
characterState.beliefs = updates.beliefs;
}
if (updates.relationships) {
Object.assign(characterState.relationships, updates.relationships);
}
if (updates.characterName !== undefined) {
characterState.characterName = updates.characterName;
}
}
/**
* Get current character state
* @returns {Object} Current character state
*/
export function getCharacterState() {
return characterState;
}
/**
* Reset character state to defaults
*/
export function resetCharacterState() {
characterState = {
characterName: null,
primaryTraits: {},
secondaryStates: {},
beliefs: [],
physicalStats: {},
sexualBiology: {},
clothing: {},
physicalState: {},
relationships: {},
contextInfo: {},
thoughts: {}
};
}
/**
* Export character state as JSON
* @returns {string} JSON string of character state
*/
export function exportCharacterState() {
return JSON.stringify(characterState, null, 2);
}
/**
* Import character state from JSON
* @param {string} jsonData - JSON string of character state
*/
export function importCharacterState(jsonData) {
try {
const imported = JSON.parse(jsonData);
characterState = imported;
return true;
} catch (error) {
console.error('[Character State] Import failed:', error);
return false;
}
}
-469
View File
@@ -1,469 +0,0 @@
/**
* Character State Parser Module
* Extracts and applies character state updates from LLM responses
*/
import {
getCharacterState,
updateCharacterState,
updateRelationship,
getRelationship
} from '../../core/characterState.js';
/**
* Extracts character state update block from LLM response
* @param {string} text - Full LLM response text
* @returns {string|null} Extracted state update block or null if not found
*/
export function extractCharacterStateBlock(text) {
if (!text) return null;
// Look for character-state code block
const stateBlockRegex = /```character-state\s*([\s\S]*?)```/i;
const match = text.match(stateBlockRegex);
if (match && match[1]) {
return match[1].trim();
}
// Fallback: look for "State Update" section
const fallbackRegex = /State Update\s*---\s*([\s\S]*?)(?=```|$)/i;
const fallbackMatch = text.match(fallbackRegex);
if (fallbackMatch && fallbackMatch[1]) {
return fallbackMatch[1].trim();
}
return null;
}
/**
* Parses emotional changes from state update text
* @param {string} stateText - State update text
* @returns {Object} Emotional state changes
*/
export function parseEmotionalChanges(stateText) {
const changes = {};
// Look for Emotional Changes section
const emotionalSection = extractSection(stateText, 'Emotional Changes');
if (!emotionalSection) return changes;
// Parse lines like "happy: +15 (reason: received compliment)"
const changeRegex = /-\s*(\w+):\s*([+-]?\d+)\s*(?:\(reason:\s*([^)]+)\))?/gi;
let match;
while ((match = changeRegex.exec(emotionalSection)) !== null) {
const emotion = match[1].toLowerCase();
const delta = parseInt(match[2]);
const reason = match[3] || '';
changes[emotion] = {
delta: delta,
reason: reason.trim()
};
}
return changes;
}
/**
* Parses physical state changes from state update text
* @param {string} stateText - State update text
* @returns {Object} Physical state changes
*/
export function parsePhysicalChanges(stateText) {
const changes = {};
// Look for Physical Changes section
const physicalSection = extractSection(stateText, 'Physical Changes');
if (!physicalSection) return changes;
// Parse lines like "Energy: -20 (reason: exhausting activity)"
const changeRegex = /-\s*(\w+):\s*([+-]?\d+)\s*(?:\(reason:\s*([^)]+)\))?/gi;
let match;
while ((match = changeRegex.exec(physicalSection)) !== null) {
const stat = match[1].toLowerCase();
const delta = parseInt(match[2]);
const reason = match[3] || '';
changes[stat] = {
delta: delta,
reason: reason.trim()
};
}
return changes;
}
/**
* Parses relationship updates from state update text
* @param {string} stateText - State update text
* @returns {Object} Relationship updates by character name
*/
export function parseRelationshipUpdates(stateText) {
const updates = {};
// Look for Relationship Updates section
const relationshipSection = extractSection(stateText, 'Relationship Updates');
if (!relationshipSection) return updates;
// Split by character entries (lines starting with "- CharacterName:")
const characterEntries = relationshipSection.split(/(?=^- )/m);
for (const entry of characterEntries) {
if (!entry.trim()) continue;
// Extract character name
const nameMatch = entry.match(/^-\s*([^:]+):/);
if (!nameMatch) continue;
const characterName = nameMatch[1].trim();
const relationshipData = {};
// Parse relationship stat changes
// Format: " - Trust: +10 (reason: showed vulnerability)"
const statRegex = /^\s*-\s*(\w+):\s*([+-]?\d+)\s*(?:\(reason:\s*([^)]+)\))?/gim;
let statMatch;
while ((statMatch = statRegex.exec(entry)) !== null) {
const stat = statMatch[1].toLowerCase();
const delta = parseInt(statMatch[2]);
const reason = statMatch[3] || '';
relationshipData[stat] = {
delta: delta,
reason: reason.trim()
};
}
// Extract thoughts
const thoughtsMatch = entry.match(/Thoughts:\s*"([^"]+)"/i);
if (thoughtsMatch) {
relationshipData.currentThoughts = thoughtsMatch[1].trim();
}
if (Object.keys(relationshipData).length > 0) {
updates[characterName] = relationshipData;
}
}
return updates;
}
/**
* Parses scene context updates from state update text
* @param {string} stateText - State update text
* @returns {Object} Context updates
*/
export function parseContextUpdates(stateText) {
const context = {};
// Look for Scene Context section
const contextSection = extractSection(stateText, 'Scene Context');
if (!contextSection) return context;
// Parse location
const locationMatch = contextSection.match(/Location:\s*([^\n]+)/i);
if (locationMatch) {
context.location = locationMatch[1].trim();
}
// Parse time
const timeMatch = contextSection.match(/Time:\s*([^\n]+)/i);
if (timeMatch) {
context.timeOfDay = timeMatch[1].trim();
}
// Parse present characters
const presentMatch = contextSection.match(/Present:\s*([^\n]+)/i);
if (presentMatch) {
const presentText = presentMatch[1].trim();
context.presentCharacters = presentText.split(',').map(s => s.trim()).filter(s => s);
}
return context;
}
/**
* Parses internal thoughts from state update text
* @param {string} stateText - State update text
* @returns {Object} Thoughts object
*/
export function parseThoughts(stateText) {
const thoughts = {};
// Look for Thoughts section
// Format: **Character's Thoughts**:\n"thought text here"
const thoughtsRegex = /\*\*[^*]+'s Thoughts\*\*:\s*"([^"]+)"/i;
const match = stateText.match(thoughtsRegex);
if (match) {
thoughts.internalMonologue = match[1].trim();
}
return thoughts;
}
/**
* Parses outfit/clothing changes from state update text
* @param {string} stateText - State update text
* @returns {Object} Clothing changes
*/
export function parseClothingChanges(stateText) {
const changes = {};
// Look for Outfit Changes section
const outfitSection = extractSection(stateText, 'Outfit Changes');
if (!outfitSection) return changes;
// Parse lines like "- shirt: removed" or "- dress: added (red cocktail dress)"
const changeRegex = /-\s*([^:]+):\s*([^\n(]+)(?:\(([^)]+)\))?/gi;
let match;
while ((match = changeRegex.exec(outfitSection)) !== null) {
const item = match[1].trim();
const action = match[2].trim();
const description = match[3] ? match[3].trim() : '';
changes[item] = {
action: action,
description: description
};
}
return changes;
}
/**
* Helper function to extract a section from state update text
* @param {string} text - Full state update text
* @param {string} sectionName - Name of section to extract
* @returns {string} Section content or empty string
*/
function extractSection(text, sectionName) {
// Match section with various formats:
// **Section Name**:
// **Section Name**
const sectionRegex = new RegExp(`\\*\\*${sectionName}\\*\\*:?\\s*([\\s\\S]*?)(?=\\*\\*|$)`, 'i');
const match = text.match(sectionRegex);
if (match && match[1]) {
return match[1].trim();
}
return '';
}
/**
* Applies emotional state changes to character state
* @param {Object} emotionalChanges - Emotional changes to apply
*/
export function applyEmotionalChanges(emotionalChanges) {
const charState = getCharacterState();
const newStates = { ...charState.secondaryStates };
for (const [emotion, change] of Object.entries(emotionalChanges)) {
if (newStates[emotion] !== undefined) {
let newValue = (newStates[emotion] || 0) + change.delta;
// Clamp between 0-100
newValue = Math.max(0, Math.min(100, newValue));
newStates[emotion] = newValue;
console.log(`[Character State] ${emotion}: ${newStates[emotion]} (${change.delta > 0 ? '+' : ''}${change.delta}) - ${change.reason}`);
}
}
updateCharacterState({ secondaryStates: newStates });
}
/**
* Applies physical state changes to character state
* @param {Object} physicalChanges - Physical changes to apply
*/
export function applyPhysicalChanges(physicalChanges) {
const charState = getCharacterState();
const newStats = { ...charState.physicalStats };
for (const [stat, change] of Object.entries(physicalChanges)) {
if (newStats[stat] !== undefined) {
let newValue = (newStats[stat] || 50) + change.delta;
// Clamp between 0-100 (or appropriate range)
newValue = Math.max(0, Math.min(100, newValue));
newStats[stat] = newValue;
console.log(`[Character State] ${stat}: ${newStats[stat]} (${change.delta > 0 ? '+' : ''}${change.delta}) - ${change.reason}`);
}
}
updateCharacterState({ physicalStats: newStats });
}
/**
* Applies relationship updates to character state
* @param {Object} relationshipUpdates - Relationship updates by character name
*/
export function applyRelationshipUpdates(relationshipUpdates) {
for (const [characterName, updates] of Object.entries(relationshipUpdates)) {
const relationship = getRelationship(characterName);
const newRelationship = { ...relationship };
// Apply delta changes
for (const [stat, change] of Object.entries(updates)) {
if (stat === 'currentThoughts') {
newRelationship.currentThoughts = change;
} else if (typeof change === 'object' && change.delta !== undefined) {
if (newRelationship[stat] !== undefined && newRelationship[stat] !== null) {
let newValue = (newRelationship[stat] || 0) + change.delta;
newValue = Math.max(0, Math.min(100, newValue));
newRelationship[stat] = newValue;
console.log(`[Character State] Relationship with ${characterName} - ${stat}: ${newValue} (${change.delta > 0 ? '+' : ''}${change.delta}) - ${change.reason}`);
}
}
}
// Update thoughts if provided
if (updates.currentThoughts) {
newRelationship.currentThoughts = updates.currentThoughts;
}
// Update the relationship
updateRelationship(characterName, newRelationship);
}
}
/**
* Main function to parse and apply all character state updates
* @param {string} responseText - Full LLM response text
* @returns {Object} Parsed state data
*/
export function parseAndApplyCharacterStateUpdate(responseText) {
console.log('[Character Parser] Parsing character state update...');
// Extract state update block
const stateBlock = extractCharacterStateBlock(responseText);
if (!stateBlock) {
console.log('[Character Parser] No character state update block found');
return null;
}
console.log('[Character Parser] Found state update block:', stateBlock.substring(0, 200));
// Parse all sections
const emotionalChanges = parseEmotionalChanges(stateBlock);
const physicalChanges = parsePhysicalChanges(stateBlock);
const relationshipUpdates = parseRelationshipUpdates(stateBlock);
const contextUpdates = parseContextUpdates(stateBlock);
const thoughts = parseThoughts(stateBlock);
const clothingChanges = parseClothingChanges(stateBlock);
// Apply changes to character state
if (Object.keys(emotionalChanges).length > 0) {
console.log('[Character Parser] Applying emotional changes:', Object.keys(emotionalChanges));
applyEmotionalChanges(emotionalChanges);
}
if (Object.keys(physicalChanges).length > 0) {
console.log('[Character Parser] Applying physical changes:', Object.keys(physicalChanges));
applyPhysicalChanges(physicalChanges);
}
if (Object.keys(relationshipUpdates).length > 0) {
console.log('[Character Parser] Applying relationship updates for:', Object.keys(relationshipUpdates));
applyRelationshipUpdates(relationshipUpdates);
}
if (Object.keys(contextUpdates).length > 0) {
console.log('[Character Parser] Updating context:', contextUpdates);
updateCharacterState({ contextInfo: contextUpdates });
}
if (Object.keys(thoughts).length > 0) {
console.log('[Character Parser] Updating thoughts');
updateCharacterState({ thoughts: thoughts });
}
// Return parsed data for display
return {
emotionalChanges,
physicalChanges,
relationshipUpdates,
contextUpdates,
thoughts,
clothingChanges,
rawStateBlock: stateBlock
};
}
/**
* Parses character initialization data from JSON
* Used when initializing character state from character card analysis
* @param {string} responseText - LLM response with JSON data
* @returns {Object|null} Parsed trait data or null if failed
*/
export function parseCharacterInitialization(responseText) {
try {
// Extract JSON block
const jsonMatch = responseText.match(/```json\s*([\s\S]*?)```/);
if (!jsonMatch) {
// Try to find JSON without code blocks
const jsonObjectMatch = responseText.match(/\{[\s\S]*\}/);
if (jsonObjectMatch) {
return JSON.parse(jsonObjectMatch[0]);
}
return null;
}
const jsonData = JSON.parse(jsonMatch[1]);
return jsonData;
} catch (error) {
console.error('[Character Parser] Failed to parse initialization data:', error);
return null;
}
}
/**
* Parses relationship analysis data from JSON
* @param {string} responseText - LLM response with JSON data
* @returns {Object|null} Parsed relationship data or null if failed
*/
export function parseRelationshipAnalysis(responseText) {
try {
// Extract JSON block
const jsonMatch = responseText.match(/```json\s*([\s\S]*?)```/);
if (!jsonMatch) {
// Try to find JSON without code blocks
const jsonObjectMatch = responseText.match(/\{[\s\S]*\}/);
if (jsonObjectMatch) {
return JSON.parse(jsonObjectMatch[0]);
}
return null;
}
const jsonData = JSON.parse(jsonMatch[1]);
return jsonData;
} catch (error) {
console.error('[Character Parser] Failed to parse relationship analysis:', error);
return null;
}
}
/**
* Cleans the LLM response by removing the character state update block
* This leaves only the actual roleplay response
* @param {string} responseText - Full LLM response
* @returns {string} Cleaned response without state update block
*/
export function removeCharacterStateBlock(responseText) {
if (!responseText) return '';
// Remove character-state code block
let cleaned = responseText.replace(/```character-state\s*[\s\S]*?```/gi, '');
// Clean up extra whitespace
cleaned = cleaned.trim();
return cleaned;
}
@@ -1,379 +0,0 @@
/**
* Character Prompt Builder Module
* Handles AI prompt generation for character state tracking
* Based on Katherine RPG System - tracks {{char}} states instead of {{user}}
*/
import { getContext } from '../../../../../../extensions.js';
import { chat, characters, this_chid } from '../../../../../../../script.js';
import { selected_group, getGroupMembers, getGroupChat } from '../../../../../../group-chats.js';
import { extensionSettings } from '../../core/state.js';
import { getCharacterState } from '../../core/characterState.js';
/**
* Gets the main character name from the current chat
* @returns {string} Character name
*/
function getCharacterName() {
if (selected_group) {
// For group chats, we'll need to track multiple characters
// For now, return the first active character
const groupMembers = getGroupMembers(selected_group);
if (groupMembers && groupMembers.length > 0) {
return groupMembers[0].name;
}
} else if (this_chid !== undefined && characters && characters[this_chid]) {
return characters[this_chid].name;
}
return 'Character';
}
/**
* Generates a summary of the current character states for LLM context
* @returns {string} Formatted character state summary
*/
export function generateCharacterStateSummary() {
const charState = getCharacterState();
const charName = charState.characterName || getCharacterName();
let summary = `=== ${charName}'s Current State ===\n\n`;
// Primary Traits (most important personality traits only)
summary += `**Core Personality Traits** (0-100 scale):\n`;
const keyTraits = {
dominance: charState.primaryTraits.dominance,
introversion: charState.primaryTraits.introversion,
emotionalStability: charState.primaryTraits.emotionalStability,
honesty: charState.primaryTraits.honesty,
empathy: charState.primaryTraits.empathy,
corruption: charState.primaryTraits.corruption
};
for (const [trait, value] of Object.entries(keyTraits)) {
if (value !== undefined && value !== null) {
summary += `- ${trait}: ${value}\n`;
}
}
summary += `\n`;
// Secondary States (current emotions)
summary += `**Current Emotional States** (0-100 intensity):\n`;
const activeStates = Object.entries(charState.secondaryStates)
.filter(([key, value]) => value > 10) // Only show non-trivial states
.sort((a, b) => b[1] - a[1]) // Sort by intensity
.slice(0, 10); // Top 10 states
if (activeStates.length > 0) {
for (const [state, value] of activeStates) {
summary += `- ${state}: ${value}\n`;
}
} else {
summary += `- (Emotionally neutral)\n`;
}
summary += `\n`;
// Physical Stats
summary += `**Physical Condition**:\n`;
summary += `- Health: ${charState.physicalStats.health || 100}%\n`;
summary += `- Energy: ${charState.physicalStats.energy || 70}%\n`;
summary += `- Hunger: ${charState.physicalStats.hunger || 40}%\n`;
summary += `- Arousal: ${charState.physicalStats.arousal || 0}%\n`;
summary += `\n`;
// Clothing Summary
if (charState.clothing && charState.clothing.totalCoverage !== undefined) {
summary += `**Current Outfit**: `;
const outfit = [];
if (charState.clothing.upperBody?.shirt?.worn) {
outfit.push(charState.clothing.upperBody.shirt.type);
}
if (charState.clothing.lowerBody?.pants?.worn) {
outfit.push(charState.clothing.lowerBody.pants.type);
}
if (outfit.length > 0) {
summary += outfit.join(', ');
} else {
summary += 'Minimal clothing';
}
summary += ` (${charState.clothing.totalCoverage}% coverage)\n\n`;
}
// Context Info
if (charState.contextInfo.location || charState.contextInfo.timeOfDay) {
summary += `**Scene Context**:\n`;
if (charState.contextInfo.location) {
summary += `- Location: ${charState.contextInfo.location}\n`;
}
if (charState.contextInfo.timeOfDay) {
summary += `- Time: ${charState.contextInfo.timeOfDay}\n`;
}
if (charState.contextInfo.presentCharacters && charState.contextInfo.presentCharacters.length > 0) {
summary += `- Present: ${charState.contextInfo.presentCharacters.join(', ')}\n`;
}
summary += `\n`;
}
// Relationships (active ones only)
const activeRelationships = Object.entries(charState.relationships)
.filter(([name, data]) => data.trust > 30 || data.love > 10 || data.attraction > 10);
if (activeRelationships.length > 0) {
summary += `**Key Relationships**:\n`;
for (const [name, rel] of activeRelationships) {
summary += `- ${name}: Trust ${rel.trust}, Love ${rel.love}, Attraction ${rel.attraction}\n`;
if (rel.currentThoughts) {
summary += ` Thoughts: "${rel.currentThoughts}"\n`;
}
}
summary += `\n`;
}
// Current Thoughts
if (charState.thoughts.internalMonologue) {
summary += `**Internal Thoughts**: "${charState.thoughts.internalMonologue}"\n\n`;
}
return summary;
}
/**
* Generates the tracking prompt for character state updates
* @returns {string} Formatted instruction text for the AI
*/
export function generateCharacterTrackingInstructions() {
const charName = getCharacterName();
const charState = getCharacterState();
let instructions = `\n=== CHARACTER STATE TRACKING ===\n\n`;
instructions += `After your response, you MUST update ${charName}'s state based on what happened in your response.\n\n`;
instructions += `Provide the updates in this exact format:\n\n`;
instructions += `\`\`\`character-state\n`;
instructions += `${charName}'s State Update\n`;
instructions += `---\n\n`;
// Emotional States Changes
instructions += `**Emotional Changes**:\n`;
instructions += `- [Emotion]: [+/- amount] (reason: [brief explanation])\n`;
instructions += `Example: "happy: +15 (reason: received compliment from {{user}})"\n`;
instructions += `Example: "anxious: -10 (reason: situation resolved peacefully)"\n`;
instructions += `(Only list emotions that changed. Use +/- notation.)\n\n`;
// Physical State Changes
instructions += `**Physical Changes**:\n`;
instructions += `- Energy: [+/- amount] (reason: [brief])\n`;
instructions += `- Arousal: [+/- amount] (reason: [brief])\n`;
instructions += `- [Other stats if changed]: [+/- amount] (reason: [brief])\n\n`;
// Relationship Changes (if applicable)
instructions += `**Relationship Updates** (if any character interactions occurred):\n`;
instructions += `- [Character Name]:\n`;
instructions += ` - Trust: [+/- amount] (reason: [brief])\n`;
instructions += ` - Love: [+/- amount] (reason: [brief])\n`;
instructions += ` - Attraction: [+/- amount] (reason: [brief])\n`;
instructions += ` - Thoughts: "[what ${charName} is thinking about this person now]"\n\n`;
// Context Updates
instructions += `**Scene Context**:\n`;
instructions += `- Location: [current location]\n`;
instructions += `- Time: [current time of day]\n`;
instructions += `- Present: [list of characters currently in scene]\n\n`;
// Internal Thoughts
instructions += `**${charName}'s Thoughts**:\n`;
instructions += `"[${charName}'s internal monologue in first person, 1-3 sentences]"\n\n`;
// Clothing Changes (if applicable)
instructions += `**Outfit Changes** (only if clothing changed):\n`;
instructions += `- [Item]: [removed/added/changed to X]\n`;
instructions += `Example: "shirt: removed", "dress: added (red cocktail dress)"\n\n`;
instructions += `\`\`\`\n\n`;
instructions += `IMPORTANT GUIDELINES:\n`;
instructions += `1. All changes should be REALISTIC and GRADUAL (+/- 1-15 for normal events, +/- 20+ only for major events)\n`;
instructions += `2. Consider ${charName}'s personality traits when determining emotional reactions\n`;
instructions += `3. Track physical needs realistically (energy decreases with activity, arousal changes with context)\n`;
instructions += `4. Relationship changes require INTERACTION - don't change relationships with characters not in the scene\n`;
instructions += `5. Internal thoughts should reflect ${charName}'s true feelings, even if different from what they say\n`;
instructions += `6. If nothing significant happened, you can note "No significant state changes"\n\n`;
return instructions;
}
/**
* Generates the full prompt for character state tracking in TOGETHER mode
* This is injected as part of the main generation
* @returns {string} Prompt text to inject
*/
export function generateCharacterTrackingPrompt() {
const charName = getCharacterName();
const stateSummary = generateCharacterStateSummary();
const instructions = generateCharacterTrackingInstructions();
let prompt = `\n--- CHARACTER STATE TRACKING ---\n\n`;
prompt += stateSummary;
prompt += instructions;
return prompt;
}
/**
* Generates the full prompt for SEPARATE character state tracking mode
* Creates a message array suitable for the generateRaw API
* @returns {Array<{role: string, content: string}>} Array of message objects for API
*/
export async function generateSeparateCharacterTrackingPrompt() {
const depth = extensionSettings.updateDepth || 4;
const charName = getCharacterName();
const userName = getContext().name1;
const charState = getCharacterState();
const messages = [];
// System message
let systemMessage = `You are a character state tracking system for an AI roleplay.\n\n`;
systemMessage += `Your ONLY job is to analyze the most recent response from ${charName} and update their internal states accordingly.\n\n`;
systemMessage += `You must track:\n`;
systemMessage += `- Emotional states (happiness, arousal, stress, etc.)\n`;
systemMessage += `- Physical condition (energy, health, hunger, etc.)\n`;
systemMessage += `- Relationships (how ${charName} feels about other characters)\n`;
systemMessage += `- Internal thoughts (what ${charName} is truly thinking)\n`;
systemMessage += `- Context (location, time, who's present)\n\n`;
systemMessage += `Be realistic and consider ${charName}'s personality when determining state changes.\n\n`;
messages.push({
role: 'system',
content: systemMessage
});
// Add current character state
const stateSummary = generateCharacterStateSummary();
messages.push({
role: 'user',
content: `Current ${charName}'s state:\n\n${stateSummary}`
});
// Add recent chat history for context
messages.push({
role: 'user',
content: `Recent conversation history (for context):\n\n`
});
const recentMessages = chat.slice(-depth);
for (const message of recentMessages) {
messages.push({
role: message.is_user ? 'user' : 'assistant',
content: `[${message.is_user ? userName : charName}]: ${message.mes}`
});
}
// Add tracking instructions
const instructions = generateCharacterTrackingInstructions();
messages.push({
role: 'user',
content: instructions + `\nProvide ONLY the character state update in the exact format specified above. Do not include any other commentary.`
});
return messages;
}
/**
* Generates a prompt for initializing character state from character card
* This is used when starting a new chat or resetting state
* @returns {string} Prompt for initialization
*/
export async function generateCharacterInitializationPrompt() {
const charName = getCharacterName();
let character = null;
if (this_chid !== undefined && characters && characters[this_chid]) {
character = characters[this_chid];
}
let prompt = `You are analyzing a character card to initialize state tracking.\n\n`;
if (character) {
prompt += `Character: ${character.name}\n\n`;
if (character.description) {
prompt += `Description:\n${character.description}\n\n`;
}
if (character.personality) {
prompt += `Personality:\n${character.personality}\n\n`;
}
if (character.scenario) {
prompt += `Scenario:\n${character.scenario}\n\n`;
}
}
prompt += `Based on this character information, provide reasonable initial values (0-100 scale) for these personality traits:\n\n`;
prompt += `\`\`\`json\n`;
prompt += `{\n`;
prompt += ` "dominance": 50,\n`;
prompt += ` "introversion": 50,\n`;
prompt += ` "emotionalStability": 50,\n`;
prompt += ` "honesty": 50,\n`;
prompt += ` "empathy": 50,\n`;
prompt += ` "corruption": 10,\n`;
prompt += ` "intelligence": 50,\n`;
prompt += ` "confidence": 50\n`;
prompt += `}\n`;
prompt += `\`\`\`\n\n`;
prompt += `Consider the character's description and personality when setting these values.\n`;
prompt += `For example:\n`;
prompt += `- A shy character would have high introversion (70-90)\n`;
prompt += `- A leader would have high dominance (70-90)\n`;
prompt += `- A kind character would have high empathy (70-90)\n\n`;
prompt += `Provide ONLY the JSON object with your estimated values.`;
return prompt;
}
/**
* Generates a relationship analysis prompt for a specific character
* Used when a new character is introduced or to analyze existing relationships
* @param {string} targetCharacterName - Name of the character to analyze relationship with
* @returns {string} Prompt for relationship analysis
*/
export function generateRelationshipAnalysisPrompt(targetCharacterName) {
const charName = getCharacterName();
const charState = getCharacterState();
let prompt = `Analyze ${charName}'s relationship with ${targetCharacterName} based on recent interactions.\n\n`;
// Add chat context
const recentMessages = chat.slice(-10).filter(msg => {
return msg.mes.toLowerCase().includes(targetCharacterName.toLowerCase());
});
if (recentMessages.length > 0) {
prompt += `Recent interactions:\n\n`;
for (const msg of recentMessages) {
prompt += `- ${msg.mes.substring(0, 200)}${msg.mes.length > 200 ? '...' : ''}\n`;
}
prompt += `\n`;
}
prompt += `Provide relationship stats (0-100 scale) in this format:\n\n`;
prompt += `\`\`\`json\n`;
prompt += `{\n`;
prompt += ` "trust": 50,\n`;
prompt += ` "love": 0,\n`;
prompt += ` "attraction": 0,\n`;
prompt += ` "respect": 50,\n`;
prompt += ` "closeness": 20,\n`;
prompt += ` "currentThoughts": "[What ${charName} thinks about ${targetCharacterName}]",\n`;
prompt += ` "relationshipStatus": "Stranger|Acquaintance|Friend|Close Friend|Lover|Enemy"\n`;
prompt += `}\n`;
prompt += `\`\`\`\n\n`;
prompt += `Consider:\n`;
prompt += `- How long they've known each other\n`;
prompt += `- Quality of interactions (positive/negative)\n`;
prompt += `- ${charName}'s personality (empathy: ${charState.primaryTraits.empathy}, trust tendency, etc.)\n`;
prompt += `- Current emotional state of ${charName}\n\n`;
prompt += `Provide ONLY the JSON object.`;
return prompt;
}
@@ -1,373 +0,0 @@
/**
* 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');
}
}