simplify heroic_ops

This commit is contained in:
Sky Johnson 2025-08-29 14:18:05 -05:00
parent 95c0561416
commit d0c51ea42f
12 changed files with 1411 additions and 3646 deletions

View File

@ -15,6 +15,7 @@ This document outlines how we successfully simplified the EverQuest II housing p
- Ground Spawn
- Groups
- Guilds
- Heroic Ops
## Before: Complex Architecture (8 Files, ~2000+ Lines)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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"`
}

View File

@ -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"
}
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -162,6 +162,16 @@ const (
OP_ModifyGuildMsg
OP_RequestGuildInfoMsg
// Heroic Opportunity system opcodes
OP_HeroicOpportunityMsg
OP_HeroicOpportunityStartMsg
OP_HeroicOpportunityCompleteMsg
OP_HeroicOpportunityTimerMsg
OP_HeroicOpportunityWheelMsg
OP_HeroicOpportunityProgressMsg
OP_HeroicOpportunityErrorMsg
OP_HeroicOpportunityShiftMsg
// Add more opcodes as needed...
_maxInternalOpcode // Sentinel value
)
@ -283,6 +293,16 @@ var OpcodeNames = map[InternalOpcode]string{
OP_GuildRecruitingDetailsMsg: "OP_GuildRecruitingDetailsMsg",
OP_ModifyGuildMsg: "OP_ModifyGuildMsg",
OP_RequestGuildInfoMsg: "OP_RequestGuildInfoMsg",
// Heroic Opportunity system opcodes
OP_HeroicOpportunityMsg: "OP_HeroicOpportunityMsg",
OP_HeroicOpportunityStartMsg: "OP_HeroicOpportunityStartMsg",
OP_HeroicOpportunityCompleteMsg: "OP_HeroicOpportunityCompleteMsg",
OP_HeroicOpportunityTimerMsg: "OP_HeroicOpportunityTimerMsg",
OP_HeroicOpportunityWheelMsg: "OP_HeroicOpportunityWheelMsg",
OP_HeroicOpportunityProgressMsg: "OP_HeroicOpportunityProgressMsg",
OP_HeroicOpportunityErrorMsg: "OP_HeroicOpportunityErrorMsg",
OP_HeroicOpportunityShiftMsg: "OP_HeroicOpportunityShiftMsg",
}
// OpcodeManager handles the mapping between client-specific opcodes and internal opcodes