simplify heroic_ops
This commit is contained in:
parent
95c0561416
commit
d0c51ea42f
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
1390
internal/heroic_ops/heroic_ops.go
Normal file
1390
internal/heroic_ops/heroic_ops.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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"`
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"`
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user