create monsters package

This commit is contained in:
Sky Johnson 2025-08-08 23:22:01 -05:00
parent ace43e1053
commit a4d4dd9777
3 changed files with 974 additions and 0 deletions

207
internal/monsters/doc.go Normal file
View File

@ -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

View File

@ -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)
}

View File

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