UNPKG

claude-code-tamagotchi

Version:

A virtual pet that lives in your Claude Code statusline

198 lines (171 loc) 6.75 kB
import { animations, Animation, getWeatherOverlay } from '../animations'; import { PetState } from './StateManager'; import { config } from '../utils/config'; export class AnimationManager { // Map moods to animation names private moodToAnimation: Record<string, string> = { 'normal': 'idle', 'happy': 'happy', 'proud': 'celebrating', 'excited': 'happy', 'concerned': 'blink', 'confused': 'blink', 'suspicious': 'blink', 'annoyed': 'sad', 'frustrated': 'sad', 'angry': 'sad', 'debugging': 'blink', 'celebrating': 'celebrating', 'tired': 'tired', 'focused': 'idle', 'sleeping': 'sleeping', 'sad': 'sad', 'sick': 'sick' }; // Get the appropriate animation for current state private getCurrentAnimation(state: PetState): string { // Special states override mood if (state.isAsleep) return 'sleeping'; if (state.isSick) return 'sick'; if (state.hunger < 20) return 'sad'; // Check for pending actions if (state.pendingAction) { switch (state.pendingAction.type) { case 'eating': return 'eating'; case 'playing': return 'playing'; case 'bathing': return 'bathing'; case 'sleeping': return 'sleeping'; } } // Map mood to animation return this.moodToAnimation[state.currentMood] || 'idle'; } // Get mood-based face with breathing variation private getMoodFace(state: PetState): string { // Extended mood faces with feedback states const moodFaces: Record<string, [string, string]> = { // Happy states 'normal': ['(◕ᴥ◕)', '(◕ᴗ◕)'], 'happy': ['(◕‿◕)', '(◕ω◕)'], 'proud': ['(◕ᴗ◕)✨', '(★ᴥ★)'], 'excited': ['(✧ᴥ✧)', '(◕ᴗ◕)!'], // Concerned states 'concerned': ['(◕_◕)', '(◕.◕)'], 'confused': ['(◕_◕)?', '(◕。◕)?'], 'suspicious': ['(¬‿¬)', '(◕_◕)...'], // Annoyed states 'annoyed': ['(◕︵◕)', '(¬_¬)'], 'frustrated': ['(╯◕︵◕)╯', '(ಠ_ಠ)'], 'angry': ['(╬◕︵◕)', '💢(◕︵◕)'], // Activity states 'debugging': ['(◕_◕)', '(◕.◕)'], 'celebrating': ['(✧ᴥ✧)', '(★ᴥ★)'], 'tired': ['(◔_◔)', '(◔‸◔)'], 'focused': ['(◕▿◕)', '(◕ᴗ◕)'], 'sleeping': ['(-ᴥ-)', '(˘ᴥ˘)'], 'sad': ['(◕︵◕)', '(◕╭╮◕)'], 'sick': ['(@_@)', '(x_x)'] }; // Special states override mood let effectiveMood = state.currentMood; if (state.isAsleep) effectiveMood = 'sleeping'; if (state.isSick) effectiveMood = 'sick'; if (state.hunger < 20) effectiveMood = 'sad'; // Check for pending actions if (state.pendingAction) { const updateNum = state.pendingAction.updateCount || 0; switch (state.pendingAction.type) { case 'eating': // Different eating animations based on update count if (updateNum % 3 === 0) return '(◕◡◕)'; // Mouth open if (updateNum % 3 === 1) return '(◕~◕)'; // Chewing return '(◕ᴥ◕)'; // Happy eating case 'playing': // Alternating play animations if (updateNum % 2 === 0) return '(◕ᴥ◕)ノ'; return '(◕ᴥ◕)/'; case 'bathing': if (updateNum % 2 === 0) return '(◕△◕)'; return '(◕▽◕)'; case 'sleeping': if (updateNum % 2 === 0) return '(-ᴥ-)'; return '(˘ᴥ˘)'; } } // Get faces for this mood (or default) const faces = moodFaces[effectiveMood] || moodFaces['normal']; // Use breathing state to pick which face (creates subtle animation) return state.breathingState ? faces[0] : faces[1]; } getFrame(state: PetState): string { // Get the appropriate animation for current state const desiredAnimation = this.getCurrentAnimation(state); // Check if we need to switch animations if (state.currentAnimationName !== desiredAnimation) { state.currentAnimationName = desiredAnimation; state.animationFrame = 0; // Reset frame counter when switching animations } // Get the animation object const animation = animations[state.currentAnimationName]; if (!animation) { // Fallback if animation doesn't exist return this.getMoodFace(state); } // Get current frame const frame = animation.frames[state.animationFrame]; let petDisplay = frame ? frame.pet : '(◕ᴥ◕)'; // Increment frame counter for next update state.animationFrame = (state.animationFrame + 1) % animation.frames.length; // Add activity indicators if (state.sessionUpdateCount > 100) { // Long coding session petDisplay = `${petDisplay} 💻`; } else if (state.sessionUpdateCount > 0 && state.sessionUpdateCount % 50 === 0) { // Milestone indicator petDisplay = `${petDisplay} ✨`; } // Add weather overlay if enabled if (config.enableWeatherEffects) { const weatherOverlay = getWeatherOverlay(config.weather); if (weatherOverlay) { petDisplay = `${petDisplay} ${weatherOverlay}`; } } // Debug logging (only if logging is enabled) if (config.enableLogging && config.debugMode) { const fs = require('fs'); fs.appendFileSync('/tmp/pet-animation.log', `Animation: ${state.currentAnimationName}, Frame: ${state.animationFrame}, Display: ${petDisplay}\n`); } return petDisplay; } // Check if current animation can be interrupted canInterrupt(state: PetState): boolean { const nonInterruptible = ['eating', 'playing', 'bathing', 'celebrating']; if (state.pendingAction && nonInterruptible.includes(state.pendingAction.type)) { // Check if action is complete const elapsed = Date.now() - state.pendingAction.startTime; return elapsed >= state.pendingAction.duration; } return !nonInterruptible.includes(state.currentAnimation); } // Force a specific animation/mood setAnimation(state: PetState, animationName: string): void { // Map old animation names to moods const animationToMood: Record<string, string> = { 'idle': 'normal', 'happy': 'happy', 'sad': 'sad', 'love': 'happy', 'celebrating': 'celebrating', 'tired': 'tired', 'sick': 'sick', 'sleeping': 'sleeping', 'blink': 'normal' }; const mood = animationToMood[animationName] || 'normal'; state.currentMood = mood as any; state.currentAnimation = animationName; state.animationStartTime = Date.now(); } }