diff --git a/internal/achievements/database.go b/internal/achievements/database.go new file mode 100644 index 0000000..032ff90 --- /dev/null +++ b/internal/achievements/database.go @@ -0,0 +1,301 @@ +package achievements + +import ( + "eq2emu/internal/database" + "fmt" + "time" +) + +// LoadAllAchievements loads all achievements from database into master list +func LoadAllAchievements(db *database.DB, masterList *MasterList) error { + query := `SELECT achievement_id, title, uncompleted_text, completed_text, + category, expansion, icon, point_value, qty_req, hide_achievement, + unknown3a, unknown3b FROM achievements` + + err := db.Query(query, func(row *database.Row) error { + achievement := NewAchievement() + achievement.ID = uint32(row.Int(0)) + achievement.Title = row.Text(1) + achievement.UncompletedText = row.Text(2) + achievement.CompletedText = row.Text(3) + achievement.Category = row.Text(4) + achievement.Expansion = row.Text(5) + achievement.Icon = uint16(row.Int(6)) + achievement.PointValue = uint32(row.Int(7)) + achievement.QtyRequired = uint32(row.Int(8)) + achievement.Hide = row.Bool(9) + achievement.Unknown3A = uint32(row.Int(10)) + achievement.Unknown3B = uint32(row.Int(11)) + + // Load requirements and rewards + if err := loadAchievementRequirements(db, achievement); err != nil { + return fmt.Errorf("failed to load requirements for achievement %d: %w", achievement.ID, err) + } + + if err := loadAchievementRewards(db, achievement); err != nil { + return fmt.Errorf("failed to load rewards for achievement %d: %w", achievement.ID, err) + } + + if !masterList.AddAchievement(achievement) { + return fmt.Errorf("duplicate achievement ID: %d", achievement.ID) + } + + return nil + }) + + return err +} + +// loadAchievementRequirements loads requirements for a specific achievement +func loadAchievementRequirements(db *database.DB, achievement *Achievement) error { + query := `SELECT achievement_id, name, qty_req + FROM achievements_requirements + WHERE achievement_id = ?` + + return db.Query(query, func(row *database.Row) error { + req := Requirement{ + AchievementID: uint32(row.Int(0)), + Name: row.Text(1), + QtyRequired: uint32(row.Int(2)), + } + achievement.AddRequirement(req) + return nil + }, achievement.ID) +} + +// loadAchievementRewards loads rewards for a specific achievement +func loadAchievementRewards(db *database.DB, achievement *Achievement) error { + query := `SELECT achievement_id, reward + FROM achievements_rewards + WHERE achievement_id = ?` + + return db.Query(query, func(row *database.Row) error { + reward := Reward{ + AchievementID: uint32(row.Int(0)), + Reward: row.Text(1), + } + achievement.AddReward(reward) + return nil + }, achievement.ID) +} + +// LoadPlayerAchievements loads player achievements from database +func LoadPlayerAchievements(db *database.DB, playerID uint32, playerList *PlayerList) error { + query := `SELECT achievement_id, title, uncompleted_text, completed_text, + category, expansion, icon, point_value, qty_req, hide_achievement, + unknown3a, unknown3b FROM achievements` + + err := db.Query(query, func(row *database.Row) error { + achievement := NewAchievement() + achievement.ID = uint32(row.Int(0)) + achievement.Title = row.Text(1) + achievement.UncompletedText = row.Text(2) + achievement.CompletedText = row.Text(3) + achievement.Category = row.Text(4) + achievement.Expansion = row.Text(5) + achievement.Icon = uint16(row.Int(6)) + achievement.PointValue = uint32(row.Int(7)) + achievement.QtyRequired = uint32(row.Int(8)) + achievement.Hide = row.Bool(9) + achievement.Unknown3A = uint32(row.Int(10)) + achievement.Unknown3B = uint32(row.Int(11)) + + // Load requirements and rewards + if err := loadAchievementRequirements(db, achievement); err != nil { + return fmt.Errorf("failed to load requirements: %w", err) + } + + if err := loadAchievementRewards(db, achievement); err != nil { + return fmt.Errorf("failed to load rewards: %w", err) + } + + if !playerList.AddAchievement(achievement) { + return fmt.Errorf("duplicate achievement ID: %d", achievement.ID) + } + + return nil + }) + + return err +} + +// LoadPlayerAchievementUpdates loads player achievement progress from database +func LoadPlayerAchievementUpdates(db *database.DB, playerID uint32, updateList *PlayerUpdateList) error { + query := `SELECT char_id, achievement_id, completed_date + FROM character_achievements + WHERE char_id = ?` + + return db.Query(query, func(row *database.Row) error { + update := NewUpdate() + update.ID = uint32(row.Int(1)) + + // Convert completed_date from Unix timestamp + if !row.IsNull(2) { + timestamp := row.Int64(2) + update.CompletedDate = time.Unix(timestamp, 0) + } + + // Load update items + if err := loadPlayerAchievementUpdateItems(db, playerID, update); err != nil { + return fmt.Errorf("failed to load update items: %w", err) + } + + if !updateList.AddUpdate(update) { + return fmt.Errorf("duplicate achievement update ID: %d", update.ID) + } + + return nil + }, playerID) +} + +// loadPlayerAchievementUpdateItems loads progress items for an achievement update +func loadPlayerAchievementUpdateItems(db *database.DB, playerID uint32, update *Update) error { + query := `SELECT achievement_id, items + FROM character_achievements_items + WHERE char_id = ? AND achievement_id = ?` + + return db.Query(query, func(row *database.Row) error { + item := UpdateItem{ + AchievementID: uint32(row.Int(0)), + ItemUpdate: uint32(row.Int(1)), + } + update.AddUpdateItem(item) + return nil + }, playerID, update.ID) +} + +// SavePlayerAchievementUpdate saves or updates player achievement progress +func SavePlayerAchievementUpdate(db *database.DB, playerID uint32, update *Update) error { + return db.Transaction(func(tx *database.DB) error { + // Save or update main achievement record + query := `INSERT OR REPLACE INTO character_achievements + (char_id, achievement_id, completed_date) VALUES (?, ?, ?)` + + var completedDate *int64 + if !update.CompletedDate.IsZero() { + timestamp := update.CompletedDate.Unix() + completedDate = ×tamp + } + + if err := tx.Exec(query, playerID, update.ID, completedDate); err != nil { + return fmt.Errorf("failed to save achievement update: %w", err) + } + + // Delete existing update items + deleteQuery := `DELETE FROM character_achievements_items + WHERE char_id = ? AND achievement_id = ?` + if err := tx.Exec(deleteQuery, playerID, update.ID); err != nil { + return fmt.Errorf("failed to delete old update items: %w", err) + } + + // Insert new update items + itemQuery := `INSERT INTO character_achievements_items + (char_id, achievement_id, items) VALUES (?, ?, ?)` + for _, item := range update.UpdateItems { + if err := tx.Exec(itemQuery, playerID, item.AchievementID, item.ItemUpdate); err != nil { + return fmt.Errorf("failed to save update item: %w", err) + } + } + + return nil + }) +} + +// DeletePlayerAchievementUpdate removes player achievement progress from database +func DeletePlayerAchievementUpdate(db *database.DB, playerID uint32, achievementID uint32) error { + return db.Transaction(func(tx *database.DB) error { + // Delete main achievement record + query := `DELETE FROM character_achievements + WHERE char_id = ? AND achievement_id = ?` + if err := tx.Exec(query, playerID, achievementID); err != nil { + return fmt.Errorf("failed to delete achievement update: %w", err) + } + + // Delete update items + itemQuery := `DELETE FROM character_achievements_items + WHERE char_id = ? AND achievement_id = ?` + if err := tx.Exec(itemQuery, playerID, achievementID); err != nil { + return fmt.Errorf("failed to delete update items: %w", err) + } + + return nil + }) +} + +// SaveAchievement saves or updates an achievement in the database +func SaveAchievement(db *database.DB, achievement *Achievement) error { + return db.Transaction(func(tx *database.DB) error { + // Save main achievement record + query := `INSERT OR REPLACE INTO achievements + (achievement_id, title, uncompleted_text, completed_text, + category, expansion, icon, point_value, qty_req, + hide_achievement, unknown3a, unknown3b) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + + if err := tx.Exec(query, achievement.ID, achievement.Title, + achievement.UncompletedText, achievement.CompletedText, + achievement.Category, achievement.Expansion, achievement.Icon, + achievement.PointValue, achievement.QtyRequired, achievement.Hide, + achievement.Unknown3A, achievement.Unknown3B); err != nil { + return fmt.Errorf("failed to save achievement: %w", err) + } + + // Delete existing requirements and rewards + if err := tx.Exec("DELETE FROM achievements_requirements WHERE achievement_id = ?", achievement.ID); err != nil { + return fmt.Errorf("failed to delete old requirements: %w", err) + } + if err := tx.Exec("DELETE FROM achievements_rewards WHERE achievement_id = ?", achievement.ID); err != nil { + return fmt.Errorf("failed to delete old rewards: %w", err) + } + + // Insert requirements + reqQuery := `INSERT INTO achievements_requirements + (achievement_id, name, qty_req) VALUES (?, ?, ?)` + for _, req := range achievement.Requirements { + if err := tx.Exec(reqQuery, req.AchievementID, req.Name, req.QtyRequired); err != nil { + return fmt.Errorf("failed to save requirement: %w", err) + } + } + + // Insert rewards + rewardQuery := `INSERT INTO achievements_rewards + (achievement_id, reward) VALUES (?, ?)` + for _, reward := range achievement.Rewards { + if err := tx.Exec(rewardQuery, reward.AchievementID, reward.Reward); err != nil { + return fmt.Errorf("failed to save reward: %w", err) + } + } + + return nil + }) +} + +// DeleteAchievement removes an achievement and all related records from database +func DeleteAchievement(db *database.DB, achievementID uint32) error { + return db.Transaction(func(tx *database.DB) error { + // Delete main achievement + if err := tx.Exec("DELETE FROM achievements WHERE achievement_id = ?", achievementID); err != nil { + return fmt.Errorf("failed to delete achievement: %w", err) + } + + // Delete requirements + if err := tx.Exec("DELETE FROM achievements_requirements WHERE achievement_id = ?", achievementID); err != nil { + return fmt.Errorf("failed to delete requirements: %w", err) + } + + // Delete rewards + if err := tx.Exec("DELETE FROM achievements_rewards WHERE achievement_id = ?", achievementID); err != nil { + return fmt.Errorf("failed to delete rewards: %w", err) + } + + // Delete player progress (optional - might want to preserve history) + if err := tx.Exec("DELETE FROM character_achievements WHERE achievement_id = ?", achievementID); err != nil { + return fmt.Errorf("failed to delete player achievements: %w", err) + } + if err := tx.Exec("DELETE FROM character_achievements_items WHERE achievement_id = ?", achievementID); err != nil { + return fmt.Errorf("failed to delete player achievement items: %w", err) + } + + return nil + }) +} diff --git a/internal/achievements/doc.go b/internal/achievements/doc.go new file mode 100644 index 0000000..65e6b36 --- /dev/null +++ b/internal/achievements/doc.go @@ -0,0 +1,32 @@ +// Package achievements provides a complete achievement system for EQ2Emulator servers. +// +// The package includes: +// - Achievement definitions with requirements and rewards +// - Master achievement list for server-wide management +// - Player-specific achievement tracking and progress +// - Database operations for persistence +// +// Basic usage: +// +// // Create master list and load from database +// masterList := achievements.NewMasterList() +// db, _ := database.Open("world.db") +// achievements.LoadAllAchievements(db, masterList) +// +// // Create player manager +// playerMgr := achievements.NewPlayerManager() +// achievements.LoadPlayerAchievements(db, playerID, playerMgr.Achievements) +// achievements.LoadPlayerAchievementUpdates(db, playerID, playerMgr.Updates) +// +// // Update player progress +// playerMgr.Updates.UpdateProgress(achievementID, newProgress) +// +// // Check completion +// if playerMgr.Updates.IsCompleted(achievementID) { +// // Handle completed achievement +// } +// +// // Save progress +// update := playerMgr.Updates.GetUpdate(achievementID) +// achievements.SavePlayerAchievementUpdate(db, playerID, update) +package achievements diff --git a/internal/achievements/master.go b/internal/achievements/master.go new file mode 100644 index 0000000..e18911b --- /dev/null +++ b/internal/achievements/master.go @@ -0,0 +1,197 @@ +package achievements + +import ( + "fmt" + "sync" +) + +// MasterList manages the global list of all achievements +type MasterList struct { + achievements map[uint32]*Achievement + mutex sync.RWMutex +} + +// NewMasterList creates a new master achievement list +func NewMasterList() *MasterList { + return &MasterList{ + achievements: make(map[uint32]*Achievement), + } +} + +// AddAchievement adds an achievement to the master list +// Returns false if achievement with same ID already exists +func (m *MasterList) AddAchievement(achievement *Achievement) bool { + if achievement == nil { + return false + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + if _, exists := m.achievements[achievement.ID]; exists { + return false + } + + m.achievements[achievement.ID] = achievement + return true +} + +// GetAchievement retrieves an achievement by ID +// Returns nil if not found +func (m *MasterList) GetAchievement(id uint32) *Achievement { + m.mutex.RLock() + defer m.mutex.RUnlock() + + return m.achievements[id] +} + +// GetAchievementClone retrieves a cloned copy of an achievement by ID +// Returns nil if not found. Safe for modification without affecting master list +func (m *MasterList) GetAchievementClone(id uint32) *Achievement { + m.mutex.RLock() + achievement := m.achievements[id] + m.mutex.RUnlock() + + if achievement == nil { + return nil + } + + return achievement.Clone() +} + +// GetAllAchievements returns a map of all achievements (read-only access) +// The returned map should not be modified +func (m *MasterList) GetAllAchievements() map[uint32]*Achievement { + m.mutex.RLock() + defer m.mutex.RUnlock() + + // Return copy of map to prevent external modification + result := make(map[uint32]*Achievement, len(m.achievements)) + for id, achievement := range m.achievements { + result[id] = achievement + } + return result +} + +// GetAchievementsByCategory returns achievements filtered by category +func (m *MasterList) GetAchievementsByCategory(category string) []*Achievement { + m.mutex.RLock() + defer m.mutex.RUnlock() + + var result []*Achievement + for _, achievement := range m.achievements { + if achievement.Category == category { + result = append(result, achievement) + } + } + return result +} + +// GetAchievementsByExpansion returns achievements filtered by expansion +func (m *MasterList) GetAchievementsByExpansion(expansion string) []*Achievement { + m.mutex.RLock() + defer m.mutex.RUnlock() + + var result []*Achievement + for _, achievement := range m.achievements { + if achievement.Expansion == expansion { + result = append(result, achievement) + } + } + return result +} + +// RemoveAchievement removes an achievement from the master list +// Returns true if achievement was found and removed +func (m *MasterList) RemoveAchievement(id uint32) bool { + m.mutex.Lock() + defer m.mutex.Unlock() + + if _, exists := m.achievements[id]; !exists { + return false + } + + delete(m.achievements, id) + return true +} + +// UpdateAchievement updates an existing achievement +// Returns error if achievement doesn't exist +func (m *MasterList) UpdateAchievement(achievement *Achievement) error { + if achievement == nil { + return fmt.Errorf("achievement cannot be nil") + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + if _, exists := m.achievements[achievement.ID]; !exists { + return fmt.Errorf("achievement with ID %d does not exist", achievement.ID) + } + + m.achievements[achievement.ID] = achievement + return nil +} + +// Clear removes all achievements from the master list +func (m *MasterList) Clear() { + m.mutex.Lock() + defer m.mutex.Unlock() + + m.achievements = make(map[uint32]*Achievement) +} + +// Size returns the number of achievements in the master list +func (m *MasterList) Size() int { + m.mutex.RLock() + defer m.mutex.RUnlock() + + return len(m.achievements) +} + +// Exists checks if an achievement with given ID exists +func (m *MasterList) Exists(id uint32) bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + + _, exists := m.achievements[id] + return exists +} + +// GetCategories returns all unique categories +func (m *MasterList) GetCategories() []string { + m.mutex.RLock() + defer m.mutex.RUnlock() + + categories := make(map[string]bool) + for _, achievement := range m.achievements { + if achievement.Category != "" { + categories[achievement.Category] = true + } + } + + result := make([]string, 0, len(categories)) + for category := range categories { + result = append(result, category) + } + return result +} + +// GetExpansions returns all unique expansions +func (m *MasterList) GetExpansions() []string { + m.mutex.RLock() + defer m.mutex.RUnlock() + + expansions := make(map[string]bool) + for _, achievement := range m.achievements { + if achievement.Expansion != "" { + expansions[achievement.Expansion] = true + } + } + + result := make([]string, 0, len(expansions)) + for expansion := range expansions { + result = append(result, expansion) + } + return result +} diff --git a/internal/achievements/player.go b/internal/achievements/player.go new file mode 100644 index 0000000..d5637c5 --- /dev/null +++ b/internal/achievements/player.go @@ -0,0 +1,282 @@ +package achievements + +import ( + "fmt" + "time" +) + +// PlayerList manages achievements for a specific player +type PlayerList struct { + achievements map[uint32]*Achievement +} + +// PlayerUpdateList manages achievement updates/progress for a specific player +type PlayerUpdateList struct { + updates map[uint32]*Update +} + +// NewPlayerList creates a new player achievement list +func NewPlayerList() *PlayerList { + return &PlayerList{ + achievements: make(map[uint32]*Achievement), + } +} + +// NewPlayerUpdateList creates a new player achievement update list +func NewPlayerUpdateList() *PlayerUpdateList { + return &PlayerUpdateList{ + updates: make(map[uint32]*Update), + } +} + +// AddAchievement adds an achievement to the player's list +// Returns false if achievement with same ID already exists +func (p *PlayerList) AddAchievement(achievement *Achievement) bool { + if achievement == nil { + return false + } + + if _, exists := p.achievements[achievement.ID]; exists { + return false + } + + p.achievements[achievement.ID] = achievement + return true +} + +// GetAchievement retrieves an achievement by ID +// Returns nil if not found +func (p *PlayerList) GetAchievement(id uint32) *Achievement { + return p.achievements[id] +} + +// GetAllAchievements returns all player achievements +func (p *PlayerList) GetAllAchievements() map[uint32]*Achievement { + result := make(map[uint32]*Achievement, len(p.achievements)) + for id, achievement := range p.achievements { + result[id] = achievement + } + return result +} + +// RemoveAchievement removes an achievement from the player's list +// Returns true if achievement was found and removed +func (p *PlayerList) RemoveAchievement(id uint32) bool { + if _, exists := p.achievements[id]; !exists { + return false + } + + delete(p.achievements, id) + return true +} + +// HasAchievement checks if player has a specific achievement +func (p *PlayerList) HasAchievement(id uint32) bool { + _, exists := p.achievements[id] + return exists +} + +// Clear removes all achievements from the player's list +func (p *PlayerList) Clear() { + p.achievements = make(map[uint32]*Achievement) +} + +// Size returns the number of achievements in the player's list +func (p *PlayerList) Size() int { + return len(p.achievements) +} + +// GetAchievementsByCategory returns player achievements filtered by category +func (p *PlayerList) GetAchievementsByCategory(category string) []*Achievement { + var result []*Achievement + for _, achievement := range p.achievements { + if achievement.Category == category { + result = append(result, achievement) + } + } + return result +} + +// AddUpdate adds an achievement update to the player's list +// Returns false if update with same ID already exists +func (p *PlayerUpdateList) AddUpdate(update *Update) bool { + if update == nil { + return false + } + + if _, exists := p.updates[update.ID]; exists { + return false + } + + p.updates[update.ID] = update + return true +} + +// GetUpdate retrieves an achievement update by ID +// Returns nil if not found +func (p *PlayerUpdateList) GetUpdate(id uint32) *Update { + return p.updates[id] +} + +// GetAllUpdates returns all player achievement updates +func (p *PlayerUpdateList) GetAllUpdates() map[uint32]*Update { + result := make(map[uint32]*Update, len(p.updates)) + for id, update := range p.updates { + result[id] = update + } + return result +} + +// UpdateProgress updates or creates achievement progress +func (p *PlayerUpdateList) UpdateProgress(achievementID uint32, itemUpdate uint32) { + update := p.updates[achievementID] + if update == nil { + update = NewUpdate() + update.ID = achievementID + p.updates[achievementID] = update + } + + // Add or update the progress item + found := false + for i := range update.UpdateItems { + if update.UpdateItems[i].AchievementID == achievementID { + update.UpdateItems[i].ItemUpdate = itemUpdate + found = true + break + } + } + + if !found { + update.AddUpdateItem(UpdateItem{ + AchievementID: achievementID, + ItemUpdate: itemUpdate, + }) + } +} + +// CompleteAchievement marks an achievement as completed +func (p *PlayerUpdateList) CompleteAchievement(achievementID uint32) { + update := p.updates[achievementID] + if update == nil { + update = NewUpdate() + update.ID = achievementID + p.updates[achievementID] = update + } + update.CompletedDate = time.Now() +} + +// IsCompleted checks if an achievement is completed +func (p *PlayerUpdateList) IsCompleted(achievementID uint32) bool { + update := p.updates[achievementID] + return update != nil && !update.CompletedDate.IsZero() +} + +// GetCompletedDate returns the completion date for an achievement +// Returns zero time if not completed +func (p *PlayerUpdateList) GetCompletedDate(achievementID uint32) time.Time { + update := p.updates[achievementID] + if update == nil { + return time.Time{} + } + return update.CompletedDate +} + +// GetProgress returns the current progress for an achievement +// Returns 0 if no progress found +func (p *PlayerUpdateList) GetProgress(achievementID uint32) uint32 { + update := p.updates[achievementID] + if update == nil || len(update.UpdateItems) == 0 { + return 0 + } + + // Return the first matching update item's progress + for _, item := range update.UpdateItems { + if item.AchievementID == achievementID { + return item.ItemUpdate + } + } + return 0 +} + +// RemoveUpdate removes an achievement update from the player's list +// Returns true if update was found and removed +func (p *PlayerUpdateList) RemoveUpdate(id uint32) bool { + if _, exists := p.updates[id]; !exists { + return false + } + + delete(p.updates, id) + return true +} + +// Clear removes all updates from the player's list +func (p *PlayerUpdateList) Clear() { + p.updates = make(map[uint32]*Update) +} + +// Size returns the number of updates in the player's list +func (p *PlayerUpdateList) Size() int { + return len(p.updates) +} + +// GetCompletedAchievements returns all completed achievement IDs +func (p *PlayerUpdateList) GetCompletedAchievements() []uint32 { + var completed []uint32 + for id, update := range p.updates { + if !update.CompletedDate.IsZero() { + completed = append(completed, id) + } + } + return completed +} + +// GetInProgressAchievements returns all in-progress achievement IDs +func (p *PlayerUpdateList) GetInProgressAchievements() []uint32 { + var inProgress []uint32 + for id, update := range p.updates { + if update.CompletedDate.IsZero() && len(update.UpdateItems) > 0 { + inProgress = append(inProgress, id) + } + } + return inProgress +} + +// PlayerManager combines achievement list and update list for a player +type PlayerManager struct { + Achievements *PlayerList + Updates *PlayerUpdateList +} + +// NewPlayerManager creates a new player manager +func NewPlayerManager() *PlayerManager { + return &PlayerManager{ + Achievements: NewPlayerList(), + Updates: NewPlayerUpdateList(), + } +} + +// CheckRequirements validates if player meets achievement requirements +// This is a basic implementation - extend as needed for specific game logic +func (pm *PlayerManager) CheckRequirements(achievement *Achievement) (bool, error) { + if achievement == nil { + return false, fmt.Errorf("achievement cannot be nil") + } + + // Basic implementation - check if we have progress >= required quantity + progress := pm.Updates.GetProgress(achievement.ID) + return progress >= achievement.QtyRequired, nil +} + +// GetCompletionStatus returns completion percentage for an achievement +func (pm *PlayerManager) GetCompletionStatus(achievement *Achievement) float64 { + if achievement == nil || achievement.QtyRequired == 0 { + return 0.0 + } + + progress := pm.Updates.GetProgress(achievement.ID) + if progress >= achievement.QtyRequired { + return 100.0 + } + + return (float64(progress) / float64(achievement.QtyRequired)) * 100.0 +} diff --git a/internal/achievements/types.go b/internal/achievements/types.go new file mode 100644 index 0000000..bda4d16 --- /dev/null +++ b/internal/achievements/types.go @@ -0,0 +1,113 @@ +package achievements + +import "time" + +// Requirement represents a single achievement requirement +type Requirement struct { + AchievementID uint32 `json:"achievement_id"` + Name string `json:"name"` + QtyRequired uint32 `json:"qty_required"` +} + +// Reward represents a single achievement reward +type Reward struct { + AchievementID uint32 `json:"achievement_id"` + Reward string `json:"reward"` +} + +// Achievement represents a complete achievement definition +type Achievement struct { + ID uint32 `json:"id"` + Title string `json:"title"` + UncompletedText string `json:"uncompleted_text"` + CompletedText string `json:"completed_text"` + Category string `json:"category"` + Expansion string `json:"expansion"` + Icon uint16 `json:"icon"` + PointValue uint32 `json:"point_value"` + QtyRequired uint32 `json:"qty_required"` + Hide bool `json:"hide"` + Unknown3A uint32 `json:"unknown3a"` + Unknown3B uint32 `json:"unknown3b"` + Requirements []Requirement `json:"requirements"` + Rewards []Reward `json:"rewards"` +} + +// UpdateItem represents a single achievement progress update +type UpdateItem struct { + AchievementID uint32 `json:"achievement_id"` + ItemUpdate uint32 `json:"item_update"` +} + +// Update represents achievement completion/progress data +type Update struct { + ID uint32 `json:"id"` + CompletedDate time.Time `json:"completed_date"` + UpdateItems []UpdateItem `json:"update_items"` +} + +// NewAchievement creates a new achievement with empty slices +func NewAchievement() *Achievement { + return &Achievement{ + Requirements: make([]Requirement, 0), + Rewards: make([]Reward, 0), + } +} + +// NewUpdate creates a new achievement update with empty slices +func NewUpdate() *Update { + return &Update{ + UpdateItems: make([]UpdateItem, 0), + } +} + +// AddRequirement adds a requirement to the achievement +func (a *Achievement) AddRequirement(req Requirement) { + a.Requirements = append(a.Requirements, req) +} + +// AddReward adds a reward to the achievement +func (a *Achievement) AddReward(reward Reward) { + a.Rewards = append(a.Rewards, reward) +} + +// AddUpdateItem adds an update item to the achievement update +func (u *Update) AddUpdateItem(item UpdateItem) { + u.UpdateItems = append(u.UpdateItems, item) +} + +// Clone creates a deep copy of the achievement +func (a *Achievement) Clone() *Achievement { + clone := &Achievement{ + ID: a.ID, + Title: a.Title, + UncompletedText: a.UncompletedText, + CompletedText: a.CompletedText, + Category: a.Category, + Expansion: a.Expansion, + Icon: a.Icon, + PointValue: a.PointValue, + QtyRequired: a.QtyRequired, + Hide: a.Hide, + Unknown3A: a.Unknown3A, + Unknown3B: a.Unknown3B, + Requirements: make([]Requirement, len(a.Requirements)), + Rewards: make([]Reward, len(a.Rewards)), + } + + copy(clone.Requirements, a.Requirements) + copy(clone.Rewards, a.Rewards) + return clone +} + +// Clone creates a deep copy of the achievement update +func (u *Update) Clone() *Update { + clone := &Update{ + ID: u.ID, + CompletedDate: u.CompletedDate, + UpdateItems: make([]UpdateItem, len(u.UpdateItems)), + } + + copy(clone.UpdateItems, u.UpdateItems) + return clone +}