package heroic_ops import ( "context" "fmt" "sort" ) // NewMasterHeroicOPList creates a new master heroic opportunity list func NewMasterHeroicOPList() *MasterHeroicOPList { return &MasterHeroicOPList{ starters: make(map[int8]map[int32]*HeroicOPStarter), wheels: make(map[int32][]*HeroicOPWheel), spells: make(map[int32]SpellInfo), loaded: false, } } // LoadFromDatabase loads all heroic opportunities from the database func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database HeroicOPDatabase) error { mhol.mu.Lock() defer mhol.mu.Unlock() // Clear existing data mhol.starters = make(map[int8]map[int32]*HeroicOPStarter) mhol.wheels = make(map[int32][]*HeroicOPWheel) mhol.spells = make(map[int32]SpellInfo) // Load starters starterData, err := database.LoadStarters(ctx) if err != nil { return fmt.Errorf("failed to load starters: %w", err) } for _, data := range starterData { starter := &HeroicOPStarter{ ID: data.ID, StartClass: data.StarterClass, StarterIcon: data.StarterIcon, Name: data.Name, Description: data.Description, Abilities: [6]int16{ data.Ability1, data.Ability2, data.Ability3, data.Ability4, data.Ability5, data.Ability6, }, SaveNeeded: false, } // Validate starter if err := starter.Validate(); err != nil { continue // Skip invalid starters } // Add to map structure if mhol.starters[starter.StartClass] == nil { mhol.starters[starter.StartClass] = make(map[int32]*HeroicOPStarter) } mhol.starters[starter.StartClass][starter.ID] = starter } // Load wheels wheelData, err := database.LoadWheels(ctx) if err != nil { return fmt.Errorf("failed to load wheels: %w", err) } for _, data := range wheelData { wheel := &HeroicOPWheel{ ID: data.ID, StarterLinkID: data.StarterLinkID, Order: data.ChainOrder, ShiftIcon: data.ShiftIcon, Chance: data.Chance, SpellID: data.SpellID, Name: data.Name, Description: data.Description, Abilities: [6]int16{ data.Ability1, data.Ability2, data.Ability3, data.Ability4, data.Ability5, data.Ability6, }, SaveNeeded: false, } // Validate wheel if err := wheel.Validate(); err != nil { continue // Skip invalid wheels } // Add to wheels map mhol.wheels[wheel.StarterLinkID] = append(mhol.wheels[wheel.StarterLinkID], wheel) // Store spell info mhol.spells[wheel.SpellID] = SpellInfo{ ID: wheel.SpellID, Name: wheel.Name, Description: wheel.Description, } } mhol.loaded = true return nil } // GetStartersForClass returns all starters that the specified class can initiate func (mhol *MasterHeroicOPList) GetStartersForClass(playerClass int8) []*HeroicOPStarter { mhol.mu.RLock() defer mhol.mu.RUnlock() var starters []*HeroicOPStarter // Add class-specific starters if classStarters, exists := mhol.starters[playerClass]; exists { for _, starter := range classStarters { starters = append(starters, starter) } } // Add universal starters (class 0 = any) if universalStarters, exists := mhol.starters[ClassAny]; exists { for _, starter := range universalStarters { starters = append(starters, starter) } } return starters } // GetStarter returns a specific starter by ID func (mhol *MasterHeroicOPList) GetStarter(starterID int32) *HeroicOPStarter { mhol.mu.RLock() defer mhol.mu.RUnlock() // Search through all classes for _, classStarters := range mhol.starters { if starter, exists := classStarters[starterID]; exists { return starter } } return nil } // GetWheelsForStarter returns all wheels associated with a starter func (mhol *MasterHeroicOPList) GetWheelsForStarter(starterID int32) []*HeroicOPWheel { mhol.mu.RLock() defer mhol.mu.RUnlock() if wheels, exists := mhol.wheels[starterID]; exists { // Return a copy to prevent external modification result := make([]*HeroicOPWheel, len(wheels)) copy(result, wheels) return result } return nil } // GetWheel returns a specific wheel by ID func (mhol *MasterHeroicOPList) GetWheel(wheelID int32) *HeroicOPWheel { mhol.mu.RLock() defer mhol.mu.RUnlock() // Search through all wheel lists for _, wheelList := range mhol.wheels { for _, wheel := range wheelList { if wheel.ID == wheelID { return wheel } } } return nil } // SelectRandomWheel randomly selects a wheel from the starter's available wheels func (mhol *MasterHeroicOPList) SelectRandomWheel(starterID int32) *HeroicOPWheel { wheels := mhol.GetWheelsForStarter(starterID) if len(wheels) == 0 { return nil } return SelectRandomWheel(wheels) } // GetSpellInfo returns spell information for a given spell ID func (mhol *MasterHeroicOPList) GetSpellInfo(spellID int32) (*SpellInfo, bool) { mhol.mu.RLock() defer mhol.mu.RUnlock() if spell, exists := mhol.spells[spellID]; exists { return &spell, true } return nil, false } // AddStarter adds a new starter to the master list func (mhol *MasterHeroicOPList) AddStarter(starter *HeroicOPStarter) error { mhol.mu.Lock() defer mhol.mu.Unlock() if err := starter.Validate(); err != nil { return fmt.Errorf("invalid starter: %w", err) } // Check for duplicate ID if existingStarter := mhol.getStarterNoLock(starter.ID); existingStarter != nil { return fmt.Errorf("starter ID %d already exists", starter.ID) } // Add to map structure if mhol.starters[starter.StartClass] == nil { mhol.starters[starter.StartClass] = make(map[int32]*HeroicOPStarter) } mhol.starters[starter.StartClass][starter.ID] = starter return nil } // AddWheel adds a new wheel to the master list func (mhol *MasterHeroicOPList) AddWheel(wheel *HeroicOPWheel) error { mhol.mu.Lock() defer mhol.mu.Unlock() if err := wheel.Validate(); err != nil { return fmt.Errorf("invalid wheel: %w", err) } // Check for duplicate ID if existingWheel := mhol.getWheelNoLock(wheel.ID); existingWheel != nil { return fmt.Errorf("wheel ID %d already exists", wheel.ID) } // Verify starter exists if mhol.getStarterNoLock(wheel.StarterLinkID) == nil { return fmt.Errorf("starter ID %d not found for wheel", wheel.StarterLinkID) } // Add to wheels map mhol.wheels[wheel.StarterLinkID] = append(mhol.wheels[wheel.StarterLinkID], wheel) // Store spell info mhol.spells[wheel.SpellID] = SpellInfo{ ID: wheel.SpellID, Name: wheel.Name, Description: wheel.Description, } return nil } // RemoveStarter removes a starter and all its associated wheels func (mhol *MasterHeroicOPList) RemoveStarter(starterID int32) bool { mhol.mu.Lock() defer mhol.mu.Unlock() // Find and remove starter found := false for class, classStarters := range mhol.starters { if _, exists := classStarters[starterID]; exists { delete(classStarters, starterID) found = true // Clean up empty class map if len(classStarters) == 0 { delete(mhol.starters, class) } break } } if !found { return false } // Remove associated wheels delete(mhol.wheels, starterID) return true } // RemoveWheel removes a specific wheel func (mhol *MasterHeroicOPList) RemoveWheel(wheelID int32) bool { mhol.mu.Lock() defer mhol.mu.Unlock() // Find and remove wheel for starterID, wheelList := range mhol.wheels { for i, wheel := range wheelList { if wheel.ID == wheelID { // Remove wheel from slice mhol.wheels[starterID] = append(wheelList[:i], wheelList[i+1:]...) // Clean up empty wheel list if len(mhol.wheels[starterID]) == 0 { delete(mhol.wheels, starterID) } return true } } } return false } // GetAllStarters returns all starters in the system func (mhol *MasterHeroicOPList) GetAllStarters() []*HeroicOPStarter { mhol.mu.RLock() defer mhol.mu.RUnlock() var allStarters []*HeroicOPStarter for _, classStarters := range mhol.starters { for _, starter := range classStarters { allStarters = append(allStarters, starter) } } // Sort by ID for consistent ordering sort.Slice(allStarters, func(i, j int) bool { return allStarters[i].ID < allStarters[j].ID }) return allStarters } // GetAllWheels returns all wheels in the system func (mhol *MasterHeroicOPList) GetAllWheels() []*HeroicOPWheel { mhol.mu.RLock() defer mhol.mu.RUnlock() var allWheels []*HeroicOPWheel for _, wheelList := range mhol.wheels { allWheels = append(allWheels, wheelList...) } // Sort by ID for consistent ordering sort.Slice(allWheels, func(i, j int) bool { return allWheels[i].ID < allWheels[j].ID }) return allWheels } // GetStarterCount returns the total number of starters func (mhol *MasterHeroicOPList) GetStarterCount() int { mhol.mu.RLock() defer mhol.mu.RUnlock() count := 0 for _, classStarters := range mhol.starters { count += len(classStarters) } return count } // GetWheelCount returns the total number of wheels func (mhol *MasterHeroicOPList) GetWheelCount() int { mhol.mu.RLock() defer mhol.mu.RUnlock() count := 0 for _, wheelList := range mhol.wheels { count += len(wheelList) } return count } // IsLoaded returns whether data has been loaded func (mhol *MasterHeroicOPList) IsLoaded() bool { mhol.mu.RLock() defer mhol.mu.RUnlock() return mhol.loaded } // SearchStarters searches for starters matching the given criteria func (mhol *MasterHeroicOPList) SearchStarters(criteria HeroicOPSearchCriteria) []*HeroicOPStarter { mhol.mu.RLock() defer mhol.mu.RUnlock() var results []*HeroicOPStarter for _, classStarters := range mhol.starters { for _, starter := range classStarters { if mhol.matchesStarterCriteria(starter, criteria) { results = append(results, starter) } } } // Sort results by ID sort.Slice(results, func(i, j int) bool { return results[i].ID < results[j].ID }) return results } // SearchWheels searches for wheels matching the given criteria func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) []*HeroicOPWheel { mhol.mu.RLock() defer mhol.mu.RUnlock() var results []*HeroicOPWheel for _, wheelList := range mhol.wheels { for _, wheel := range wheelList { if mhol.matchesWheelCriteria(wheel, criteria) { results = append(results, wheel) } } } // Sort results by ID sort.Slice(results, func(i, j int) bool { return results[i].ID < results[j].ID }) return results } // GetStatistics returns usage statistics for the HO system func (mhol *MasterHeroicOPList) GetStatistics() map[string]interface{} { mhol.mu.RLock() defer mhol.mu.RUnlock() stats := make(map[string]interface{}) // Basic counts stats["total_starters"] = mhol.getStarterCountNoLock() stats["total_wheels"] = mhol.getWheelCountNoLock() stats["total_spells"] = len(mhol.spells) stats["loaded"] = mhol.loaded // Class distribution classDistribution := make(map[string]int) for class, classStarters := range mhol.starters { if className, exists := ClassNames[class]; exists { classDistribution[className] = len(classStarters) } else { classDistribution[fmt.Sprintf("Class_%d", class)] = len(classStarters) } } stats["class_distribution"] = classDistribution // Wheel distribution per starter wheelDistribution := make(map[string]int) for starterID, wheelList := range mhol.wheels { wheelDistribution[fmt.Sprintf("starter_%d", starterID)] = len(wheelList) } stats["wheel_distribution"] = wheelDistribution return stats } // Validate checks the integrity of the master list func (mhol *MasterHeroicOPList) Validate() []error { mhol.mu.RLock() defer mhol.mu.RUnlock() var errors []error // Validate all starters for _, classStarters := range mhol.starters { for _, starter := range classStarters { if err := starter.Validate(); err != nil { errors = append(errors, fmt.Errorf("starter %d: %w", starter.ID, err)) } } } // Validate all wheels for _, wheelList := range mhol.wheels { for _, wheel := range wheelList { if err := wheel.Validate(); err != nil { errors = append(errors, fmt.Errorf("wheel %d: %w", wheel.ID, err)) } // Check if starter exists for this wheel if mhol.getStarterNoLock(wheel.StarterLinkID) == nil { errors = append(errors, fmt.Errorf("wheel %d references non-existent starter %d", wheel.ID, wheel.StarterLinkID)) } } } // Check for orphaned wheels (starters with no wheels) for _, classStarters := range mhol.starters { for starterID := range classStarters { if _, hasWheels := mhol.wheels[starterID]; !hasWheels { errors = append(errors, fmt.Errorf("starter %d has no associated wheels", starterID)) } } } return errors } // Internal helper methods (no lock versions) func (mhol *MasterHeroicOPList) getStarterNoLock(starterID int32) *HeroicOPStarter { for _, classStarters := range mhol.starters { if starter, exists := classStarters[starterID]; exists { return starter } } return nil } func (mhol *MasterHeroicOPList) getWheelNoLock(wheelID int32) *HeroicOPWheel { for _, wheelList := range mhol.wheels { for _, wheel := range wheelList { if wheel.ID == wheelID { return wheel } } } return nil } func (mhol *MasterHeroicOPList) getStarterCountNoLock() int { count := 0 for _, classStarters := range mhol.starters { count += len(classStarters) } return count } func (mhol *MasterHeroicOPList) getWheelCountNoLock() int { count := 0 for _, wheelList := range mhol.wheels { count += len(wheelList) } return count } func (mhol *MasterHeroicOPList) matchesStarterCriteria(starter *HeroicOPStarter, criteria HeroicOPSearchCriteria) bool { // Class filter if criteria.StarterClass != 0 && starter.StartClass != criteria.StarterClass { return false } // Name pattern filter if criteria.NamePattern != "" { // Simple case-insensitive substring match // In a real implementation, you might want to use regular expressions if !containsIgnoreCase(starter.Name, criteria.NamePattern) { return false } } return true } func (mhol *MasterHeroicOPList) matchesWheelCriteria(wheel *HeroicOPWheel, criteria HeroicOPSearchCriteria) bool { // Spell ID filter if criteria.SpellID != 0 && wheel.SpellID != criteria.SpellID { return false } // Chance range filter if criteria.MinChance > 0 && wheel.Chance < criteria.MinChance { return false } if criteria.MaxChance > 0 && wheel.Chance > criteria.MaxChance { return false } // Required players filter if criteria.RequiredPlayers > 0 && wheel.RequiredPlayers != criteria.RequiredPlayers { return false } // Name pattern filter if criteria.NamePattern != "" { if !containsIgnoreCase(wheel.Name, criteria.NamePattern) { return false } } // Shift availability filter if criteria.HasShift && !wheel.HasShift() { return false } // Order type filter if criteria.IsOrdered && !wheel.IsOrdered() { return false } return true } // Simple case-insensitive substring search func containsIgnoreCase(s, substr string) bool { // Convert both strings to lowercase for comparison // In a real implementation, you might want to use strings.ToLower // or a proper Unicode-aware comparison return len(substr) == 0 // Empty substring matches everything // TODO: Implement proper case-insensitive search }