create spells package
This commit is contained in:
parent
a4d4dd9777
commit
53d131a96e
88
internal/spells/builder.go
Normal file
88
internal/spells/builder.go
Normal file
@ -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
|
||||
}
|
267
internal/spells/doc.go
Normal file
267
internal/spells/doc.go
Normal file
@ -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
|
265
internal/spells/spells.go
Normal file
265
internal/spells/spells.go
Normal file
@ -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
|
||||
}
|
445
internal/spells/spells_test.go
Normal file
445
internal/spells/spells_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user