package main import ( "context" "encoding/json" "fmt" "os" "strings" "github.com/denisovdennis/autohero/internal/model" "github.com/denisovdennis/autohero/internal/storage" ) func loadGearCatalog(ctx context.Context, cs *storage.ContentStore, base string) ([]model.GearFamily, error) { switch strings.ToLower(strings.TrimSpace(base)) { case "", "db": return cs.LoadGearFamilies(ctx) case "code": return model.DefaultGearFamilies(), nil default: return nil, fmt.Errorf("gear-base must be db or code, got %q", base) } } // gearFamilyPatchJSON holds optional overrides merged onto catalog families after DB/code load. type gearFamilyPatchJSON struct { BasePrimary *int `json:"basePrimary,omitempty"` SpeedModifier *float64 `json:"speedModifier,omitempty"` BaseCrit *float64 `json:"baseCrit,omitempty"` AgilityBonus *int `json:"agilityBonus,omitempty"` StatType *string `json:"statType,omitempty"` } // listOverlayKeys returns top-level JSON object keys (patch targets). func listOverlayKeys(path string) ([]string, error) { b, err := os.ReadFile(path) if err != nil { return nil, err } var raw map[string]json.RawMessage if err := json.Unmarshal(b, &raw); err != nil { return nil, fmt.Errorf("gear overlay JSON: %w", err) } keys := make([]string, 0, len(raw)) for k := range raw { keys = append(keys, strings.TrimSpace(k)) } return keys, nil } // applyGearOverlayJSON clones families, applies patches keyed by "name" or "slot:name" (e.g. "main_hand:Soul Reaver"). func applyGearOverlayJSON(path string, families []model.GearFamily) ([]model.GearFamily, error) { b, err := os.ReadFile(path) if err != nil { return nil, err } var raw map[string]json.RawMessage if err := json.Unmarshal(b, &raw); err != nil { return nil, fmt.Errorf("gear overlay JSON: %w", err) } patches := make(map[string]gearFamilyPatchJSON, len(raw)) for k, v := range raw { var p gearFamilyPatchJSON if err := json.Unmarshal(v, &p); err != nil { return nil, fmt.Errorf("gear overlay key %q: %w", k, err) } patches[strings.TrimSpace(k)] = p } out := cloneGearFamilies(families) for i := range out { key := overlayKey(out[i]) if p, ok := patches[key]; ok { mergePatch(&out[i], p) continue } if p, ok := patches[out[i].Name]; ok { mergePatch(&out[i], p) } } return out, nil } func overlayKey(g model.GearFamily) string { return fmt.Sprintf("%s:%s", g.Slot, g.Name) } func cloneGearFamilies(f []model.GearFamily) []model.GearFamily { out := make([]model.GearFamily, len(f)) copy(out, f) return out } func mergePatch(g *model.GearFamily, p gearFamilyPatchJSON) { if p.BasePrimary != nil { g.BasePrimary = *p.BasePrimary } if p.SpeedModifier != nil { g.SpeedModifier = *p.SpeedModifier } if p.BaseCrit != nil { g.BaseCrit = *p.BaseCrit } if p.AgilityBonus != nil { g.AgilityBonus = *p.AgilityBonus } if p.StatType != nil { g.StatType = *p.StatType } } // printGearOverlayMigrationSQL prints UPDATEs for gear (and equipment_items for extended slots) for each overlay key. func printGearOverlayMigrationSQL(families []model.GearFamily, overlayKeys []string) { bySlotName := make(map[string]model.GearFamily, len(families)) byName := make(map[string]model.GearFamily, len(families)) for _, g := range families { bySlotName[overlayKey(g)] = g byName[g.Name] = g } fmt.Printf("-- balanceall -gear-print-sql (pairs with -gear-overlay)\n\n") for _, k := range overlayKeys { g, ok := bySlotName[k] if !ok { g, ok = byName[k] } if !ok { fmt.Printf("-- WARNING: no catalog family for overlay key %q\n", k) continue } r := catalogRarityForName(g.Name) ps := model.ScalePrimary(g.BasePrimary, 1, r) fmt.Printf(`UPDATE public.gear SET base_primary = %d, primary_stat = %d WHERE slot = '%s' AND name = %s; `, g.BasePrimary, ps, string(g.Slot), sqlQuote(g.Name)) switch g.Slot { case model.SlotMainHand, model.SlotChest: // Catalog is fully in public.gear above. default: if g.FormID != "" { fmt.Printf(`UPDATE public.equipment_items SET base_primary = %d, primary_stat = %d WHERE form_id = %s; `, g.BasePrimary, ps, sqlQuote(g.FormID)) } } fmt.Println() } } func catalogRarityForName(name string) model.Rarity { switch name { case "Rusty Dagger", "Iron Sword", "Rusty Axe", "Leather Armor", "Chainmail", "Iron Plate": return model.RarityCommon case "Iron Dagger", "Steel Sword", "Battle Axe", "Ranger's Vest", "Reinforced Mail", "Steel Plate": return model.RarityUncommon case "Assassin's Blade", "Longsword", "War Axe", "Shadow Cloak", "Battle Armor", "Fortress Armor", "Guardian's Plate": return model.RarityRare case "Phantom Edge", "Excalibur", "Infernal Axe", "Phantom Garb", "Royal Guard", "Dragon Scale", "Guardian's Bastion": return model.RarityEpic case "Fang of the Void", "Soul Reaver", "Godslayer's Edge", "Whisper of the Void", "Crown of Eternity", "Dragon Slayer", "Ancient Guardian's Aegis": return model.RarityLegendary default: return model.RarityCommon } } func sqlQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", "''") + "'" }