eq2go/internal/heroic_ops/heroic_op.go

722 lines
17 KiB
Go

package heroic_ops
import (
"fmt"
"math/rand"
"sync"
"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)
}