608 lines
15 KiB
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
|
|
}
|