package spells import ( "fmt" "sync" "time" ) // InterruptStruct represents a spell interruption event type InterruptStruct struct { InterruptedEntityID int32 // ID of the entity being interrupted SpellID int32 // ID of the spell being interrupted ErrorCode int16 // Error code for the interruption FromMovement bool // Whether interruption was caused by movement Canceled bool // Whether the spell was canceled vs interrupted Timestamp time.Time // When the interrupt occurred } // CastTimer represents a spell casting timer type CastTimer struct { CasterID int32 // ID of the entity casting TargetID int32 // ID of the target SpellID int32 // ID of the spell being cast ZoneID int32 // ID of the zone where casting occurs StartTime time.Time // When casting started Duration time.Duration // How long the cast takes InHeroicOpp bool // Whether this is part of a heroic opportunity DeleteTimer bool // Flag to mark timer for deletion IsEntityCommand bool // Whether this is an entity command vs spell mutex sync.RWMutex // Thread safety } // RecastTimer represents a spell recast cooldown timer type RecastTimer struct { CasterID int32 // ID of the entity with the recast ClientID int32 // ID of the client (if player) SpellID int32 // ID of the spell on cooldown LinkedTimerID int32 // ID of linked timer group TypeGroupSpellID int32 // Type group for timer sharing StartTime time.Time // When the recast started Duration time.Duration // How long the recast lasts StayLocked bool // Whether spell stays locked after recast mutex sync.RWMutex // Thread safety } // CastSpell represents a spell casting request type CastSpell struct { CasterID int32 // ID of the entity casting TargetID int32 // ID of the target SpellID int32 // ID of the spell to cast ZoneID int32 // ID of the zone } // SpellQueue represents a player's spell queue entry type SpellQueueEntry struct { SpellID int32 // ID of the queued spell Priority int32 // Queue priority QueuedTime time.Time // When the spell was queued TargetID int32 // Target for the spell HostileOnly bool // Whether this is a hostile-only queue } // HeroicOpportunity represents a heroic opportunity instance type HeroicOpportunity struct { ID int32 // Unique identifier InitiatorID int32 // ID of the player/group that started it TargetID int32 // ID of the target StartTime time.Time // When the HO started Duration time.Duration // Total time allowed CurrentStep int32 // Current step in the sequence TotalSteps int32 // Total steps in the sequence IsGroup bool // Whether this is a group HO Complete bool // Whether the HO completed successfully WheelID int32 // ID of the wheel type mutex sync.RWMutex // Thread safety } // SpellProcess manages all spell casting for a zone type SpellProcess struct { // Core collections activeSpells map[int32]*LuaSpell // Active spells by spell instance ID castTimers []*CastTimer // Active cast timers recastTimers []*RecastTimer // Active recast timers interruptQueue []*InterruptStruct // Queued interruptions spellQueues map[int32][]*SpellQueueEntry // Player spell queues by player ID // Heroic Opportunities soloHeroicOps map[int32]*HeroicOpportunity // Solo HOs by client ID groupHeroicOps map[int32]*HeroicOpportunity // Group HOs by group ID // Targeting and removal removeTargetList map[int32][]int32 // Targets to remove by spell ID spellCancelList []int32 // Spells marked for cancellation // State management lastProcessTime time.Time // Last time Process() was called nextSpellID int32 // Next available spell instance ID // Thread safety mutex sync.RWMutex // Main process mutex // TODO: Add when other systems are available // zoneServer *ZoneServer // Reference to zone server // luaInterface *LuaInterface // Reference to Lua interface } // NewSpellProcess creates a new spell process instance func NewSpellProcess() *SpellProcess { return &SpellProcess{ activeSpells: make(map[int32]*LuaSpell), castTimers: make([]*CastTimer, 0), recastTimers: make([]*RecastTimer, 0), interruptQueue: make([]*InterruptStruct, 0), spellQueues: make(map[int32][]*SpellQueueEntry), soloHeroicOps: make(map[int32]*HeroicOpportunity), groupHeroicOps: make(map[int32]*HeroicOpportunity), removeTargetList: make(map[int32][]int32), spellCancelList: make([]int32, 0), lastProcessTime: time.Now(), nextSpellID: 1, } } // Process handles the main spell processing loop func (sp *SpellProcess) Process() { sp.mutex.Lock() defer sp.mutex.Unlock() now := time.Now() // Only process every 50ms to match C++ implementation if now.Sub(sp.lastProcessTime) < time.Duration(ProcessCheckInterval)*time.Millisecond { return } sp.lastProcessTime = now // Process active spells (duration checks, ticks) sp.processActiveSpells(now) // Process spell cancellations sp.processSpellCancellations() // Process interrupts sp.processInterrupts() // Process cast timers sp.processCastTimers(now) // Process recast timers sp.processRecastTimers(now) // Process spell queues sp.processSpellQueues() // Process heroic opportunities sp.processHeroicOpportunities(now) } // processActiveSpells handles duration checks and spell ticks func (sp *SpellProcess) processActiveSpells(now time.Time) { expiredSpells := make([]int32, 0) for spellID, luaSpell := range sp.activeSpells { if luaSpell == nil { expiredSpells = append(expiredSpells, spellID) continue } // Check if spell duration has expired // TODO: Implement proper duration checking based on spell data // This would check luaSpell.spell.GetSpellData().duration1 etc. // Check if spell needs to tick // TODO: Implement spell tick processing // This would call ProcessSpell(luaSpell, false) for tick effects } // Remove expired spells for _, spellID := range expiredSpells { sp.deleteCasterSpell(spellID, "expired") } } // processSpellCancellations handles spells marked for cancellation func (sp *SpellProcess) processSpellCancellations() { if len(sp.spellCancelList) == 0 { return } canceledSpells := make([]int32, len(sp.spellCancelList)) copy(canceledSpells, sp.spellCancelList) sp.spellCancelList = sp.spellCancelList[:0] // Clear the list for _, spellID := range canceledSpells { sp.deleteCasterSpell(spellID, "canceled") } } // processInterrupts handles queued spell interruptions func (sp *SpellProcess) processInterrupts() { if len(sp.interruptQueue) == 0 { return } interrupts := make([]*InterruptStruct, len(sp.interruptQueue)) copy(interrupts, sp.interruptQueue) sp.interruptQueue = sp.interruptQueue[:0] // Clear the queue for _, interrupt := range interrupts { sp.checkInterrupt(interrupt) } } // processCastTimers handles spell casting completion func (sp *SpellProcess) processCastTimers(now time.Time) { completedTimers := make([]*CastTimer, 0) remainingTimers := make([]*CastTimer, 0) for _, timer := range sp.castTimers { if timer.DeleteTimer { // Timer marked for deletion continue } if now.Sub(timer.StartTime) >= timer.Duration { // Cast time completed timer.DeleteTimer = true completedTimers = append(completedTimers, timer) // TODO: Send finish cast packet to client // TODO: Call CastProcessedSpell or CastProcessedEntityCommand } else { remainingTimers = append(remainingTimers, timer) } } sp.castTimers = remainingTimers } // processRecastTimers handles spell cooldown expiration func (sp *SpellProcess) processRecastTimers(now time.Time) { expiredTimers := make([]*RecastTimer, 0) remainingTimers := make([]*RecastTimer, 0) for _, timer := range sp.recastTimers { if now.Sub(timer.StartTime) >= timer.Duration { // Recast timer expired expiredTimers = append(expiredTimers, timer) // TODO: Unlock spell for the caster if not a maintained effect // TODO: Send spell book update to client } else { remainingTimers = append(remainingTimers, timer) } } sp.recastTimers = remainingTimers } // processSpellQueues handles queued spells for players func (sp *SpellProcess) processSpellQueues() { for playerID, queue := range sp.spellQueues { if len(queue) == 0 { continue } // TODO: Check if player is casting and can cast next spell // TODO: Process highest priority spell from queue // This would call ProcessSpell for the queued spell _ = playerID // Placeholder to avoid unused variable error } } // processHeroicOpportunities handles heroic opportunity timers func (sp *SpellProcess) processHeroicOpportunities(now time.Time) { // Process solo heroic opportunities expiredSolo := make([]int32, 0) for clientID, ho := range sp.soloHeroicOps { if now.Sub(ho.StartTime) >= ho.Duration { ho.Complete = true expiredSolo = append(expiredSolo, clientID) // TODO: Send heroic opportunity update packet } } for _, clientID := range expiredSolo { delete(sp.soloHeroicOps, clientID) } // Process group heroic opportunities expiredGroup := make([]int32, 0) for groupID, ho := range sp.groupHeroicOps { if now.Sub(ho.StartTime) >= ho.Duration { ho.Complete = true expiredGroup = append(expiredGroup, groupID) // TODO: Send heroic opportunity update to all group members } } for _, groupID := range expiredGroup { delete(sp.groupHeroicOps, groupID) } } // RemoveCaster removes references to a caster when they are destroyed func (sp *SpellProcess) RemoveCaster(casterID int32) { sp.mutex.Lock() defer sp.mutex.Unlock() // Remove from active spells expiredSpells := make([]int32, 0) for spellID, luaSpell := range sp.activeSpells { if luaSpell != nil && luaSpell.CasterID == casterID { luaSpell.CasterID = 0 // Clear caster reference expiredSpells = append(expiredSpells, spellID) } } // Clean up spells with invalid casters for _, spellID := range expiredSpells { sp.deleteCasterSpell(spellID, "caster removed") } // Remove cast timers for this caster remainingCastTimers := make([]*CastTimer, 0) for _, timer := range sp.castTimers { if timer.CasterID != casterID { remainingCastTimers = append(remainingCastTimers, timer) } } sp.castTimers = remainingCastTimers // Remove recast timers for this caster remainingRecastTimers := make([]*RecastTimer, 0) for _, timer := range sp.recastTimers { if timer.CasterID != casterID { remainingRecastTimers = append(remainingRecastTimers, timer) } } sp.recastTimers = remainingRecastTimers // Remove spell queue for this caster delete(sp.spellQueues, casterID) } // Interrupt creates an interrupt request for a casting entity func (sp *SpellProcess) Interrupt(entityID int32, spellID int32, errorCode int16, cancel, fromMovement bool) { sp.mutex.Lock() defer sp.mutex.Unlock() interrupt := &InterruptStruct{ InterruptedEntityID: entityID, SpellID: spellID, ErrorCode: errorCode, FromMovement: fromMovement, Canceled: cancel, Timestamp: time.Now(), } sp.interruptQueue = append(sp.interruptQueue, interrupt) } // checkInterrupt processes a single interrupt func (sp *SpellProcess) checkInterrupt(interrupt *InterruptStruct) { if interrupt == nil { return } // TODO: Implement interrupt processing // This would: // 1. Find the casting entity // 2. Send finish cast packet if needed // 3. Remove spell timers from spawn // 4. Set entity casting state to false // 5. Send interrupt packet to zone // 6. Send spell failed packet if error code > 0 // 7. Unlock spell for player // 8. Send spell book update fmt.Printf("Processing interrupt for entity %d, spell %d, error %d\n", interrupt.InterruptedEntityID, interrupt.SpellID, interrupt.ErrorCode) } // IsReady checks if an entity can cast a spell (not on recast) func (sp *SpellProcess) IsReady(spellID, casterID int32) bool { sp.mutex.RLock() defer sp.mutex.RUnlock() // TODO: Check if caster is currently casting // if caster.IsCasting() { return false } // Check recast timers for _, timer := range sp.recastTimers { if timer.SpellID == spellID && timer.CasterID == casterID { return false // Still on cooldown } } return true } // AddSpellToQueue adds a spell to a player's casting queue func (sp *SpellProcess) AddSpellToQueue(spellID, casterID, targetID int32, priority int32) { sp.mutex.Lock() defer sp.mutex.Unlock() entry := &SpellQueueEntry{ SpellID: spellID, Priority: priority, QueuedTime: time.Now(), TargetID: targetID, } if sp.spellQueues[casterID] == nil { sp.spellQueues[casterID] = make([]*SpellQueueEntry, 0) } // Add to queue (TODO: sort by priority) sp.spellQueues[casterID] = append(sp.spellQueues[casterID], entry) // Limit queue size if len(sp.spellQueues[casterID]) > MaxQueuedSpells { sp.spellQueues[casterID] = sp.spellQueues[casterID][1:] // Remove oldest } } // RemoveSpellFromQueue removes a specific spell from a player's queue func (sp *SpellProcess) RemoveSpellFromQueue(spellID, casterID int32) bool { sp.mutex.Lock() defer sp.mutex.Unlock() queue, exists := sp.spellQueues[casterID] if !exists { return false } for i, entry := range queue { if entry.SpellID == spellID { // Remove entry from queue sp.spellQueues[casterID] = append(queue[:i], queue[i+1:]...) return true } } return false } // ClearSpellQueue clears a player's spell queue func (sp *SpellProcess) ClearSpellQueue(casterID int32, hostileOnly bool) { sp.mutex.Lock() defer sp.mutex.Unlock() if !hostileOnly { delete(sp.spellQueues, casterID) return } // TODO: Remove only hostile spells // This would require checking spell data to determine if spell is hostile } // AddSpellCancel marks a spell for cancellation func (sp *SpellProcess) AddSpellCancel(spellID int32) { sp.mutex.Lock() defer sp.mutex.Unlock() sp.spellCancelList = append(sp.spellCancelList, spellID) } // deleteCasterSpell removes a spell from active processing func (sp *SpellProcess) deleteCasterSpell(spellID int32, reason string) bool { luaSpell, exists := sp.activeSpells[spellID] if !exists { return false } // TODO: Implement proper spell removal // This would: // 1. Handle concentration return for toggle spells // 2. Check recast for non-duration-until-cancel spells // 3. Unlock spell for player // 4. Remove procs from caster // 5. Remove maintained spell from caster // 6. Remove targets from spell // 7. Process spell removal effects fmt.Printf("Removing spell %d, reason: %s\n", spellID, reason) delete(sp.activeSpells, spellID) // Clean up removal targets list delete(sp.removeTargetList, spellID) _ = luaSpell // Placeholder to avoid unused variable error return true } // GetActiveSpellCount returns the number of active spells func (sp *SpellProcess) GetActiveSpellCount() int { sp.mutex.RLock() defer sp.mutex.RUnlock() return len(sp.activeSpells) } // GetQueuedSpellCount returns the number of queued spells for a player func (sp *SpellProcess) GetQueuedSpellCount(casterID int32) int { sp.mutex.RLock() defer sp.mutex.RUnlock() if queue, exists := sp.spellQueues[casterID]; exists { return len(queue) } return 0 } // GetRecastTimeRemaining returns remaining recast time for a spell func (sp *SpellProcess) GetRecastTimeRemaining(spellID, casterID int32) time.Duration { sp.mutex.RLock() defer sp.mutex.RUnlock() for _, timer := range sp.recastTimers { if timer.SpellID == spellID && timer.CasterID == casterID { elapsed := time.Since(timer.StartTime) if elapsed >= timer.Duration { return 0 } return timer.Duration - elapsed } } return 0 } // RemoveAllSpells removes all spells from processing (used during shutdown) func (sp *SpellProcess) RemoveAllSpells(reloadSpells bool) { sp.mutex.Lock() defer sp.mutex.Unlock() // Clear all spell collections if reloadSpells { // Keep some data for reload sp.activeSpells = make(map[int32]*LuaSpell) } else { // Complete cleanup for spellID := range sp.activeSpells { sp.deleteCasterSpell(spellID, "shutdown") } sp.activeSpells = make(map[int32]*LuaSpell) } sp.castTimers = make([]*CastTimer, 0) sp.recastTimers = make([]*RecastTimer, 0) sp.interruptQueue = make([]*InterruptStruct, 0) sp.spellQueues = make(map[int32][]*SpellQueueEntry) sp.soloHeroicOps = make(map[int32]*HeroicOpportunity) sp.groupHeroicOps = make(map[int32]*HeroicOpportunity) sp.removeTargetList = make(map[int32][]int32) sp.spellCancelList = make([]int32, 0) } // NewCastTimer creates a new cast timer func (ct *CastTimer) IsExpired() bool { ct.mutex.RLock() defer ct.mutex.RUnlock() return time.Since(ct.StartTime) >= ct.Duration } // NewRecastTimer creates a new recast timer func (rt *RecastTimer) IsExpired() bool { rt.mutex.RLock() defer rt.mutex.RUnlock() return time.Since(rt.StartTime) >= rt.Duration } // GetRemainingTime returns remaining time on the recast func (rt *RecastTimer) GetRemainingTime() time.Duration { rt.mutex.RLock() defer rt.mutex.RUnlock() elapsed := time.Since(rt.StartTime) if elapsed >= rt.Duration { return 0 } return rt.Duration - elapsed }