722 lines
17 KiB
Go
722 lines
17 KiB
Go
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)
|
|
}
|