package world import ( "fmt" "math/rand" "os" "path/filepath" "strings" "sync" "thehouseoficarus/internal/action" "gopkg.in/yaml.v3" ) type MobDropTable struct { Remains string `yaml:"remains"` Loot []action.DropEntry `yaml:"loot"` } type MobDef struct { ID string `yaml:"id"` Name string `yaml:"name"` Description string `yaml:"description"` IdleDescriptions []string `yaml:"idle_descriptions"` CombatDescriptions []string `yaml:"combat_descriptions"` Attack int `yaml:"attack"` Strength int `yaml:"strength"` Defense int `yaml:"defense"` HP int `yaml:"hp"` Ranged int `yaml:"ranged"` Science int `yaml:"science"` Speed float64 `yaml:"speed"` Aggressive bool `yaml:"aggressive"` Protected bool `yaml:"protected"` Unique bool `yaml:"unique"` RespawnTicks float64 `yaml:"respawn_ticks"` Drops MobDropTable `yaml:"drops"` AttackBonus int `yaml:"attack_bonus"` StrengthBonus int `yaml:"strength_bonus"` AttackType string `yaml:"attack_type"` StabDefense int `yaml:"stab_defense"` SlashDefense int `yaml:"slash_defense"` CrushDefense int `yaml:"crush_defense"` ScienceDefense int `yaml:"science_defense"` RangedDefense int `yaml:"ranged_defense"` Weakness string `yaml:"weakness"` StealTable string `yaml:"steal_table"` StealLevel int `yaml:"steal_level"` StealXP int `yaml:"steal_xp"` StealSpeed float64 `yaml:"steal_speed"` AssassinLevel int `yaml:"assassin_level"` FinishingBlow string `yaml:"finishing_blow"` DamageWithout string `yaml:"damage_without"` Talk *action.TalkConfig `yaml:"talk,omitempty"` } func (d *MobDef) IsTalkable() bool { return d.Talk != nil } type MobInstance struct { InstanceID string DefID string Name string HP int MaxHP int Attack int Strength int Defense int Ranged int Science int Speed float64 Aggressive bool Protected bool Unique bool RespawnTicks float64 RoomID int HomeRoomID int Drops MobDropTable IdleDescription string WanderRooms []int WanderInterval float64 WanderTickCounter int regenerateTick int AttackBonus int StrengthBonus int AttackType string StabDefense int SlashDefense int CrushDefense int ScienceDefense int RangedDefense int Weakness string StealTable string StealLevel int StealXP int StealSpeed float64 AssassinLevel int FinishingBlow string DamageWithout string TalkConfig *action.TalkConfig } func (m *MobInstance) IsTalkable() bool { return m.TalkConfig != nil } const ( MatchNone = 0 MatchPrefix = 1 MatchExact = 2 ) func (m *MobInstance) MatchQuality(input string) int { lower := strings.ToLower(input) if strings.ToLower(m.Name) == lower { return MatchExact } if action.WordPrefixMatch(input, m.Name) { return MatchPrefix } return MatchNone } func pickIdleDescription(descriptions []string) string { if len(descriptions) == 0 { return "" } return descriptions[rand.Intn(len(descriptions))] } func (m *MobInstance) StartRegen() { if m.regenerateTick == 0 { m.regenerateTick = 100 } } type MobStore struct { dataDir string mu sync.Mutex pathIndex map[string]string defs map[string]*MobDef instances map[string]*MobInstance } func NewMobStore(dataDir string) *MobStore { return &MobStore{ dataDir: dataDir, pathIndex: action.BuildPathIndex(filepath.Join(dataDir, "mobs")), defs: make(map[string]*MobDef), instances: make(map[string]*MobInstance), } } func (s *MobStore) LoadDef(id string) (*MobDef, error) { s.mu.Lock() if def, ok := s.defs[id]; ok { s.mu.Unlock() return def, nil } s.mu.Unlock() path, ok := s.pathIndex[id] if !ok { return nil, fmt.Errorf("read mob %s: no such mob", id) } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read mob %s: %w", id, err) } var def MobDef if err := yaml.Unmarshal(data, &def); err != nil { return nil, fmt.Errorf("parse mob %s: %w", id, err) } s.mu.Lock() s.defs[id] = &def s.mu.Unlock() return &def, nil } func (s *MobStore) AllDefIDs() map[string]bool { ids := make(map[string]bool) for id := range s.pathIndex { ids[id] = true } return ids } func (s *MobStore) GetInstance(id string) *MobInstance { s.mu.Lock() defer s.mu.Unlock() return s.instances[id] } func (s *MobStore) AllInstances() []*MobInstance { s.mu.Lock() defer s.mu.Unlock() var out []*MobInstance for _, inst := range s.instances { out = append(out, inst) } return out } func (s *MobStore) MobsInRoom(roomID int) []*MobInstance { s.mu.Lock() defer s.mu.Unlock() var out []*MobInstance for _, inst := range s.instances { if inst.RoomID == roomID && inst.HP > 0 { out = append(out, inst) } } return out } func (s *MobStore) Tick() { s.mu.Lock() defer s.mu.Unlock() for _, inst := range s.instances { if inst.HP <= 0 || inst.HP >= inst.MaxHP { inst.regenerateTick = 0 continue } inst.regenerateTick-- if inst.regenerateTick <= 0 { inst.HP++ if inst.HP >= inst.MaxHP { inst.HP = inst.MaxHP inst.regenerateTick = 0 } else { inst.regenerateTick = 100 } } } } func (s *MobStore) RollIdleDescription(inst *MobInstance) { def, err := s.LoadDef(inst.DefID) if err != nil { return } inst.IdleDescription = pickIdleDescription(def.IdleDescriptions) inst.WanderTickCounter = 0 } func (s *MobStore) SeedMobs(roomID int, entries []RoomMob) { type defWrapper struct { def *MobDef err error rm RoomMob } defs := make([]defWrapper, len(entries)) for i, rm := range entries { d, err := s.LoadDef(rm.ID) defs[i] = defWrapper{d, err, rm} } s.mu.Lock() defer s.mu.Unlock() for i, dw := range defs { if dw.err != nil { continue } defID := dw.rm.ID instID := fmt.Sprintf("%s_%d_%d", defID, roomID, i) if _, exists := s.instances[instID]; exists { continue } inst := &MobInstance{ InstanceID: instID, DefID: defID, Name: dw.def.Name, HP: dw.def.HP, MaxHP: dw.def.HP, Attack: dw.def.Attack, Strength: dw.def.Strength, Defense: dw.def.Defense, Ranged: dw.def.Ranged, Science: dw.def.Science, Speed: dw.def.Speed, Aggressive: dw.def.Aggressive, Protected: dw.def.Protected, Unique: dw.def.Unique, RespawnTicks: dw.def.RespawnTicks, WanderRooms: dw.rm.WanderRooms, WanderInterval: dw.rm.WanderInterval, RoomID: roomID, HomeRoomID: roomID, Drops: dw.def.Drops, AttackBonus: dw.def.AttackBonus, StrengthBonus: dw.def.StrengthBonus, AttackType: dw.def.AttackType, StabDefense: dw.def.StabDefense, SlashDefense: dw.def.SlashDefense, CrushDefense: dw.def.CrushDefense, ScienceDefense: dw.def.ScienceDefense, RangedDefense: dw.def.RangedDefense, Weakness: dw.def.Weakness, StealTable: dw.def.StealTable, StealLevel: dw.def.StealLevel, StealXP: dw.def.StealXP, StealSpeed: dw.def.StealSpeed, AssassinLevel: dw.def.AssassinLevel, FinishingBlow: dw.def.FinishingBlow, DamageWithout: dw.def.DamageWithout, TalkConfig: dw.def.Talk, } inst.IdleDescription = pickIdleDescription(dw.def.IdleDescriptions) s.instances[instID] = inst } }