UNPKG

claude-code-tamagotchi

Version:

A virtual pet that lives in your Claude Code statusline

453 lines (371 loc) โ€ข 13.6 kB
import { StateManager, PetState } from './StateManager'; import { AnimationManager } from './AnimationManager'; import { ActivitySystem } from './ActivitySystem'; import { FeedbackSystem } from './feedback/FeedbackSystem'; import { config } from '../utils/config'; import * as fs from 'fs'; export interface PetAction { command: string; parameter?: string; timestamp: number; } export class PetEngine { private stateManager: StateManager; private animationManager: AnimationManager; private activitySystem: ActivitySystem; private feedbackSystem: FeedbackSystem; private state: PetState | null = null; private transcriptPath?: string; private sessionId?: string; constructor() { this.stateManager = new StateManager(); this.animationManager = new AnimationManager(); this.activitySystem = new ActivitySystem(); this.feedbackSystem = new FeedbackSystem(); } async initialize(): Promise<void> { this.state = await this.stateManager.load(); } async update(transcriptPath?: string, sessionId?: string): Promise<void> { if (!this.state) { await this.initialize(); } if (!this.state) { throw new Error('Failed to initialize pet state'); } // Store for later use this.transcriptPath = transcriptPath; this.sessionId = sessionId; // Check for pending actions from commands await this.checkForActions(); // Apply activity-based updates instead of time decay this.activitySystem.applyActivityUpdate(this.state); // Process feedback if enabled if (config.feedbackEnabled && transcriptPath && sessionId) { this.feedbackSystem.processFeedback(this.state, transcriptPath, sessionId); } // Check if pending action is complete if (this.state.pendingAction) { // Increment update count for the action this.state.pendingAction.updateCount = (this.state.pendingAction.updateCount || 0) + 1; // Handle bathing - increase cleanliness and show fun messages if (this.state.pendingAction.type === 'bathing') { // Increase cleanliness by 10% each update this.state.cleanliness = Math.min(100, this.state.cleanliness + 10); // Fun shower messages based on progress const showerMessages = [ `๐ŸŽต Rubber ducky, you're the one... ๐ŸŽต`, `Scrub-a-dub-dub! ๐Ÿงผ`, `๐ŸŽต Splish splash, I was taking a bath! ๐ŸŽต`, `*blows soap bubbles* ๐Ÿซง`, `Is that shampoo or ice cream? ๐Ÿค”`, `๐ŸŽต Singing in the rain... er, shower! ๐ŸŽต`, `*makes mohawk with shampoo* ๐ŸฆŽ`, `Bubble beard! I'm Santa! ๐ŸŽ…`, `Almost done... so sparkly! โœจ`, `All clean! That was fun! ๐ŸŒŸ` ]; const messageIndex = Math.min(this.state.pendingAction.updateCount - 1, showerMessages.length - 1); this.state.systemMessage = showerMessages[messageIndex]; this.state.messageTimestamp = Date.now(); } // Check if action is complete based on update count if (this.state.pendingAction.updateCount >= this.state.pendingAction.duration) { // Action complete if (this.state.pendingAction.type === 'bathing') { this.state.systemMessage = `All clean and sparkly! ๐ŸŒŸ`; this.state.messageTimestamp = Date.now(); } this.state.pendingAction = null; } } // Save updated state await this.stateManager.save(this.state); } private async checkForActions(): Promise<void> { if (!this.state) return; // Check for action file if (fs.existsSync(config.actionFile)) { try { const actionData = fs.readFileSync(config.actionFile, 'utf-8'); const action: PetAction = JSON.parse(actionData); // Process the action this.processAction(action); // Delete the action file fs.unlinkSync(config.actionFile); } catch (error) { if (config.debugMode) { console.error('Failed to process action:', error); } // Delete corrupted action file try { fs.unlinkSync(config.actionFile); } catch {} } } } private processAction(action: PetAction): void { if (!this.state) return; // Check if we can interrupt current animation if (!this.animationManager.canInterrupt(this.state)) { return; // Action will be retried next update } switch (action.command) { case 'feed': this.handleFeed(action.parameter || 'cookie'); break; case 'pet': this.handlePet(); break; case 'play': this.handlePlay(action.parameter || 'ball'); break; case 'sleep': this.handleSleep(); break; case 'wake': this.handleWake(); break; case 'clean': this.handleClean(); break; case 'heal': this.handleHeal(); break; case 'name': if (action.parameter) { this.state.name = action.parameter; } break; case 'trick': this.handleTrick(action.parameter); break; } } private handleFeed(food: string): void { if (!this.state) return; // Set eating action to last for 8 updates this.state.pendingAction = { type: 'eating', item: food, startTime: Date.now(), duration: 8, // Will last for 8 updates updateCount: 0 // Track how many updates have passed }; this.state.lastFed = Date.now(); this.state.systemMessage = `The ${food} was yummy! ๐Ÿ˜‹`; this.state.messageTimestamp = Date.now(); // Increase hunger immediately this.state.hunger = Math.min(100, this.state.hunger + 35); } private handlePet(): void { if (!this.state) return; this.animationManager.setAnimation(this.state, 'love'); this.state.happiness = Math.min(100, this.state.happiness + 15); this.state.lastPetted = Date.now(); } private handlePlay(toy: string): void { if (!this.state) return; // Set playing action to last for 6 updates this.state.pendingAction = { type: 'playing', item: toy, startTime: Date.now(), duration: 6, // Will last for 6 updates updateCount: 0 }; this.state.lastPlayed = Date.now(); // Increase happiness and decrease energy this.state.happiness = Math.min(100, this.state.happiness + 20); this.state.energy = Math.max(0, this.state.energy - 10); } private handleSleep(): void { if (!this.state) return; this.state.isAsleep = true; this.state.lastSlept = Date.now(); this.state.systemMessage = `${this.state.name} is going to sleep... ๐Ÿ˜ด`; this.state.messageTimestamp = Date.now(); } private handleWake(): void { if (!this.state) return; if (this.state.isAsleep) { this.state.isAsleep = false; this.state.systemMessage = `${this.state.name} woke up! โ˜€๏ธ`; this.state.messageTimestamp = Date.now(); this.animationManager.setAnimation(this.state, 'blink'); } } private handleClean(): void { if (!this.state) return; this.state.pendingAction = { type: 'bathing', startTime: Date.now(), duration: 10, // 10 updates to fully clean updateCount: 0 }; this.state.lastCleaned = Date.now(); this.state.systemMessage = `Bath time! ๐Ÿ›`; this.state.messageTimestamp = Date.now(); } private handleHeal(): void { if (!this.state) return; if (this.state.isSick || this.state.health < 50) { this.state.health = Math.min(100, this.state.health + 30); this.state.isSick = false; this.animationManager.setAnimation(this.state, 'happy'); } } private handleTrick(trick?: string): void { if (!this.state || !trick) return; if (!this.state.tricks.includes(trick)) { this.state.tricks.push(trick); this.animationManager.setAnimation(this.state, 'celebrating'); } } // Process input for keyword detection processInput(input: string): void { if (!this.state) return; this.activitySystem.detectKeywords(this.state, input); } getDisplay(): string { if (!this.state) { return '(โ—•แดฅโ—•) Loading...'; } const petFrame = this.animationManager.getFrame(this.state); // Save state after animation frame increment this.stateManager.save(this.state); // Build display with activity context let display = `${petFrame} ${this.state.name}`; // Add activity indicators if (this.state.pendingAction) { // Show what pet is doing with specific item if (this.state.pendingAction.type === 'eating') { // Use food-specific emoji const foodEmojis: Record<string, string> = { 'cookie': '๐Ÿช', 'pizza': '๐Ÿ•', 'sushi': '๐Ÿฃ', 'apple': '๐ŸŽ', 'carrot': '๐Ÿฅ•', 'steak': '๐Ÿฅฉ', 'fish': '๐ŸŸ', 'candy': '๐Ÿฌ' }; const foodEmoji = foodEmojis[this.state.pendingAction.item || 'cookie'] || '๐Ÿช'; display += ` ${foodEmoji}`; } else if (this.state.pendingAction.type === 'playing') { // Use toy-specific emoji const toyEmojis: Record<string, string> = { 'ball': '๐ŸŽพ', 'frisbee': '๐Ÿฅ', 'laser': '๐Ÿ”ด', 'yarn': '๐Ÿงถ', 'puzzle': '๐Ÿงฉ' }; const toyEmoji = toyEmojis[this.state.pendingAction.item || 'ball'] || '๐ŸŽพ'; display += ` ${toyEmoji}`; } else { const actionEmojis: Record<string, string> = { 'sleeping': '๐Ÿ˜ด', 'bathing': '๐Ÿ›' }; display += ` ${actionEmojis[this.state.pendingAction.type] || ''}`; } } else { // Show mood or status const mood = this.getMoodEmoji(); display += ` ${mood}`; } // Add session indicators if (this.state.sessionUpdateCount > 200) { display += ' ๐Ÿ”ฅ'; // On fire! Long session } else if (this.state.sessionUpdateCount > 100) { display += ' ๐Ÿ’ช'; // Strong session } else if (this.state.sessionUpdateCount === 50 || this.state.sessionUpdateCount === 100 || this.state.sessionUpdateCount === 150) { display += ' โœจ'; // Milestone } // Add alert indicators if (this.state.hunger < 20) { display += ' ๐Ÿ–'; // Hungry! } if (this.state.energy < 20) { display += ' ๐Ÿ’ค'; // Sleepy } return display; } private getMoodEmoji(): string { if (!this.state) return ''; if (this.state.isAsleep) return '๐Ÿ˜ด'; if (this.state.isSick) return '๐Ÿค’'; if (this.state.happiness > 80) return '๐Ÿ˜Š'; if (this.state.happiness > 50) return '๐Ÿ™‚'; if (this.state.happiness > 30) return '๐Ÿ˜'; return '๐Ÿ˜ข'; } getStats(): string { if (!this.state) return 'No pet data'; // Compact stats with critical alerts let stats = ''; // Show critical stats in red if low if (this.state.hunger < 30) { stats += `๐Ÿ– ${Math.round(this.state.hunger)}%โš ๏ธ `; } else { stats += `๐Ÿ– ${Math.round(this.state.hunger)}% `; } if (this.state.energy < 30) { stats += `โšก ${Math.round(this.state.energy)}%โš ๏ธ `; } else { stats += `โšก ${Math.round(this.state.energy)}% `; } if (this.state.cleanliness < 30) { stats += `๐Ÿงผ ${Math.round(this.state.cleanliness)}%โš ๏ธ `; } else { stats += `๐Ÿงผ ${Math.round(this.state.cleanliness)}% `; } stats += `โค๏ธ ${Math.round(this.state.happiness)}%`; // Add session info if explicitly enabled const showSession = process.env.PET_SHOW_SESSION === 'true'; if (showSession) { stats += ` | Session: ${this.state.sessionUpdateCount}`; } return stats; } getDetailedStats(): object { return this.state || {}; } getSystemMessage(): string | null { if (!this.state || !this.state.systemMessage) return null; // Clear message after 10 seconds (about 30 updates) const messageAge = Date.now() - (this.state.messageTimestamp || 0); if (messageAge > 10000) { this.state.systemMessage = undefined; this.state.messageTimestamp = undefined; return null; } return this.state.systemMessage; } getCurrentThought(): string | null { if (!this.state) return null; // Check for feedback thought (conversation-relevant) const feedbackThought = this.feedbackSystem.getFeedbackThought(this.state); // Use ratio to decide which type of thought to show if (feedbackThought && Math.random() < config.conversationThoughtRatio) { // Show conversation-relevant thought (funny observation about the code) return feedbackThought; } // Fall back to regular thought (mood/stats based) if (!this.state.currentThought) return null; // Thoughts last longer than system messages const thoughtAge = Date.now() - (this.state.thoughtTimestamp || 0); if (thoughtAge > 30000) { // 30 seconds return null; } return this.state.currentThought; } getFeedbackIcon(): string | null { if (!this.state) return null; return this.feedbackSystem.getFeedbackIcon(this.state); } }