589 lines
16 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|