package action import ( "fmt" "math/rand" "os" "path/filepath" "sync" "gopkg.in/yaml.v3" ) type Store struct { dataDir string mu sync.Mutex cache map[string]*RawBehavior } type RawBehavior struct { ID string Type string Raw map[string]any } func NewStore(dataDir string) *Store { return &Store{ dataDir: dataDir, cache: make(map[string]*RawBehavior), } } func (s *Store) Load(id string) (*RawBehavior, error) { s.mu.Lock() if b, ok := s.cache[id]; ok { s.mu.Unlock() return b, nil } s.mu.Unlock() path := filepath.Join(s.dataDir, "behaviors", id+".yaml") data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read behavior %s: %w", id, err) } var raw map[string]any if err := yaml.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("parse behavior %s: %w", id, err) } rb := &RawBehavior{ID: id, Raw: raw} if t, ok := raw["type"].(string); ok { rb.Type = t } if rid, ok := raw["id"].(string); ok { rb.ID = rid } s.mu.Lock() s.cache[id] = rb s.mu.Unlock() return rb, nil } func unmarshalRaw[T any](rb *RawBehavior, expectedType string) (*T, error) { if rb.Type != expectedType { return nil, fmt.Errorf("behavior %s is type %s, expected %s", rb.ID, rb.Type, expectedType) } var cfg T raw, _ := yaml.Marshal(rb.Raw) if err := yaml.Unmarshal(raw, &cfg); err != nil { return nil, fmt.Errorf("parse %s config %s: %w", expectedType, rb.ID, err) } return &cfg, nil } func (s *Store) LoadGather(id string) (*GatherConfig, error) { rb, err := s.Load(id) if err != nil { return nil, err } return unmarshalRaw[GatherConfig](rb, "gather") } func (s *Store) LoadTalk(id string) (*TalkConfig, error) { rb, err := s.Load(id) if err != nil { return nil, err } return unmarshalRaw[TalkConfig](rb, "talk") } func (s *Store) LoadUse(id string) (*UseConfig, error) { rb, err := s.Load(id) if err != nil { return nil, err } return unmarshalRaw[UseConfig](rb, "use") } func (s *Store) LoadToggle(id string) (*ToggleConfig, error) { rb, err := s.Load(id) if err != nil { return nil, err } return unmarshalRaw[ToggleConfig](rb, "toggle") } func SuccessChance(cfg SuccessFormula, level int, requiredLevel int) float64 { chance := cfg.Base + float64(level-requiredLevel)*cfg.PerLevel if chance > cfg.Cap { chance = cfg.Cap } if chance < 0 { chance = 0 } return chance } func (s *Store) LoadDropTable(id string) (*DropTableDef, error) { path := filepath.Join(s.dataDir, "drops", id+".yaml") data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read drop table %s: %w", id, err) } var dt DropTableDef if err := yaml.Unmarshal(data, &dt); err != nil { return nil, fmt.Errorf("parse drop table %s: %w", id, err) } return &dt, nil } func (s *Store) ResolveDrop(drops []DropEntry) *DropEntry { if len(drops) == 0 { return nil } total := 0 for _, d := range drops { total += d.Weight } if total <= 0 { return nil } roll := rand.Intn(total) cumulative := 0 for i := range drops { cumulative += drops[i].Weight if roll < cumulative { if drops[i].Table != "" { sub, err := s.LoadDropTable(drops[i].Table) if err == nil { if resolved := s.ResolveDrop(sub.Drops); resolved != nil { qty := drops[i].Quantity if qty <= 0 { qty = resolved.Quantity } if qty <= 0 { qty = 1 } return &DropEntry{ ItemID: resolved.ItemID, Quantity: qty, Depletes: drops[i].Depletes, Message: drops[i].Message, } } } return nil } return &drops[i] } } return &drops[0] }