feat: achievements

This commit is contained in:
2026-01-19 18:32:46 -08:00
parent 1ce43dcff8
commit b691a91c53
14 changed files with 1687 additions and 4 deletions
+445
View File
@@ -0,0 +1,445 @@
import { writable, derived } from 'svelte/store';
import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';
import type { Achievement, AchievementUnlockedEvent, AchievementId } from '$lib/types/achievements';
interface AchievementState {
achievements: Record<AchievementId, Achievement>;
totalUnlocked: number;
lastUnlocked: Achievement | null;
}
// Initial achievement definitions
const achievementDefinitions: Record<AchievementId, Omit<Achievement, 'unlocked' | 'unlockedAt'>> = {
// Token milestones
FirstSteps: {
id: 'FirstSteps',
name: 'First Steps',
description: 'Generated your first 1,000 tokens',
icon: '👶',
rarity: 'common',
maxProgress: 1000,
},
GrowingStrong: {
id: 'GrowingStrong',
name: 'Growing Strong',
description: 'Reached 10,000 tokens total',
icon: '🌱',
rarity: 'common',
maxProgress: 10000,
},
BlossomingCoder: {
id: 'BlossomingCoder',
name: 'Blossoming Coder',
description: 'Generated 100,000 tokens - you\'re really growing!',
icon: '🌸',
rarity: 'rare',
maxProgress: 100000,
},
TokenMaster: {
id: 'TokenMaster',
name: 'Token Master',
description: 'One million tokens! You\'re unstoppable!',
icon: '👑',
rarity: 'legendary',
maxProgress: 1000000,
},
// Code generation
HelloWorld: {
id: 'HelloWorld',
name: 'Hello, World!',
description: 'Generated your first code block',
icon: '👋',
rarity: 'common',
maxProgress: 1,
},
CodeWizard: {
id: 'CodeWizard',
name: 'Code Wizard',
description: '100 code blocks generated',
icon: '🧙‍♀️',
rarity: 'rare',
maxProgress: 100,
},
ThousandBlocks: {
id: 'ThousandBlocks',
name: 'Thousand Blocks',
description: '1,000 code blocks! You\'re a code machine!',
icon: '🏗️',
rarity: 'epic',
maxProgress: 1000,
},
// File operations
FileManipulator: {
id: 'FileManipulator',
name: 'File Manipulator',
description: 'Edited 10 files',
icon: '📝',
rarity: 'common',
maxProgress: 10,
},
FileArchitect: {
id: 'FileArchitect',
name: 'File Architect',
description: 'Created or edited 100 files',
icon: '🏛️',
rarity: 'rare',
maxProgress: 100,
},
// Conversation milestones
ConversationStarter: {
id: 'ConversationStarter',
name: 'Conversation Starter',
description: 'Exchanged 10 messages',
icon: '💬',
rarity: 'common',
maxProgress: 10,
},
ChattyKathy: {
id: 'ChattyKathy',
name: 'Chatty Kathy',
description: '100 messages exchanged',
icon: '🗣️',
rarity: 'common',
maxProgress: 100,
},
Conversationalist: {
id: 'Conversationalist',
name: 'Master Conversationalist',
description: '1,000 messages! We\'re really connecting!',
icon: '💖',
rarity: 'rare',
maxProgress: 1000,
},
// Tool usage
Toolsmith: {
id: 'Toolsmith',
name: 'Toolsmith',
description: 'Used 5 different tools',
icon: '🔨',
rarity: 'common',
maxProgress: 5,
},
ToolMaster: {
id: 'ToolMaster',
name: 'Tool Master',
description: 'Used 10 different tools efficiently',
icon: '🛠️',
rarity: 'rare',
maxProgress: 10,
},
// Time-based achievements
EarlyBird: {
id: 'EarlyBird',
name: 'Early Bird',
description: 'Started a session between 5 AM and 7 AM',
icon: '🌅',
rarity: 'common',
},
NightOwl: {
id: 'NightOwl',
name: 'Night Owl',
description: 'Coding after midnight',
icon: '🦉',
rarity: 'common',
},
AllNighter: {
id: 'AllNighter',
name: 'All Nighter',
description: 'Worked through the night (2 AM - 5 AM)',
icon: '🌙',
rarity: 'rare',
},
WeekendWarrior: {
id: 'WeekendWarrior',
name: 'Weekend Warrior',
description: 'Coding on a weekend',
icon: '⚔️',
rarity: 'common',
},
DedicatedDeveloper: {
id: 'DedicatedDeveloper',
name: 'Dedicated Developer',
description: 'Coded for 30 days in a row',
icon: '🏆',
rarity: 'legendary',
},
// Search and exploration
Explorer: {
id: 'Explorer',
name: 'Explorer',
description: 'Used search tools 50 times',
icon: '🔍',
rarity: 'common',
maxProgress: 50,
},
MasterSearcher: {
id: 'MasterSearcher',
name: 'Master Searcher',
description: 'Searched 500 times across files',
icon: '🕵️‍♀️',
rarity: 'rare',
maxProgress: 500,
},
// Session achievements
QuickSession: {
id: 'QuickSession',
name: 'Quick Session',
description: 'Completed a productive session in under 5 minutes',
icon: '⚡',
rarity: 'common',
},
FocusedWork: {
id: 'FocusedWork',
name: 'Focused Work',
description: 'Worked for 30 minutes straight',
icon: '🎯',
rarity: 'common',
},
DeepDive: {
id: 'DeepDive',
name: 'Deep Dive',
description: 'Worked for 2 hours continuously',
icon: '🏊‍♀️',
rarity: 'rare',
},
MarathonSession: {
id: 'MarathonSession',
name: 'Marathon Session',
description: '5+ hour coding session!',
icon: '🏃‍♀️',
rarity: 'epic',
},
// Special achievements
FirstMessage: {
id: 'FirstMessage',
name: 'First Message',
description: 'Sent your first message to Hikari',
icon: '✨',
rarity: 'common',
maxProgress: 1,
},
FirstTool: {
id: 'FirstTool',
name: 'First Tool',
description: 'Used your first tool',
icon: '🔧',
rarity: 'common',
maxProgress: 1,
},
FirstCodeBlock: {
id: 'FirstCodeBlock',
name: 'First Code',
description: 'Generated your first code block',
icon: '📦',
rarity: 'common',
maxProgress: 1,
},
FirstFileEdit: {
id: 'FirstFileEdit',
name: 'First Edit',
description: 'Made your first file edit',
icon: '✏️',
rarity: 'common',
maxProgress: 1,
},
Polyglot: {
id: 'Polyglot',
name: 'Polyglot',
description: 'Generated code in 5+ languages in one session',
icon: '🌍',
rarity: 'rare',
maxProgress: 5,
},
SpeedCoder: {
id: 'SpeedCoder',
name: 'Speed Coder',
description: 'Generated 10 code blocks in 10 minutes',
icon: '🚀',
rarity: 'rare',
},
ClaudeConnoisseur: {
id: 'ClaudeConnoisseur',
name: 'Claude Connoisseur',
description: 'Used all available Claude models',
icon: '🎨',
rarity: 'epic',
maxProgress: 5, // Adjust based on available models
},
MarathonCoder: {
id: 'MarathonCoder',
name: 'Marathon Coder',
description: '10,000 tokens in a single session',
icon: '🏃‍♂️',
rarity: 'epic',
maxProgress: 10000,
},
};
// Initialize all achievements as locked
const initialAchievements: Record<AchievementId, Achievement> = {} as Record<AchievementId, Achievement>;
for (const [id, def] of Object.entries(achievementDefinitions)) {
initialAchievements[id as AchievementId] = {
...def,
unlocked: false,
progress: 0,
};
}
// Create the main store
function createAchievementsStore() {
const { subscribe, update } = writable<AchievementState>({
achievements: initialAchievements,
totalUnlocked: 0,
lastUnlocked: null,
});
return {
subscribe,
unlockAchievement: (event: AchievementUnlockedEvent) => {
update(state => {
const achievement = state.achievements[event.achievement.id];
if (achievement && !achievement.unlocked) {
achievement.unlocked = true;
achievement.unlockedAt = event.achievement.unlocked_at ? new Date(event.achievement.unlocked_at) : new Date();
state.totalUnlocked++;
state.lastUnlocked = achievement;
}
return state;
});
},
updateProgress: (id: AchievementId, progress: number) => {
update(state => {
const achievement = state.achievements[id];
if (achievement) {
achievement.progress = progress;
}
return state;
});
},
reset: () => {
update(() => ({
achievements: initialAchievements,
totalUnlocked: 0,
lastUnlocked: null,
}));
}
};
}
export const achievementsStore = createAchievementsStore();
// Derived stores for different views
export const unlockedAchievements = derived(
achievementsStore,
$store => Object.values($store.achievements).filter(a => a.unlocked)
);
export const lockedAchievements = derived(
achievementsStore,
$store => Object.values($store.achievements).filter(a => !a.unlocked)
);
export const achievementsByRarity = derived(
achievementsStore,
$store => {
const byRarity: Record<string, Achievement[]> = {
common: [],
rare: [],
epic: [],
legendary: [],
};
for (const achievement of Object.values($store.achievements)) {
byRarity[achievement.rarity].push(achievement);
}
return byRarity;
}
);
export const achievementProgress = derived(
achievementsStore,
$store => ({
unlocked: $store.totalUnlocked,
total: Object.keys($store.achievements).length,
percentage: Math.round(($store.totalUnlocked / Object.keys($store.achievements).length) * 100),
})
);
// Initialize achievement listener
export async function initAchievementsListener() {
// Listen for achievement unlocked events
await listen<AchievementUnlockedEvent>('achievement:unlocked', (event) => {
achievementsStore.unlockAchievement(event.payload);
});
// Load saved achievements from persistent storage
try {
const savedAchievements = await invoke<AchievementUnlockedEvent[]>('load_saved_achievements');
// Update the store with saved achievements
for (const event of savedAchievements) {
achievementsStore.unlockAchievement(event);
}
} catch (error) {
console.error('Failed to load saved achievements:', error);
}
}
// Export achievement categories for the display panel
export const achievementCategories = [
{
name: 'Token Milestones',
description: 'Track your token generation progress',
ids: ['FirstSteps', 'GrowingStrong', 'BlossomingCoder', 'TokenMaster'] as AchievementId[],
},
{
name: 'Code Generation',
description: 'Achievements for generating code',
ids: ['HelloWorld', 'CodeWizard', 'ThousandBlocks'] as AchievementId[],
},
{
name: 'File Operations',
description: 'Working with files and projects',
ids: ['FileManipulator', 'FileArchitect'] as AchievementId[],
},
{
name: 'Conversations',
description: 'Building our relationship through chat',
ids: ['ConversationStarter', 'ChattyKathy', 'Conversationalist'] as AchievementId[],
},
{
name: 'Tools & Skills',
description: 'Mastering different tools',
ids: ['Toolsmith', 'ToolMaster'] as AchievementId[],
},
{
name: 'Time-Based',
description: 'When you code matters too!',
ids: ['EarlyBird', 'NightOwl', 'AllNighter', 'WeekendWarrior', 'DedicatedDeveloper'] as AchievementId[],
},
{
name: 'Search & Explore',
description: 'Finding what you need',
ids: ['Explorer', 'MasterSearcher'] as AchievementId[],
},
{
name: 'Session Records',
description: 'Your coding session achievements',
ids: ['QuickSession', 'FocusedWork', 'DeepDive', 'MarathonSession', 'MarathonCoder'] as AchievementId[],
},
{
name: 'Special',
description: 'Unique accomplishments',
ids: ['FirstMessage', 'FirstTool', 'FirstCodeBlock', 'FirstFileEdit', 'Polyglot', 'SpeedCoder', 'ClaudeConnoisseur'] as AchievementId[],
},
];