UNPKG

@codejoy/terminal-pet

Version:

A virtual pet that lives in your terminal and grows with your coding activity

358 lines (301 loc) 9.61 kB
const fs = require('fs'); const path = require('path'); const os = require('os'); class Pet { constructor(name = 'CodeBuddy', type = 'cat') { this.name = name; this.type = type; this.level = 1; this.experience = 0; this.health = 100; this.happiness = 80; this.hunger = 30; this.energy = 100; this.age = 0; // in days this.lastFed = Date.now(); this.lastPlayed = Date.now(); this.lastCommit = null; this.totalCommits = 0; this.streak = 0; this.maxStreak = 0; this.mood = 'happy'; this.isAlive = true; this.birthDate = Date.now(); this.achievements = []; this.stats = { linesOfCode: 0, bugsFixed: 0, featuresAdded: 0, refactorings: 0 }; } // Pet state management update() { if (!this.isAlive) return; const now = Date.now(); const timeSinceLastUpdate = now - (this.lastUpdate || now); const hoursPassed = timeSinceLastUpdate / (1000 * 60 * 60); // Natural decay over time this.hunger = Math.min(100, this.hunger + (hoursPassed * 2)); this.energy = Math.max(0, this.energy - (hoursPassed * 1.5)); this.happiness = Math.max(0, this.happiness - (hoursPassed * 0.5)); // Health is affected by hunger and happiness if (this.hunger > 80) { this.health = Math.max(0, this.health - (hoursPassed * 2)); } if (this.happiness < 20) { this.health = Math.max(0, this.health - (hoursPassed * 1)); } // Update age const daysPassed = (now - this.birthDate) / (1000 * 60 * 60 * 24); this.age = Math.floor(daysPassed); // Update mood based on stats this.updateMood(); // Check if pet dies if (this.health <= 0) { this.isAlive = false; this.mood = 'dead'; } this.lastUpdate = now; } updateMood() { if (!this.isAlive) { this.mood = 'dead'; return; } if (this.health < 20) { this.mood = 'sick'; } else if (this.hunger > 80) { this.mood = 'hungry'; } else if (this.energy < 20) { this.mood = 'tired'; } else if (this.happiness > 80 && this.health > 80) { this.mood = 'ecstatic'; } else if (this.happiness > 60) { this.mood = 'happy'; } else if (this.happiness < 30) { this.mood = 'sad'; } else { this.mood = 'neutral'; } } // Actions feed() { if (!this.isAlive) return false; this.hunger = Math.max(0, this.hunger - 30); this.happiness = Math.min(100, this.happiness + 10); this.health = Math.min(100, this.health + 5); this.lastFed = Date.now(); this.checkAchievements('feed'); return true; } play() { if (!this.isAlive) return false; if (this.energy < 20) return false; this.happiness = Math.min(100, this.happiness + 20); this.energy = Math.max(0, this.energy - 15); this.hunger = Math.min(100, this.hunger + 5); this.lastPlayed = Date.now(); this.checkAchievements('play'); return true; } sleep() { if (!this.isAlive) return false; this.energy = 100; this.health = Math.min(100, this.health + 10); this.hunger = Math.min(100, this.hunger + 10); return true; } // Coding activity rewards onCommit(commitMessage = '') { if (!this.isAlive) return; const now = Date.now(); const daysSinceLastCommit = this.lastCommit ? (now - this.lastCommit) / (1000 * 60 * 60 * 24) : 0; // Update streak if (daysSinceLastCommit <= 1) { this.streak += 1; } else { this.streak = 1; } this.maxStreak = Math.max(this.maxStreak, this.streak); // Rewards based on commit const baseReward = 10; const streakBonus = Math.min(this.streak * 2, 20); const experienceGain = baseReward + streakBonus; this.experience += experienceGain; this.happiness = Math.min(100, this.happiness + 15); this.health = Math.min(100, this.health + 5); this.hunger = Math.min(100, this.hunger + 5); // Coding makes you hungry! // Analyze commit message for bonus rewards this.analyzeCommitMessage(commitMessage); this.totalCommits += 1; this.lastCommit = now; // Level up check this.checkLevelUp(); this.checkAchievements('commit'); } analyzeCommitMessage(message) { const lowerMessage = message.toLowerCase(); if (lowerMessage.includes('fix') || lowerMessage.includes('bug')) { this.stats.bugsFixed += 1; this.experience += 5; } if (lowerMessage.includes('feat') || lowerMessage.includes('add')) { this.stats.featuresAdded += 1; this.experience += 8; } if (lowerMessage.includes('refactor') || lowerMessage.includes('clean')) { this.stats.refactorings += 1; this.experience += 6; } // Bonus for good commit messages if (message.length > 20 && !lowerMessage.includes('wip')) { this.experience += 3; this.happiness = Math.min(100, this.happiness + 5); } } checkLevelUp() { const requiredExp = this.level * 100; if (this.experience >= requiredExp) { this.level += 1; this.experience -= requiredExp; this.happiness = Math.min(100, this.happiness + 20); this.health = 100; // Full heal on level up this.checkAchievements('levelup'); return true; } return false; } checkAchievements(action) { const newAchievements = []; // Commit-based achievements if (action === 'commit') { if (this.totalCommits === 1 && !this.achievements.includes('first_commit')) { newAchievements.push('first_commit'); } if (this.totalCommits === 100 && !this.achievements.includes('century')) { newAchievements.push('century'); } if (this.streak === 7 && !this.achievements.includes('week_streak')) { newAchievements.push('week_streak'); } if (this.streak === 30 && !this.achievements.includes('month_streak')) { newAchievements.push('month_streak'); } } // Level-based achievements if (action === 'levelup') { if (this.level === 10 && !this.achievements.includes('double_digits')) { newAchievements.push('double_digits'); } if (this.level === 50 && !this.achievements.includes('half_century')) { newAchievements.push('half_century'); } } // Care-based achievements if (action === 'feed' && this.hunger === 0 && !this.achievements.includes('well_fed')) { newAchievements.push('well_fed'); } if (action === 'play' && this.happiness === 100 && !this.achievements.includes('pure_joy')) { newAchievements.push('pure_joy'); } this.achievements.push(...newAchievements); return newAchievements; } // Getters for display getStatusBar() { const healthBar = this.createBar(this.health, '❤️', '💔'); const happinessBar = this.createBar(this.happiness, '😊', '😢'); const hungerBar = this.createBar(100 - this.hunger, '🍖', '🍽️'); const energyBar = this.createBar(this.energy, '⚡', '😴'); return { health: healthBar, happiness: happinessBar, hunger: hungerBar, energy: energyBar }; } createBar(value, fullChar, emptyChar) { const barLength = 10; const filledLength = Math.floor((value / 100) * barLength); const emptyLength = barLength - filledLength; return fullChar.repeat(filledLength) + emptyChar.repeat(emptyLength); } getInfo() { return { name: this.name, type: this.type, level: this.level, experience: this.experience, age: this.age, mood: this.mood, isAlive: this.isAlive, streak: this.streak, maxStreak: this.maxStreak, totalCommits: this.totalCommits, achievements: this.achievements, stats: this.stats }; } // Persistence save() { const saveDir = path.join(os.homedir(), '.terminal-pet'); if (!fs.existsSync(saveDir)) { fs.mkdirSync(saveDir, { recursive: true }); } const saveFile = path.join(saveDir, 'pet.json'); const data = { name: this.name, type: this.type, level: this.level, experience: this.experience, health: this.health, happiness: this.happiness, hunger: this.hunger, energy: this.energy, age: this.age, lastFed: this.lastFed, lastPlayed: this.lastPlayed, lastCommit: this.lastCommit, lastUpdate: this.lastUpdate, totalCommits: this.totalCommits, streak: this.streak, maxStreak: this.maxStreak, mood: this.mood, isAlive: this.isAlive, birthDate: this.birthDate, achievements: this.achievements, stats: this.stats }; fs.writeFileSync(saveFile, JSON.stringify(data, null, 2)); } static load() { const saveFile = path.join(os.homedir(), '.terminal-pet', 'pet.json'); if (!fs.existsSync(saveFile)) { return null; } try { const data = JSON.parse(fs.readFileSync(saveFile, 'utf8')); const pet = new Pet(); // Restore all properties Object.keys(data).forEach(key => { pet[key] = data[key]; }); // Update pet state based on time passed pet.update(); return pet; } catch (error) { console.error('Error loading pet:', error); return null; } } // Reset pet (for new game) static reset() { const saveFile = path.join(os.homedir(), '.terminal-pet', 'pet.json'); if (fs.existsSync(saveFile)) { fs.unlinkSync(saveFile); } } } module.exports = Pet;