eq2go/internal/heroic_ops/master_list.go

608 lines
15 KiB
Go

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]any {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
stats := make(map[string]any)
// 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
}