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 }