package heroic_ops import ( "fmt" "math/rand" "time" ) // NewHeroicOPStarter creates a new heroic opportunity starter func NewHeroicOPStarter(id int32, startClass int8, starterIcon int16) *HeroicOPStarter { return &HeroicOPStarter{ ID: id, StartClass: startClass, StarterIcon: starterIcon, Abilities: [6]int16{}, SaveNeeded: false, } } // 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, 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 } // NewHeroicOPWheel creates a new heroic opportunity wheel func NewHeroicOPWheel(id int32, starterLinkID int32, order int8) *HeroicOPWheel { return &HeroicOPWheel{ ID: id, StarterLinkID: starterLinkID, Order: order, Abilities: [6]int16{}, Chance: 1.0, SaveNeeded: false, } } // 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, 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 } // NewHeroicOP creates a new heroic opportunity instance func NewHeroicOP(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, SaveNeeded: false, } } // 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 } // 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 *MasterHeroicOPList) 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 } // 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)), 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 } // Helper functions for random selection // 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] } // 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) }