From d0c51ea42facfc48f22f1cd28e7f7d440955ab98 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 29 Aug 2025 14:18:05 -0500 Subject: [PATCH] simplify heroic_ops --- SIMPLIFICATION.md | 1 + internal/heroic_ops/heroic_op_instance.go | 704 ----------- internal/heroic_ops/heroic_op_starter.go | 313 ----- internal/heroic_ops/heroic_op_wheel.go | 405 ------ internal/heroic_ops/heroic_ops.go | 1390 +++++++++++++++++++++ internal/heroic_ops/interfaces.go | 218 ---- internal/heroic_ops/manager.go | 588 --------- internal/heroic_ops/master.go | 720 ----------- internal/heroic_ops/packets.go | 458 ------- internal/heroic_ops/types.go | 190 --- internal/heroic_ops/utils.go | 50 - internal/packets/opcodes.go | 20 + 12 files changed, 1411 insertions(+), 3646 deletions(-) delete mode 100644 internal/heroic_ops/heroic_op_instance.go delete mode 100644 internal/heroic_ops/heroic_op_starter.go delete mode 100644 internal/heroic_ops/heroic_op_wheel.go create mode 100644 internal/heroic_ops/heroic_ops.go delete mode 100644 internal/heroic_ops/interfaces.go delete mode 100644 internal/heroic_ops/manager.go delete mode 100644 internal/heroic_ops/master.go delete mode 100644 internal/heroic_ops/packets.go delete mode 100644 internal/heroic_ops/types.go delete mode 100644 internal/heroic_ops/utils.go diff --git a/SIMPLIFICATION.md b/SIMPLIFICATION.md index 655a5e6..a90cd41 100644 --- a/SIMPLIFICATION.md +++ b/SIMPLIFICATION.md @@ -15,6 +15,7 @@ This document outlines how we successfully simplified the EverQuest II housing p - Ground Spawn - Groups - Guilds +- Heroic Ops ## Before: Complex Architecture (8 Files, ~2000+ Lines) diff --git a/internal/heroic_ops/heroic_op_instance.go b/internal/heroic_ops/heroic_op_instance.go deleted file mode 100644 index 468612b..0000000 --- a/internal/heroic_ops/heroic_op_instance.go +++ /dev/null @@ -1,704 +0,0 @@ -package heroic_ops - -import ( - "fmt" - "time" - - "eq2emu/internal/database" -) - -// NewHeroicOP creates a new heroic opportunity instance -func NewHeroicOP(db *database.Database, instanceID int64, encounterID int32) *HeroicOP { - return &HeroicOP{ - ID: instanceID, - EncounterID: encounterID, - State: HOStateInactive, - StartTime: time.Now(), - Participants: make(map[int32]bool), - CurrentStarters: make([]int32, 0), - TotalTime: DefaultWheelTimerSeconds * 1000, // Convert to milliseconds - TimeRemaining: DefaultWheelTimerSeconds * 1000, - db: db, - isNew: true, - SaveNeeded: false, - } -} - -// LoadHeroicOP loads a heroic opportunity instance by ID -func LoadHeroicOP(db *database.Database, instanceID int64) (*HeroicOP, error) { - ho := &HeroicOP{ - db: db, - isNew: false, - } - - // Load basic HO data - query := `SELECT id, encounter_id, starter_id, wheel_id, state, start_time, wheel_start_time, time_remaining, total_time, - complete, shift_used, starter_progress, completed_by, spell_name, spell_description, - countered1, countered2, countered3, countered4, countered5, countered6 - FROM heroic_op_instances WHERE id = ?` - - var startTimeStr, wheelStartTimeStr string - err := db.QueryRow(query, instanceID).Scan( - &ho.ID, - &ho.EncounterID, - &ho.StarterID, - &ho.WheelID, - &ho.State, - &startTimeStr, - &wheelStartTimeStr, - &ho.TimeRemaining, - &ho.TotalTime, - &ho.Complete, - &ho.ShiftUsed, - &ho.StarterProgress, - &ho.CompletedBy, - &ho.SpellName, - &ho.SpellDescription, - &ho.Countered[0], - &ho.Countered[1], - &ho.Countered[2], - &ho.Countered[3], - &ho.Countered[4], - &ho.Countered[5], - ) - - if err != nil { - return nil, fmt.Errorf("failed to load heroic op instance: %w", err) - } - - // Parse time fields - if startTimeStr != "" { - ho.StartTime, err = time.Parse(time.RFC3339, startTimeStr) - if err != nil { - return nil, fmt.Errorf("failed to parse start time: %w", err) - } - } - - if wheelStartTimeStr != "" { - ho.WheelStartTime, err = time.Parse(time.RFC3339, wheelStartTimeStr) - if err != nil { - return nil, fmt.Errorf("failed to parse wheel start time: %w", err) - } - } - - // Load participants - ho.Participants = make(map[int32]bool) - participantQuery := "SELECT character_id FROM heroic_op_participants WHERE instance_id = ?" - rows, err := db.Query(participantQuery, instanceID) - if err != nil { - return nil, fmt.Errorf("failed to load participants: %w", err) - } - defer rows.Close() - - for rows.Next() { - var characterID int32 - if err := rows.Scan(&characterID); err != nil { - return nil, fmt.Errorf("failed to scan participant: %w", err) - } - ho.Participants[characterID] = true - } - - // Load current starters - ho.CurrentStarters = make([]int32, 0) - starterQuery := "SELECT starter_id FROM heroic_op_current_starters WHERE instance_id = ?" - starterRows, err := db.Query(starterQuery, instanceID) - if err != nil { - return nil, fmt.Errorf("failed to load current starters: %w", err) - } - defer starterRows.Close() - - for starterRows.Next() { - var starterID int32 - if err := starterRows.Scan(&starterID); err != nil { - return nil, fmt.Errorf("failed to scan current starter: %w", err) - } - ho.CurrentStarters = append(ho.CurrentStarters, starterID) - } - - ho.SaveNeeded = false - return ho, nil -} - -// GetID returns the instance ID -func (ho *HeroicOP) GetID() int64 { - ho.mu.RLock() - defer ho.mu.RUnlock() - return ho.ID -} - -// Save persists the heroic op instance to the database -func (ho *HeroicOP) Save() error { - ho.mu.Lock() - defer ho.mu.Unlock() - - if !ho.SaveNeeded { - return nil - } - - 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, shift_used, starter_progress, completed_by, spell_name, spell_description, - countered1, countered2, countered3, countered4, countered5, countered6) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - - _, err := ho.db.Exec(query, - ho.ID, - ho.EncounterID, - ho.StarterID, - ho.WheelID, - ho.State, - ho.StartTime.Format(time.RFC3339), - ho.WheelStartTime.Format(time.RFC3339), - ho.TimeRemaining, - ho.TotalTime, - ho.Complete, - ho.ShiftUsed, - ho.StarterProgress, - ho.CompletedBy, - ho.SpellName, - ho.SpellDescription, - ho.Countered[0], - ho.Countered[1], - ho.Countered[2], - ho.Countered[3], - ho.Countered[4], - ho.Countered[5], - ) - if err != nil { - return fmt.Errorf("failed to insert heroic op instance: %w", 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 = ?, shift_used = ?, starter_progress = ?, completed_by = ?, - spell_name = ?, spell_description = ?, countered1 = ?, countered2 = ?, countered3 = ?, countered4 = ?, - countered5 = ?, countered6 = ? WHERE id = ?` - - _, err := ho.db.Exec(query, - ho.EncounterID, - ho.StarterID, - ho.WheelID, - ho.State, - ho.StartTime.Format(time.RFC3339), - ho.WheelStartTime.Format(time.RFC3339), - ho.TimeRemaining, - ho.TotalTime, - ho.Complete, - ho.ShiftUsed, - ho.StarterProgress, - ho.CompletedBy, - ho.SpellName, - ho.SpellDescription, - ho.Countered[0], - ho.Countered[1], - ho.Countered[2], - ho.Countered[3], - ho.Countered[4], - ho.Countered[5], - ho.ID, - ) - if err != nil { - return fmt.Errorf("failed to update heroic op instance: %w", err) - } - } - - // Save participants - if err := ho.saveParticipants(); err != nil { - return fmt.Errorf("failed to save participants: %w", err) - } - - // Save current starters - if err := ho.saveCurrentStarters(); err != nil { - return fmt.Errorf("failed to save current starters: %w", err) - } - - ho.SaveNeeded = false - return nil -} - -// saveParticipants saves the participants to the database (internal helper) -func (ho *HeroicOP) saveParticipants() error { - // Delete existing participants - deleteQuery := "DELETE FROM heroic_op_participants WHERE instance_id = ?" - _, err := ho.db.Exec(deleteQuery, ho.ID) - if err != nil { - return err - } - - // Insert current participants - for characterID := range ho.Participants { - insertQuery := "INSERT INTO heroic_op_participants (instance_id, character_id) VALUES (?, ?)" - _, err := ho.db.Exec(insertQuery, ho.ID, characterID) - if err != nil { - return err - } - } - - return nil -} - -// saveCurrentStarters saves the current starters to the database (internal helper) -func (ho *HeroicOP) saveCurrentStarters() error { - // Delete existing current starters - deleteQuery := "DELETE FROM heroic_op_current_starters WHERE instance_id = ?" - _, err := ho.db.Exec(deleteQuery, ho.ID) - if err != nil { - return err - } - - // Insert current starters - for _, starterID := range ho.CurrentStarters { - insertQuery := "INSERT INTO heroic_op_current_starters (instance_id, starter_id) VALUES (?, ?)" - _, err := ho.db.Exec(insertQuery, ho.ID, starterID) - if err != nil { - return err - } - } - - return nil -} - -// Delete removes the heroic op instance from the database -func (ho *HeroicOP) Delete() error { - ho.mu.Lock() - defer ho.mu.Unlock() - - if ho.isNew { - return nil // Nothing to delete - } - - // Delete related records first (foreign key constraints) - deleteParticipants := "DELETE FROM heroic_op_participants WHERE instance_id = ?" - _, err := ho.db.Exec(deleteParticipants, ho.ID) - if err != nil { - return fmt.Errorf("failed to delete participants: %w", err) - } - - deleteStarters := "DELETE FROM heroic_op_current_starters WHERE instance_id = ?" - _, err = ho.db.Exec(deleteStarters, ho.ID) - if err != nil { - return fmt.Errorf("failed to delete current starters: %w", err) - } - - // Delete main record - query := "DELETE FROM heroic_op_instances WHERE id = ?" - _, err = ho.db.Exec(query, ho.ID) - if err != nil { - return fmt.Errorf("failed to delete heroic op instance: %w", err) - } - - return nil -} - -// Reload refreshes the heroic op instance data from the database -func (ho *HeroicOP) Reload() error { - ho.mu.Lock() - defer ho.mu.Unlock() - - if ho.isNew { - return fmt.Errorf("cannot reload unsaved heroic op instance") - } - - // Reload from database - reloaded, err := LoadHeroicOP(ho.db, ho.ID) - if err != nil { - return err - } - - // Copy all fields except database connection and isNew flag - ho.EncounterID = reloaded.EncounterID - ho.StarterID = reloaded.StarterID - ho.WheelID = reloaded.WheelID - ho.State = reloaded.State - ho.StartTime = reloaded.StartTime - ho.WheelStartTime = reloaded.WheelStartTime - ho.TimeRemaining = reloaded.TimeRemaining - ho.TotalTime = reloaded.TotalTime - ho.Complete = reloaded.Complete - ho.Countered = reloaded.Countered - ho.ShiftUsed = reloaded.ShiftUsed - ho.StarterProgress = reloaded.StarterProgress - ho.Participants = reloaded.Participants - ho.CurrentStarters = reloaded.CurrentStarters - ho.CompletedBy = reloaded.CompletedBy - ho.SpellName = reloaded.SpellName - ho.SpellDescription = reloaded.SpellDescription - - ho.SaveNeeded = false - return nil -} - -// AddParticipant adds a character to the HO participants -func (ho *HeroicOP) AddParticipant(characterID int32) { - ho.mu.Lock() - defer ho.mu.Unlock() - - ho.Participants[characterID] = true - ho.SaveNeeded = true -} - -// RemoveParticipant removes a character from the HO participants -func (ho *HeroicOP) RemoveParticipant(characterID int32) { - ho.mu.Lock() - defer ho.mu.Unlock() - - delete(ho.Participants, characterID) - ho.SaveNeeded = true -} - -// IsParticipant checks if a character is participating in this HO -func (ho *HeroicOP) IsParticipant(characterID int32) bool { - ho.mu.RLock() - defer ho.mu.RUnlock() - - return ho.Participants[characterID] -} - -// GetParticipants returns a slice 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 -} - -// Copy creates a deep copy of the HO instance -func (ho *HeroicOP) Copy() *HeroicOP { - ho.mu.RLock() - defer ho.mu.RUnlock() - - newHO := &HeroicOP{ - ID: ho.ID, - EncounterID: ho.EncounterID, - StarterID: ho.StarterID, - WheelID: ho.WheelID, - State: ho.State, - StartTime: ho.StartTime, - WheelStartTime: ho.WheelStartTime, - TimeRemaining: ho.TimeRemaining, - TotalTime: ho.TotalTime, - Complete: ho.Complete, - Countered: ho.Countered, // Arrays are copied by value - ShiftUsed: ho.ShiftUsed, - StarterProgress: ho.StarterProgress, - CompletedBy: ho.CompletedBy, - SpellName: ho.SpellName, - SpellDescription: ho.SpellDescription, - Participants: make(map[int32]bool, len(ho.Participants)), - CurrentStarters: make([]int32, len(ho.CurrentStarters)), - db: ho.db, - isNew: true, // Copy is always new unless explicitly saved - SaveNeeded: false, - } - - // Deep copy participants map - for characterID, participating := range ho.Participants { - newHO.Participants[characterID] = participating - } - - // Deep copy current starters slice - copy(newHO.CurrentStarters, ho.CurrentStarters) - - return newHO -} - -// StartStarterChain initiates the starter chain phase -func (ho *HeroicOP) StartStarterChain(availableStarters []int32) { - ho.mu.Lock() - defer ho.mu.Unlock() - - ho.State = HOStateStarterChain - ho.CurrentStarters = make([]int32, len(availableStarters)) - copy(ho.CurrentStarters, availableStarters) - ho.StarterProgress = 0 - ho.StartTime = time.Now() - ho.SaveNeeded = true -} - -// ProcessStarterAbility processes an ability during starter chain phase -func (ho *HeroicOP) ProcessStarterAbility(abilityIcon int16, masterList *MasterList) bool { - ho.mu.Lock() - defer ho.mu.Unlock() - - if ho.State != HOStateStarterChain { - return false - } - - // Filter out starters that don't match this ability at current position - newStarters := make([]int32, 0) - - for _, starterID := range ho.CurrentStarters { - starter := masterList.GetStarter(starterID) - if starter != nil && starter.MatchesAbility(int(ho.StarterProgress), abilityIcon) { - // Check if this completes the starter - if starter.IsComplete(int(ho.StarterProgress)) { - // Starter completed, transition to wheel phase - ho.StarterID = starterID - ho.SaveNeeded = true - return true - } - newStarters = append(newStarters, starterID) - } - } - - ho.CurrentStarters = newStarters - ho.StarterProgress++ - ho.SaveNeeded = true - - // If no starters remain, HO fails - return len(ho.CurrentStarters) > 0 -} - -// StartWheelPhase initiates the wheel phase -func (ho *HeroicOP) StartWheelPhase(wheel *HeroicOPWheel, timerSeconds int32) { - ho.mu.Lock() - defer ho.mu.Unlock() - - ho.State = HOStateWheelPhase - ho.WheelID = wheel.ID - ho.WheelStartTime = time.Now() - ho.TotalTime = timerSeconds * 1000 // Convert to milliseconds - ho.TimeRemaining = ho.TotalTime - ho.SpellName = wheel.Name - ho.SpellDescription = wheel.Description - - // Clear countered array - for i := range ho.Countered { - ho.Countered[i] = 0 - } - - ho.SaveNeeded = true -} - -// 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() - - if ho.State != HOStateWheelPhase { - return false - } - - // Check for shift attempt - if ho.ShiftUsed == ShiftNotUsed && wheel.CanShift(abilityIcon) { - // Allow shift only if no progress made (unordered) or at start (ordered) - canShift := false - if wheel.IsOrdered() { - // For ordered, can shift only if no abilities completed - canShift = true - for i := 0; i < MaxAbilities; i++ { - if ho.Countered[i] != 0 { - canShift = false - break - } - } - } else { - // For unordered, can shift only if no abilities completed - canShift = true - for i := 0; i < MaxAbilities; i++ { - if ho.Countered[i] != 0 { - canShift = false - break - } - } - } - - if canShift { - ho.ShiftUsed = ShiftUsed - ho.SaveNeeded = true - return true // Caller should handle wheel shifting - } - return false - } - - // Check if ability can be used - if !wheel.CanUseAbility(abilityIcon, ho.Countered) { - return false - } - - // Find matching ability position and mark as countered - for i := 0; i < MaxAbilities; i++ { - if ho.Countered[i] == 0 && wheel.GetAbility(i) == abilityIcon { - ho.Countered[i] = 1 - ho.AddParticipant(characterID) - ho.SaveNeeded = true - - // Check if wheel is complete - complete := true - for j := 0; j < MaxAbilities; j++ { - if wheel.GetAbility(j) != AbilityIconNone && ho.Countered[j] == 0 { - complete = false - break - } - } - - if complete { - ho.Complete = HOComplete - ho.State = HOStateComplete - ho.CompletedBy = characterID - } - - return true - } - } - - return false -} - -// UpdateTimer updates the remaining time for the HO -func (ho *HeroicOP) UpdateTimer(deltaMS int32) bool { - ho.mu.Lock() - defer ho.mu.Unlock() - - if ho.State != HOStateWheelPhase { - return true // Timer not active - } - - ho.TimeRemaining -= deltaMS - - if ho.TimeRemaining <= 0 { - ho.TimeRemaining = 0 - ho.State = HOStateFailed - ho.SaveNeeded = true - return false // Timer expired - } - - ho.SaveNeeded = true - return true -} - -// IsComplete checks if the HO is successfully completed -func (ho *HeroicOP) IsComplete() bool { - ho.mu.RLock() - defer ho.mu.RUnlock() - - return ho.Complete == HOComplete && ho.State == HOStateComplete -} - -// IsFailed checks if the HO has failed -func (ho *HeroicOP) IsFailed() bool { - ho.mu.RLock() - defer ho.mu.RUnlock() - - return ho.State == HOStateFailed -} - -// IsActive checks if the HO is currently active (in progress) -func (ho *HeroicOP) IsActive() bool { - ho.mu.RLock() - defer ho.mu.RUnlock() - - return ho.State == HOStateStarterChain || ho.State == HOStateWheelPhase -} - -// GetProgress returns the completion percentage (0.0 - 1.0) -func (ho *HeroicOP) GetProgress() float32 { - ho.mu.RLock() - defer ho.mu.RUnlock() - - if ho.State != HOStateWheelPhase { - return 0.0 - } - - completed := 0 - total := 0 - - for i := 0; i < MaxAbilities; i++ { - if ho.Countered[i] != 0 { - completed++ - } - if ho.Countered[i] != 0 || ho.Countered[i] == 0 { // All positions count - total++ - } - } - - if total == 0 { - return 0.0 - } - - return float32(completed) / float32(total) -} - -// GetPacketData returns data formatted for client packets -func (ho *HeroicOP) GetPacketData(wheel *HeroicOPWheel) *PacketData { - ho.mu.RLock() - defer ho.mu.RUnlock() - - data := &PacketData{ - SpellName: ho.SpellName, - SpellDescription: ho.SpellDescription, - TimeRemaining: ho.TimeRemaining, - TotalTime: ho.TotalTime, - Complete: ho.Complete, - State: ho.State, - CanShift: false, - ShiftIcon: 0, - } - - if wheel != nil { - data.Abilities = wheel.Abilities - data.CanShift = ho.ShiftUsed == ShiftNotUsed && wheel.HasShift() - data.ShiftIcon = wheel.ShiftIcon - } - - data.Countered = ho.Countered - - return data -} - -// Validate checks if the HO instance is in a valid state -func (ho *HeroicOP) Validate() error { - ho.mu.RLock() - defer ho.mu.RUnlock() - - if ho.ID <= 0 { - return fmt.Errorf("invalid HO instance ID: %d", ho.ID) - } - - if ho.EncounterID <= 0 { - return fmt.Errorf("invalid encounter ID: %d", ho.EncounterID) - } - - if ho.State < HOStateInactive || ho.State > HOStateFailed { - return fmt.Errorf("invalid HO state: %d", ho.State) - } - - if ho.State == HOStateWheelPhase { - if ho.WheelID <= 0 { - return fmt.Errorf("wheel phase requires valid wheel ID") - } - - if ho.TotalTime <= 0 { - return fmt.Errorf("wheel phase requires valid timer") - } - } - - return nil -} - -// GetElapsedTime returns the elapsed time since HO started -func (ho *HeroicOP) GetElapsedTime() time.Duration { - ho.mu.RLock() - defer ho.mu.RUnlock() - - return time.Since(ho.StartTime) -} - -// GetWheelElapsedTime returns the elapsed time since wheel phase started -func (ho *HeroicOP) GetWheelElapsedTime() time.Duration { - ho.mu.RLock() - defer ho.mu.RUnlock() - - if ho.State != HOStateWheelPhase { - return 0 - } - - return time.Since(ho.WheelStartTime) -} \ No newline at end of file diff --git a/internal/heroic_ops/heroic_op_starter.go b/internal/heroic_ops/heroic_op_starter.go deleted file mode 100644 index cca0895..0000000 --- a/internal/heroic_ops/heroic_op_starter.go +++ /dev/null @@ -1,313 +0,0 @@ -package heroic_ops - -import ( - "fmt" - - "eq2emu/internal/database" -) - -// NewHeroicOPStarter creates a new heroic opportunity starter -func NewHeroicOPStarter(db *database.Database) *HeroicOPStarter { - return &HeroicOPStarter{ - db: db, - isNew: true, - Abilities: [6]int16{}, - SaveNeeded: false, - } -} - -// LoadHeroicOPStarter loads a heroic opportunity starter by ID -func LoadHeroicOPStarter(db *database.Database, id int32) (*HeroicOPStarter, error) { - starter := &HeroicOPStarter{ - db: db, - isNew: false, - } - - query := "SELECT id, start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description FROM heroic_op_starters WHERE id = ?" - err := db.QueryRow(query, id).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 nil, fmt.Errorf("failed to load heroic op starter: %w", err) - } - - starter.SaveNeeded = false - return starter, nil -} - -// GetID returns the starter ID -func (hos *HeroicOPStarter) GetID() int32 { - hos.mu.RLock() - defer hos.mu.RUnlock() - return hos.ID -} - -// Save persists the starter to the database -func (hos *HeroicOPStarter) Save() error { - hos.mu.Lock() - defer hos.mu.Unlock() - - if !hos.SaveNeeded { - return nil - } - - if hos.isNew { - // Insert new record - query := `INSERT INTO heroic_op_starters (id, start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - _, err := hos.db.Exec(query, - hos.ID, - hos.StartClass, - hos.StarterIcon, - hos.Abilities[0], - hos.Abilities[1], - hos.Abilities[2], - hos.Abilities[3], - hos.Abilities[4], - hos.Abilities[5], - hos.Name, - hos.Description, - ) - if err != nil { - return fmt.Errorf("failed to insert heroic op starter: %w", err) - } - hos.isNew = false - } else { - // Update existing record - query := `UPDATE heroic_op_starters SET start_class = ?, starter_icon = ?, ability1 = ?, ability2 = ?, ability3 = ?, ability4 = ?, ability5 = ?, ability6 = ?, name = ?, description = ? WHERE id = ?` - _, err := hos.db.Exec(query, - hos.StartClass, - hos.StarterIcon, - hos.Abilities[0], - hos.Abilities[1], - hos.Abilities[2], - hos.Abilities[3], - hos.Abilities[4], - hos.Abilities[5], - hos.Name, - hos.Description, - hos.ID, - ) - if err != nil { - return fmt.Errorf("failed to update heroic op starter: %w", err) - } - } - - hos.SaveNeeded = false - return nil -} - -// Delete removes the starter from the database -func (hos *HeroicOPStarter) Delete() error { - hos.mu.Lock() - defer hos.mu.Unlock() - - if hos.isNew { - return nil // Nothing to delete - } - - query := "DELETE FROM heroic_op_starters WHERE id = ?" - _, err := hos.db.Exec(query, hos.ID) - if err != nil { - return fmt.Errorf("failed to delete heroic op starter: %w", err) - } - - return nil -} - -// Reload refreshes the starter data from the database -func (hos *HeroicOPStarter) Reload() error { - hos.mu.Lock() - defer hos.mu.Unlock() - - if hos.isNew { - return fmt.Errorf("cannot reload unsaved heroic op starter") - } - - query := "SELECT start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description FROM heroic_op_starters WHERE id = ?" - err := hos.db.QueryRow(query, hos.ID).Scan( - &hos.StartClass, - &hos.StarterIcon, - &hos.Abilities[0], - &hos.Abilities[1], - &hos.Abilities[2], - &hos.Abilities[3], - &hos.Abilities[4], - &hos.Abilities[5], - &hos.Name, - &hos.Description, - ) - - if err != nil { - return fmt.Errorf("failed to reload heroic op starter: %w", err) - } - - hos.SaveNeeded = false - return nil -} - -// Copy creates a deep copy of the starter -func (hos *HeroicOPStarter) Copy() *HeroicOPStarter { - hos.mu.RLock() - defer hos.mu.RUnlock() - - newStarter := &HeroicOPStarter{ - ID: hos.ID, - StartClass: hos.StartClass, - StarterIcon: hos.StarterIcon, - Abilities: hos.Abilities, // Arrays are copied by value - Name: hos.Name, - Description: hos.Description, - db: hos.db, - isNew: true, // Copy is always new unless explicitly saved - SaveNeeded: false, - } - - return newStarter -} - -// GetAbility returns the ability icon at the specified position -func (hos *HeroicOPStarter) GetAbility(position int) int16 { - hos.mu.RLock() - defer hos.mu.RUnlock() - - if position < 0 || position >= MaxAbilities { - return AbilityIconNone - } - - return hos.Abilities[position] -} - -// SetAbility sets the ability icon at the specified position -func (hos *HeroicOPStarter) SetAbility(position int, abilityIcon int16) bool { - hos.mu.Lock() - defer hos.mu.Unlock() - - if position < 0 || position >= MaxAbilities { - return false - } - - hos.Abilities[position] = abilityIcon - hos.SaveNeeded = true - return true -} - -// IsComplete checks if the starter chain is complete (has completion marker) -func (hos *HeroicOPStarter) IsComplete(position int) bool { - hos.mu.RLock() - defer hos.mu.RUnlock() - - if position < 0 || position >= MaxAbilities { - return false - } - - return hos.Abilities[position] == AbilityIconAny -} - -// CanInitiate checks if the specified class can initiate this starter -func (hos *HeroicOPStarter) CanInitiate(playerClass int8) bool { - hos.mu.RLock() - defer hos.mu.RUnlock() - - return hos.StartClass == ClassAny || hos.StartClass == playerClass -} - -// MatchesAbility checks if the given ability matches the current position -func (hos *HeroicOPStarter) MatchesAbility(position int, abilityIcon int16) bool { - hos.mu.RLock() - defer hos.mu.RUnlock() - - if position < 0 || position >= MaxAbilities { - return false - } - - requiredAbility := hos.Abilities[position] - - // Wildcard matches any ability - if requiredAbility == AbilityIconAny { - return true - } - - // Exact match required - return requiredAbility == abilityIcon -} - -// Validate checks if the starter is properly configured -func (hos *HeroicOPStarter) Validate() error { - hos.mu.RLock() - defer hos.mu.RUnlock() - - if hos.ID <= 0 { - return fmt.Errorf("invalid starter ID: %d", hos.ID) - } - - if hos.StarterIcon <= 0 { - return fmt.Errorf("invalid starter icon: %d", hos.StarterIcon) - } - - // Check for at least one non-zero ability - hasAbility := false - for _, ability := range hos.Abilities { - if ability != AbilityIconNone { - hasAbility = true - break - } - } - - if !hasAbility { - return fmt.Errorf("starter must have at least one ability") - } - - return nil -} - -// SetID sets the starter ID -func (hos *HeroicOPStarter) SetID(id int32) { - hos.mu.Lock() - defer hos.mu.Unlock() - hos.ID = id - hos.SaveNeeded = true -} - -// SetStartClass sets the start class -func (hos *HeroicOPStarter) SetStartClass(startClass int8) { - hos.mu.Lock() - defer hos.mu.Unlock() - hos.StartClass = startClass - hos.SaveNeeded = true -} - -// SetStarterIcon sets the starter icon -func (hos *HeroicOPStarter) SetStarterIcon(icon int16) { - hos.mu.Lock() - defer hos.mu.Unlock() - hos.StarterIcon = icon - hos.SaveNeeded = true -} - -// SetName sets the starter name -func (hos *HeroicOPStarter) SetName(name string) { - hos.mu.Lock() - defer hos.mu.Unlock() - hos.Name = name - hos.SaveNeeded = true -} - -// SetDescription sets the starter description -func (hos *HeroicOPStarter) SetDescription(description string) { - hos.mu.Lock() - defer hos.mu.Unlock() - hos.Description = description - hos.SaveNeeded = true -} \ No newline at end of file diff --git a/internal/heroic_ops/heroic_op_wheel.go b/internal/heroic_ops/heroic_op_wheel.go deleted file mode 100644 index 0ce24ab..0000000 --- a/internal/heroic_ops/heroic_op_wheel.go +++ /dev/null @@ -1,405 +0,0 @@ -package heroic_ops - -import ( - "fmt" - - "eq2emu/internal/database" -) - -// NewHeroicOPWheel creates a new heroic opportunity wheel -func NewHeroicOPWheel(db *database.Database) *HeroicOPWheel { - return &HeroicOPWheel{ - db: db, - isNew: true, - Abilities: [6]int16{}, - Chance: 1.0, - SaveNeeded: false, - } -} - -// LoadHeroicOPWheel loads a heroic opportunity wheel by ID -func LoadHeroicOPWheel(db *database.Database, id int32) (*HeroicOPWheel, error) { - wheel := &HeroicOPWheel{ - db: db, - isNew: false, - } - - 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 WHERE id = ?` - err := db.QueryRow(query, id).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 nil, fmt.Errorf("failed to load heroic op wheel: %w", err) - } - - wheel.SaveNeeded = false - return wheel, nil -} - -// GetID returns the wheel ID -func (how *HeroicOPWheel) GetID() int32 { - how.mu.RLock() - defer how.mu.RUnlock() - return how.ID -} - -// Save persists the wheel to the database -func (how *HeroicOPWheel) Save() error { - how.mu.Lock() - defer how.mu.Unlock() - - if !how.SaveNeeded { - return nil - } - - if how.isNew { - // Insert new record - query := `INSERT INTO heroic_op_wheels (id, starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6, spell_id, name, description, required_players) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - _, err := how.db.Exec(query, - how.ID, - how.StarterLinkID, - how.Order, - how.ShiftIcon, - how.Chance, - how.Abilities[0], - how.Abilities[1], - how.Abilities[2], - how.Abilities[3], - how.Abilities[4], - how.Abilities[5], - how.SpellID, - how.Name, - how.Description, - how.RequiredPlayers, - ) - if err != nil { - return fmt.Errorf("failed to insert heroic op wheel: %w", err) - } - how.isNew = false - } else { - // Update existing record - query := `UPDATE heroic_op_wheels SET starter_link_id = ?, chain_order = ?, shift_icon = ?, chance = ?, ability1 = ?, ability2 = ?, ability3 = ?, ability4 = ?, ability5 = ?, ability6 = ?, spell_id = ?, name = ?, description = ?, required_players = ? WHERE id = ?` - _, err := how.db.Exec(query, - how.StarterLinkID, - how.Order, - how.ShiftIcon, - how.Chance, - how.Abilities[0], - how.Abilities[1], - how.Abilities[2], - how.Abilities[3], - how.Abilities[4], - how.Abilities[5], - how.SpellID, - how.Name, - how.Description, - how.RequiredPlayers, - how.ID, - ) - if err != nil { - return fmt.Errorf("failed to update heroic op wheel: %w", err) - } - } - - how.SaveNeeded = false - return nil -} - -// Delete removes the wheel from the database -func (how *HeroicOPWheel) Delete() error { - how.mu.Lock() - defer how.mu.Unlock() - - if how.isNew { - return nil // Nothing to delete - } - - query := "DELETE FROM heroic_op_wheels WHERE id = ?" - _, err := how.db.Exec(query, how.ID) - if err != nil { - return fmt.Errorf("failed to delete heroic op wheel: %w", err) - } - - return nil -} - -// Reload refreshes the wheel data from the database -func (how *HeroicOPWheel) Reload() error { - how.mu.Lock() - defer how.mu.Unlock() - - if how.isNew { - return fmt.Errorf("cannot reload unsaved heroic op wheel") - } - - query := `SELECT starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6, - spell_id, name, description, required_players FROM heroic_op_wheels WHERE id = ?` - err := how.db.QueryRow(query, how.ID).Scan( - &how.StarterLinkID, - &how.Order, - &how.ShiftIcon, - &how.Chance, - &how.Abilities[0], - &how.Abilities[1], - &how.Abilities[2], - &how.Abilities[3], - &how.Abilities[4], - &how.Abilities[5], - &how.SpellID, - &how.Name, - &how.Description, - &how.RequiredPlayers, - ) - - if err != nil { - return fmt.Errorf("failed to reload heroic op wheel: %w", err) - } - - how.SaveNeeded = false - return nil -} - -// Copy creates a deep copy of the wheel -func (how *HeroicOPWheel) Copy() *HeroicOPWheel { - how.mu.RLock() - defer how.mu.RUnlock() - - newWheel := &HeroicOPWheel{ - ID: how.ID, - StarterLinkID: how.StarterLinkID, - Order: how.Order, - ShiftIcon: how.ShiftIcon, - Chance: how.Chance, - Abilities: how.Abilities, // Arrays are copied by value - SpellID: how.SpellID, - Name: how.Name, - Description: how.Description, - RequiredPlayers: how.RequiredPlayers, - db: how.db, - isNew: true, // Copy is always new unless explicitly saved - SaveNeeded: false, - } - - return newWheel -} - -// GetAbility returns the ability icon at the specified position -func (how *HeroicOPWheel) GetAbility(position int) int16 { - how.mu.RLock() - defer how.mu.RUnlock() - - if position < 0 || position >= MaxAbilities { - return AbilityIconNone - } - - return how.Abilities[position] -} - -// SetAbility sets the ability icon at the specified position -func (how *HeroicOPWheel) SetAbility(position int, abilityIcon int16) bool { - how.mu.Lock() - defer how.mu.Unlock() - - if position < 0 || position >= MaxAbilities { - return false - } - - how.Abilities[position] = abilityIcon - how.SaveNeeded = true - return true -} - -// IsOrdered checks if this wheel requires ordered completion -func (how *HeroicOPWheel) IsOrdered() bool { - how.mu.RLock() - defer how.mu.RUnlock() - - return how.Order >= WheelOrderOrdered -} - -// HasShift checks if this wheel has a shift ability -func (how *HeroicOPWheel) HasShift() bool { - how.mu.RLock() - defer how.mu.RUnlock() - - return how.ShiftIcon > 0 -} - -// CanShift checks if shifting is possible with the given ability -func (how *HeroicOPWheel) CanShift(abilityIcon int16) bool { - how.mu.RLock() - defer how.mu.RUnlock() - - return how.ShiftIcon > 0 && how.ShiftIcon == abilityIcon -} - -// GetNextRequiredAbility returns the next required ability for ordered wheels -func (how *HeroicOPWheel) GetNextRequiredAbility(countered [6]int8) int16 { - how.mu.RLock() - defer how.mu.RUnlock() - - if !how.IsOrdered() { - return AbilityIconNone // Any uncompleted ability works for unordered - } - - // Find first uncompleted ability in order - for i := 0; i < MaxAbilities; i++ { - if countered[i] == 0 && how.Abilities[i] != AbilityIconNone { - return how.Abilities[i] - } - } - - return AbilityIconNone -} - -// CanUseAbility checks if an ability can be used on this wheel -func (how *HeroicOPWheel) CanUseAbility(abilityIcon int16, countered [6]int8) bool { - how.mu.RLock() - defer how.mu.RUnlock() - - // Check if this is a shift attempt - if how.CanShift(abilityIcon) { - return true - } - - if how.IsOrdered() { - // For ordered wheels, only the next required ability can be used - nextRequired := how.GetNextRequiredAbility(countered) - return nextRequired == abilityIcon - } else { - // For unordered wheels, any uncompleted matching ability can be used - for i := 0; i < MaxAbilities; i++ { - if countered[i] == 0 && how.Abilities[i] == abilityIcon { - return true - } - } - } - - return false -} - -// Validate checks if the wheel is properly configured -func (how *HeroicOPWheel) Validate() error { - how.mu.RLock() - defer how.mu.RUnlock() - - if how.ID <= 0 { - return fmt.Errorf("invalid wheel ID: %d", how.ID) - } - - if how.StarterLinkID <= 0 { - return fmt.Errorf("invalid starter link ID: %d", how.StarterLinkID) - } - - if how.Chance < MinChance || how.Chance > MaxChance { - return fmt.Errorf("invalid chance: %f (must be %f-%f)", how.Chance, MinChance, MaxChance) - } - - if how.SpellID <= 0 { - return fmt.Errorf("invalid spell ID: %d", how.SpellID) - } - - // Check for at least one non-zero ability - hasAbility := false - for _, ability := range how.Abilities { - if ability != AbilityIconNone { - hasAbility = true - break - } - } - - if !hasAbility { - return fmt.Errorf("wheel must have at least one ability") - } - - return nil -} - -// SetID sets the wheel ID -func (how *HeroicOPWheel) SetID(id int32) { - how.mu.Lock() - defer how.mu.Unlock() - how.ID = id - how.SaveNeeded = true -} - -// SetStarterLinkID sets the starter link ID -func (how *HeroicOPWheel) SetStarterLinkID(id int32) { - how.mu.Lock() - defer how.mu.Unlock() - how.StarterLinkID = id - how.SaveNeeded = true -} - -// SetOrder sets the wheel order -func (how *HeroicOPWheel) SetOrder(order int8) { - how.mu.Lock() - defer how.mu.Unlock() - how.Order = order - how.SaveNeeded = true -} - -// SetShiftIcon sets the shift icon -func (how *HeroicOPWheel) SetShiftIcon(icon int16) { - how.mu.Lock() - defer how.mu.Unlock() - how.ShiftIcon = icon - how.SaveNeeded = true -} - -// SetChance sets the wheel chance -func (how *HeroicOPWheel) SetChance(chance float32) { - how.mu.Lock() - defer how.mu.Unlock() - how.Chance = chance - how.SaveNeeded = true -} - -// SetSpellID sets the spell ID -func (how *HeroicOPWheel) SetSpellID(id int32) { - how.mu.Lock() - defer how.mu.Unlock() - how.SpellID = id - how.SaveNeeded = true -} - -// SetName sets the wheel name -func (how *HeroicOPWheel) SetName(name string) { - how.mu.Lock() - defer how.mu.Unlock() - how.Name = name - how.SaveNeeded = true -} - -// SetDescription sets the wheel description -func (how *HeroicOPWheel) SetDescription(description string) { - how.mu.Lock() - defer how.mu.Unlock() - how.Description = description - how.SaveNeeded = true -} - -// SetRequiredPlayers sets the required players -func (how *HeroicOPWheel) SetRequiredPlayers(required int8) { - how.mu.Lock() - defer how.mu.Unlock() - how.RequiredPlayers = required - how.SaveNeeded = true -} \ No newline at end of file diff --git a/internal/heroic_ops/heroic_ops.go b/internal/heroic_ops/heroic_ops.go new file mode 100644 index 0000000..ffb1425 --- /dev/null +++ b/internal/heroic_ops/heroic_ops.go @@ -0,0 +1,1390 @@ +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 +} \ No newline at end of file diff --git a/internal/heroic_ops/interfaces.go b/internal/heroic_ops/interfaces.go deleted file mode 100644 index b7699f4..0000000 --- a/internal/heroic_ops/interfaces.go +++ /dev/null @@ -1,218 +0,0 @@ -package heroic_ops - -import ( - "context" - "time" -) - -// HeroicOPDatabase defines the interface for database operations -type HeroicOPDatabase interface { - // Starter operations - LoadStarters(ctx context.Context) ([]HeroicOPData, error) - LoadStarter(ctx context.Context, starterID int32) (*HeroicOPData, error) - SaveStarter(ctx context.Context, starter *HeroicOPStarter) error - DeleteStarter(ctx context.Context, starterID int32) error - - // Wheel operations - LoadWheels(ctx context.Context) ([]HeroicOPData, error) - LoadWheelsForStarter(ctx context.Context, starterID int32) ([]HeroicOPData, error) - LoadWheel(ctx context.Context, wheelID int32) (*HeroicOPData, error) - SaveWheel(ctx context.Context, wheel *HeroicOPWheel) error - DeleteWheel(ctx context.Context, wheelID int32) error - - // Instance operations - SaveHOInstance(ctx context.Context, ho *HeroicOP) error - LoadHOInstance(ctx context.Context, instanceID int64) (*HeroicOP, error) - DeleteHOInstance(ctx context.Context, instanceID int64) error - - // Statistics and events - SaveHOEvent(ctx context.Context, event *HeroicOPEvent) error - LoadHOEvents(ctx context.Context, instanceID int64) ([]HeroicOPEvent, error) - GetHOStatistics(ctx context.Context, characterID int32) (*HeroicOPStatistics, error) - - // Utility operations - GetNextStarterID(ctx context.Context) (int32, error) - GetNextWheelID(ctx context.Context) (int32, error) - GetNextInstanceID(ctx context.Context) (int64, error) - EnsureHOTables(ctx context.Context) error -} - -// HeroicOPEventHandler defines the interface for handling HO events -type HeroicOPEventHandler interface { - // HO lifecycle events - OnHOStarted(ho *HeroicOP, initiatorID int32) - OnHOCompleted(ho *HeroicOP, completedBy int32, spellID int32) - OnHOFailed(ho *HeroicOP, reason string) - OnHOTimerExpired(ho *HeroicOP) - - // Progress events - OnAbilityUsed(ho *HeroicOP, characterID int32, abilityIcon int16, success bool) - OnWheelShifted(ho *HeroicOP, characterID int32, newWheelID int32) - OnStarterMatched(ho *HeroicOP, starterID int32, characterID int32) - OnStarterEliminated(ho *HeroicOP, starterID int32, characterID int32) - - // Phase transitions - OnWheelPhaseStarted(ho *HeroicOP, wheelID int32, timeRemaining int32) - OnProgressMade(ho *HeroicOP, characterID int32, progressPercent float32) -} - -// SpellManager defines the interface for spell system integration -type SpellManager interface { - // Get spell information - GetSpellInfo(spellID int32) (*SpellInfo, error) - GetSpellName(spellID int32) string - GetSpellDescription(spellID int32) string - - // Cast spells - CastSpell(casterID int32, spellID int32, targets []int32) error - IsSpellValid(spellID int32) bool -} - -// ClientManager defines the interface for client communication -type ClientManager interface { - // Send HO packets to clients - SendHOUpdate(characterID int32, data *PacketData) error - SendHOStart(characterID int32, ho *HeroicOP) error - SendHOComplete(characterID int32, ho *HeroicOP, success bool) error - SendHOTimer(characterID int32, timeRemaining int32, totalTime int32) error - - // Broadcast to multiple clients - BroadcastHOUpdate(characterIDs []int32, data *PacketData) error - BroadcastHOEvent(characterIDs []int32, eventType int, data string) error - - // Client validation - IsClientConnected(characterID int32) bool - GetClientVersion(characterID int32) int -} - -// EncounterManager defines the interface for encounter system integration -type EncounterManager interface { - // Get encounter information - GetEncounterParticipants(encounterID int32) ([]int32, error) - IsEncounterActive(encounterID int32) bool - GetEncounterInfo(encounterID int32) (*EncounterInfo, error) - - // HO integration - CanStartHO(encounterID int32, initiatorID int32) bool - NotifyHOStarted(encounterID int32, instanceID int64) - NotifyHOCompleted(encounterID int32, instanceID int64, success bool) -} - -// PlayerManager defines the interface for player system integration -type PlayerManager interface { - // Get player information - GetPlayerInfo(characterID int32) (*PlayerInfo, error) - GetPlayerClass(characterID int32) (int8, error) - GetPlayerLevel(characterID int32) (int16, error) - IsPlayerOnline(characterID int32) bool - - // Player abilities - CanPlayerUseAbility(characterID int32, abilityIcon int16) bool - GetPlayerAbilities(characterID int32) ([]int16, error) - - // Player state - IsPlayerInCombat(characterID int32) bool - GetPlayerEncounter(characterID int32) (int32, error) -} - -// LogHandler defines the interface for logging operations -type LogHandler 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) -} - -// TimerManager defines the interface for timer management -type TimerManager interface { - // Timer operations - StartTimer(instanceID int64, duration time.Duration, callback func()) error - StopTimer(instanceID int64) error - UpdateTimer(instanceID int64, newDuration time.Duration) error - GetTimeRemaining(instanceID int64) (time.Duration, error) - - // Timer queries - IsTimerActive(instanceID int64) bool - GetActiveTimers() []int64 -} - -// CacheManager defines the interface for caching operations -type CacheManager interface { - // Cache operations - Set(key string, value any, expiration time.Duration) error - Get(key string) (any, bool) - Delete(key string) error - Clear() error - - // Cache statistics - GetHitRate() float64 - GetSize() int - GetCapacity() int -} - -// Additional integration interfaces - -// EncounterInfo contains encounter details -type EncounterInfo struct { - ID int32 `json:"id"` - Name string `json:"name"` - Participants []int32 `json:"participants"` - IsActive bool `json:"is_active"` - StartTime time.Time `json:"start_time"` - Level int16 `json:"level"` -} - -// PlayerInfo contains player details needed for HO system -type PlayerInfo struct { - CharacterID int32 `json:"character_id"` - CharacterName string `json:"character_name"` - AccountID int32 `json:"account_id"` - AdventureClass int8 `json:"adventure_class"` - AdventureLevel int16 `json:"adventure_level"` - Zone string `json:"zone"` - IsOnline bool `json:"is_online"` - InCombat bool `json:"in_combat"` - EncounterID int32 `json:"encounter_id"` -} - -// Adapter interfaces for integration with existing systems - -// HeroicOPAware defines interface for entities that can participate in HOs -type HeroicOPAware interface { - GetCharacterID() int32 - GetClass() int8 - GetLevel() int16 - CanParticipateInHO() bool - GetCurrentEncounter() int32 -} - -// EntityHOAdapter adapts entity system for HO integration -type EntityHOAdapter struct { - entity HeroicOPAware -} - -// PacketBuilder defines interface for building HO packets -type PacketBuilder interface { - BuildHOStartPacket(ho *HeroicOP) ([]byte, error) - BuildHOUpdatePacket(ho *HeroicOP) ([]byte, error) - BuildHOCompletePacket(ho *HeroicOP, success bool) ([]byte, error) - BuildHOTimerPacket(timeRemaining, totalTime int32) ([]byte, error) -} - -// StatisticsCollector defines interface for collecting HO statistics -type StatisticsCollector interface { - RecordHOStarted(instanceID int64, starterID int32, characterID int32) - RecordHOCompleted(instanceID int64, success bool, completionTime time.Duration) - RecordAbilityUsed(instanceID int64, characterID int32, abilityIcon int16) - RecordShiftUsed(instanceID int64, characterID int32) - GetStatistics() *HeroicOPStatistics - Reset() -} - -// ConfigManager defines interface for configuration management -type ConfigManager interface { - GetHOConfig() *HeroicOPConfig - UpdateHOConfig(config *HeroicOPConfig) error - GetConfigValue(key string) any - SetConfigValue(key string, value any) error -} diff --git a/internal/heroic_ops/manager.go b/internal/heroic_ops/manager.go deleted file mode 100644 index 5f34c03..0000000 --- a/internal/heroic_ops/manager.go +++ /dev/null @@ -1,588 +0,0 @@ -package heroic_ops - -import ( - "context" - "fmt" - "time" - - "eq2emu/internal/database" -) - -// NewHeroicOPManager creates a new heroic opportunity manager -func NewHeroicOPManager(masterList *MasterList, database HeroicOPDatabase, db *database.Database, - clientManager ClientManager, encounterManager EncounterManager, playerManager PlayerManager) *HeroicOPManager { - return &HeroicOPManager{ - activeHOs: make(map[int64]*HeroicOP), - encounterHOs: make(map[int32][]*HeroicOP), - masterList: masterList, - database: database, - db: db, - clientManager: clientManager, - encounterManager: encounterManager, - playerManager: playerManager, - 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() { - // The master list will need to be loaded externally with LoadFromDatabase(db) - return fmt.Errorf("master list must be loaded before initializing manager") - } - - if hom.logger != nil { - starterCount, wheelCount := hom.masterList.GetCount() - hom.logger.LogInfo("heroic_ops", "Initialized HO manager with %d starters and %d wheels", - starterCount, wheelCount) - } - - 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(hom.db, 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() - - // Use the master list's statistics and supplement with runtime data - masterStats := hom.masterList.GetStatistics() - - // Add runtime statistics - participationStats := make(map[int32]int64) - for _, ho := range hom.activeHOs { - for characterID := range ho.Participants { - participationStats[characterID]++ - } - } - - // Return extended statistics - return &HeroicOPStatistics{ - TotalStarters: masterStats.TotalStarters, - TotalWheels: masterStats.TotalWheels, - ClassDistribution: masterStats.ClassDistribution, - OrderedWheelsCount: masterStats.OrderedWheelsCount, - ShiftWheelsCount: masterStats.ShiftWheelsCount, - SpellCount: masterStats.SpellCount, - AverageChance: masterStats.AverageChance, - ActiveHOCount: int64(len(hom.activeHOs)), - // TODO: Get additional statistics from database - } -} - -// 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 { - _ = ho.GetParticipants() // participants will be used when spell manager is integrated - // 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() - data := ho.GetPacketData(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) - data := ho.GetPacketData(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() - - for _, characterID := range participants { - // TODO: Implement shift packet sending when client manager supports it - _ = characterID - _ = oldWheelID - _ = newWheelID - } -} - -// 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) - } - } -} diff --git a/internal/heroic_ops/master.go b/internal/heroic_ops/master.go deleted file mode 100644 index 4d0e903..0000000 --- a/internal/heroic_ops/master.go +++ /dev/null @@ -1,720 +0,0 @@ -package heroic_ops - -import ( - "fmt" - "sort" - "sync" - "strings" - - "eq2emu/internal/database" -) - -// MasterList provides optimized heroic opportunity management with O(1) performance characteristics -type MasterList struct { - mu sync.RWMutex - - // Core data storage - O(1) access by ID - starters map[int32]*HeroicOPStarter // starter_id -> starter - wheels map[int32]*HeroicOPWheel // wheel_id -> wheel - - // 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 - - // Lazy metadata caching - computed on demand - totalStarters int - totalWheels int - classDistribution map[int8]int - metadataValid bool - - loaded bool -} - -// NewMasterList creates a new bespoke heroic opportunity master list -func NewMasterList() *MasterList { - return &MasterList{ - starters: make(map[int32]*HeroicOPStarter), - wheels: make(map[int32]*HeroicOPWheel), - 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), - loaded: false, - } -} - -// LoadFromDatabase loads all heroic opportunities from the database with optimal indexing -func (ml *MasterList) LoadFromDatabase(db *database.Database) error { - ml.mu.Lock() - defer ml.mu.Unlock() - - // Clear existing data - ml.clearIndices() - - // Load all starters - if err := ml.loadStarters(db); err != nil { - return fmt.Errorf("failed to load starters: %w", err) - } - - // Load all wheels - if err := ml.loadWheels(db); err != nil { - return fmt.Errorf("failed to load wheels: %w", err) - } - - // Build specialized indices for O(1) performance - ml.buildIndices() - - ml.loaded = true - return nil -} - -// loadStarters loads all starters from database -func (ml *MasterList) loadStarters(db *database.Database) 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 := db.Query(query) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - starter := &HeroicOPStarter{ - db: db, - 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 - ml.starters[starter.ID] = starter - } - - return rows.Err() -} - -// loadWheels loads all wheels from database -func (ml *MasterList) loadWheels(db *database.Database) 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 := db.Query(query) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - wheel := &HeroicOPWheel{ - db: db, - 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 - ml.wheels[wheel.ID] = wheel - - // Store spell info - ml.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 (ml *MasterList) buildIndices() { - // Build class-based starter index - for _, starter := range ml.starters { - if ml.byClass[starter.StartClass] == nil { - ml.byClass[starter.StartClass] = make(map[int32]*HeroicOPStarter) - } - ml.byClass[starter.StartClass][starter.ID] = starter - } - - // Build wheel indices - for _, wheel := range ml.wheels { - // By starter ID - ml.byStarterID[wheel.StarterLinkID] = append(ml.byStarterID[wheel.StarterLinkID], wheel) - - // By spell ID - ml.bySpellID[wheel.SpellID] = append(ml.bySpellID[wheel.SpellID], wheel) - - // By chance range (for performance optimization) - chanceRange := ml.getChanceRange(wheel.Chance) - ml.byChance[chanceRange] = append(ml.byChance[chanceRange], wheel) - - // Special wheel types - if wheel.IsOrdered() { - ml.orderedWheels[wheel.ID] = wheel - } - - if wheel.HasShift() { - ml.shiftWheels[wheel.ID] = wheel - } - } - - // Sort wheels by chance for deterministic selection - for _, wheels := range ml.byStarterID { - sort.Slice(wheels, func(i, j int) bool { - return wheels[i].Chance > wheels[j].Chance - }) - } - - ml.metadataValid = false // Invalidate cached metadata -} - -// clearIndices clears all indices -func (ml *MasterList) clearIndices() { - ml.starters = make(map[int32]*HeroicOPStarter) - ml.wheels = make(map[int32]*HeroicOPWheel) - ml.byClass = make(map[int8]map[int32]*HeroicOPStarter) - ml.byStarterID = make(map[int32][]*HeroicOPWheel) - ml.bySpellID = make(map[int32][]*HeroicOPWheel) - ml.byChance = make(map[string][]*HeroicOPWheel) - ml.orderedWheels = make(map[int32]*HeroicOPWheel) - ml.shiftWheels = make(map[int32]*HeroicOPWheel) - ml.spellInfo = make(map[int32]SpellInfo) - ml.metadataValid = false -} - -// getChanceRange returns a chance range string for indexing -func (ml *MasterList) 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" - } -} - -// O(1) Starter Operations - -// GetStarter retrieves a starter by ID with O(1) performance -func (ml *MasterList) GetStarter(id int32) *HeroicOPStarter { - ml.mu.RLock() - defer ml.mu.RUnlock() - return ml.starters[id] -} - -// GetStartersForClass returns all starters for a specific class with O(1) performance -func (ml *MasterList) GetStartersForClass(class int8) []*HeroicOPStarter { - ml.mu.RLock() - defer ml.mu.RUnlock() - - var result []*HeroicOPStarter - - // Add class-specific starters - if classStarters, exists := ml.byClass[class]; exists { - for _, starter := range classStarters { - result = append(result, starter) - } - } - - // Add universal starters (class 0 = any) - if universalStarters, exists := ml.byClass[ClassAny]; exists { - for _, starter := range universalStarters { - result = append(result, starter) - } - } - - return result -} - -// O(1) Wheel Operations - -// GetWheel retrieves a wheel by ID with O(1) performance -func (ml *MasterList) GetWheel(id int32) *HeroicOPWheel { - ml.mu.RLock() - defer ml.mu.RUnlock() - return ml.wheels[id] -} - -// GetWheelsForStarter returns all wheels for a starter with O(1) performance -func (ml *MasterList) GetWheelsForStarter(starterID int32) []*HeroicOPWheel { - ml.mu.RLock() - defer ml.mu.RUnlock() - - wheels := ml.byStarterID[starterID] - if wheels == nil { - return nil - } - - // Return copy to prevent external modification - result := make([]*HeroicOPWheel, len(wheels)) - copy(result, wheels) - return result -} - -// GetWheelsForSpell returns all wheels that cast a specific spell with O(1) performance -func (ml *MasterList) GetWheelsForSpell(spellID int32) []*HeroicOPWheel { - ml.mu.RLock() - defer ml.mu.RUnlock() - - wheels := ml.bySpellID[spellID] - 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 (ml *MasterList) SelectRandomWheel(starterID int32) *HeroicOPWheel { - wheels := ml.GetWheelsForStarter(starterID) - if len(wheels) == 0 { - return nil - } - return SelectRandomWheel(wheels) -} - -// O(1) Specialized Queries - -// GetOrderedWheels returns all ordered wheels with O(1) performance -func (ml *MasterList) GetOrderedWheels() []*HeroicOPWheel { - ml.mu.RLock() - defer ml.mu.RUnlock() - - result := make([]*HeroicOPWheel, 0, len(ml.orderedWheels)) - for _, wheel := range ml.orderedWheels { - result = append(result, wheel) - } - return result -} - -// GetShiftWheels returns all wheels with shift abilities with O(1) performance -func (ml *MasterList) GetShiftWheels() []*HeroicOPWheel { - ml.mu.RLock() - defer ml.mu.RUnlock() - - result := make([]*HeroicOPWheel, 0, len(ml.shiftWheels)) - for _, wheel := range ml.shiftWheels { - result = append(result, wheel) - } - return result -} - -// GetWheelsByChanceRange returns wheels within a chance range with O(1) performance -func (ml *MasterList) GetWheelsByChanceRange(minChance, maxChance float32) []*HeroicOPWheel { - ml.mu.RLock() - defer ml.mu.RUnlock() - - var result []*HeroicOPWheel - - // Use indexed chance ranges for performance - for rangeKey, wheels := range ml.byChance { - for _, wheel := range wheels { - if wheel.Chance >= minChance && wheel.Chance <= maxChance { - result = append(result, wheel) - } - } - // Break early if we found wheels in this range - if len(result) > 0 && !ml.shouldContinueChanceSearch(rangeKey, minChance, maxChance) { - break - } - } - - return result -} - -// shouldContinueChanceSearch determines if we should continue searching other chance ranges -func (ml *MasterList) shouldContinueChanceSearch(currentRange string, minChance, maxChance float32) bool { - // Optimize search by stopping early based on range analysis - switch currentRange { - case "very_high": - return maxChance < 75.0 - case "high": - return maxChance < 50.0 || minChance > 75.0 - case "medium": - return maxChance < 25.0 || minChance > 50.0 - case "low": - return maxChance < 10.0 || minChance > 25.0 - default: // very_low - return minChance > 10.0 - } -} - -// Spell Information - -// GetSpellInfo returns spell information with O(1) performance -func (ml *MasterList) GetSpellInfo(spellID int32) (*SpellInfo, bool) { - ml.mu.RLock() - defer ml.mu.RUnlock() - - info, exists := ml.spellInfo[spellID] - return &info, exists -} - -// Advanced Search with Optimized Performance - -// Search performs advanced search with multiple criteria -func (ml *MasterList) Search(criteria HeroicOPSearchCriteria) *HeroicOPSearchResults { - ml.mu.RLock() - defer ml.mu.RUnlock() - - results := &HeroicOPSearchResults{ - Starters: make([]*HeroicOPStarter, 0), - Wheels: make([]*HeroicOPWheel, 0), - } - - // Optimize search strategy based on criteria - if criteria.StarterClass != 0 { - // Class-specific search - use class index - if classStarters, exists := ml.byClass[criteria.StarterClass]; exists { - for _, starter := range classStarters { - if ml.matchesStarterCriteria(starter, criteria) { - results.Starters = append(results.Starters, starter) - } - } - } - } else { - // Search all starters - for _, starter := range ml.starters { - if ml.matchesStarterCriteria(starter, criteria) { - results.Starters = append(results.Starters, starter) - } - } - } - - // Wheel search optimization - if criteria.SpellID != 0 { - // Spell-specific search - use spell index - if spellWheels, exists := ml.bySpellID[criteria.SpellID]; exists { - for _, wheel := range spellWheels { - if ml.matchesWheelCriteria(wheel, criteria) { - results.Wheels = append(results.Wheels, wheel) - } - } - } - } else if criteria.MinChance > 0 || criteria.MaxChance > 0 { - // Chance-based search - use chance ranges - minChance := criteria.MinChance - maxChance := criteria.MaxChance - if maxChance == 0 { - maxChance = MaxChance - } - results.Wheels = ml.GetWheelsByChanceRange(minChance, maxChance) - } else { - // Search all wheels - for _, wheel := range ml.wheels { - if ml.matchesWheelCriteria(wheel, criteria) { - results.Wheels = append(results.Wheels, wheel) - } - } - } - - return results -} - -// matchesStarterCriteria checks if starter matches search criteria -func (ml *MasterList) matchesStarterCriteria(starter *HeroicOPStarter, criteria HeroicOPSearchCriteria) bool { - if criteria.StarterClass != 0 && starter.StartClass != criteria.StarterClass { - return false - } - - if criteria.NamePattern != "" { - if !strings.Contains(strings.ToLower(starter.Name), strings.ToLower(criteria.NamePattern)) { - return false - } - } - - return true -} - -// matchesWheelCriteria checks if wheel matches search criteria -func (ml *MasterList) matchesWheelCriteria(wheel *HeroicOPWheel, criteria HeroicOPSearchCriteria) bool { - if criteria.SpellID != 0 && wheel.SpellID != criteria.SpellID { - return false - } - - if criteria.MinChance > 0 && wheel.Chance < criteria.MinChance { - return false - } - - if criteria.MaxChance > 0 && wheel.Chance > criteria.MaxChance { - return false - } - - if criteria.RequiredPlayers > 0 && wheel.RequiredPlayers != criteria.RequiredPlayers { - return false - } - - if criteria.NamePattern != "" { - if !strings.Contains(strings.ToLower(wheel.Name), strings.ToLower(criteria.NamePattern)) { - return false - } - } - - if criteria.HasShift && !wheel.HasShift() { - return false - } - - if criteria.IsOrdered && !wheel.IsOrdered() { - return false - } - - return true -} - -// Statistics with Lazy Caching - -// GetStatistics returns comprehensive statistics with lazy caching for optimal performance -func (ml *MasterList) GetStatistics() *HeroicOPStatistics { - ml.mu.Lock() - defer ml.mu.Unlock() - - ml.ensureMetadataValid() - - return &HeroicOPStatistics{ - TotalStarters: int64(ml.totalStarters), - TotalWheels: int64(ml.totalWheels), - ClassDistribution: ml.copyClassDistribution(), - OrderedWheelsCount: int64(len(ml.orderedWheels)), - ShiftWheelsCount: int64(len(ml.shiftWheels)), - SpellCount: int64(len(ml.spellInfo)), - AverageChance: ml.calculateAverageChance(), - } -} - -// ensureMetadataValid ensures cached metadata is current -func (ml *MasterList) ensureMetadataValid() { - if ml.metadataValid { - return - } - - // Recompute cached metadata - ml.totalStarters = len(ml.starters) - ml.totalWheels = len(ml.wheels) - - ml.classDistribution = make(map[int8]int) - for _, starter := range ml.starters { - ml.classDistribution[starter.StartClass]++ - } - - ml.metadataValid = true -} - -// copyClassDistribution creates a copy of class distribution for thread safety -func (ml *MasterList) copyClassDistribution() map[int32]int64 { - result := make(map[int32]int64) - for class, count := range ml.classDistribution { - result[int32(class)] = int64(count) - } - return result -} - -// calculateAverageChance computes average wheel chance -func (ml *MasterList) calculateAverageChance() float64 { - if len(ml.wheels) == 0 { - return 0.0 - } - - total := float64(0) - for _, wheel := range ml.wheels { - total += float64(wheel.Chance) - } - - return total / float64(len(ml.wheels)) -} - -// Modification Operations - -// AddStarter adds a starter to the master list with index updates -func (ml *MasterList) AddStarter(starter *HeroicOPStarter) error { - ml.mu.Lock() - defer ml.mu.Unlock() - - if err := starter.Validate(); err != nil { - return fmt.Errorf("invalid starter: %w", err) - } - - if _, exists := ml.starters[starter.ID]; exists { - return fmt.Errorf("starter ID %d already exists", starter.ID) - } - - // Add to primary storage - ml.starters[starter.ID] = starter - - // Update indices - if ml.byClass[starter.StartClass] == nil { - ml.byClass[starter.StartClass] = make(map[int32]*HeroicOPStarter) - } - ml.byClass[starter.StartClass][starter.ID] = starter - - ml.metadataValid = false - return nil -} - -// AddWheel adds a wheel to the master list with index updates -func (ml *MasterList) AddWheel(wheel *HeroicOPWheel) error { - ml.mu.Lock() - defer ml.mu.Unlock() - - if err := wheel.Validate(); err != nil { - return fmt.Errorf("invalid wheel: %w", err) - } - - if _, exists := ml.wheels[wheel.ID]; exists { - return fmt.Errorf("wheel ID %d already exists", wheel.ID) - } - - // Verify starter exists - if _, exists := ml.starters[wheel.StarterLinkID]; !exists { - return fmt.Errorf("starter ID %d not found for wheel", wheel.StarterLinkID) - } - - // Add to primary storage - ml.wheels[wheel.ID] = wheel - - // Update indices - ml.byStarterID[wheel.StarterLinkID] = append(ml.byStarterID[wheel.StarterLinkID], wheel) - ml.bySpellID[wheel.SpellID] = append(ml.bySpellID[wheel.SpellID], wheel) - - chanceRange := ml.getChanceRange(wheel.Chance) - ml.byChance[chanceRange] = append(ml.byChance[chanceRange], wheel) - - if wheel.IsOrdered() { - ml.orderedWheels[wheel.ID] = wheel - } - - if wheel.HasShift() { - ml.shiftWheels[wheel.ID] = wheel - } - - // Store spell info - ml.spellInfo[wheel.SpellID] = SpellInfo{ - ID: wheel.SpellID, - Name: wheel.Name, - Description: wheel.Description, - } - - ml.metadataValid = false - return nil -} - -// Utility Methods - -// IsLoaded returns whether data has been loaded -func (ml *MasterList) IsLoaded() bool { - ml.mu.RLock() - defer ml.mu.RUnlock() - return ml.loaded -} - -// GetCount returns total counts with O(1) performance -func (ml *MasterList) GetCount() (starters, wheels int) { - ml.mu.RLock() - defer ml.mu.RUnlock() - return len(ml.starters), len(ml.wheels) -} - -// Validate performs comprehensive validation of the master list -func (ml *MasterList) Validate() []error { - ml.mu.RLock() - defer ml.mu.RUnlock() - - var errors []error - - // Validate all starters - for _, starter := range ml.starters { - if err := starter.Validate(); err != nil { - errors = append(errors, fmt.Errorf("starter %d: %w", starter.ID, err)) - } - } - - // Validate all wheels - for _, wheel := range ml.wheels { - 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 _, exists := ml.starters[wheel.StarterLinkID]; !exists { - errors = append(errors, fmt.Errorf("wheel %d references non-existent starter %d", wheel.ID, wheel.StarterLinkID)) - } - } - - // Check for orphaned starters (starters with no wheels) - for _, starter := range ml.starters { - if wheels := ml.byStarterID[starter.ID]; len(wheels) == 0 { - errors = append(errors, fmt.Errorf("starter %d has no associated wheels", starter.ID)) - } - } - - return errors -} - -// HeroicOPSearchResults contains search results -type HeroicOPSearchResults struct { - Starters []*HeroicOPStarter - Wheels []*HeroicOPWheel -} - -// HeroicOPStatistics contains extended statistics -type HeroicOPStatistics 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"` -} \ No newline at end of file diff --git a/internal/heroic_ops/packets.go b/internal/heroic_ops/packets.go deleted file mode 100644 index 5d551e4..0000000 --- a/internal/heroic_ops/packets.go +++ /dev/null @@ -1,458 +0,0 @@ -package heroic_ops - -import ( - "encoding/binary" - "fmt" - "math" -) - -// HeroicOPPacketBuilder handles building packets for heroic opportunity client communication -type HeroicOPPacketBuilder struct { - clientVersion int -} - -// NewHeroicOPPacketBuilder creates a new packet builder -func NewHeroicOPPacketBuilder(clientVersion int) *HeroicOPPacketBuilder { - return &HeroicOPPacketBuilder{ - clientVersion: clientVersion, - } -} - -// BuildHOStartPacket builds the initial HO start packet -func (hpb *HeroicOPPacketBuilder) BuildHOStartPacket(ho *HeroicOP) ([]byte, error) { - if ho == nil { - return nil, fmt.Errorf("heroic opportunity is nil") - } - - // Start with base packet structure - packet := make([]byte, 0, 256) - - // Packet header (simplified - real implementation would use proper packet structure) - // This is a placeholder implementation - packet = append(packet, 0x01) // HO Start packet type - - // HO Instance ID (8 bytes) - idBytes := make([]byte, 8) - binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) - packet = append(packet, idBytes...) - - // Encounter ID (4 bytes) - encounterBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(encounterBytes, uint32(ho.EncounterID)) - packet = append(packet, encounterBytes...) - - // State (1 byte) - packet = append(packet, byte(ho.State)) - - // Starter ID (4 bytes) - starterBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(starterBytes, uint32(ho.StarterID)) - packet = append(packet, starterBytes...) - - return packet, nil -} - -// BuildHOUpdatePacket builds an HO update packet for wheel phase -func (hpb *HeroicOPPacketBuilder) BuildHOUpdatePacket(ho *HeroicOP) ([]byte, error) { - if ho == nil { - return nil, fmt.Errorf("heroic opportunity is nil") - } - - // Build packet based on HO state - packet := make([]byte, 0, 512) - - // Packet header - packet = append(packet, 0x02) // HO Update packet type - - // HO Instance ID (8 bytes) - idBytes := make([]byte, 8) - binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) - packet = append(packet, idBytes...) - - // State (1 byte) - packet = append(packet, byte(ho.State)) - - if ho.State == HOStateWheelPhase { - // Wheel ID (4 bytes) - wheelBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(wheelBytes, uint32(ho.WheelID)) - packet = append(packet, wheelBytes...) - - // Time remaining (4 bytes) - timeBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(timeBytes, uint32(ho.TimeRemaining)) - packet = append(packet, timeBytes...) - - // Total time (4 bytes) - totalTimeBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(totalTimeBytes, uint32(ho.TotalTime)) - packet = append(packet, totalTimeBytes...) - - // Countered array (6 bytes) - for i := 0; i < MaxAbilities; i++ { - packet = append(packet, byte(ho.Countered[i])) - } - - // Complete flag (1 byte) - packet = append(packet, byte(ho.Complete)) - - // Shift used flag (1 byte) - packet = append(packet, byte(ho.ShiftUsed)) - - // Spell name length and data - spellNameBytes := []byte(ho.SpellName) - packet = append(packet, byte(len(spellNameBytes))) - packet = append(packet, spellNameBytes...) - - // Spell description length and data - spellDescBytes := []byte(ho.SpellDescription) - descLen := make([]byte, 2) - binary.LittleEndian.PutUint16(descLen, uint16(len(spellDescBytes))) - packet = append(packet, descLen...) - packet = append(packet, spellDescBytes...) - } - - return packet, nil -} - -// BuildHOCompletePacket builds completion packet -func (hpb *HeroicOPPacketBuilder) BuildHOCompletePacket(ho *HeroicOP, success bool) ([]byte, error) { - if ho == nil { - return nil, fmt.Errorf("heroic opportunity is nil") - } - - packet := make([]byte, 0, 256) - - // Packet header - packet = append(packet, 0x03) // HO Complete packet type - - // HO Instance ID (8 bytes) - idBytes := make([]byte, 8) - binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) - packet = append(packet, idBytes...) - - // Success flag (1 byte) - if success { - packet = append(packet, 0x01) - } else { - packet = append(packet, 0x00) - } - - // Completed by character ID (4 bytes) - completedByBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(completedByBytes, uint32(ho.CompletedBy)) - packet = append(packet, completedByBytes...) - - if success { - // Spell ID if successful (4 bytes) - spellBytes := make([]byte, 4) - // Note: In real implementation, get spell ID from wheel - binary.LittleEndian.PutUint32(spellBytes, 0) // Placeholder - packet = append(packet, spellBytes...) - } - - return packet, nil -} - -// BuildHOTimerPacket builds timer update packet -func (hpb *HeroicOPPacketBuilder) BuildHOTimerPacket(timeRemaining, totalTime int32) ([]byte, error) { - packet := make([]byte, 0, 16) - - // Packet header - packet = append(packet, 0x04) // HO Timer packet type - - // Time remaining (4 bytes) - timeBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(timeBytes, uint32(timeRemaining)) - packet = append(packet, timeBytes...) - - // Total time (4 bytes) - totalTimeBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(totalTimeBytes, uint32(totalTime)) - packet = append(packet, totalTimeBytes...) - - return packet, nil -} - -// BuildHOWheelPacket builds wheel-specific packet with abilities -func (hpb *HeroicOPPacketBuilder) BuildHOWheelPacket(ho *HeroicOP, wheel *HeroicOPWheel) ([]byte, error) { - if ho == nil || wheel == nil { - return nil, fmt.Errorf("heroic opportunity or wheel is nil") - } - - packet := make([]byte, 0, 512) - - // Packet header - packet = append(packet, 0x05) // HO Wheel packet type - - // HO Instance ID (8 bytes) - idBytes := make([]byte, 8) - binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) - packet = append(packet, idBytes...) - - // Wheel ID (4 bytes) - wheelBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(wheelBytes, uint32(wheel.ID)) - packet = append(packet, wheelBytes...) - - // Order type (1 byte) - packet = append(packet, byte(wheel.Order)) - - // Shift icon (2 bytes) - shiftBytes := make([]byte, 2) - binary.LittleEndian.PutUint16(shiftBytes, uint16(wheel.ShiftIcon)) - packet = append(packet, shiftBytes...) - - // Abilities (12 bytes - 2 bytes per ability) - for i := 0; i < MaxAbilities; i++ { - abilityBytes := make([]byte, 2) - binary.LittleEndian.PutUint16(abilityBytes, uint16(wheel.Abilities[i])) - packet = append(packet, abilityBytes...) - } - - // Countered status (6 bytes) - for i := 0; i < MaxAbilities; i++ { - packet = append(packet, byte(ho.Countered[i])) - } - - // Timer information - timeBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(timeBytes, uint32(ho.TimeRemaining)) - packet = append(packet, timeBytes...) - - totalTimeBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(totalTimeBytes, uint32(ho.TotalTime)) - packet = append(packet, totalTimeBytes...) - - // Spell information - spellBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(spellBytes, uint32(wheel.SpellID)) - packet = append(packet, spellBytes...) - - // Spell name length and data - spellNameBytes := []byte(wheel.Name) - packet = append(packet, byte(len(spellNameBytes))) - packet = append(packet, spellNameBytes...) - - // Spell description length and data - spellDescBytes := []byte(wheel.Description) - descLen := make([]byte, 2) - binary.LittleEndian.PutUint16(descLen, uint16(len(spellDescBytes))) - packet = append(packet, descLen...) - packet = append(packet, spellDescBytes...) - - return packet, nil -} - -// BuildHOProgressPacket builds progress update packet -func (hpb *HeroicOPPacketBuilder) BuildHOProgressPacket(ho *HeroicOP, progressPercent float32) ([]byte, error) { - if ho == nil { - return nil, fmt.Errorf("heroic opportunity is nil") - } - - packet := make([]byte, 0, 32) - - // Packet header - packet = append(packet, 0x06) // HO Progress packet type - - // HO Instance ID (8 bytes) - idBytes := make([]byte, 8) - binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) - packet = append(packet, idBytes...) - - // Progress percentage as float (4 bytes) - progressBits := math.Float32bits(progressPercent) - progressBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(progressBytes, progressBits) - packet = append(packet, progressBytes...) - - // Current completion count (1 byte) - completed := int8(0) - for i := 0; i < MaxAbilities; i++ { - if ho.Countered[i] != 0 { - completed++ - } - } - packet = append(packet, byte(completed)) - - return packet, nil -} - -// BuildHOErrorPacket builds error notification packet -func (hpb *HeroicOPPacketBuilder) BuildHOErrorPacket(instanceID int64, errorCode int, errorMessage string) ([]byte, error) { - packet := make([]byte, 0, 256) - - // Packet header - packet = append(packet, 0x07) // HO Error packet type - - // HO Instance ID (8 bytes) - idBytes := make([]byte, 8) - binary.LittleEndian.PutUint64(idBytes, uint64(instanceID)) - packet = append(packet, idBytes...) - - // Error code (2 bytes) - errorBytes := make([]byte, 2) - binary.LittleEndian.PutUint16(errorBytes, uint16(errorCode)) - packet = append(packet, errorBytes...) - - // Error message length and data - messageBytes := []byte(errorMessage) - packet = append(packet, byte(len(messageBytes))) - packet = append(packet, messageBytes...) - - return packet, nil -} - -// BuildHOShiftPacket builds wheel shift notification packet -func (hpb *HeroicOPPacketBuilder) BuildHOShiftPacket(ho *HeroicOP, oldWheelID, newWheelID int32) ([]byte, error) { - if ho == nil { - return nil, fmt.Errorf("heroic opportunity is nil") - } - - packet := make([]byte, 0, 32) - - // Packet header - packet = append(packet, 0x08) // HO Shift packet type - - // HO Instance ID (8 bytes) - idBytes := make([]byte, 8) - binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) - packet = append(packet, idBytes...) - - // Old wheel ID (4 bytes) - oldWheelBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(oldWheelBytes, uint32(oldWheelID)) - packet = append(packet, oldWheelBytes...) - - // New wheel ID (4 bytes) - newWheelBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(newWheelBytes, uint32(newWheelID)) - packet = append(packet, newWheelBytes...) - - return packet, nil -} - -// PacketData conversion methods - -// ToPacketData converts HO and wheel to packet data structure -func (hpb *HeroicOPPacketBuilder) ToPacketData(ho *HeroicOP, wheel *HeroicOPWheel) *PacketData { - 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 -} - -// Helper methods for packet validation - -// ValidatePacketSize checks if packet size is within acceptable limits -func (hpb *HeroicOPPacketBuilder) ValidatePacketSize(packet []byte) error { - const maxPacketSize = 1024 // 1KB limit for HO packets - - if len(packet) > maxPacketSize { - return fmt.Errorf("packet size %d exceeds maximum %d", len(packet), maxPacketSize) - } - - return nil -} - -// GetPacketTypeDescription returns human-readable packet type description -func (hpb *HeroicOPPacketBuilder) GetPacketTypeDescription(packetType byte) string { - switch packetType { - case 0x01: - return "HO Start" - case 0x02: - return "HO Update" - case 0x03: - return "HO Complete" - case 0x04: - return "HO Timer" - case 0x05: - return "HO Wheel" - case 0x06: - return "HO Progress" - case 0x07: - return "HO Error" - case 0x08: - return "HO Shift" - default: - return "Unknown" - } -} - -// Client version specific methods - -// IsVersionSupported checks if client version supports specific features -func (hpb *HeroicOPPacketBuilder) IsVersionSupported(feature string) bool { - // Version-specific feature support - switch feature { - case "wheel_shifting": - return hpb.clientVersion >= 546 // Example version requirement - case "progress_updates": - return hpb.clientVersion >= 564 - case "extended_timers": - return hpb.clientVersion >= 572 - default: - return true // Basic features supported in all versions - } -} - -// GetVersionSpecificPacketSize returns packet size limits for client version -func (hpb *HeroicOPPacketBuilder) GetVersionSpecificPacketSize() int { - if hpb.clientVersion >= 564 { - return 1024 // Newer clients support larger packets - } - return 512 // Older clients have smaller limits -} - -// Error codes for HO system -const ( - HOErrorNone = iota - HOErrorInvalidState - HOErrorTimerExpired - HOErrorAbilityNotAllowed - HOErrorShiftAlreadyUsed - HOErrorPlayerNotInEncounter - HOErrorEncounterEnded - HOErrorSystemDisabled -) - -// GetErrorMessage returns human-readable error message for error code -func GetErrorMessage(errorCode int) string { - switch errorCode { - case HOErrorNone: - return "No error" - case HOErrorInvalidState: - return "Heroic opportunity is in an invalid state" - case HOErrorTimerExpired: - return "Heroic opportunity timer has expired" - case HOErrorAbilityNotAllowed: - return "This ability cannot be used for the current heroic opportunity" - case HOErrorShiftAlreadyUsed: - return "Wheel shift has already been used" - case HOErrorPlayerNotInEncounter: - return "Player is not in the encounter" - case HOErrorEncounterEnded: - return "Encounter has ended" - case HOErrorSystemDisabled: - return "Heroic opportunity system is disabled" - default: - return "Unknown error" - } -} diff --git a/internal/heroic_ops/types.go b/internal/heroic_ops/types.go deleted file mode 100644 index 1d1a4c2..0000000 --- a/internal/heroic_ops/types.go +++ /dev/null @@ -1,190 +0,0 @@ -package heroic_ops - -import ( - "sync" - "time" - - "eq2emu/internal/database" -) - -// 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 - db *database.Database `json:"-"` // Database connection - 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 - db *database.Database `json:"-"` // Database connection - 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 - db *database.Database `json:"-"` // Database connection - isNew bool `json:"-"` // True if this is a new record not yet saved -} - -// HeroicOPProgress tracks progress during starter chain phase -type HeroicOPProgress struct { - StarterID int32 `json:"starter_id"` // Starter being tracked - CurrentPosition int8 `json:"current_position"` // Current position in the chain (0-5) - IsEliminated bool `json:"is_eliminated"` // Whether this starter has been eliminated -} - -// HeroicOPData represents database record structure -type HeroicOPData struct { - ID int32 `json:"id"` - HOType string `json:"ho_type"` // "Starter" or "Wheel" - StarterClass int8 `json:"starter_class"` // For starters - StarterIcon int16 `json:"starter_icon"` // For starters - StarterLinkID int32 `json:"starter_link_id"` // For wheels - ChainOrder int8 `json:"chain_order"` // For wheels - ShiftIcon int16 `json:"shift_icon"` // For wheels - SpellID int32 `json:"spell_id"` // For wheels - Chance float32 `json:"chance"` // For wheels - Ability1 int16 `json:"ability1"` - Ability2 int16 `json:"ability2"` - Ability3 int16 `json:"ability3"` - Ability4 int16 `json:"ability4"` - Ability5 int16 `json:"ability5"` - Ability6 int16 `json:"ability6"` - Name string `json:"name"` - Description string `json:"description"` -} - -// HeroicOPManager manages active heroic opportunity instances -type HeroicOPManager struct { - mu sync.RWMutex - activeHOs map[int64]*HeroicOP // instance_id -> HO - encounterHOs map[int32][]*HeroicOP // encounter_id -> HOs - masterList *MasterList - database HeroicOPDatabase - db *database.Database // Direct database connection - eventHandler HeroicOPEventHandler - logger LogHandler - clientManager ClientManager - encounterManager EncounterManager - playerManager PlayerManager - nextInstanceID int64 - // Configuration - defaultWheelTimer int32 // milliseconds - maxConcurrentHOs int - enableLogging bool - enableStatistics bool -} - -// SpellInfo contains information about completion spells -type SpellInfo struct { - ID int32 `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Icon int16 `json:"icon"` -} - - -// HeroicOPSearchCriteria for searching heroic opportunities -type HeroicOPSearchCriteria struct { - StarterClass int8 `json:"starter_class"` // Filter by starter class (0 = any) - SpellID int32 `json:"spell_id"` // Filter by completion spell - MinChance float32 `json:"min_chance"` // Minimum wheel chance - MaxChance float32 `json:"max_chance"` // Maximum wheel chance - RequiredPlayers int8 `json:"required_players"` // Filter by player requirements - NamePattern string `json:"name_pattern"` // Filter by name pattern - HasShift bool `json:"has_shift"` // Filter by shift availability - IsOrdered bool `json:"is_ordered"` // Filter by wheel order type -} - -// HeroicOPEvent represents an event in the HO system -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 -} - -// PacketData represents data sent to client for HO display -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 -} - -// PlayerHOInfo contains player-specific HO information -type PlayerHOInfo struct { - CharacterID int32 `json:"character_id"` - ParticipatingHOs []int64 `json:"participating_hos"` // HO instance IDs - LastActivity time.Time `json:"last_activity"` - TotalHOsJoined int64 `json:"total_hos_joined"` - TotalHOsCompleted int64 `json:"total_hos_completed"` - SuccessRate float64 `json:"success_rate"` -} - -// Configuration structure for HO system -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"` -} diff --git a/internal/heroic_ops/utils.go b/internal/heroic_ops/utils.go deleted file mode 100644 index 9f88166..0000000 --- a/internal/heroic_ops/utils.go +++ /dev/null @@ -1,50 +0,0 @@ -package heroic_ops - -import ( - "math/rand" -) - -// SelectRandomWheel selects a random wheel from a list based on chance values -func SelectRandomWheel(wheels []*HeroicOPWheel) *HeroicOPWheel { - if len(wheels) == 0 { - return nil - } - - if len(wheels) == 1 { - return wheels[0] - } - - // Calculate total chance - totalChance := float32(0.0) - for _, wheel := range wheels { - totalChance += wheel.Chance - } - - if totalChance <= 0.0 { - // If no chances set, select randomly with equal probability - return wheels[rand.Intn(len(wheels))] - } - - // Random selection based on weighted chance - randomValue := rand.Float32() * totalChance - currentChance := float32(0.0) - - for _, wheel := range wheels { - currentChance += wheel.Chance - if randomValue <= currentChance { - return wheel - } - } - - // Fallback to last wheel (shouldn't happen with proper math) - return wheels[len(wheels)-1] -} - -// 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 -} \ No newline at end of file diff --git a/internal/packets/opcodes.go b/internal/packets/opcodes.go index 7579cfe..b72bce6 100644 --- a/internal/packets/opcodes.go +++ b/internal/packets/opcodes.go @@ -162,6 +162,16 @@ const ( OP_ModifyGuildMsg OP_RequestGuildInfoMsg + // Heroic Opportunity system opcodes + OP_HeroicOpportunityMsg + OP_HeroicOpportunityStartMsg + OP_HeroicOpportunityCompleteMsg + OP_HeroicOpportunityTimerMsg + OP_HeroicOpportunityWheelMsg + OP_HeroicOpportunityProgressMsg + OP_HeroicOpportunityErrorMsg + OP_HeroicOpportunityShiftMsg + // Add more opcodes as needed... _maxInternalOpcode // Sentinel value ) @@ -283,6 +293,16 @@ var OpcodeNames = map[InternalOpcode]string{ OP_GuildRecruitingDetailsMsg: "OP_GuildRecruitingDetailsMsg", OP_ModifyGuildMsg: "OP_ModifyGuildMsg", OP_RequestGuildInfoMsg: "OP_RequestGuildInfoMsg", + + // Heroic Opportunity system opcodes + OP_HeroicOpportunityMsg: "OP_HeroicOpportunityMsg", + OP_HeroicOpportunityStartMsg: "OP_HeroicOpportunityStartMsg", + OP_HeroicOpportunityCompleteMsg: "OP_HeroicOpportunityCompleteMsg", + OP_HeroicOpportunityTimerMsg: "OP_HeroicOpportunityTimerMsg", + OP_HeroicOpportunityWheelMsg: "OP_HeroicOpportunityWheelMsg", + OP_HeroicOpportunityProgressMsg: "OP_HeroicOpportunityProgressMsg", + OP_HeroicOpportunityErrorMsg: "OP_HeroicOpportunityErrorMsg", + OP_HeroicOpportunityShiftMsg: "OP_HeroicOpportunityShiftMsg", } // OpcodeManager handles the mapping between client-specific opcodes and internal opcodes