package world import ( "fmt" "math/rand" "os" "path/filepath" "strings" "sync" "gopkg.in/yaml.v3" ) type LootEntry struct { ItemID string `yaml:"item_id"` Weight int `yaml:"weight"` Quantity int `yaml:"quantity"` } type DropTable struct { Remains string `yaml:"remains"` Loot []LootEntry `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"` Speed int `yaml:"speed"` Aggressive bool `yaml:"aggressive"` Protected bool `yaml:"protected"` Unique bool `yaml:"unique"` RespawnTicks int `yaml:"respawn_ticks"` Wander int `yaml:"wander"` WanderTick int `yaml:"wander_tick"` WanderChance float64 `yaml:"wander_chance"` Drops DropTable `yaml:"drops"` } type MobInstance struct { InstanceID string DefID string Name string HP int MaxHP int Attack int Strength int Defense int Speed int Aggressive bool Protected bool Unique bool RespawnTicks int RoomID int HomeRoomID int Drops DropTable IdleDescription string Wander int WanderTick int WanderChance float64 WanderTickCounter int regenerateTick int } const ( MatchNone = 0 MatchPrefix = 1 MatchExact = 2 ) func WordPrefixMatch(input, name string) bool { lower := strings.ToLower(input) for _, word := range strings.Fields(name) { if strings.HasPrefix(strings.ToLower(word), lower) { return true } } return false } func (m *MobInstance) MatchQuality(input string) int { lower := strings.ToLower(input) if strings.ToLower(m.Name) == lower { return MatchExact } if 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 defs map[string]*MobDef instances map[string]*MobInstance } func NewMobStore(dataDir string) *MobStore { return &MobStore{ dataDir: dataDir, 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 := filepath.Join(s.dataDir, "mobs", id+".yaml") 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) SpawnMob(defID string, roomID int, instanceID string) (*MobInstance, error) { def, err := s.LoadDef(defID) if err != nil { return nil, err } s.mu.Lock() defer s.mu.Unlock() inst := &MobInstance{ InstanceID: instanceID, DefID: defID, Name: def.Name, HP: def.HP, MaxHP: def.HP, Attack: def.Attack, Strength: def.Strength, Defense: def.Defense, Speed: def.Speed, Aggressive: def.Aggressive, RespawnTicks: def.RespawnTicks, RoomID: roomID, Drops: def.Drops, } s.instances[instanceID] = inst return inst, nil } func (s *MobStore) GetInstance(id string) *MobInstance { s.mu.Lock() defer s.mu.Unlock() return s.instances[id] } func (s *MobStore) RemoveInstance(id string) { s.mu.Lock() defer s.mu.Unlock() delete(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.Wander = def.Wander inst.WanderTick = def.WanderTick inst.WanderChance = def.WanderChance inst.WanderTickCounter = 0 } func (s *MobStore) SeedMobs(roomID int, mobIDs []string) { type defWrapper struct { def *MobDef err error } defs := make([]defWrapper, len(mobIDs)) for i, defID := range mobIDs { d, err := s.LoadDef(defID) defs[i] = defWrapper{d, err} } s.mu.Lock() defer s.mu.Unlock() for i, dw := range defs { if dw.err != nil { continue } defID := mobIDs[i] instID := fmt.Sprintf("%s_%d_%d", defID, roomID, i) if inst, exists := s.instances[instID]; exists { // Already exists — skip (respawn is handled by timers) _ = inst 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, Speed: dw.def.Speed, Aggressive: dw.def.Aggressive, Protected: dw.def.Protected, Unique: dw.def.Unique, RespawnTicks: dw.def.RespawnTicks, Wander: dw.def.Wander, WanderTick: dw.def.WanderTick, WanderChance: dw.def.WanderChance, RoomID: roomID, HomeRoomID: roomID, Drops: dw.def.Drops, } inst.IdleDescription = pickIdleDescription(dw.def.IdleDescriptions) s.instances[instID] = inst } }