package heroic_ops import ( "context" "fmt" "math/rand" "sort" "sync" "time" "eq2emu/internal/database" "eq2emu/internal/packets" ) // Simplified Heroic Opportunities System // Consolidates all functionality from 10 files into unified architecture // Preserves 100% C++ functionality while eliminating Active Record patterns // Core Data Structures // HeroicOPStarter represents a starter chain for heroic opportunities type HeroicOPStarter struct { mu sync.RWMutex ID int32 `json:"id"` // Unique identifier for this starter StartClass int8 `json:"start_class"` // Class that can initiate this starter (0 = any) StarterIcon int16 `json:"starter_icon"` // Icon displayed for the starter Abilities [6]int16 `json:"abilities"` // Array of ability icons in sequence Name string `json:"name"` // Display name for this starter Description string `json:"description"` // Description text SaveNeeded bool `json:"-"` // Flag indicating if database save is needed // Database integration fields isNew bool `json:"-"` // True if this is a new record not yet saved } // HeroicOPWheel represents the wheel phase of a heroic opportunity type HeroicOPWheel struct { mu sync.RWMutex ID int32 `json:"id"` // Unique identifier for this wheel StarterLinkID int32 `json:"starter_link_id"` // ID of the starter this wheel belongs to Order int8 `json:"order"` // 0 = unordered, 1+ = ordered ShiftIcon int16 `json:"shift_icon"` // Icon that can shift/change the wheel Chance float32 `json:"chance"` // Probability factor for selecting this wheel Abilities [6]int16 `json:"abilities"` // Array of ability icons for the wheel SpellID int32 `json:"spell_id"` // Spell cast when HO completes successfully Name string `json:"name"` // Display name for this wheel Description string `json:"description"` // Description text RequiredPlayers int8 `json:"required_players"` // Minimum players required SaveNeeded bool `json:"-"` // Flag indicating if database save is needed // Database integration fields isNew bool `json:"-"` // True if this is a new record not yet saved } // HeroicOP represents an active heroic opportunity instance type HeroicOP struct { mu sync.RWMutex ID int64 `json:"id"` // Unique instance ID EncounterID int32 `json:"encounter_id"` // Encounter this HO belongs to StarterID int32 `json:"starter_id"` // ID of the completed starter WheelID int32 `json:"wheel_id"` // ID of the active wheel State int8 `json:"state"` // Current HO state StartTime time.Time `json:"start_time"` // When the HO started WheelStartTime time.Time `json:"wheel_start_time"` // When wheel phase started TimeRemaining int32 `json:"time_remaining"` // Milliseconds remaining TotalTime int32 `json:"total_time"` // Total time allocated (ms) Complete int8 `json:"complete"` // Completion status (0/1) Countered [6]int8 `json:"countered"` // Which wheel abilities are completed ShiftUsed int8 `json:"shift_used"` // Whether shift has been used StarterProgress int8 `json:"starter_progress"` // Current position in starter chain Participants map[int32]bool `json:"participants"` // Character IDs that participated CurrentStarters []int32 `json:"current_starters"` // Active starter IDs during chain phase CompletedBy int32 `json:"completed_by"` // Character ID that completed the HO SpellName string `json:"spell_name"` // Name of completion spell SpellDescription string `json:"spell_description"` // Description of completion spell SaveNeeded bool `json:"-"` // Flag indicating if database save is needed // Database integration fields isNew bool `json:"-"` // True if this is a new record not yet saved } // HeroicOPManager manages the entire heroic opportunity system type HeroicOPManager struct { mu sync.RWMutex database *database.Database logger Logger // Core data storage - O(1) access by ID starters map[int32]*HeroicOPStarter // starter_id -> starter wheels map[int32]*HeroicOPWheel // wheel_id -> wheel activeHOs map[int64]*HeroicOP // instance_id -> HO encounterHOs map[int32][]*HeroicOP // encounter_id -> HOs // Specialized indices for O(1) lookups byClass map[int8]map[int32]*HeroicOPStarter // class -> starter_id -> starter byStarterID map[int32][]*HeroicOPWheel // starter_id -> wheels bySpellID map[int32][]*HeroicOPWheel // spell_id -> wheels byChance map[string][]*HeroicOPWheel // chance_range -> wheels orderedWheels map[int32]*HeroicOPWheel // wheel_id -> ordered wheels only shiftWheels map[int32]*HeroicOPWheel // wheel_id -> wheels with shifts spellInfo map[int32]SpellInfo // spell_id -> spell info // System state nextInstanceID int64 loaded bool // Configuration defaultWheelTimer int32 // milliseconds maxConcurrentHOs int enableLogging bool enableStatistics bool // Statistics tracking stats HeroicOPManagerStats } // Supporting structures type SpellInfo struct { ID int32 `json:"id"` Name string `json:"name"` Description string `json:"description"` Icon int16 `json:"icon"` } type HeroicOPManagerStats struct { TotalStarters int64 `json:"total_starters"` TotalWheels int64 `json:"total_wheels"` ClassDistribution map[int32]int64 `json:"class_distribution"` OrderedWheelsCount int64 `json:"ordered_wheels_count"` ShiftWheelsCount int64 `json:"shift_wheels_count"` SpellCount int64 `json:"spell_count"` AverageChance float64 `json:"average_chance"` ActiveHOCount int64 `json:"active_ho_count"` } type PacketData struct { SpellName string `json:"spell_name"` SpellDescription string `json:"spell_description"` TimeRemaining int32 `json:"time_remaining"` // milliseconds TotalTime int32 `json:"total_time"` // milliseconds Abilities [6]int16 `json:"abilities"` // Current wheel abilities Countered [6]int8 `json:"countered"` // Completion status Complete int8 `json:"complete"` // Overall completion (0/1) State int8 `json:"state"` // Current HO state CanShift bool `json:"can_shift"` // Whether shift is available ShiftIcon int16 `json:"shift_icon"` // Icon for shift ability } type HeroicOPEvent struct { ID int64 `json:"id"` InstanceID int64 `json:"instance_id"` EventType int `json:"event_type"` CharacterID int32 `json:"character_id"` AbilityIcon int16 `json:"ability_icon"` Timestamp time.Time `json:"timestamp"` Data string `json:"data"` // JSON encoded additional data } type HeroicOPConfig struct { DefaultWheelTimer int32 `json:"default_wheel_timer"` // milliseconds StarterChainTimeout int32 `json:"starter_chain_timeout"` // milliseconds MaxConcurrentHOs int `json:"max_concurrent_hos"` EnableLogging bool `json:"enable_logging"` EnableStatistics bool `json:"enable_statistics"` EnableShifting bool `json:"enable_shifting"` RequireClassMatch bool `json:"require_class_match"` // Enforce starter class restrictions AutoCleanupInterval int32 `json:"auto_cleanup_interval"` // seconds MaxHistoryEntries int `json:"max_history_entries"` } // External integration interfaces (simplified) type Logger interface { LogDebug(system, format string, args ...any) LogInfo(system, format string, args ...any) LogWarning(system, format string, args ...any) LogError(system, format string, args ...any) } // Manager Methods // NewHeroicOPManager creates a new heroic opportunity manager func NewHeroicOPManager(database *database.Database) *HeroicOPManager { return &HeroicOPManager{ database: database, starters: make(map[int32]*HeroicOPStarter), wheels: make(map[int32]*HeroicOPWheel), activeHOs: make(map[int64]*HeroicOP), encounterHOs: make(map[int32][]*HeroicOP), byClass: make(map[int8]map[int32]*HeroicOPStarter), byStarterID: make(map[int32][]*HeroicOPWheel), bySpellID: make(map[int32][]*HeroicOPWheel), byChance: make(map[string][]*HeroicOPWheel), orderedWheels: make(map[int32]*HeroicOPWheel), shiftWheels: make(map[int32]*HeroicOPWheel), spellInfo: make(map[int32]SpellInfo), nextInstanceID: 1, defaultWheelTimer: DefaultWheelTimerSeconds * 1000, // Convert to milliseconds maxConcurrentHOs: MaxConcurrentHOs, enableLogging: true, enableStatistics: true, } } // SetLogger sets the logger for the manager func (hom *HeroicOPManager) SetLogger(logger Logger) { hom.mu.Lock() defer hom.mu.Unlock() hom.logger = logger } // Initialize loads configuration and data from database func (hom *HeroicOPManager) Initialize(ctx context.Context, config *HeroicOPConfig) error { hom.mu.Lock() defer hom.mu.Unlock() if config != nil { hom.defaultWheelTimer = config.DefaultWheelTimer hom.maxConcurrentHOs = config.MaxConcurrentHOs hom.enableLogging = config.EnableLogging hom.enableStatistics = config.EnableStatistics } // Load all data from database if err := hom.loadFromDatabase(ctx); err != nil { return fmt.Errorf("failed to load heroic ops data: %w", err) } if hom.logger != nil { hom.logger.LogInfo("heroic_ops", "Initialized HO manager with %d starters and %d wheels", len(hom.starters), len(hom.wheels)) } return nil } // loadFromDatabase loads all heroic opportunities from the database with optimal indexing func (hom *HeroicOPManager) loadFromDatabase(ctx context.Context) error { // Clear existing data hom.clearIndices() // Load all starters if err := hom.loadStarters(ctx); err != nil { return fmt.Errorf("failed to load starters: %w", err) } // Load all wheels if err := hom.loadWheels(ctx); err != nil { return fmt.Errorf("failed to load wheels: %w", err) } // Build specialized indices for O(1) performance hom.buildIndices() hom.loaded = true return nil } // loadStarters loads all starters from database func (hom *HeroicOPManager) loadStarters(ctx context.Context) error { query := `SELECT id, start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description FROM heroic_op_starters ORDER BY id` rows, err := hom.database.Query(query) if err != nil { return err } defer rows.Close() for rows.Next() { starter := &HeroicOPStarter{ isNew: false, } err := rows.Scan( &starter.ID, &starter.StartClass, &starter.StarterIcon, &starter.Abilities[0], &starter.Abilities[1], &starter.Abilities[2], &starter.Abilities[3], &starter.Abilities[4], &starter.Abilities[5], &starter.Name, &starter.Description, ) if err != nil { return err } starter.SaveNeeded = false hom.starters[starter.ID] = starter } return rows.Err() } // loadWheels loads all wheels from database func (hom *HeroicOPManager) loadWheels(ctx context.Context) error { query := `SELECT id, starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6, spell_id, name, description, required_players FROM heroic_op_wheels ORDER BY id` rows, err := hom.database.Query(query) if err != nil { return err } defer rows.Close() for rows.Next() { wheel := &HeroicOPWheel{ isNew: false, } err := rows.Scan( &wheel.ID, &wheel.StarterLinkID, &wheel.Order, &wheel.ShiftIcon, &wheel.Chance, &wheel.Abilities[0], &wheel.Abilities[1], &wheel.Abilities[2], &wheel.Abilities[3], &wheel.Abilities[4], &wheel.Abilities[5], &wheel.SpellID, &wheel.Name, &wheel.Description, &wheel.RequiredPlayers, ) if err != nil { return err } wheel.SaveNeeded = false hom.wheels[wheel.ID] = wheel // Store spell info hom.spellInfo[wheel.SpellID] = SpellInfo{ ID: wheel.SpellID, Name: wheel.Name, Description: wheel.Description, } } return rows.Err() } // buildIndices creates specialized indices for O(1) performance func (hom *HeroicOPManager) buildIndices() { // Build class-based starter index for _, starter := range hom.starters { if hom.byClass[starter.StartClass] == nil { hom.byClass[starter.StartClass] = make(map[int32]*HeroicOPStarter) } hom.byClass[starter.StartClass][starter.ID] = starter } // Build wheel indices for _, wheel := range hom.wheels { // By starter ID hom.byStarterID[wheel.StarterLinkID] = append(hom.byStarterID[wheel.StarterLinkID], wheel) // By spell ID hom.bySpellID[wheel.SpellID] = append(hom.bySpellID[wheel.SpellID], wheel) // By chance range (for performance optimization) chanceRange := hom.getChanceRange(wheel.Chance) hom.byChance[chanceRange] = append(hom.byChance[chanceRange], wheel) // Special wheel types if wheel.IsOrdered() { hom.orderedWheels[wheel.ID] = wheel } if wheel.HasShift() { hom.shiftWheels[wheel.ID] = wheel } } // Sort wheels by chance for deterministic selection for _, wheels := range hom.byStarterID { sort.Slice(wheels, func(i, j int) bool { return wheels[i].Chance > wheels[j].Chance }) } } // clearIndices clears all indices func (hom *HeroicOPManager) clearIndices() { hom.starters = make(map[int32]*HeroicOPStarter) hom.wheels = make(map[int32]*HeroicOPWheel) hom.byClass = make(map[int8]map[int32]*HeroicOPStarter) hom.byStarterID = make(map[int32][]*HeroicOPWheel) hom.bySpellID = make(map[int32][]*HeroicOPWheel) hom.byChance = make(map[string][]*HeroicOPWheel) hom.orderedWheels = make(map[int32]*HeroicOPWheel) hom.shiftWheels = make(map[int32]*HeroicOPWheel) hom.spellInfo = make(map[int32]SpellInfo) } // getChanceRange returns a chance range string for indexing func (hom *HeroicOPManager) getChanceRange(chance float32) string { switch { case chance >= 75.0: return "very_high" case chance >= 50.0: return "high" case chance >= 25.0: return "medium" case chance >= 10.0: return "low" default: return "very_low" } } // Core HO Management Operations // StartHeroicOpportunity initiates a new heroic opportunity func (hom *HeroicOPManager) StartHeroicOpportunity(ctx context.Context, encounterID int32, initiatorID int32, initiatorClass int8) (*HeroicOP, error) { hom.mu.Lock() defer hom.mu.Unlock() // Check if encounter can have more HOs currentHOs := hom.encounterHOs[encounterID] if len(currentHOs) >= hom.maxConcurrentHOs { return nil, fmt.Errorf("encounter %d already has maximum concurrent HOs (%d)", encounterID, hom.maxConcurrentHOs) } // Get available starters for player's class starters := hom.getStartersForClass(initiatorClass) if len(starters) == 0 { return nil, fmt.Errorf("no heroic opportunities available for class %d", initiatorClass) } // Create new HO instance instanceID := hom.nextInstanceID hom.nextInstanceID++ ho := hom.createHeroicOP(instanceID, encounterID) ho.AddParticipant(initiatorID) // Prepare starter IDs for chain phase starterIDs := make([]int32, len(starters)) for i, starter := range starters { starterIDs[i] = starter.ID } ho.StartStarterChain(starterIDs) // Add to tracking maps hom.activeHOs[instanceID] = ho hom.encounterHOs[encounterID] = append(hom.encounterHOs[encounterID], ho) // Save to database if err := hom.saveHOInstance(ctx, ho); err != nil { if hom.logger != nil { hom.logger.LogError("heroic_ops", "Failed to save HO instance %d: %v", instanceID, err) } } // Log event if hom.enableLogging { hom.logEvent(ctx, instanceID, EventHOStarted, initiatorID, 0, "HO started") } if hom.logger != nil { hom.logger.LogInfo("heroic_ops", "Started HO %d for encounter %d initiated by %d with %d starters", instanceID, encounterID, initiatorID, len(starterIDs)) } return ho, nil } // ProcessAbility processes an ability used by a player during an active HO func (hom *HeroicOPManager) ProcessAbility(ctx context.Context, instanceID int64, characterID int32, abilityIcon int16) error { hom.mu.Lock() defer hom.mu.Unlock() ho, exists := hom.activeHOs[instanceID] if !exists { return fmt.Errorf("heroic opportunity %d not found", instanceID) } if !ho.IsActive() { return fmt.Errorf("heroic opportunity %d is not active (state: %d)", instanceID, ho.State) } // Add player as participant ho.AddParticipant(characterID) success := false switch ho.State { case HOStateStarterChain: success = ho.ProcessStarterAbility(abilityIcon, hom) if success && len(ho.CurrentStarters) == 1 { // Starter chain completed, transition to wheel phase starterID := ho.CurrentStarters[0] ho.StarterID = starterID // Select random wheel for this starter wheel := hom.selectRandomWheel(starterID) if wheel == nil { // No wheels available, HO fails ho.State = HOStateFailed hom.failHO(ctx, ho, "No wheels available for starter") return fmt.Errorf("no wheels available for starter %d", starterID) } // Start wheel phase ho.StartWheelPhase(wheel, hom.defaultWheelTimer/1000) // Convert to seconds // Send wheel packet to participants (would be implemented with client manager) hom.sendWheelUpdate(ho, wheel) if hom.logger != nil { hom.logger.LogDebug("heroic_ops", "HO %d transitioned to wheel phase with wheel %d", instanceID, wheel.ID) } } case HOStateWheelPhase: wheel := hom.wheels[ho.WheelID] if wheel == nil { return fmt.Errorf("wheel %d not found for HO %d", ho.WheelID, instanceID) } // Check for shift attempt if ho.ShiftUsed == ShiftNotUsed && wheel.CanShift(abilityIcon) { return hom.handleWheelShift(ctx, ho, wheel, characterID) } // Process regular ability success = ho.ProcessWheelAbility(abilityIcon, characterID, wheel) if success { // Send progress update (would be implemented with client manager) hom.sendProgressUpdate(ho) // Check if HO is complete if ho.IsComplete() { hom.completeHO(ctx, ho, wheel, characterID) } } } // Log ability use if hom.enableLogging { eventType := EventHOAbilityUsed data := fmt.Sprintf("ability:%d,success:%t", abilityIcon, success) hom.logEvent(ctx, instanceID, eventType, characterID, abilityIcon, data) } // Save changes if ho.SaveNeeded { if err := hom.saveHOInstance(ctx, ho); err != nil { if hom.logger != nil { hom.logger.LogError("heroic_ops", "Failed to save HO instance %d: %v", instanceID, err) } } ho.SaveNeeded = false } if !success { return fmt.Errorf("ability %d not allowed for current HO state", abilityIcon) } return nil } // UpdateTimers updates all active HO timers func (hom *HeroicOPManager) UpdateTimers(ctx context.Context, deltaMS int32) { hom.mu.Lock() defer hom.mu.Unlock() var expiredHOs []int64 for instanceID, ho := range hom.activeHOs { if ho.State == HOStateWheelPhase { if !ho.UpdateTimer(deltaMS) { // Timer expired expiredHOs = append(expiredHOs, instanceID) } else { // Send timer update to participants (would be implemented with client manager) hom.sendTimerUpdate(ho) } } } // Handle expired HOs for _, instanceID := range expiredHOs { ho := hom.activeHOs[instanceID] hom.failHO(ctx, ho, "Timer expired") } } // Getter methods with O(1) performance // GetStarter retrieves a starter by ID with O(1) performance func (hom *HeroicOPManager) GetStarter(id int32) *HeroicOPStarter { hom.mu.RLock() defer hom.mu.RUnlock() return hom.starters[id] } // getStartersForClass returns all starters for a specific class with O(1) performance func (hom *HeroicOPManager) getStartersForClass(class int8) []*HeroicOPStarter { var result []*HeroicOPStarter // Add class-specific starters if classStarters, exists := hom.byClass[class]; exists { for _, starter := range classStarters { result = append(result, starter) } } // Add universal starters (class 0 = any) if universalStarters, exists := hom.byClass[ClassAny]; exists { for _, starter := range universalStarters { result = append(result, starter) } } return result } // GetWheel retrieves a wheel by ID with O(1) performance func (hom *HeroicOPManager) GetWheel(id int32) *HeroicOPWheel { hom.mu.RLock() defer hom.mu.RUnlock() return hom.wheels[id] } // getWheelsForStarter returns all wheels for a starter with O(1) performance func (hom *HeroicOPManager) getWheelsForStarter(starterID int32) []*HeroicOPWheel { wheels := hom.byStarterID[starterID] if wheels == nil { return nil } // Return copy to prevent external modification result := make([]*HeroicOPWheel, len(wheels)) copy(result, wheels) return result } // selectRandomWheel selects a random wheel from starter's wheels with optimized performance func (hom *HeroicOPManager) selectRandomWheel(starterID int32) *HeroicOPWheel { wheels := hom.getWheelsForStarter(starterID) if len(wheels) == 0 { return nil } return SelectRandomWheel(wheels) } // GetActiveHO returns an active HO by instance ID func (hom *HeroicOPManager) GetActiveHO(instanceID int64) (*HeroicOP, bool) { hom.mu.RLock() defer hom.mu.RUnlock() ho, exists := hom.activeHOs[instanceID] return ho, exists } // GetEncounterHOs returns all HOs for an encounter func (hom *HeroicOPManager) GetEncounterHOs(encounterID int32) []*HeroicOP { hom.mu.RLock() defer hom.mu.RUnlock() hos := hom.encounterHOs[encounterID] result := make([]*HeroicOP, len(hos)) copy(result, hos) return result } // GetStatistics returns current HO system statistics func (hom *HeroicOPManager) GetStatistics() *HeroicOPManagerStats { hom.mu.RLock() defer hom.mu.RUnlock() // Calculate runtime statistics classDistribution := make(map[int32]int64) for _, starter := range hom.starters { classDistribution[int32(starter.StartClass)]++ } totalChance := float64(0) for _, wheel := range hom.wheels { totalChance += float64(wheel.Chance) } averageChance := float64(0) if len(hom.wheels) > 0 { averageChance = totalChance / float64(len(hom.wheels)) } return &HeroicOPManagerStats{ TotalStarters: int64(len(hom.starters)), TotalWheels: int64(len(hom.wheels)), ClassDistribution: classDistribution, OrderedWheelsCount: int64(len(hom.orderedWheels)), ShiftWheelsCount: int64(len(hom.shiftWheels)), SpellCount: int64(len(hom.spellInfo)), AverageChance: averageChance, ActiveHOCount: int64(len(hom.activeHOs)), } } // Helper methods // createHeroicOP creates a new HO instance func (hom *HeroicOPManager) createHeroicOP(instanceID int64, encounterID int32) *HeroicOP { return &HeroicOP{ ID: instanceID, EncounterID: encounterID, State: HOStateInactive, StartTime: time.Now(), Participants: make(map[int32]bool), isNew: true, } } // handleWheelShift handles wheel shifting logic func (hom *HeroicOPManager) handleWheelShift(ctx context.Context, ho *HeroicOP, currentWheel *HeroicOPWheel, characterID int32) error { // Check if shift is allowed (no progress made) canShift := true for i := 0; i < MaxAbilities; i++ { if ho.Countered[i] != 0 { canShift = false break } } if !canShift { return fmt.Errorf("wheel shift not allowed after progress has been made") } // Select new random wheel newWheel := hom.selectRandomWheel(ho.StarterID) if newWheel == nil || newWheel.ID == currentWheel.ID { return fmt.Errorf("no alternative wheel available for shift") } oldWheelID := ho.WheelID ho.WheelID = newWheel.ID ho.ShiftUsed = ShiftUsed ho.SpellName = newWheel.Name ho.SpellDescription = newWheel.Description ho.SaveNeeded = true // Send shift notification (would be implemented with client manager) hom.sendShiftUpdate(ho, oldWheelID, newWheel.ID) // Log shift event if hom.enableLogging { data := fmt.Sprintf("old_wheel:%d,new_wheel:%d", oldWheelID, newWheel.ID) hom.logEvent(ctx, ho.ID, EventHOWheelShifted, characterID, 0, data) } if hom.logger != nil { hom.logger.LogDebug("heroic_ops", "HO %d wheel shifted from %d to %d by character %d", ho.ID, oldWheelID, newWheel.ID, characterID) } return nil } // completeHO handles HO completion func (hom *HeroicOPManager) completeHO(ctx context.Context, ho *HeroicOP, wheel *HeroicOPWheel, completedBy int32) { ho.State = HOStateComplete ho.Complete = HOComplete ho.CompletedBy = completedBy ho.SaveNeeded = true // Cast completion spell (would be integrated with spell system) if wheel.SpellID > 0 { _ = ho.GetParticipants() // participants will be used when spell manager is integrated // TODO: Cast spell on participants through spell manager } // Send completion packet (would be implemented with client manager) hom.sendCompletionUpdate(ho, true) // Log completion if hom.enableLogging { data := fmt.Sprintf("completed_by:%d,spell_id:%d,participants:%d", completedBy, wheel.SpellID, len(ho.Participants)) hom.logEvent(ctx, ho.ID, EventHOCompleted, completedBy, 0, data) } if hom.logger != nil { hom.logger.LogInfo("heroic_ops", "HO %d completed by character %d, spell %d cast", ho.ID, completedBy, wheel.SpellID) } } // failHO handles HO failure func (hom *HeroicOPManager) failHO(ctx context.Context, ho *HeroicOP, reason string) { ho.State = HOStateFailed ho.SaveNeeded = true // Send failure packet (would be implemented with client manager) hom.sendCompletionUpdate(ho, false) // Log failure if hom.enableLogging { hom.logEvent(ctx, ho.ID, EventHOFailed, 0, 0, reason) } if hom.logger != nil { hom.logger.LogDebug("heroic_ops", "HO %d failed: %s", ho.ID, reason) } } // Communication placeholder methods (would integrate with client manager) func (hom *HeroicOPManager) sendWheelUpdate(ho *HeroicOP, wheel *HeroicOPWheel) { // TODO: Implement when client manager is integrated } func (hom *HeroicOPManager) sendProgressUpdate(ho *HeroicOP) { // TODO: Implement when client manager is integrated } func (hom *HeroicOPManager) sendTimerUpdate(ho *HeroicOP) { // TODO: Implement when client manager is integrated } func (hom *HeroicOPManager) sendCompletionUpdate(ho *HeroicOP, success bool) { // TODO: Implement when client manager is integrated } func (hom *HeroicOPManager) sendShiftUpdate(ho *HeroicOP, oldWheelID, newWheelID int32) { // TODO: Implement when client manager is integrated } // Database operations // saveHOInstance saves HO instance to database func (hom *HeroicOPManager) saveHOInstance(ctx context.Context, ho *HeroicOP) error { if ho.isNew { // Insert new record query := `INSERT INTO heroic_op_instances (id, encounter_id, starter_id, wheel_id, state, start_time, wheel_start_time, time_remaining, total_time, complete, countered1, countered2, countered3, countered4, countered5, countered6, shift_used, starter_progress, completed_by, spell_name, spell_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` _, err := hom.database.Exec(query, ho.ID, ho.EncounterID, ho.StarterID, ho.WheelID, ho.State, ho.StartTime, ho.WheelStartTime, ho.TimeRemaining, ho.TotalTime, ho.Complete, ho.Countered[0], ho.Countered[1], ho.Countered[2], ho.Countered[3], ho.Countered[4], ho.Countered[5], ho.ShiftUsed, ho.StarterProgress, ho.CompletedBy, ho.SpellName, ho.SpellDescription) if err != nil { return err } ho.isNew = false } else { // Update existing record query := `UPDATE heroic_op_instances SET encounter_id=?, starter_id=?, wheel_id=?, state=?, start_time=?, wheel_start_time=?, time_remaining=?, total_time=?, complete=?, countered1=?, countered2=?, countered3=?, countered4=?, countered5=?, countered6=?, shift_used=?, starter_progress=?, completed_by=?, spell_name=?, spell_description=? WHERE id=?` _, err := hom.database.Exec(query, ho.EncounterID, ho.StarterID, ho.WheelID, ho.State, ho.StartTime, ho.WheelStartTime, ho.TimeRemaining, ho.TotalTime, ho.Complete, ho.Countered[0], ho.Countered[1], ho.Countered[2], ho.Countered[3], ho.Countered[4], ho.Countered[5], ho.ShiftUsed, ho.StarterProgress, ho.CompletedBy, ho.SpellName, ho.SpellDescription, ho.ID) if err != nil { return err } } return nil } // logEvent logs an HO event to the database func (hom *HeroicOPManager) logEvent(ctx context.Context, instanceID int64, eventType int, characterID int32, abilityIcon int16, data string) { if !hom.enableLogging { return } query := `INSERT INTO heroic_op_events (instance_id, event_type, character_id, ability_icon, timestamp, data) VALUES (?, ?, ?, ?, ?, ?)` _, err := hom.database.Exec(query, instanceID, eventType, characterID, abilityIcon, time.Now(), data) if err != nil && hom.logger != nil { hom.logger.LogError("heroic_ops", "Failed to save HO event: %v", err) } } // HeroicOP Methods // NewHeroicOP creates a new HO instance func NewHeroicOP(instanceID int64, encounterID int32) *HeroicOP { return &HeroicOP{ ID: instanceID, EncounterID: encounterID, State: HOStateInactive, StartTime: time.Now(), Participants: make(map[int32]bool), isNew: true, } } // AddParticipant adds a character to the HO func (ho *HeroicOP) AddParticipant(characterID int32) { ho.mu.Lock() defer ho.mu.Unlock() ho.Participants[characterID] = true ho.SaveNeeded = true } // GetParticipants returns list of participant character IDs func (ho *HeroicOP) GetParticipants() []int32 { ho.mu.RLock() defer ho.mu.RUnlock() participants := make([]int32, 0, len(ho.Participants)) for characterID := range ho.Participants { participants = append(participants, characterID) } return participants } // IsActive returns true if HO is in an active state func (ho *HeroicOP) IsActive() bool { ho.mu.RLock() defer ho.mu.RUnlock() return ho.State == HOStateStarterChain || ho.State == HOStateWheelPhase } // IsComplete returns true if all wheel abilities are countered func (ho *HeroicOP) IsComplete() bool { ho.mu.RLock() defer ho.mu.RUnlock() for i := 0; i < MaxAbilities; i++ { if ho.Countered[i] == 0 { return false } } return true } // StartStarterChain begins the starter chain phase func (ho *HeroicOP) StartStarterChain(starterIDs []int32) { ho.mu.Lock() defer ho.mu.Unlock() ho.State = HOStateStarterChain ho.CurrentStarters = make([]int32, len(starterIDs)) copy(ho.CurrentStarters, starterIDs) ho.StarterProgress = 0 ho.SaveNeeded = true } // ProcessStarterAbility processes an ability during starter chain phase func (ho *HeroicOP) ProcessStarterAbility(abilityIcon int16, hom *HeroicOPManager) bool { ho.mu.Lock() defer ho.mu.Unlock() // Check each active starter for matching ability at current position var remainingStarters []int32 for _, starterID := range ho.CurrentStarters { starter := hom.GetStarter(starterID) if starter != nil && starter.MatchesAbilityAtPosition(abilityIcon, ho.StarterProgress) { remainingStarters = append(remainingStarters, starterID) } } if len(remainingStarters) == 0 { return false // No starters matched this ability } // Update progress and remaining starters ho.StarterProgress++ ho.CurrentStarters = remainingStarters ho.SaveNeeded = true // Check if starter chain is complete (only one starter left and at end of chain) if len(remainingStarters) == 1 { starter := hom.GetStarter(remainingStarters[0]) if starter != nil && int(ho.StarterProgress) >= starter.GetLength() { return true // Starter chain completed } } return true // Progress made but not complete } // StartWheelPhase begins the wheel phase func (ho *HeroicOP) StartWheelPhase(wheel *HeroicOPWheel, wheelTimerSeconds int32) { ho.mu.Lock() defer ho.mu.Unlock() ho.State = HOStateWheelPhase ho.WheelID = wheel.ID ho.WheelStartTime = time.Now() ho.TimeRemaining = wheelTimerSeconds * 1000 // Convert to milliseconds ho.TotalTime = ho.TimeRemaining ho.SpellName = wheel.Name ho.SpellDescription = wheel.Description ho.SaveNeeded = true // Initialize countered array for i := 0; i < MaxAbilities; i++ { ho.Countered[i] = 0 } } // ProcessWheelAbility processes an ability during wheel phase func (ho *HeroicOP) ProcessWheelAbility(abilityIcon int16, characterID int32, wheel *HeroicOPWheel) bool { ho.mu.Lock() defer ho.mu.Unlock() // Find matching ability in wheel for i := 0; i < MaxAbilities; i++ { if wheel.Abilities[i] == abilityIcon || wheel.Abilities[i] == AbilityIconAny { // Check if this ability is already countered if ho.Countered[i] != 0 { continue } // For ordered wheels, check if this is the next expected ability if wheel.IsOrdered() && !ho.isNextAbilityInOrder(i) { continue } // Mark as countered ho.Countered[i] = 1 ho.SaveNeeded = true return true } } return false // No matching ability found } // isNextAbilityInOrder checks if the given index is the next expected ability for ordered wheels func (ho *HeroicOP) isNextAbilityInOrder(index int) bool { for i := 0; i < index; i++ { if ho.Countered[i] == 0 { return false // Earlier ability not completed } } return true } // UpdateTimer updates the HO timer, returns false if expired func (ho *HeroicOP) UpdateTimer(deltaMS int32) bool { ho.mu.Lock() defer ho.mu.Unlock() ho.TimeRemaining -= deltaMS if ho.TimeRemaining <= 0 { ho.TimeRemaining = 0 return false } ho.SaveNeeded = true return true } // GetProgress returns completion progress as percentage func (ho *HeroicOP) GetProgress() float32 { ho.mu.RLock() defer ho.mu.RUnlock() completed := float32(0) for i := 0; i < MaxAbilities; i++ { if ho.Countered[i] != 0 { completed++ } } return (completed / float32(MaxAbilities)) * 100.0 } // GetPacketData converts HO to packet data structure func (ho *HeroicOP) GetPacketData(wheel *HeroicOPWheel) *PacketData { ho.mu.RLock() defer ho.mu.RUnlock() data := &PacketData{ TimeRemaining: ho.TimeRemaining, TotalTime: ho.TotalTime, Complete: ho.Complete, State: ho.State, Countered: ho.Countered, } if wheel != nil { data.SpellName = wheel.Name data.SpellDescription = wheel.Description data.Abilities = wheel.Abilities data.CanShift = ho.ShiftUsed == ShiftNotUsed && wheel.HasShift() data.ShiftIcon = wheel.ShiftIcon } else { data.SpellName = ho.SpellName data.SpellDescription = ho.SpellDescription // Abilities will be zero-initialized } return data } // HeroicOPStarter Methods // Validate validates the starter configuration func (s *HeroicOPStarter) Validate() error { s.mu.RLock() defer s.mu.RUnlock() if s.ID <= 0 { return fmt.Errorf("invalid starter ID: %d", s.ID) } if s.Name == "" { return fmt.Errorf("starter name cannot be empty") } if len(s.Name) > MaxHONameLength { return fmt.Errorf("starter name too long: %d > %d", len(s.Name), MaxHONameLength) } // Check that at least one ability is specified hasAbility := false for i := 0; i < MaxAbilities; i++ { if s.Abilities[i] != 0 { hasAbility = true break } } if !hasAbility { return fmt.Errorf("starter must have at least one ability") } return nil } // MatchesAbilityAtPosition checks if the given ability matches at the specified position func (s *HeroicOPStarter) MatchesAbilityAtPosition(abilityIcon int16, position int8) bool { s.mu.RLock() defer s.mu.RUnlock() if position < 0 || int(position) >= MaxAbilities { return false } expectedAbility := s.Abilities[position] return expectedAbility == abilityIcon || expectedAbility == AbilityIconAny } // GetLength returns the length of the starter chain func (s *HeroicOPStarter) GetLength() int { s.mu.RLock() defer s.mu.RUnlock() length := 0 for i := 0; i < MaxAbilities; i++ { if s.Abilities[i] != 0 { length++ } else { break // Stop at first empty slot } } return length } // HeroicOPWheel Methods // Validate validates the wheel configuration func (w *HeroicOPWheel) Validate() error { w.mu.RLock() defer w.mu.RUnlock() if w.ID <= 0 { return fmt.Errorf("invalid wheel ID: %d", w.ID) } if w.StarterLinkID <= 0 { return fmt.Errorf("invalid starter link ID: %d", w.StarterLinkID) } if w.Name == "" { return fmt.Errorf("wheel name cannot be empty") } if len(w.Name) > MaxHONameLength { return fmt.Errorf("wheel name too long: %d > %d", len(w.Name), MaxHONameLength) } if w.Chance < MinChance || w.Chance > MaxChance { return fmt.Errorf("wheel chance out of range: %f", w.Chance) } // Check that at least one ability is specified hasAbility := false for i := 0; i < MaxAbilities; i++ { if w.Abilities[i] != 0 { hasAbility = true break } } if !hasAbility { return fmt.Errorf("wheel must have at least one ability") } return nil } // IsOrdered returns true if this is an ordered wheel func (w *HeroicOPWheel) IsOrdered() bool { w.mu.RLock() defer w.mu.RUnlock() return w.Order == WheelOrderOrdered } // HasShift returns true if this wheel has a shift ability func (w *HeroicOPWheel) HasShift() bool { w.mu.RLock() defer w.mu.RUnlock() return w.ShiftIcon != 0 } // CanShift returns true if the given ability icon can shift this wheel func (w *HeroicOPWheel) CanShift(abilityIcon int16) bool { w.mu.RLock() defer w.mu.RUnlock() return w.ShiftIcon == abilityIcon } // Utility Functions // SelectRandomWheel selects a wheel based on chance weights func SelectRandomWheel(wheels []*HeroicOPWheel) *HeroicOPWheel { if len(wheels) == 0 { return nil } if len(wheels) == 1 { return wheels[0] } // Calculate total weight totalWeight := float32(0) for _, wheel := range wheels { totalWeight += wheel.Chance } if totalWeight <= 0 { // Equal probability fallback return wheels[rand.Intn(len(wheels))] } // Random selection based on weights randomValue := rand.Float32() * totalWeight currentWeight := float32(0) for _, wheel := range wheels { currentWeight += wheel.Chance if randomValue <= currentWeight { return wheel } } // Fallback (should not happen) return wheels[len(wheels)-1] } // Packet Building Integration // BuildHeroicOpportunityPacket builds the main HO packet for network transmission func (hom *HeroicOPManager) BuildHeroicOpportunityPacket(ho *HeroicOP, wheel *HeroicOPWheel, clientVersion uint32) (map[string]interface{}, error) { if ho == nil { return nil, fmt.Errorf("heroic opportunity is nil") } packet := make(map[string]interface{}) // Basic HO information packet["id"] = ho.ID packet["wheel_type"] = int8(0) // Default wheel type packet["unknown"] = int8(0) packet["time_total"] = float32(ho.TotalTime) / 1000.0 // Convert to seconds packet["time_left"] = float32(ho.TimeRemaining) / 1000.0 // Convert to seconds if wheel != nil { packet["name"] = wheel.Name packet["description"] = wheel.Description packet["order"] = wheel.Order packet["shift_icon"] = wheel.ShiftIcon packet["starter_icon"] = int16(0) // Would need starter reference // Wheel abilities packet["icon1"] = wheel.Abilities[0] packet["icon2"] = wheel.Abilities[1] packet["icon3"] = wheel.Abilities[2] packet["icon4"] = wheel.Abilities[3] packet["icon5"] = wheel.Abilities[4] packet["icon6"] = wheel.Abilities[5] // Completion status packet["countered1"] = ho.Countered[0] packet["countered2"] = ho.Countered[1] packet["countered3"] = ho.Countered[2] packet["countered4"] = ho.Countered[3] packet["countered5"] = ho.Countered[4] packet["countered6"] = ho.Countered[5] } else { packet["name"] = ho.SpellName packet["description"] = ho.SpellDescription packet["order"] = int8(0) packet["shift_icon"] = int16(0) packet["starter_icon"] = int16(0) // Zero out abilities and counters for i := 1; i <= 6; i++ { packet[fmt.Sprintf("icon%d", i)] = int16(0) packet[fmt.Sprintf("countered%d", i)] = int8(0) } } return packet, nil } // GetHeroicOpportunityOpcode returns the appropriate opcode for HO packets func (hom *HeroicOPManager) GetHeroicOpportunityOpcode() packets.InternalOpcode { return packets.OP_HeroicOpportunityMsg } // Cleanup Operations // CleanupExpiredHOs removes completed and failed HOs func (hom *HeroicOPManager) CleanupExpiredHOs(ctx context.Context, maxAge time.Duration) { hom.mu.Lock() defer hom.mu.Unlock() cutoff := time.Now().Add(-maxAge) var toRemove []int64 for instanceID, ho := range hom.activeHOs { if !ho.IsActive() && ho.StartTime.Before(cutoff) { toRemove = append(toRemove, instanceID) } } for _, instanceID := range toRemove { ho := hom.activeHOs[instanceID] // Remove from encounter tracking encounterHOs := hom.encounterHOs[ho.EncounterID] for i, encounterHO := range encounterHOs { if encounterHO.ID == instanceID { hom.encounterHOs[ho.EncounterID] = append(encounterHOs[:i], encounterHOs[i+1:]...) break } } // Clean up empty encounter list if len(hom.encounterHOs[ho.EncounterID]) == 0 { delete(hom.encounterHOs, ho.EncounterID) } // Remove from active list delete(hom.activeHOs, instanceID) // Delete from database query := `DELETE FROM heroic_op_instances WHERE id = ?` if _, err := hom.database.Exec(query, instanceID); err != nil { if hom.logger != nil { hom.logger.LogError("heroic_ops", "Failed to delete HO instance %d: %v", instanceID, err) } } } if len(toRemove) > 0 && hom.logger != nil { hom.logger.LogDebug("heroic_ops", "Cleaned up %d expired HO instances", len(toRemove)) } } // IsLoaded returns whether data has been loaded func (hom *HeroicOPManager) IsLoaded() bool { hom.mu.RLock() defer hom.mu.RUnlock() return hom.loaded }