diff --git a/internal/monsters/doc.go b/internal/monsters/doc.go new file mode 100644 index 0000000..1dc5436 --- /dev/null +++ b/internal/monsters/doc.go @@ -0,0 +1,207 @@ +/* +Package monsters is the active record implementation for monsters in the game. + +# Basic Usage + +To retrieve a monster by ID: + + monster, err := monsters.Find(db, 1) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Found monster: %s (level: %d, HP: %d)\n", monster.Name, monster.Level, monster.MaxHP) + +To get all monsters: + + allMonsters, err := monsters.All(db) + if err != nil { + log.Fatal(err) + } + for _, monster := range allMonsters { + fmt.Printf("Monster: %s\n", monster.Name) + } + +To filter monsters by level: + + level5Monsters, err := monsters.ByLevel(db, 5) + if err != nil { + log.Fatal(err) + } + +To get monsters within a level range: + + monsters1to10, err := monsters.ByLevelRange(db, 1, 10) + if err != nil { + log.Fatal(err) + } + +To filter monsters by immunity type: + + hurtImmune, err := monsters.ByImmunity(db, monsters.ImmuneHurt) + if err != nil { + log.Fatal(err) + } + +# Creating Monsters with Builder Pattern + +The package provides a fluent builder interface for creating new monsters: + + monster, err := monsters.NewBuilder(db). + WithName("Fire Dragon"). + WithMaxHP(500). + WithMaxDmg(100). + WithArmor(50). + WithLevel(25). + WithMaxExp(1000). + WithMaxGold(500). + WithImmunity(monsters.ImmuneHurt). + Create() + + if err != nil { + log.Fatal(err) + } + fmt.Printf("Created monster with ID: %d\n", monster.ID) + +# Updating Monsters + +Monsters can be modified and saved back to the database: + + monster, _ := monsters.Find(db, 1) + monster.Name = "Enhanced Blue Slime" + monster.MaxHP = 10 + monster.Level = 2 + + err := monster.Save() + if err != nil { + log.Fatal(err) + } + +# Deleting Monsters + +Monsters can be removed from the database: + + monster, _ := monsters.Find(db, 1) + err := monster.Delete() + if err != nil { + log.Fatal(err) + } + +# Monster Immunity Types + +The package defines immunity type constants: + + monsters.ImmuneNone = 0 // No immunity + monsters.ImmuneHurt = 1 // Immune to Hurt spells (Pain, Maim, Rend, Chaos) + monsters.ImmuneSleep = 2 // Immune to Sleep spells (Sleep, Dream, Nightmare) + +Helper methods are available to check immunity: + + if monster.IsHurtImmune() { + fmt.Println("This monster is immune to Hurt spells") + } + if monster.IsSleepImmune() { + fmt.Println("This monster is immune to Sleep spells") + } + if monster.HasImmunity() { + fmt.Printf("Monster has immunity: %s\n", monster.ImmunityName()) + } + +# Database Schema + +The monsters table has the following structure: + + CREATE TABLE monsters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + max_hp INTEGER NOT NULL DEFAULT 0, + max_dmg INTEGER NOT NULL DEFAULT 0, + armor INTEGER NOT NULL DEFAULT 0, + level INTEGER NOT NULL DEFAULT 0, + max_exp INTEGER NOT NULL DEFAULT 0, + max_gold INTEGER NOT NULL DEFAULT 0, + immune INTEGER NOT NULL DEFAULT 0 + ) + +Where: + - id: Unique identifier + - name: Display name of the monster + - max_hp: Maximum hit points + - max_dmg: Maximum damage per attack + - armor: Armor class/defense rating + - level: Monster level (affects encounter chances) + - max_exp: Maximum experience points awarded when defeated + - max_gold: Maximum gold awarded when defeated + - immune: Immunity type (0=none, 1=hurt spells, 2=sleep spells) + +# Monster Statistics + +## Combat Stats + +The core combat statistics define the monster's fighting capability: + + fmt.Printf("HP: %d, Damage: %d, Armor: %d\n", + monster.MaxHP, monster.MaxDmg, monster.Armor) + +## Rewards + +Monsters provide experience and gold rewards when defeated: + + fmt.Printf("Rewards: %d exp, %d gold\n", + monster.MaxExp, monster.MaxGold) + +## Utility Methods + +The package provides several utility methods for analyzing monsters: + + // Difficulty rating based on stats relative to level + difficulty := monster.DifficultyRating() + fmt.Printf("Difficulty: %.2f\n", difficulty) + + // Experience efficiency (exp per HP) + expEfficiency := monster.ExpPerHP() + fmt.Printf("Exp per HP: %.2f\n", expEfficiency) + + // Gold efficiency (gold per HP) + goldEfficiency := monster.GoldPerHP() + fmt.Printf("Gold per HP: %.2f\n", goldEfficiency) + +These methods help with: + - Balancing monster difficulty + - Identifying efficient farming targets + - Analyzing risk/reward ratios + +# Level-Based Queries + +Monsters are commonly filtered by level for encounter generation: + + // Get all monsters at a specific level + level10Monsters, err := monsters.ByLevel(db, 10) + + // Get monsters within a level range (inclusive) + earlyGameMonsters, err := monsters.ByLevelRange(db, 1, 5) + + // All monsters are ordered by level, then ID for consistent results + +# Immunity System + +The immunity system affects spell combat mechanics: + + // Check specific immunities + if monster.IsHurtImmune() { + // Hurt spells (Pain, Maim, Rend, Chaos) won't work + } + if monster.IsSleepImmune() { + // Sleep spells (Sleep, Dream, Nightmare) won't work + } + + // Get all monsters with specific immunity + hurtImmuneMonsters, err := monsters.ByImmunity(db, monsters.ImmuneHurt) + +# Error Handling + +All functions return appropriate errors for common failure cases: + - Monster not found (Find returns error for non-existent IDs) + - Database connection issues + - Invalid operations (e.g., saving/deleting monsters without IDs) +*/ +package monsters \ No newline at end of file diff --git a/internal/monsters/monsters.go b/internal/monsters/monsters.go new file mode 100644 index 0000000..cdc65e1 --- /dev/null +++ b/internal/monsters/monsters.go @@ -0,0 +1,359 @@ +package monsters + +import ( + "fmt" + + "dk/internal/database" + "zombiezen.com/go/sqlite" +) + +// Monster represents a monster in the database +type Monster struct { + ID int `json:"id"` + Name string `json:"name"` + MaxHP int `json:"max_hp"` + MaxDmg int `json:"max_dmg"` + Armor int `json:"armor"` + Level int `json:"level"` + MaxExp int `json:"max_exp"` + MaxGold int `json:"max_gold"` + Immune int `json:"immune"` + + db *database.DB +} + +// Immunity constants for monster immunity types +const ( + ImmuneNone = 0 + ImmuneHurt = 1 // Immune to Hurt spells + ImmuneSleep = 2 // Immune to Sleep spells +) + +// Find retrieves a monster by ID +func Find(db *database.DB, id int) (*Monster, error) { + monster := &Monster{db: db} + + query := "SELECT id, name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune FROM monsters WHERE id = ?" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + monster.ID = stmt.ColumnInt(0) + monster.Name = stmt.ColumnText(1) + monster.MaxHP = stmt.ColumnInt(2) + monster.MaxDmg = stmt.ColumnInt(3) + monster.Armor = stmt.ColumnInt(4) + monster.Level = stmt.ColumnInt(5) + monster.MaxExp = stmt.ColumnInt(6) + monster.MaxGold = stmt.ColumnInt(7) + monster.Immune = stmt.ColumnInt(8) + return nil + }, id) + + if err != nil { + return nil, fmt.Errorf("failed to find monster: %w", err) + } + + if monster.ID == 0 { + return nil, fmt.Errorf("monster with ID %d not found", id) + } + + return monster, nil +} + +// All retrieves all monsters +func All(db *database.DB) ([]*Monster, error) { + var monsters []*Monster + + query := "SELECT id, name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune FROM monsters ORDER BY level, id" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + monster := &Monster{ + ID: stmt.ColumnInt(0), + Name: stmt.ColumnText(1), + MaxHP: stmt.ColumnInt(2), + MaxDmg: stmt.ColumnInt(3), + Armor: stmt.ColumnInt(4), + Level: stmt.ColumnInt(5), + MaxExp: stmt.ColumnInt(6), + MaxGold: stmt.ColumnInt(7), + Immune: stmt.ColumnInt(8), + db: db, + } + monsters = append(monsters, monster) + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to retrieve all monsters: %w", err) + } + + return monsters, nil +} + +// ByLevel retrieves monsters by level +func ByLevel(db *database.DB, level int) ([]*Monster, error) { + var monsters []*Monster + + query := "SELECT id, name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune FROM monsters WHERE level = ? ORDER BY id" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + monster := &Monster{ + ID: stmt.ColumnInt(0), + Name: stmt.ColumnText(1), + MaxHP: stmt.ColumnInt(2), + MaxDmg: stmt.ColumnInt(3), + Armor: stmt.ColumnInt(4), + Level: stmt.ColumnInt(5), + MaxExp: stmt.ColumnInt(6), + MaxGold: stmt.ColumnInt(7), + Immune: stmt.ColumnInt(8), + db: db, + } + monsters = append(monsters, monster) + return nil + }, level) + + if err != nil { + return nil, fmt.Errorf("failed to retrieve monsters by level: %w", err) + } + + return monsters, nil +} + +// ByLevelRange retrieves monsters within a level range (inclusive) +func ByLevelRange(db *database.DB, minLevel, maxLevel int) ([]*Monster, error) { + var monsters []*Monster + + query := "SELECT id, name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune FROM monsters WHERE level BETWEEN ? AND ? ORDER BY level, id" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + monster := &Monster{ + ID: stmt.ColumnInt(0), + Name: stmt.ColumnText(1), + MaxHP: stmt.ColumnInt(2), + MaxDmg: stmt.ColumnInt(3), + Armor: stmt.ColumnInt(4), + Level: stmt.ColumnInt(5), + MaxExp: stmt.ColumnInt(6), + MaxGold: stmt.ColumnInt(7), + Immune: stmt.ColumnInt(8), + db: db, + } + monsters = append(monsters, monster) + return nil + }, minLevel, maxLevel) + + if err != nil { + return nil, fmt.Errorf("failed to retrieve monsters by level range: %w", err) + } + + return monsters, nil +} + +// ByImmunity retrieves monsters by immunity type +func ByImmunity(db *database.DB, immunityType int) ([]*Monster, error) { + var monsters []*Monster + + query := "SELECT id, name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune FROM monsters WHERE immune = ? ORDER BY level, id" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + monster := &Monster{ + ID: stmt.ColumnInt(0), + Name: stmt.ColumnText(1), + MaxHP: stmt.ColumnInt(2), + MaxDmg: stmt.ColumnInt(3), + Armor: stmt.ColumnInt(4), + Level: stmt.ColumnInt(5), + MaxExp: stmt.ColumnInt(6), + MaxGold: stmt.ColumnInt(7), + Immune: stmt.ColumnInt(8), + db: db, + } + monsters = append(monsters, monster) + return nil + }, immunityType) + + if err != nil { + return nil, fmt.Errorf("failed to retrieve monsters by immunity: %w", err) + } + + return monsters, nil +} + +// Builder provides a fluent interface for creating monsters +type Builder struct { + monster *Monster + db *database.DB +} + +// NewBuilder creates a new monster builder +func NewBuilder(db *database.DB) *Builder { + return &Builder{ + monster: &Monster{db: db}, + db: db, + } +} + +// WithName sets the monster name +func (b *Builder) WithName(name string) *Builder { + b.monster.Name = name + return b +} + +// WithMaxHP sets the monster's maximum hit points +func (b *Builder) WithMaxHP(maxHP int) *Builder { + b.monster.MaxHP = maxHP + return b +} + +// WithMaxDmg sets the monster's maximum damage +func (b *Builder) WithMaxDmg(maxDmg int) *Builder { + b.monster.MaxDmg = maxDmg + return b +} + +// WithArmor sets the monster's armor value +func (b *Builder) WithArmor(armor int) *Builder { + b.monster.Armor = armor + return b +} + +// WithLevel sets the monster's level +func (b *Builder) WithLevel(level int) *Builder { + b.monster.Level = level + return b +} + +// WithMaxExp sets the monster's maximum experience reward +func (b *Builder) WithMaxExp(maxExp int) *Builder { + b.monster.MaxExp = maxExp + return b +} + +// WithMaxGold sets the monster's maximum gold reward +func (b *Builder) WithMaxGold(maxGold int) *Builder { + b.monster.MaxGold = maxGold + return b +} + +// WithImmunity sets the monster's immunity type +func (b *Builder) WithImmunity(immunity int) *Builder { + b.monster.Immune = immunity + return b +} + +// Create saves the monster to the database and returns it +func (b *Builder) Create() (*Monster, error) { + // Use a transaction to ensure we can get the ID + var monster *Monster + err := b.db.Transaction(func(tx *database.Tx) error { + query := `INSERT INTO monsters (name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + + if err := tx.Exec(query, b.monster.Name, b.monster.MaxHP, b.monster.MaxDmg, b.monster.Armor, + b.monster.Level, b.monster.MaxExp, b.monster.MaxGold, b.monster.Immune); err != nil { + return fmt.Errorf("failed to insert monster: %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 monster with the ID + monster = &Monster{ + ID: lastID, + Name: b.monster.Name, + MaxHP: b.monster.MaxHP, + MaxDmg: b.monster.MaxDmg, + Armor: b.monster.Armor, + Level: b.monster.Level, + MaxExp: b.monster.MaxExp, + MaxGold: b.monster.MaxGold, + Immune: b.monster.Immune, + db: b.db, + } + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to create monster: %w", err) + } + + return monster, nil +} + +// Save updates an existing monster in the database +func (m *Monster) Save() error { + if m.ID == 0 { + return fmt.Errorf("cannot save monster without ID") + } + + query := `UPDATE monsters SET name = ?, max_hp = ?, max_dmg = ?, armor = ?, level = ?, max_exp = ?, max_gold = ?, immune = ? WHERE id = ?` + return m.db.Exec(query, m.Name, m.MaxHP, m.MaxDmg, m.Armor, m.Level, m.MaxExp, m.MaxGold, m.Immune, m.ID) +} + +// Delete removes the monster from the database +func (m *Monster) Delete() error { + if m.ID == 0 { + return fmt.Errorf("cannot delete monster without ID") + } + + query := "DELETE FROM monsters WHERE id = ?" + return m.db.Exec(query, m.ID) +} + +// IsHurtImmune returns true if the monster is immune to Hurt spells +func (m *Monster) IsHurtImmune() bool { + return m.Immune == ImmuneHurt +} + +// IsSleepImmune returns true if the monster is immune to Sleep spells +func (m *Monster) IsSleepImmune() bool { + return m.Immune == ImmuneSleep +} + +// HasImmunity returns true if the monster has any immunity +func (m *Monster) HasImmunity() bool { + return m.Immune != ImmuneNone +} + +// ImmunityName returns the string representation of the monster's immunity +func (m *Monster) ImmunityName() string { + switch m.Immune { + case ImmuneNone: + return "None" + case ImmuneHurt: + return "Hurt Spells" + case ImmuneSleep: + return "Sleep Spells" + default: + return "Unknown" + } +} + +// DifficultyRating calculates a simple difficulty rating based on stats +func (m *Monster) DifficultyRating() float64 { + // Simple formula: (HP + Damage + Armor) / Level + // Higher values indicate tougher monsters relative to their level + if m.Level == 0 { + return 0 + } + return float64(m.MaxHP+m.MaxDmg+m.Armor) / float64(m.Level) +} + +// ExpPerHP returns the experience reward per hit point (efficiency metric) +func (m *Monster) ExpPerHP() float64 { + if m.MaxHP == 0 { + return 0 + } + return float64(m.MaxExp) / float64(m.MaxHP) +} + +// GoldPerHP returns the gold reward per hit point (efficiency metric) +func (m *Monster) GoldPerHP() float64 { + if m.MaxHP == 0 { + return 0 + } + return float64(m.MaxGold) / float64(m.MaxHP) +} \ No newline at end of file diff --git a/internal/monsters/monsters_test.go b/internal/monsters/monsters_test.go new file mode 100644 index 0000000..11e6c75 --- /dev/null +++ b/internal/monsters/monsters_test.go @@ -0,0 +1,408 @@ +package monsters + +import ( + "os" + "testing" + + "dk/internal/database" +) + +func setupTestDB(t *testing.T) *database.DB { + testDB := "test_monsters.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 monsters table + createTable := `CREATE TABLE monsters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + max_hp INTEGER NOT NULL DEFAULT 0, + max_dmg INTEGER NOT NULL DEFAULT 0, + armor INTEGER NOT NULL DEFAULT 0, + level INTEGER NOT NULL DEFAULT 0, + max_exp INTEGER NOT NULL DEFAULT 0, + max_gold INTEGER NOT NULL DEFAULT 0, + immune INTEGER NOT NULL DEFAULT 0 + )` + + if err := db.Exec(createTable); err != nil { + t.Fatalf("Failed to create monsters table: %v", err) + } + + // Insert test data + testMonsters := `INSERT INTO monsters (name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune) VALUES + ('Blue Slime', 4, 3, 1, 1, 1, 1, 0), + ('Red Slime', 6, 5, 1, 1, 2, 1, 0), + ('Shadow', 10, 9, 3, 2, 6, 2, 1), + ('Silver Slime', 15, 100, 200, 30, 15, 1000, 2), + ('Raven', 16, 13, 5, 4, 18, 6, 0)` + + if err := db.Exec(testMonsters); err != nil { + t.Fatalf("Failed to insert test monsters: %v", err) + } + + return db +} + +func TestFind(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Test finding existing monster + monster, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find monster: %v", err) + } + + if monster.ID != 1 { + t.Errorf("Expected ID 1, got %d", monster.ID) + } + if monster.Name != "Blue Slime" { + t.Errorf("Expected name 'Blue Slime', got '%s'", monster.Name) + } + if monster.MaxHP != 4 { + t.Errorf("Expected max_hp 4, got %d", monster.MaxHP) + } + if monster.MaxDmg != 3 { + t.Errorf("Expected max_dmg 3, got %d", monster.MaxDmg) + } + if monster.Armor != 1 { + t.Errorf("Expected armor 1, got %d", monster.Armor) + } + if monster.Level != 1 { + t.Errorf("Expected level 1, got %d", monster.Level) + } + if monster.MaxExp != 1 { + t.Errorf("Expected max_exp 1, got %d", monster.MaxExp) + } + if monster.MaxGold != 1 { + t.Errorf("Expected max_gold 1, got %d", monster.MaxGold) + } + if monster.Immune != ImmuneNone { + t.Errorf("Expected immune %d, got %d", ImmuneNone, monster.Immune) + } + + // Test finding non-existent monster + _, err = Find(db, 999) + if err == nil { + t.Error("Expected error when finding non-existent monster") + } +} + +func TestAll(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + monsters, err := All(db) + if err != nil { + t.Fatalf("Failed to get all monsters: %v", err) + } + + if len(monsters) != 5 { + t.Errorf("Expected 5 monsters, got %d", len(monsters)) + } + + // Check first monster (should be ordered by level, then id) + if monsters[0].Name != "Blue Slime" { + t.Errorf("Expected first monster to be 'Blue Slime', got '%s'", monsters[0].Name) + } +} + +func TestByLevel(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Test level 1 monsters + level1Monsters, err := ByLevel(db, 1) + if err != nil { + t.Fatalf("Failed to get level 1 monsters: %v", err) + } + + if len(level1Monsters) != 2 { + t.Errorf("Expected 2 level 1 monsters, got %d", len(level1Monsters)) + } + + for _, monster := range level1Monsters { + if monster.Level != 1 { + t.Errorf("Expected level 1, got %d for monster %s", monster.Level, monster.Name) + } + } + + // Test level that doesn't exist + noMonsters, err := ByLevel(db, 999) + if err != nil { + t.Fatalf("Failed to query non-existent level: %v", err) + } + + if len(noMonsters) != 0 { + t.Errorf("Expected 0 monsters at level 999, got %d", len(noMonsters)) + } +} + +func TestByLevelRange(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Test level range 1-2 + monsters, err := ByLevelRange(db, 1, 2) + if err != nil { + t.Fatalf("Failed to get monsters by level range: %v", err) + } + + if len(monsters) != 3 { + t.Errorf("Expected 3 monsters in level range 1-2, got %d", len(monsters)) + } + + // Verify all monsters are within range + for _, monster := range monsters { + if monster.Level < 1 || monster.Level > 2 { + t.Errorf("Monster %s level %d is outside range 1-2", monster.Name, monster.Level) + } + } + + // Verify ordering (by level, then id) + if monsters[0].Level > monsters[len(monsters)-1].Level { + t.Error("Expected monsters to be ordered by level") + } +} + +func TestByImmunity(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Test Hurt immune monsters + hurtImmune, err := ByImmunity(db, ImmuneHurt) + if err != nil { + t.Fatalf("Failed to get Hurt immune monsters: %v", err) + } + + if len(hurtImmune) != 1 { + t.Errorf("Expected 1 Hurt immune monster, got %d", len(hurtImmune)) + } + + if len(hurtImmune) > 0 && hurtImmune[0].Name != "Shadow" { + t.Errorf("Expected Hurt immune monster to be 'Shadow', got '%s'", hurtImmune[0].Name) + } + + // Test Sleep immune monsters + sleepImmune, err := ByImmunity(db, ImmuneSleep) + if err != nil { + t.Fatalf("Failed to get Sleep immune monsters: %v", err) + } + + if len(sleepImmune) != 1 { + t.Errorf("Expected 1 Sleep immune monster, got %d", len(sleepImmune)) + } + + // Test no immunity monsters + noImmunity, err := ByImmunity(db, ImmuneNone) + if err != nil { + t.Fatalf("Failed to get non-immune monsters: %v", err) + } + + if len(noImmunity) != 3 { + t.Errorf("Expected 3 non-immune monsters, got %d", len(noImmunity)) + } +} + +func TestBuilder(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Create new monster using builder + monster, err := NewBuilder(db). + WithName("Test Dragon"). + WithMaxHP(100). + WithMaxDmg(25). + WithArmor(10). + WithLevel(15). + WithMaxExp(500). + WithMaxGold(100). + WithImmunity(ImmuneHurt). + Create() + + if err != nil { + t.Fatalf("Failed to create monster with builder: %v", err) + } + + if monster.ID == 0 { + t.Error("Expected non-zero ID after creation") + } + if monster.Name != "Test Dragon" { + t.Errorf("Expected name 'Test Dragon', got '%s'", monster.Name) + } + if monster.MaxHP != 100 { + t.Errorf("Expected max_hp 100, got %d", monster.MaxHP) + } + if monster.MaxDmg != 25 { + t.Errorf("Expected max_dmg 25, got %d", monster.MaxDmg) + } + if monster.Armor != 10 { + t.Errorf("Expected armor 10, got %d", monster.Armor) + } + if monster.Level != 15 { + t.Errorf("Expected level 15, got %d", monster.Level) + } + if monster.MaxExp != 500 { + t.Errorf("Expected max_exp 500, got %d", monster.MaxExp) + } + if monster.MaxGold != 100 { + t.Errorf("Expected max_gold 100, got %d", monster.MaxGold) + } + if monster.Immune != ImmuneHurt { + t.Errorf("Expected immune %d, got %d", ImmuneHurt, monster.Immune) + } + + // Verify it was saved to database + foundMonster, err := Find(db, monster.ID) + if err != nil { + t.Fatalf("Failed to find created monster: %v", err) + } + + if foundMonster.Name != "Test Dragon" { + t.Errorf("Created monster not found in database") + } +} + +func TestSave(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + monster, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find monster: %v", err) + } + + // Modify monster + monster.Name = "Updated Blue Slime" + monster.MaxHP = 8 + monster.Level = 2 + + // Save changes + err = monster.Save() + if err != nil { + t.Fatalf("Failed to save monster: %v", err) + } + + // Verify changes were saved + updatedMonster, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find updated monster: %v", err) + } + + if updatedMonster.Name != "Updated Blue Slime" { + t.Errorf("Expected updated name 'Updated Blue Slime', got '%s'", updatedMonster.Name) + } + if updatedMonster.MaxHP != 8 { + t.Errorf("Expected updated max_hp 8, got %d", updatedMonster.MaxHP) + } + if updatedMonster.Level != 2 { + t.Errorf("Expected updated level 2, got %d", updatedMonster.Level) + } +} + +func TestDelete(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + monster, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find monster: %v", err) + } + + // Delete monster + err = monster.Delete() + if err != nil { + t.Fatalf("Failed to delete monster: %v", err) + } + + // Verify monster was deleted + _, err = Find(db, 1) + if err == nil { + t.Error("Expected error when finding deleted monster") + } +} + +func TestImmunityMethods(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + blueSlime, _ := Find(db, 1) // No immunity + shadow, _ := Find(db, 3) // Hurt immune + silverSlime, _ := Find(db, 4) // Sleep immune + + // Test IsHurtImmune + if blueSlime.IsHurtImmune() { + t.Error("Expected blue slime not to be Hurt immune") + } + if !shadow.IsHurtImmune() { + t.Error("Expected shadow to be Hurt immune") + } + if silverSlime.IsHurtImmune() { + t.Error("Expected silver slime not to be Hurt immune") + } + + // Test IsSleepImmune + if blueSlime.IsSleepImmune() { + t.Error("Expected blue slime not to be Sleep immune") + } + if shadow.IsSleepImmune() { + t.Error("Expected shadow not to be Sleep immune") + } + if !silverSlime.IsSleepImmune() { + t.Error("Expected silver slime to be Sleep immune") + } + + // Test HasImmunity + if blueSlime.HasImmunity() { + t.Error("Expected blue slime to have no immunity") + } + if !shadow.HasImmunity() { + t.Error("Expected shadow to have immunity") + } + if !silverSlime.HasImmunity() { + t.Error("Expected silver slime to have immunity") + } + + // Test ImmunityName + if blueSlime.ImmunityName() != "None" { + t.Errorf("Expected blue slime immunity name 'None', got '%s'", blueSlime.ImmunityName()) + } + if shadow.ImmunityName() != "Hurt Spells" { + t.Errorf("Expected shadow immunity name 'Hurt Spells', got '%s'", shadow.ImmunityName()) + } + if silverSlime.ImmunityName() != "Sleep Spells" { + t.Errorf("Expected silver slime immunity name 'Sleep Spells', got '%s'", silverSlime.ImmunityName()) + } +} + +func TestUtilityMethods(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + blueSlime, _ := Find(db, 1) + + // Test DifficultyRating + expectedDifficulty := float64(4+3+1) / float64(1) // (HP + Damage + Armor) / Level + if blueSlime.DifficultyRating() != expectedDifficulty { + t.Errorf("Expected difficulty rating %.2f, got %.2f", expectedDifficulty, blueSlime.DifficultyRating()) + } + + // Test ExpPerHP + expectedExpPerHP := float64(1) / float64(4) // Exp / HP + if blueSlime.ExpPerHP() != expectedExpPerHP { + t.Errorf("Expected exp per HP %.2f, got %.2f", expectedExpPerHP, blueSlime.ExpPerHP()) + } + + // Test GoldPerHP + expectedGoldPerHP := float64(1) / float64(4) // Gold / HP + if blueSlime.GoldPerHP() != expectedGoldPerHP { + t.Errorf("Expected gold per HP %.2f, got %.2f", expectedGoldPerHP, blueSlime.GoldPerHP()) + } +} \ No newline at end of file