claude-code-tamagotchi
Version:
A virtual pet that lives in your Claude Code statusline
453 lines (371 loc) โข 13.6 kB
text/typescript
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);
}
}