diff --git a/assets/dk.css b/assets/dk.css index f5b4c2f..ed8341d 100644 --- a/assets/dk.css +++ b/assets/dk.css @@ -112,6 +112,7 @@ div.title { background-color: #eeeeee; font-weight: bold; padding: 5px; + margin-bottom: 0.1rem; } footer { @@ -263,4 +264,50 @@ button.img-button { outline: none; background: none; cursor: pointer; +} + +div#statbars { + display: flex; + justify-content: space-around; + margin: 1rem 0; + + & > div.stat { + display: flex; + flex-direction: column; + align-items: center; + + & > div.container { + display: flex; + align-items: flex-end; + background-color: rgba(0, 0, 0, 0.6); + width: 16px; + height: 160px; + border: 1px solid rgba(0, 0, 0, 0.6); + + & > div.bar { + width: 100%; + height: 100%; + background-color: white; + } + + &#hp > div.bar { + background: #56ab2f; + background: linear-gradient(to left, #a8e063, #56ab2f); + } + + &#mp > div.bar { + background: #00c6ff; + background: linear-gradient(to right, #0072ff, #00c6ff); + } + + &#tp > div.bar { + background: #757f9a; + background: linear-gradient(to right, #757f9a, #d7dde8); + } + } + + & > label { + + } + } } \ No newline at end of file diff --git a/internal/auth/auth.go b/internal/auth/auth.go index b277f57..5cd9524 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -25,9 +25,9 @@ func (am *AuthManager) Authenticate(usernameOrEmail, plainPassword string) (*use var user *users.User var err error - user, err = users.GetByUsername(usernameOrEmail) + user, err = users.ByUsername(usernameOrEmail) if err != nil { - user, err = users.GetByEmail(usernameOrEmail) + user, err = users.ByEmail(usernameOrEmail) if err != nil { return nil, err } diff --git a/internal/database/model.go b/internal/database/model.go new file mode 100644 index 0000000..692fae1 --- /dev/null +++ b/internal/database/model.go @@ -0,0 +1,122 @@ +package database + +import ( + "fmt" + "reflect" + "strings" + + "zombiezen.com/go/sqlite" +) + +// Model interface for trackable database models +type Model interface { + GetTableName() string + GetID() int + SetID(id int) + GetDirtyFields() map[string]any + SetDirty(field string, value any) + ClearDirty() + IsDirty() bool +} + +// BaseModel provides common model functionality +type BaseModel struct { + FieldTracker +} + +// Set uses reflection to set a field and track changes +func Set(model Model, field string, value any) error { + v := reflect.ValueOf(model).Elem() + fieldVal := v.FieldByName(field) + + if !fieldVal.IsValid() { + return fmt.Errorf("field %s does not exist", field) + } + + if !fieldVal.CanSet() { + return fmt.Errorf("field %s cannot be set", field) + } + + // Get current value for comparison + currentVal := fieldVal.Interface() + + // Only set if value has changed + if !reflect.DeepEqual(currentVal, value) { + // Convert value to correct type + newVal := reflect.ValueOf(value) + if newVal.Type().ConvertibleTo(fieldVal.Type()) { + fieldVal.Set(newVal.Convert(fieldVal.Type())) + + // Convert field name to snake_case for database + dbField := toSnakeCase(field) + model.SetDirty(dbField, value) + } else { + return fmt.Errorf("cannot convert %T to %s", value, fieldVal.Type()) + } + } + + return nil +} + +// toSnakeCase converts CamelCase to snake_case +func toSnakeCase(s string) string { + var result strings.Builder + for i, r := range s { + if i > 0 && r >= 'A' && r <= 'Z' { + result.WriteByte('_') + } + if r >= 'A' && r <= 'Z' { + result.WriteRune(r - 'A' + 'a') + } else { + result.WriteRune(r) + } + } + return result.String() +} + +// Save updates only dirty fields +func Save(model Model) error { + if model.GetID() == 0 { + return fmt.Errorf("cannot save model without ID") + } + return UpdateDirty(model) +} + +// Insert creates a new record and sets the ID +func Insert(model Model, columns string, values ...any) error { + if model.GetID() != 0 { + return fmt.Errorf("model already has ID %d, use Save() to update", model.GetID()) + } + + return Transaction(func(tx *Tx) error { + placeholders := strings.Repeat("?,", len(values)) + placeholders = placeholders[:len(placeholders)-1] // Remove trailing comma + + query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", + model.GetTableName(), columns, placeholders) + + if err := tx.Exec(query, values...); err != nil { + return fmt.Errorf("failed to insert: %w", err) + } + + 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) + } + + model.SetID(id) + return nil + }) +} + +// Delete removes the record +func Delete(model Model) error { + if model.GetID() == 0 { + return fmt.Errorf("cannot delete model without ID") + } + return Exec("DELETE FROM ? WHERE id = ?", model.GetTableName(), model.GetID()) +} diff --git a/internal/database/trackable.go b/internal/database/trackable.go new file mode 100644 index 0000000..9c2f2e9 --- /dev/null +++ b/internal/database/trackable.go @@ -0,0 +1,82 @@ +package database + +import ( + "fmt" + "strings" +) + +// Trackable interface for models that can track field changes +type Trackable interface { + GetTableName() string + GetID() int + GetDirtyFields() map[string]any + SetDirty(field string, value any) + ClearDirty() + IsDirty() bool +} + +// FieldTracker provides dirty field tracking functionality +type FieldTracker struct { + dirty map[string]any +} + +// SetDirty marks a field as dirty with its new value +func (ft *FieldTracker) SetDirty(field string, value any) { + if ft.dirty == nil { + ft.dirty = make(map[string]any) + } + ft.dirty[field] = value +} + +// GetDirtyFields returns map of dirty fields and their values +func (ft *FieldTracker) GetDirtyFields() map[string]any { + if ft.dirty == nil { + return make(map[string]any) + } + return ft.dirty +} + +// ClearDirty clears all dirty field tracking +func (ft *FieldTracker) ClearDirty() { + ft.dirty = nil +} + +// IsDirty returns true if any fields have been modified +func (ft *FieldTracker) IsDirty() bool { + return len(ft.dirty) > 0 +} + +// UpdateDirty updates only dirty fields in the database +func UpdateDirty(model Trackable) error { + if !model.IsDirty() { + return nil // No changes to save + } + + dirty := model.GetDirtyFields() + if len(dirty) == 0 { + return nil + } + + // Build dynamic UPDATE query + var setParts []string + var args []any + + for field, value := range dirty { + setParts = append(setParts, field+" = ?") + args = append(args, value) + } + + args = append(args, model.GetID()) // Add ID for WHERE clause + + query := fmt.Sprintf("UPDATE %s SET %s WHERE id = ?", + model.GetTableName(), + strings.Join(setParts, ", ")) + + err := Exec(query, args...) + if err != nil { + return fmt.Errorf("failed to update %s: %w", model.GetTableName(), err) + } + + model.ClearDirty() + return nil +} diff --git a/internal/helpers/math.go b/internal/helpers/math.go new file mode 100644 index 0000000..564cc9a --- /dev/null +++ b/internal/helpers/math.go @@ -0,0 +1,10 @@ +package helpers + +// ClampPct divides two floats and clamps them to the given limits. Provides +// divide-by-zero protection by just returning 0. +func ClampPct(num, denom, minimum, maximum float64) float64 { + if denom == 0 { + return 0 + } + return max(minimum, min(maximum, (num/denom)*100)) +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index b3907c2..80cd78c 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -46,6 +46,10 @@ func RequireAuth(paths ...string) router.Middleware { return } + user := ctx.UserValue("user").(*users.User) + user.UpdateLastOnline() + user.Save() + next(ctx, params) } } @@ -105,4 +109,4 @@ func Logout(ctx router.Ctx, authManager *auth.AuthManager) { ctx.SetUserValue("session", nil) ctx.SetUserValue("user", nil) -} \ No newline at end of file +} diff --git a/internal/routes/auth.go b/internal/routes/auth.go index aca1fb1..80a8384 100644 --- a/internal/routes/auth.go +++ b/internal/routes/auth.go @@ -143,7 +143,7 @@ func processRegister(ctx router.Ctx, _ []string) { return } - if _, err := users.GetByUsername(username); err == nil { + if _, err := users.ByUsername(username); err == nil { auth.SetFlashMessage(ctx, "error", "Username already exists") auth.SetFormData(ctx, map[string]string{ "username": username, @@ -153,7 +153,7 @@ func processRegister(ctx router.Ctx, _ []string) { return } - if _, err := users.GetByEmail(email); err == nil { + if _, err := users.ByEmail(email); err == nil { auth.SetFlashMessage(ctx, "error", "Email already registered") auth.SetFormData(ctx, map[string]string{ "username": username, diff --git a/internal/template/components/asides.go b/internal/template/components/asides.go index f7e3f60..b1cb18c 100644 --- a/internal/template/components/asides.go +++ b/internal/template/components/asides.go @@ -1,6 +1,7 @@ package components import ( + "dk/internal/helpers" "dk/internal/middleware" "dk/internal/router" "dk/internal/template" @@ -61,31 +62,24 @@ func LeftAside(ctx router.Ctx) string { // RightAside generates the right sidebar content func RightAside(ctx router.Ctx) string { - if !middleware.IsAuthenticated(ctx) { + user := middleware.GetCurrentUser(ctx) + if user == nil { return "" } - // Load and render the rightside template with user data - rightSideTmpl, err := template.Cache.Load("rightside.html") - if err != nil { - return "" // Silently fail - sidebar is optional - } - - // Get the current user from session - currentUser := middleware.GetCurrentUser(ctx) - if currentUser == nil { - return "" - } - - user, err := users.Find(currentUser.ID) + tmpl, err := template.Cache.Load("rightside.html") if err != nil { return "" } - // Pass the user object directly to the template - rightSideData := map[string]any{ - "user": user.ToMap(), - } + hpPct := helpers.ClampPct(float64(user.HP), float64(user.MaxHP), 0, 100) + mpPct := helpers.ClampPct(float64(user.MP), float64(user.MaxMP), 0, 100) + tpPct := helpers.ClampPct(float64(user.TP), float64(user.MaxTP), 0, 100) - return rightSideTmpl.RenderNamed(rightSideData) + return tmpl.RenderNamed(map[string]any{ + "user": user.ToMap(), + "hppct": hpPct, + "mppct": mpPct, + "tppct": tpPct, + }) } diff --git a/internal/template/components/town.go b/internal/template/components/town.go index 24ca39e..199caf4 100644 --- a/internal/template/components/town.go +++ b/internal/template/components/town.go @@ -3,7 +3,9 @@ package components import ( "dk/internal/helpers/markdown" "dk/internal/news" + "dk/internal/users" "fmt" + "time" ) func GenerateTownNews() string { @@ -22,5 +24,19 @@ func GenerateTownNews() string { func GenerateTownWhosOnline() string { title := `
Who's Online
` + onlineUsers, err := users.Online(10 * time.Minute) + if err == nil && len(onlineUsers) > 0 { + if len(onlineUsers) == 1 { + title += "

There is 1 user online in the last 10 minutes:

" + } else { + title += fmt.Sprintf("

There are %d users online in the last 10 minutes:

", len(onlineUsers)) + } + + for _, user := range onlineUsers { + title += fmt.Sprintf(`
%s
`, user.Username) + } + return title + } + return title + "
No one!
" } diff --git a/internal/users/users.go b/internal/users/users.go index a3a42ab..dc78107 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -13,6 +13,8 @@ import ( // User represents a user in the database type User struct { + database.BaseModel + ID int `db:"id" json:"id"` Username string `db:"username" json:"username"` Password string `db:"password" json:"password"` @@ -65,296 +67,221 @@ type User struct { Towns string `db:"towns" json:"towns"` } +// Implement Model interface +func (u *User) GetTableName() string { + return "users" +} + +func (u *User) GetID() int { + return u.ID +} + +func (u *User) SetID(id int) { + u.ID = id +} + +// Convenience methods wrapping generic functions +func (u *User) Set(field string, value any) error { + return database.Set(u, field, value) +} + +func (u *User) Save() error { + return database.Save(u) +} + +func (u *User) Delete() error { + return database.Delete(u) +} + // New creates a new User with sensible defaults func New() *User { now := time.Now().Unix() return &User{ - 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 + Verified: 0, + Token: "", + Registered: now, + LastOnline: now, + Auth: 0, + X: 0, + Y: 0, + ClassID: 1, + Currently: "In Town", + Fighting: 0, + HP: 15, + MP: 0, + TP: 10, + MaxHP: 15, + MaxMP: 0, + MaxTP: 10, + Level: 1, + Gold: 100, + Exp: 0, + Strength: 5, + Dexterity: 5, + Attack: 5, + Defense: 5, + Spells: "", + Towns: "", } } var userScanner = scanner.New[User]() -// userColumns returns the column list for user queries func userColumns() string { return userScanner.Columns() } -// scanUser populates a User struct using the fast scanner func scanUser(stmt *sqlite.Stmt) *User { user := &User{} userScanner.Scan(stmt, user) return user } -// Find retrieves a user by ID +// Query functions func Find(id int) (*User, error) { var user *User - query := `SELECT ` + userColumns() + ` FROM users WHERE id = ?` - err := database.Query(query, func(stmt *sqlite.Stmt) error { user = scanUser(stmt) 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() ([]*User, error) { var users []*User - query := `SELECT ` + userColumns() + ` FROM users ORDER BY registered DESC, id DESC` - err := database.Query(query, func(stmt *sqlite.Stmt) error { user := scanUser(stmt) 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(username string) (*User, error) { var user *User - query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(username) = LOWER(?) LIMIT 1` - err := database.Query(query, func(stmt *sqlite.Stmt) error { user = scanUser(stmt) 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(email string) (*User, error) { var user *User - query := `SELECT ` + userColumns() + ` FROM users WHERE email = ? LIMIT 1` - err := database.Query(query, func(stmt *sqlite.Stmt) error { user = scanUser(stmt) 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(level int) ([]*User, error) { var users []*User - query := `SELECT ` + userColumns() + ` FROM users WHERE level = ? ORDER BY exp DESC, id ASC` - err := database.Query(query, func(stmt *sqlite.Stmt) error { user := scanUser(stmt) 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(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 := database.Query(query, func(stmt *sqlite.Stmt) error { user := scanUser(stmt) 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") - } +func (u *User) Insert() error { + columns := `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` - 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 database.Exec(query, u.Username, u.Password, u.Email, u.Verified, u.Token, + values := []any{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) + u.Slot1Name, u.Slot2Name, u.Slot3Name, u.DropCode, u.Spells, u.Towns} + + return database.Insert(u, columns, values...) } -// Insert saves a new user to the database and sets the ID -func (u *User) Insert() error { - if u.ID != 0 { - return fmt.Errorf("user already has ID %d, use Save() to update", u.ID) - } - - // Use a transaction to ensure we can get the ID - err := database.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, 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); 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) - } - - u.ID = id - return nil - }) - - return err -} - -// Delete removes the user from the database -func (u *User) Delete() error { - if u.ID == 0 { - return fmt.Errorf("cannot delete user without ID") - } - - return database.Exec("DELETE FROM users WHERE id = ?", u.ID) -} - -// RegisteredTime returns the registration timestamp as a time.Time +// Helper methods 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() + u.Set("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{} @@ -362,12 +289,10 @@ func (u *User) GetSpellIDs() []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, ",") + u.Set("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 { @@ -378,7 +303,6 @@ func (u *User) HasSpell(spellID string) bool { return false } -// GetTownIDs returns town IDs as a slice of strings func (u *User) GetTownIDs() []string { if u.Towns == "" { return []string{} @@ -386,12 +310,10 @@ func (u *User) GetTownIDs() []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, ",") + u.Set("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 { @@ -402,7 +324,6 @@ func (u *User) HasVisitedTown(townID string) bool { return false } -// GetEquipment returns all equipped item information func (u *User) GetEquipment() map[string]any { return map[string]any{ "weapon": map[string]any{"id": u.WeaponID, "name": u.WeaponName}, @@ -414,7 +335,6 @@ func (u *User) GetEquipment() map[string]any { } } -// GetStats returns combat-relevant stats func (u *User) GetStats() map[string]int { return map[string]int{ "level": u.Level, @@ -433,59 +353,13 @@ func (u *User) GetStats() map[string]int { } } -// 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 -} - -// GetByUsername retrieves a user by username -func GetByUsername(username string) (*User, error) { - var user *User - - query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(username) = LOWER(?) LIMIT 1` - - err := database.Query(query, func(stmt *sqlite.Stmt) error { - user = scanUser(stmt) - return nil - }, username) - - if err != nil { - return nil, fmt.Errorf("query failed: %w", err) - } - - if user == nil { - return nil, fmt.Errorf("user not found: %s", username) - } - - return user, nil -} - -// GetByEmail retrieves a user by email -func GetByEmail(email string) (*User, error) { - var user *User - - query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1` - - err := database.Query(query, func(stmt *sqlite.Stmt) error { - user = scanUser(stmt) - return nil - }, email) - - if err != nil { - return nil, fmt.Errorf("query failed: %w", err) - } - - if user == nil { - return nil, fmt.Errorf("user not found: %s", email) - } - - return user, nil + u.Set("X", x) + u.Set("Y", y) } // ToMap converts the user to a map for efficient template rendering diff --git a/templates/rightside.html b/templates/rightside.html index 212a0ed..fd4d75a 100644 --- a/templates/rightside.html +++ b/templates/rightside.html @@ -1,58 +1,57 @@ - - - - - - - -
Character
- {user.Username}
- Level: {user.Level}
- Exp: {user.Exp}
- Gold: {user.Gold}
- HP: {user.HP}
- MP: {user.MP}
- TP: {user.TP}
- {statbars}
- Extended Stats -
+
+
Character
+ +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+ + Extended Stats +

- - - - - - - -
Inventory
- - - - - - - - - - - - - -
Weapon{weaponname}
Armor{armorname}
Shield{shieldname}
- {slot1name}
- {slot2name}
- {slot3name} -
+
+
Inventory
+
+ Weapon + {weaponname} +
+
+ Armor + {armorname} +
+
+ Shield + {shieldname} +
+ {slot1name} + {slot2name} + {slot3name} +
-
- - - - - - - - -
Fast Spells
{magiclist}
+
+
Fast Spells
+ {magiclist} +
\ No newline at end of file