ravis-adventure
Version:
A profound CLI consciousness exploration platform featuring 220+ scenes of AI ethics, human-AI collaboration, and philosophical inquiry with Ravi, your meta-aware AI companion
730 lines (623 loc) • 23.2 kB
JavaScript
/**
* @fileoverview Unified Narrative Controller for Ravi's Adventure
* Integrates DialogueSystem, StoryEngine, Character Management, and Story Progression
* Provides centralized API for all narrative interactions and cross-system event handling
*/
import DialogueSystem from './dialogue-system.js'
import StoryEngine from './story-engine.js'
/**
* Central controller for all narrative systems in Ravi's Adventure
* Coordinates between DialogueSystem, StoryEngine, Character (Ravi), and StoryManager
*/
class NarrativeController {
constructor(gameEngine) {
this.gameEngine = gameEngine
this.gameState = gameEngine.gameState
// Initialize core systems
this.storyEngine = new StoryEngine()
this.dialogueSystem = new DialogueSystem(this.storyEngine, this.gameState)
// Character and story manager will be injected from gameEngine
this.characterManager = null // Will be set to gameEngine.ravi
this.storyManager = null // Will be set to gameEngine.storyManager
// Event system for cross-system communication
this.eventListeners = new Map()
this.eventQueue = []
// Narrative state tracking
this.narrativeFlags = new Map()
this.activeConversation = null
this.currentNarrativeContext = {}
// Integration hooks
this.initializeEventSystem()
this.setupCrossSystemIntegration()
}
/**
* Initialize the narrative controller with external systems
* @param {Object} ravi - Ravi character instance
* @param {Object} storyManager - Story manager instance
*/
initialize(ravi, storyManager) {
this.characterManager = ravi
this.storyManager = storyManager
// Setup bidirectional communication
this.setupCharacterIntegration()
this.setupStoryProgressionIntegration()
this.emit('narrative-controller-initialized', {
systems: ['DialogueSystem', 'StoryEngine', 'CharacterManager', 'StoryManager']
})
}
/**
* Initialize the event system for cross-system communication
*/
initializeEventSystem() {
// Core narrative events
this.addEventListener('dialogue-triggered', this.handleDialogueEvent.bind(this))
this.addEventListener('story-choice-made', this.handleStoryChoiceEvent.bind(this))
this.addEventListener('character-relationship-changed', this.handleRelationshipEvent.bind(this))
this.addEventListener('story-progress-updated', this.handleStoryProgressEvent.bind(this))
this.addEventListener('narrative-context-changed', this.handleContextChangeEvent.bind(this))
}
/**
* Setup cross-system integration patterns
*/
setupCrossSystemIntegration() {
// Story engine integration with dialogue system
this.storyEngine.registerChoiceCallback('dialogue_response', async (choiceData) => {
return this.generateContextualDialogue(choiceData)
})
// Dialogue system personality updates affect story progression
this.storyEngine.registerChoiceCallback('personality_update', async (choiceData) => {
return this.updatePersonalityFromStory(choiceData)
})
}
/**
* Setup character integration with narrative systems
*/
setupCharacterIntegration() {
if (!this.characterManager) return
// Enhance Ravi's responses with story context
const originalHandleConversation = this.characterManager.handleConversation.bind(this.characterManager)
this.characterManager.handleConversation = (message) => {
// Get enhanced response with story context
const enhancedResponse = this.generateStoryAwareResponse(message)
if (enhancedResponse) {
return enhancedResponse
}
return originalHandleConversation(message)
}
// Track relationship changes for story impact
const originalAdjustRelationship = this.characterManager.adjustRelationship.bind(this.characterManager)
this.characterManager.adjustRelationship = (amount) => {
const oldLevel = this.characterManager.personality.relationshipLevel
originalAdjustRelationship(amount)
const newLevel = this.characterManager.personality.relationshipLevel
this.emit('character-relationship-changed', {
oldLevel,
newLevel,
change: amount,
character: 'ravi'
})
}
}
/**
* Setup story progression integration
*/
setupStoryProgressionIntegration() {
if (!this.storyManager) return
// Enhanced story progression with narrative context
const originalUpdateProgress = this.storyManager.updateProgress.bind(this.storyManager)
this.storyManager.updateProgress = () => {
const progressMade = originalUpdateProgress()
if (progressMade) {
this.emit('story-progress-updated', {
currentStory: this.storyManager.currentStory,
objectives: this.storyManager.getCurrentObjectives(),
timestamp: Date.now()
})
}
return progressMade
}
}
/**
* Generate dialogue response with full narrative context
* @param {string} trigger - Dialogue trigger
* @param {Object} context - Additional context information
* @returns {Object} Enhanced dialogue response
*/
generateDialogue(trigger, context = {}) {
// Enrich context with narrative state
const enrichedContext = {
...context,
storyProgress: this.getStoryProgress(),
characterRelationships: this.getCharacterRelationships(),
narrativeFlags: Object.fromEntries(this.narrativeFlags),
currentStoryContext: this.getCurrentStoryContext()
}
// Generate base dialogue response
const dialogueResponse = this.dialogueSystem.generateResponse(trigger, enrichedContext)
// Enhance with story-specific elements
const storyEnhancement = this.getStorySpecificDialogue(trigger, enrichedContext)
if (storyEnhancement) {
dialogueResponse.text += ` ${storyEnhancement}`
dialogueResponse.storyEnhanced = true
}
// Add meta-narrative elements if appropriate
const metaElements = this.generateMetaNarrativeElements(trigger, enrichedContext)
if (metaElements) {
dialogueResponse.metaElements = metaElements
}
this.emit('dialogue-generated', {
trigger,
response: dialogueResponse,
context: enrichedContext
})
return dialogueResponse
}
/**
* Process story choice with full narrative integration
* @param {string} choiceId - Choice identifier
* @param {Object} choiceData - Choice data and context
* @returns {Object} Choice result with narrative consequences
*/
async processStoryChoice(choiceId, choiceData = {}) {
// Pre-choice narrative context
const preChoiceContext = this.getCurrentNarrativeContext()
// Make the choice through story engine
const storyResult = await this.storyEngine.makeChoice(choiceId, choiceData)
// Generate dialogue response to choice
const dialogueResponse = this.generateDialogue('story_choice_made', {
choiceId,
choiceData,
storyResult,
preChoiceContext
})
// Update narrative context
this.updateNarrativeContext({
lastChoice: { choiceId, choiceData, timestamp: Date.now() },
storyResult,
dialogueResponse
})
// Emit integrated event
this.emit('story-choice-made', {
choiceId,
choiceData,
storyResult,
dialogueResponse,
narrativeContext: this.getCurrentNarrativeContext()
})
// Update story progression
if (this.storyManager) {
this.storyManager.updateProgress()
}
return {
storyResult,
dialogueResponse,
narrativeContext: this.getCurrentNarrativeContext()
}
}
/**
* Handle game events with full narrative response
* @param {string} eventType - Type of game event
* @param {Object} eventData - Event-specific data
* @returns {Object} Comprehensive narrative response
*/
handleGameEvent(eventType, eventData = {}) {
const responses = []
// Get dialogue system response
const dialogueResponse = this.dialogueSystem.respondToGameEvent(eventType, eventData)
if (dialogueResponse) {
responses.push({
type: 'dialogue',
source: 'DialogueSystem',
response: dialogueResponse
})
}
// Get character-specific response if available
if (this.characterManager && this.characterManager.respondToGameEvent) {
const characterResponse = this.characterManager.respondToGameEvent(eventType, eventData)
if (characterResponse) {
responses.push({
type: 'character',
source: 'CharacterManager',
response: characterResponse
})
}
}
// Check for story-specific reactions
const storyReaction = this.getStoryReactionToEvent(eventType, eventData)
if (storyReaction) {
responses.push({
type: 'story',
source: 'StoryEngine',
response: storyReaction
})
}
// Emit comprehensive event
this.emit('game-event-processed', {
eventType,
eventData,
responses,
timestamp: Date.now()
})
return {
eventType,
responses,
primary: responses[0] || null
}
}
/**
* Start an interactive conversation with narrative context
* @param {Object} conversationOptions - Conversation configuration
*/
startConversation(conversationOptions = {}) {
const context = {
...conversationOptions,
storyContext: this.getCurrentStoryContext(),
characterState: this.getCharacterState(),
narrativeHistory: this.getNarrativeHistory()
}
this.activeConversation = {
startTime: Date.now(),
context,
messages: []
}
// Generate context-aware conversation starter
const starter = this.generateContextualConversationStarter(context)
this.emit('conversation-started', {
context,
starter
})
return starter
}
/**
* Process conversation message with full narrative integration
* @param {string} message - User message
* @returns {Object} Integrated conversation response
*/
processConversationMessage(message) {
if (!this.activeConversation) {
this.startConversation()
}
// Record message
this.activeConversation.messages.push({
type: 'user',
message,
timestamp: Date.now()
})
// Generate integrated response
const dialogueResponse = this.generateDialogue('conversation', {
message,
conversationHistory: this.activeConversation.messages,
storyContext: this.getCurrentStoryContext()
})
// Get character response if different system
let characterResponse = null
if (this.characterManager && this.characterManager.handleConversation) {
characterResponse = this.characterManager.handleConversation(message)
}
// Combine responses intelligently
const integratedResponse = this.integrateConversationResponses(
dialogueResponse,
characterResponse,
message
)
// Record response
this.activeConversation.messages.push({
type: 'system',
response: integratedResponse,
timestamp: Date.now()
})
this.emit('conversation-message-processed', {
message,
response: integratedResponse,
conversationState: this.activeConversation
})
return integratedResponse
}
/**
* Get current story progress information
* @returns {Object} Story progress data
*/
getStoryProgress() {
if (!this.storyManager) return {}
return {
currentStory: this.storyManager.currentStory,
objectives: this.storyManager.getCurrentObjectives(),
availableStories: this.storyManager.getAvailableStories(),
storyState: this.storyManager.getStoryState(),
storyEngineState: this.storyEngine.getStoryState()
}
}
/**
* Get character relationship information
* @returns {Object} Character relationship data
*/
getCharacterRelationships() {
const relationships = {}
if (this.characterManager) {
relationships.ravi = {
level: this.characterManager.personality.relationshipLevel,
mood: this.characterManager.personality.mood,
traits: this.characterManager.personality.traits,
memory: Object.fromEntries(this.characterManager.personality.memory || new Map())
}
}
// Include story engine character relationships
if (this.storyEngine.characterRelationships) {
Object.assign(relationships, this.storyEngine.characterRelationships)
}
return relationships
}
/**
* Get current story context for narrative decisions
* @returns {Object} Current story context
*/
getCurrentStoryContext() {
return {
currentScene: this.storyEngine.getCurrentScene(),
storyFlags: Object.fromEntries(this.storyEngine.globalFlags || new Map()),
storyHistory: this.storyEngine.storyHistory || [],
inventory: this.storyEngine.getInventory ? this.storyEngine.getInventory() : {},
narrativeFlags: Object.fromEntries(this.narrativeFlags)
}
}
/**
* Get comprehensive character state
* @returns {Object} Character state information
*/
getCharacterState() {
if (!this.characterManager) return {}
return {
personality: this.characterManager.personality,
stats: this.characterManager.getStats ? this.characterManager.getStats() : {},
dialogueState: this.dialogueSystem.getPersonalityState()
}
}
/**
* Get narrative history for context
* @returns {Array} Narrative history events
*/
getNarrativeHistory() {
return [
...(this.dialogueSystem.getConversationHistory ? this.dialogueSystem.getConversationHistory(20) : []),
...(this.storyEngine.storyHistory || []).slice(-10),
...this.eventQueue.slice(-10)
].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0))
}
/**
* Update narrative context
* @param {Object} contextUpdate - Context updates to apply
*/
updateNarrativeContext(contextUpdate) {
this.currentNarrativeContext = {
...this.currentNarrativeContext,
...contextUpdate,
lastUpdated: Date.now()
}
this.emit('narrative-context-changed', {
update: contextUpdate,
fullContext: this.currentNarrativeContext
})
}
/**
* Get current comprehensive narrative context
* @returns {Object} Complete narrative context
*/
getCurrentNarrativeContext() {
return {
...this.currentNarrativeContext,
story: this.getCurrentStoryContext(),
character: this.getCharacterState(),
dialogue: {
mood: this.dialogueSystem.currentMood,
history: this.dialogueSystem.getConversationHistory ? this.dialogueSystem.getConversationHistory(5) : []
},
timestamp: Date.now()
}
}
// Event system methods
addEventListener(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, [])
}
this.eventListeners.get(event).push(callback)
}
removeEventListener(event, callback) {
if (this.eventListeners.has(event)) {
const callbacks = this.eventListeners.get(event)
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
}
}
emit(event, data) {
// Queue event for history
this.eventQueue.push({
event,
data,
timestamp: Date.now()
})
// Keep queue manageable
if (this.eventQueue.length > 100) {
this.eventQueue.shift()
}
// Execute listeners
if (this.eventListeners.has(event)) {
this.eventListeners.get(event).forEach(callback => {
try {
callback(data)
} catch (error) {
console.error(`Error in event listener for ${event}:`, error)
}
})
}
}
// Helper methods for narrative enhancement
generateStoryAwareResponse(message) {
const storyContext = this.getCurrentStoryContext()
const objectives = this.storyManager ? this.storyManager.getCurrentObjectives() : []
// Check if message relates to current objectives
const relevantObjective = objectives.find(obj =>
message.toLowerCase().includes(obj.name.toLowerCase()) ||
message.toLowerCase().includes(obj.objective.toLowerCase())
)
if (relevantObjective) {
return this.generateObjectiveRelatedResponse(message, relevantObjective)
}
return null
}
generateObjectiveRelatedResponse(message, objective) {
const responses = [
`Ah, speaking of "${objective.name}"! That's actually one of our current objectives: ${objective.objective}`,
`Interesting that you mention that - it relates to what we're working on: ${objective.objective}`,
`You're thinking about "${objective.name}"? Perfect timing! That's exactly what we need to focus on.`
]
const response = responses[Math.floor(Math.random() * responses.length)]
if (this.characterManager) {
console.log(`\nRavi: "${response}"`)
this.characterManager.personality.responseCount++
this.characterManager.adjustRelationship(2)
}
return {
text: response,
objectiveRelated: true,
objective
}
}
getStorySpecificDialogue(trigger, context) {
if (!this.storyManager) return null
const currentStory = this.storyManager.currentStory
const storySpecificResponses = {
'tutorial': {
'help_request': 'Since we\'re just getting started, don\'t worry about making mistakes. This tutorial is all about learning!',
'good_choice': 'You\'re getting the hang of this tutorial really well!'
},
'bug-hunt': {
'confusion': 'Confusion is normal when hunting bugs - they\'re sneaky little things!',
'help_request': 'Bug hunting requires patience. Let\'s methodically work through this.'
}
}
return storySpecificResponses[currentStory]?.[trigger] || null
}
generateMetaNarrativeElements(trigger, context) {
// Add meta-narrative elements based on narrative controller state
const elements = []
// Integration awareness
if (Math.random() < 0.1) {
elements.push('(The narrative systems are working together seamlessly)')
}
// Context awareness
if (context.storyProgress && Math.random() < 0.15) {
elements.push(`(Currently tracking ${context.storyProgress.objectives?.length || 0} objectives)`)
}
return elements.length > 0 ? elements : null
}
getStoryReactionToEvent(eventType, eventData) {
// Generate story-specific reactions to game events
const currentScene = this.storyEngine.getCurrentScene()
if (!currentScene) return null
const reactions = {
'achievement_unlocked': () => `The story world acknowledges your achievement: "${eventData.achievementName}"`,
'inventory_full': () => 'The narrative weight of your possessions is becoming quite heavy...',
'save_game': () => 'Your story progress has been preserved in the digital archives.'
}
const reactionGenerator = reactions[eventType]
return reactionGenerator ? { text: reactionGenerator() } : null
}
generateContextualConversationStarter(context) {
const starters = [
'Given everything that\'s happening in our story, what\'s on your mind?',
'With all the narrative threads we\'re following, I\'m curious about your thoughts.',
'Considering our current objectives and story progress, what would you like to discuss?'
]
return {
text: starters[Math.floor(Math.random() * starters.length)],
contextual: true,
context
}
}
integrateConversationResponses(dialogueResponse, characterResponse, message) {
// Intelligently combine responses from different systems
if (!characterResponse) return dialogueResponse
// Prefer character response for personal interactions
if (message.toLowerCase().includes('you') || message.toLowerCase().includes('your')) {
return characterResponse
}
// Prefer dialogue response for story-related topics
if (dialogueResponse.storyEnhanced) {
return dialogueResponse
}
// Default to character response
return characterResponse
}
// Event handlers
handleDialogueEvent(data) {
this.updateNarrativeContext({
lastDialogue: data,
dialogueCount: (this.currentNarrativeContext.dialogueCount || 0) + 1
})
}
handleStoryChoiceEvent(data) {
this.updateNarrativeContext({
lastChoice: data,
choiceCount: (this.currentNarrativeContext.choiceCount || 0) + 1
})
}
handleRelationshipEvent(data) {
// Potentially trigger story events based on relationship changes
if (data.newLevel >= 50 && data.oldLevel < 50) {
this.storyEngine.setFlag('ravi_friendship_achieved', true)
}
if (data.newLevel >= 75 && data.oldLevel < 75) {
this.storyEngine.setFlag('ravi_close_bond', true)
}
}
handleStoryProgressEvent(data) {
// Update dialogue system with story progress
this.dialogueSystem.metaKnowledge.set('current_progress', {
responses: [
`We're making great progress on "${data.currentStory}"!`,
`${data.objectives.length} objectives remaining in our current story.`,
'The story is evolving based on our choices and interactions.'
]
})
}
handleContextChangeEvent(data) {
// React to narrative context changes
console.log(`[NarrativeController] Context updated: ${Object.keys(data.update).join(', ')}`)
}
// Save/Load support
getNarrativeState() {
return {
narrativeFlags: Object.fromEntries(this.narrativeFlags),
currentNarrativeContext: this.currentNarrativeContext,
activeConversation: this.activeConversation,
eventHistory: this.eventQueue.slice(-20), // Keep recent events
dialogueState: this.dialogueSystem.getPersonalityState ? this.dialogueSystem.getPersonalityState() : {},
storyEngineState: this.storyEngine.getStoryState()
}
}
async loadNarrativeState(state) {
if (!state) return
this.narrativeFlags = new Map(Object.entries(state.narrativeFlags || {}))
this.currentNarrativeContext = state.currentNarrativeContext || {}
this.activeConversation = state.activeConversation || null
this.eventQueue = state.eventHistory || []
// Restore dialogue system state
if (state.dialogueState && this.dialogueSystem.resetPersonality) {
this.dialogueSystem.resetPersonality()
if (state.dialogueState.traits) {
Object.assign(this.dialogueSystem.personalityTraits, state.dialogueState.traits)
}
if (state.dialogueState.mood) {
this.dialogueSystem.currentMood = state.dialogueState.mood
}
}
// Restore story engine state
if (state.storyEngineState) {
await this.storyEngine.loadStoryState(state.storyEngineState)
}
this.emit('narrative-state-loaded', { state })
}
}
export default NarrativeController