diff options
| author | historia <[not public]> | 2026-06-09 05:59:20 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-09 05:59:20 -0400 |
| commit | 9eb5ab1818b6b19501fd7209db1987c9e06cc919 (patch) | |
| tree | 76e046b0bf611dfe939a8c8f6507467de2e9e20f /internal/player | |
| download | thehouseoficarus-9eb5ab1818b6b19501fd7209db1987c9e06cc919.tar.gz | |
first commit
Diffstat (limited to 'internal/player')
| -rw-r--r-- | internal/player/account.go | 7 | ||||
| -rw-r--r-- | internal/player/password.go | 32 | ||||
| -rw-r--r-- | internal/player/player.go | 147 | ||||
| -rw-r--r-- | internal/player/store.go | 136 | ||||
| -rw-r--r-- | internal/player/xp.go | 41 |
5 files changed, 363 insertions, 0 deletions
diff --git a/internal/player/account.go b/internal/player/account.go new file mode 100644 index 0000000..5072009 --- /dev/null +++ b/internal/player/account.go @@ -0,0 +1,7 @@ +package player + +type Account struct { + Name string `yaml:"name"` + PasswordHash string `yaml:"password_hash"` + Characters []string `yaml:"characters"` +} diff --git a/internal/player/password.go b/internal/player/password.go new file mode 100644 index 0000000..17e4218 --- /dev/null +++ b/internal/player/password.go @@ -0,0 +1,32 @@ +package player + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "strings" +) + +func HashPassword(password string) (string, error) { + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + return "", err + } + hash := sha256.Sum256(append(salt, []byte(password)...)) + return fmt.Sprintf("sha256:%s:%x", base64.RawStdEncoding.EncodeToString(salt), hash), nil +} + +func CheckPassword(password, stored string) bool { + parts := strings.SplitN(stored, ":", 3) + if len(parts) != 3 || parts[0] != "sha256" { + return false + } + salt, err := base64.RawStdEncoding.DecodeString(parts[1]) + if err != nil { + return false + } + hash := sha256.Sum256(append(salt, []byte(password)...)) + expected := fmt.Sprintf("%x", hash) + return parts[2] == expected +} diff --git a/internal/player/player.go b/internal/player/player.go new file mode 100644 index 0000000..33689c5 --- /dev/null +++ b/internal/player/player.go @@ -0,0 +1,147 @@ +package player + +import "thirdcollapse/internal/object" + +type SkillName string + +const ( + Attack SkillName = "attack" + Strength SkillName = "strength" + Defense SkillName = "defense" + Hitpoints SkillName = "hitpoints" + Ranged SkillName = "ranged" + Science SkillName = "science" + Technology SkillName = "technology" + Fishing SkillName = "fishing" + Cooking SkillName = "cooking" + Woodcutting SkillName = "woodcutting" + Mining SkillName = "mining" + Smithing SkillName = "smithing" + Crafting SkillName = "crafting" + Fletching SkillName = "fletching" + Alchemy SkillName = "alchemy" + Thieving SkillName = "thieving" + Agility SkillName = "agility" + Construction SkillName = "construction" + Scavenging SkillName = "scavenging" + Hunter SkillName = "hunter" +) + +var AllSkills = []SkillName{ + Attack, Strength, Defense, Hitpoints, Ranged, Science, Technology, + Fishing, Cooking, Woodcutting, Mining, Smithing, Crafting, Fletching, + Alchemy, Thieving, Agility, Construction, Scavenging, Hunter, +} + +type AttackStyle string + +const ( + Accurate AttackStyle = "accurate" + Aggressive AttackStyle = "aggressive" + Defensive AttackStyle = "defensive" + Balanced AttackStyle = "balanced" +) + +type Skill struct { + Name SkillName + XP int +} + +func (s Skill) Level() int { + return LevelForXP(s.XP) +} + +type InventorySlot struct { + ItemID string `yaml:"item_id"` + Quantity int `yaml:"quantity"` +} + +type Player struct { + Name string `yaml:"name"` + Skills map[SkillName]int `yaml:"skills"` // xp + Inventory map[int]*InventorySlot `yaml:"inventory"` // slot 0-27 -> item + Equipment map[object.EquipSlot]string `yaml:"equipment"` // slot -> item_id + Toolbelt []string `yaml:"toolbelt"` // item_ids + RoomID int `yaml:"room_id"` + HP int `yaml:"hp"` + Credits int `yaml:"credits"` + AttackStyle AttackStyle `yaml:"attack_style"` +} + +func (p *Player) InvSlot(i int) *InventorySlot { + if p.Inventory == nil { + return nil + } + return p.Inventory[i] +} + +func (p *Player) SetInvSlot(i int, slot *InventorySlot) { + if p.Inventory == nil { + p.Inventory = make(map[int]*InventorySlot) + } + if slot == nil { + delete(p.Inventory, i) + } else { + p.Inventory[i] = slot + } +} + +func (p *Player) FirstFreeSlot() int { + for i := 0; i < 28; i++ { + if p.Inventory[i] == nil { + return i + } + } + return -1 +} + +func (p *Player) FreeSlots() int { + used := len(p.Inventory) + return 28 - used +} + +func New(name string) *Player { + p := &Player{ + Name: name, + Skills: make(map[SkillName]int), + Equipment: make(map[object.EquipSlot]string), + AttackStyle: Accurate, + RoomID: 0, + } + for _, s := range AllSkills { + p.Skills[s] = 0 + } + p.Skills[Hitpoints] = XPForLevel(10) + p.HP = p.MaxHP() + return p +} + +func (p *Player) Level(s SkillName) int { + return LevelForXP(p.Skills[s]) +} + +func (p *Player) AddXP(s SkillName, amount int) { + if p.Skills == nil { + p.Skills = make(map[SkillName]int) + } + p.Skills[s] += amount +} + +func (p *Player) CombatLevel() int { + base := 0.25 * float64(p.Level(Defense)+p.Level(Hitpoints)+p.Level(Science)) + att := float64(p.Level(Attack)) + str := float64(p.Level(Strength)) + + if float64(p.Level(Ranged))*1.5 > att+str { + base += 0.375 * float64(p.Level(Ranged)) + } else { + base += 0.25 * (att + str) + } + + base += 0.125 * float64(p.Level(Technology)) + return int(base) +} + +func (p *Player) MaxHP() int { + return p.Level(Hitpoints) +} diff --git a/internal/player/store.go b/internal/player/store.go new file mode 100644 index 0000000..c19d9fb --- /dev/null +++ b/internal/player/store.go @@ -0,0 +1,136 @@ +package player + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "thirdcollapse/internal/object" + "gopkg.in/yaml.v3" +) + +type AccountStore struct { + dataDir string +} + +func NewAccountStore(dataDir string) *AccountStore { + return &AccountStore{dataDir: dataDir} +} + +func (s *AccountStore) AccountPath(name string) string { + return filepath.Join(s.dataDir, "players", "accounts", name+".yaml") +} + +func (s *AccountStore) LoadAccount(name string) (*Account, error) { + path := s.AccountPath(name) + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("account not found: %s", name) + } + var acc Account + if err := yaml.Unmarshal(data, &acc); err != nil { + return nil, fmt.Errorf("parse account: %w", err) + } + return &acc, nil +} + +func (s *AccountStore) SaveAccount(acc *Account) error { + path := s.AccountPath(acc.Name) + data, err := yaml.Marshal(acc) + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} + +func (s *AccountStore) AccountExists(name string) bool { + _, err := s.FindAccount(name) + return err == nil +} + +func (s *AccountStore) FindAccount(name string) (string, error) { + path := s.AccountPath(name) + if _, err := os.Stat(path); err == nil { + return name, nil + } + dir := filepath.Join(s.dataDir, "players", "accounts") + entries, err := os.ReadDir(dir) + if err != nil { + return "", fmt.Errorf("account not found: %s", name) + } + lower := strings.ToLower(name) + for _, e := range entries { + if e.IsDir() { + continue + } + base := strings.TrimSuffix(e.Name(), ".yaml") + if strings.ToLower(base) == lower { + return base, nil + } + } + return "", fmt.Errorf("account not found: %s", name) +} + +func (s *AccountStore) CharPath(name string) string { + return filepath.Join(s.dataDir, "players", "characters", name+".yaml") +} + +func (s *AccountStore) LoadCharacter(name string) (*Player, error) { + path := s.CharPath(name) + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("character not found: %s", name) + } + var p Player + if err := yaml.Unmarshal(data, &p); err != nil { + return nil, fmt.Errorf("parse character: %w", err) + } + if p.Skills == nil { + p.Skills = make(map[SkillName]int) + } + if p.Equipment == nil { + p.Equipment = make(map[object.EquipSlot]string) + } + if p.Inventory == nil { + p.Inventory = make(map[int]*InventorySlot) + } + return &p, nil +} + +func (s *AccountStore) SaveCharacter(p *Player) error { + path := s.CharPath(p.Name) + data, err := yaml.Marshal(p) + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} + +func (s *AccountStore) CharacterExists(name string) bool { + _, err := s.FindCharacter(name) + return err == nil +} + +func (s *AccountStore) FindCharacter(name string) (string, error) { + path := s.CharPath(name) + if _, err := os.Stat(path); err == nil { + return name, nil + } + dir := filepath.Join(s.dataDir, "players", "characters") + entries, err := os.ReadDir(dir) + if err != nil { + return "", fmt.Errorf("character not found: %s", name) + } + lower := strings.ToLower(name) + for _, e := range entries { + if e.IsDir() { + continue + } + base := strings.TrimSuffix(e.Name(), ".yaml") + if strings.ToLower(base) == lower { + return base, nil + } + } + return "", fmt.Errorf("character not found: %s", name) +} diff --git a/internal/player/xp.go b/internal/player/xp.go new file mode 100644 index 0000000..ef59a25 --- /dev/null +++ b/internal/player/xp.go @@ -0,0 +1,41 @@ +package player + +var XPTable = [100]int{ + 0, 83, 174, 276, 388, 512, 650, 801, 969, 1154, + 1358, 1584, 1833, 2107, 2411, 2746, 3115, 3523, 3973, 4470, + 5018, 5624, 6291, 7028, 7842, 8740, 9730, 10824, 12031, 13363, + 14833, 16456, 18247, 20224, 22406, 24815, 27473, 30408, 33648, 37224, + 41171, 45529, 50339, 55649, 61512, 67983, 75127, 83014, 91721, 101333, + 111945, 123660, 136594, 150872, 166636, 184040, 203254, 224466, 247886, 273742, + 302288, 333804, 368599, 407015, 449428, 496254, 547953, 605032, 668051, 737627, + 814445, 899257, 992895, 1096278, 1210421, 1336443, 1475581, 1629200, 1798808, 1986068, + 2192818, 2421087, 2673114, 2951373, 3258594, 3597792, 3972294, 4385776, 4842295, 5346332, + 5902831, 6517253, 7195629, 7944614, 8771558, 9684577, 10692629, 11805606, 13034431, +} + +func LevelForXP(xp int) int { + for level := 1; level < 99; level++ { + if xp < XPTable[level] { + return level + } + } + return 99 +} + +func XPForLevel(level int) int { + if level <= 1 { + return 0 + } + if level > 99 { + level = 99 + } + return XPTable[level-1] +} + +func XPForNextLevel(xp int) int { + current := LevelForXP(xp) + if current >= 99 { + return 0 + } + return XPTable[current] - xp +} |
