create users package
This commit is contained in:
parent
96857e8110
commit
c7d08d8004
235
internal/users/builder.go
Normal file
235
internal/users/builder.go
Normal 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
500
internal/users/doc.go
Normal 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
424
internal/users/users.go
Normal 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
|
||||||
|
}
|
670
internal/users/users_test.go
Normal file
670
internal/users/users_test.go
Normal 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"])
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user