eq2go/internal/spells/spell_process.go

592 lines
17 KiB
Go

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
}