diff --git a/internal/spells/builder.go b/internal/spells/builder.go new file mode 100644 index 0000000..ae5472d --- /dev/null +++ b/internal/spells/builder.go @@ -0,0 +1,88 @@ +package spells + +import ( + "dk/internal/database" + "fmt" + + "zombiezen.com/go/sqlite" +) + +// Builder provides a fluent interface for creating spells +type Builder struct { + spell *Spell + db *database.DB +} + +// NewBuilder creates a new spell builder +func NewBuilder(db *database.DB) *Builder { + return &Builder{ + spell: &Spell{db: db}, + db: db, + } +} + +// WithName sets the spell name +func (b *Builder) WithName(name string) *Builder { + b.spell.Name = name + return b +} + +// WithMP sets the spell's mana point cost +func (b *Builder) WithMP(mp int) *Builder { + b.spell.MP = mp + return b +} + +// WithAttribute sets the spell's attribute (power/effectiveness) +func (b *Builder) WithAttribute(attribute int) *Builder { + b.spell.Attribute = attribute + return b +} + +// WithType sets the spell type +func (b *Builder) WithType(spellType int) *Builder { + b.spell.Type = spellType + return b +} + +// Create saves the spell to the database and returns it +func (b *Builder) Create() (*Spell, error) { + // Use a transaction to ensure we can get the ID + var spell *Spell + err := b.db.Transaction(func(tx *database.Tx) error { + query := `INSERT INTO spells (name, mp, attribute, type) + VALUES (?, ?, ?, ?)` + + if err := tx.Exec(query, b.spell.Name, b.spell.MP, b.spell.Attribute, b.spell.Type); err != nil { + return fmt.Errorf("failed to insert spell: %w", err) + } + + // Get the last inserted ID within the same transaction + var lastID int + err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { + lastID = stmt.ColumnInt(0) + return nil + }) + if err != nil { + return fmt.Errorf("failed to get last insert ID: %w", err) + } + + // Create the spell with the ID + spell = &Spell{ + ID: lastID, + Name: b.spell.Name, + MP: b.spell.MP, + Attribute: b.spell.Attribute, + Type: b.spell.Type, + db: b.db, + } + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to create spell: %w", err) + } + + return spell, nil +} diff --git a/internal/spells/doc.go b/internal/spells/doc.go new file mode 100644 index 0000000..afada45 --- /dev/null +++ b/internal/spells/doc.go @@ -0,0 +1,267 @@ +/* +Package spells is the active record implementation for spells in the game. + +# Basic Usage + +To retrieve a spell by ID: + + spell, err := spells.Find(db, 1) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Found spell: %s (MP: %d, Power: %d)\n", spell.Name, spell.MP, spell.Attribute) + +To get all spells: + + allSpells, err := spells.All(db) + if err != nil { + log.Fatal(err) + } + for _, spell := range allSpells { + fmt.Printf("Spell: %s\n", spell.Name) + } + +To find a spell by name: + + heal, err := spells.ByName(db, "Heal") + if err != nil { + log.Fatal(err) + } + +To filter spells by type: + + healingSpells, err := spells.ByType(db, spells.TypeHealing) + if err != nil { + log.Fatal(err) + } + +To get spells within MP budget: + + affordableSpells, err := spells.ByMaxMP(db, 10) + if err != nil { + log.Fatal(err) + } + +To get spells of specific type within MP budget: + + cheapHurtSpells, err := spells.ByTypeAndMaxMP(db, spells.TypeHurt, 15) + if err != nil { + log.Fatal(err) + } + +# Creating Spells with Builder Pattern + +The package provides a fluent builder interface for creating new spells: + + spell, err := spells.NewBuilder(db). + WithName("Lightning Bolt"). + WithMP(25). + WithAttribute(70). + WithType(spells.TypeHurt). + Create() + + if err != nil { + log.Fatal(err) + } + fmt.Printf("Created spell with ID: %d\n", spell.ID) + +# Updating Spells + +Spells can be modified and saved back to the database: + + spell, _ := spells.Find(db, 1) + spell.Name = "Greater Heal" + spell.MP = 8 + spell.Attribute = 20 + + err := spell.Save() + if err != nil { + log.Fatal(err) + } + +# Deleting Spells + +Spells can be removed from the database: + + spell, _ := spells.Find(db, 1) + err := spell.Delete() + if err != nil { + log.Fatal(err) + } + +# Spell Types + +The package defines spell type constants: + + spells.TypeHealing = 1 // Healing spells (Heal, Revive, Life, Breath, Gaia) + spells.TypeHurt = 2 // Hurt spells (Hurt, Pain, Maim, Rend, Chaos) + spells.TypeSleep = 3 // Sleep spells (Sleep, Dream, Nightmare) + spells.TypeAttackBoost = 4 // Attack boost spells (Craze, Rage, Fury) + spells.TypeDefenseBoost = 5 // Defense boost spells (Ward, Fend, Barrier) + +Helper methods are available to check spell types: + + if spell.IsHealing() { + fmt.Println("This spell heals the caster") + } + if spell.IsHurt() { + fmt.Println("This spell damages enemies") + } + if spell.IsSleep() { + fmt.Println("This spell puts enemies to sleep") + } + if spell.IsAttackBoost() { + fmt.Println("This spell boosts attack power") + } + if spell.IsDefenseBoost() { + fmt.Println("This spell boosts defense") + } + + fmt.Printf("Spell type: %s\n", spell.TypeName()) + +# Database Schema + +The spells table has the following structure: + + CREATE TABLE spells ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + mp INTEGER NOT NULL DEFAULT 0, + attribute INTEGER NOT NULL DEFAULT 0, + type INTEGER NOT NULL DEFAULT 0 + ) + +Where: + - id: Unique identifier + - name: Display name of the spell + - mp: Mana points required to cast the spell + - attribute: Power/effectiveness of the spell + - type: Spell type (1-5 as defined above) + +# Spell Mechanics + +## Mana Point System + +All spells require MP to cast: + + if spell.CanCast(player.CurrentMP) { + fmt.Printf("You can cast %s\n", spell.Name) + } else { + fmt.Printf("Not enough MP to cast %s (need %d, have %d)\n", + spell.Name, spell.MP, player.CurrentMP) + } + +## Spell Efficiency + +Calculate efficiency as attribute per MP cost: + + efficiency := spell.Efficiency() + fmt.Printf("%s efficiency: %.2f power per MP\n", spell.Name, efficiency) + +This helps players choose the most cost-effective spells. + +## Offensive vs Support Spells + +Spells are categorized by their primary purpose: + + if spell.IsOffensive() { + // Hurt and Sleep spells - used against enemies + fmt.Println("Offensive spell - targets enemies") + } + + if spell.IsSupport() { + // Healing and boost spells - used to help player + fmt.Println("Support spell - helps the caster") + } + +# Spell Categories + +## Healing Spells (Type 1) + +Restore hit points to the caster: +- Heal: Basic healing +- Revive: Moderate healing +- Life: Strong healing +- Breath: Very strong healing +- Gaia: Ultimate healing + +## Hurt Spells (Type 2) + +Deal damage to enemies: +- Hurt: Basic damage +- Pain: Moderate damage +- Maim: Strong damage +- Rend: Very strong damage +- Chaos: Ultimate damage + +Note: Some monsters have immunity to Hurt spells. + +## Sleep Spells (Type 3) + +Put enemies to sleep, preventing them from attacking: +- Sleep: Basic sleep effect +- Dream: Moderate sleep effect +- Nightmare: Strong sleep effect + +Note: Some monsters have immunity to Sleep spells. + +## Attack Boost Spells (Type 4) + +Temporarily increase the caster's attack power: +- Craze: Basic attack boost +- Rage: Moderate attack boost +- Fury: Strong attack boost + +## Defense Boost Spells (Type 5) + +Temporarily increase the caster's defense: +- Ward: Basic defense boost +- Fend: Moderate defense boost +- Barrier: Strong defense boost + +# Query Patterns + +## Finding Castable Spells + +Get spells a player can cast with their current MP: + + currentMP := 25 + castableSpells, err := spells.ByMaxMP(db, currentMP) + + // Filter by type and MP budget + castableHurtSpells, err := spells.ByTypeAndMaxMP(db, spells.TypeHurt, currentMP) + +## Spell Progression + +Players typically learn spells in order of power/cost: + + // Get all healing spells ordered by cost + healingSpells, err := spells.ByType(db, spells.TypeHealing) + // Results are ordered by MP cost automatically + +## Combat Spell Selection + +For combat AI or recommendations: + + // Get most efficient hurt spells within budget + hurtSpells, err := spells.ByTypeAndMaxMP(db, spells.TypeHurt, availableMP) + + // Find most efficient spell + var bestSpell *spells.Spell + var bestEfficiency float64 + + for _, spell := range hurtSpells { + if eff := spell.Efficiency(); eff > bestEfficiency { + bestEfficiency = eff + bestSpell = spell + } + } + +# Error Handling + +All functions return appropriate errors for common failure cases: + - Spell not found (Find/ByName returns error for non-existent spells) + - Database connection issues + - Invalid operations (e.g., saving/deleting spells without IDs) +*/ +package spells \ No newline at end of file diff --git a/internal/spells/spells.go b/internal/spells/spells.go new file mode 100644 index 0000000..543b73c --- /dev/null +++ b/internal/spells/spells.go @@ -0,0 +1,265 @@ +package spells + +import ( + "fmt" + + "dk/internal/database" + + "zombiezen.com/go/sqlite" +) + +// Spell represents a spell in the database +type Spell struct { + ID int `json:"id"` + Name string `json:"name"` + MP int `json:"mp"` + Attribute int `json:"attribute"` + Type int `json:"type"` + + db *database.DB +} + +// SpellType constants for spell types +const ( + TypeHealing = 1 + TypeHurt = 2 + TypeSleep = 3 + TypeAttackBoost = 4 + TypeDefenseBoost = 5 +) + +// Find retrieves a spell by ID +func Find(db *database.DB, id int) (*Spell, error) { + spell := &Spell{db: db} + + query := "SELECT id, name, mp, attribute, type FROM spells WHERE id = ?" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + spell.ID = stmt.ColumnInt(0) + spell.Name = stmt.ColumnText(1) + spell.MP = stmt.ColumnInt(2) + spell.Attribute = stmt.ColumnInt(3) + spell.Type = stmt.ColumnInt(4) + return nil + }, id) + + if err != nil { + return nil, fmt.Errorf("failed to find spell: %w", err) + } + + if spell.ID == 0 { + return nil, fmt.Errorf("spell with ID %d not found", id) + } + + return spell, nil +} + +// All retrieves all spells +func All(db *database.DB) ([]*Spell, error) { + var spells []*Spell + + query := "SELECT id, name, mp, attribute, type FROM spells ORDER BY type, mp, id" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + spell := &Spell{ + ID: stmt.ColumnInt(0), + Name: stmt.ColumnText(1), + MP: stmt.ColumnInt(2), + Attribute: stmt.ColumnInt(3), + Type: stmt.ColumnInt(4), + db: db, + } + 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(db *database.DB, spellType int) ([]*Spell, error) { + var spells []*Spell + + query := "SELECT id, name, mp, attribute, type FROM spells WHERE type = ? ORDER BY mp, id" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + spell := &Spell{ + ID: stmt.ColumnInt(0), + Name: stmt.ColumnText(1), + MP: stmt.ColumnInt(2), + Attribute: stmt.ColumnInt(3), + Type: stmt.ColumnInt(4), + db: db, + } + 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(db *database.DB, maxMP int) ([]*Spell, error) { + var spells []*Spell + + query := "SELECT id, name, mp, attribute, type FROM spells WHERE mp <= ? ORDER BY type, mp, id" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + spell := &Spell{ + ID: stmt.ColumnInt(0), + Name: stmt.ColumnText(1), + MP: stmt.ColumnInt(2), + Attribute: stmt.ColumnInt(3), + Type: stmt.ColumnInt(4), + db: db, + } + 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(db *database.DB, spellType, maxMP int) ([]*Spell, error) { + var spells []*Spell + + query := "SELECT id, name, mp, attribute, type FROM spells WHERE type = ? AND mp <= ? ORDER BY mp, id" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + spell := &Spell{ + ID: stmt.ColumnInt(0), + Name: stmt.ColumnText(1), + MP: stmt.ColumnInt(2), + Attribute: stmt.ColumnInt(3), + Type: stmt.ColumnInt(4), + db: db, + } + 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(db *database.DB, name string) (*Spell, error) { + spell := &Spell{db: db} + + query := "SELECT id, name, mp, attribute, type FROM spells WHERE LOWER(name) = LOWER(?) LIMIT 1" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + spell.ID = stmt.ColumnInt(0) + spell.Name = stmt.ColumnText(1) + spell.MP = stmt.ColumnInt(2) + spell.Attribute = stmt.ColumnInt(3) + spell.Type = stmt.ColumnInt(4) + return nil + }, name) + + if err != nil { + return nil, fmt.Errorf("failed to find spell by name: %w", err) + } + + if spell.ID == 0 { + 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 s.db.Exec(query, s.Name, s.MP, s.Attribute, s.Type, s.ID) +} + +// 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 s.db.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 +} diff --git a/internal/spells/spells_test.go b/internal/spells/spells_test.go new file mode 100644 index 0000000..b259bbb --- /dev/null +++ b/internal/spells/spells_test.go @@ -0,0 +1,445 @@ +package spells + +import ( + "os" + "testing" + + "dk/internal/database" +) + +func setupTestDB(t *testing.T) *database.DB { + testDB := "test_spells.db" + t.Cleanup(func() { + os.Remove(testDB) + }) + + db, err := database.Open(testDB) + if err != nil { + t.Fatalf("Failed to open test database: %v", err) + } + + // Create spells table + createTable := `CREATE TABLE spells ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + mp INTEGER NOT NULL DEFAULT 0, + attribute INTEGER NOT NULL DEFAULT 0, + type INTEGER NOT NULL DEFAULT 0 + )` + + if err := db.Exec(createTable); err != nil { + t.Fatalf("Failed to create spells table: %v", err) + } + + // Insert test data + testSpells := `INSERT INTO spells (name, mp, attribute, type) VALUES + ('Heal', 5, 10, 1), + ('Revive', 10, 25, 1), + ('Hurt', 5, 15, 2), + ('Pain', 12, 35, 2), + ('Sleep', 10, 5, 3), + ('Dream', 30, 9, 3), + ('Craze', 10, 10, 4), + ('Ward', 10, 10, 5)` + + if err := db.Exec(testSpells); err != nil { + t.Fatalf("Failed to insert test spells: %v", err) + } + + return db +} + +func TestFind(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Test finding existing spell + spell, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find spell: %v", err) + } + + if spell.ID != 1 { + t.Errorf("Expected ID 1, got %d", spell.ID) + } + if spell.Name != "Heal" { + t.Errorf("Expected name 'Heal', got '%s'", spell.Name) + } + if spell.MP != 5 { + t.Errorf("Expected MP 5, got %d", spell.MP) + } + if spell.Attribute != 10 { + t.Errorf("Expected attribute 10, got %d", spell.Attribute) + } + if spell.Type != TypeHealing { + t.Errorf("Expected type %d, got %d", TypeHealing, spell.Type) + } + + // Test finding non-existent spell + _, err = Find(db, 999) + if err == nil { + t.Error("Expected error when finding non-existent spell") + } +} + +func TestAll(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + spells, err := All(db) + if err != nil { + t.Fatalf("Failed to get all spells: %v", err) + } + + if len(spells) != 8 { + t.Errorf("Expected 8 spells, got %d", len(spells)) + } + + // Check ordering (by type, then MP, then ID) + if spells[0].Type > spells[1].Type { + t.Error("Expected spells to be ordered by type first") + } +} + +func TestByType(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Test healing spells + healingSpells, err := ByType(db, TypeHealing) + if err != nil { + t.Fatalf("Failed to get healing spells: %v", err) + } + + if len(healingSpells) != 2 { + t.Errorf("Expected 2 healing spells, got %d", len(healingSpells)) + } + + for _, spell := range healingSpells { + if spell.Type != TypeHealing { + t.Errorf("Expected healing spell, got type %d", spell.Type) + } + } + + // Test hurt spells + hurtSpells, err := ByType(db, TypeHurt) + if err != nil { + t.Fatalf("Failed to get hurt spells: %v", err) + } + + if len(hurtSpells) != 2 { + t.Errorf("Expected 2 hurt spells, got %d", len(hurtSpells)) + } + + // Verify ordering within type (by MP) + if hurtSpells[0].MP > hurtSpells[1].MP { + t.Error("Expected spells within type to be ordered by MP") + } +} + +func TestByMaxMP(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Test spells with MP <= 10 + lowMPSpells, err := ByMaxMP(db, 10) + if err != nil { + t.Fatalf("Failed to get low MP spells: %v", err) + } + + expectedCount := 6 // Heal(5), Hurt(5), Sleep(10), Craze(10), Revive(10), Ward(10) + if len(lowMPSpells) != expectedCount { + t.Errorf("Expected %d spells with MP <= 10, got %d", expectedCount, len(lowMPSpells)) + } + + // Verify all spells have MP <= 10 + for _, spell := range lowMPSpells { + if spell.MP > 10 { + t.Errorf("Spell %s has MP %d, expected <= 10", spell.Name, spell.MP) + } + } + + // Test very low MP threshold + veryLowMPSpells, err := ByMaxMP(db, 5) + if err != nil { + t.Fatalf("Failed to get very low MP spells: %v", err) + } + + if len(veryLowMPSpells) != 2 { // Only Heal and Hurt + t.Errorf("Expected 2 spells with MP <= 5, got %d", len(veryLowMPSpells)) + } +} + +func TestByTypeAndMaxMP(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Test healing spells with MP <= 10 + healingSpells, err := ByTypeAndMaxMP(db, TypeHealing, 10) + if err != nil { + t.Fatalf("Failed to get healing spells with MP <= 10: %v", err) + } + + expectedCount := 2 // Heal(5) and Revive(10) + if len(healingSpells) != expectedCount { + t.Errorf("Expected %d healing spells with MP <= 10, got %d", expectedCount, len(healingSpells)) + } + + // Verify all are healing spells and within MP limit + for _, spell := range healingSpells { + if spell.Type != TypeHealing { + t.Errorf("Expected healing spell, got type %d", spell.Type) + } + if spell.MP > 10 { + t.Errorf("Spell %s has MP %d, expected <= 10", spell.Name, spell.MP) + } + } + + // Test hurt spells with very low MP + lowHurtSpells, err := ByTypeAndMaxMP(db, TypeHurt, 5) + if err != nil { + t.Fatalf("Failed to get hurt spells with MP <= 5: %v", err) + } + + if len(lowHurtSpells) != 1 { // Only Hurt(5) + t.Errorf("Expected 1 hurt spell with MP <= 5, got %d", len(lowHurtSpells)) + } +} + +func TestByName(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Test finding existing spell by name + spell, err := ByName(db, "Heal") + if err != nil { + t.Fatalf("Failed to find spell by name: %v", err) + } + + if spell.Name != "Heal" { + t.Errorf("Expected name 'Heal', got '%s'", spell.Name) + } + if spell.Type != TypeHealing { + t.Errorf("Expected healing spell, got type %d", spell.Type) + } + + // Test case insensitivity + spellLower, err := ByName(db, "heal") + if err != nil { + t.Fatalf("Failed to find spell by lowercase name: %v", err) + } + + if spellLower.ID != spell.ID { + t.Error("Case insensitive search should return same spell") + } + + // Test non-existent spell + _, err = ByName(db, "Fireball") + if err == nil { + t.Error("Expected error when finding non-existent spell by name") + } +} + +func TestBuilder(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Create new spell using builder + spell, err := NewBuilder(db). + WithName("Lightning"). + WithMP(25). + WithAttribute(60). + WithType(TypeHurt). + Create() + + if err != nil { + t.Fatalf("Failed to create spell with builder: %v", err) + } + + if spell.ID == 0 { + t.Error("Expected non-zero ID after creation") + } + if spell.Name != "Lightning" { + t.Errorf("Expected name 'Lightning', got '%s'", spell.Name) + } + if spell.MP != 25 { + t.Errorf("Expected MP 25, got %d", spell.MP) + } + if spell.Attribute != 60 { + t.Errorf("Expected attribute 60, got %d", spell.Attribute) + } + if spell.Type != TypeHurt { + t.Errorf("Expected type %d, got %d", TypeHurt, spell.Type) + } + + // Verify it was saved to database + foundSpell, err := Find(db, spell.ID) + if err != nil { + t.Fatalf("Failed to find created spell: %v", err) + } + + if foundSpell.Name != "Lightning" { + t.Errorf("Created spell not found in database") + } +} + +func TestSave(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + spell, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find spell: %v", err) + } + + // Modify spell + spell.Name = "Enhanced Heal" + spell.MP = 7 + spell.Attribute = 15 + + // Save changes + err = spell.Save() + if err != nil { + t.Fatalf("Failed to save spell: %v", err) + } + + // Verify changes were saved + updatedSpell, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find updated spell: %v", err) + } + + if updatedSpell.Name != "Enhanced Heal" { + t.Errorf("Expected updated name 'Enhanced Heal', got '%s'", updatedSpell.Name) + } + if updatedSpell.MP != 7 { + t.Errorf("Expected updated MP 7, got %d", updatedSpell.MP) + } + if updatedSpell.Attribute != 15 { + t.Errorf("Expected updated attribute 15, got %d", updatedSpell.Attribute) + } +} + +func TestDelete(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + spell, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find spell: %v", err) + } + + // Delete spell + err = spell.Delete() + if err != nil { + t.Fatalf("Failed to delete spell: %v", err) + } + + // Verify spell was deleted + _, err = Find(db, 1) + if err == nil { + t.Error("Expected error when finding deleted spell") + } +} + +func TestSpellTypeMethods(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + heal, _ := Find(db, 1) // Healing + hurt, _ := Find(db, 3) // Hurt + sleep, _ := Find(db, 5) // Sleep + craze, _ := Find(db, 7) // Attack boost + ward, _ := Find(db, 8) // Defense boost + + // Test type checking methods + if !heal.IsHealing() { + t.Error("Expected Heal to be healing spell") + } + if heal.IsHurt() { + t.Error("Expected Heal not to be hurt spell") + } + + if !hurt.IsHurt() { + t.Error("Expected Hurt to be hurt spell") + } + if hurt.IsHealing() { + t.Error("Expected Hurt not to be healing spell") + } + + if !sleep.IsSleep() { + t.Error("Expected Sleep to be sleep spell") + } + + if !craze.IsAttackBoost() { + t.Error("Expected Craze to be attack boost spell") + } + + if !ward.IsDefenseBoost() { + t.Error("Expected Ward to be defense boost spell") + } + + // Test TypeName + if heal.TypeName() != "Healing" { + t.Errorf("Expected Heal type name 'Healing', got '%s'", heal.TypeName()) + } + if hurt.TypeName() != "Hurt" { + t.Errorf("Expected Hurt type name 'Hurt', got '%s'", hurt.TypeName()) + } + if sleep.TypeName() != "Sleep" { + t.Errorf("Expected Sleep type name 'Sleep', got '%s'", sleep.TypeName()) + } + if craze.TypeName() != "Attack Boost" { + t.Errorf("Expected Craze type name 'Attack Boost', got '%s'", craze.TypeName()) + } + if ward.TypeName() != "Defense Boost" { + t.Errorf("Expected Ward type name 'Defense Boost', got '%s'", ward.TypeName()) + } +} + +func TestUtilityMethods(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + heal, _ := Find(db, 1) // MP: 5, Attribute: 10 + hurt, _ := Find(db, 3) // MP: 5, Attribute: 15 + sleep, _ := Find(db, 5) // MP: 10, Attribute: 5 + + // Test CanCast + if !heal.CanCast(10) { + t.Error("Expected to be able to cast Heal with 10 MP") + } + if heal.CanCast(3) { + t.Error("Expected not to be able to cast Heal with 3 MP") + } + + // Test Efficiency + expectedHealEff := float64(10) / float64(5) // 2.0 + if heal.Efficiency() != expectedHealEff { + t.Errorf("Expected Heal efficiency %.2f, got %.2f", expectedHealEff, heal.Efficiency()) + } + + expectedHurtEff := float64(15) / float64(5) // 3.0 + if hurt.Efficiency() != expectedHurtEff { + t.Errorf("Expected Hurt efficiency %.2f, got %.2f", expectedHurtEff, hurt.Efficiency()) + } + + // Test IsOffensive + if heal.IsOffensive() { + t.Error("Expected Heal not to be offensive") + } + if !hurt.IsOffensive() { + t.Error("Expected Hurt to be offensive") + } + if !sleep.IsOffensive() { + t.Error("Expected Sleep to be offensive") + } + + // Test IsSupport + if !heal.IsSupport() { + t.Error("Expected Heal to be support spell") + } + if hurt.IsSupport() { + t.Error("Expected Hurt not to be support spell") + } +} \ No newline at end of file