modernize collections
This commit is contained in:
parent
a47ad4f737
commit
6df4b00201
@ -1,315 +0,0 @@
|
||||
# Collections System
|
||||
|
||||
The collections system provides comprehensive achievement-based item collection functionality for EverQuest II server emulation, converted from the original C++ EQ2EMu implementation.
|
||||
|
||||
## Overview
|
||||
|
||||
The collections system allows players to find specific items scattered throughout the game world and combine them into collections for rewards. When players complete a collection by finding all required items, they receive rewards such as experience, coins, items, or a choice of selectable items.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
**Collection** - Individual collection with required items, rewards, and completion tracking
|
||||
**MasterCollectionList** - Registry of all available collections in the game
|
||||
**PlayerCollectionList** - Per-player collection progress and completion tracking
|
||||
**CollectionManager** - High-level collection system coordinator
|
||||
**CollectionService** - Service layer for game integration and client communication
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Item-Based Collections**: Players find specific items to complete collections
|
||||
- **Multiple Reward Types**: Coins, experience points, fixed items, and selectable items
|
||||
- **Progress Tracking**: Real-time tracking of collection completion progress
|
||||
- **Category Organization**: Collections organized by categories for easy browsing
|
||||
- **Level Restrictions**: Collections appropriate for different player levels
|
||||
- **Thread Safety**: All operations use proper Go concurrency patterns
|
||||
- **Database Persistence**: Player progress saved automatically
|
||||
|
||||
## Collection Structure
|
||||
|
||||
### Collection Data
|
||||
- **ID**: Unique collection identifier
|
||||
- **Name**: Display name for the collection
|
||||
- **Category**: Organizational category (e.g., "Artifacts", "Shinies")
|
||||
- **Level**: Recommended level for the collection
|
||||
- **Items**: List of required items with index positions
|
||||
- **Rewards**: Coins, XP, items, and selectable items
|
||||
|
||||
### Item States
|
||||
- **Not Found** (0): Player hasn't found this item yet
|
||||
- **Found** (1): Player has found and added this item to the collection
|
||||
|
||||
### Collection States
|
||||
- **Incomplete**: Not all required items have been found
|
||||
- **Ready to Turn In**: All items found but not yet completed
|
||||
- **Completed**: Collection has been turned in and rewards claimed
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Collections Table
|
||||
```sql
|
||||
CREATE TABLE collections (
|
||||
id INTEGER PRIMARY KEY,
|
||||
collection_name TEXT NOT NULL,
|
||||
collection_category TEXT NOT NULL DEFAULT '',
|
||||
level INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Collection Details Table
|
||||
```sql
|
||||
CREATE TABLE collection_details (
|
||||
collection_id INTEGER NOT NULL,
|
||||
item_id INTEGER NOT NULL,
|
||||
item_index INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (collection_id, item_id),
|
||||
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### Collection Rewards Table
|
||||
```sql
|
||||
CREATE TABLE collection_rewards (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL,
|
||||
reward_type TEXT NOT NULL, -- 'Item', 'Selectable', 'Coin', 'XP'
|
||||
reward_value TEXT NOT NULL,
|
||||
reward_quantity INTEGER NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### Player Collections Table
|
||||
```sql
|
||||
CREATE TABLE character_collections (
|
||||
char_id INTEGER NOT NULL,
|
||||
collection_id INTEGER NOT NULL,
|
||||
completed INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (char_id, collection_id),
|
||||
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### Player Collection Items Table
|
||||
```sql
|
||||
CREATE TABLE character_collection_items (
|
||||
char_id INTEGER NOT NULL,
|
||||
collection_id INTEGER NOT NULL,
|
||||
collection_item_id INTEGER NOT NULL,
|
||||
found_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (char_id, collection_id, collection_item_id),
|
||||
FOREIGN KEY (char_id, collection_id) REFERENCES character_collections(char_id, collection_id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### System Initialization
|
||||
|
||||
```go
|
||||
// Initialize collection service
|
||||
database := NewDatabaseCollectionManager(db)
|
||||
itemLookup := NewItemLookupService()
|
||||
clientManager := NewClientManager()
|
||||
|
||||
service := NewCollectionService(database, itemLookup, clientManager)
|
||||
err := service.Initialize(ctx)
|
||||
```
|
||||
|
||||
### Player Operations
|
||||
|
||||
```go
|
||||
// Load player collections when they log in
|
||||
err := service.LoadPlayerCollections(ctx, characterID)
|
||||
|
||||
// Process when player finds an item
|
||||
err := service.ProcessItemFound(characterID, itemID)
|
||||
|
||||
// Complete a collection
|
||||
rewardProvider := NewRewardProvider()
|
||||
err := service.CompleteCollection(characterID, collectionID, rewardProvider)
|
||||
|
||||
// Get player's collection progress
|
||||
progress, err := service.GetPlayerCollectionProgress(characterID)
|
||||
|
||||
// Unload when player logs out
|
||||
err := service.UnloadPlayerCollections(ctx, characterID)
|
||||
```
|
||||
|
||||
### Collection Management
|
||||
|
||||
```go
|
||||
// Get all collections in a category
|
||||
collections := manager.GetCollectionsByCategory("Artifacts")
|
||||
|
||||
// Search collections by name
|
||||
collections := manager.SearchCollections("Ancient")
|
||||
|
||||
// Get collections appropriate for player level
|
||||
collections := manager.GetAvailableCollections(playerLevel)
|
||||
|
||||
// Check if item is needed by any collection
|
||||
needed := masterList.NeedsItem(itemID)
|
||||
```
|
||||
|
||||
## Reward System
|
||||
|
||||
### Reward Types
|
||||
|
||||
**Coin Rewards**
|
||||
```go
|
||||
collection.SetRewardCoin(50000) // 5 gold
|
||||
```
|
||||
|
||||
**Experience Rewards**
|
||||
```go
|
||||
collection.SetRewardXP(10000) // 10,000 XP
|
||||
```
|
||||
|
||||
**Item Rewards** (automatically given)
|
||||
```go
|
||||
collection.AddRewardItem(CollectionRewardItem{
|
||||
ItemID: 12345,
|
||||
Quantity: 1,
|
||||
})
|
||||
```
|
||||
|
||||
**Selectable Rewards** (player chooses one)
|
||||
```go
|
||||
collection.AddSelectableRewardItem(CollectionRewardItem{
|
||||
ItemID: 12346,
|
||||
Quantity: 1,
|
||||
})
|
||||
```
|
||||
|
||||
## Integration Interfaces
|
||||
|
||||
### ItemLookup
|
||||
Provides item information and validation for collections.
|
||||
|
||||
### ClientManager
|
||||
Handles client communication for collection updates and lists.
|
||||
|
||||
### RewardProvider
|
||||
Manages distribution of collection rewards to players.
|
||||
|
||||
### CollectionEventHandler
|
||||
Handles collection-related events for logging and notifications.
|
||||
|
||||
## Thread Safety
|
||||
|
||||
All operations are thread-safe using:
|
||||
- `sync.RWMutex` for collection and list operations
|
||||
- Atomic updates for collection progress
|
||||
- Database transactions for consistency
|
||||
- Proper locking hierarchies to prevent deadlocks
|
||||
|
||||
## Performance Features
|
||||
|
||||
- **Efficient Lookups**: Hash-based collection and item lookups
|
||||
- **Lazy Loading**: Player collections loaded only when needed
|
||||
- **Batch Operations**: Multiple items and collections processed together
|
||||
- **Connection Pooling**: Efficient database connection management
|
||||
- **Caching**: Master collections cached in memory
|
||||
|
||||
## Event System
|
||||
|
||||
The collections system provides comprehensive event handling:
|
||||
|
||||
```go
|
||||
// Item found event
|
||||
OnItemFound(characterID, collectionID, itemID int32)
|
||||
|
||||
// Collection completed event
|
||||
OnCollectionCompleted(characterID, collectionID int32)
|
||||
|
||||
// Rewards claimed event
|
||||
OnRewardClaimed(characterID, collectionID int32, rewards []CollectionRewardItem, coin, xp int64)
|
||||
```
|
||||
|
||||
## Statistics and Monitoring
|
||||
|
||||
### System Statistics
|
||||
- Total collections available
|
||||
- Collections per category
|
||||
- Total items across all collections
|
||||
- Reward distribution statistics
|
||||
|
||||
### Player Statistics
|
||||
- Collections completed
|
||||
- Collections in progress
|
||||
- Items found
|
||||
- Progress percentages
|
||||
|
||||
## Error Handling
|
||||
|
||||
Comprehensive error handling covers:
|
||||
- Database connection failures
|
||||
- Invalid collection or item IDs
|
||||
- Reward distribution failures
|
||||
- Concurrent access issues
|
||||
- Data validation errors
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Areas marked for future implementation:
|
||||
- Collection discovery mechanics
|
||||
- Rare item collection bonuses
|
||||
- Collection sharing and trading
|
||||
- Achievement integration
|
||||
- Collection leaderboards
|
||||
- Seasonal collections
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
internal/collections/
|
||||
├── README.md # This documentation
|
||||
├── constants.go # Collection constants and limits
|
||||
├── types.go # Core data structures
|
||||
├── interfaces.go # Integration interfaces
|
||||
├── collections.go # Collection implementation
|
||||
├── master_list.go # Master collection registry
|
||||
├── player_list.go # Player collection tracking
|
||||
├── database.go # Database operations
|
||||
└── manager.go # High-level collection services
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `eq2emu/internal/database` - Database wrapper
|
||||
- Standard library: `context`, `sync`, `fmt`, `strings`, `time`
|
||||
|
||||
## Testing
|
||||
|
||||
The collections system is designed for comprehensive testing:
|
||||
- Mock interfaces for all dependencies
|
||||
- Unit tests for collection logic
|
||||
- Integration tests with database
|
||||
- Concurrent operation testing
|
||||
- Performance benchmarking
|
||||
|
||||
## Migration from C++
|
||||
|
||||
Key changes from the C++ implementation:
|
||||
- Go interfaces for better modularity
|
||||
- Context-based operations for cancellation
|
||||
- Proper error handling with wrapped errors
|
||||
- Thread-safe operations using sync primitives
|
||||
- Database connection pooling
|
||||
- Event-driven architecture for notifications
|
||||
|
||||
## Integration Notes
|
||||
|
||||
When integrating with the game server:
|
||||
1. Initialize the collection service at server startup
|
||||
2. Load player collections on character login
|
||||
3. Process item finds during gameplay
|
||||
4. Handle collection completion through UI interactions
|
||||
5. Save collections on logout or periodic intervals
|
||||
6. Clean up resources during server shutdown
|
423
internal/collections/collection.go
Normal file
423
internal/collections/collection.go
Normal file
@ -0,0 +1,423 @@
|
||||
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
|
||||
}
|
325
internal/collections/collection_test.go
Normal file
325
internal/collections/collection_test.go
Normal file
@ -0,0 +1,325 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Test creating a new collection
|
||||
collection := New(db)
|
||||
if collection == nil {
|
||||
t.Fatal("New returned nil")
|
||||
}
|
||||
|
||||
if !collection.IsNew() {
|
||||
t.Error("New collection should be marked as new")
|
||||
}
|
||||
|
||||
if len(collection.CollectionItems) != 0 {
|
||||
t.Error("New collection should have empty items slice")
|
||||
}
|
||||
|
||||
if len(collection.RewardItems) != 0 {
|
||||
t.Error("New collection should have empty reward items slice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWithData(t *testing.T) {
|
||||
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
collection := NewWithData(100, "Test Collection", "Heritage", 20, db)
|
||||
if collection == nil {
|
||||
t.Fatal("NewWithData returned nil")
|
||||
}
|
||||
|
||||
if collection.GetID() != 100 {
|
||||
t.Errorf("Expected ID 100, got %d", collection.GetID())
|
||||
}
|
||||
|
||||
if collection.GetName() != "Test Collection" {
|
||||
t.Errorf("Expected name 'Test Collection', got '%s'", collection.GetName())
|
||||
}
|
||||
|
||||
if collection.GetCategory() != "Heritage" {
|
||||
t.Errorf("Expected category 'Heritage', got '%s'", collection.GetCategory())
|
||||
}
|
||||
|
||||
if collection.GetLevel() != 20 {
|
||||
t.Errorf("Expected level 20, got %d", collection.GetLevel())
|
||||
}
|
||||
|
||||
if !collection.IsNew() {
|
||||
t.Error("NewWithData should create new collection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionItems(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
collection := NewWithData(100, "Test", "Heritage", 20, db)
|
||||
|
||||
// Add collection items
|
||||
collection.CollectionItems = append(collection.CollectionItems, CollectionItem{
|
||||
ItemID: 12345,
|
||||
Index: 0,
|
||||
Found: ItemNotFound,
|
||||
})
|
||||
|
||||
collection.CollectionItems = append(collection.CollectionItems, CollectionItem{
|
||||
ItemID: 12346,
|
||||
Index: 1,
|
||||
Found: ItemNotFound,
|
||||
})
|
||||
|
||||
// Test NeedsItem
|
||||
if !collection.NeedsItem(12345) {
|
||||
t.Error("Collection should need item 12345")
|
||||
}
|
||||
|
||||
if collection.NeedsItem(99999) {
|
||||
t.Error("Collection should not need item 99999")
|
||||
}
|
||||
|
||||
// Test GetCollectionItemByItemID
|
||||
item := collection.GetCollectionItemByItemID(12345)
|
||||
if item == nil {
|
||||
t.Error("Should find collection item by ID")
|
||||
}
|
||||
|
||||
if item.ItemID != 12345 {
|
||||
t.Errorf("Expected item ID 12345, got %d", item.ItemID)
|
||||
}
|
||||
|
||||
// Test MarkItemFound
|
||||
if !collection.MarkItemFound(12345) {
|
||||
t.Error("Should successfully mark item as found")
|
||||
}
|
||||
|
||||
// Verify item is now marked as found
|
||||
if collection.CollectionItems[0].Found != ItemFound {
|
||||
t.Error("Item should be marked as found")
|
||||
}
|
||||
|
||||
if !collection.SaveNeeded {
|
||||
t.Error("Collection should be marked as needing save")
|
||||
}
|
||||
|
||||
// Test that marking the same item again fails
|
||||
if collection.MarkItemFound(12345) {
|
||||
t.Error("Should not mark already found item again")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionProgress(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
collection := NewWithData(100, "Test", "Heritage", 20, db)
|
||||
|
||||
// Add collection items
|
||||
for i := 0; i < 4; i++ {
|
||||
collection.CollectionItems = append(collection.CollectionItems, CollectionItem{
|
||||
ItemID: int32(12345 + i),
|
||||
Index: int8(i),
|
||||
Found: ItemNotFound,
|
||||
})
|
||||
}
|
||||
|
||||
// Initially 0% progress
|
||||
if progress := collection.GetProgress(); progress != 0.0 {
|
||||
t.Errorf("Expected 0%% progress, got %.1f%%", progress)
|
||||
}
|
||||
|
||||
// Not ready to turn in
|
||||
if collection.GetIsReadyToTurnIn() {
|
||||
t.Error("Collection should not be ready to turn in")
|
||||
}
|
||||
|
||||
// Mark some items found
|
||||
collection.MarkItemFound(12345) // 25%
|
||||
collection.MarkItemFound(12346) // 50%
|
||||
|
||||
if progress := collection.GetProgress(); progress != 50.0 {
|
||||
t.Errorf("Expected 50%% progress, got %.1f%%", progress)
|
||||
}
|
||||
|
||||
// Still not ready
|
||||
if collection.GetIsReadyToTurnIn() {
|
||||
t.Error("Collection should not be ready to turn in at 50%")
|
||||
}
|
||||
|
||||
// Mark remaining items
|
||||
collection.MarkItemFound(12347) // 75%
|
||||
collection.MarkItemFound(12348) // 100%
|
||||
|
||||
if progress := collection.GetProgress(); progress != 100.0 {
|
||||
t.Errorf("Expected 100%% progress, got %.1f%%", progress)
|
||||
}
|
||||
|
||||
// Now ready to turn in
|
||||
if !collection.GetIsReadyToTurnIn() {
|
||||
t.Error("Collection should be ready to turn in at 100%")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionRewards(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
collection := NewWithData(100, "Test", "Heritage", 20, db)
|
||||
|
||||
// Set coin and XP rewards
|
||||
collection.RewardCoin = 1000
|
||||
collection.RewardXP = 500
|
||||
|
||||
// Add item rewards
|
||||
collection.RewardItems = append(collection.RewardItems, CollectionRewardItem{
|
||||
ItemID: 50001,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
collection.SelectableRewardItems = append(collection.SelectableRewardItems, CollectionRewardItem{
|
||||
ItemID: 50002,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
collection.SelectableRewardItems = append(collection.SelectableRewardItems, CollectionRewardItem{
|
||||
ItemID: 50003,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
if collection.RewardCoin != 1000 {
|
||||
t.Errorf("Expected 1000 coin reward, got %d", collection.RewardCoin)
|
||||
}
|
||||
|
||||
if collection.RewardXP != 500 {
|
||||
t.Errorf("Expected 500 XP reward, got %d", collection.RewardXP)
|
||||
}
|
||||
|
||||
if len(collection.RewardItems) != 1 {
|
||||
t.Errorf("Expected 1 reward item, got %d", len(collection.RewardItems))
|
||||
}
|
||||
|
||||
if len(collection.SelectableRewardItems) != 2 {
|
||||
t.Errorf("Expected 2 selectable reward items, got %d", len(collection.SelectableRewardItems))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionClone(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
original := NewWithData(500, "Original Collection", "Heritage", 30, db)
|
||||
original.RewardCoin = 2000
|
||||
original.RewardXP = 1000
|
||||
|
||||
// Add some items
|
||||
original.CollectionItems = append(original.CollectionItems, CollectionItem{
|
||||
ItemID: 12345,
|
||||
Index: 0,
|
||||
Found: ItemFound,
|
||||
})
|
||||
|
||||
original.RewardItems = append(original.RewardItems, CollectionRewardItem{
|
||||
ItemID: 50001,
|
||||
Quantity: 2,
|
||||
})
|
||||
|
||||
clone := original.Clone()
|
||||
|
||||
if clone == nil {
|
||||
t.Fatal("Clone returned nil")
|
||||
}
|
||||
|
||||
if clone == original {
|
||||
t.Error("Clone returned same pointer as original")
|
||||
}
|
||||
|
||||
// Test that all fields are copied
|
||||
if clone.GetID() != original.GetID() {
|
||||
t.Errorf("Clone ID = %v, want %v", clone.GetID(), original.GetID())
|
||||
}
|
||||
|
||||
if clone.GetName() != original.GetName() {
|
||||
t.Errorf("Clone Name = %v, want %v", clone.GetName(), original.GetName())
|
||||
}
|
||||
|
||||
if clone.RewardCoin != original.RewardCoin {
|
||||
t.Errorf("Clone RewardCoin = %v, want %v", clone.RewardCoin, original.RewardCoin)
|
||||
}
|
||||
|
||||
if len(clone.CollectionItems) != len(original.CollectionItems) {
|
||||
t.Errorf("Clone items length = %v, want %v", len(clone.CollectionItems), len(original.CollectionItems))
|
||||
}
|
||||
|
||||
if len(clone.RewardItems) != len(original.RewardItems) {
|
||||
t.Errorf("Clone reward items length = %v, want %v", len(clone.RewardItems), len(original.RewardItems))
|
||||
}
|
||||
|
||||
if !clone.IsNew() {
|
||||
t.Error("Clone should always be marked as new")
|
||||
}
|
||||
|
||||
// Verify modification independence
|
||||
clone.Name = "Modified Clone"
|
||||
if original.GetName() == "Modified Clone" {
|
||||
t.Error("Modifying clone affected original")
|
||||
}
|
||||
|
||||
// Verify slice independence
|
||||
if len(original.CollectionItems) > 0 && len(clone.CollectionItems) > 0 {
|
||||
clone.CollectionItems[0].Found = ItemNotFound
|
||||
if original.CollectionItems[0].Found == ItemNotFound {
|
||||
t.Error("Modifying clone items affected original")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionCompletion(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
collection := NewWithData(100, "Test", "Heritage", 20, db)
|
||||
|
||||
// Add items
|
||||
collection.CollectionItems = append(collection.CollectionItems, CollectionItem{
|
||||
ItemID: 12345,
|
||||
Index: 0,
|
||||
Found: ItemNotFound,
|
||||
})
|
||||
|
||||
// Not ready when incomplete
|
||||
if collection.GetIsReadyToTurnIn() {
|
||||
t.Error("Incomplete collection should not be ready to turn in")
|
||||
}
|
||||
|
||||
// Mark as completed
|
||||
collection.Completed = true
|
||||
|
||||
// Completed collections are never ready to turn in
|
||||
if collection.GetIsReadyToTurnIn() {
|
||||
t.Error("Completed collection should not be ready to turn in")
|
||||
}
|
||||
|
||||
// Mark item found and set not completed
|
||||
collection.Completed = false
|
||||
collection.MarkItemFound(12345)
|
||||
|
||||
// Now should be ready
|
||||
if !collection.GetIsReadyToTurnIn() {
|
||||
t.Error("Collection with all items found should be ready to turn in")
|
||||
}
|
||||
}
|
@ -1,499 +0,0 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewCollection creates a new collection instance
|
||||
func NewCollection() *Collection {
|
||||
return &Collection{
|
||||
collectionItems: make([]CollectionItem, 0),
|
||||
rewardItems: make([]CollectionRewardItem, 0),
|
||||
selectableRewardItems: make([]CollectionRewardItem, 0),
|
||||
lastModified: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewCollectionFromData creates a collection from another collection (copy constructor)
|
||||
func NewCollectionFromData(source *Collection) *Collection {
|
||||
if source == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
source.mu.RLock()
|
||||
defer source.mu.RUnlock()
|
||||
|
||||
collection := &Collection{
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
category: source.category,
|
||||
level: source.level,
|
||||
rewardCoin: source.rewardCoin,
|
||||
rewardXP: source.rewardXP,
|
||||
completed: source.completed,
|
||||
saveNeeded: source.saveNeeded,
|
||||
collectionItems: make([]CollectionItem, len(source.collectionItems)),
|
||||
rewardItems: make([]CollectionRewardItem, len(source.rewardItems)),
|
||||
selectableRewardItems: make([]CollectionRewardItem, len(source.selectableRewardItems)),
|
||||
lastModified: time.Now(),
|
||||
}
|
||||
|
||||
// Deep copy collection items
|
||||
copy(collection.collectionItems, source.collectionItems)
|
||||
|
||||
// Deep copy reward items
|
||||
copy(collection.rewardItems, source.rewardItems)
|
||||
|
||||
// Deep copy selectable reward items
|
||||
copy(collection.selectableRewardItems, source.selectableRewardItems)
|
||||
|
||||
return collection
|
||||
}
|
||||
|
||||
// SetID sets the collection ID
|
||||
func (c *Collection) SetID(id int32) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.id = id
|
||||
}
|
||||
|
||||
// SetName sets the collection name
|
||||
func (c *Collection) SetName(name string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(name) > MaxCollectionNameLength {
|
||||
name = name[:MaxCollectionNameLength]
|
||||
}
|
||||
c.name = name
|
||||
}
|
||||
|
||||
// SetCategory sets the collection category
|
||||
func (c *Collection) SetCategory(category string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(category) > MaxCollectionCategoryLength {
|
||||
category = category[:MaxCollectionCategoryLength]
|
||||
}
|
||||
c.category = category
|
||||
}
|
||||
|
||||
// SetLevel sets the collection level
|
||||
func (c *Collection) SetLevel(level int8) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.level = level
|
||||
}
|
||||
|
||||
// SetCompleted sets the collection completion status
|
||||
func (c *Collection) SetCompleted(completed bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.completed = completed
|
||||
c.lastModified = time.Now()
|
||||
}
|
||||
|
||||
// SetSaveNeeded sets whether the collection needs to be saved
|
||||
func (c *Collection) SetSaveNeeded(saveNeeded bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.saveNeeded = saveNeeded
|
||||
}
|
||||
|
||||
// SetRewardCoin sets the coin reward amount
|
||||
func (c *Collection) SetRewardCoin(coin int64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.rewardCoin = coin
|
||||
}
|
||||
|
||||
// SetRewardXP sets the XP reward amount
|
||||
func (c *Collection) SetRewardXP(xp int64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.rewardXP = xp
|
||||
}
|
||||
|
||||
// AddCollectionItem adds a required item to the collection
|
||||
func (c *Collection) AddCollectionItem(item CollectionItem) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.collectionItems = append(c.collectionItems, item)
|
||||
}
|
||||
|
||||
// AddRewardItem adds a reward item to the collection
|
||||
func (c *Collection) AddRewardItem(item CollectionRewardItem) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.rewardItems = append(c.rewardItems, item)
|
||||
}
|
||||
|
||||
// AddSelectableRewardItem adds a selectable reward item to the collection
|
||||
func (c *Collection) AddSelectableRewardItem(item CollectionRewardItem) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.selectableRewardItems = append(c.selectableRewardItems, item)
|
||||
}
|
||||
|
||||
// GetID returns the collection ID
|
||||
func (c *Collection) GetID() int32 {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.id
|
||||
}
|
||||
|
||||
// GetName returns the collection name
|
||||
func (c *Collection) GetName() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.name
|
||||
}
|
||||
|
||||
// GetCategory returns the collection category
|
||||
func (c *Collection) GetCategory() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.category
|
||||
}
|
||||
|
||||
// GetLevel returns the collection level
|
||||
func (c *Collection) GetLevel() int8 {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.level
|
||||
}
|
||||
|
||||
// GetCompleted returns whether the collection is completed
|
||||
func (c *Collection) GetCompleted() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.completed
|
||||
}
|
||||
|
||||
// GetSaveNeeded returns whether the collection needs to be saved
|
||||
func (c *Collection) GetSaveNeeded() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.saveNeeded
|
||||
}
|
||||
|
||||
// GetRewardCoin returns the coin reward amount
|
||||
func (c *Collection) GetRewardCoin() int64 {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.rewardCoin
|
||||
}
|
||||
|
||||
// GetRewardXP returns the XP reward amount
|
||||
func (c *Collection) GetRewardXP() int64 {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.rewardXP
|
||||
}
|
||||
|
||||
// GetCollectionItems returns a copy of the collection items
|
||||
func (c *Collection) GetCollectionItems() []CollectionItem {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
items := make([]CollectionItem, len(c.collectionItems))
|
||||
copy(items, c.collectionItems)
|
||||
return items
|
||||
}
|
||||
|
||||
// GetRewardItems returns a copy of the reward items
|
||||
func (c *Collection) GetRewardItems() []CollectionRewardItem {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
items := make([]CollectionRewardItem, len(c.rewardItems))
|
||||
copy(items, c.rewardItems)
|
||||
return items
|
||||
}
|
||||
|
||||
// GetSelectableRewardItems returns a copy of the selectable reward items
|
||||
func (c *Collection) GetSelectableRewardItems() []CollectionRewardItem {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
items := make([]CollectionRewardItem, len(c.selectableRewardItems))
|
||||
copy(items, c.selectableRewardItems)
|
||||
return items
|
||||
}
|
||||
|
||||
// NeedsItem checks if the collection needs a specific item
|
||||
func (c *Collection) NeedsItem(itemID int32) bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.completed {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, item := range c.collectionItems {
|
||||
if item.ItemID == itemID {
|
||||
return item.Found == ItemNotFound
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCollectionItemByItemID returns the collection item for a specific item ID
|
||||
func (c *Collection) GetCollectionItemByItemID(itemID int32) *CollectionItem {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
for i := range c.collectionItems {
|
||||
if c.collectionItems[i].ItemID == itemID {
|
||||
return &c.collectionItems[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIsReadyToTurnIn checks if all required items have been found
|
||||
func (c *Collection) GetIsReadyToTurnIn() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.completed {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, item := range c.collectionItems {
|
||||
if item.Found == ItemNotFound {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MarkItemFound marks an item as found in the collection
|
||||
func (c *Collection) MarkItemFound(itemID int32) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.completed {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range c.collectionItems {
|
||||
if c.collectionItems[i].ItemID == itemID && c.collectionItems[i].Found == ItemNotFound {
|
||||
c.collectionItems[i].Found = ItemFound
|
||||
c.saveNeeded = true
|
||||
c.lastModified = time.Now()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetProgress returns the completion progress as a percentage
|
||||
func (c *Collection) GetProgress() float64 {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if len(c.collectionItems) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
foundCount := 0
|
||||
for _, item := range c.collectionItems {
|
||||
if item.Found == ItemFound {
|
||||
foundCount++
|
||||
}
|
||||
}
|
||||
|
||||
return float64(foundCount) / float64(len(c.collectionItems)) * 100.0
|
||||
}
|
||||
|
||||
// GetFoundItemsCount returns the number of found items
|
||||
func (c *Collection) GetFoundItemsCount() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
count := 0
|
||||
for _, item := range c.collectionItems {
|
||||
if item.Found == ItemFound {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// GetTotalItemsCount returns the total number of required items
|
||||
func (c *Collection) GetTotalItemsCount() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.collectionItems)
|
||||
}
|
||||
|
||||
// GetCollectionInfo returns detailed collection information
|
||||
func (c *Collection) GetCollectionInfo() CollectionInfo {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return CollectionInfo{
|
||||
ID: c.id,
|
||||
Name: c.name,
|
||||
Category: c.category,
|
||||
Level: c.level,
|
||||
Completed: c.completed,
|
||||
ReadyToTurnIn: c.getIsReadyToTurnInNoLock(),
|
||||
ItemsFound: c.getFoundItemsCountNoLock(),
|
||||
ItemsTotal: len(c.collectionItems),
|
||||
RewardCoin: c.rewardCoin,
|
||||
RewardXP: c.rewardXP,
|
||||
RewardItems: append([]CollectionRewardItem(nil), c.rewardItems...),
|
||||
SelectableRewards: append([]CollectionRewardItem(nil), c.selectableRewardItems...),
|
||||
RequiredItems: append([]CollectionItem(nil), c.collectionItems...),
|
||||
}
|
||||
}
|
||||
|
||||
// GetCollectionProgress returns detailed progress information
|
||||
func (c *Collection) GetCollectionProgress() CollectionProgress {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
var foundItems, neededItems []CollectionItem
|
||||
for _, item := range c.collectionItems {
|
||||
if item.Found == ItemFound {
|
||||
foundItems = append(foundItems, item)
|
||||
} else {
|
||||
neededItems = append(neededItems, item)
|
||||
}
|
||||
}
|
||||
|
||||
return CollectionProgress{
|
||||
CollectionID: c.id,
|
||||
Name: c.name,
|
||||
Category: c.category,
|
||||
Level: c.level,
|
||||
Completed: c.completed,
|
||||
ReadyToTurnIn: c.getIsReadyToTurnInNoLock(),
|
||||
Progress: c.getProgressNoLock(),
|
||||
ItemsFound: foundItems,
|
||||
ItemsNeeded: neededItems,
|
||||
LastUpdated: c.lastModified,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadFromRewardData loads reward data into the collection
|
||||
func (c *Collection) LoadFromRewardData(rewards []CollectionRewardData) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
for _, reward := range rewards {
|
||||
switch strings.ToLower(reward.RewardType) {
|
||||
case strings.ToLower(RewardTypeItem):
|
||||
itemID, err := strconv.ParseInt(reward.RewardValue, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid item ID in reward: %s", reward.RewardValue)
|
||||
}
|
||||
c.rewardItems = append(c.rewardItems, CollectionRewardItem{
|
||||
ItemID: int32(itemID),
|
||||
Quantity: reward.Quantity,
|
||||
})
|
||||
|
||||
case strings.ToLower(RewardTypeSelectable):
|
||||
itemID, err := strconv.ParseInt(reward.RewardValue, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid item ID in selectable reward: %s", reward.RewardValue)
|
||||
}
|
||||
c.selectableRewardItems = append(c.selectableRewardItems, CollectionRewardItem{
|
||||
ItemID: int32(itemID),
|
||||
Quantity: reward.Quantity,
|
||||
})
|
||||
|
||||
case strings.ToLower(RewardTypeCoin):
|
||||
coin, err := strconv.ParseInt(reward.RewardValue, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid coin amount in reward: %s", reward.RewardValue)
|
||||
}
|
||||
c.rewardCoin = coin
|
||||
|
||||
case strings.ToLower(RewardTypeXP):
|
||||
xp, err := strconv.ParseInt(reward.RewardValue, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid XP amount in reward: %s", reward.RewardValue)
|
||||
}
|
||||
c.rewardXP = xp
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown reward type: %s", reward.RewardType)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks if the collection data is valid
|
||||
func (c *Collection) Validate() error {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.id <= 0 {
|
||||
return fmt.Errorf("collection ID must be positive")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(c.name) == "" {
|
||||
return fmt.Errorf("collection name cannot be empty")
|
||||
}
|
||||
|
||||
if len(c.collectionItems) == 0 {
|
||||
return fmt.Errorf("collection must have at least one required item")
|
||||
}
|
||||
|
||||
// Check for duplicate item IDs
|
||||
itemIDs := make(map[int32]bool)
|
||||
for _, item := range c.collectionItems {
|
||||
if itemIDs[item.ItemID] {
|
||||
return fmt.Errorf("duplicate item ID in collection: %d", item.ItemID)
|
||||
}
|
||||
itemIDs[item.ItemID] = true
|
||||
|
||||
if item.ItemID <= 0 {
|
||||
return fmt.Errorf("collection item ID must be positive: %d", item.ItemID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper methods (no lock versions for internal use)
|
||||
|
||||
func (c *Collection) getIsReadyToTurnInNoLock() bool {
|
||||
if c.completed {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, item := range c.collectionItems {
|
||||
if item.Found == ItemNotFound {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Collection) getFoundItemsCountNoLock() int {
|
||||
count := 0
|
||||
for _, item := range c.collectionItems {
|
||||
if item.Found == ItemFound {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (c *Collection) getProgressNoLock() float64 {
|
||||
if len(c.collectionItems) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
foundCount := c.getFoundItemsCountNoLock()
|
||||
return float64(foundCount) / float64(len(c.collectionItems)) * 100.0
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,36 +1,13 @@
|
||||
package collections
|
||||
|
||||
// Collection reward types
|
||||
const (
|
||||
RewardTypeItem = "Item"
|
||||
RewardTypeSelectable = "Selectable"
|
||||
RewardTypeCoin = "Coin"
|
||||
RewardTypeXP = "XP"
|
||||
)
|
||||
|
||||
// Collection item states
|
||||
const (
|
||||
ItemNotFound = 0
|
||||
ItemFound = 1
|
||||
)
|
||||
|
||||
// Collection states
|
||||
const (
|
||||
CollectionIncomplete = false
|
||||
CollectionCompleted = true
|
||||
)
|
||||
|
||||
// String length limits
|
||||
const (
|
||||
MaxCollectionNameLength = 512
|
||||
MaxCollectionCategoryLength = 512
|
||||
)
|
||||
|
||||
// Database table names
|
||||
const (
|
||||
TableCollections = "collections"
|
||||
TableCollectionDetails = "collection_details"
|
||||
TableCollectionRewards = "collection_rewards"
|
||||
TableCharacterCollections = "character_collections"
|
||||
TableCharacterCollectionItems = "character_collection_items"
|
||||
)
|
||||
|
@ -1,585 +0,0 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"zombiezen.com/go/sqlite"
|
||||
"zombiezen.com/go/sqlite/sqlitex"
|
||||
)
|
||||
|
||||
// DatabaseCollectionManager implements CollectionDatabase interface using sqlitex.Pool
|
||||
type DatabaseCollectionManager struct {
|
||||
pool *sqlitex.Pool
|
||||
}
|
||||
|
||||
// NewDatabaseCollectionManager creates a new database collection manager
|
||||
func NewDatabaseCollectionManager(pool *sqlitex.Pool) *DatabaseCollectionManager {
|
||||
return &DatabaseCollectionManager{
|
||||
pool: pool,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadCollections retrieves all collections from database
|
||||
func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]CollectionData, error) {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
query := "SELECT `id`, `collection_name`, `collection_category`, `level` FROM `collections`"
|
||||
|
||||
var collections []CollectionData
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
var collection CollectionData
|
||||
collection.ID = int32(stmt.ColumnInt64(0))
|
||||
collection.Name = stmt.ColumnText(1)
|
||||
collection.Category = stmt.ColumnText(2)
|
||||
collection.Level = int8(stmt.ColumnInt64(3))
|
||||
collections = append(collections, collection)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query collections: %w", err)
|
||||
}
|
||||
|
||||
return collections, nil
|
||||
}
|
||||
|
||||
// LoadCollectionItems retrieves items for a specific collection
|
||||
func (dcm *DatabaseCollectionManager) LoadCollectionItems(ctx context.Context, collectionID int32) ([]CollectionItem, error) {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
query := `SELECT item_id, item_index
|
||||
FROM collection_details
|
||||
WHERE collection_id = ?
|
||||
ORDER BY item_index ASC`
|
||||
|
||||
var items []CollectionItem
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
Args: []any{collectionID},
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
var item CollectionItem
|
||||
item.ItemID = int32(stmt.ColumnInt64(0))
|
||||
item.Index = int8(stmt.ColumnInt64(1))
|
||||
// Items start as not found
|
||||
item.Found = ItemNotFound
|
||||
items = append(items, item)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query collection items for collection %d: %w", collectionID, err)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// LoadCollectionRewards retrieves rewards for a specific collection
|
||||
func (dcm *DatabaseCollectionManager) LoadCollectionRewards(ctx context.Context, collectionID int32) ([]CollectionRewardData, error) {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
query := `SELECT collection_id, reward_type, reward_value, reward_quantity
|
||||
FROM collection_rewards
|
||||
WHERE collection_id = ?`
|
||||
|
||||
var rewards []CollectionRewardData
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
Args: []any{collectionID},
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
var reward CollectionRewardData
|
||||
reward.CollectionID = int32(stmt.ColumnInt64(0))
|
||||
reward.RewardType = stmt.ColumnText(1)
|
||||
reward.RewardValue = stmt.ColumnText(2)
|
||||
reward.Quantity = int8(stmt.ColumnInt64(3))
|
||||
rewards = append(rewards, reward)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query collection rewards for collection %d: %w", collectionID, err)
|
||||
}
|
||||
|
||||
return rewards, nil
|
||||
}
|
||||
|
||||
// LoadPlayerCollections retrieves player's collection progress
|
||||
func (dcm *DatabaseCollectionManager) LoadPlayerCollections(ctx context.Context, characterID int32) ([]PlayerCollectionData, error) {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
query := `SELECT char_id, collection_id, completed
|
||||
FROM character_collections
|
||||
WHERE char_id = ?`
|
||||
|
||||
var collections []PlayerCollectionData
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
Args: []any{characterID},
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
var collection PlayerCollectionData
|
||||
collection.CharacterID = int32(stmt.ColumnInt64(0))
|
||||
collection.CollectionID = int32(stmt.ColumnInt64(1))
|
||||
collection.Completed = stmt.ColumnInt64(2) != 0
|
||||
collections = append(collections, collection)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query player collections for character %d: %w", characterID, err)
|
||||
}
|
||||
|
||||
return collections, nil
|
||||
}
|
||||
|
||||
// LoadPlayerCollectionItems retrieves player's found collection items
|
||||
func (dcm *DatabaseCollectionManager) LoadPlayerCollectionItems(ctx context.Context, characterID, collectionID int32) ([]int32, error) {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
query := `SELECT collection_item_id
|
||||
FROM character_collection_items
|
||||
WHERE char_id = ? AND collection_id = ?`
|
||||
|
||||
var itemIDs []int32
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
Args: []any{characterID, collectionID},
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
itemID := int32(stmt.ColumnInt64(0))
|
||||
itemIDs = append(itemIDs, itemID)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query player collection items for character %d, collection %d: %w", characterID, collectionID, err)
|
||||
}
|
||||
|
||||
return itemIDs, nil
|
||||
}
|
||||
|
||||
// SavePlayerCollection saves player collection completion status
|
||||
func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context, characterID, collectionID int32, completed bool) error {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
completedInt := 0
|
||||
if completed {
|
||||
completedInt = 1
|
||||
}
|
||||
|
||||
query := `INSERT INTO character_collections (char_id, collection_id, completed)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(char_id, collection_id)
|
||||
DO UPDATE SET completed = ?`
|
||||
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
Args: []any{characterID, collectionID, completedInt, completedInt},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save player collection for character %d, collection %d: %w", characterID, collectionID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SavePlayerCollectionItem saves a found collection item
|
||||
func (dcm *DatabaseCollectionManager) SavePlayerCollectionItem(ctx context.Context, characterID, collectionID, itemID int32) error {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
|
||||
VALUES (?, ?, ?)`
|
||||
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
Args: []any{characterID, collectionID, itemID},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save player collection item for character %d, collection %d, item %d: %w", characterID, collectionID, itemID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SavePlayerCollections saves all modified player collections
|
||||
func (dcm *DatabaseCollectionManager) SavePlayerCollections(ctx context.Context, characterID int32, collections []*Collection) error {
|
||||
if len(collections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
// Use a transaction for atomic updates
|
||||
err = sqlitex.Execute(conn, "BEGIN", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
defer sqlitex.Execute(conn, "ROLLBACK", nil)
|
||||
|
||||
for _, collection := range collections {
|
||||
if !collection.GetSaveNeeded() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Save collection completion status
|
||||
if err := dcm.savePlayerCollectionInTx(conn, characterID, collection); err != nil {
|
||||
return fmt.Errorf("failed to save collection %d: %w", collection.GetID(), err)
|
||||
}
|
||||
|
||||
// Save found items
|
||||
if err := dcm.savePlayerCollectionItemsInTx(conn, characterID, collection); err != nil {
|
||||
return fmt.Errorf("failed to save collection items for collection %d: %w", collection.GetID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return sqlitex.Execute(conn, "COMMIT", nil)
|
||||
}
|
||||
|
||||
// savePlayerCollectionInTx saves a single collection within a transaction
|
||||
func (dcm *DatabaseCollectionManager) savePlayerCollectionInTx(conn *sqlite.Conn, characterID int32, collection *Collection) error {
|
||||
completedInt := 0
|
||||
if collection.GetCompleted() {
|
||||
completedInt = 1
|
||||
}
|
||||
|
||||
query := `INSERT INTO character_collections (char_id, collection_id, completed)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(char_id, collection_id)
|
||||
DO UPDATE SET completed = ?`
|
||||
|
||||
return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
Args: []any{characterID, collection.GetID(), completedInt, completedInt},
|
||||
})
|
||||
}
|
||||
|
||||
// savePlayerCollectionItemsInTx saves collection items within a transaction
|
||||
func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsInTx(conn *sqlite.Conn, characterID int32, collection *Collection) error {
|
||||
items := collection.GetCollectionItems()
|
||||
|
||||
for _, item := range items {
|
||||
if item.Found == ItemFound {
|
||||
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
|
||||
VALUES (?, ?, ?)`
|
||||
|
||||
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
Args: []any{characterID, collection.GetID(), item.ItemID},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save item %d: %w", item.ItemID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureCollectionTables creates the collection tables if they don't exist
|
||||
func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context) error {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
queries := []string{
|
||||
`CREATE TABLE IF NOT EXISTS collections (
|
||||
id INTEGER PRIMARY KEY,
|
||||
collection_name TEXT NOT NULL,
|
||||
collection_category TEXT NOT NULL DEFAULT '',
|
||||
level INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS collection_details (
|
||||
collection_id INTEGER NOT NULL,
|
||||
item_id INTEGER NOT NULL,
|
||||
item_index INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (collection_id, item_id),
|
||||
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS collection_rewards (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL,
|
||||
reward_type TEXT NOT NULL,
|
||||
reward_value TEXT NOT NULL,
|
||||
reward_quantity INTEGER NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS character_collections (
|
||||
char_id INTEGER NOT NULL,
|
||||
collection_id INTEGER NOT NULL,
|
||||
completed INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (char_id, collection_id),
|
||||
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS character_collection_items (
|
||||
char_id INTEGER NOT NULL,
|
||||
collection_id INTEGER NOT NULL,
|
||||
collection_item_id INTEGER NOT NULL,
|
||||
found_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (char_id, collection_id, collection_item_id),
|
||||
FOREIGN KEY (char_id, collection_id) REFERENCES character_collections(char_id, collection_id) ON DELETE CASCADE
|
||||
)`,
|
||||
}
|
||||
|
||||
for i, query := range queries {
|
||||
err := sqlitex.Execute(conn, query, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create collection table %d: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create indexes for better performance
|
||||
indexes := []string{
|
||||
`CREATE INDEX IF NOT EXISTS idx_collection_details_collection_id ON collection_details(collection_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_collection_rewards_collection_id ON collection_rewards(collection_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_character_collections_char_id ON character_collections(char_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_character_collection_items_char_id ON character_collection_items(char_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_character_collection_items_collection_id ON character_collection_items(collection_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_collections_category ON collections(collection_category)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_collections_level ON collections(level)`,
|
||||
}
|
||||
|
||||
for i, query := range indexes {
|
||||
err := sqlitex.Execute(conn, query, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create collection index %d: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCollectionCount returns the total number of collections in the database
|
||||
func (dcm *DatabaseCollectionManager) GetCollectionCount(ctx context.Context) (int, error) {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
query := "SELECT COUNT(*) FROM collections"
|
||||
|
||||
var count int
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
count = int(stmt.ColumnInt64(0))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get collection count: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetPlayerCollectionCount returns the number of collections a player has
|
||||
func (dcm *DatabaseCollectionManager) GetPlayerCollectionCount(ctx context.Context, characterID int32) (int, error) {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ?"
|
||||
|
||||
var count int
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
Args: []any{characterID},
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
count = int(stmt.ColumnInt64(0))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get player collection count for character %d: %w", characterID, err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetCompletedCollectionCount returns the number of completed collections for a player
|
||||
func (dcm *DatabaseCollectionManager) GetCompletedCollectionCount(ctx context.Context, characterID int32) (int, error) {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ? AND completed = 1"
|
||||
|
||||
var count int
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
Args: []any{characterID},
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
count = int(stmt.ColumnInt64(0))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get completed collection count for character %d: %w", characterID, err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// DeletePlayerCollection removes a player's collection progress
|
||||
func (dcm *DatabaseCollectionManager) DeletePlayerCollection(ctx context.Context, characterID, collectionID int32) error {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
// Use a transaction to ensure both tables are updated atomically
|
||||
err = sqlitex.Execute(conn, "BEGIN", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
defer sqlitex.Execute(conn, "ROLLBACK", nil)
|
||||
|
||||
// Delete collection items first due to foreign key constraint
|
||||
err = sqlitex.Execute(conn, "DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?", &sqlitex.ExecOptions{
|
||||
Args: []any{characterID, collectionID},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete player collection items: %w", err)
|
||||
}
|
||||
|
||||
// Delete collection
|
||||
err = sqlitex.Execute(conn, "DELETE FROM character_collections WHERE char_id = ? AND collection_id = ?", &sqlitex.ExecOptions{
|
||||
Args: []any{characterID, collectionID},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete player collection: %w", err)
|
||||
}
|
||||
|
||||
return sqlitex.Execute(conn, "COMMIT", nil)
|
||||
}
|
||||
|
||||
// GetCollectionStatistics returns database-level collection statistics
|
||||
func (dcm *DatabaseCollectionManager) GetCollectionStatistics(ctx context.Context) (CollectionStatistics, error) {
|
||||
conn, err := dcm.pool.Take(context.Background())
|
||||
if err != nil {
|
||||
return CollectionStatistics{}, fmt.Errorf("failed to get connection: %w", err)
|
||||
}
|
||||
defer dcm.pool.Put(conn)
|
||||
|
||||
var stats CollectionStatistics
|
||||
|
||||
// Total collections
|
||||
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM collections", &sqlitex.ExecOptions{
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
stats.TotalCollections = int(stmt.ColumnInt64(0))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return stats, fmt.Errorf("failed to get total collections: %w", err)
|
||||
}
|
||||
|
||||
// Total collection items
|
||||
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM collection_details", &sqlitex.ExecOptions{
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
stats.TotalItems = int(stmt.ColumnInt64(0))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return stats, fmt.Errorf("failed to get total items: %w", err)
|
||||
}
|
||||
|
||||
// Players with collections
|
||||
err = sqlitex.Execute(conn, "SELECT COUNT(DISTINCT char_id) FROM character_collections", &sqlitex.ExecOptions{
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
stats.PlayersWithCollections = int(stmt.ColumnInt64(0))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return stats, fmt.Errorf("failed to get players with collections: %w", err)
|
||||
}
|
||||
|
||||
// Completed collections across all players
|
||||
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM character_collections WHERE completed = 1", &sqlitex.ExecOptions{
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
stats.CompletedCollections = int(stmt.ColumnInt64(0))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return stats, fmt.Errorf("failed to get completed collections: %w", err)
|
||||
}
|
||||
|
||||
// Active collections (incomplete with at least one item found) across all players
|
||||
query := `SELECT COUNT(DISTINCT cc.char_id || '-' || cc.collection_id)
|
||||
FROM character_collections cc
|
||||
JOIN character_collection_items cci ON cc.char_id = cci.char_id AND cc.collection_id = cci.collection_id
|
||||
WHERE cc.completed = 0`
|
||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
stats.ActiveCollections = int(stmt.ColumnInt64(0))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return stats, fmt.Errorf("failed to get active collections: %w", err)
|
||||
}
|
||||
|
||||
// Found items across all players
|
||||
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM character_collection_items", &sqlitex.ExecOptions{
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
stats.FoundItems = int(stmt.ColumnInt64(0))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return stats, fmt.Errorf("failed to get found items: %w", err)
|
||||
}
|
||||
|
||||
// Total rewards
|
||||
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM collection_rewards", &sqlitex.ExecOptions{
|
||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||
stats.TotalRewards = int(stmt.ColumnInt64(0))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return stats, fmt.Errorf("failed to get total rewards: %w", err)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
70
internal/collections/doc.go
Normal file
70
internal/collections/doc.go
Normal file
@ -0,0 +1,70 @@
|
||||
// Package collections provides collection quest management for the EverQuest II server emulator.
|
||||
//
|
||||
// Collections are special quests where players gather specific items scattered throughout
|
||||
// the world. When all items are found, the collection can be turned in for rewards.
|
||||
// The system tracks both master collections available to all players and individual
|
||||
// player progress on those collections.
|
||||
//
|
||||
// Basic Usage:
|
||||
//
|
||||
// db, _ := database.NewSQLite("collections.db")
|
||||
//
|
||||
// // Create new collection
|
||||
// collection := collections.New(db)
|
||||
// collection.ID = 1001
|
||||
// collection.Name = "Antonian Cameos"
|
||||
// collection.Category = "Heritage"
|
||||
// collection.Level = 20
|
||||
// collection.Save()
|
||||
//
|
||||
// // Load existing collection
|
||||
// loaded, _ := collections.Load(db, 1001)
|
||||
// loaded.Delete()
|
||||
//
|
||||
// // Add collection items
|
||||
// collection.CollectionItems = append(collection.CollectionItems, collections.CollectionItem{
|
||||
// ItemID: 12345,
|
||||
// Index: 0,
|
||||
// Found: collections.ItemNotFound,
|
||||
// })
|
||||
//
|
||||
// Master List Management:
|
||||
//
|
||||
// masterList := collections.NewMasterList()
|
||||
// masterList.LoadAllCollections(db)
|
||||
// masterList.AddCollection(collection)
|
||||
//
|
||||
// // Find collections
|
||||
// found := masterList.GetCollection(1001)
|
||||
// heritage := masterList.FindCollectionsByCategory("Heritage")
|
||||
// needsItem := masterList.GetCollectionsNeedingItem(12345)
|
||||
//
|
||||
// Player Collection Management:
|
||||
//
|
||||
// playerList := collections.NewPlayerList(characterID, db)
|
||||
// playerList.LoadPlayerCollections(masterList)
|
||||
//
|
||||
// // Check if player needs an item
|
||||
// if playerList.NeedsItem(12345) {
|
||||
// // Award the item to collections that need it
|
||||
// }
|
||||
//
|
||||
// // Find collections ready to turn in
|
||||
// readyCollections := playerList.GetCollectionsToHandIn()
|
||||
//
|
||||
// Collection Features:
|
||||
//
|
||||
// // Check collection progress
|
||||
// progress := collection.GetProgress() // Returns percentage 0-100
|
||||
// ready := collection.GetIsReadyToTurnIn()
|
||||
//
|
||||
// // Mark items as found
|
||||
// found := collection.MarkItemFound(itemID)
|
||||
//
|
||||
// // Clone collections for players
|
||||
// playerCollection := masterCollection.Clone()
|
||||
//
|
||||
// The package supports both master collections (defined by game data) and player-specific
|
||||
// collection instances with individual progress tracking. Collections can have multiple
|
||||
// reward types including items, coin, and experience points.
|
||||
package collections
|
@ -1,176 +0,0 @@
|
||||
package collections
|
||||
|
||||
import "context"
|
||||
|
||||
// CollectionDatabase defines database operations for collections
|
||||
type CollectionDatabase interface {
|
||||
// LoadCollections retrieves all collections from database
|
||||
LoadCollections(ctx context.Context) ([]CollectionData, error)
|
||||
|
||||
// LoadCollectionItems retrieves items for a specific collection
|
||||
LoadCollectionItems(ctx context.Context, collectionID int32) ([]CollectionItem, error)
|
||||
|
||||
// LoadCollectionRewards retrieves rewards for a specific collection
|
||||
LoadCollectionRewards(ctx context.Context, collectionID int32) ([]CollectionRewardData, error)
|
||||
|
||||
// LoadPlayerCollections retrieves player's collection progress
|
||||
LoadPlayerCollections(ctx context.Context, characterID int32) ([]PlayerCollectionData, error)
|
||||
|
||||
// LoadPlayerCollectionItems retrieves player's found collection items
|
||||
LoadPlayerCollectionItems(ctx context.Context, characterID, collectionID int32) ([]int32, error)
|
||||
|
||||
// SavePlayerCollection saves player collection completion status
|
||||
SavePlayerCollection(ctx context.Context, characterID, collectionID int32, completed bool) error
|
||||
|
||||
// SavePlayerCollectionItem saves a found collection item
|
||||
SavePlayerCollectionItem(ctx context.Context, characterID, collectionID, itemID int32) error
|
||||
|
||||
// SavePlayerCollections saves all modified player collections
|
||||
SavePlayerCollections(ctx context.Context, characterID int32, collections []*Collection) error
|
||||
}
|
||||
|
||||
// ItemLookup provides item information for collections
|
||||
type ItemLookup interface {
|
||||
// GetItem retrieves an item by ID
|
||||
GetItem(itemID int32) (ItemInfo, error)
|
||||
|
||||
// ItemExists checks if an item exists
|
||||
ItemExists(itemID int32) bool
|
||||
|
||||
// GetItemName returns the name of an item
|
||||
GetItemName(itemID int32) string
|
||||
}
|
||||
|
||||
// PlayerManager provides player information for collections
|
||||
type PlayerManager interface {
|
||||
// GetPlayerInfo retrieves basic player information
|
||||
GetPlayerInfo(characterID int32) (PlayerInfo, error)
|
||||
|
||||
// IsPlayerOnline checks if a player is currently online
|
||||
IsPlayerOnline(characterID int32) bool
|
||||
|
||||
// GetPlayerLevel returns player's current level
|
||||
GetPlayerLevel(characterID int32) int8
|
||||
}
|
||||
|
||||
// ClientManager handles client communication for collections
|
||||
type ClientManager interface {
|
||||
// SendCollectionUpdate notifies client of collection changes
|
||||
SendCollectionUpdate(characterID int32, collection *Collection) error
|
||||
|
||||
// SendCollectionComplete notifies client of collection completion
|
||||
SendCollectionComplete(characterID int32, collection *Collection) error
|
||||
|
||||
// SendCollectionList sends available collections to client
|
||||
SendCollectionList(characterID int32, collections []CollectionInfo) error
|
||||
|
||||
// SendCollectionProgress sends collection progress to client
|
||||
SendCollectionProgress(characterID int32, progress []CollectionProgress) error
|
||||
}
|
||||
|
||||
// ItemInfo contains item information needed for collections
|
||||
type ItemInfo struct {
|
||||
ID int32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Icon int32 `json:"icon"`
|
||||
Level int8 `json:"level"`
|
||||
Rarity int8 `json:"rarity"`
|
||||
}
|
||||
|
||||
// PlayerInfo contains basic player information
|
||||
type PlayerInfo struct {
|
||||
CharacterID int32 `json:"character_id"`
|
||||
CharacterName string `json:"character_name"`
|
||||
Level int8 `json:"level"`
|
||||
Race int32 `json:"race"`
|
||||
Class int32 `json:"class"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
}
|
||||
|
||||
// CollectionAware interface for entities that can participate in collections
|
||||
type CollectionAware interface {
|
||||
GetCharacterID() int32
|
||||
GetLevel() int8
|
||||
HasItem(itemID int32) bool
|
||||
GetCollectionList() *PlayerCollectionList
|
||||
}
|
||||
|
||||
// EntityCollectionAdapter adapts entities to work with collection system
|
||||
type EntityCollectionAdapter struct {
|
||||
entity interface {
|
||||
GetID() int32
|
||||
// Add other entity methods as needed
|
||||
}
|
||||
playerManager PlayerManager
|
||||
}
|
||||
|
||||
// GetCharacterID returns the character ID from the adapted entity
|
||||
func (a *EntityCollectionAdapter) GetCharacterID() int32 {
|
||||
return a.entity.GetID()
|
||||
}
|
||||
|
||||
// GetLevel returns the character level from player manager
|
||||
func (a *EntityCollectionAdapter) GetLevel() int8 {
|
||||
if info, err := a.playerManager.GetPlayerInfo(a.entity.GetID()); err == nil {
|
||||
return info.Level
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// HasItem checks if the character has a specific item (placeholder)
|
||||
func (a *EntityCollectionAdapter) HasItem(itemID int32) bool {
|
||||
// TODO: Implement item checking through entity system
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCollectionList placeholder for getting player collection list
|
||||
func (a *EntityCollectionAdapter) GetCollectionList() *PlayerCollectionList {
|
||||
// TODO: Implement collection list retrieval
|
||||
return nil
|
||||
}
|
||||
|
||||
// RewardProvider handles collection reward distribution
|
||||
type RewardProvider interface {
|
||||
// GiveItem gives an item to a player
|
||||
GiveItem(characterID int32, itemID int32, quantity int8) error
|
||||
|
||||
// GiveCoin gives coins to a player
|
||||
GiveCoin(characterID int32, amount int64) error
|
||||
|
||||
// GiveXP gives experience points to a player
|
||||
GiveXP(characterID int32, amount int64) error
|
||||
|
||||
// ValidateRewards checks if rewards can be given
|
||||
ValidateRewards(characterID int32, rewards []CollectionRewardItem, coin, xp int64) error
|
||||
}
|
||||
|
||||
// CollectionEventHandler handles collection-related events
|
||||
type CollectionEventHandler interface {
|
||||
// OnCollectionStarted called when player starts a collection
|
||||
OnCollectionStarted(characterID, collectionID int32)
|
||||
|
||||
// OnItemFound called when player finds a collection item
|
||||
OnItemFound(characterID, collectionID, itemID int32)
|
||||
|
||||
// OnCollectionCompleted called when player completes a collection
|
||||
OnCollectionCompleted(characterID, collectionID int32)
|
||||
|
||||
// OnRewardClaimed called when player claims collection rewards
|
||||
OnRewardClaimed(characterID, collectionID int32, rewards []CollectionRewardItem, coin, xp int64)
|
||||
}
|
||||
|
||||
// LogHandler provides logging functionality
|
||||
type LogHandler interface {
|
||||
// LogDebug logs debug messages
|
||||
LogDebug(category, message string, args ...any)
|
||||
|
||||
// LogInfo logs informational messages
|
||||
LogInfo(category, message string, args ...any)
|
||||
|
||||
// LogError logs error messages
|
||||
LogError(category, message string, args ...any)
|
||||
|
||||
// LogWarning logs warning messages
|
||||
LogWarning(category, message string, args ...any)
|
||||
}
|
@ -1,380 +0,0 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// NewCollectionManager creates a new collection manager instance
|
||||
func NewCollectionManager(database CollectionDatabase, itemLookup ItemLookup) *CollectionManager {
|
||||
return &CollectionManager{
|
||||
masterList: NewMasterCollectionList(database),
|
||||
database: database,
|
||||
itemLookup: itemLookup,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize initializes the collection system by loading all collections
|
||||
func (cm *CollectionManager) Initialize(ctx context.Context) error {
|
||||
return cm.masterList.Initialize(ctx, cm.itemLookup)
|
||||
}
|
||||
|
||||
// GetMasterList returns the master collection list
|
||||
func (cm *CollectionManager) GetMasterList() *MasterCollectionList {
|
||||
return cm.masterList
|
||||
}
|
||||
|
||||
// CreatePlayerCollectionList creates a new player collection list
|
||||
func (cm *CollectionManager) CreatePlayerCollectionList(characterID int32) *PlayerCollectionList {
|
||||
return NewPlayerCollectionList(characterID, cm.database)
|
||||
}
|
||||
|
||||
// GetCollection returns a collection by ID from the master list
|
||||
func (cm *CollectionManager) GetCollection(collectionID int32) *Collection {
|
||||
return cm.masterList.GetCollection(collectionID)
|
||||
}
|
||||
|
||||
// GetCollectionCopy returns a copy of a collection by ID
|
||||
func (cm *CollectionManager) GetCollectionCopy(collectionID int32) *Collection {
|
||||
return cm.masterList.GetCollectionCopy(collectionID)
|
||||
}
|
||||
|
||||
// ProcessItemFound processes when a player finds an item across all collections
|
||||
func (cm *CollectionManager) ProcessItemFound(playerList *PlayerCollectionList, itemID int32) ([]*Collection, error) {
|
||||
if playerList == nil {
|
||||
return nil, fmt.Errorf("player collection list is nil")
|
||||
}
|
||||
|
||||
return playerList.ProcessItemFound(itemID, cm.masterList)
|
||||
}
|
||||
|
||||
// CompleteCollection processes collection completion for a player
|
||||
func (cm *CollectionManager) CompleteCollection(playerList *PlayerCollectionList, collectionID int32, rewardProvider RewardProvider) error {
|
||||
if playerList == nil {
|
||||
return fmt.Errorf("player collection list is nil")
|
||||
}
|
||||
|
||||
collection := playerList.GetCollection(collectionID)
|
||||
if collection == nil {
|
||||
return fmt.Errorf("collection %d not found in player list", collectionID)
|
||||
}
|
||||
|
||||
if !collection.GetIsReadyToTurnIn() {
|
||||
return fmt.Errorf("collection %d is not ready to complete", collectionID)
|
||||
}
|
||||
|
||||
// Give rewards if provider is available
|
||||
if rewardProvider != nil {
|
||||
characterID := playerList.GetCharacterID()
|
||||
|
||||
// Give coin reward
|
||||
if coin := collection.GetRewardCoin(); coin > 0 {
|
||||
if err := rewardProvider.GiveCoin(characterID, coin); err != nil {
|
||||
return fmt.Errorf("failed to give coin reward: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Give XP reward
|
||||
if xp := collection.GetRewardXP(); xp > 0 {
|
||||
if err := rewardProvider.GiveXP(characterID, xp); err != nil {
|
||||
return fmt.Errorf("failed to give XP reward: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Give item rewards
|
||||
for _, reward := range collection.GetRewardItems() {
|
||||
if err := rewardProvider.GiveItem(characterID, reward.ItemID, reward.Quantity); err != nil {
|
||||
return fmt.Errorf("failed to give item reward %d: %w", reward.ItemID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark collection as completed
|
||||
return playerList.CompleteCollection(collectionID)
|
||||
}
|
||||
|
||||
// GetAvailableCollections returns collections available to a player based on level
|
||||
func (cm *CollectionManager) GetAvailableCollections(playerLevel int8) []*Collection {
|
||||
return cm.masterList.GetCollectionsByLevel(0, playerLevel)
|
||||
}
|
||||
|
||||
// GetCollectionsByCategory returns collections in a specific category
|
||||
func (cm *CollectionManager) GetCollectionsByCategory(category string) []*Collection {
|
||||
return cm.masterList.GetCollectionsByCategory(category)
|
||||
}
|
||||
|
||||
// GetAllCategories returns all collection categories
|
||||
func (cm *CollectionManager) GetAllCategories() []string {
|
||||
return cm.masterList.GetCategories()
|
||||
}
|
||||
|
||||
// SearchCollections searches for collections by name
|
||||
func (cm *CollectionManager) SearchCollections(searchTerm string) []*Collection {
|
||||
return cm.masterList.FindCollectionsByName(searchTerm)
|
||||
}
|
||||
|
||||
// ValidateSystemIntegrity validates the integrity of the collection system
|
||||
func (cm *CollectionManager) ValidateSystemIntegrity() []error {
|
||||
return cm.masterList.ValidateIntegrity(cm.itemLookup)
|
||||
}
|
||||
|
||||
// GetSystemStatistics returns overall collection system statistics
|
||||
func (cm *CollectionManager) GetSystemStatistics() CollectionStatistics {
|
||||
return cm.masterList.GetStatistics()
|
||||
}
|
||||
|
||||
// CollectionService provides high-level collection system services
|
||||
type CollectionService struct {
|
||||
manager *CollectionManager
|
||||
playerLists map[int32]*PlayerCollectionList
|
||||
mu sync.RWMutex
|
||||
clientManager ClientManager
|
||||
eventHandler CollectionEventHandler
|
||||
logger LogHandler
|
||||
}
|
||||
|
||||
// NewCollectionService creates a new collection service
|
||||
func NewCollectionService(database CollectionDatabase, itemLookup ItemLookup, clientManager ClientManager) *CollectionService {
|
||||
return &CollectionService{
|
||||
manager: NewCollectionManager(database, itemLookup),
|
||||
playerLists: make(map[int32]*PlayerCollectionList),
|
||||
clientManager: clientManager,
|
||||
}
|
||||
}
|
||||
|
||||
// SetEventHandler sets the collection event handler
|
||||
func (cs *CollectionService) SetEventHandler(handler CollectionEventHandler) {
|
||||
cs.eventHandler = handler
|
||||
}
|
||||
|
||||
// SetLogger sets the logger for the service
|
||||
func (cs *CollectionService) SetLogger(logger LogHandler) {
|
||||
cs.logger = logger
|
||||
}
|
||||
|
||||
// Initialize initializes the collection service
|
||||
func (cs *CollectionService) Initialize(ctx context.Context) error {
|
||||
return cs.manager.Initialize(ctx)
|
||||
}
|
||||
|
||||
// LoadPlayerCollections loads collections for a specific player
|
||||
func (cs *CollectionService) LoadPlayerCollections(ctx context.Context, characterID int32) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
playerList := cs.manager.CreatePlayerCollectionList(characterID)
|
||||
if err := playerList.Initialize(ctx, cs.manager.GetMasterList()); err != nil {
|
||||
return fmt.Errorf("failed to initialize player collections: %w", err)
|
||||
}
|
||||
|
||||
cs.playerLists[characterID] = playerList
|
||||
|
||||
if cs.logger != nil {
|
||||
cs.logger.LogDebug("collections", "Loaded %d collections for character %d",
|
||||
playerList.Size(), characterID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnloadPlayerCollections unloads collections for a player (when they log out)
|
||||
func (cs *CollectionService) UnloadPlayerCollections(ctx context.Context, characterID int32) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
playerList, exists := cs.playerLists[characterID]
|
||||
if !exists {
|
||||
return nil // Already unloaded
|
||||
}
|
||||
|
||||
// Save any pending changes
|
||||
if err := playerList.SaveCollections(ctx); err != nil {
|
||||
if cs.logger != nil {
|
||||
cs.logger.LogError("collections", "Failed to save collections for character %d: %v", characterID, err)
|
||||
}
|
||||
return fmt.Errorf("failed to save collections: %w", err)
|
||||
}
|
||||
|
||||
delete(cs.playerLists, characterID)
|
||||
|
||||
if cs.logger != nil {
|
||||
cs.logger.LogDebug("collections", "Unloaded collections for character %d", characterID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessItemFound processes when a player finds an item
|
||||
func (cs *CollectionService) ProcessItemFound(characterID, itemID int32) error {
|
||||
cs.mu.RLock()
|
||||
playerList, exists := cs.playerLists[characterID]
|
||||
cs.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("player collections not loaded for character %d", characterID)
|
||||
}
|
||||
|
||||
updatedCollections, err := cs.manager.ProcessItemFound(playerList, itemID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to process found item: %w", err)
|
||||
}
|
||||
|
||||
// Notify client and event handler of updates
|
||||
for _, collection := range updatedCollections {
|
||||
if cs.clientManager != nil {
|
||||
cs.clientManager.SendCollectionUpdate(characterID, collection)
|
||||
}
|
||||
|
||||
if cs.eventHandler != nil {
|
||||
cs.eventHandler.OnItemFound(characterID, collection.GetID(), itemID)
|
||||
}
|
||||
|
||||
if cs.logger != nil {
|
||||
cs.logger.LogDebug("collections", "Character %d found item %d for collection %d (%s)",
|
||||
characterID, itemID, collection.GetID(), collection.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteCollection processes collection completion
|
||||
func (cs *CollectionService) CompleteCollection(characterID, collectionID int32, rewardProvider RewardProvider) error {
|
||||
cs.mu.RLock()
|
||||
playerList, exists := cs.playerLists[characterID]
|
||||
cs.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("player collections not loaded for character %d", characterID)
|
||||
}
|
||||
|
||||
collection := playerList.GetCollection(collectionID)
|
||||
if collection == nil {
|
||||
return fmt.Errorf("collection %d not found for character %d", collectionID, characterID)
|
||||
}
|
||||
|
||||
// Complete the collection
|
||||
if err := cs.manager.CompleteCollection(playerList, collectionID, rewardProvider); err != nil {
|
||||
return fmt.Errorf("failed to complete collection: %w", err)
|
||||
}
|
||||
|
||||
// Notify client and event handler
|
||||
if cs.clientManager != nil {
|
||||
cs.clientManager.SendCollectionComplete(characterID, collection)
|
||||
}
|
||||
|
||||
if cs.eventHandler != nil {
|
||||
rewards := collection.GetRewardItems()
|
||||
selectableRewards := collection.GetSelectableRewardItems()
|
||||
allRewards := append(rewards, selectableRewards...)
|
||||
cs.eventHandler.OnCollectionCompleted(characterID, collectionID)
|
||||
cs.eventHandler.OnRewardClaimed(characterID, collectionID, allRewards,
|
||||
collection.GetRewardCoin(), collection.GetRewardXP())
|
||||
}
|
||||
|
||||
if cs.logger != nil {
|
||||
cs.logger.LogInfo("collections", "Character %d completed collection %d (%s)",
|
||||
characterID, collectionID, collection.GetName())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlayerCollections returns all collections for a player
|
||||
func (cs *CollectionService) GetPlayerCollections(characterID int32) ([]*Collection, error) {
|
||||
cs.mu.RLock()
|
||||
playerList, exists := cs.playerLists[characterID]
|
||||
cs.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("player collections not loaded for character %d", characterID)
|
||||
}
|
||||
|
||||
return playerList.GetAllCollections(), nil
|
||||
}
|
||||
|
||||
// GetPlayerCollectionProgress returns detailed progress for all player collections
|
||||
func (cs *CollectionService) GetPlayerCollectionProgress(characterID int32) ([]CollectionProgress, error) {
|
||||
cs.mu.RLock()
|
||||
playerList, exists := cs.playerLists[characterID]
|
||||
cs.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("player collections not loaded for character %d", characterID)
|
||||
}
|
||||
|
||||
return playerList.GetCollectionProgress(), nil
|
||||
}
|
||||
|
||||
// SendCollectionList sends available collections to a player
|
||||
func (cs *CollectionService) SendCollectionList(characterID int32, playerLevel int8) error {
|
||||
if cs.clientManager == nil {
|
||||
return fmt.Errorf("client manager not available")
|
||||
}
|
||||
|
||||
collections := cs.manager.GetAvailableCollections(playerLevel)
|
||||
collectionInfos := make([]CollectionInfo, len(collections))
|
||||
|
||||
for i, collection := range collections {
|
||||
collectionInfos[i] = collection.GetCollectionInfo()
|
||||
}
|
||||
|
||||
return cs.clientManager.SendCollectionList(characterID, collectionInfos)
|
||||
}
|
||||
|
||||
// SaveAllPlayerCollections saves all loaded player collections
|
||||
func (cs *CollectionService) SaveAllPlayerCollections(ctx context.Context) error {
|
||||
cs.mu.RLock()
|
||||
playerLists := make(map[int32]*PlayerCollectionList)
|
||||
for characterID, playerList := range cs.playerLists {
|
||||
playerLists[characterID] = playerList
|
||||
}
|
||||
cs.mu.RUnlock()
|
||||
|
||||
var saveErrors []error
|
||||
for characterID, playerList := range playerLists {
|
||||
if err := playerList.SaveCollections(ctx); err != nil {
|
||||
saveErrors = append(saveErrors, fmt.Errorf("character %d: %w", characterID, err))
|
||||
if cs.logger != nil {
|
||||
cs.logger.LogError("collections", "Failed to save collections for character %d: %v", characterID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(saveErrors) > 0 {
|
||||
return fmt.Errorf("failed to save some player collections: %v", saveErrors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLoadedPlayerCount returns the number of players with loaded collections
|
||||
func (cs *CollectionService) GetLoadedPlayerCount() int {
|
||||
cs.mu.RLock()
|
||||
defer cs.mu.RUnlock()
|
||||
return len(cs.playerLists)
|
||||
}
|
||||
|
||||
// IsPlayerLoaded checks if a player's collections are loaded
|
||||
func (cs *CollectionService) IsPlayerLoaded(characterID int32) bool {
|
||||
cs.mu.RLock()
|
||||
defer cs.mu.RUnlock()
|
||||
_, exists := cs.playerLists[characterID]
|
||||
return exists
|
||||
}
|
||||
|
||||
// GetMasterCollection returns a collection from the master list
|
||||
func (cs *CollectionService) GetMasterCollection(collectionID int32) *Collection {
|
||||
return cs.manager.GetCollection(collectionID)
|
||||
}
|
||||
|
||||
// GetAllCategories returns all collection categories
|
||||
func (cs *CollectionService) GetAllCategories() []string {
|
||||
return cs.manager.GetAllCategories()
|
||||
}
|
||||
|
||||
// SearchCollections searches for collections by name
|
||||
func (cs *CollectionService) SearchCollections(searchTerm string) []*Collection {
|
||||
return cs.manager.SearchCollections(searchTerm)
|
||||
}
|
327
internal/collections/master.go
Normal file
327
internal/collections/master.go
Normal file
@ -0,0 +1,327 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"eq2emu/internal/common"
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// MasterList manages a collection of collections using the generic MasterList base
|
||||
type MasterList struct {
|
||||
*common.MasterList[int32, *Collection]
|
||||
}
|
||||
|
||||
// NewMasterList creates a new collection master list
|
||||
func NewMasterList() *MasterList {
|
||||
return &MasterList{
|
||||
MasterList: common.NewMasterList[int32, *Collection](),
|
||||
}
|
||||
}
|
||||
|
||||
// AddCollection adds a collection to the master list
|
||||
func (ml *MasterList) AddCollection(collection *Collection) bool {
|
||||
return ml.Add(collection)
|
||||
}
|
||||
|
||||
// GetCollection retrieves a collection by ID
|
||||
func (ml *MasterList) GetCollection(id int32) *Collection {
|
||||
return ml.Get(id)
|
||||
}
|
||||
|
||||
// GetCollectionSafe retrieves a collection by ID with existence check
|
||||
func (ml *MasterList) GetCollectionSafe(id int32) (*Collection, bool) {
|
||||
return ml.GetSafe(id)
|
||||
}
|
||||
|
||||
// HasCollection checks if a collection exists by ID
|
||||
func (ml *MasterList) HasCollection(id int32) bool {
|
||||
return ml.Exists(id)
|
||||
}
|
||||
|
||||
// RemoveCollection removes a collection by ID
|
||||
func (ml *MasterList) RemoveCollection(id int32) bool {
|
||||
return ml.Remove(id)
|
||||
}
|
||||
|
||||
// GetAllCollections returns all collections as a map
|
||||
func (ml *MasterList) GetAllCollections() map[int32]*Collection {
|
||||
return ml.GetAll()
|
||||
}
|
||||
|
||||
// GetAllCollectionsList returns all collections as a slice
|
||||
func (ml *MasterList) GetAllCollectionsList() []*Collection {
|
||||
return ml.GetAllSlice()
|
||||
}
|
||||
|
||||
// GetCollectionCount returns the number of collections
|
||||
func (ml *MasterList) GetCollectionCount() int {
|
||||
return ml.Size()
|
||||
}
|
||||
|
||||
// ClearCollections removes all collections from the list
|
||||
func (ml *MasterList) ClearCollections() {
|
||||
ml.Clear()
|
||||
}
|
||||
|
||||
// NeedsItem checks if any collection needs the specified item
|
||||
func (ml *MasterList) NeedsItem(itemID int32) bool {
|
||||
for _, collection := range ml.GetAll() {
|
||||
if collection.NeedsItem(itemID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FindCollectionsByCategory finds collections in a specific category
|
||||
func (ml *MasterList) FindCollectionsByCategory(category string) []*Collection {
|
||||
return ml.Filter(func(collection *Collection) bool {
|
||||
return collection.GetCategory() == category
|
||||
})
|
||||
}
|
||||
|
||||
// FindCollectionsByLevel finds collections for a specific level range
|
||||
func (ml *MasterList) FindCollectionsByLevel(minLevel, maxLevel int8) []*Collection {
|
||||
return ml.Filter(func(collection *Collection) bool {
|
||||
level := collection.GetLevel()
|
||||
return level >= minLevel && level <= maxLevel
|
||||
})
|
||||
}
|
||||
|
||||
// GetCollectionsNeedingItem returns all collections that need a specific item
|
||||
func (ml *MasterList) GetCollectionsNeedingItem(itemID int32) []*Collection {
|
||||
return ml.Filter(func(collection *Collection) bool {
|
||||
return collection.NeedsItem(itemID)
|
||||
})
|
||||
}
|
||||
|
||||
// GetCategories returns all unique collection categories
|
||||
func (ml *MasterList) GetCategories() []string {
|
||||
categoryMap := make(map[string]bool)
|
||||
ml.ForEach(func(id int32, collection *Collection) {
|
||||
categoryMap[collection.GetCategory()] = true
|
||||
})
|
||||
|
||||
categories := make([]string, 0, len(categoryMap))
|
||||
for category := range categoryMap {
|
||||
categories = append(categories, category)
|
||||
}
|
||||
return categories
|
||||
}
|
||||
|
||||
// ValidateCollections checks all collections for consistency
|
||||
func (ml *MasterList) ValidateCollections() []string {
|
||||
var issues []string
|
||||
|
||||
ml.ForEach(func(id int32, collection *Collection) {
|
||||
if collection == nil {
|
||||
issues = append(issues, fmt.Sprintf("Collection ID %d is nil", id))
|
||||
return
|
||||
}
|
||||
|
||||
if collection.GetID() != id {
|
||||
issues = append(issues, fmt.Sprintf("Collection ID mismatch: map key %d != collection ID %d", id, collection.GetID()))
|
||||
}
|
||||
|
||||
if len(collection.GetName()) == 0 {
|
||||
issues = append(issues, fmt.Sprintf("Collection ID %d has empty name", id))
|
||||
}
|
||||
|
||||
if len(collection.GetCategory()) == 0 {
|
||||
issues = append(issues, fmt.Sprintf("Collection ID %d has empty category", id))
|
||||
}
|
||||
|
||||
if collection.GetLevel() < 0 {
|
||||
issues = append(issues, fmt.Sprintf("Collection ID %d has negative level: %d", id, collection.GetLevel()))
|
||||
}
|
||||
|
||||
if len(collection.CollectionItems) == 0 {
|
||||
issues = append(issues, fmt.Sprintf("Collection ID %d has no collection items", id))
|
||||
}
|
||||
|
||||
// Check for duplicate item indices
|
||||
indexMap := make(map[int8]bool)
|
||||
for _, item := range collection.CollectionItems {
|
||||
if indexMap[item.Index] {
|
||||
issues = append(issues, fmt.Sprintf("Collection ID %d has duplicate item index: %d", id, item.Index))
|
||||
}
|
||||
indexMap[item.Index] = true
|
||||
}
|
||||
})
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
// IsValid returns true if all collections are valid
|
||||
func (ml *MasterList) IsValid() bool {
|
||||
issues := ml.ValidateCollections()
|
||||
return len(issues) == 0
|
||||
}
|
||||
|
||||
// GetStatistics returns statistics about the collection collection
|
||||
func (ml *MasterList) GetStatistics() map[string]any {
|
||||
stats := make(map[string]any)
|
||||
stats["total_collections"] = ml.Size()
|
||||
|
||||
if ml.IsEmpty() {
|
||||
return stats
|
||||
}
|
||||
|
||||
// Count by category
|
||||
categoryCounts := make(map[string]int)
|
||||
var totalItems, totalRewards int
|
||||
var minLevel, maxLevel int8 = 127, 0
|
||||
var minID, maxID int32
|
||||
first := true
|
||||
|
||||
ml.ForEach(func(id int32, collection *Collection) {
|
||||
categoryCounts[collection.GetCategory()]++
|
||||
totalItems += len(collection.CollectionItems)
|
||||
totalRewards += len(collection.RewardItems) + len(collection.SelectableRewardItems)
|
||||
|
||||
level := collection.GetLevel()
|
||||
if level < minLevel {
|
||||
minLevel = level
|
||||
}
|
||||
if level > maxLevel {
|
||||
maxLevel = level
|
||||
}
|
||||
|
||||
if first {
|
||||
minID = id
|
||||
maxID = id
|
||||
first = false
|
||||
} else {
|
||||
if id < minID {
|
||||
minID = id
|
||||
}
|
||||
if id > maxID {
|
||||
maxID = id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
stats["collections_by_category"] = categoryCounts
|
||||
stats["total_collection_items"] = totalItems
|
||||
stats["total_rewards"] = totalRewards
|
||||
stats["min_level"] = minLevel
|
||||
stats["max_level"] = maxLevel
|
||||
stats["min_id"] = minID
|
||||
stats["max_id"] = maxID
|
||||
stats["id_range"] = maxID - minID
|
||||
stats["average_items_per_collection"] = float64(totalItems) / float64(ml.Size())
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// LoadAllCollections loads all collections from the database into the master list
|
||||
func (ml *MasterList) LoadAllCollections(db *database.Database) error {
|
||||
if db == nil {
|
||||
return fmt.Errorf("database connection is nil")
|
||||
}
|
||||
|
||||
// Clear existing collections
|
||||
ml.Clear()
|
||||
|
||||
query := `SELECT id, collection_name, collection_category, level FROM collections ORDER BY id`
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query collections: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
collection := &Collection{
|
||||
db: db,
|
||||
isNew: false,
|
||||
CollectionItems: make([]CollectionItem, 0),
|
||||
RewardItems: make([]CollectionRewardItem, 0),
|
||||
SelectableRewardItems: make([]CollectionRewardItem, 0),
|
||||
}
|
||||
|
||||
err := rows.Scan(&collection.ID, &collection.Name, &collection.Category, &collection.Level)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to scan collection: %w", err)
|
||||
}
|
||||
|
||||
// Load collection items for this collection
|
||||
itemQuery := `SELECT item_id, item_index, found FROM collection_items WHERE collection_id = ? ORDER BY item_index`
|
||||
itemRows, err := db.Query(itemQuery, collection.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load collection items for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
|
||||
for itemRows.Next() {
|
||||
var item CollectionItem
|
||||
if err := itemRows.Scan(&item.ItemID, &item.Index, &item.Found); err != nil {
|
||||
itemRows.Close()
|
||||
return fmt.Errorf("failed to scan collection item: %w", err)
|
||||
}
|
||||
collection.CollectionItems = append(collection.CollectionItems, item)
|
||||
}
|
||||
itemRows.Close()
|
||||
|
||||
// Load rewards for this collection
|
||||
rewardQuery := `SELECT reward_type, reward_value, reward_quantity FROM collection_rewards WHERE collection_id = ?`
|
||||
rewardRows, err := db.Query(rewardQuery, collection.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load rewards for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
|
||||
for rewardRows.Next() {
|
||||
var rewardType, rewardValue string
|
||||
var quantity int8
|
||||
|
||||
if err := rewardRows.Scan(&rewardType, &rewardValue, &quantity); err != nil {
|
||||
rewardRows.Close()
|
||||
return 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
rewardRows.Close()
|
||||
|
||||
if !ml.AddCollection(collection) {
|
||||
return fmt.Errorf("failed to add collection %d to master list", collection.ID)
|
||||
}
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("error iterating collection rows: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAllCollectionsFromDatabase is a convenience function that creates a master list and loads all collections
|
||||
func LoadAllCollectionsFromDatabase(db *database.Database) (*MasterList, error) {
|
||||
masterList := NewMasterList()
|
||||
err := masterList.LoadAllCollections(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return masterList, nil
|
||||
}
|
@ -1,336 +0,0 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewMasterCollectionList creates a new master collection list
|
||||
func NewMasterCollectionList(database CollectionDatabase) *MasterCollectionList {
|
||||
return &MasterCollectionList{
|
||||
collections: make(map[int32]*Collection),
|
||||
database: database,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize loads all collections from the database
|
||||
func (mcl *MasterCollectionList) Initialize(ctx context.Context, itemLookup ItemLookup) error {
|
||||
mcl.mu.Lock()
|
||||
defer mcl.mu.Unlock()
|
||||
|
||||
// Load collection data
|
||||
collectionData, err := mcl.database.LoadCollections(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load collections: %w", err)
|
||||
}
|
||||
|
||||
totalItems := 0
|
||||
totalRewards := 0
|
||||
|
||||
for _, data := range collectionData {
|
||||
collection := NewCollection()
|
||||
collection.SetID(data.ID)
|
||||
collection.SetName(data.Name)
|
||||
collection.SetCategory(data.Category)
|
||||
collection.SetLevel(data.Level)
|
||||
|
||||
// Load collection items
|
||||
items, err := mcl.database.LoadCollectionItems(ctx, data.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load items for collection %d: %w", data.ID, err)
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
// Validate item exists
|
||||
if itemLookup != nil && !itemLookup.ItemExists(item.ItemID) {
|
||||
continue // Skip non-existent items
|
||||
}
|
||||
collection.AddCollectionItem(item)
|
||||
totalItems++
|
||||
}
|
||||
|
||||
// Load collection rewards
|
||||
rewards, err := mcl.database.LoadCollectionRewards(ctx, data.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load rewards for collection %d: %w", data.ID, err)
|
||||
}
|
||||
|
||||
if err := collection.LoadFromRewardData(rewards); err != nil {
|
||||
return fmt.Errorf("failed to load reward data for collection %d: %w", data.ID, err)
|
||||
}
|
||||
totalRewards += len(rewards)
|
||||
|
||||
// Validate collection before adding
|
||||
if err := collection.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid collection %d (%s): %w", data.ID, data.Name, err)
|
||||
}
|
||||
|
||||
if !mcl.addCollectionNoLock(collection) {
|
||||
return fmt.Errorf("duplicate collection ID: %d", data.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddCollection adds a collection to the master list
|
||||
func (mcl *MasterCollectionList) AddCollection(collection *Collection) bool {
|
||||
mcl.mu.Lock()
|
||||
defer mcl.mu.Unlock()
|
||||
return mcl.addCollectionNoLock(collection)
|
||||
}
|
||||
|
||||
// addCollectionNoLock adds a collection without acquiring the lock
|
||||
func (mcl *MasterCollectionList) addCollectionNoLock(collection *Collection) bool {
|
||||
if collection == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
id := collection.GetID()
|
||||
if _, exists := mcl.collections[id]; exists {
|
||||
return false
|
||||
}
|
||||
|
||||
mcl.collections[id] = collection
|
||||
return true
|
||||
}
|
||||
|
||||
// GetCollection retrieves a collection by ID
|
||||
func (mcl *MasterCollectionList) GetCollection(collectionID int32) *Collection {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
return mcl.collections[collectionID]
|
||||
}
|
||||
|
||||
// GetCollectionCopy retrieves a copy of a collection by ID
|
||||
func (mcl *MasterCollectionList) GetCollectionCopy(collectionID int32) *Collection {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
if collection, exists := mcl.collections[collectionID]; exists {
|
||||
return NewCollectionFromData(collection)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearCollections removes all collections
|
||||
func (mcl *MasterCollectionList) ClearCollections() {
|
||||
mcl.mu.Lock()
|
||||
defer mcl.mu.Unlock()
|
||||
mcl.collections = make(map[int32]*Collection)
|
||||
}
|
||||
|
||||
// Size returns the number of collections
|
||||
func (mcl *MasterCollectionList) Size() int {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
return len(mcl.collections)
|
||||
}
|
||||
|
||||
// NeedsItem checks if any collection needs a specific item
|
||||
func (mcl *MasterCollectionList) NeedsItem(itemID int32) bool {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
for _, collection := range mcl.collections {
|
||||
if collection.NeedsItem(itemID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCollectionsByCategory returns collections in a specific category
|
||||
func (mcl *MasterCollectionList) GetCollectionsByCategory(category string) []*Collection {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
var result []*Collection
|
||||
for _, collection := range mcl.collections {
|
||||
if collection.GetCategory() == category {
|
||||
result = append(result, collection)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetCollectionsByLevel returns collections for a specific level range
|
||||
func (mcl *MasterCollectionList) GetCollectionsByLevel(minLevel, maxLevel int8) []*Collection {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
var result []*Collection
|
||||
for _, collection := range mcl.collections {
|
||||
level := collection.GetLevel()
|
||||
if level >= minLevel && level <= maxLevel {
|
||||
result = append(result, collection)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAllCollections returns all collections
|
||||
func (mcl *MasterCollectionList) GetAllCollections() []*Collection {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
result := make([]*Collection, 0, len(mcl.collections))
|
||||
for _, collection := range mcl.collections {
|
||||
result = append(result, collection)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetCollectionIDs returns all collection IDs
|
||||
func (mcl *MasterCollectionList) GetCollectionIDs() []int32 {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
ids := make([]int32, 0, len(mcl.collections))
|
||||
for id := range mcl.collections {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
sort.Slice(ids, func(i, j int) bool {
|
||||
return ids[i] < ids[j]
|
||||
})
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
// GetCategories returns all unique categories
|
||||
func (mcl *MasterCollectionList) GetCategories() []string {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
categoryMap := make(map[string]bool)
|
||||
for _, collection := range mcl.collections {
|
||||
category := collection.GetCategory()
|
||||
if category != "" {
|
||||
categoryMap[category] = true
|
||||
}
|
||||
}
|
||||
|
||||
categories := make([]string, 0, len(categoryMap))
|
||||
for category := range categoryMap {
|
||||
categories = append(categories, category)
|
||||
}
|
||||
|
||||
sort.Strings(categories)
|
||||
return categories
|
||||
}
|
||||
|
||||
// GetCollectionsRequiringItem returns collections that need a specific item
|
||||
func (mcl *MasterCollectionList) GetCollectionsRequiringItem(itemID int32) []*Collection {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
var result []*Collection
|
||||
for _, collection := range mcl.collections {
|
||||
if collection.NeedsItem(itemID) {
|
||||
result = append(result, collection)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetStatistics returns master collection list statistics
|
||||
func (mcl *MasterCollectionList) GetStatistics() CollectionStatistics {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
stats := CollectionStatistics{
|
||||
TotalCollections: len(mcl.collections),
|
||||
}
|
||||
|
||||
for _, collection := range mcl.collections {
|
||||
if collection.GetCompleted() {
|
||||
stats.CompletedCollections++
|
||||
}
|
||||
if collection.GetTotalItemsCount() > 0 {
|
||||
stats.ActiveCollections++
|
||||
}
|
||||
stats.TotalItems += collection.GetTotalItemsCount()
|
||||
stats.FoundItems += collection.GetFoundItemsCount()
|
||||
stats.TotalRewards += len(collection.GetRewardItems()) + len(collection.GetSelectableRewardItems())
|
||||
if collection.GetRewardCoin() > 0 {
|
||||
stats.TotalRewards++
|
||||
}
|
||||
if collection.GetRewardXP() > 0 {
|
||||
stats.TotalRewards++
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// ValidateIntegrity checks the integrity of all collections
|
||||
func (mcl *MasterCollectionList) ValidateIntegrity(itemLookup ItemLookup) []error {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
var errors []error
|
||||
|
||||
for _, collection := range mcl.collections {
|
||||
if err := collection.Validate(); err != nil {
|
||||
errors = append(errors, fmt.Errorf("collection %d (%s): %w",
|
||||
collection.GetID(), collection.GetName(), err))
|
||||
}
|
||||
|
||||
// Check if all required items exist
|
||||
if itemLookup != nil {
|
||||
for _, item := range collection.GetCollectionItems() {
|
||||
if !itemLookup.ItemExists(item.ItemID) {
|
||||
errors = append(errors, fmt.Errorf("collection %d (%s) references non-existent item %d",
|
||||
collection.GetID(), collection.GetName(), item.ItemID))
|
||||
}
|
||||
}
|
||||
|
||||
// Check reward items
|
||||
for _, item := range collection.GetRewardItems() {
|
||||
if !itemLookup.ItemExists(item.ItemID) {
|
||||
errors = append(errors, fmt.Errorf("collection %d (%s) has non-existent reward item %d",
|
||||
collection.GetID(), collection.GetName(), item.ItemID))
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range collection.GetSelectableRewardItems() {
|
||||
if !itemLookup.ItemExists(item.ItemID) {
|
||||
errors = append(errors, fmt.Errorf("collection %d (%s) has non-existent selectable reward item %d",
|
||||
collection.GetID(), collection.GetName(), item.ItemID))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// FindCollectionsByName searches for collections by name (case-insensitive)
|
||||
func (mcl *MasterCollectionList) FindCollectionsByName(searchTerm string) []*Collection {
|
||||
mcl.mu.RLock()
|
||||
defer mcl.mu.RUnlock()
|
||||
|
||||
var result []*Collection
|
||||
searchLower := strings.ToLower(searchTerm)
|
||||
|
||||
for _, collection := range mcl.collections {
|
||||
if strings.Contains(strings.ToLower(collection.GetName()), searchLower) {
|
||||
result = append(result, collection)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].GetName() < result[j].GetName()
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
365
internal/collections/master_test.go
Normal file
365
internal/collections/master_test.go
Normal file
@ -0,0 +1,365 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
func TestNewMasterList(t *testing.T) {
|
||||
masterList := NewMasterList()
|
||||
|
||||
if masterList == nil {
|
||||
t.Fatal("NewMasterList returned nil")
|
||||
}
|
||||
|
||||
if masterList.GetCollectionCount() != 0 {
|
||||
t.Errorf("Expected count 0, got %d", masterList.GetCollectionCount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterListBasicOperations(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Create test collections
|
||||
collection1 := NewWithData(1001, "Heritage Collection", "Heritage", 20, db)
|
||||
collection2 := NewWithData(1002, "Treasured Collection", "Treasured", 30, db)
|
||||
|
||||
// Test adding
|
||||
if !masterList.AddCollection(collection1) {
|
||||
t.Error("Should successfully add collection1")
|
||||
}
|
||||
|
||||
if !masterList.AddCollection(collection2) {
|
||||
t.Error("Should successfully add collection2")
|
||||
}
|
||||
|
||||
// Test duplicate add (should fail)
|
||||
if masterList.AddCollection(collection1) {
|
||||
t.Error("Should not add duplicate collection")
|
||||
}
|
||||
|
||||
if masterList.GetCollectionCount() != 2 {
|
||||
t.Errorf("Expected count 2, got %d", masterList.GetCollectionCount())
|
||||
}
|
||||
|
||||
// Test retrieving
|
||||
retrieved := masterList.GetCollection(1001)
|
||||
if retrieved == nil {
|
||||
t.Error("Should retrieve added collection")
|
||||
}
|
||||
|
||||
if retrieved.GetName() != "Heritage Collection" {
|
||||
t.Errorf("Expected name 'Heritage Collection', got '%s'", retrieved.GetName())
|
||||
}
|
||||
|
||||
// Test safe retrieval
|
||||
retrieved, exists := masterList.GetCollectionSafe(1001)
|
||||
if !exists || retrieved == nil {
|
||||
t.Error("GetCollectionSafe should return collection and true")
|
||||
}
|
||||
|
||||
_, exists = masterList.GetCollectionSafe(9999)
|
||||
if exists {
|
||||
t.Error("GetCollectionSafe should return false for non-existent ID")
|
||||
}
|
||||
|
||||
// Test HasCollection
|
||||
if !masterList.HasCollection(1001) {
|
||||
t.Error("HasCollection should return true for existing ID")
|
||||
}
|
||||
|
||||
if masterList.HasCollection(9999) {
|
||||
t.Error("HasCollection should return false for non-existent ID")
|
||||
}
|
||||
|
||||
// Test removing
|
||||
if !masterList.RemoveCollection(1001) {
|
||||
t.Error("Should successfully remove collection")
|
||||
}
|
||||
|
||||
if masterList.GetCollectionCount() != 1 {
|
||||
t.Errorf("Expected count 1, got %d", masterList.GetCollectionCount())
|
||||
}
|
||||
|
||||
if masterList.HasCollection(1001) {
|
||||
t.Error("Collection should be removed")
|
||||
}
|
||||
|
||||
// Test clear
|
||||
masterList.ClearCollections()
|
||||
if masterList.GetCollectionCount() != 0 {
|
||||
t.Errorf("Expected count 0 after clear, got %d", masterList.GetCollectionCount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterListItemNeeds(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Create collections with items
|
||||
collection1 := NewWithData(1001, "Heritage Collection", "Heritage", 20, db)
|
||||
collection1.CollectionItems = append(collection1.CollectionItems, CollectionItem{
|
||||
ItemID: 12345,
|
||||
Index: 0,
|
||||
Found: ItemNotFound,
|
||||
})
|
||||
collection1.CollectionItems = append(collection1.CollectionItems, CollectionItem{
|
||||
ItemID: 12346,
|
||||
Index: 1,
|
||||
Found: ItemFound, // Already found
|
||||
})
|
||||
|
||||
collection2 := NewWithData(1002, "Treasured Collection", "Treasured", 30, db)
|
||||
collection2.CollectionItems = append(collection2.CollectionItems, CollectionItem{
|
||||
ItemID: 12347,
|
||||
Index: 0,
|
||||
Found: ItemNotFound,
|
||||
})
|
||||
|
||||
masterList.AddCollection(collection1)
|
||||
masterList.AddCollection(collection2)
|
||||
|
||||
// Test NeedsItem
|
||||
if !masterList.NeedsItem(12345) {
|
||||
t.Error("MasterList should need item 12345")
|
||||
}
|
||||
|
||||
if masterList.NeedsItem(12346) {
|
||||
t.Error("MasterList should not need item 12346 (already found)")
|
||||
}
|
||||
|
||||
if !masterList.NeedsItem(12347) {
|
||||
t.Error("MasterList should need item 12347")
|
||||
}
|
||||
|
||||
if masterList.NeedsItem(99999) {
|
||||
t.Error("MasterList should not need item 99999")
|
||||
}
|
||||
|
||||
// Test GetCollectionsNeedingItem
|
||||
needingItem := masterList.GetCollectionsNeedingItem(12345)
|
||||
if len(needingItem) != 1 {
|
||||
t.Errorf("Expected 1 collection needing item 12345, got %d", len(needingItem))
|
||||
}
|
||||
|
||||
needingNone := masterList.GetCollectionsNeedingItem(99999)
|
||||
if len(needingNone) != 0 {
|
||||
t.Errorf("Expected 0 collections needing item 99999, got %d", len(needingNone))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterListFiltering(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Add test collections
|
||||
collections := []*Collection{
|
||||
NewWithData(1, "Heritage 1", "Heritage", 10, db),
|
||||
NewWithData(2, "Heritage 2", "Heritage", 20, db),
|
||||
NewWithData(3, "Treasured 1", "Treasured", 15, db),
|
||||
NewWithData(4, "Treasured 2", "Treasured", 25, db),
|
||||
NewWithData(5, "Legendary 1", "Legendary", 30, db),
|
||||
}
|
||||
|
||||
for _, collection := range collections {
|
||||
masterList.AddCollection(collection)
|
||||
}
|
||||
|
||||
// Test FindCollectionsByCategory
|
||||
heritageCollections := masterList.FindCollectionsByCategory("Heritage")
|
||||
if len(heritageCollections) != 2 {
|
||||
t.Errorf("FindCollectionsByCategory('Heritage') returned %v results, want 2", len(heritageCollections))
|
||||
}
|
||||
|
||||
treasuredCollections := masterList.FindCollectionsByCategory("Treasured")
|
||||
if len(treasuredCollections) != 2 {
|
||||
t.Errorf("FindCollectionsByCategory('Treasured') returned %v results, want 2", len(treasuredCollections))
|
||||
}
|
||||
|
||||
// Test FindCollectionsByLevel
|
||||
lowLevel := masterList.FindCollectionsByLevel(10, 15)
|
||||
if len(lowLevel) != 2 {
|
||||
t.Errorf("FindCollectionsByLevel(10, 15) returned %v results, want 2", len(lowLevel))
|
||||
}
|
||||
|
||||
midLevel := masterList.FindCollectionsByLevel(20, 25)
|
||||
if len(midLevel) != 2 {
|
||||
t.Errorf("FindCollectionsByLevel(20, 25) returned %v results, want 2", len(midLevel))
|
||||
}
|
||||
|
||||
highLevel := masterList.FindCollectionsByLevel(30, 40)
|
||||
if len(highLevel) != 1 {
|
||||
t.Errorf("FindCollectionsByLevel(30, 40) returned %v results, want 1", len(highLevel))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterListCategories(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Add collections with different categories
|
||||
masterList.AddCollection(NewWithData(1, "Test1", "Heritage", 10, db))
|
||||
masterList.AddCollection(NewWithData(2, "Test2", "Heritage", 20, db))
|
||||
masterList.AddCollection(NewWithData(3, "Test3", "Treasured", 15, db))
|
||||
masterList.AddCollection(NewWithData(4, "Test4", "Legendary", 30, db))
|
||||
|
||||
categories := masterList.GetCategories()
|
||||
|
||||
expectedCategories := []string{"Heritage", "Treasured", "Legendary"}
|
||||
if len(categories) != len(expectedCategories) {
|
||||
t.Errorf("Expected %d categories, got %d", len(expectedCategories), len(categories))
|
||||
}
|
||||
|
||||
// Check that all expected categories are present
|
||||
categoryMap := make(map[string]bool)
|
||||
for _, category := range categories {
|
||||
categoryMap[category] = true
|
||||
}
|
||||
|
||||
for _, expected := range expectedCategories {
|
||||
if !categoryMap[expected] {
|
||||
t.Errorf("Expected category '%s' not found", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterListGetAll(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Add test collections
|
||||
for i := int32(1); i <= 3; i++ {
|
||||
collection := NewWithData(i*100, "Test", "Heritage", 20, db)
|
||||
masterList.AddCollection(collection)
|
||||
}
|
||||
|
||||
// Test GetAllCollections (map)
|
||||
allMap := masterList.GetAllCollections()
|
||||
if len(allMap) != 3 {
|
||||
t.Errorf("GetAllCollections() returned %v items, want 3", len(allMap))
|
||||
}
|
||||
|
||||
// Verify it's a copy by modifying returned map
|
||||
delete(allMap, 100)
|
||||
if masterList.GetCollectionCount() != 3 {
|
||||
t.Error("Modifying returned map affected internal state")
|
||||
}
|
||||
|
||||
// Test GetAllCollectionsList (slice)
|
||||
allList := masterList.GetAllCollectionsList()
|
||||
if len(allList) != 3 {
|
||||
t.Errorf("GetAllCollectionsList() returned %v items, want 3", len(allList))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterListValidation(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Add valid collection
|
||||
collection1 := NewWithData(100, "Valid Collection", "Heritage", 20, db)
|
||||
collection1.CollectionItems = append(collection1.CollectionItems, CollectionItem{
|
||||
ItemID: 12345,
|
||||
Index: 0,
|
||||
Found: ItemNotFound,
|
||||
})
|
||||
masterList.AddCollection(collection1)
|
||||
|
||||
issues := masterList.ValidateCollections()
|
||||
if len(issues) != 0 {
|
||||
t.Errorf("ValidateCollections() returned issues for valid data: %v", issues)
|
||||
}
|
||||
|
||||
if !masterList.IsValid() {
|
||||
t.Error("IsValid() should return true for valid data")
|
||||
}
|
||||
|
||||
// Add invalid collection (empty name)
|
||||
collection2 := NewWithData(200, "", "Heritage", 20, db)
|
||||
masterList.AddCollection(collection2)
|
||||
|
||||
issues = masterList.ValidateCollections()
|
||||
if len(issues) == 0 {
|
||||
t.Error("ValidateCollections() should return issues for invalid data")
|
||||
}
|
||||
|
||||
if masterList.IsValid() {
|
||||
t.Error("IsValid() should return false for invalid data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterListStatistics(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Add collections with different categories and levels
|
||||
collection1 := NewWithData(10, "Heritage1", "Heritage", 10, db)
|
||||
collection1.CollectionItems = append(collection1.CollectionItems, CollectionItem{ItemID: 1, Index: 0, Found: 0})
|
||||
collection1.CollectionItems = append(collection1.CollectionItems, CollectionItem{ItemID: 2, Index: 1, Found: 0})
|
||||
collection1.RewardItems = append(collection1.RewardItems, CollectionRewardItem{ItemID: 1001, Quantity: 1})
|
||||
|
||||
collection2 := NewWithData(20, "Heritage2", "Heritage", 20, db)
|
||||
collection2.CollectionItems = append(collection2.CollectionItems, CollectionItem{ItemID: 3, Index: 0, Found: 0})
|
||||
collection2.RewardItems = append(collection2.RewardItems, CollectionRewardItem{ItemID: 1002, Quantity: 1})
|
||||
collection2.SelectableRewardItems = append(collection2.SelectableRewardItems, CollectionRewardItem{ItemID: 1003, Quantity: 1})
|
||||
|
||||
collection3 := NewWithData(30, "Treasured1", "Treasured", 30, db)
|
||||
collection3.CollectionItems = append(collection3.CollectionItems, CollectionItem{ItemID: 4, Index: 0, Found: 0})
|
||||
|
||||
masterList.AddCollection(collection1)
|
||||
masterList.AddCollection(collection2)
|
||||
masterList.AddCollection(collection3)
|
||||
|
||||
stats := masterList.GetStatistics()
|
||||
|
||||
if total, ok := stats["total_collections"].(int); !ok || total != 3 {
|
||||
t.Errorf("total_collections = %v, want 3", stats["total_collections"])
|
||||
}
|
||||
|
||||
if totalItems, ok := stats["total_collection_items"].(int); !ok || totalItems != 4 {
|
||||
t.Errorf("total_collection_items = %v, want 4", stats["total_collection_items"])
|
||||
}
|
||||
|
||||
if totalRewards, ok := stats["total_rewards"].(int); !ok || totalRewards != 3 {
|
||||
t.Errorf("total_rewards = %v, want 3", stats["total_rewards"])
|
||||
}
|
||||
|
||||
if minLevel, ok := stats["min_level"].(int8); !ok || minLevel != 10 {
|
||||
t.Errorf("min_level = %v, want 10", stats["min_level"])
|
||||
}
|
||||
|
||||
if maxLevel, ok := stats["max_level"].(int8); !ok || maxLevel != 30 {
|
||||
t.Errorf("max_level = %v, want 30", stats["max_level"])
|
||||
}
|
||||
|
||||
if categoryCounts, ok := stats["collections_by_category"].(map[string]int); ok {
|
||||
if categoryCounts["Heritage"] != 2 {
|
||||
t.Errorf("Heritage collections = %v, want 2", categoryCounts["Heritage"])
|
||||
}
|
||||
if categoryCounts["Treasured"] != 1 {
|
||||
t.Errorf("Treasured collections = %v, want 1", categoryCounts["Treasured"])
|
||||
}
|
||||
} else {
|
||||
t.Error("collections_by_category not found in statistics")
|
||||
}
|
||||
|
||||
if avgItems, ok := stats["average_items_per_collection"].(float64); !ok || avgItems != float64(4)/3 {
|
||||
t.Errorf("average_items_per_collection = %v, want %v", avgItems, float64(4)/3)
|
||||
}
|
||||
}
|
281
internal/collections/player.go
Normal file
281
internal/collections/player.go
Normal file
@ -0,0 +1,281 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// PlayerList manages collections for a specific player
|
||||
type PlayerList struct {
|
||||
CharacterID int32
|
||||
collections map[int32]*Collection
|
||||
db *database.Database
|
||||
}
|
||||
|
||||
// NewPlayerList creates a new player collection list
|
||||
func NewPlayerList(characterID int32, db *database.Database) *PlayerList {
|
||||
return &PlayerList{
|
||||
CharacterID: characterID,
|
||||
collections: make(map[int32]*Collection),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// AddCollection adds a collection to the player's list
|
||||
func (pl *PlayerList) AddCollection(collection *Collection) bool {
|
||||
if collection == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, exists := pl.collections[collection.GetID()]; exists {
|
||||
return false // Already exists
|
||||
}
|
||||
|
||||
pl.collections[collection.GetID()] = collection
|
||||
return true
|
||||
}
|
||||
|
||||
// GetCollection retrieves a collection by ID
|
||||
func (pl *PlayerList) GetCollection(id int32) *Collection {
|
||||
return pl.collections[id]
|
||||
}
|
||||
|
||||
// RemoveCollection removes a collection from the player's list
|
||||
func (pl *PlayerList) RemoveCollection(id int32) bool {
|
||||
if _, exists := pl.collections[id]; exists {
|
||||
delete(pl.collections, id)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ClearCollections removes all collections from the player's list
|
||||
func (pl *PlayerList) ClearCollections() {
|
||||
pl.collections = make(map[int32]*Collection)
|
||||
}
|
||||
|
||||
// Size returns the number of collections the player has
|
||||
func (pl *PlayerList) Size() int {
|
||||
return len(pl.collections)
|
||||
}
|
||||
|
||||
// GetCollections returns all player collections
|
||||
func (pl *PlayerList) GetCollections() map[int32]*Collection {
|
||||
return pl.collections
|
||||
}
|
||||
|
||||
// NeedsItem checks if any of the player's collections need the specified item
|
||||
func (pl *PlayerList) NeedsItem(itemID int32) bool {
|
||||
for _, collection := range pl.collections {
|
||||
if collection.NeedsItem(itemID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasCollectionsToHandIn checks if the player has any collections ready to turn in
|
||||
func (pl *PlayerList) HasCollectionsToHandIn() bool {
|
||||
for _, collection := range pl.collections {
|
||||
if collection.GetIsReadyToTurnIn() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCollectionsToHandIn returns all collections ready to turn in
|
||||
func (pl *PlayerList) GetCollectionsToHandIn() []*Collection {
|
||||
var readyCollections []*Collection
|
||||
for _, collection := range pl.collections {
|
||||
if collection.GetIsReadyToTurnIn() {
|
||||
readyCollections = append(readyCollections, collection)
|
||||
}
|
||||
}
|
||||
return readyCollections
|
||||
}
|
||||
|
||||
// GetCompletedCollections returns all completed collections
|
||||
func (pl *PlayerList) GetCompletedCollections() []*Collection {
|
||||
var completedCollections []*Collection
|
||||
for _, collection := range pl.collections {
|
||||
if collection.Completed {
|
||||
completedCollections = append(completedCollections, collection)
|
||||
}
|
||||
}
|
||||
return completedCollections
|
||||
}
|
||||
|
||||
// GetActiveCollections returns all non-completed collections
|
||||
func (pl *PlayerList) GetActiveCollections() []*Collection {
|
||||
var activeCollections []*Collection
|
||||
for _, collection := range pl.collections {
|
||||
if !collection.Completed {
|
||||
activeCollections = append(activeCollections, collection)
|
||||
}
|
||||
}
|
||||
return activeCollections
|
||||
}
|
||||
|
||||
// LoadPlayerCollections loads all collections for the player from database
|
||||
func (pl *PlayerList) LoadPlayerCollections(masterList *MasterList) error {
|
||||
if pl.db == nil {
|
||||
return fmt.Errorf("no database connection available")
|
||||
}
|
||||
|
||||
// Clear existing collections
|
||||
pl.ClearCollections()
|
||||
|
||||
// Load player's collection progress
|
||||
query := `SELECT collection_id, completed FROM character_collections WHERE char_id = ?`
|
||||
rows, err := pl.db.Query(query, pl.CharacterID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load player collections: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var collectionID int32
|
||||
var completed bool
|
||||
|
||||
if err := rows.Scan(&collectionID, &completed); err != nil {
|
||||
return fmt.Errorf("failed to scan player collection: %w", err)
|
||||
}
|
||||
|
||||
// Get the master collection
|
||||
masterCollection := masterList.GetCollection(collectionID)
|
||||
if masterCollection == nil {
|
||||
continue // Skip if collection doesn't exist in master list
|
||||
}
|
||||
|
||||
// Create a copy for the player
|
||||
playerCollection := masterCollection.Clone()
|
||||
playerCollection.Completed = completed
|
||||
|
||||
// Load player's found items
|
||||
itemQuery := `SELECT collection_item_id FROM character_collection_items WHERE char_id = ? AND collection_id = ?`
|
||||
itemRows, err := pl.db.Query(itemQuery, pl.CharacterID, collectionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load player collection items: %w", err)
|
||||
}
|
||||
|
||||
for itemRows.Next() {
|
||||
var itemID int32
|
||||
if err := itemRows.Scan(&itemID); err != nil {
|
||||
itemRows.Close()
|
||||
return fmt.Errorf("failed to scan player collection item: %w", err)
|
||||
}
|
||||
|
||||
// Mark the item as found
|
||||
if collectionItem := playerCollection.GetCollectionItemByItemID(itemID); collectionItem != nil {
|
||||
collectionItem.Found = 1
|
||||
}
|
||||
}
|
||||
itemRows.Close()
|
||||
|
||||
pl.AddCollection(playerCollection)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SavePlayerCollection saves a player's collection progress
|
||||
func (pl *PlayerList) SavePlayerCollection(collectionID int32) error {
|
||||
if pl.db == nil {
|
||||
return fmt.Errorf("no database connection available")
|
||||
}
|
||||
|
||||
collection := pl.GetCollection(collectionID)
|
||||
if collection == nil {
|
||||
return fmt.Errorf("collection %d not found for player %d", collectionID, pl.CharacterID)
|
||||
}
|
||||
|
||||
// Update or insert player collection record
|
||||
_, err := pl.db.Exec(`
|
||||
INSERT INTO character_collections (char_id, collection_id, completed)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(char_id, collection_id)
|
||||
DO UPDATE SET completed = ?`,
|
||||
pl.CharacterID, collectionID, collection.Completed, collection.Completed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save player collection: %w", err)
|
||||
}
|
||||
|
||||
// Delete existing found items and re-insert
|
||||
_, err = pl.db.Exec(`DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?`,
|
||||
pl.CharacterID, collectionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete old collection items: %w", err)
|
||||
}
|
||||
|
||||
// Insert found items
|
||||
for _, item := range collection.CollectionItems {
|
||||
if item.Found != 0 {
|
||||
_, err = pl.db.Exec(`
|
||||
INSERT INTO character_collection_items (char_id, collection_id, collection_item_id)
|
||||
VALUES (?, ?, ?)`,
|
||||
pl.CharacterID, collectionID, item.ItemID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save collection item: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collection.SaveNeeded = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAllCollections saves all player collections that need saving
|
||||
func (pl *PlayerList) SaveAllCollections() error {
|
||||
for collectionID, collection := range pl.collections {
|
||||
if collection.SaveNeeded {
|
||||
if err := pl.SavePlayerCollection(collectionID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStatistics returns statistics about the player's collections
|
||||
func (pl *PlayerList) GetStatistics() map[string]any {
|
||||
stats := make(map[string]any)
|
||||
stats["total_collections"] = len(pl.collections)
|
||||
|
||||
completed := 0
|
||||
readyToTurnIn := 0
|
||||
totalItemsFound := 0
|
||||
totalItemsNeeded := 0
|
||||
|
||||
for _, collection := range pl.collections {
|
||||
if collection.Completed {
|
||||
completed++
|
||||
}
|
||||
if collection.GetIsReadyToTurnIn() {
|
||||
readyToTurnIn++
|
||||
}
|
||||
|
||||
for _, item := range collection.CollectionItems {
|
||||
if item.Found != 0 {
|
||||
totalItemsFound++
|
||||
} else {
|
||||
totalItemsNeeded++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stats["completed_collections"] = completed
|
||||
stats["ready_to_turn_in"] = readyToTurnIn
|
||||
stats["active_collections"] = len(pl.collections) - completed
|
||||
stats["total_items_found"] = totalItemsFound
|
||||
stats["total_items_needed"] = totalItemsNeeded
|
||||
|
||||
if totalItemsFound+totalItemsNeeded > 0 {
|
||||
stats["overall_progress"] = float64(totalItemsFound) / float64(totalItemsFound+totalItemsNeeded) * 100.0
|
||||
} else {
|
||||
stats["overall_progress"] = 0.0
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
@ -1,397 +0,0 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// NewPlayerCollectionList creates a new player collection list
|
||||
func NewPlayerCollectionList(characterID int32, database CollectionDatabase) *PlayerCollectionList {
|
||||
return &PlayerCollectionList{
|
||||
characterID: characterID,
|
||||
collections: make(map[int32]*Collection),
|
||||
database: database,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize loads player's collection progress from database
|
||||
func (pcl *PlayerCollectionList) Initialize(ctx context.Context, masterList *MasterCollectionList) error {
|
||||
pcl.mu.Lock()
|
||||
defer pcl.mu.Unlock()
|
||||
|
||||
// Load player collection data
|
||||
playerCollections, err := pcl.database.LoadPlayerCollections(ctx, pcl.characterID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load player collections: %w", err)
|
||||
}
|
||||
|
||||
for _, playerCollection := range playerCollections {
|
||||
// Get the master collection template
|
||||
masterCollection := masterList.GetCollection(playerCollection.CollectionID)
|
||||
if masterCollection == nil {
|
||||
continue // Skip collections that no longer exist
|
||||
}
|
||||
|
||||
// Create a copy for the player
|
||||
collection := NewCollectionFromData(masterCollection)
|
||||
if collection == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
collection.SetCompleted(playerCollection.Completed)
|
||||
|
||||
// Load player's found items
|
||||
foundItems, err := pcl.database.LoadPlayerCollectionItems(ctx, pcl.characterID, playerCollection.CollectionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load player collection items for collection %d: %w", playerCollection.CollectionID, err)
|
||||
}
|
||||
|
||||
// Mark found items
|
||||
for _, itemID := range foundItems {
|
||||
collection.MarkItemFound(itemID)
|
||||
}
|
||||
|
||||
// Reset save needed flag after loading
|
||||
collection.SetSaveNeeded(false)
|
||||
|
||||
pcl.collections[playerCollection.CollectionID] = collection
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddCollection adds a collection to the player's list
|
||||
func (pcl *PlayerCollectionList) AddCollection(collection *Collection) bool {
|
||||
pcl.mu.Lock()
|
||||
defer pcl.mu.Unlock()
|
||||
|
||||
if collection == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
id := collection.GetID()
|
||||
if _, exists := pcl.collections[id]; exists {
|
||||
return false
|
||||
}
|
||||
|
||||
pcl.collections[id] = collection
|
||||
return true
|
||||
}
|
||||
|
||||
// GetCollection retrieves a collection by ID
|
||||
func (pcl *PlayerCollectionList) GetCollection(collectionID int32) *Collection {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
return pcl.collections[collectionID]
|
||||
}
|
||||
|
||||
// ClearCollections removes all collections
|
||||
func (pcl *PlayerCollectionList) ClearCollections() {
|
||||
pcl.mu.Lock()
|
||||
defer pcl.mu.Unlock()
|
||||
pcl.collections = make(map[int32]*Collection)
|
||||
}
|
||||
|
||||
// Size returns the number of collections
|
||||
func (pcl *PlayerCollectionList) Size() int {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
return len(pcl.collections)
|
||||
}
|
||||
|
||||
// NeedsItem checks if any player collection or potential collection needs an item
|
||||
func (pcl *PlayerCollectionList) NeedsItem(itemID int32, masterList *MasterCollectionList) bool {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
// Check player's active collections first
|
||||
for _, collection := range pcl.collections {
|
||||
if collection.NeedsItem(itemID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any master collection the player doesn't have needs this item
|
||||
if masterList != nil {
|
||||
for _, masterCollection := range masterList.GetAllCollections() {
|
||||
if masterCollection.NeedsItem(itemID) {
|
||||
// Player doesn't have this collection yet
|
||||
if _, hasCollection := pcl.collections[masterCollection.GetID()]; !hasCollection {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HasCollectionsToHandIn checks if any collections are ready to turn in
|
||||
func (pcl *PlayerCollectionList) HasCollectionsToHandIn() bool {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
for _, collection := range pcl.collections {
|
||||
if collection.GetIsReadyToTurnIn() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCollectionsReadyToTurnIn returns collections that are ready to complete
|
||||
func (pcl *PlayerCollectionList) GetCollectionsReadyToTurnIn() []*Collection {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
var result []*Collection
|
||||
for _, collection := range pcl.collections {
|
||||
if collection.GetIsReadyToTurnIn() {
|
||||
result = append(result, collection)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetCompletedCollections returns all completed collections
|
||||
func (pcl *PlayerCollectionList) GetCompletedCollections() []*Collection {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
var result []*Collection
|
||||
for _, collection := range pcl.collections {
|
||||
if collection.GetCompleted() {
|
||||
result = append(result, collection)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetActiveCollections returns all active (incomplete) collections
|
||||
func (pcl *PlayerCollectionList) GetActiveCollections() []*Collection {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
var result []*Collection
|
||||
for _, collection := range pcl.collections {
|
||||
if !collection.GetCompleted() {
|
||||
result = append(result, collection)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAllCollections returns all player collections
|
||||
func (pcl *PlayerCollectionList) GetAllCollections() []*Collection {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
result := make([]*Collection, 0, len(pcl.collections))
|
||||
for _, collection := range pcl.collections {
|
||||
result = append(result, collection)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetCollectionsByCategory returns collections in a specific category
|
||||
func (pcl *PlayerCollectionList) GetCollectionsByCategory(category string) []*Collection {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
var result []*Collection
|
||||
for _, collection := range pcl.collections {
|
||||
if collection.GetCategory() == category {
|
||||
result = append(result, collection)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ProcessItemFound processes when a player finds an item that may belong to collections
|
||||
func (pcl *PlayerCollectionList) ProcessItemFound(itemID int32, masterList *MasterCollectionList) ([]*Collection, error) {
|
||||
pcl.mu.Lock()
|
||||
defer pcl.mu.Unlock()
|
||||
|
||||
var updatedCollections []*Collection
|
||||
|
||||
// Check existing player collections
|
||||
for _, collection := range pcl.collections {
|
||||
if collection.NeedsItem(itemID) {
|
||||
if collection.MarkItemFound(itemID) {
|
||||
updatedCollections = append(updatedCollections, collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if player should start new collections
|
||||
if masterList != nil {
|
||||
for _, masterCollection := range masterList.GetAllCollections() {
|
||||
// Skip if player already has this collection
|
||||
if _, hasCollection := pcl.collections[masterCollection.GetID()]; hasCollection {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if master collection needs this item
|
||||
if masterCollection.NeedsItem(itemID) {
|
||||
// Create new collection for player
|
||||
newCollection := NewCollectionFromData(masterCollection)
|
||||
if newCollection != nil {
|
||||
newCollection.MarkItemFound(itemID)
|
||||
pcl.collections[masterCollection.GetID()] = newCollection
|
||||
updatedCollections = append(updatedCollections, newCollection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCollections, nil
|
||||
}
|
||||
|
||||
// CompleteCollection marks a collection as completed
|
||||
func (pcl *PlayerCollectionList) CompleteCollection(collectionID int32) error {
|
||||
pcl.mu.Lock()
|
||||
defer pcl.mu.Unlock()
|
||||
|
||||
collection, exists := pcl.collections[collectionID]
|
||||
if !exists {
|
||||
return fmt.Errorf("collection %d not found", collectionID)
|
||||
}
|
||||
|
||||
if collection.GetCompleted() {
|
||||
return fmt.Errorf("collection %d is already completed", collectionID)
|
||||
}
|
||||
|
||||
if !collection.GetIsReadyToTurnIn() {
|
||||
return fmt.Errorf("collection %d is not ready to complete", collectionID)
|
||||
}
|
||||
|
||||
collection.SetCompleted(true)
|
||||
collection.SetSaveNeeded(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCollectionsNeedingSave returns collections that need to be saved
|
||||
func (pcl *PlayerCollectionList) GetCollectionsNeedingSave() []*Collection {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
var result []*Collection
|
||||
for _, collection := range pcl.collections {
|
||||
if collection.GetSaveNeeded() {
|
||||
result = append(result, collection)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SaveCollections saves all collections that need saving
|
||||
func (pcl *PlayerCollectionList) SaveCollections(ctx context.Context) error {
|
||||
collectionsToSave := pcl.GetCollectionsNeedingSave()
|
||||
if len(collectionsToSave) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := pcl.database.SavePlayerCollections(ctx, pcl.characterID, collectionsToSave); err != nil {
|
||||
return fmt.Errorf("failed to save player collections: %w", err)
|
||||
}
|
||||
|
||||
// Mark collections as saved
|
||||
for _, collection := range collectionsToSave {
|
||||
collection.SetSaveNeeded(false)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStatistics returns player collection statistics
|
||||
func (pcl *PlayerCollectionList) GetStatistics() CollectionStatistics {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
stats := CollectionStatistics{
|
||||
TotalCollections: len(pcl.collections),
|
||||
PlayersWithCollections: 1, // This player
|
||||
}
|
||||
|
||||
for _, collection := range pcl.collections {
|
||||
if collection.GetCompleted() {
|
||||
stats.CompletedCollections++
|
||||
}
|
||||
if !collection.GetCompleted() && collection.GetFoundItemsCount() > 0 {
|
||||
stats.ActiveCollections++
|
||||
}
|
||||
stats.TotalItems += collection.GetTotalItemsCount()
|
||||
stats.FoundItems += collection.GetFoundItemsCount()
|
||||
stats.TotalRewards += len(collection.GetRewardItems()) + len(collection.GetSelectableRewardItems())
|
||||
if collection.GetRewardCoin() > 0 {
|
||||
stats.TotalRewards++
|
||||
}
|
||||
if collection.GetRewardXP() > 0 {
|
||||
stats.TotalRewards++
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// GetCollectionProgress returns detailed progress for all collections
|
||||
func (pcl *PlayerCollectionList) GetCollectionProgress() []CollectionProgress {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
progress := make([]CollectionProgress, 0, len(pcl.collections))
|
||||
for _, collection := range pcl.collections {
|
||||
progress = append(progress, collection.GetCollectionProgress())
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
sort.Slice(progress, func(i, j int) bool {
|
||||
return progress[i].Name < progress[j].Name
|
||||
})
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
// GetCharacterID returns the character ID for this collection list
|
||||
func (pcl *PlayerCollectionList) GetCharacterID() int32 {
|
||||
return pcl.characterID
|
||||
}
|
||||
|
||||
// RemoveCollection removes a collection from the player's list
|
||||
func (pcl *PlayerCollectionList) RemoveCollection(collectionID int32) bool {
|
||||
pcl.mu.Lock()
|
||||
defer pcl.mu.Unlock()
|
||||
|
||||
if _, exists := pcl.collections[collectionID]; exists {
|
||||
delete(pcl.collections, collectionID)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCollectionIDs returns all collection IDs the player has
|
||||
func (pcl *PlayerCollectionList) GetCollectionIDs() []int32 {
|
||||
pcl.mu.RLock()
|
||||
defer pcl.mu.RUnlock()
|
||||
|
||||
ids := make([]int32, 0, len(pcl.collections))
|
||||
for id := range pcl.collections {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
sort.Slice(ids, func(i, j int) bool {
|
||||
return ids[i] < ids[j]
|
||||
})
|
||||
|
||||
return ids
|
||||
}
|
@ -1,104 +1,10 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CollectionItem represents an item required for a collection
|
||||
type CollectionItem struct {
|
||||
ItemID int32 `json:"item_id" db:"item_id"`
|
||||
Index int8 `json:"index" db:"item_index"`
|
||||
Found int8 `json:"found" db:"found"`
|
||||
}
|
||||
|
||||
// CollectionRewardItem represents a reward item for completing a collection
|
||||
type CollectionRewardItem struct {
|
||||
ItemID int32 `json:"item_id" db:"item_id"`
|
||||
Quantity int8 `json:"quantity" db:"quantity"`
|
||||
}
|
||||
|
||||
// Collection represents a collection that players can complete
|
||||
type Collection struct {
|
||||
mu sync.RWMutex
|
||||
id int32
|
||||
name string
|
||||
category string
|
||||
level int8
|
||||
rewardCoin int64
|
||||
rewardXP int64
|
||||
completed bool
|
||||
saveNeeded bool
|
||||
collectionItems []CollectionItem
|
||||
rewardItems []CollectionRewardItem
|
||||
selectableRewardItems []CollectionRewardItem
|
||||
lastModified time.Time
|
||||
}
|
||||
|
||||
// CollectionData represents collection data for database operations
|
||||
type CollectionData struct {
|
||||
ID int32 `json:"id" db:"id"`
|
||||
Name string `json:"collection_name" db:"collection_name"`
|
||||
Category string `json:"collection_category" db:"collection_category"`
|
||||
Level int8 `json:"level" db:"level"`
|
||||
}
|
||||
|
||||
// CollectionRewardData represents reward data from database
|
||||
type CollectionRewardData struct {
|
||||
CollectionID int32 `json:"collection_id" db:"collection_id"`
|
||||
RewardType string `json:"reward_type" db:"reward_type"`
|
||||
RewardValue string `json:"reward_value" db:"reward_value"`
|
||||
Quantity int8 `json:"reward_quantity" db:"reward_quantity"`
|
||||
}
|
||||
|
||||
// PlayerCollectionData represents player collection progress
|
||||
type PlayerCollectionData struct {
|
||||
CharacterID int32 `json:"char_id" db:"char_id"`
|
||||
CollectionID int32 `json:"collection_id" db:"collection_id"`
|
||||
Completed bool `json:"completed" db:"completed"`
|
||||
}
|
||||
|
||||
// PlayerCollectionItemData represents player found collection items
|
||||
type PlayerCollectionItemData struct {
|
||||
CharacterID int32 `json:"char_id" db:"char_id"`
|
||||
CollectionID int32 `json:"collection_id" db:"collection_id"`
|
||||
CollectionItemID int32 `json:"collection_item_id" db:"collection_item_id"`
|
||||
}
|
||||
|
||||
// MasterCollectionList manages all available collections in the game
|
||||
type MasterCollectionList struct {
|
||||
mu sync.RWMutex
|
||||
collections map[int32]*Collection
|
||||
database CollectionDatabase
|
||||
}
|
||||
|
||||
// PlayerCollectionList manages collections for a specific player
|
||||
type PlayerCollectionList struct {
|
||||
mu sync.RWMutex
|
||||
characterID int32
|
||||
collections map[int32]*Collection
|
||||
database CollectionDatabase
|
||||
}
|
||||
|
||||
// CollectionManager provides high-level collection management
|
||||
type CollectionManager struct {
|
||||
masterList *MasterCollectionList
|
||||
database CollectionDatabase
|
||||
itemLookup ItemLookup
|
||||
}
|
||||
|
||||
// CollectionStatistics provides collection system usage statistics
|
||||
type CollectionStatistics struct {
|
||||
TotalCollections int
|
||||
CompletedCollections int
|
||||
ActiveCollections int
|
||||
TotalItems int
|
||||
FoundItems int
|
||||
TotalRewards int
|
||||
PlayersWithCollections int
|
||||
}
|
||||
|
||||
// CollectionInfo provides basic collection information
|
||||
// CollectionInfo provides basic collection information for client display
|
||||
type CollectionInfo struct {
|
||||
ID int32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
Loading…
x
Reference in New Issue
Block a user