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
|
- Ground Spawn
|
||||||
- Groups
|
- Groups
|
||||||
- Guilds
|
- Guilds
|
||||||
|
- Heroic Ops
|
||||||
|
|
||||||
## Before: Complex Architecture (8 Files, ~2000+ Lines)
|
## 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_ModifyGuildMsg
|
||||||
OP_RequestGuildInfoMsg
|
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...
|
// Add more opcodes as needed...
|
||||||
_maxInternalOpcode // Sentinel value
|
_maxInternalOpcode // Sentinel value
|
||||||
)
|
)
|
||||||
@ -283,6 +293,16 @@ var OpcodeNames = map[InternalOpcode]string{
|
|||||||
OP_GuildRecruitingDetailsMsg: "OP_GuildRecruitingDetailsMsg",
|
OP_GuildRecruitingDetailsMsg: "OP_GuildRecruitingDetailsMsg",
|
||||||
OP_ModifyGuildMsg: "OP_ModifyGuildMsg",
|
OP_ModifyGuildMsg: "OP_ModifyGuildMsg",
|
||||||
OP_RequestGuildInfoMsg: "OP_RequestGuildInfoMsg",
|
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
|
// OpcodeManager handles the mapping between client-specific opcodes and internal opcodes
|
||||||
|
Loading…
x
Reference in New Issue
Block a user