422 lines
9.5 KiB
Go
422 lines
9.5 KiB
Go
package spells
|
|
|
|
import (
|
|
"dk/internal/store"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Spell represents a spell in the game
|
|
type Spell struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
MP int `json:"mp"`
|
|
Attribute int `json:"attribute"`
|
|
Type int `json:"type"`
|
|
}
|
|
|
|
func (s *Spell) Save() error {
|
|
spellStore := GetStore()
|
|
spellStore.UpdateSpell(s)
|
|
return nil
|
|
}
|
|
|
|
func (s *Spell) Delete() error {
|
|
spellStore := GetStore()
|
|
spellStore.RemoveSpell(s.ID)
|
|
return nil
|
|
}
|
|
|
|
// Creates a new Spell with sensible defaults
|
|
func New() *Spell {
|
|
return &Spell{
|
|
Name: "",
|
|
MP: 5, // Default MP cost
|
|
Attribute: 10, // Default attribute value
|
|
Type: TypeHealing, // Default to healing spell
|
|
}
|
|
}
|
|
|
|
// Validate checks if spell has valid values
|
|
func (s *Spell) Validate() error {
|
|
if s.Name == "" {
|
|
return fmt.Errorf("spell name cannot be empty")
|
|
}
|
|
if s.MP < 0 {
|
|
return fmt.Errorf("spell MP cannot be negative")
|
|
}
|
|
if s.Attribute < 0 {
|
|
return fmt.Errorf("spell Attribute cannot be negative")
|
|
}
|
|
if s.Type < TypeHealing || s.Type > TypeDefenseBoost {
|
|
return fmt.Errorf("invalid spell type: %d", s.Type)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SpellType constants for spell types
|
|
const (
|
|
TypeHealing = 1
|
|
TypeHurt = 2
|
|
TypeSleep = 3
|
|
TypeAttackBoost = 4
|
|
TypeDefenseBoost = 5
|
|
)
|
|
|
|
// SpellStore provides in-memory storage with O(1) lookups and spell-specific indices
|
|
type SpellStore struct {
|
|
*store.BaseStore[Spell] // Embedded generic store
|
|
byType map[int][]int // Type -> []ID
|
|
byName map[string]int // Name (lowercase) -> ID
|
|
byMP map[int][]int // MP -> []ID
|
|
allByTypeMP []int // All IDs sorted by type, MP, ID
|
|
mu sync.RWMutex // Protects indices
|
|
}
|
|
|
|
// Global in-memory store
|
|
var spellStore *SpellStore
|
|
var storeOnce sync.Once
|
|
|
|
// Initialize the in-memory store
|
|
func initStore() {
|
|
spellStore = &SpellStore{
|
|
BaseStore: store.NewBaseStore[Spell](),
|
|
byType: make(map[int][]int),
|
|
byName: make(map[string]int),
|
|
byMP: make(map[int][]int),
|
|
allByTypeMP: make([]int, 0),
|
|
}
|
|
}
|
|
|
|
// GetStore returns the global spell store
|
|
func GetStore() *SpellStore {
|
|
storeOnce.Do(initStore)
|
|
return spellStore
|
|
}
|
|
|
|
// AddSpell adds a spell to the in-memory store and updates all indices
|
|
func (ss *SpellStore) AddSpell(spell *Spell) {
|
|
ss.mu.Lock()
|
|
defer ss.mu.Unlock()
|
|
|
|
// Validate spell
|
|
if err := spell.Validate(); err != nil {
|
|
return
|
|
}
|
|
|
|
// Add to base store
|
|
ss.Add(spell.ID, spell)
|
|
|
|
// Rebuild indices
|
|
ss.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
// RemoveSpell removes a spell from the store and updates indices
|
|
func (ss *SpellStore) RemoveSpell(id int) {
|
|
ss.mu.Lock()
|
|
defer ss.mu.Unlock()
|
|
|
|
// Remove from base store
|
|
ss.Remove(id)
|
|
|
|
// Rebuild indices
|
|
ss.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
// UpdateSpell updates a spell efficiently
|
|
func (ss *SpellStore) UpdateSpell(spell *Spell) {
|
|
ss.mu.Lock()
|
|
defer ss.mu.Unlock()
|
|
|
|
// Validate spell
|
|
if err := spell.Validate(); err != nil {
|
|
return
|
|
}
|
|
|
|
// Update base store
|
|
ss.Add(spell.ID, spell)
|
|
|
|
// Rebuild indices
|
|
ss.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
// LoadData loads spell data from JSON file, or starts with empty store
|
|
func LoadData(dataPath string) error {
|
|
ss := GetStore()
|
|
|
|
// Load from base store, which handles JSON loading
|
|
if err := ss.BaseStore.LoadData(dataPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Rebuild indices from loaded data
|
|
ss.rebuildIndices()
|
|
return nil
|
|
}
|
|
|
|
// SaveData saves spell data to JSON file
|
|
func SaveData(dataPath string) error {
|
|
ss := GetStore()
|
|
return ss.BaseStore.SaveData(dataPath)
|
|
}
|
|
|
|
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
|
|
func (ss *SpellStore) rebuildIndicesUnsafe() {
|
|
// Clear indices
|
|
ss.byType = make(map[int][]int)
|
|
ss.byName = make(map[string]int)
|
|
ss.byMP = make(map[int][]int)
|
|
ss.allByTypeMP = make([]int, 0)
|
|
|
|
// Collect all spells and build indices
|
|
allSpells := ss.GetAll()
|
|
|
|
for id, spell := range allSpells {
|
|
// Type index
|
|
ss.byType[spell.Type] = append(ss.byType[spell.Type], id)
|
|
|
|
// Name index (case-insensitive)
|
|
ss.byName[strings.ToLower(spell.Name)] = id
|
|
|
|
// MP index
|
|
ss.byMP[spell.MP] = append(ss.byMP[spell.MP], id)
|
|
|
|
// All IDs
|
|
ss.allByTypeMP = append(ss.allByTypeMP, id)
|
|
}
|
|
|
|
// Sort allByTypeMP by type, then MP, then ID
|
|
sort.Slice(ss.allByTypeMP, func(i, j int) bool {
|
|
spellI, _ := ss.GetByID(ss.allByTypeMP[i])
|
|
spellJ, _ := ss.GetByID(ss.allByTypeMP[j])
|
|
if spellI.Type != spellJ.Type {
|
|
return spellI.Type < spellJ.Type
|
|
}
|
|
if spellI.MP != spellJ.MP {
|
|
return spellI.MP < spellJ.MP
|
|
}
|
|
return ss.allByTypeMP[i] < ss.allByTypeMP[j]
|
|
})
|
|
|
|
// Sort type indices by MP, then ID
|
|
for spellType := range ss.byType {
|
|
sort.Slice(ss.byType[spellType], func(i, j int) bool {
|
|
spellI, _ := ss.GetByID(ss.byType[spellType][i])
|
|
spellJ, _ := ss.GetByID(ss.byType[spellType][j])
|
|
if spellI.MP != spellJ.MP {
|
|
return spellI.MP < spellJ.MP
|
|
}
|
|
return ss.byType[spellType][i] < ss.byType[spellType][j]
|
|
})
|
|
}
|
|
|
|
// Sort MP indices by type, then ID
|
|
for mp := range ss.byMP {
|
|
sort.Slice(ss.byMP[mp], func(i, j int) bool {
|
|
spellI, _ := ss.GetByID(ss.byMP[mp][i])
|
|
spellJ, _ := ss.GetByID(ss.byMP[mp][j])
|
|
if spellI.Type != spellJ.Type {
|
|
return spellI.Type < spellJ.Type
|
|
}
|
|
return ss.byMP[mp][i] < ss.byMP[mp][j]
|
|
})
|
|
}
|
|
}
|
|
|
|
// rebuildIndices rebuilds all spell-specific indices from base store data
|
|
func (ss *SpellStore) rebuildIndices() {
|
|
ss.mu.Lock()
|
|
defer ss.mu.Unlock()
|
|
ss.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
// Retrieves a spell by ID
|
|
func Find(id int) (*Spell, error) {
|
|
ss := GetStore()
|
|
spell, exists := ss.GetByID(id)
|
|
if !exists {
|
|
return nil, fmt.Errorf("spell with ID %d not found", id)
|
|
}
|
|
return spell, nil
|
|
}
|
|
|
|
// Retrieves all spells
|
|
func All() ([]*Spell, error) {
|
|
ss := GetStore()
|
|
ss.mu.RLock()
|
|
defer ss.mu.RUnlock()
|
|
|
|
result := make([]*Spell, 0, len(ss.allByTypeMP))
|
|
for _, id := range ss.allByTypeMP {
|
|
if spell, exists := ss.GetByID(id); exists {
|
|
result = append(result, spell)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Retrieves spells by type
|
|
func ByType(spellType int) ([]*Spell, error) {
|
|
ss := GetStore()
|
|
ss.mu.RLock()
|
|
defer ss.mu.RUnlock()
|
|
|
|
ids, exists := ss.byType[spellType]
|
|
if !exists {
|
|
return []*Spell{}, nil
|
|
}
|
|
|
|
result := make([]*Spell, 0, len(ids))
|
|
for _, id := range ids {
|
|
if spell, exists := ss.GetByID(id); exists {
|
|
result = append(result, spell)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Retrieves spells that cost at most the specified MP
|
|
func ByMaxMP(maxMP int) ([]*Spell, error) {
|
|
ss := GetStore()
|
|
ss.mu.RLock()
|
|
defer ss.mu.RUnlock()
|
|
|
|
var result []*Spell
|
|
for mp := 0; mp <= maxMP; mp++ {
|
|
if ids, exists := ss.byMP[mp]; exists {
|
|
for _, id := range ids {
|
|
if spell, exists := ss.GetByID(id); exists {
|
|
result = append(result, spell)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Retrieves spells of a specific type that cost at most the specified MP
|
|
func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) {
|
|
ss := GetStore()
|
|
ss.mu.RLock()
|
|
defer ss.mu.RUnlock()
|
|
|
|
ids, exists := ss.byType[spellType]
|
|
if !exists {
|
|
return []*Spell{}, nil
|
|
}
|
|
|
|
var result []*Spell
|
|
for _, id := range ids {
|
|
if spell, exists := ss.GetByID(id); exists && spell.MP <= maxMP {
|
|
result = append(result, spell)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Retrieves a spell by name (case-insensitive)
|
|
func ByName(name string) (*Spell, error) {
|
|
ss := GetStore()
|
|
ss.mu.RLock()
|
|
defer ss.mu.RUnlock()
|
|
|
|
id, exists := ss.byName[strings.ToLower(name)]
|
|
if !exists {
|
|
return nil, fmt.Errorf("spell with name '%s' not found", name)
|
|
}
|
|
|
|
spell, exists := ss.GetByID(id)
|
|
if !exists {
|
|
return nil, fmt.Errorf("spell with name '%s' not found", name)
|
|
}
|
|
|
|
return spell, nil
|
|
}
|
|
|
|
// Saves a new spell to the in-memory store and sets the ID
|
|
func (s *Spell) Insert() error {
|
|
ss := GetStore()
|
|
|
|
// Validate before insertion
|
|
if err := s.Validate(); err != nil {
|
|
return fmt.Errorf("validation failed: %w", err)
|
|
}
|
|
|
|
// Assign new ID if not set
|
|
if s.ID == 0 {
|
|
s.ID = ss.GetNextID()
|
|
}
|
|
|
|
// Add to store
|
|
ss.AddSpell(s)
|
|
return nil
|
|
}
|
|
|
|
// Returns true if the spell is a healing spell
|
|
func (s *Spell) IsHealing() bool {
|
|
return s.Type == TypeHealing
|
|
}
|
|
|
|
// Returns true if the spell is a hurt spell
|
|
func (s *Spell) IsHurt() bool {
|
|
return s.Type == TypeHurt
|
|
}
|
|
|
|
// Returns true if the spell is a sleep spell
|
|
func (s *Spell) IsSleep() bool {
|
|
return s.Type == TypeSleep
|
|
}
|
|
|
|
// Returns true if the spell boosts attack
|
|
func (s *Spell) IsAttackBoost() bool {
|
|
return s.Type == TypeAttackBoost
|
|
}
|
|
|
|
// Returns true if the spell boosts defense
|
|
func (s *Spell) IsDefenseBoost() bool {
|
|
return s.Type == TypeDefenseBoost
|
|
}
|
|
|
|
// Returns the string representation of the spell type
|
|
func (s *Spell) TypeName() string {
|
|
switch s.Type {
|
|
case TypeHealing:
|
|
return "Healing"
|
|
case TypeHurt:
|
|
return "Hurt"
|
|
case TypeSleep:
|
|
return "Sleep"
|
|
case TypeAttackBoost:
|
|
return "Attack Boost"
|
|
case TypeDefenseBoost:
|
|
return "Defense Boost"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
// Returns true if the spell can be cast with the given MP
|
|
func (s *Spell) CanCast(availableMP int) bool {
|
|
return availableMP >= s.MP
|
|
}
|
|
|
|
// Returns the attribute per MP ratio (higher is more efficient)
|
|
func (s *Spell) Efficiency() float64 {
|
|
if s.MP == 0 {
|
|
return 0
|
|
}
|
|
return float64(s.Attribute) / float64(s.MP)
|
|
}
|
|
|
|
// Returns true if the spell is used for attacking
|
|
func (s *Spell) IsOffensive() bool {
|
|
return s.Type == TypeHurt || s.Type == TypeSleep
|
|
}
|
|
|
|
// Returns true if the spell is used for support/buffs
|
|
func (s *Spell) IsSupport() bool {
|
|
return s.Type == TypeHealing || s.Type == TypeAttackBoost || s.Type == TypeDefenseBoost
|
|
}
|