package spells import ( "fmt" "dk/internal/database" "dk/internal/helpers/scanner" "zombiezen.com/go/sqlite" ) // Spell represents a spell in the database type Spell struct { ID int `db:"id" json:"id"` Name string `db:"name" json:"name"` MP int `db:"mp" json:"mp"` Attribute int `db:"attribute" json:"attribute"` Type int `db:"type" json:"type"` } // New 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 } } var spellScanner = scanner.New[Spell]() // spellColumns returns the column list for spell queries func spellColumns() string { return spellScanner.Columns() } // scanSpell populates a Spell struct using the fast scanner func scanSpell(stmt *sqlite.Stmt) *Spell { spell := &Spell{} spellScanner.Scan(stmt, spell) return spell } // SpellType constants for spell types const ( TypeHealing = 1 TypeHurt = 2 TypeSleep = 3 TypeAttackBoost = 4 TypeDefenseBoost = 5 ) // Find retrieves a spell by ID func Find(id int) (*Spell, error) { var spell *Spell query := `SELECT ` + spellColumns() + ` FROM spells WHERE id = ?` err := database.Query(query, func(stmt *sqlite.Stmt) error { spell = scanSpell(stmt) return nil }, id) if err != nil { return nil, fmt.Errorf("failed to find spell: %w", err) } if spell == nil { return nil, fmt.Errorf("spell with ID %d not found", id) } return spell, nil } // All retrieves all spells func All() ([]*Spell, error) { var spells []*Spell query := `SELECT ` + spellColumns() + ` FROM spells ORDER BY type, mp, id` err := database.Query(query, func(stmt *sqlite.Stmt) error { spell := scanSpell(stmt) spells = append(spells, spell) return nil }) if err != nil { return nil, fmt.Errorf("failed to retrieve all spells: %w", err) } return spells, nil } // ByType retrieves spells by type func ByType(spellType int) ([]*Spell, error) { var spells []*Spell query := `SELECT ` + spellColumns() + ` FROM spells WHERE type = ? ORDER BY mp, id` err := database.Query(query, func(stmt *sqlite.Stmt) error { spell := scanSpell(stmt) spells = append(spells, spell) return nil }, spellType) if err != nil { return nil, fmt.Errorf("failed to retrieve spells by type: %w", err) } return spells, nil } // ByMaxMP retrieves spells that cost at most the specified MP func ByMaxMP(maxMP int) ([]*Spell, error) { var spells []*Spell query := `SELECT ` + spellColumns() + ` FROM spells WHERE mp <= ? ORDER BY type, mp, id` err := database.Query(query, func(stmt *sqlite.Stmt) error { spell := scanSpell(stmt) spells = append(spells, spell) return nil }, maxMP) if err != nil { return nil, fmt.Errorf("failed to retrieve spells by max MP: %w", err) } return spells, nil } // ByTypeAndMaxMP retrieves spells of a specific type that cost at most the specified MP func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) { var spells []*Spell query := `SELECT ` + spellColumns() + ` FROM spells WHERE type = ? AND mp <= ? ORDER BY mp, id` err := database.Query(query, func(stmt *sqlite.Stmt) error { spell := scanSpell(stmt) spells = append(spells, spell) return nil }, spellType, maxMP) if err != nil { return nil, fmt.Errorf("failed to retrieve spells by type and max MP: %w", err) } return spells, nil } // ByName retrieves a spell by name (case-insensitive) func ByName(name string) (*Spell, error) { var spell *Spell query := `SELECT ` + spellColumns() + ` FROM spells WHERE LOWER(name) = LOWER(?) LIMIT 1` err := database.Query(query, func(stmt *sqlite.Stmt) error { spell = scanSpell(stmt) return nil }, name) if err != nil { return nil, fmt.Errorf("failed to find spell by name: %w", err) } if spell == nil { return nil, fmt.Errorf("spell with name '%s' not found", name) } return spell, nil } // Save updates an existing spell in the database func (s *Spell) Save() error { if s.ID == 0 { return fmt.Errorf("cannot save spell without ID") } query := `UPDATE spells SET name = ?, mp = ?, attribute = ?, type = ? WHERE id = ?` return database.Exec(query, s.Name, s.MP, s.Attribute, s.Type, s.ID) } // Insert saves a new spell to the database and sets the ID func (s *Spell) Insert() error { if s.ID != 0 { return fmt.Errorf("spell already has ID %d, use Save() to update", s.ID) } // Use a transaction to ensure we can get the ID err := database.Transaction(func(tx *database.Tx) error { query := `INSERT INTO spells (name, mp, attribute, type) VALUES (?, ?, ?, ?)` if err := tx.Exec(query, s.Name, s.MP, s.Attribute, s.Type); err != nil { return fmt.Errorf("failed to insert spell: %w", err) } // Get the last insert ID var id int err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { id = stmt.ColumnInt(0) return nil }) if err != nil { return fmt.Errorf("failed to get insert ID: %w", err) } s.ID = id return nil }) return err } // Delete removes the spell from the database func (s *Spell) Delete() error { if s.ID == 0 { return fmt.Errorf("cannot delete spell without ID") } query := "DELETE FROM spells WHERE id = ?" return database.Exec(query, s.ID) } // IsHealing returns true if the spell is a healing spell func (s *Spell) IsHealing() bool { return s.Type == TypeHealing } // IsHurt returns true if the spell is a hurt spell func (s *Spell) IsHurt() bool { return s.Type == TypeHurt } // IsSleep returns true if the spell is a sleep spell func (s *Spell) IsSleep() bool { return s.Type == TypeSleep } // IsAttackBoost returns true if the spell boosts attack func (s *Spell) IsAttackBoost() bool { return s.Type == TypeAttackBoost } // IsDefenseBoost returns true if the spell boosts defense func (s *Spell) IsDefenseBoost() bool { return s.Type == TypeDefenseBoost } // TypeName 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" } } // CanCast returns true if the spell can be cast with the given MP func (s *Spell) CanCast(availableMP int) bool { return availableMP >= s.MP } // Efficiency 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) } // IsOffensive returns true if the spell is used for attacking func (s *Spell) IsOffensive() bool { return s.Type == TypeHurt || s.Type == TypeSleep } // IsSupport 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 } // ToMap converts the spell to a map for efficient template rendering func (s *Spell) ToMap() map[string]any { return map[string]any{ "ID": s.ID, "Name": s.Name, "MP": s.MP, "Attribute": s.Attribute, "Type": s.Type, // Computed values "IsHealing": s.IsHealing(), "IsHurt": s.IsHurt(), "IsSleep": s.IsSleep(), "IsAttackBoost": s.IsAttackBoost(), "IsDefenseBoost": s.IsDefenseBoost(), "TypeName": s.TypeName(), "Efficiency": s.Efficiency(), "IsOffensive": s.IsOffensive(), "IsSupport": s.IsSupport(), } }