diff --git a/internal/install/install.go b/internal/install/install.go index f3d7312..189c114 100644 --- a/internal/install/install.go +++ b/internal/install/install.go @@ -1,3 +1,11 @@ +// package install is the home of the install command +// +// Its purpose is to set up the intial database structure and data, +// then create a "demo" user to act as the initial admin account. +// +// At the moment, it simply creates a static structure and admin user; +// in the future I'd like to add migrations and prompt for account +// creation. package install import ( @@ -16,24 +24,20 @@ func Run() error { start := time.Now() - // Open database connection db, err := database.Open(dbPath) if err != nil { return err } defer db.Close() - // Create tables if err := createTables(db); err != nil { return fmt.Errorf("failed to create tables: %w", err) } - // Populate initial data if err := populateData(db); err != nil { return fmt.Errorf("failed to populate data: %w", err) } - // Create demo user if err := createDemoUser(db); err != nil { return fmt.Errorf("failed to create demo user: %w", err) } @@ -197,7 +201,7 @@ func populateData(db *database.DB) error { if err := db.Exec("INSERT INTO control VALUES (1, 250, 1, '', 'Mage', 'Warrior', 'Paladin')"); err != nil { return fmt.Errorf("failed to populate control table: %w", err) } - fmt.Println("✓ Control table populated") + fmt.Println("✓ control table populated") dropsSQL := `INSERT INTO drops VALUES (1, 'Life Pebble', 1, 1, 'maxhp,10', ''), @@ -235,7 +239,7 @@ func populateData(db *database.DB) error { if err := db.Exec(dropsSQL); err != nil { return fmt.Errorf("failed to populate drops table: %w", err) } - fmt.Println("✓ Drops table populated") + fmt.Println("✓ drops table populated") itemsSQL := `INSERT INTO items VALUES (1, 1, 'Stick', 10, 2, ''), @@ -274,7 +278,7 @@ func populateData(db *database.DB) error { if err := db.Exec(itemsSQL); err != nil { return fmt.Errorf("failed to populate items table: %w", err) } - fmt.Println("✓ Items table populated") + fmt.Println("✓ items table populated") monstersSQL := `INSERT INTO monsters VALUES (1, 'Blue Slime', 4, 3, 1, 1, 1, 1, 0), @@ -431,15 +435,13 @@ func populateData(db *database.DB) error { if err := db.Exec(monstersSQL); err != nil { return fmt.Errorf("failed to populate monsters table: %w", err) } - fmt.Println("✓ Monsters table populated (sample data)") + fmt.Println("✓ monsters table populated") - // News table if err := db.Exec("INSERT INTO news (author, content) VALUES (1, 'Welcome to Dragon Knight! This is your first news post.')"); err != nil { return fmt.Errorf("failed to populate news table: %w", err) } - fmt.Println("✓ News table populated") + fmt.Println("✓ news table populated") - // Spells table spellsSQL := `INSERT INTO spells VALUES (1, 'Heal', 5, 10, 1), (2, 'Revive', 10, 25, 1), @@ -463,9 +465,8 @@ func populateData(db *database.DB) error { if err := db.Exec(spellsSQL); err != nil { return fmt.Errorf("failed to populate spells table: %w", err) } - fmt.Println("✓ Spells table populated") + fmt.Println("✓ spells table populated") - // Towns table townsSQL := `INSERT INTO towns VALUES (1, 'Midworld', 0, 0, 5, 0, 0, '1,2,3,17,18,19,28,29'), (2, 'Roma', 30, 30, 10, 25, 5, '2,3,4,18,19,29'), @@ -478,20 +479,19 @@ func populateData(db *database.DB) error { if err := db.Exec(townsSQL); err != nil { return fmt.Errorf("failed to populate towns table: %w", err) } - fmt.Println("✓ Towns table populated") + fmt.Println("✓ towns table populated") return nil } func createDemoUser(db *database.DB) error { - // Hash the password using argon2id hashedPassword, err := password.Hash("Demo123!") if err != nil { return fmt.Errorf("failed to hash password: %w", err) } stmt := `INSERT INTO users (username, password, email, verified, class_id, auth) - VALUES (?, ?, ?, 1, 1, 1)` + VALUES (?, ?, ?, 1, 1, 4)` if err := db.Exec(stmt, "demo", hashedPassword, "demo@demo.com"); err != nil { return fmt.Errorf("failed to create demo user: %w", err) diff --git a/internal/items/doc.go b/internal/items/doc.go new file mode 100644 index 0000000..17f20db --- /dev/null +++ b/internal/items/doc.go @@ -0,0 +1,123 @@ +/* +Package items is the active record implementation for items in the game. + +# Basic Usage + +To retrieve an item by ID: + + item, err := items.Find(db, 1) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Found item: %s (value: %d)\n", item.Name, item.Value) + +To get all items: + + allItems, err := items.All(db) + if err != nil { + log.Fatal(err) + } + for _, item := range allItems { + fmt.Printf("Item: %s\n", item.Name) + } + +To filter items by type: + + weapons, err := items.ByType(db, items.TypeWeapon) + if err != nil { + log.Fatal(err) + } + +# Creating Items with Builder Pattern + +The package provides a fluent builder interface for creating new items: + + item, err := items.NewBuilder(db). + WithType(items.TypeWeapon). + WithName("Excalibur"). + WithValue(5000). + WithAtt(100). + WithSpecial("strength,25"). + Create() + + if err != nil { + log.Fatal(err) + } + fmt.Printf("Created item with ID: %d\n", item.ID) + +# Updating Items + +Items can be modified and saved back to the database: + + item, _ := items.Find(db, 1) + item.Name = "Enhanced Sword" + item.Value += 100 + + err := item.Save() + if err != nil { + log.Fatal(err) + } + +# Deleting Items + +Items can be removed from the database: + + item, _ := items.Find(db, 1) + err := item.Delete() + if err != nil { + log.Fatal(err) + } + +# Item Types + +The package defines three item types as constants: + + items.TypeWeapon = 1 // Swords, axes, etc. + items.TypeArmor = 2 // Protective gear + items.TypeShield = 3 // Shields and bucklers + +Helper methods are available to check item types: + + if item.IsWeapon() { + fmt.Println("This is a weapon") + } + fmt.Printf("Item type: %s\n", item.TypeName()) + +# Database Schema + +The items table has the following structure: + + CREATE TABLE items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type INTEGER NOT NULL DEFAULT 0, + name TEXT NOT NULL, + value INTEGER NOT NULL DEFAULT 0, + att INTEGER NOT NULL DEFAULT 0, + special TEXT NOT NULL DEFAULT '' + ) + +Where: + - id: Unique identifier + - type: Item type (1=weapon, 2=armor, 3=shield) + - name: Display name of the item + - value: Gold value/cost + - att: Attack or defense attribute bonus + - special: Special attributes in "key,value" format + +# Special Attributes + +The special field contains comma-separated key-value pairs for item bonuses: + + "strength,10" // +10 strength + "maxhp,25" // +25 max health + "expbonus,5" // +5% experience bonus + "maxhp,50,strength,25" // Multiple bonuses + +# Error Handling + +All functions return appropriate errors for common failure cases: + - Item not found (Find returns error for non-existent IDs) + - Database connection issues + - Invalid operations (e.g., saving/deleting items without IDs) +*/ +package items diff --git a/internal/items/items.go b/internal/items/items.go new file mode 100644 index 0000000..a165515 --- /dev/null +++ b/internal/items/items.go @@ -0,0 +1,241 @@ +package items + +import ( + "fmt" + + "dk/internal/database" + "zombiezen.com/go/sqlite" +) + +// Item represents an item in the database +type Item struct { + ID int `json:"id"` + Type int `json:"type"` + Name string `json:"name"` + Value int `json:"value"` + Att int `json:"att"` + Special string `json:"special"` + + db *database.DB +} + +// ItemType constants for item types +const ( + TypeWeapon = 1 + TypeArmor = 2 + TypeShield = 3 +) + +// Find retrieves an item by ID +func Find(db *database.DB, id int) (*Item, error) { + item := &Item{db: db} + + query := "SELECT id, type, name, value, att, special FROM items WHERE id = ?" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + item.ID = stmt.ColumnInt(0) + item.Type = stmt.ColumnInt(1) + item.Name = stmt.ColumnText(2) + item.Value = stmt.ColumnInt(3) + item.Att = stmt.ColumnInt(4) + item.Special = stmt.ColumnText(5) + return nil + }, id) + + if err != nil { + return nil, fmt.Errorf("failed to find item: %w", err) + } + + if item.ID == 0 { + return nil, fmt.Errorf("item with ID %d not found", id) + } + + return item, nil +} + +// All retrieves all items +func All(db *database.DB) ([]*Item, error) { + var items []*Item + + query := "SELECT id, type, name, value, att, special FROM items ORDER BY id" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + item := &Item{ + ID: stmt.ColumnInt(0), + Type: stmt.ColumnInt(1), + Name: stmt.ColumnText(2), + Value: stmt.ColumnInt(3), + Att: stmt.ColumnInt(4), + Special: stmt.ColumnText(5), + db: db, + } + items = append(items, item) + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to retrieve all items: %w", err) + } + + return items, nil +} + +// ByType retrieves items by type +func ByType(db *database.DB, itemType int) ([]*Item, error) { + var items []*Item + + query := "SELECT id, type, name, value, att, special FROM items WHERE type = ? ORDER BY id" + err := db.Query(query, func(stmt *sqlite.Stmt) error { + item := &Item{ + ID: stmt.ColumnInt(0), + Type: stmt.ColumnInt(1), + Name: stmt.ColumnText(2), + Value: stmt.ColumnInt(3), + Att: stmt.ColumnInt(4), + Special: stmt.ColumnText(5), + db: db, + } + items = append(items, item) + return nil + }, itemType) + + if err != nil { + return nil, fmt.Errorf("failed to retrieve items by type: %w", err) + } + + return items, nil +} + +// Builder provides a fluent interface for creating items +type Builder struct { + item *Item + db *database.DB +} + +// NewBuilder creates a new item builder +func NewBuilder(db *database.DB) *Builder { + return &Builder{ + item: &Item{db: db}, + db: db, + } +} + +// WithType sets the item type +func (b *Builder) WithType(itemType int) *Builder { + b.item.Type = itemType + return b +} + +// WithName sets the item name +func (b *Builder) WithName(name string) *Builder { + b.item.Name = name + return b +} + +// WithValue sets the item value +func (b *Builder) WithValue(value int) *Builder { + b.item.Value = value + return b +} + +// WithAtt sets the item attack/defense value +func (b *Builder) WithAtt(att int) *Builder { + b.item.Att = att + return b +} + +// WithSpecial sets the item special attributes +func (b *Builder) WithSpecial(special string) *Builder { + b.item.Special = special + return b +} + +// Create saves the item to the database and returns it +func (b *Builder) Create() (*Item, error) { + // Use a transaction to ensure we can get the ID + var item *Item + err := b.db.Transaction(func(tx *database.Tx) error { + query := `INSERT INTO items (type, name, value, att, special) + VALUES (?, ?, ?, ?, ?)` + + if err := tx.Exec(query, b.item.Type, b.item.Name, b.item.Value, b.item.Att, b.item.Special); err != nil { + return fmt.Errorf("failed to insert item: %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 item with the ID + item = &Item{ + ID: lastID, + Type: b.item.Type, + Name: b.item.Name, + Value: b.item.Value, + Att: b.item.Att, + Special: b.item.Special, + db: b.db, + } + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to create item: %w", err) + } + + return item, nil +} + +// Save updates an existing item in the database +func (i *Item) Save() error { + if i.ID == 0 { + return fmt.Errorf("cannot save item without ID") + } + + query := `UPDATE items SET type = ?, name = ?, value = ?, att = ?, special = ? WHERE id = ?` + return i.db.Exec(query, i.Type, i.Name, i.Value, i.Att, i.Special, i.ID) +} + +// Delete removes the item from the database +func (i *Item) Delete() error { + if i.ID == 0 { + return fmt.Errorf("cannot delete item without ID") + } + + query := "DELETE FROM items WHERE id = ?" + return i.db.Exec(query, i.ID) +} + +// IsWeapon returns true if the item is a weapon +func (i *Item) IsWeapon() bool { + return i.Type == TypeWeapon +} + +// IsArmor returns true if the item is armor +func (i *Item) IsArmor() bool { + return i.Type == TypeArmor +} + +// IsShield returns true if the item is a shield +func (i *Item) IsShield() bool { + return i.Type == TypeShield +} + +// TypeName returns the string representation of the item type +func (i *Item) TypeName() string { + switch i.Type { + case TypeWeapon: + return "Weapon" + case TypeArmor: + return "Armor" + case TypeShield: + return "Shield" + default: + return "Unknown" + } +} \ No newline at end of file diff --git a/internal/items/items_test.go b/internal/items/items_test.go new file mode 100644 index 0000000..b3020d0 --- /dev/null +++ b/internal/items/items_test.go @@ -0,0 +1,272 @@ +package items + +import ( + "os" + "testing" + + "dk/internal/database" +) + +func setupTestDB(t *testing.T) *database.DB { + testDB := "test_items.db" + t.Cleanup(func() { + os.Remove(testDB) + }) + + db, err := database.Open(testDB) + if err != nil { + t.Fatalf("Failed to open test database: %v", err) + } + + createTable := `CREATE TABLE items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type INTEGER NOT NULL DEFAULT 0, + name TEXT NOT NULL, + value INTEGER NOT NULL DEFAULT 0, + att INTEGER NOT NULL DEFAULT 0, + special TEXT NOT NULL DEFAULT '' + )` + + if err := db.Exec(createTable); err != nil { + t.Fatalf("Failed to create items table: %v", err) + } + + testItems := `INSERT INTO items (type, name, value, att, special) VALUES + (1, 'Test Sword', 100, 10, 'strength,5'), + (2, 'Test Armor', 200, 15, 'maxhp,25'), + (3, 'Test Shield', 150, 8, '')` + + if err := db.Exec(testItems); err != nil { + t.Fatalf("Failed to insert test items: %v", err) + } + + return db +} + +func TestFind(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Test finding existing item + item, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find item: %v", err) + } + + if item.ID != 1 { + t.Errorf("Expected ID 1, got %d", item.ID) + } + if item.Name != "Test Sword" { + t.Errorf("Expected name 'Test Sword', got '%s'", item.Name) + } + if item.Type != TypeWeapon { + t.Errorf("Expected type %d, got %d", TypeWeapon, item.Type) + } + if item.Value != 100 { + t.Errorf("Expected value 100, got %d", item.Value) + } + if item.Att != 10 { + t.Errorf("Expected att 10, got %d", item.Att) + } + if item.Special != "strength,5" { + t.Errorf("Expected special 'strength,5', got '%s'", item.Special) + } + + // Test finding non-existent item + _, err = Find(db, 999) + if err == nil { + t.Error("Expected error when finding non-existent item") + } +} + +func TestAll(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + items, err := All(db) + if err != nil { + t.Fatalf("Failed to get all items: %v", err) + } + + if len(items) != 3 { + t.Errorf("Expected 3 items, got %d", len(items)) + } + + // Check first item + if items[0].Name != "Test Sword" { + t.Errorf("Expected first item to be 'Test Sword', got '%s'", items[0].Name) + } +} + +func TestByType(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + weapons, err := ByType(db, TypeWeapon) + if err != nil { + t.Fatalf("Failed to get weapons: %v", err) + } + + if len(weapons) != 1 { + t.Errorf("Expected 1 weapon, got %d", len(weapons)) + } + + if weapons[0].Name != "Test Sword" { + t.Errorf("Expected weapon to be 'Test Sword', got '%s'", weapons[0].Name) + } + + armor, err := ByType(db, TypeArmor) + if err != nil { + t.Fatalf("Failed to get armor: %v", err) + } + + if len(armor) != 1 { + t.Errorf("Expected 1 armor, got %d", len(armor)) + } +} + +func TestBuilder(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // Create new item using builder + item, err := NewBuilder(db). + WithType(TypeWeapon). + WithName("Builder Sword"). + WithValue(500). + WithAtt(25). + WithSpecial("dexterity,10"). + Create() + + if err != nil { + t.Fatalf("Failed to create item with builder: %v", err) + } + + if item.ID == 0 { + t.Error("Expected non-zero ID after creation") + } + if item.Name != "Builder Sword" { + t.Errorf("Expected name 'Builder Sword', got '%s'", item.Name) + } + if item.Type != TypeWeapon { + t.Errorf("Expected type %d, got %d", TypeWeapon, item.Type) + } + if item.Value != 500 { + t.Errorf("Expected value 500, got %d", item.Value) + } + if item.Att != 25 { + t.Errorf("Expected att 25, got %d", item.Att) + } + if item.Special != "dexterity,10" { + t.Errorf("Expected special 'dexterity,10', got '%s'", item.Special) + } + + // Verify it was saved to database + foundItem, err := Find(db, item.ID) + if err != nil { + t.Fatalf("Failed to find created item: %v", err) + } + + if foundItem.Name != "Builder Sword" { + t.Errorf("Created item not found in database") + } +} + +func TestSave(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + item, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find item: %v", err) + } + + // Modify item + item.Name = "Updated Sword" + item.Value = 150 + + // Save changes + err = item.Save() + if err != nil { + t.Fatalf("Failed to save item: %v", err) + } + + // Verify changes were saved + updatedItem, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find updated item: %v", err) + } + + if updatedItem.Name != "Updated Sword" { + t.Errorf("Expected updated name 'Updated Sword', got '%s'", updatedItem.Name) + } + if updatedItem.Value != 150 { + t.Errorf("Expected updated value 150, got %d", updatedItem.Value) + } +} + +func TestDelete(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + item, err := Find(db, 1) + if err != nil { + t.Fatalf("Failed to find item: %v", err) + } + + // Delete item + err = item.Delete() + if err != nil { + t.Fatalf("Failed to delete item: %v", err) + } + + // Verify item was deleted + _, err = Find(db, 1) + if err == nil { + t.Error("Expected error when finding deleted item") + } +} + +func TestItemTypeMethods(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + sword, _ := Find(db, 1) + armor, _ := Find(db, 2) + shield, _ := Find(db, 3) + + // Test IsWeapon + if !sword.IsWeapon() { + t.Error("Expected sword to be weapon") + } + if armor.IsWeapon() { + t.Error("Expected armor not to be weapon") + } + + // Test IsArmor + if !armor.IsArmor() { + t.Error("Expected armor to be armor") + } + if sword.IsArmor() { + t.Error("Expected sword not to be armor") + } + + // Test IsShield + if !shield.IsShield() { + t.Error("Expected shield to be shield") + } + if sword.IsShield() { + t.Error("Expected sword not to be shield") + } + + // Test TypeName + if sword.TypeName() != "Weapon" { + t.Errorf("Expected sword type name 'Weapon', got '%s'", sword.TypeName()) + } + if armor.TypeName() != "Armor" { + t.Errorf("Expected armor type name 'Armor', got '%s'", armor.TypeName()) + } + if shield.TypeName() != "Shield" { + t.Errorf("Expected shield type name 'Shield', got '%s'", shield.TypeName()) + } +}