package player import ( "sort" "thehouseoficarus/internal/action" "thehouseoficarus/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" Firemaking SkillName = "firemaking" Mining SkillName = "mining" Smithing SkillName = "smithing" Crafting SkillName = "crafting" Fletching SkillName = "fletching" Pharmacy SkillName = "pharmacy" Thieving SkillName = "thieving" Agility SkillName = "agility" Construction SkillName = "construction" Scavenging SkillName = "scavenging" Hacking SkillName = "hacking" Assassin SkillName = "assassin" Farming SkillName = "farming" ) var AllSkills = []SkillName{ Attack, Strength, Defense, Hitpoints, Ranged, Science, Technology, Fishing, Cooking, Woodcutting, Firemaking, Mining, Smithing, Crafting, Fletching, Pharmacy, Thieving, Agility, Construction, Scavenging, Hacking, Assassin, Farming, } var SkillAbbr = map[SkillName]string{ Attack: "atk", Strength: "str", Defense: "def", Hitpoints: "hp", Ranged: "rng", Science: "sci", Technology: "tec", Fishing: "fis", Cooking: "cok", Woodcutting: "wct", Firemaking: "fmk", Mining: "min", Smithing: "smt", Crafting: "cft", Fletching: "flt", Pharmacy: "pha", Thieving: "thv", Agility: "agl", Construction: "con", Scavenging: "scv", Hacking: "hck", Assassin: "asm", Farming: "frm", } 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"` Quality int `yaml:"quality,omitempty"` MaxQuality int `yaml:"max_quality,omitempty"` } type OptionType int const ( OptBool OptionType = iota OptString OptInt ) type OptionDef struct { Name string Type OptionType Default any ValidValues []string Description string } var OptionDefs = []OptionDef{ {"description", OptBool, true, nil, "Long room descriptions when moving"}, {"tiny_map", OptString, "right", []string{"off", "right", "left"}, "Mini-map display (off/right/left)"}, {"xp_drops", OptBool, true, nil, "XP drop messages"}, {"exits", OptBool, true, nil, "Long exit display in look"}, {"mob_enter", OptBool, true, nil, "Messages when mobs enter the room"}, {"mob_leave", OptBool, true, nil, "Messages when mobs leave the room"}, {"mob_spawn", OptBool, true, nil, "Messages when mobs spawn in the area"}, {"reserve", OptBool, true, nil, "Show full reserved item details"}, {"depletion", OptBool, false, nil, "Show depletion and despawn timers"}, {"despawn", OptBool, false, nil, "Show ground item despawn timers"}, {"color", OptString, "xterm256", []string{"none", "ansi", "xterm256"}, "Color output mode"}, {"mapwidth", OptInt, 30, nil, "Map width for the map command"}, {"mapheight", OptInt, 20, nil, "Map height for the map command"}, {"mappadding", OptString, "none", []string{"none", "x", "y", "xy"}, "Map output padding mode"}, {"automap", OptBool, false, nil, "Show map automatically after moving"}, {"queue_silently", OptBool, true, nil, "Suppress messages for queued tick actions"}, {"room_desc_width", OptInt, 70, nil, "Maximum width for room descriptions"}, {"unicode", OptBool, true, nil, "Unicode box-drawing characters"}, {"run_countdown", OptBool, false, nil, "Show countdown messages when fleeing combat"}, {"visual_ticks", OptBool, false, nil, "Display a tick marker every game tick"}, {"visual_tick_count", OptInt, 0, nil, "Cycle length for tick counter (0 = no counter)"}, {"visual_tick_text", OptString, "Tick", nil, "Text displayed for visual ticks"}, {"fletch_all", OptBool, false, nil, "Auto-start fletching when only one product is possible"}, {"smith_all", OptBool, false, nil, "Auto-start smithing when only one product is possible"}, {"cook_all", OptBool, false, nil, "Auto-start cooking when only one product is possible"}, {"smelt_all", OptBool, false, nil, "Auto-start smelting when only one product is possible"}, {"mix_all", OptBool, false, nil, "Auto-start mixing when only one product is possible"}, {"construct_all", OptBool, false, nil, "Auto-start constructing when only one product is possible"}, } var optionByName map[string]*OptionDef func init() { optionByName = make(map[string]*OptionDef, len(OptionDefs)) for i := range OptionDefs { optionByName[OptionDefs[i].Name] = &OptionDefs[i] } } func GetOptionDef(name string) *OptionDef { return optionByName[name] } type Player struct { Name string `yaml:"name"` Skills map[SkillName]int `yaml:"skills"` Inventory map[int]*InventorySlot `yaml:"inventory"` Bank map[int]*InventorySlot `yaml:"bank"` Equipment map[object.EquipSlot]string `yaml:"equipment"` EquipQuality map[string]int `yaml:"equip_quality,omitempty"` RoomID int `yaml:"room_id"` HP int `yaml:"hp"` Credits int `yaml:"credits"` Description string `yaml:"description"` AttackStyle AttackStyle `yaml:"attack_style"` Prompt string `yaml:"prompt"` Options map[string]any `yaml:"-"` Flags map[string]any `yaml:"flags"` RegenerateTick int AmmoQty int `yaml:"ammo_qty,omitempty"` Action *action.Action `yaml:"-"` ActionState any `yaml:"-"` BackgroundAction *action.Action `yaml:"-"` BackgroundActionState any `yaml:"-"` WalkSequence []string `yaml:"-"` ConsumeCooldown int `yaml:"-"` MoveTicks int `yaml:"-"` MoveDirection string `yaml:"-"` MoveTarget int `yaml:"-"` VisualTickCurrent int `yaml:"-"` AutocastMod string `yaml:"-"` Battery float64 `yaml:"battery"` ActiveTechs map[string]bool `yaml:"-"` QuickTech string `yaml:"quick_tech,omitempty"` TechActivatedSinceTick map[string]bool `yaml:"-"` Sneaking bool `yaml:"-"` SneakNotified map[string]bool `yaml:"-"` ActiveBuffs []PotionBuff `yaml:"-"` } type PotionBuff struct { Stat string BonusPercent int TicksLeft int } func (p *Player) ClearMoveState() { p.MoveTicks = 0 p.MoveDirection = "" p.MoveTarget = 0 } func (p *Player) OptionBool(name string) bool { def := GetOptionDef(name) if def == nil || def.Type != OptBool { return false } if p.Options == nil { return def.Default.(bool) } val, ok := p.Options[name] if !ok { return def.Default.(bool) } b, ok := val.(bool) if !ok { return def.Default.(bool) } return b } func (p *Player) OptionString(name string) string { def := GetOptionDef(name) if def == nil || def.Type != OptString { return "" } if p.Options == nil { return def.Default.(string) } val, ok := p.Options[name] if !ok { return def.Default.(string) } s, ok := val.(string) if !ok { return def.Default.(string) } return s } func (p *Player) OptionInt(name string) int { def := GetOptionDef(name) if def == nil || def.Type != OptInt { return 0 } if p.Options == nil { return def.Default.(int) } val, ok := p.Options[name] if !ok { return def.Default.(int) } switch v := val.(type) { case int: return v case int64: return int(v) case float64: return int(v) } return def.Default.(int) } 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), Bank: make(map[int]*InventorySlot), Equipment: make(map[object.EquipSlot]string), AttackStyle: Accurate, Prompt: "> ", RoomID: 0, } for _, s := range AllSkills { p.Skills[s] = 0 } p.Skills[Hitpoints] = XPForLevel(10) p.HP = p.MaxHP() p.Battery = p.MaxBattery() 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) AddSkillXP(s SkillName, amount int) int { oldLevel := p.Level(s) p.AddXP(s, amount) newLevel := p.Level(s) if newLevel > oldLevel { return newLevel } return 0 } func (p *Player) CombatLevel() int { base := 0.25 * float64(p.Level(Defense)+p.Level(Hitpoints)+p.Level(Technology)) 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(Science)) return int(base) } func (p *Player) MaxHP() int { return p.Level(Hitpoints) } func (p *Player) StartRegen() { if p.RegenerateTick == 0 { p.RegenerateTick = 100 } } func (p *Player) HasItem(itemID string) bool { for _, slot := range p.Inventory { if slot != nil && slot.ItemID == itemID && slot.Quantity > 0 { return true } } return false } func (p *Player) CountItem(itemID string) int { count := 0 for _, slot := range p.Inventory { if slot != nil && slot.ItemID == itemID { count += slot.Quantity } } return count } func (p *Player) RemoveItem(itemID string, qty int) bool { remaining := qty for i, slot := range p.Inventory { if slot == nil || slot.ItemID != itemID { continue } if slot.Quantity <= remaining { remaining -= slot.Quantity delete(p.Inventory, i) } else { slot.Quantity -= remaining remaining = 0 } if remaining <= 0 { return true } } return remaining <= 0 } func (p *Player) MaxBattery() float64 { return float64(p.Level(Technology)) } func (p *Player) HasActiveTech(techID string) bool { if p.ActiveTechs == nil { return false } return p.ActiveTechs[techID] } func (p *Player) ActivateTech(techID string) { if p.ActiveTechs == nil { p.ActiveTechs = make(map[string]bool) } if p.TechActivatedSinceTick == nil { p.TechActivatedSinceTick = make(map[string]bool) } p.ActiveTechs[techID] = true p.TechActivatedSinceTick[techID] = true } func (p *Player) DeactivateTech(techID string) { if p.ActiveTechs != nil { delete(p.ActiveTechs, techID) } } func (p *Player) DeactivateAllTechs() { p.ActiveTechs = nil p.TechActivatedSinceTick = nil } func (p *Player) BankSlot(i int) *InventorySlot { if p.Bank == nil { return nil } return p.Bank[i] } func (p *Player) SetBankSlot(i int, slot *InventorySlot) { if p.Bank == nil { p.Bank = make(map[int]*InventorySlot) } if slot == nil { delete(p.Bank, i) } else { p.Bank[i] = slot } } func (p *Player) NextBankSlot() int { if p.Bank == nil { return 0 } i := 0 for { if p.Bank[i] == nil { return i } i++ } } func (p *Player) BankSlots() []int { if p.Bank == nil { return nil } keys := make([]int, 0, len(p.Bank)) for k := range p.Bank { keys = append(keys, k) } sort.Ints(keys) return keys } func (p *Player) BankCount() int { return len(p.Bank) } func (p *Player) ActiveTechList() []string { var result []string for id := range p.ActiveTechs { result = append(result, id) } sort.Strings(result) return result }