package world import ( "fmt" "math/rand" "os" "path/filepath" "strings" "sync" "thirdcollapse/internal/action" "gopkg.in/yaml.v3" ) type DropTable 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"` BehaviorID string `yaml:"behavior"` 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"` WanderRooms []int `yaml:"wander_rooms"` WanderInterval int `yaml:"wander_interval"` Drops DropTable `yaml:"drops"` } type MobInstance struct { InstanceID string DefID string Name string BehaviorID 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 WanderRooms []int WanderInterval int 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) 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.WanderRooms = def.WanderRooms inst.WanderInterval = def.WanderInterval 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, BehaviorID: dw.def.BehaviorID, 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, WanderRooms: dw.def.WanderRooms, WanderInterval: dw.def.WanderInterval, RoomID: roomID, HomeRoomID: roomID, Drops: dw.def.Drops, } inst.IdleDescription = pickIdleDescription(dw.def.IdleDescriptions) s.instances[instID] = inst } }