aboutsummaryrefslogtreecommitdiff
path: root/internal/player
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-09 05:59:20 -0400
committerhistoria <[not public]>2026-06-09 05:59:20 -0400
commit9eb5ab1818b6b19501fd7209db1987c9e06cc919 (patch)
tree76e046b0bf611dfe939a8c8f6507467de2e9e20f /internal/player
downloadthehouseoficarus-9eb5ab1818b6b19501fd7209db1987c9e06cc919.tar.gz
first commit
Diffstat (limited to 'internal/player')
-rw-r--r--internal/player/account.go7
-rw-r--r--internal/player/password.go32
-rw-r--r--internal/player/player.go147
-rw-r--r--internal/player/store.go136
-rw-r--r--internal/player/xp.go41
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
+}