create items package

This commit is contained in:
Sky Johnson 2025-08-08 23:01:26 -05:00
parent 58248ec339
commit 0210e7dd28
4 changed files with 652 additions and 16 deletions

View File

@ -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 package install
import ( import (
@ -16,24 +24,20 @@ func Run() error {
start := time.Now() start := time.Now()
// Open database connection
db, err := database.Open(dbPath) db, err := database.Open(dbPath)
if err != nil { if err != nil {
return err return err
} }
defer db.Close() defer db.Close()
// Create tables
if err := createTables(db); err != nil { if err := createTables(db); err != nil {
return fmt.Errorf("failed to create tables: %w", err) return fmt.Errorf("failed to create tables: %w", err)
} }
// Populate initial data
if err := populateData(db); err != nil { if err := populateData(db); err != nil {
return fmt.Errorf("failed to populate data: %w", err) return fmt.Errorf("failed to populate data: %w", err)
} }
// Create demo user
if err := createDemoUser(db); err != nil { if err := createDemoUser(db); err != nil {
return fmt.Errorf("failed to create demo user: %w", err) 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 { 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) 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 dropsSQL := `INSERT INTO drops VALUES
(1, 'Life Pebble', 1, 1, 'maxhp,10', ''), (1, 'Life Pebble', 1, 1, 'maxhp,10', ''),
@ -235,7 +239,7 @@ func populateData(db *database.DB) error {
if err := db.Exec(dropsSQL); err != nil { if err := db.Exec(dropsSQL); err != nil {
return fmt.Errorf("failed to populate drops table: %w", err) 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 itemsSQL := `INSERT INTO items VALUES
(1, 1, 'Stick', 10, 2, ''), (1, 1, 'Stick', 10, 2, ''),
@ -274,7 +278,7 @@ func populateData(db *database.DB) error {
if err := db.Exec(itemsSQL); err != nil { if err := db.Exec(itemsSQL); err != nil {
return fmt.Errorf("failed to populate items table: %w", err) 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 monstersSQL := `INSERT INTO monsters VALUES
(1, 'Blue Slime', 4, 3, 1, 1, 1, 1, 0), (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 { if err := db.Exec(monstersSQL); err != nil {
return fmt.Errorf("failed to populate monsters table: %w", err) 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 { 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) 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 spellsSQL := `INSERT INTO spells VALUES
(1, 'Heal', 5, 10, 1), (1, 'Heal', 5, 10, 1),
(2, 'Revive', 10, 25, 1), (2, 'Revive', 10, 25, 1),
@ -463,9 +465,8 @@ func populateData(db *database.DB) error {
if err := db.Exec(spellsSQL); err != nil { if err := db.Exec(spellsSQL); err != nil {
return fmt.Errorf("failed to populate spells table: %w", err) 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 townsSQL := `INSERT INTO towns VALUES
(1, 'Midworld', 0, 0, 5, 0, 0, '1,2,3,17,18,19,28,29'), (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'), (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 { if err := db.Exec(townsSQL); err != nil {
return fmt.Errorf("failed to populate towns table: %w", err) return fmt.Errorf("failed to populate towns table: %w", err)
} }
fmt.Println("✓ Towns table populated") fmt.Println("✓ towns table populated")
return nil return nil
} }
func createDemoUser(db *database.DB) error { func createDemoUser(db *database.DB) error {
// Hash the password using argon2id
hashedPassword, err := password.Hash("Demo123!") hashedPassword, err := password.Hash("Demo123!")
if err != nil { if err != nil {
return fmt.Errorf("failed to hash password: %w", err) return fmt.Errorf("failed to hash password: %w", err)
} }
stmt := `INSERT INTO users (username, password, email, verified, class_id, auth) 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 { if err := db.Exec(stmt, "demo", hashedPassword, "demo@demo.com"); err != nil {
return fmt.Errorf("failed to create demo user: %w", err) return fmt.Errorf("failed to create demo user: %w", err)

123
internal/items/doc.go Normal file
View File

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

241
internal/items/items.go Normal file
View File

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

View File

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