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
}