396 lines
8.4 KiB
Go

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
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,
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
}