312 lines
7.5 KiB
Go

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(),
}
}