package collections import ( "fmt" "time" "eq2emu/internal/database" ) // Collection represents a collection that players can complete type Collection struct { ID int32 `json:"id"` Name string `json:"name"` Category string `json:"category"` Level int8 `json:"level"` RewardCoin int64 `json:"reward_coin"` RewardXP int64 `json:"reward_xp"` Completed bool `json:"completed"` SaveNeeded bool `json:"-"` CollectionItems []CollectionItem `json:"collection_items"` RewardItems []CollectionRewardItem `json:"reward_items"` SelectableRewardItems []CollectionRewardItem `json:"selectable_reward_items"` LastModified time.Time `json:"last_modified"` db *database.Database `json:"-"` isNew bool `json:"-"` } // CollectionItem represents an item required for a collection type CollectionItem struct { ItemID int32 `json:"item_id"` Index int8 `json:"index"` Found int8 `json:"found"` } // CollectionRewardItem represents a reward item for completing a collection type CollectionRewardItem struct { ItemID int32 `json:"item_id"` Quantity int8 `json:"quantity"` } // New creates a new collection with the given database func New(db *database.Database) *Collection { return &Collection{ db: db, isNew: true, CollectionItems: make([]CollectionItem, 0), RewardItems: make([]CollectionRewardItem, 0), SelectableRewardItems: make([]CollectionRewardItem, 0), LastModified: time.Now(), } } // NewWithData creates a new collection with data func NewWithData(id int32, name, category string, level int8, db *database.Database) *Collection { return &Collection{ ID: id, Name: name, Category: category, Level: level, db: db, isNew: true, CollectionItems: make([]CollectionItem, 0), RewardItems: make([]CollectionRewardItem, 0), SelectableRewardItems: make([]CollectionRewardItem, 0), LastModified: time.Now(), } } // Load loads a collection by ID from the database func Load(db *database.Database, id int32) (*Collection, error) { collection := &Collection{ db: db, isNew: false, CollectionItems: make([]CollectionItem, 0), RewardItems: make([]CollectionRewardItem, 0), SelectableRewardItems: make([]CollectionRewardItem, 0), } // Load collection base data query := `SELECT id, collection_name, collection_category, level FROM collections WHERE id = ?` row := db.QueryRow(query, id) err := row.Scan(&collection.ID, &collection.Name, &collection.Category, &collection.Level) if err != nil { return nil, fmt.Errorf("failed to load collection %d: %w", id, err) } // Load collection items itemQuery := `SELECT item_id, item_index, found FROM collection_items WHERE collection_id = ? ORDER BY item_index` rows, err := db.Query(itemQuery, id) if err != nil { return nil, fmt.Errorf("failed to load collection items: %w", err) } defer rows.Close() for rows.Next() { var item CollectionItem if err := rows.Scan(&item.ItemID, &item.Index, &item.Found); err != nil { return nil, fmt.Errorf("failed to scan collection item: %w", err) } collection.CollectionItems = append(collection.CollectionItems, item) } // Load reward data rewardQuery := `SELECT reward_type, reward_value, reward_quantity FROM collection_rewards WHERE collection_id = ?` rows, err = db.Query(rewardQuery, id) if err != nil { return nil, fmt.Errorf("failed to load collection rewards: %w", err) } defer rows.Close() for rows.Next() { var rewardType string var rewardValue string var quantity int8 if err := rows.Scan(&rewardType, &rewardValue, &quantity); err != nil { return nil, fmt.Errorf("failed to scan collection reward: %w", err) } switch rewardType { case "coin": fmt.Sscanf(rewardValue, "%d", &collection.RewardCoin) case "xp": fmt.Sscanf(rewardValue, "%d", &collection.RewardXP) case "item": var itemID int32 fmt.Sscanf(rewardValue, "%d", &itemID) collection.RewardItems = append(collection.RewardItems, CollectionRewardItem{ ItemID: itemID, Quantity: quantity, }) case "selectable_item": var itemID int32 fmt.Sscanf(rewardValue, "%d", &itemID) collection.SelectableRewardItems = append(collection.SelectableRewardItems, CollectionRewardItem{ ItemID: itemID, Quantity: quantity, }) } } collection.LastModified = time.Now() return collection, nil } // GetID returns the collection ID (implements Identifiable interface) func (c *Collection) GetID() int32 { return c.ID } // GetName returns the collection name func (c *Collection) GetName() string { return c.Name } // GetCategory returns the collection category func (c *Collection) GetCategory() string { return c.Category } // GetLevel returns the collection level func (c *Collection) GetLevel() int8 { return c.Level } // GetIsReadyToTurnIn returns true if all items have been found func (c *Collection) GetIsReadyToTurnIn() bool { if c.Completed { return false } for _, item := range c.CollectionItems { if item.Found == 0 { return false } } return true } // NeedsItem checks if the collection needs a specific item func (c *Collection) NeedsItem(itemID int32) bool { for _, item := range c.CollectionItems { if item.ItemID == itemID && item.Found == 0 { return true } } return false } // GetCollectionItemByItemID returns the collection item by item ID func (c *Collection) GetCollectionItemByItemID(itemID int32) *CollectionItem { for i := range c.CollectionItems { if c.CollectionItems[i].ItemID == itemID { return &c.CollectionItems[i] } } return nil } // MarkItemFound marks an item as found in the collection func (c *Collection) MarkItemFound(itemID int32) bool { for i := range c.CollectionItems { if c.CollectionItems[i].ItemID == itemID && c.CollectionItems[i].Found == 0 { c.CollectionItems[i].Found = 1 c.SaveNeeded = true c.LastModified = time.Now() return true } } return false } // GetProgress returns the collection progress percentage func (c *Collection) GetProgress() float64 { if len(c.CollectionItems) == 0 { return 0.0 } found := 0 for _, item := range c.CollectionItems { if item.Found != 0 { found++ } } return float64(found) / float64(len(c.CollectionItems)) * 100.0 } // IsNew returns true if this is a new collection not yet saved to database func (c *Collection) IsNew() bool { return c.isNew } // Save saves the collection to the database func (c *Collection) Save() error { if c.db == nil { return fmt.Errorf("no database connection available") } if c.isNew { return c.insert() } return c.update() } // Delete removes the collection from the database func (c *Collection) Delete() error { if c.db == nil { return fmt.Errorf("no database connection available") } if c.isNew { return fmt.Errorf("cannot delete unsaved collection") } // Delete collection items first _, err := c.db.Exec(`DELETE FROM collection_items WHERE collection_id = ?`, c.ID) if err != nil { return fmt.Errorf("failed to delete collection items: %w", err) } // Delete collection rewards _, err = c.db.Exec(`DELETE FROM collection_rewards WHERE collection_id = ?`, c.ID) if err != nil { return fmt.Errorf("failed to delete collection rewards: %w", err) } // Delete collection _, err = c.db.Exec(`DELETE FROM collections WHERE id = ?`, c.ID) if err != nil { return fmt.Errorf("failed to delete collection %d: %w", c.ID, err) } return nil } // Reload reloads the collection data from the database func (c *Collection) Reload() error { if c.db == nil { return fmt.Errorf("no database connection available") } if c.isNew { return fmt.Errorf("cannot reload unsaved collection") } reloaded, err := Load(c.db, c.ID) if err != nil { return err } // Copy reloaded data c.Name = reloaded.Name c.Category = reloaded.Category c.Level = reloaded.Level c.RewardCoin = reloaded.RewardCoin c.RewardXP = reloaded.RewardXP c.CollectionItems = reloaded.CollectionItems c.RewardItems = reloaded.RewardItems c.SelectableRewardItems = reloaded.SelectableRewardItems c.LastModified = reloaded.LastModified return nil } // Clone creates a copy of the collection func (c *Collection) Clone() *Collection { newCollection := &Collection{ ID: c.ID, Name: c.Name, Category: c.Category, Level: c.Level, RewardCoin: c.RewardCoin, RewardXP: c.RewardXP, Completed: c.Completed, SaveNeeded: c.SaveNeeded, db: c.db, isNew: true, // Clone is always new CollectionItems: make([]CollectionItem, len(c.CollectionItems)), RewardItems: make([]CollectionRewardItem, len(c.RewardItems)), SelectableRewardItems: make([]CollectionRewardItem, len(c.SelectableRewardItems)), LastModified: time.Now(), } copy(newCollection.CollectionItems, c.CollectionItems) copy(newCollection.RewardItems, c.RewardItems) copy(newCollection.SelectableRewardItems, c.SelectableRewardItems) return newCollection } // insert inserts a new collection into the database func (c *Collection) insert() error { query := `INSERT INTO collections (collection_name, collection_category, level) VALUES (?, ?, ?)` result, err := c.db.Exec(query, c.Name, c.Category, c.Level) if err != nil { return fmt.Errorf("failed to insert collection: %w", err) } id, err := result.LastInsertId() if err != nil { return fmt.Errorf("failed to get inserted collection ID: %w", err) } c.ID = int32(id) // Insert collection items for _, item := range c.CollectionItems { _, err = c.db.Exec(`INSERT INTO collection_items (collection_id, item_id, item_index, found) VALUES (?, ?, ?, ?)`, c.ID, item.ItemID, item.Index, item.Found) if err != nil { return fmt.Errorf("failed to insert collection item: %w", err) } } // Insert rewards if c.RewardCoin > 0 { _, err = c.db.Exec(`INSERT INTO collection_rewards (collection_id, reward_type, reward_value, reward_quantity) VALUES (?, 'coin', ?, 1)`, c.ID, fmt.Sprintf("%d", c.RewardCoin)) if err != nil { return fmt.Errorf("failed to insert coin reward: %w", err) } } if c.RewardXP > 0 { _, err = c.db.Exec(`INSERT INTO collection_rewards (collection_id, reward_type, reward_value, reward_quantity) VALUES (?, 'xp', ?, 1)`, c.ID, fmt.Sprintf("%d", c.RewardXP)) if err != nil { return fmt.Errorf("failed to insert XP reward: %w", err) } } for _, reward := range c.RewardItems { _, err = c.db.Exec(`INSERT INTO collection_rewards (collection_id, reward_type, reward_value, reward_quantity) VALUES (?, 'item', ?, ?)`, c.ID, fmt.Sprintf("%d", reward.ItemID), reward.Quantity) if err != nil { return fmt.Errorf("failed to insert item reward: %w", err) } } for _, reward := range c.SelectableRewardItems { _, err = c.db.Exec(`INSERT INTO collection_rewards (collection_id, reward_type, reward_value, reward_quantity) VALUES (?, 'selectable_item', ?, ?)`, c.ID, fmt.Sprintf("%d", reward.ItemID), reward.Quantity) if err != nil { return fmt.Errorf("failed to insert selectable item reward: %w", err) } } c.isNew = false c.SaveNeeded = false return nil } // update updates an existing collection in the database func (c *Collection) update() error { query := `UPDATE collections SET collection_name = ?, collection_category = ?, level = ? WHERE id = ?` result, err := c.db.Exec(query, c.Name, c.Category, c.Level, c.ID) if err != nil { return fmt.Errorf("failed to update collection: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get rows affected: %w", err) } if rowsAffected == 0 { return fmt.Errorf("collection %d not found for update", c.ID) } // Update collection items (just update found status) for _, item := range c.CollectionItems { _, err = c.db.Exec(`UPDATE collection_items SET found = ? WHERE collection_id = ? AND item_id = ?`, item.Found, c.ID, item.ItemID) if err != nil { return fmt.Errorf("failed to update collection item: %w", err) } } c.SaveNeeded = false return nil }