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 }