2025-08-08 14:31:44 -05:00

589 lines
16 KiB
Go

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