eq2go/internal/spells/spell_manager.go

598 lines
16 KiB
Go

package spells
import (
"fmt"
"sync"
"sync/atomic"
)
// SpellScriptTimer represents a timer for spell script execution
type SpellScriptTimer struct {
// TODO: Add LuaSpell reference when implemented
// Spell *LuaSpell // The spell being timed
SpellID int32 // Spell ID for identification
CustomFunction string // Custom function to call
Time int32 // Timer duration
Caster int32 // Caster entity ID
Target int32 // Target entity ID
DeleteWhenDone bool // Whether to delete timer when finished
}
// MasterSpellList manages all spells in the game
// This replaces the C++ MasterSpellList functionality
type MasterSpellList struct {
// Spell storage
spells map[int32]*Spell // Spells by ID
spellsByName map[string]*Spell // Spells by name for lookup
spellsByTier map[int32]map[int8]*Spell // Spells by ID and tier
// ID management
maxSpellID int32 // Highest assigned spell ID
// Thread safety
mutex sync.RWMutex
}
// NewMasterSpellList creates a new master spell list
func NewMasterSpellList() *MasterSpellList {
return &MasterSpellList{
spells: make(map[int32]*Spell),
spellsByName: make(map[string]*Spell),
spellsByTier: make(map[int32]map[int8]*Spell),
maxSpellID: 0,
}
}
// AddSpell adds a spell to the master list
func (msl *MasterSpellList) AddSpell(spell *Spell) bool {
if spell == nil {
return false
}
msl.mutex.Lock()
defer msl.mutex.Unlock()
spellID := spell.GetSpellID()
// Update max spell ID
if spellID > msl.maxSpellID {
msl.maxSpellID = spellID
}
// Add to main spell map
msl.spells[spellID] = spell
// Add to name lookup
name := spell.GetName()
if name != "" {
msl.spellsByName[name] = spell
}
// Add to tier lookup
tier := spell.GetSpellTier()
if msl.spellsByTier[spellID] == nil {
msl.spellsByTier[spellID] = make(map[int8]*Spell)
}
msl.spellsByTier[spellID][tier] = spell
return true
}
// GetSpell retrieves a spell by ID
func (msl *MasterSpellList) GetSpell(spellID int32) *Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
return msl.spells[spellID]
}
// GetSpellByName retrieves a spell by name
func (msl *MasterSpellList) GetSpellByName(name string) *Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
return msl.spellsByName[name]
}
// GetSpellByIDAndTier retrieves a specific tier of a spell
func (msl *MasterSpellList) GetSpellByIDAndTier(spellID int32, tier int8) *Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
if tierMap, exists := msl.spellsByTier[spellID]; exists {
return tierMap[tier]
}
return nil
}
// RemoveSpell removes a spell from the master list
func (msl *MasterSpellList) RemoveSpell(spellID int32) bool {
msl.mutex.Lock()
defer msl.mutex.Unlock()
spell, exists := msl.spells[spellID]
if !exists {
return false
}
// Remove from main map
delete(msl.spells, spellID)
// Remove from name lookup
name := spell.GetName()
if name != "" {
delete(msl.spellsByName, name)
}
// Remove from tier lookup
delete(msl.spellsByTier, spellID)
return true
}
// GetNewMaxSpellID returns the next available spell ID
func (msl *MasterSpellList) GetNewMaxSpellID() int32 {
return atomic.AddInt32(&msl.maxSpellID, 1)
}
// GetSpellCount returns the total number of spells
func (msl *MasterSpellList) GetSpellCount() int {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
return len(msl.spells)
}
// GetAllSpells returns a copy of all spells (expensive operation)
func (msl *MasterSpellList) GetAllSpells() []*Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
spells := make([]*Spell, 0, len(msl.spells))
for _, spell := range msl.spells {
spells = append(spells, spell)
}
return spells
}
// GetSpellsByType returns all spells of a specific type
func (msl *MasterSpellList) GetSpellsByType(spellType int16) []*Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
spells := make([]*Spell, 0)
for _, spell := range msl.spells {
if spell.GetSpellData().Type == spellType {
spells = append(spells, spell)
}
}
return spells
}
// GetSpellsByBookType returns all spells of a specific book type
func (msl *MasterSpellList) GetSpellsByBookType(bookType int32) []*Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
spells := make([]*Spell, 0)
for _, spell := range msl.spells {
if spell.GetSpellData().SpellBookType == bookType {
spells = append(spells, spell)
}
}
return spells
}
// Global spell list instance
var masterSpellList *MasterSpellList
var initSpellListOnce sync.Once
// GetMasterSpellList returns the global master spell list (singleton)
func GetMasterSpellList() *MasterSpellList {
initSpellListOnce.Do(func() {
masterSpellList = NewMasterSpellList()
})
return masterSpellList
}
// SpellCasting represents an active spell casting attempt
type SpellCasting struct {
Spell *Spell // The spell being cast
Caster int32 // Caster entity ID
Target int32 // Target entity ID
CastTime int32 // Total cast time
TimeRemaining int32 // Time remaining in cast
Interrupted bool // Whether casting was interrupted
// TODO: Add Entity references when implemented
// CasterEntity *Entity
// TargetEntity *Entity
}
// SpellBook represents a character's spell book
type SpellBook struct {
// Spell storage organized by type
spells map[int32]*Spell // All known spells by ID
spellsByType map[int32][]*Spell // Spells organized by book type
// Spell bar/hotbar assignments
spellBars map[int8]map[int8]*Spell // [bar][slot] = spell
// Thread safety
mutex sync.RWMutex
}
// NewSpellBook creates a new spell book
func NewSpellBook() *SpellBook {
return &SpellBook{
spells: make(map[int32]*Spell),
spellsByType: make(map[int32][]*Spell),
spellBars: make(map[int8]map[int8]*Spell),
}
}
// AddSpell adds a spell to the spell book
func (sb *SpellBook) AddSpell(spell *Spell) bool {
if spell == nil {
return false
}
sb.mutex.Lock()
defer sb.mutex.Unlock()
spellID := spell.GetSpellID()
// Check if spell already exists
if _, exists := sb.spells[spellID]; exists {
return false
}
// Add to main collection
sb.spells[spellID] = spell
// Add to type collection
bookType := spell.GetSpellData().SpellBookType
if sb.spellsByType[bookType] == nil {
sb.spellsByType[bookType] = make([]*Spell, 0)
}
sb.spellsByType[bookType] = append(sb.spellsByType[bookType], spell)
return true
}
// RemoveSpell removes a spell from the spell book
func (sb *SpellBook) RemoveSpell(spellID int32) bool {
sb.mutex.Lock()
defer sb.mutex.Unlock()
spell, exists := sb.spells[spellID]
if !exists {
return false
}
// Remove from main collection
delete(sb.spells, spellID)
// Remove from type collection
bookType := spell.GetSpellData().SpellBookType
if spells, exists := sb.spellsByType[bookType]; exists {
for i, s := range spells {
if s.GetSpellID() == spellID {
sb.spellsByType[bookType] = append(spells[:i], spells[i+1:]...)
break
}
}
}
// Remove from spell bars
for barID, bar := range sb.spellBars {
for slot, s := range bar {
if s != nil && s.GetSpellID() == spellID {
delete(sb.spellBars[barID], slot)
}
}
}
return true
}
// GetSpell retrieves a spell from the spell book
func (sb *SpellBook) GetSpell(spellID int32) *Spell {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
return sb.spells[spellID]
}
// HasSpell checks if the spell book contains a specific spell
func (sb *SpellBook) HasSpell(spellID int32) bool {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
_, exists := sb.spells[spellID]
return exists
}
// GetSpellsByType returns all spells of a specific book type
func (sb *SpellBook) GetSpellsByType(bookType int32) []*Spell {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
if spells, exists := sb.spellsByType[bookType]; exists {
// Return a copy to prevent external modification
result := make([]*Spell, len(spells))
copy(result, spells)
return result
}
return make([]*Spell, 0)
}
// SetSpellBarSlot assigns a spell to a specific bar and slot
func (sb *SpellBook) SetSpellBarSlot(barID, slot int8, spell *Spell) bool {
sb.mutex.Lock()
defer sb.mutex.Unlock()
// Ensure the spell is in the spell book
if spell != nil {
if _, exists := sb.spells[spell.GetSpellID()]; !exists {
return false
}
}
// Initialize bar if needed
if sb.spellBars[barID] == nil {
sb.spellBars[barID] = make(map[int8]*Spell)
}
// Set the slot
if spell == nil {
delete(sb.spellBars[barID], slot)
} else {
sb.spellBars[barID][slot] = spell
}
return true
}
// GetSpellBarSlot retrieves the spell at a specific bar and slot
func (sb *SpellBook) GetSpellBarSlot(barID, slot int8) *Spell {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
if bar, exists := sb.spellBars[barID]; exists {
return bar[slot]
}
return nil
}
// GetSpellCount returns the total number of spells in the book
func (sb *SpellBook) GetSpellCount() int {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
return len(sb.spells)
}
// String returns a string representation of the spell book
func (sb *SpellBook) String() string {
return fmt.Sprintf("SpellBook[Spells=%d, Types=%d]",
sb.GetSpellCount(), len(sb.spellsByType))
}
// SpellManager manages the master spell list, player spell books, and spell processing
type SpellManager struct {
masterList *MasterSpellList // Global spell definitions
spellBooks map[int32]*SpellBook // Player spell books by character ID
spellProcess *SpellProcess // Spell processing system
targeting *SpellTargeting // Spell targeting system
resourceChecker *SpellResourceChecker // Resource checking system
mutex sync.RWMutex // Thread safety
}
// NewSpellManager creates a new spell manager
func NewSpellManager() *SpellManager {
return &SpellManager{
masterList: NewMasterSpellList(),
spellBooks: make(map[int32]*SpellBook),
spellProcess: NewSpellProcess(),
targeting: NewSpellTargeting(),
resourceChecker: NewSpellResourceChecker(),
}
}
// GetMasterList returns the master spell list
func (sm *SpellManager) GetMasterList() *MasterSpellList {
return sm.masterList
}
// GetSpellProcess returns the spell processing system
func (sm *SpellManager) GetSpellProcess() *SpellProcess {
return sm.spellProcess
}
// GetTargeting returns the spell targeting system
func (sm *SpellManager) GetTargeting() *SpellTargeting {
return sm.targeting
}
// GetResourceChecker returns the resource checking system
func (sm *SpellManager) GetResourceChecker() *SpellResourceChecker {
return sm.resourceChecker
}
// GetSpellBook retrieves or creates a spell book for a character
func (sm *SpellManager) GetSpellBook(characterID int32) *SpellBook {
sm.mutex.Lock()
defer sm.mutex.Unlock()
if book, exists := sm.spellBooks[characterID]; exists {
return book
}
// Create new spell book
book := NewSpellBook()
sm.spellBooks[characterID] = book
return book
}
// RemoveSpellBook removes a character's spell book (when they log out)
func (sm *SpellManager) RemoveSpellBook(characterID int32) {
sm.mutex.Lock()
defer sm.mutex.Unlock()
delete(sm.spellBooks, characterID)
}
// ProcessSpells processes all active spells (should be called regularly)
func (sm *SpellManager) ProcessSpells() {
sm.spellProcess.Process()
}
// CastSpell attempts to cast a spell for a character
func (sm *SpellManager) CastSpell(casterID, targetID, spellID int32) error {
// Get the spell from master list
spell := sm.masterList.GetSpell(spellID)
if spell == nil {
return fmt.Errorf("spell %d not found", spellID)
}
// Create LuaSpell instance
luaSpell := NewLuaSpell(spell, casterID)
luaSpell.InitialTarget = targetID
// Check resources
results := sm.resourceChecker.CheckAllResources(luaSpell, 0, 0)
for _, result := range results {
if !result.HasSufficient {
return fmt.Errorf("insufficient resources: %s", result.ErrorMessage)
}
}
// Get targets
targetResult := sm.targeting.GetSpellTargets(luaSpell, nil)
if targetResult.ErrorCode != 0 {
return fmt.Errorf("targeting failed: %s", targetResult.ErrorMessage)
}
if len(targetResult.ValidTargets) == 0 {
return fmt.Errorf("no valid targets found")
}
// TODO: Add spell to cast queue or process immediately
// This would integrate with the entity system to actually cast the spell
return nil
}
// InterruptSpell interrupts a spell being cast by an entity
func (sm *SpellManager) InterruptSpell(entityID, spellID int32, errorCode int16, cancel, fromMovement bool) {
sm.spellProcess.Interrupt(entityID, spellID, errorCode, cancel, fromMovement)
}
// AddSpellToQueue adds a spell to a player's casting queue
func (sm *SpellManager) AddSpellToQueue(spellID, casterID, targetID int32, priority int32) {
sm.spellProcess.AddSpellToQueue(spellID, casterID, targetID, priority)
}
// RemoveSpellFromQueue removes a spell from a player's casting queue
func (sm *SpellManager) RemoveSpellFromQueue(spellID, casterID int32) bool {
return sm.spellProcess.RemoveSpellFromQueue(spellID, casterID)
}
// IsSpellReady checks if a spell is ready to cast (not on cooldown)
func (sm *SpellManager) IsSpellReady(spellID, casterID int32) bool {
return sm.spellProcess.IsReady(spellID, casterID)
}
// GetSpellRecastTime returns remaining recast time for a spell
func (sm *SpellManager) GetSpellRecastTime(spellID, casterID int32) int32 {
remaining := sm.spellProcess.GetRecastTimeRemaining(spellID, casterID)
return int32(remaining.Milliseconds())
}
// CanCastSpell performs comprehensive checks to determine if a spell can be cast
func (sm *SpellManager) CanCastSpell(casterID, targetID, spellID int32) (bool, string) {
// Check if spell exists
spell := sm.masterList.GetSpell(spellID)
if spell == nil {
return false, "Spell not found"
}
// Check if spell is ready (not on cooldown)
if !sm.spellProcess.IsReady(spellID, casterID) {
remaining := sm.spellProcess.GetRecastTimeRemaining(spellID, casterID)
return false, fmt.Sprintf("Spell on cooldown for %d seconds", int(remaining.Seconds()))
}
// Create temporary LuaSpell for resource checks
luaSpell := NewLuaSpell(spell, casterID)
luaSpell.InitialTarget = targetID
// Check resources
results := sm.resourceChecker.CheckAllResources(luaSpell, 0, 0)
for _, result := range results {
if !result.HasSufficient {
return false, result.ErrorMessage
}
}
// Check targeting
targetResult := sm.targeting.GetSpellTargets(luaSpell, nil)
if targetResult.ErrorCode != 0 {
return false, targetResult.ErrorMessage
}
if len(targetResult.ValidTargets) == 0 {
return false, "No valid targets"
}
return true, ""
}
// GetSpellInfo returns comprehensive information about a spell
func (sm *SpellManager) GetSpellInfo(spellID int32) map[string]interface{} {
spell := sm.masterList.GetSpell(spellID)
if spell == nil {
return nil
}
info := make(map[string]interface{})
// Basic spell info
info["id"] = spell.GetSpellID()
info["name"] = spell.GetName()
info["description"] = spell.GetDescription()
info["tier"] = spell.GetSpellTier()
info["type"] = spell.GetSpellData().SpellBookType
// Resource requirements
info["resources"] = sm.resourceChecker.GetResourceSummary(spell)
// Targeting info
info["targeting"] = sm.targeting.GetTargetingInfo(spell)
// Classification
info["is_buff"] = spell.IsBuffSpell()
info["is_debuff"] = !spell.IsBuffSpell() && spell.IsControlSpell()
info["is_heal"] = spell.IsHealSpell()
info["is_damage"] = spell.IsDamageSpell()
info["is_offensive"] = spell.IsOffenseSpell()
return info
}
// GetActiveSpellCount returns the number of active spells being processed
func (sm *SpellManager) GetActiveSpellCount() int {
return sm.spellProcess.GetActiveSpellCount()
}
// Shutdown gracefully shuts down the spell manager
func (sm *SpellManager) Shutdown() {
sm.spellProcess.RemoveAllSpells(false)
}