generated from nhcarrigan/template
feat: achievements
This commit is contained in:
@@ -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[],
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user