package world import ( "fmt" "math/rand" "os" "path/filepath" "strings" "sync" "thehouseoficarus/internal/action" "thehouseoficarus/internal/object" "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"` Size string `yaml:"size"` // Kind discriminates a normal combat mob ("" or "combat") from a "task" // worksite (build a solar panel, survey a rock, etc.). A task mob reuses the // entire combat engine but its HP drains to 0 to COMPLETE the work; it never // attacks back (the room hazard, if any, supplies the danger). The progress // bar is displayed inverted (filling toward 100%). Kind string `yaml:"kind"` Verb string `yaml:"verb"` // flavor verb for messages (default "work") ProgressNoun string `yaml:"progress_noun"` // flavor noun, e.g. "construction" CompleteMessage string `yaml:"complete_message"` // shown on completion Talk *action.TalkConfig `yaml:"talk,omitempty"` } func (d *MobDef) IsTalkable() bool { return d.Talk != nil } func (d *MobDef) IsTask() bool { return d.Kind == "task" } 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 Size string Kind string Verb string ProgressNoun string CompleteMessage string TalkConfig *action.TalkConfig } func (m *MobInstance) IsTalkable() bool { return m.TalkConfig != nil } func (m *MobInstance) IsTask() bool { return m.Kind == "task" } func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []int, wanderInterval float64) *MobInstance { return &MobInstance{ InstanceID: instanceID, DefID: def.ID, Name: def.Name, HP: def.HP, MaxHP: def.HP, Attack: def.Attack, Strength: def.Strength, Defense: def.Defense, Ranged: def.Ranged, Science: def.Science, Speed: def.Speed, Aggressive: def.Aggressive, Protected: def.Protected, Unique: def.Unique, RespawnTicks: def.RespawnTicks, RoomID: roomID, HomeRoomID: roomID, WanderRooms: wanderRooms, WanderInterval: wanderInterval, Drops: def.Drops, AttackBonus: def.AttackBonus, StrengthBonus: def.StrengthBonus, AttackType: def.AttackType, StabDefense: def.StabDefense, SlashDefense: def.SlashDefense, CrushDefense: def.CrushDefense, ScienceDefense: def.ScienceDefense, RangedDefense: def.RangedDefense, Weakness: def.Weakness, StealTable: def.StealTable, StealLevel: def.StealLevel, StealXP: def.StealXP, StealSpeed: def.StealSpeed, AssassinLevel: def.AssassinLevel, FinishingBlow: def.FinishingBlow, DamageWithout: def.DamageWithout, Size: def.Size, Kind: def.Kind, Verb: def.Verb, ProgressNoun: def.ProgressNoun, CompleteMessage: def.CompleteMessage, TalkConfig: def.Talk, } } 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 object.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 := NewMobInstance(dw.def, instID, roomID, dw.rm.WanderRooms, dw.rm.WanderInterval) inst.IdleDescription = pickIdleDescription(dw.def.IdleDescriptions) s.instances[instID] = inst } }