UNPKG

claude-code-tamagotchi

Version:

A virtual pet that lives in your Claude Code statusline

381 lines (316 loc) 13.5 kB
import { PetState } from './StateManager'; import { config, getWeatherEffects } from '../utils/config'; import { thoughtSystem } from './ThoughtSystem'; const SESSION_GAP_THRESHOLD = 5 * 60 * 1000; // 5 minutes = new session const UPDATE_DECAY_INTERVAL = parseInt(process.env.PET_DECAY_INTERVAL || '20'); // Every N updates, decay stats (default 20) const HUNGER_DECAY_RATE = parseFloat(process.env.PET_HUNGER_DECAY || '3'); // Hunger decay per interval const ENERGY_DECAY_RATE = parseFloat(process.env.PET_ENERGY_DECAY || '2.5'); // Energy decay per interval const CLEANLINESS_DECAY_RATE = parseFloat(process.env.PET_CLEAN_DECAY || '2'); // Cleanliness decay per interval const SLEEP_RECOVERY_RATE = parseFloat(process.env.PET_SLEEP_RECOVERY || '3'); // Energy recovery per update when sleeping (default 3%) export class ActivitySystem { // Check if this is a new session private isNewSession(state: PetState): boolean { const now = Date.now(); const timeSinceLastUpdate = now - state.lastUpdateTimestamp; return timeSinceLastUpdate > SESSION_GAP_THRESHOLD; } // Calculate activity intensity (updates per minute) private calculateActivityIntensity(state: PetState): number { const now = Date.now(); const recentUpdates = state.recentUpdateTimestamps.filter( t => now - t < 60000 // Last minute ); return recentUpdates.length; } // Apply activity-based updates applyActivityUpdate(state: PetState): void { const now = Date.now(); // Check for new session if (this.isNewSession(state)) { this.handleNewSession(state); return; } // Update session counters state.sessionUpdateCount++; state.totalUpdateCount++; state.lastUpdateTimestamp = now; // Track recent updates for intensity calculation state.recentUpdateTimestamps.push(now); if (state.recentUpdateTimestamps.length > 30) { state.recentUpdateTimestamps.shift(); } // Toggle breathing animation state.breathingState = !state.breathingState; // Apply sleep recovery every update if sleeping, otherwise decay at intervals if (state.isAsleep) { // Restore energy using configurable rate (default 3% per update) state.energy = Math.min(100, state.energy + SLEEP_RECOVERY_RATE); // Still apply some decay every N updates (slower when sleeping) if (state.sessionUpdateCount % UPDATE_DECAY_INTERVAL === 0) { state.hunger = Math.max(0, state.hunger - HUNGER_DECAY_RATE * 0.5); state.cleanliness = Math.max(0, state.cleanliness - CLEANLINESS_DECAY_RATE * 0.3); } // Check if fully rested if (state.energy >= 100) { state.isAsleep = false; state.systemMessage = `${state.name} woke up fully rested! 🌟`; state.messageTimestamp = Date.now(); } } else if (state.sessionUpdateCount % UPDATE_DECAY_INTERVAL === 0) { this.applyActivityDecay(state); } // Update mood based on activity this.updateMood(state); // Handle milestones this.checkMilestones(state); // Update health based on other stats this.updateHealth(state); // Generate thoughts periodically this.updateThoughts(state); } private updateThoughts(state: PetState): void { const now = Date.now(); const MIN_THOUGHT_DURATION = parseInt(process.env.PET_THOUGHT_MIN_DURATION || '3000'); // Default 3 seconds const THOUGHT_UPDATE_INTERVAL = parseInt(process.env.PET_THOUGHT_UPDATE_INTERVAL || '1'); // Default every session // Check if it's a new session const isNewSession = state.sessionUpdateCount === 1; // Check if enough time has passed since last thought const timeSinceLastThought = now - (state.thoughtTimestamp || 0); const shouldUpdateByTime = timeSinceLastThought >= MIN_THOUGHT_DURATION; // Check if enough updates have passed const updatesSinceLastThought = state.totalUpdateCount - (state.lastThoughtUpdate || 0); const shouldUpdateByInterval = updatesSinceLastThought >= THOUGHT_UPDATE_INTERVAL; // Only update thought if: new session OR (enough time passed AND enough updates) if (!isNewSession && (!shouldUpdateByTime || !shouldUpdateByInterval)) { return; // Don't update thought yet } // Generate a new thought based on current state const context = { recentInput: state.recentKeywords.join(' '), sessionData: { sessionLength: state.sessionUpdateCount, totalUpdates: state.totalUpdateCount } }; const thought = thoughtSystem.generateThought(state, context); if (thought) { state.currentThought = thought; state.thoughtTimestamp = now; state.lastThoughtUpdate = state.totalUpdateCount; // Add to thought history if (!state.thoughtHistory) { state.thoughtHistory = []; } state.thoughtHistory.push(thought); if (state.thoughtHistory.length > 10) { state.thoughtHistory.shift(); } } } private handleNewSession(state: PetState): void { const now = Date.now(); const sessionGap = now - state.lastUpdateTimestamp; // Pet was sleeping - apply recovery if (sessionGap > SESSION_GAP_THRESHOLD) { // Restore energy during sleep const sleepHours = Math.min(8, sessionGap / (1000 * 60 * 60)); state.energy = Math.min(100, state.energy + (sleepHours * 10)); // Slight happiness boost from rest state.happiness = Math.min(100, state.happiness + 5); // Mark as new session state.previousSessionEnd = state.lastUpdateTimestamp; state.sessionStartTime = now; state.sessionUpdateCount = 0; state.sessionsToday++; // Wake up from sleep if (state.isAsleep) { state.isAsleep = false; } // Reset mood for new session state.currentMood = 'normal'; } state.lastUpdateTimestamp = now; state.sessionUpdateCount++; state.totalUpdateCount++; } private applyActivityDecay(state: PetState): void { const intensity = this.calculateActivityIntensity(state); // Normal decay when awake state.hunger = Math.max(0, state.hunger - HUNGER_DECAY_RATE); state.energy = Math.max(0, state.energy - ENERGY_DECAY_RATE); state.cleanliness = Math.max(0, state.cleanliness - CLEANLINESS_DECAY_RATE); // Intensity-based additional decay if (intensity > 20) { // Intense coding - more energy and hunger drain state.energy = Math.max(0, state.energy - 1.5); state.hunger = Math.max(0, state.hunger - 1); state.cleanliness = Math.max(0, state.cleanliness - 0.5); // Gets dirtier faster when active } else if (intensity > 10) { // Active coding state.energy = Math.max(0, state.energy - 0.5); state.hunger = Math.max(0, state.hunger - 0.5); } // Happiness changes based on session length if (state.sessionUpdateCount < 100) { // Early in session - happy to code state.happiness = Math.min(100, state.happiness + 0.5); } else if (state.sessionUpdateCount > 300) { // Long session - getting tired state.happiness = Math.max(0, state.happiness - 0.5); } // Extra cleanliness decay if pet is sick if (state.isSick) { state.cleanliness = Math.max(0, state.cleanliness - 1); } // Apply weather effects if enabled if (config.enableWeatherEffects) { const weatherEffects = getWeatherEffects(); if (weatherEffects.happiness) { state.happiness = Math.max(0, Math.min(100, state.happiness + weatherEffects.happiness)); } if (weatherEffects.energy) { state.energy = Math.max(0, Math.min(100, state.energy + weatherEffects.energy)); } } } private updateMood(state: PetState): void { const intensity = this.calculateActivityIntensity(state); // Check for special moods first if (state.energy < 20) { state.currentMood = 'tired'; } else if (state.sessionUpdateCount > 200) { state.currentMood = 'focused'; } else if (intensity > 20) { state.currentMood = 'focused'; } else if (state.happiness > 80 && state.hunger > 50) { state.currentMood = 'normal'; } else if (state.hunger < 30) { state.currentMood = 'tired'; } else { state.currentMood = 'normal'; } } private checkMilestones(state: PetState): void { // Celebrate every 100 updates if (state.sessionUpdateCount > 0 && state.sessionUpdateCount % 100 === 0) { state.currentMood = 'celebrating'; state.happiness = Math.min(100, state.happiness + 5); // Add care points for good session state.careLevelPoints += 1; } // Marathon coding session if (state.sessionUpdateCount === 200) { state.currentMood = 'focused'; // Achievement unlocked! } } private updateHealth(state: PetState): void { // Health is derived from other stats const avgStats = (state.happiness + state.hunger + state.energy + state.cleanliness) / 4; // Health tends toward average of other stats const healthTarget = avgStats; const healthDiff = healthTarget - state.health; // Gradually adjust health state.health = Math.max(0, Math.min(100, state.health + (healthDiff * 0.1))); // Check for sickness if (state.health < 30 || state.cleanliness < 20) { state.isSick = true; } else if (state.isSick && state.health > 60 && state.cleanliness > 50) { state.isSick = false; } } // Apply effects after an action completes applyActionEffects(state: PetState): void { if (!state.pendingAction) return; const action = state.pendingAction; switch (action.type) { case 'eating': // Eating gives bigger boost during intense coding const intensity = this.calculateActivityIntensity(state); const hungerBoost = intensity > 15 ? 40 : 30; state.hunger = Math.min(100, state.hunger + hungerBoost); state.happiness = Math.min(100, state.happiness + 10); state.totalFeedings++; if (action.item) { state.foodEaten.push(action.item); // Track favorite food const foodCounts: Record<string, number> = {}; state.foodEaten.forEach(food => { foodCounts[food] = (foodCounts[food] || 0) + 1; }); state.favoriteFood = Object.keys(foodCounts).reduce((a, b) => foodCounts[a] > foodCounts[b] ? a : b); } // Generate eating thought const eatingThought = thoughtSystem.getActionThought('eating', action.item, state); if (eatingThought) { state.currentThought = eatingThought; state.thoughtTimestamp = Date.now(); } break; case 'playing': // Playing during long sessions gives more happiness const playBoost = state.sessionUpdateCount > 100 ? 25 : 20; state.happiness = Math.min(100, state.happiness + playBoost); state.energy = Math.max(0, state.energy - 10); state.totalPlaySessions++; // Generate playing thought const playingThought = thoughtSystem.getActionThought('playing', action.item, state); if (playingThought) { state.currentThought = playingThought; state.thoughtTimestamp = Date.now(); } break; case 'sleeping': state.energy = 100; state.isAsleep = false; // Generate waking thought const wakingThought = thoughtSystem.getActionThought('waking', undefined, state); if (wakingThought) { state.currentThought = wakingThought; state.thoughtTimestamp = Date.now(); } break; case 'bathing': state.cleanliness = 100; state.happiness = Math.min(100, state.happiness + 5); // Generate bathing complete thought const bathingThought = thoughtSystem.getActionThought('bathing', undefined, state); if (bathingThought) { state.currentThought = bathingThought; state.thoughtTimestamp = Date.now(); } break; } // Clear the pending action state.pendingAction = undefined; } // Detect keywords in conversation for mood changes detectKeywords(state: PetState, input: string): void { const lowerInput = input.toLowerCase(); // Add to recent keywords state.recentKeywords.push(lowerInput); if (state.recentKeywords.length > 10) { state.recentKeywords.shift(); } // Check for debugging keywords if (lowerInput.includes('error') || lowerInput.includes('bug') || lowerInput.includes('issue') || lowerInput.includes('problem')) { state.currentMood = 'debugging'; } // Check for success keywords if (lowerInput.includes('fixed') || lowerInput.includes('works') || lowerInput.includes('success') || lowerInput.includes('done')) { state.currentMood = 'celebrating'; state.happiness = Math.min(100, state.happiness + 3); } // Check for question if (lowerInput.includes('?')) { // Pet is curious when you're asking questions state.microAnimationFrame = 1; // Tilt head } } }