396 lines
8.4 KiB
Go
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
|
|
}
|