390 lines
8.7 KiB
Go

package users
import (
"fmt"
"slices"
"sort"
"strings"
"time"
"dk/internal/helpers"
nigiri "git.sharkk.net/Sharkk/Nigiri"
)
// User represents a user in the game
type User struct {
ID int `json:"id"`
Username string `json:"username" db:"required,unique"`
Password string `json:"password" db:"required"`
Email string `json:"email" db:"required,unique"`
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"`
FightID int `json:"fight_id"`
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" db:"index"`
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"`
Spells string `json:"spells"`
Towns string `json:"towns"`
}
// Global store
var store *nigiri.BaseStore[User]
var db *nigiri.Collection
// Init sets up the Nigiri store and indices
func Init(collection *nigiri.Collection) {
db = collection
store = nigiri.NewBaseStore[User]()
// Register custom indices
store.RegisterIndex("byUsername", nigiri.BuildCaseInsensitiveLookupIndex(func(u *User) string {
return u.Username
}))
store.RegisterIndex("byEmail", nigiri.BuildStringLookupIndex(func(u *User) string {
return u.Email
}))
store.RegisterIndex("byLevel", nigiri.BuildIntGroupIndex(func(u *User) int {
return u.Level
}))
store.RegisterIndex("allByRegistered", nigiri.BuildSortedListIndex(func(a, b *User) bool {
if a.Registered != b.Registered {
return a.Registered > b.Registered // DESC
}
return a.ID > b.ID // DESC
}))
store.RegisterIndex("allByLevelExp", nigiri.BuildSortedListIndex(func(a, b *User) bool {
if a.Level != b.Level {
return a.Level > b.Level // Level DESC
}
if a.Exp != b.Exp {
return a.Exp > b.Exp // Exp DESC
}
return a.ID < b.ID // ID ASC
}))
store.RebuildIndices()
}
// GetStore returns the users store
func GetStore() *nigiri.BaseStore[User] {
if store == nil {
panic("users store not initialized - call Initialize first")
}
return store
}
// New creates a new User with sensible defaults
func New() *User {
now := time.Now().Unix()
return &User{
Verified: 0,
Token: "",
Registered: now,
LastOnline: now,
Auth: 0,
X: 0,
Y: 0,
ClassID: 1,
Currently: "In Town",
FightID: 0,
HP: 10,
MP: 10,
TP: 10,
MaxHP: 10,
MaxMP: 10,
MaxTP: 10,
Level: 1,
Gold: 100,
Exp: 0,
Strength: 0,
Dexterity: 0,
Attack: 0,
Defense: 0,
Spells: "",
Towns: "",
}
}
// Validate checks if user has valid values
func (u *User) Validate() error {
if strings.TrimSpace(u.Username) == "" {
return fmt.Errorf("user username cannot be empty")
}
if strings.TrimSpace(u.Email) == "" {
return fmt.Errorf("user email cannot be empty")
}
if u.Registered <= 0 {
return fmt.Errorf("user Registered timestamp must be positive")
}
if u.LastOnline <= 0 {
return fmt.Errorf("user LastOnline timestamp must be positive")
}
if u.Level < 1 {
return fmt.Errorf("user Level must be at least 1")
}
if u.HP < 0 {
return fmt.Errorf("user HP cannot be negative")
}
if u.MaxHP < 1 {
return fmt.Errorf("user MaxHP must be at least 1")
}
return nil
}
// CRUD operations
func (u *User) Save() error {
if u.ID == 0 {
id, err := store.Create(u)
if err != nil {
return err
}
u.ID = id
return nil
}
return store.Update(u.ID, u)
}
func (u *User) Delete() error {
store.Remove(u.ID)
return nil
}
// Insert with ID assignment
func (u *User) Insert() error {
id, err := store.Create(u)
if err != nil {
return err
}
u.ID = id
return nil
}
// Query functions
func Find(id int) (*User, error) {
user, exists := store.Find(id)
if !exists {
return nil, fmt.Errorf("user with ID %d not found", id)
}
return user, nil
}
func GetByID(id int) *User {
user, exists := store.Find(id)
if !exists {
return nil
}
return user
}
func All() ([]*User, error) {
return store.AllSorted("allByRegistered"), nil
}
func ByUsername(username string) (*User, error) {
user, exists := store.LookupByIndex("byUsername", strings.ToLower(username))
if !exists {
return nil, fmt.Errorf("user with username '%s' not found", username)
}
return user, nil
}
func ByEmail(email string) (*User, error) {
user, exists := store.LookupByIndex("Email_idx", email)
if !exists {
return nil, fmt.Errorf("user with email '%s' not found", email)
}
return user, nil
}
func ByLevel(level int) ([]*User, error) {
return store.GroupByIndex("level_idx", level), nil
}
func Online(within time.Duration) ([]*User, error) {
cutoff := time.Now().Add(-within).Unix()
result := store.FilterByIndex("allByRegistered", func(u *User) bool {
return u.LastOnline >= cutoff
})
// Sort by last_online DESC, then ID ASC
sort.Slice(result, func(i, j int) bool {
if result[i].LastOnline != result[j].LastOnline {
return result[i].LastOnline > result[j].LastOnline // DESC
}
return result[i].ID < result[j].ID // ASC
})
return result, nil
}
// Helper methods
func (u *User) RegisteredTime() time.Time {
return time.Unix(u.Registered, 0)
}
func (u *User) LastOnlineTime() time.Time {
return time.Unix(u.LastOnline, 0)
}
func (u *User) UpdateLastOnline() {
u.LastOnline = time.Now().Unix()
}
func (u *User) IsVerified() bool {
return u.Verified == 1
}
func (u *User) IsAdmin() bool {
return u.Auth >= 4
}
func (u *User) IsModerator() bool {
return u.Auth >= 3
}
func (u *User) IsFighting() bool {
return u.FightID > 0
}
func (u *User) IsAlive() bool {
return u.HP > 0
}
func (u *User) GetSpellIDs() []int {
return helpers.StringToInts(u.Spells)
}
func (u *User) SetSpellIDs(spells []int) {
u.Spells = helpers.IntsToString(spells)
}
func (u *User) HasSpell(spellID int) bool {
return slices.Contains(u.GetSpellIDs(), spellID)
}
func (u *User) GetTownIDs() []int {
return helpers.StringToInts(u.Towns)
}
func (u *User) SetTownIDs(towns []int) {
u.Towns = helpers.IntsToString(towns)
}
func (u *User) HasTownMap(townID int) bool {
return slices.Contains(u.GetTownIDs(), townID)
}
func (u *User) GetEquipment() map[string]any {
return map[string]any{
"weapon": map[string]any{"id": u.WeaponID, "name": u.WeaponName},
"armor": map[string]any{"id": u.ArmorID, "name": u.ArmorName},
"shield": map[string]any{"id": u.ShieldID, "name": u.ShieldName},
"slot1": map[string]any{"id": u.Slot1ID, "name": u.Slot1Name},
"slot2": map[string]any{"id": u.Slot2ID, "name": u.Slot2Name},
"slot3": map[string]any{"id": u.Slot3ID, "name": u.Slot3Name},
}
}
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,
}
}
func (u *User) GetPosition() (int, int) {
return u.X, u.Y
}
func (u *User) SetPosition(x, y int) {
u.X = x
u.Y = y
}
func (u *User) ExpNeededForNextLevel() int {
return u.Level * u.Level * u.Level
}
func (u *User) GrantExp(expAmount int) {
u.Exp += expAmount
u.checkLevelUp()
}
func (u *User) checkLevelUp() {
expNeeded := u.ExpNeededForNextLevel()
if u.Exp >= expNeeded {
// Level up
u.Level++
u.Strength++
u.Dexterity++
// Reset exp and carry over excess
excessExp := u.Exp - expNeeded
u.Exp = 0
// Recursive level up if enough excess exp
if excessExp > 0 {
u.GrantExp(excessExp)
}
}
}
func (u *User) ExpProgress() float64 {
if u.Level == 1 {
return float64(u.Exp) / float64(u.ExpNeededForNextLevel()) * 100
}
currentLevelExp := u.Level * u.Level * u.Level
nextLevelExp := u.ExpNeededForNextLevel()
progressExp := u.Exp
return float64(progressExp) / float64(nextLevelExp-currentLevelExp) * 100
}