package behavior import ( "fmt" "math/rand" "os" "path/filepath" "strings" "sync" "gopkg.in/yaml.v3" ) func BuildPathIndex(dir string) map[string]string { index := make(map[string]string) filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { return nil } id := strings.TrimSuffix(d.Name(), ".yaml") index[id] = path return nil }) return index } func BuildRoomIndex(dir string) map[int]string { index := make(map[int]string) filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { return nil } id := strings.TrimSuffix(d.Name(), ".yaml") var n int if _, scanErr := fmt.Sscanf(id, "%d", &n); scanErr == nil { index[n] = path } return nil }) return index } func WalkYAMLDir(dir string, fn func(path, id string, data []byte) error) error { return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { return nil } data, readErr := os.ReadFile(path) if readErr != nil { return nil } id := strings.TrimSuffix(d.Name(), ".yaml") return fn(path, id, data) }) } type Duplicate struct { ID string Paths []string } func CheckDuplicateIDs(dir string) []Duplicate { seen := make(map[string][]string) filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { return nil } id := strings.TrimSuffix(d.Name(), ".yaml") seen[id] = append(seen[id], path) return nil }) var dups []Duplicate for id, paths := range seen { if len(paths) > 1 { dups = append(dups, Duplicate{ID: id, Paths: paths}) } } return dups } 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 } var ( dropIndex map[string]string dropIndexMu sync.Mutex ) func loadDropIndex(dataDir string) map[string]string { dropIndexMu.Lock() defer dropIndexMu.Unlock() if dropIndex == nil { dropIndex = BuildPathIndex(filepath.Join(dataDir, "drops")) } return dropIndex } func ClearDropIndex() { dropIndexMu.Lock() defer dropIndexMu.Unlock() dropIndex = nil } func LoadDropTable(dataDir, id string) (*DropTableDef, error) { index := loadDropIndex(dataDir) path, ok := index[id] if !ok { path = filepath.Join(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 } // pickWeighted returns a single entry chosen by weight, without resolving any // table reference on it. Returns nil if the list is empty or has no weight. func pickWeighted(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 { return &drops[i] } } return &drops[0] } func ResolveDrop(dataDir string, drops []DropEntry) *DropEntry { picked := pickWeighted(drops) if picked == nil { return nil } if picked.Table != "" { sub, err := LoadDropTable(dataDir, picked.Table) if err != nil { return nil } resolved := ResolveDrop(dataDir, sub.Drops) if resolved == nil { return nil } qty := picked.Quantity if qty <= 0 { qty = resolved.Quantity } if qty <= 0 { qty = 1 } return &DropEntry{ ItemID: resolved.ItemID, Quantity: qty, Depletes: picked.Depletes, SuccessMessage: picked.SuccessMessage, } } return picked } // ResolveDropList performs one weighted pick from drops. A plain item entry // yields a single result. When the picked entry references a sub-table, its // Quantity is treated as the number of INDEPENDENT rolls against that sub-table // (default 1) — each roll may produce a different item — and all results are // returned. Tables are just weighted lists of items; this is skill-independent. func ResolveDropList(dataDir string, drops []DropEntry) []DropEntry { picked := pickWeighted(drops) if picked == nil { return nil } if picked.Table == "" { return []DropEntry{*picked} } sub, err := LoadDropTable(dataDir, picked.Table) if err != nil { return nil } rolls := picked.Quantity if rolls <= 0 { rolls = 1 } var out []DropEntry for i := 0; i < rolls; i++ { if resolved := ResolveDrop(dataDir, sub.Drops); resolved != nil && resolved.ItemID != "" { out = append(out, *resolved) } } return out }