package heroic_ops import ( "context" "fmt" "time" ) // NewHeroicOPManager creates a new heroic opportunity manager func NewHeroicOPManager(masterList *MasterHeroicOPList, database HeroicOPDatabase, clientManager ClientManager, encounterManager EncounterManager, playerManager PlayerManager) *HeroicOPManager { return &HeroicOPManager{ activeHOs: make(map[int64]*HeroicOP), encounterHOs: make(map[int32][]*HeroicOP), masterList: masterList, database: database, nextInstanceID: 1, defaultWheelTimer: DefaultWheelTimerSeconds * 1000, // Convert to milliseconds maxConcurrentHOs: MaxConcurrentHOs, enableLogging: true, enableStatistics: true, } } // SetEventHandler sets the event handler for HO events func (hom *HeroicOPManager) SetEventHandler(handler HeroicOPEventHandler) { hom.eventHandler = handler } // SetLogger sets the logger for the manager func (hom *HeroicOPManager) SetLogger(logger LogHandler) { hom.logger = logger } // Initialize loads configuration and prepares the manager 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 } // Ensure master list is loaded if !hom.masterList.IsLoaded() { if err := hom.masterList.LoadFromDatabase(ctx, hom.database); err != nil { return fmt.Errorf("failed to load heroic opportunities: %w", err) } } if hom.logger != nil { hom.logger.LogInfo("heroic_ops", "Initialized HO manager with %d starters and %d wheels", hom.masterList.GetStarterCount(), hom.masterList.GetWheelCount()) } return nil } // StartHeroicOpportunity initiates a new heroic opportunity func (hom *HeroicOPManager) StartHeroicOpportunity(ctx context.Context, encounterID int32, initiatorID int32) (*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 initiator's class playerInfo, err := hom.playerManager.GetPlayerInfo(initiatorID) if err != nil { return nil, fmt.Errorf("failed to get player info for initiator %d: %w", initiatorID, err) } // Get available starters for player's class starters := hom.masterList.GetStartersForClass(playerInfo.AdventureClass) if len(starters) == 0 { return nil, fmt.Errorf("no heroic opportunities available for class %d", playerInfo.AdventureClass) } // Create new HO instance instanceID := hom.nextInstanceID hom.nextInstanceID++ ho := NewHeroicOP(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.database.SaveHOInstance(ctx, ho); err != nil { if hom.logger != nil { hom.logger.LogError("heroic_ops", "Failed to save HO instance %d: %v", instanceID, err) } } // Notify event handler if hom.eventHandler != nil { hom.eventHandler.OnHOStarted(ho, initiatorID) } // 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.masterList) 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.masterList.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 // Notify event handler if hom.eventHandler != nil { hom.eventHandler.OnWheelPhaseStarted(ho, wheel.ID, ho.TimeRemaining) } // Send wheel packet to participants 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.masterList.GetWheel(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 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) } // Notify event handler if hom.eventHandler != nil { hom.eventHandler.OnAbilityUsed(ho, characterID, abilityIcon, success) if success { progress := ho.GetProgress() hom.eventHandler.OnProgressMade(ho, characterID, progress) } } // Save changes if ho.SaveNeeded { if err := hom.database.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 hom.sendTimerUpdate(ho) } } } // Handle expired HOs for _, instanceID := range expiredHOs { ho := hom.activeHOs[instanceID] hom.failHO(ctx, ho, "Timer expired") } } // 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 } // 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 if err := hom.database.DeleteHOInstance(ctx, 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)) } } // GetStatistics returns current HO system statistics func (hom *HeroicOPManager) GetStatistics() *HeroicOPStatistics { hom.mu.RLock() defer hom.mu.RUnlock() stats := &HeroicOPStatistics{ ActiveHOCount: len(hom.activeHOs), ParticipationStats: make(map[int32]int64), } // Count participants for _, ho := range hom.activeHOs { for characterID := range ho.Participants { stats.ParticipationStats[characterID]++ } } // TODO: Get additional statistics from database // This is a simplified implementation return stats } // Helper methods // 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.masterList.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 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) } // Notify event handler if hom.eventHandler != nil { hom.eventHandler.OnWheelShifted(ho, characterID, newWheel.ID) } 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 if wheel.SpellID > 0 { participants := ho.GetParticipants() // TODO: Cast spell on participants through spell manager // hom.spellManager.CastSpell(completedBy, wheel.SpellID, participants) } // Send completion packet 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) } // Notify event handler if hom.eventHandler != nil { hom.eventHandler.OnHOCompleted(ho, completedBy, wheel.SpellID) } 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 hom.sendCompletionUpdate(ho, false) // Log failure if hom.enableLogging { hom.logEvent(ctx, ho.ID, EventHOFailed, 0, 0, reason) } // Notify event handler if hom.eventHandler != nil { hom.eventHandler.OnHOFailed(ho, reason) } if hom.logger != nil { hom.logger.LogDebug("heroic_ops", "HO %d failed: %s", ho.ID, reason) } } // Communication helper methods func (hom *HeroicOPManager) sendWheelUpdate(ho *HeroicOP, wheel *HeroicOPWheel) { if hom.clientManager == nil { return } participants := ho.GetParticipants() packetBuilder := NewHeroicOPPacketBuilder(0) // Default version data := packetBuilder.ToPacketData(ho, wheel) for _, characterID := range participants { if err := hom.clientManager.SendHOUpdate(characterID, data); err != nil { if hom.logger != nil { hom.logger.LogWarning("heroic_ops", "Failed to send HO update to character %d: %v", characterID, err) } } } } func (hom *HeroicOPManager) sendProgressUpdate(ho *HeroicOP) { if hom.clientManager == nil { return } participants := ho.GetParticipants() wheel := hom.masterList.GetWheel(ho.WheelID) packetBuilder := NewHeroicOPPacketBuilder(0) data := packetBuilder.ToPacketData(ho, wheel) for _, characterID := range participants { if err := hom.clientManager.SendHOUpdate(characterID, data); err != nil { if hom.logger != nil { hom.logger.LogWarning("heroic_ops", "Failed to send progress update to character %d: %v", characterID, err) } } } } func (hom *HeroicOPManager) sendTimerUpdate(ho *HeroicOP) { if hom.clientManager == nil { return } participants := ho.GetParticipants() for _, characterID := range participants { if err := hom.clientManager.SendHOTimer(characterID, ho.TimeRemaining, ho.TotalTime); err != nil { if hom.logger != nil { hom.logger.LogWarning("heroic_ops", "Failed to send timer update to character %d: %v", characterID, err) } } } } func (hom *HeroicOPManager) sendCompletionUpdate(ho *HeroicOP, success bool) { if hom.clientManager == nil { return } participants := ho.GetParticipants() for _, characterID := range participants { if err := hom.clientManager.SendHOComplete(characterID, ho, success); err != nil { if hom.logger != nil { hom.logger.LogWarning("heroic_ops", "Failed to send completion update to character %d: %v", characterID, err) } } } } func (hom *HeroicOPManager) sendShiftUpdate(ho *HeroicOP, oldWheelID, newWheelID int32) { if hom.clientManager == nil { return } participants := ho.GetParticipants() packetBuilder := NewHeroicOPPacketBuilder(0) for _, characterID := range participants { if packet, err := packetBuilder.BuildHOShiftPacket(ho, oldWheelID, newWheelID); err == nil { // TODO: Send packet through client manager _ = packet // Placeholder } } } // 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 } event := &HeroicOPEvent{ InstanceID: instanceID, EventType: eventType, CharacterID: characterID, AbilityIcon: abilityIcon, Timestamp: time.Now(), Data: data, } if err := hom.database.SaveHOEvent(ctx, event); err != nil { if hom.logger != nil { hom.logger.LogError("heroic_ops", "Failed to save HO event: %v", err) } } }