591 lines
17 KiB
Go
591 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
|
|
} |