create users package

This commit is contained in:
Sky Johnson 2025-08-09 00:05:06 -05:00
parent 96857e8110
commit c7d08d8004
4 changed files with 1829 additions and 0 deletions

235
internal/users/builder.go Normal file
View File

@ -0,0 +1,235 @@
package users
import (
"fmt"
"time"
"dk/internal/database"
"zombiezen.com/go/sqlite"
)
// Builder provides a fluent interface for creating users
type Builder struct {
user *User
db *database.DB
}
// NewBuilder creates a new user builder with default values
func NewBuilder(db *database.DB) *Builder {
now := time.Now().Unix()
return &Builder{
user: &User{
db: db,
Verified: 0, // Default unverified
Token: "", // Empty verification token
Registered: now, // Current time
LastOnline: now, // Current time
Auth: 0, // Default no special permissions
X: 0, // Default starting position
Y: 0, // Default starting position
ClassID: 1, // Default to class 1
Currently: "In Town", // Default status
Fighting: 0, // Default not fighting
HP: 15, // Default starting HP
MP: 0, // Default starting MP
TP: 10, // Default starting TP
MaxHP: 15, // Default starting max HP
MaxMP: 0, // Default starting max MP
MaxTP: 10, // Default starting max TP
Level: 1, // Default starting level
Gold: 100, // Default starting gold
Exp: 0, // Default starting exp
Strength: 5, // Default starting strength
Dexterity: 5, // Default starting dexterity
Attack: 5, // Default starting attack
Defense: 5, // Default starting defense
Spells: "", // No spells initially
Towns: "", // No towns visited initially
},
db: db,
}
}
// WithUsername sets the username
func (b *Builder) WithUsername(username string) *Builder {
b.user.Username = username
return b
}
// WithPassword sets the password
func (b *Builder) WithPassword(password string) *Builder {
b.user.Password = password
return b
}
// WithEmail sets the email address
func (b *Builder) WithEmail(email string) *Builder {
b.user.Email = email
return b
}
// WithVerified sets the verification status
func (b *Builder) WithVerified(verified bool) *Builder {
if verified {
b.user.Verified = 1
} else {
b.user.Verified = 0
}
return b
}
// WithToken sets the verification token
func (b *Builder) WithToken(token string) *Builder {
b.user.Token = token
return b
}
// WithAuth sets the authorization level
func (b *Builder) WithAuth(auth int) *Builder {
b.user.Auth = auth
return b
}
// AsAdmin sets the user as admin (auth = 4)
func (b *Builder) AsAdmin() *Builder {
b.user.Auth = 4
return b
}
// AsModerator sets the user as moderator (auth = 2)
func (b *Builder) AsModerator() *Builder {
b.user.Auth = 2
return b
}
// WithClassID sets the character class ID
func (b *Builder) WithClassID(classID int) *Builder {
b.user.ClassID = classID
return b
}
// WithPosition sets the starting coordinates
func (b *Builder) WithPosition(x, y int) *Builder {
b.user.X = x
b.user.Y = y
return b
}
// WithLevel sets the starting level
func (b *Builder) WithLevel(level int) *Builder {
b.user.Level = level
return b
}
// WithGold sets the starting gold amount
func (b *Builder) WithGold(gold int) *Builder {
b.user.Gold = gold
return b
}
// WithStats sets the core character stats
func (b *Builder) WithStats(strength, dexterity, attack, defense int) *Builder {
b.user.Strength = strength
b.user.Dexterity = dexterity
b.user.Attack = attack
b.user.Defense = defense
return b
}
// WithHP sets current and maximum HP
func (b *Builder) WithHP(hp, maxHP int) *Builder {
b.user.HP = hp
b.user.MaxHP = maxHP
return b
}
// WithMP sets current and maximum MP
func (b *Builder) WithMP(mp, maxMP int) *Builder {
b.user.MP = mp
b.user.MaxMP = maxMP
return b
}
// WithTP sets current and maximum TP
func (b *Builder) WithTP(tp, maxTP int) *Builder {
b.user.TP = tp
b.user.MaxTP = maxTP
return b
}
// WithCurrently sets the current status message
func (b *Builder) WithCurrently(currently string) *Builder {
b.user.Currently = currently
return b
}
// WithRegistered sets the registration timestamp
func (b *Builder) WithRegistered(registered int64) *Builder {
b.user.Registered = registered
return b
}
// WithRegisteredTime sets the registration timestamp from time.Time
func (b *Builder) WithRegisteredTime(t time.Time) *Builder {
b.user.Registered = t.Unix()
return b
}
// WithSpells sets the user's known spells
func (b *Builder) WithSpells(spells []string) *Builder {
b.user.SetSpellIDs(spells)
return b
}
// WithTowns sets the user's visited towns
func (b *Builder) WithTowns(towns []string) *Builder {
b.user.SetTownIDs(towns)
return b
}
// Create saves the user to the database and returns the created user with ID
func (b *Builder) Create() (*User, error) {
// Use a transaction to ensure we can get the ID
var user *User
err := b.db.Transaction(func(tx *database.Tx) error {
query := `INSERT INTO users (username, password, email, verified, token, registered, last_online, auth,
x, y, class_id, currently, fighting, monster_id, monster_hp, monster_sleep, monster_immune,
uber_damage, uber_defense, hp, mp, tp, max_hp, max_mp, max_tp, level, gold, exp,
gold_bonus, exp_bonus, strength, dexterity, attack, defense, weapon_id, armor_id, shield_id,
slot_1_id, slot_2_id, slot_3_id, weapon_name, armor_name, shield_name,
slot_1_name, slot_2_name, slot_3_name, drop_code, spells, towns)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
if err := tx.Exec(query, b.user.Username, b.user.Password, b.user.Email, b.user.Verified, b.user.Token,
b.user.Registered, b.user.LastOnline, b.user.Auth, b.user.X, b.user.Y, b.user.ClassID, b.user.Currently,
b.user.Fighting, b.user.MonsterID, b.user.MonsterHP, b.user.MonsterSleep, b.user.MonsterImmune,
b.user.UberDamage, b.user.UberDefense, b.user.HP, b.user.MP, b.user.TP, b.user.MaxHP, b.user.MaxMP, b.user.MaxTP,
b.user.Level, b.user.Gold, b.user.Exp, b.user.GoldBonus, b.user.ExpBonus, b.user.Strength, b.user.Dexterity,
b.user.Attack, b.user.Defense, b.user.WeaponID, b.user.ArmorID, b.user.ShieldID, b.user.Slot1ID,
b.user.Slot2ID, b.user.Slot3ID, b.user.WeaponName, b.user.ArmorName, b.user.ShieldName,
b.user.Slot1Name, b.user.Slot2Name, b.user.Slot3Name, b.user.DropCode, b.user.Spells, b.user.Towns); err != nil {
return fmt.Errorf("failed to insert user: %w", err)
}
// Get the last insert ID
var id int
err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error {
id = stmt.ColumnInt(0)
return nil
})
if err != nil {
return fmt.Errorf("failed to get insert ID: %w", err)
}
b.user.ID = id
user = b.user
return nil
})
if err != nil {
return nil, err
}
return user, nil
}

500
internal/users/doc.go Normal file
View File

@ -0,0 +1,500 @@
/*
Package users is the active record implementation for user accounts in the game.
The users package provides comprehensive user management for the game, including authentication, character progression, inventory, equipment, and game state management. It handles all aspects of player accounts from registration to advanced gameplay features.
# Basic Usage
To retrieve a user by ID:
user, err := users.Find(db, 1)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %s (Level %d)\n", user.Username, user.Level)
To find a user by username:
user, err := users.ByUsername(db, "playerName")
if err != nil {
log.Fatal(err)
}
To find a user by email:
user, err := users.ByEmail(db, "player@example.com")
if err != nil {
log.Fatal(err)
}
# Creating Users with Builder Pattern
The package provides a comprehensive builder for creating new user accounts:
## Basic User Creation
user, err := users.NewBuilder(db).
WithUsername("newplayer").
WithPassword("hashedPassword").
WithEmail("newplayer@example.com").
WithClassID(1).
Create()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created user with ID: %d\n", user.ID)
## Advanced User Creation
user, err := users.NewBuilder(db).
WithUsername("hero").
WithPassword("secureHash").
WithEmail("hero@example.com").
WithVerified(true).
WithClassID(2).
WithPosition(100, -50).
WithLevel(5).
WithGold(500).
WithStats(8, 7, 10, 9). // strength, dex, attack, defense
WithHP(30, 30).
WithMP(20, 20).
WithSpells([]string{"1", "3", "5"}).
WithTowns([]string{"1", "2"}).
AsAdmin().
Create()
The builder automatically sets sensible defaults for all fields if not specified.
# User Management
## Authentication and Verification
user, _ := users.Find(db, userID)
// Check verification status
if user.IsVerified() {
fmt.Println("User email is verified")
}
// Check authorization levels
if user.IsAdmin() {
fmt.Println("User has admin privileges")
}
if user.IsModerator() {
fmt.Println("User has moderator privileges")
}
## Activity Tracking
// Update last online time
user.UpdateLastOnline()
user.Save()
// Get activity information
registered := user.RegisteredTime()
lastOnline := user.LastOnlineTime()
fmt.Printf("Registered: %s\n", registered.Format("Jan 2, 2006"))
fmt.Printf("Last online: %s\n", lastOnline.Format("Jan 2 15:04"))
# Character Management
## Stats and Progression
user, _ := users.Find(db, userID)
// Get character stats
stats := user.GetStats()
fmt.Printf("Level %d: HP %d/%d, MP %d/%d\n",
stats["level"], stats["hp"], stats["max_hp"],
stats["mp"], stats["max_mp"])
// Update character progression
user.Level = 10
user.Exp = 5000
user.MaxHP = 50
user.HP = 50
user.Save()
## Position and Movement
// Get current position
x, y := user.GetPosition()
fmt.Printf("Player at (%d, %d)\n", x, y)
// Move player
user.SetPosition(newX, newY)
user.Currently = "Exploring the forest"
user.Save()
## Combat Status
if user.IsFighting() {
fmt.Printf("Fighting monster ID %d (HP: %d)\n",
user.MonsterID, user.MonsterHP)
}
if user.IsAlive() {
fmt.Printf("Player has %d HP remaining\n", user.HP)
}
# Spell System
## Spell Management
user, _ := users.Find(db, userID)
// Get known spells
spells := user.GetSpellIDs()
fmt.Printf("Player knows %d spells: %v\n", len(spells), spells)
// Check if player knows a specific spell
if user.HasSpell("5") {
fmt.Println("Player knows spell 5")
}
// Learn new spells
newSpells := append(spells, "7", "8")
user.SetSpellIDs(newSpells)
user.Save()
## Spell Integration
func castSpell(db *database.DB, userID int, spellID string) error {
user, err := users.Find(db, userID)
if err != nil {
return err
}
if !user.HasSpell(spellID) {
return fmt.Errorf("user doesn't know spell %s", spellID)
}
// Spell casting logic here...
return nil
}
# Town and Travel System
## Town Visits
user, _ := users.Find(db, userID)
// Get visited towns
towns := user.GetTownIDs()
fmt.Printf("Visited %d towns: %v\n", len(towns), towns)
// Check if player has visited a town
if user.HasVisitedTown("3") {
fmt.Println("Player has been to town 3")
}
// Visit new town
visitedTowns := append(towns, "4")
user.SetTownIDs(visitedTowns)
user.Save()
## Travel Integration
func visitTown(db *database.DB, userID int, townID string) error {
user, err := users.Find(db, userID)
if err != nil {
return err
}
// Add town to visited list if not already there
if !user.HasVisitedTown(townID) {
towns := user.GetTownIDs()
user.SetTownIDs(append(towns, townID))
}
// Update position and status
// town coordinates would be looked up here
user.Currently = fmt.Sprintf("In town %s", townID)
return user.Save()
}
# Equipment System
## Equipment Management
user, _ := users.Find(db, userID)
// Get all equipment
equipment := user.GetEquipment()
weapon := equipment["weapon"].(map[string]interface{})
armor := equipment["armor"].(map[string]interface{})
fmt.Printf("Weapon: %s (ID: %d)\n", weapon["name"], weapon["id"])
fmt.Printf("Armor: %s (ID: %d)\n", armor["name"], armor["id"])
// Equip new items
user.WeaponID = 15
user.WeaponName = "Dragon Sword"
user.ArmorID = 8
user.ArmorName = "Steel Plate"
user.Save()
# Query Operations
## Level-Based Queries
// Get all players at a specific level
level5Players, err := users.ByLevel(db, 5)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Level 5 players (%d):\n", len(level5Players))
for _, player := range level5Players {
fmt.Printf("- %s (EXP: %d)\n", player.Username, player.Exp)
}
## Online Status Queries
// Get players online in the last hour
onlinePlayers, err := users.Online(db, time.Hour)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Players online in last hour (%d):\n", len(onlinePlayers))
for _, player := range onlinePlayers {
lastSeen := time.Since(player.LastOnlineTime())
fmt.Printf("- %s (last seen %v ago)\n", player.Username, lastSeen)
}
# Database Schema
The users table contains extensive character and game state information:
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
password TEXT NOT NULL,
email TEXT NOT NULL,
verified INTEGER NOT NULL DEFAULT 0,
token TEXT NOT NULL DEFAULT '',
registered INTEGER NOT NULL DEFAULT (unixepoch()),
last_online INTEGER NOT NULL DEFAULT (unixepoch()),
auth INTEGER NOT NULL DEFAULT 0,
x INTEGER NOT NULL DEFAULT 0,
y INTEGER NOT NULL DEFAULT 0,
class_id INTEGER NOT NULL DEFAULT 0,
currently TEXT NOT NULL DEFAULT 'In Town',
fighting INTEGER NOT NULL DEFAULT 0,
-- Combat state fields
monster_id INTEGER NOT NULL DEFAULT 0,
monster_hp INTEGER NOT NULL DEFAULT 0,
monster_sleep INTEGER NOT NULL DEFAULT 0,
monster_immune INTEGER NOT NULL DEFAULT 0,
uber_damage INTEGER NOT NULL DEFAULT 0,
uber_defense INTEGER NOT NULL DEFAULT 0,
-- Character stats
hp INTEGER NOT NULL DEFAULT 15,
mp INTEGER NOT NULL DEFAULT 0,
tp INTEGER NOT NULL DEFAULT 10,
max_hp INTEGER NOT NULL DEFAULT 15,
max_mp INTEGER NOT NULL DEFAULT 0,
max_tp INTEGER NOT NULL DEFAULT 10,
level INTEGER NOT NULL DEFAULT 1,
gold INTEGER NOT NULL DEFAULT 100,
exp INTEGER NOT NULL DEFAULT 0,
gold_bonus INTEGER NOT NULL DEFAULT 0,
exp_bonus INTEGER NOT NULL DEFAULT 0,
strength INTEGER NOT NULL DEFAULT 5,
dexterity INTEGER NOT NULL DEFAULT 5,
attack INTEGER NOT NULL DEFAULT 5,
defense INTEGER NOT NULL DEFAULT 5,
-- Equipment
weapon_id INTEGER NOT NULL DEFAULT 0,
armor_id INTEGER NOT NULL DEFAULT 0,
shield_id INTEGER NOT NULL DEFAULT 0,
slot_1_id INTEGER NOT NULL DEFAULT 0,
slot_2_id INTEGER NOT NULL DEFAULT 0,
slot_3_id INTEGER NOT NULL DEFAULT 0,
weapon_name TEXT NOT NULL DEFAULT '',
armor_name TEXT NOT NULL DEFAULT '',
shield_name TEXT NOT NULL DEFAULT '',
slot_1_name TEXT NOT NULL DEFAULT '',
slot_2_name TEXT NOT NULL DEFAULT '',
slot_3_name TEXT NOT NULL DEFAULT '',
-- Game state
drop_code INTEGER NOT NULL DEFAULT 0,
spells TEXT NOT NULL DEFAULT '',
towns TEXT NOT NULL DEFAULT ''
)
# Advanced Features
## Character Progression
func levelUpCharacter(user *users.User, newLevel int) {
user.Level = newLevel
// Increase base stats
user.MaxHP += 5
user.HP = user.MaxHP // Full heal on level up
user.MaxMP += 2
user.MP = user.MaxMP
// Stat bonuses
user.Strength++
user.Attack++
user.Defense++
user.Save()
}
## Combat Integration
func startCombat(user *users.User, monsterID int) error {
if user.IsFighting() {
return fmt.Errorf("already in combat")
}
user.Fighting = 1
user.MonsterID = monsterID
// monster HP would be looked up from monsters table
user.MonsterHP = 50
user.Currently = "Fighting"
return user.Save()
}
func endCombat(user *users.User, won bool) error {
user.Fighting = 0
user.MonsterID = 0
user.MonsterHP = 0
user.MonsterSleep = 0
user.MonsterImmune = 0
if won {
user.Currently = "Victorious"
// Award experience and gold
} else {
user.Currently = "Defeated"
user.HP = 0 // Player defeated
}
return user.Save()
}
## Administrative Functions
func promoteUser(db *database.DB, username string, authLevel int) error {
user, err := users.ByUsername(db, username)
if err != nil {
return err
}
user.Auth = authLevel
return user.Save()
}
func getUsersByAuthLevel(db *database.DB, minAuth int) ([]*users.User, error) {
allUsers, err := users.All(db)
if err != nil {
return nil, err
}
var authorizedUsers []*users.User
for _, user := range allUsers {
if user.Auth >= minAuth {
authorizedUsers = append(authorizedUsers, user)
}
}
return authorizedUsers, nil
}
# Performance Considerations
The users table is large and frequently accessed. Consider:
## Efficient Queries
// Use specific lookups when possible
user, _ := users.ByUsername(db, username) // Uses index
user, _ := users.ByEmail(db, email) // Uses index
// Limit results for admin interfaces
onlineUsers, _ := users.Online(db, time.Hour) // Bounded by time
levelUsers, _ := users.ByLevel(db, targetLevel) // Bounded by level
## Caching Strategies
// Cache frequently accessed user data
type UserCache struct {
users map[int]*users.User
mutex sync.RWMutex
}
func (c *UserCache) GetUser(db *database.DB, id int) (*users.User, error) {
c.mutex.RLock()
if user, ok := c.users[id]; ok {
c.mutex.RUnlock()
return user, nil
}
c.mutex.RUnlock()
user, err := users.Find(db, id)
if err != nil {
return nil, err
}
c.mutex.Lock()
c.users[id] = user
c.mutex.Unlock()
return user, nil
}
# Integration Examples
## Session Management
func authenticateUser(db *database.DB, username, password string) (*users.User, error) {
user, err := users.ByUsername(db, username)
if err != nil {
return nil, fmt.Errorf("user not found")
}
if !user.IsVerified() {
return nil, fmt.Errorf("email not verified")
}
// Verify password (implement password checking)
if !verifyPassword(user.Password, password) {
return nil, fmt.Errorf("invalid password")
}
// Update last online
user.UpdateLastOnline()
user.Save()
return user, nil
}
## Game State Management
func saveGameState(user *users.User, gameData GameState) error {
user.X = gameData.X
user.Y = gameData.Y
user.HP = gameData.HP
user.MP = gameData.MP
user.Currently = gameData.Status
if gameData.InCombat {
user.Fighting = 1
user.MonsterID = gameData.MonsterID
user.MonsterHP = gameData.MonsterHP
}
return user.Save()
}
The users package provides comprehensive player account management with support for all game mechanics including character progression, combat, equipment, spells, and world exploration.
*/
package users

424
internal/users/users.go Normal file
View File

@ -0,0 +1,424 @@
package users
import (
"fmt"
"strings"
"time"
"dk/internal/database"
"zombiezen.com/go/sqlite"
)
// User represents a user in the database
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Verified int `json:"verified"`
Token string `json:"token"`
Registered int64 `json:"registered"`
LastOnline int64 `json:"last_online"`
Auth int `json:"auth"`
X int `json:"x"`
Y int `json:"y"`
ClassID int `json:"class_id"`
Currently string `json:"currently"`
Fighting int `json:"fighting"`
MonsterID int `json:"monster_id"`
MonsterHP int `json:"monster_hp"`
MonsterSleep int `json:"monster_sleep"`
MonsterImmune int `json:"monster_immune"`
UberDamage int `json:"uber_damage"`
UberDefense int `json:"uber_defense"`
HP int `json:"hp"`
MP int `json:"mp"`
TP int `json:"tp"`
MaxHP int `json:"max_hp"`
MaxMP int `json:"max_mp"`
MaxTP int `json:"max_tp"`
Level int `json:"level"`
Gold int `json:"gold"`
Exp int `json:"exp"`
GoldBonus int `json:"gold_bonus"`
ExpBonus int `json:"exp_bonus"`
Strength int `json:"strength"`
Dexterity int `json:"dexterity"`
Attack int `json:"attack"`
Defense int `json:"defense"`
WeaponID int `json:"weapon_id"`
ArmorID int `json:"armor_id"`
ShieldID int `json:"shield_id"`
Slot1ID int `json:"slot_1_id"`
Slot2ID int `json:"slot_2_id"`
Slot3ID int `json:"slot_3_id"`
WeaponName string `json:"weapon_name"`
ArmorName string `json:"armor_name"`
ShieldName string `json:"shield_name"`
Slot1Name string `json:"slot_1_name"`
Slot2Name string `json:"slot_2_name"`
Slot3Name string `json:"slot_3_name"`
DropCode int `json:"drop_code"`
Spells string `json:"spells"`
Towns string `json:"towns"`
db *database.DB
}
// userColumns returns the column list for user queries (excluding id for inserts)
func userColumns() string {
return `id, username, password, email, verified, token, registered, last_online, auth,
x, y, class_id, currently, fighting, monster_id, monster_hp, monster_sleep, monster_immune,
uber_damage, uber_defense, hp, mp, tp, max_hp, max_mp, max_tp, level, gold, exp,
gold_bonus, exp_bonus, strength, dexterity, attack, defense, weapon_id, armor_id, shield_id,
slot_1_id, slot_2_id, slot_3_id, weapon_name, armor_name, shield_name,
slot_1_name, slot_2_name, slot_3_name, drop_code, spells, towns`
}
// scanUser populates a User struct from a sqlite.Stmt
func scanUser(stmt *sqlite.Stmt, db *database.DB) *User {
return &User{
ID: stmt.ColumnInt(0),
Username: stmt.ColumnText(1),
Password: stmt.ColumnText(2),
Email: stmt.ColumnText(3),
Verified: stmt.ColumnInt(4),
Token: stmt.ColumnText(5),
Registered: stmt.ColumnInt64(6),
LastOnline: stmt.ColumnInt64(7),
Auth: stmt.ColumnInt(8),
X: stmt.ColumnInt(9),
Y: stmt.ColumnInt(10),
ClassID: stmt.ColumnInt(11),
Currently: stmt.ColumnText(12),
Fighting: stmt.ColumnInt(13),
MonsterID: stmt.ColumnInt(14),
MonsterHP: stmt.ColumnInt(15),
MonsterSleep: stmt.ColumnInt(16),
MonsterImmune: stmt.ColumnInt(17),
UberDamage: stmt.ColumnInt(18),
UberDefense: stmt.ColumnInt(19),
HP: stmt.ColumnInt(20),
MP: stmt.ColumnInt(21),
TP: stmt.ColumnInt(22),
MaxHP: stmt.ColumnInt(23),
MaxMP: stmt.ColumnInt(24),
MaxTP: stmt.ColumnInt(25),
Level: stmt.ColumnInt(26),
Gold: stmt.ColumnInt(27),
Exp: stmt.ColumnInt(28),
GoldBonus: stmt.ColumnInt(29),
ExpBonus: stmt.ColumnInt(30),
Strength: stmt.ColumnInt(31),
Dexterity: stmt.ColumnInt(32),
Attack: stmt.ColumnInt(33),
Defense: stmt.ColumnInt(34),
WeaponID: stmt.ColumnInt(35),
ArmorID: stmt.ColumnInt(36),
ShieldID: stmt.ColumnInt(37),
Slot1ID: stmt.ColumnInt(38),
Slot2ID: stmt.ColumnInt(39),
Slot3ID: stmt.ColumnInt(40),
WeaponName: stmt.ColumnText(41),
ArmorName: stmt.ColumnText(42),
ShieldName: stmt.ColumnText(43),
Slot1Name: stmt.ColumnText(44),
Slot2Name: stmt.ColumnText(45),
Slot3Name: stmt.ColumnText(46),
DropCode: stmt.ColumnInt(47),
Spells: stmt.ColumnText(48),
Towns: stmt.ColumnText(49),
db: db,
}
}
// Find retrieves a user by ID
func Find(db *database.DB, id int) (*User, error) {
var user *User
query := `SELECT ` + userColumns() + ` FROM users WHERE id = ?`
err := db.Query(query, func(stmt *sqlite.Stmt) error {
user = scanUser(stmt, db)
return nil
}, id)
if err != nil {
return nil, fmt.Errorf("failed to find user: %w", err)
}
if user == nil {
return nil, fmt.Errorf("user with ID %d not found", id)
}
return user, nil
}
// All retrieves all users ordered by registration date (newest first)
func All(db *database.DB) ([]*User, error) {
var users []*User
query := `SELECT ` + userColumns() + ` FROM users ORDER BY registered DESC, id DESC`
err := db.Query(query, func(stmt *sqlite.Stmt) error {
user := scanUser(stmt, db)
users = append(users, user)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to retrieve all users: %w", err)
}
return users, nil
}
// ByUsername retrieves a user by username (case-insensitive)
func ByUsername(db *database.DB, username string) (*User, error) {
var user *User
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(username) = LOWER(?) LIMIT 1`
err := db.Query(query, func(stmt *sqlite.Stmt) error {
user = scanUser(stmt, db)
return nil
}, username)
if err != nil {
return nil, fmt.Errorf("failed to find user by username: %w", err)
}
if user == nil {
return nil, fmt.Errorf("user with username '%s' not found", username)
}
return user, nil
}
// ByEmail retrieves a user by email address
func ByEmail(db *database.DB, email string) (*User, error) {
var user *User
query := `SELECT ` + userColumns() + ` FROM users WHERE email = ? LIMIT 1`
err := db.Query(query, func(stmt *sqlite.Stmt) error {
user = scanUser(stmt, db)
return nil
}, email)
if err != nil {
return nil, fmt.Errorf("failed to find user by email: %w", err)
}
if user == nil {
return nil, fmt.Errorf("user with email '%s' not found", email)
}
return user, nil
}
// ByLevel retrieves users at a specific level
func ByLevel(db *database.DB, level int) ([]*User, error) {
var users []*User
query := `SELECT ` + userColumns() + ` FROM users WHERE level = ? ORDER BY exp DESC, id ASC`
err := db.Query(query, func(stmt *sqlite.Stmt) error {
user := scanUser(stmt, db)
users = append(users, user)
return nil
}, level)
if err != nil {
return nil, fmt.Errorf("failed to retrieve users by level: %w", err)
}
return users, nil
}
// Online retrieves users who have been online within the specified duration
func Online(db *database.DB, within time.Duration) ([]*User, error) {
var users []*User
cutoff := time.Now().Add(-within).Unix()
query := `SELECT ` + userColumns() + ` FROM users WHERE last_online >= ? ORDER BY last_online DESC, id ASC`
err := db.Query(query, func(stmt *sqlite.Stmt) error {
user := scanUser(stmt, db)
users = append(users, user)
return nil
}, cutoff)
if err != nil {
return nil, fmt.Errorf("failed to retrieve online users: %w", err)
}
return users, nil
}
// Save updates an existing user in the database
func (u *User) Save() error {
if u.ID == 0 {
return fmt.Errorf("cannot save user without ID")
}
query := `UPDATE users SET username = ?, password = ?, email = ?, verified = ?, token = ?,
registered = ?, last_online = ?, auth = ?, x = ?, y = ?, class_id = ?, currently = ?,
fighting = ?, monster_id = ?, monster_hp = ?, monster_sleep = ?, monster_immune = ?,
uber_damage = ?, uber_defense = ?, hp = ?, mp = ?, tp = ?, max_hp = ?, max_mp = ?, max_tp = ?,
level = ?, gold = ?, exp = ?, gold_bonus = ?, exp_bonus = ?, strength = ?, dexterity = ?,
attack = ?, defense = ?, weapon_id = ?, armor_id = ?, shield_id = ?, slot_1_id = ?,
slot_2_id = ?, slot_3_id = ?, weapon_name = ?, armor_name = ?, shield_name = ?,
slot_1_name = ?, slot_2_name = ?, slot_3_name = ?, drop_code = ?, spells = ?, towns = ?
WHERE id = ?`
return u.db.Exec(query, u.Username, u.Password, u.Email, u.Verified, u.Token,
u.Registered, u.LastOnline, u.Auth, u.X, u.Y, u.ClassID, u.Currently,
u.Fighting, u.MonsterID, u.MonsterHP, u.MonsterSleep, u.MonsterImmune,
u.UberDamage, u.UberDefense, u.HP, u.MP, u.TP, u.MaxHP, u.MaxMP, u.MaxTP,
u.Level, u.Gold, u.Exp, u.GoldBonus, u.ExpBonus, u.Strength, u.Dexterity,
u.Attack, u.Defense, u.WeaponID, u.ArmorID, u.ShieldID, u.Slot1ID,
u.Slot2ID, u.Slot3ID, u.WeaponName, u.ArmorName, u.ShieldName,
u.Slot1Name, u.Slot2Name, u.Slot3Name, u.DropCode, u.Spells, u.Towns, u.ID)
}
// Delete removes the user from the database
func (u *User) Delete() error {
if u.ID == 0 {
return fmt.Errorf("cannot delete user without ID")
}
query := "DELETE FROM users WHERE id = ?"
return u.db.Exec(query, u.ID)
}
// RegisteredTime returns the registration timestamp as a time.Time
func (u *User) RegisteredTime() time.Time {
return time.Unix(u.Registered, 0)
}
// LastOnlineTime returns the last online timestamp as a time.Time
func (u *User) LastOnlineTime() time.Time {
return time.Unix(u.LastOnline, 0)
}
// UpdateLastOnline sets the last online timestamp to current time
func (u *User) UpdateLastOnline() {
u.LastOnline = time.Now().Unix()
}
// IsVerified returns true if the user's email is verified
func (u *User) IsVerified() bool {
return u.Verified == 1
}
// IsAdmin returns true if the user has admin privileges (auth >= 4)
func (u *User) IsAdmin() bool {
return u.Auth >= 4
}
// IsModerator returns true if the user has moderator privileges (auth >= 2)
func (u *User) IsModerator() bool {
return u.Auth >= 2
}
// IsFighting returns true if the user is currently fighting
func (u *User) IsFighting() bool {
return u.Fighting == 1
}
// IsAlive returns true if the user has HP > 0
func (u *User) IsAlive() bool {
return u.HP > 0
}
// GetSpellIDs returns spell IDs as a slice of strings
func (u *User) GetSpellIDs() []string {
if u.Spells == "" {
return []string{}
}
return strings.Split(u.Spells, ",")
}
// SetSpellIDs sets spell IDs from a slice of strings
func (u *User) SetSpellIDs(spells []string) {
u.Spells = strings.Join(spells, ",")
}
// HasSpell returns true if the user knows the specified spell ID
func (u *User) HasSpell(spellID string) bool {
spells := u.GetSpellIDs()
for _, spell := range spells {
if strings.TrimSpace(spell) == spellID {
return true
}
}
return false
}
// GetTownIDs returns town IDs as a slice of strings
func (u *User) GetTownIDs() []string {
if u.Towns == "" {
return []string{}
}
return strings.Split(u.Towns, ",")
}
// SetTownIDs sets town IDs from a slice of strings
func (u *User) SetTownIDs(towns []string) {
u.Towns = strings.Join(towns, ",")
}
// HasVisitedTown returns true if the user has visited the specified town ID
func (u *User) HasVisitedTown(townID string) bool {
towns := u.GetTownIDs()
for _, town := range towns {
if strings.TrimSpace(town) == townID {
return true
}
}
return false
}
// GetEquipment returns all equipped item information
func (u *User) GetEquipment() map[string]interface{} {
return map[string]interface{}{
"weapon": map[string]interface{}{"id": u.WeaponID, "name": u.WeaponName},
"armor": map[string]interface{}{"id": u.ArmorID, "name": u.ArmorName},
"shield": map[string]interface{}{"id": u.ShieldID, "name": u.ShieldName},
"slot1": map[string]interface{}{"id": u.Slot1ID, "name": u.Slot1Name},
"slot2": map[string]interface{}{"id": u.Slot2ID, "name": u.Slot2Name},
"slot3": map[string]interface{}{"id": u.Slot3ID, "name": u.Slot3Name},
}
}
// GetStats returns combat-relevant stats
func (u *User) GetStats() map[string]int {
return map[string]int{
"level": u.Level,
"hp": u.HP,
"mp": u.MP,
"tp": u.TP,
"max_hp": u.MaxHP,
"max_mp": u.MaxMP,
"max_tp": u.MaxTP,
"strength": u.Strength,
"dexterity": u.Dexterity,
"attack": u.Attack,
"defense": u.Defense,
"uber_damage": u.UberDamage,
"uber_defense": u.UberDefense,
}
}
// GetPosition returns the user's coordinates
func (u *User) GetPosition() (int, int) {
return u.X, u.Y
}
// SetPosition sets the user's coordinates
func (u *User) SetPosition(x, y int) {
u.X = x
u.Y = y
}

View File

@ -0,0 +1,670 @@
package users
import (
"os"
"testing"
"time"
"dk/internal/database"
)
func setupTestDB(t *testing.T) *database.DB {
testDB := "test_users.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 users table
createTable := `CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
password TEXT NOT NULL,
email TEXT NOT NULL,
verified INTEGER NOT NULL DEFAULT 0,
token TEXT NOT NULL DEFAULT '',
registered INTEGER NOT NULL DEFAULT (unixepoch()),
last_online INTEGER NOT NULL DEFAULT (unixepoch()),
auth INTEGER NOT NULL DEFAULT 0,
x INTEGER NOT NULL DEFAULT 0,
y INTEGER NOT NULL DEFAULT 0,
class_id INTEGER NOT NULL DEFAULT 0,
currently TEXT NOT NULL DEFAULT 'In Town',
fighting INTEGER NOT NULL DEFAULT 0,
monster_id INTEGER NOT NULL DEFAULT 0,
monster_hp INTEGER NOT NULL DEFAULT 0,
monster_sleep INTEGER NOT NULL DEFAULT 0,
monster_immune INTEGER NOT NULL DEFAULT 0,
uber_damage INTEGER NOT NULL DEFAULT 0,
uber_defense INTEGER NOT NULL DEFAULT 0,
hp INTEGER NOT NULL DEFAULT 15,
mp INTEGER NOT NULL DEFAULT 0,
tp INTEGER NOT NULL DEFAULT 10,
max_hp INTEGER NOT NULL DEFAULT 15,
max_mp INTEGER NOT NULL DEFAULT 0,
max_tp INTEGER NOT NULL DEFAULT 10,
level INTEGER NOT NULL DEFAULT 1,
gold INTEGER NOT NULL DEFAULT 100,
exp INTEGER NOT NULL DEFAULT 0,
gold_bonus INTEGER NOT NULL DEFAULT 0,
exp_bonus INTEGER NOT NULL DEFAULT 0,
strength INTEGER NOT NULL DEFAULT 5,
dexterity INTEGER NOT NULL DEFAULT 5,
attack INTEGER NOT NULL DEFAULT 5,
defense INTEGER NOT NULL DEFAULT 5,
weapon_id INTEGER NOT NULL DEFAULT 0,
armor_id INTEGER NOT NULL DEFAULT 0,
shield_id INTEGER NOT NULL DEFAULT 0,
slot_1_id INTEGER NOT NULL DEFAULT 0,
slot_2_id INTEGER NOT NULL DEFAULT 0,
slot_3_id INTEGER NOT NULL DEFAULT 0,
weapon_name TEXT NOT NULL DEFAULT '',
armor_name TEXT NOT NULL DEFAULT '',
shield_name TEXT NOT NULL DEFAULT '',
slot_1_name TEXT NOT NULL DEFAULT '',
slot_2_name TEXT NOT NULL DEFAULT '',
slot_3_name TEXT NOT NULL DEFAULT '',
drop_code INTEGER NOT NULL DEFAULT 0,
spells TEXT NOT NULL DEFAULT '',
towns TEXT NOT NULL DEFAULT ''
)`
if err := db.Exec(createTable); err != nil {
t.Fatalf("Failed to create users table: %v", err)
}
// Insert test data with specific timestamps
now := time.Now().Unix()
testUsers := `INSERT INTO users (username, password, email, verified, token, registered, last_online, auth,
x, y, class_id, level, gold, exp, hp, mp, tp, max_hp, max_mp, max_tp,
strength, dexterity, attack, defense, spells, towns) VALUES
('alice', 'hashed_pass_1', 'alice@example.com', 1, '', ?, ?, 0, 10, 20, 1, 5, 500, 1250, 25, 15, 12, 25, 15, 12, 8, 7, 10, 8, '1,2,5', '1,2'),
('bob', 'hashed_pass_2', 'bob@example.com', 1, '', ?, ?, 2, -5, 15, 2, 3, 300, 750, 20, 8, 10, 20, 8, 10, 6, 8, 8, 9, '3,4', '1'),
('charlie', 'hashed_pass_3', 'charlie@example.com', 0, 'verify_token_123', ?, ?, 4, 0, 0, 3, 1, 100, 0, 15, 0, 10, 15, 0, 10, 5, 5, 5, 5, '', ''),
('diana', 'hashed_pass_4', 'diana@example.com', 1, '', ?, ?, 0, 25, -10, 1, 8, 1200, 3500, 35, 25, 15, 35, 25, 15, 12, 10, 15, 12, '1,2,3,6,7', '1,2,3,4')`
timestamps := []interface{}{
now - 86400*7, now - 3600*2, // alice: registered 1 week ago, last online 2 hours ago
now - 86400*5, now - 86400*1, // bob: registered 5 days ago, last online 1 day ago
now - 86400*1, now - 86400*1, // charlie: registered 1 day ago, last online 1 day ago
now - 86400*30, now - 3600*1, // diana: registered 1 month ago, last online 1 hour ago
}
if err := db.Exec(testUsers, timestamps...); err != nil {
t.Fatalf("Failed to insert test users: %v", err)
}
return db
}
func TestFind(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test finding existing user
user, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find user: %v", err)
}
if user.ID != 1 {
t.Errorf("Expected ID 1, got %d", user.ID)
}
if user.Username != "alice" {
t.Errorf("Expected username 'alice', got '%s'", user.Username)
}
if user.Email != "alice@example.com" {
t.Errorf("Expected email 'alice@example.com', got '%s'", user.Email)
}
if user.Verified != 1 {
t.Errorf("Expected verified 1, got %d", user.Verified)
}
if user.Auth != 0 {
t.Errorf("Expected auth 0, got %d", user.Auth)
}
if user.Level != 5 {
t.Errorf("Expected level 5, got %d", user.Level)
}
// Test finding non-existent user
_, err = Find(db, 999)
if err == nil {
t.Error("Expected error when finding non-existent user")
}
}
func TestAll(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
users, err := All(db)
if err != nil {
t.Fatalf("Failed to get all users: %v", err)
}
if len(users) != 4 {
t.Errorf("Expected 4 users, got %d", len(users))
}
// Check ordering (by registered DESC)
if len(users) >= 2 {
if users[0].Registered < users[1].Registered {
t.Error("Expected users to be ordered by registration date (newest first)")
}
}
}
func TestByUsername(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test finding existing user by username
user, err := ByUsername(db, "alice")
if err != nil {
t.Fatalf("Failed to find user by username: %v", err)
}
if user.Username != "alice" {
t.Errorf("Expected username 'alice', got '%s'", user.Username)
}
if user.ID != 1 {
t.Errorf("Expected ID 1, got %d", user.ID)
}
// Test case insensitive search
userUpper, err := ByUsername(db, "ALICE")
if err != nil {
t.Fatalf("Failed to find user by uppercase username: %v", err)
}
if userUpper.ID != user.ID {
t.Error("Expected case insensitive search to return same user")
}
// Test non-existent user
_, err = ByUsername(db, "nonexistent")
if err == nil {
t.Error("Expected error when finding non-existent user by username")
}
}
func TestByEmail(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test finding existing user by email
user, err := ByEmail(db, "bob@example.com")
if err != nil {
t.Fatalf("Failed to find user by email: %v", err)
}
if user.Email != "bob@example.com" {
t.Errorf("Expected email 'bob@example.com', got '%s'", user.Email)
}
if user.Username != "bob" {
t.Errorf("Expected username 'bob', got '%s'", user.Username)
}
// Test non-existent email
_, err = ByEmail(db, "nonexistent@example.com")
if err == nil {
t.Error("Expected error when finding non-existent user by email")
}
}
func TestByLevel(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test users at level 1
level1Users, err := ByLevel(db, 1)
if err != nil {
t.Fatalf("Failed to get users by level: %v", err)
}
expectedCount := 1 // Charlie is level 1
if len(level1Users) != expectedCount {
t.Errorf("Expected %d users at level 1, got %d", expectedCount, len(level1Users))
}
// Verify all users are level 1
for _, user := range level1Users {
if user.Level != 1 {
t.Errorf("Expected level 1, got %d for user %s", user.Level, user.Username)
}
}
// Test level with no users
noUsers, err := ByLevel(db, 99)
if err != nil {
t.Fatalf("Failed to query non-existent level: %v", err)
}
if len(noUsers) != 0 {
t.Errorf("Expected 0 users at level 99, got %d", len(noUsers))
}
}
func TestOnline(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test users online within the last 6 hours
onlineUsers, err := Online(db, 6*time.Hour)
if err != nil {
t.Fatalf("Failed to get online users: %v", err)
}
// Alice (2 hours ago) and Diana (1 hour ago) should be included
expectedCount := 2
if len(onlineUsers) != expectedCount {
t.Errorf("Expected %d users online within 6 hours, got %d", expectedCount, len(onlineUsers))
}
// Check ordering (by last_online DESC)
if len(onlineUsers) >= 2 {
if onlineUsers[0].LastOnline < onlineUsers[1].LastOnline {
t.Error("Expected online users to be ordered by last online time")
}
}
// Test narrow time window
recentUsers, err := Online(db, 30*time.Minute)
if err != nil {
t.Fatalf("Failed to get recently online users: %v", err)
}
// No users should be online within the last 30 minutes
if len(recentUsers) != 0 {
t.Errorf("Expected 0 users online within 30 minutes, got %d", len(recentUsers))
}
}
func TestBuilder(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Create new user using builder
testTime := time.Now()
user, err := NewBuilder(db).
WithUsername("testuser").
WithPassword("hashed_password").
WithEmail("test@example.com").
WithVerified(true).
WithAuth(2).
WithClassID(2).
WithPosition(50, -25).
WithLevel(3).
WithGold(250).
WithStats(7, 6, 8, 7).
WithHP(20, 20).
WithMP(10, 10).
WithTP(12, 12).
WithCurrently("Exploring").
WithRegisteredTime(testTime).
WithSpells([]string{"1", "3", "5"}).
WithTowns([]string{"1", "2"}).
Create()
if err != nil {
t.Fatalf("Failed to create user with builder: %v", err)
}
if user.ID == 0 {
t.Error("Expected non-zero ID after creation")
}
if user.Username != "testuser" {
t.Errorf("Expected username 'testuser', got '%s'", user.Username)
}
if user.Email != "test@example.com" {
t.Errorf("Expected email 'test@example.com', got '%s'", user.Email)
}
if user.Verified != 1 {
t.Errorf("Expected verified 1, got %d", user.Verified)
}
if user.Auth != 2 {
t.Errorf("Expected auth 2, got %d", user.Auth)
}
if user.ClassID != 2 {
t.Errorf("Expected class_id 2, got %d", user.ClassID)
}
if user.X != 50 || user.Y != -25 {
t.Errorf("Expected position (50, -25), got (%d, %d)", user.X, user.Y)
}
if user.Level != 3 {
t.Errorf("Expected level 3, got %d", user.Level)
}
if user.Gold != 250 {
t.Errorf("Expected gold 250, got %d", user.Gold)
}
if user.Registered != testTime.Unix() {
t.Errorf("Expected registered time %d, got %d", testTime.Unix(), user.Registered)
}
// Verify it was saved to database
foundUser, err := Find(db, user.ID)
if err != nil {
t.Fatalf("Failed to find created user: %v", err)
}
if foundUser.Username != "testuser" {
t.Errorf("Created user not found in database")
}
// Test builder with defaults
defaultUser, err := NewBuilder(db).
WithUsername("defaultuser").
WithPassword("password").
WithEmail("default@example.com").
Create()
if err != nil {
t.Fatalf("Failed to create user with defaults: %v", err)
}
// Should have default values
if defaultUser.Level != 1 {
t.Errorf("Expected default level 1, got %d", defaultUser.Level)
}
if defaultUser.Gold != 100 {
t.Errorf("Expected default gold 100, got %d", defaultUser.Gold)
}
if defaultUser.HP != 15 {
t.Errorf("Expected default HP 15, got %d", defaultUser.HP)
}
// Test convenience methods
adminUser, err := NewBuilder(db).
WithUsername("admin").
WithPassword("admin_pass").
WithEmail("admin@example.com").
AsAdmin().
Create()
if err != nil {
t.Fatalf("Failed to create admin user: %v", err)
}
if adminUser.Auth != 4 {
t.Errorf("Expected admin auth 4, got %d", adminUser.Auth)
}
moderatorUser, err := NewBuilder(db).
WithUsername("mod").
WithPassword("mod_pass").
WithEmail("mod@example.com").
AsModerator().
Create()
if err != nil {
t.Fatalf("Failed to create moderator user: %v", err)
}
if moderatorUser.Auth != 2 {
t.Errorf("Expected moderator auth 2, got %d", moderatorUser.Auth)
}
}
func TestSave(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
user, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find user: %v", err)
}
// Modify user
user.Username = "alice_updated"
user.Email = "alice_updated@example.com"
user.Level = 10
user.Gold = 1000
user.UpdateLastOnline()
// Save changes
err = user.Save()
if err != nil {
t.Fatalf("Failed to save user: %v", err)
}
// Verify changes were saved
updatedUser, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find updated user: %v", err)
}
if updatedUser.Username != "alice_updated" {
t.Errorf("Expected updated username 'alice_updated', got '%s'", updatedUser.Username)
}
if updatedUser.Email != "alice_updated@example.com" {
t.Errorf("Expected updated email, got '%s'", updatedUser.Email)
}
if updatedUser.Level != 10 {
t.Errorf("Expected updated level 10, got %d", updatedUser.Level)
}
if updatedUser.Gold != 1000 {
t.Errorf("Expected updated gold 1000, got %d", updatedUser.Gold)
}
}
func TestDelete(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
user, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find user: %v", err)
}
// Delete user
err = user.Delete()
if err != nil {
t.Fatalf("Failed to delete user: %v", err)
}
// Verify user was deleted
_, err = Find(db, 1)
if err == nil {
t.Error("Expected error when finding deleted user")
}
}
func TestUserMethods(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
alice, _ := Find(db, 1) // verified, auth 0
bob, _ := Find(db, 2) // verified, auth 2 (moderator)
charlie, _ := Find(db, 3) // unverified, auth 4 (admin)
// Test time methods
registeredTime := alice.RegisteredTime()
if registeredTime.IsZero() {
t.Error("Expected non-zero registered time")
}
lastOnlineTime := alice.LastOnlineTime()
if lastOnlineTime.IsZero() {
t.Error("Expected non-zero last online time")
}
// Test UpdateLastOnline
originalLastOnline := alice.LastOnline
alice.UpdateLastOnline()
if alice.LastOnline <= originalLastOnline {
t.Error("Expected last online to be updated to current time")
}
// Test verification status
if !alice.IsVerified() {
t.Error("Expected alice to be verified")
}
if charlie.IsVerified() {
t.Error("Expected charlie not to be verified")
}
// Test authorization levels
if alice.IsAdmin() {
t.Error("Expected alice not to be admin")
}
if alice.IsModerator() {
t.Error("Expected alice not to be moderator")
}
if !bob.IsModerator() {
t.Error("Expected bob to be moderator")
}
if !charlie.IsAdmin() {
t.Error("Expected charlie to be admin")
}
// Test combat status
if alice.IsFighting() {
t.Error("Expected alice not to be fighting")
}
if !alice.IsAlive() {
t.Error("Expected alice to be alive")
}
// Test position
x, y := alice.GetPosition()
if x != 10 || y != 20 {
t.Errorf("Expected position (10, 20), got (%d, %d)", x, y)
}
alice.SetPosition(30, 40)
x, y = alice.GetPosition()
if x != 30 || y != 40 {
t.Errorf("Expected updated position (30, 40), got (%d, %d)", x, y)
}
}
func TestSpellMethods(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
alice, _ := Find(db, 1) // spells: "1,2,5"
// Test GetSpellIDs
spells := alice.GetSpellIDs()
expectedSpells := []string{"1", "2", "5"}
if len(spells) != len(expectedSpells) {
t.Errorf("Expected %d spells, got %d", len(expectedSpells), len(spells))
}
for i, expected := range expectedSpells {
if i < len(spells) && spells[i] != expected {
t.Errorf("Expected spell '%s' at position %d, got '%s'", expected, i, spells[i])
}
}
// Test HasSpell
if !alice.HasSpell("1") {
t.Error("Expected alice to have spell '1'")
}
if !alice.HasSpell("2") {
t.Error("Expected alice to have spell '2'")
}
if alice.HasSpell("3") {
t.Error("Expected alice not to have spell '3'")
}
// Test SetSpellIDs
newSpells := []string{"3", "4", "6", "7"}
alice.SetSpellIDs(newSpells)
if alice.Spells != "3,4,6,7" {
t.Errorf("Expected spells '3,4,6,7', got '%s'", alice.Spells)
}
// Test with empty spells
charlie, _ := Find(db, 3) // empty spells
emptySpells := charlie.GetSpellIDs()
if len(emptySpells) != 0 {
t.Errorf("Expected 0 spells for empty list, got %d", len(emptySpells))
}
}
func TestTownMethods(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
alice, _ := Find(db, 1) // towns: "1,2"
// Test GetTownIDs
towns := alice.GetTownIDs()
expectedTowns := []string{"1", "2"}
if len(towns) != len(expectedTowns) {
t.Errorf("Expected %d towns, got %d", len(expectedTowns), len(towns))
}
for i, expected := range expectedTowns {
if i < len(towns) && towns[i] != expected {
t.Errorf("Expected town '%s' at position %d, got '%s'", expected, i, towns[i])
}
}
// Test HasVisitedTown
if !alice.HasVisitedTown("1") {
t.Error("Expected alice to have visited town '1'")
}
if !alice.HasVisitedTown("2") {
t.Error("Expected alice to have visited town '2'")
}
if alice.HasVisitedTown("3") {
t.Error("Expected alice not to have visited town '3'")
}
// Test SetTownIDs
newTowns := []string{"1", "2", "3", "4"}
alice.SetTownIDs(newTowns)
if alice.Towns != "1,2,3,4" {
t.Errorf("Expected towns '1,2,3,4', got '%s'", alice.Towns)
}
// Test with empty towns
charlie, _ := Find(db, 3) // empty towns
emptyTowns := charlie.GetTownIDs()
if len(emptyTowns) != 0 {
t.Errorf("Expected 0 towns for empty list, got %d", len(emptyTowns))
}
}
func TestGetEquipmentAndStats(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
alice, _ := Find(db, 1)
// Test GetEquipment
equipment := alice.GetEquipment()
if equipment == nil {
t.Error("Expected non-nil equipment map")
}
weapon, ok := equipment["weapon"].(map[string]interface{})
if !ok {
t.Error("Expected weapon to be a map")
}
if weapon["id"].(int) != alice.WeaponID {
t.Errorf("Expected weapon ID %d, got %v", alice.WeaponID, weapon["id"])
}
// Test GetStats
stats := alice.GetStats()
if stats == nil {
t.Error("Expected non-nil stats map")
}
if stats["level"] != alice.Level {
t.Errorf("Expected level %d, got %d", alice.Level, stats["level"])
}
if stats["hp"] != alice.HP {
t.Errorf("Expected HP %d, got %d", alice.HP, stats["hp"])
}
if stats["strength"] != alice.Strength {
t.Errorf("Expected strength %d, got %d", alice.Strength, stats["strength"])
}
}