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) }