package users import ( "fmt" "slices" "strings" "time" "dk/internal/database" "dk/internal/helpers" "dk/internal/models/classes" "dk/internal/models/spells" "dk/internal/models/towns" ) // User represents a user in the game type User struct { ID int Username string Password string Email string Verified int Token string Registered int64 LastOnline int64 Auth int X int Y int ClassID int Currently string FightID int HP int MP int TP int MaxHP int MaxMP int MaxTP int Level int Gold int Exp int GoldBonus int ExpBonus int Strength int Dexterity int Attack int Defense int WeaponID int ArmorID int ShieldID int Slot1ID int Slot2ID int Slot3ID int WeaponName string ArmorName string ShieldName string Slot1Name string Slot2Name string Slot3Name string Spells string Towns string } // 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 } func (u *User) Delete() error { return database.Exec("DELETE FROM users WHERE id = %d", u.ID) } func (u *User) Insert() error { id, err := database.Insert("users", u, "id") if err != nil { return err } u.ID = int(id) return nil } func Find(id int) (*User, error) { var user User err := database.Get(&user, "SELECT * FROM users WHERE id = %d", id) if err != nil { return nil, fmt.Errorf("user with ID %d not found", id) } return &user, nil } func All() ([]*User, error) { var users []*User err := database.Select(&users, "SELECT * FROM users ORDER BY registered DESC, id DESC") return users, err } func ByUsername(username string) (*User, error) { var user User err := database.Get(&user, "SELECT * FROM users WHERE username = %s COLLATE NOCASE", username) if err != nil { return nil, fmt.Errorf("user with username '%s' not found", username) } return &user, nil } func ByEmail(email string) (*User, error) { var user User err := database.Get(&user, "SELECT * FROM users WHERE email = %s", email) if err != nil { return nil, fmt.Errorf("user with email '%s' not found", email) } return &user, nil } func ByLevel(level int) ([]*User, error) { var users []*User err := database.Select(&users, "SELECT * FROM users WHERE level = %d ORDER BY exp DESC, id ASC", level) return users, err } func Online(within time.Duration) ([]*User, error) { cutoff := time.Now().Add(-within).Unix() var users []*User err := database.Select(&users, "SELECT * FROM users WHERE last_online >= %d ORDER BY last_online DESC, id ASC", cutoff) return users, err } 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) GetSpells() ([]*spells.Spell, error) { return spells.UserSpells(u.ID) } func (u *User) GetHealingSpells() ([]*spells.Spell, error) { return spells.UserHealingSpells(u.ID) } func (u *User) HasSpell(spellID int) bool { return spells.HasSpell(u.ID, spellID) } func (u *User) GrantSpell(spellID int) error { return spells.GrantSpell(u.ID, spellID) } func (u *User) GrantSpells(spellIDs []int) error { return spells.GrantSpells(u.ID, spellIDs) } func (u *User) LearnNewSpells() error { newSpells, err := spells.UnlocksForClassAtLevel(u.ClassID, u.Level) if err != nil { return err } var spellIDs []int for _, spell := range newSpells { spellIDs = append(spellIDs, spell.ID) } return u.GrantSpells(spellIDs) } 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 helpers.ExpAtLevel(u.Level + 1) } func (u *User) GrantExp(expAmount int) map[string]any { oldLevel := u.Level newLevel, newStr, newDex, newExp := u.CalculateLevelUp(expAmount) updates := map[string]any{ "exp": newExp, } // Only include level/stats if they actually changed if newLevel > oldLevel { updates["level"] = newLevel updates["strength"] = newStr updates["dexterity"] = newDex // Learn new spells for each level gained for level := oldLevel + 1; level <= newLevel; level++ { if err := u.learnSpellsForLevel(level); err != nil { // Don't fail the level up if spells fail fmt.Printf("Failed to grant spells for level %d to user %d: %v\n", level, u.ID, err) } } } return updates } func (u *User) CalculateLevelUp(expGain int) (newLevel, newStr, newDex, newExp int) { level := u.Level str := u.Strength dex := u.Dexterity totalExp := u.Exp + expGain for { expNeeded := helpers.ExpAtLevel(level + 1) if totalExp < expNeeded { break } level++ str++ dex++ totalExp -= expNeeded } return level, str, dex, totalExp } func (u *User) ExpProgress() float64 { if u.Level == 1 { return float64(u.Exp) / float64(u.ExpNeededForNextLevel()) * 100 } currentLevelExp := helpers.ExpAtLevel(u.Level) nextLevelExp := u.ExpNeededForNextLevel() progressExp := u.Exp return float64(progressExp) / float64(nextLevelExp-currentLevelExp) * 100 } func (u *User) Class() *classes.Class { class, err := classes.Find(u.ClassID) if err != nil { class, err = classes.Find(1) if err != nil { panic("There should always be at least one class") } return class } return class } func (u *User) learnSpellsForLevel(level int) error { newSpells, err := spells.UnlocksForClassAtLevel(u.ClassID, level) if err != nil { return err } var spellIDs []int for _, spell := range newSpells { spellIDs = append(spellIDs, spell.ID) } return u.GrantSpells(spellIDs) } func (u *User) GetKnownTowns() ([]*towns.Town, error) { townIDs := u.GetTownIDs() result := make([]*towns.Town, 0, len(townIDs)) for _, townID := range townIDs { town, err := towns.Find(townID) if err != nil { // Skip invalid town IDs rather than failing entirely continue } result = append(result, town) } return result, nil }