package cmd import ( "fmt" "strings" "github.com/runescape-wiki/rsw/internal/extract" "github.com/runescape-wiki/rsw/internal/prices" "github.com/runescape-wiki/rsw/internal/render" "github.com/runescape-wiki/rsw/internal/wiki" "github.com/spf13/cobra" ) func newItemCmd() *cobra.Command { return &cobra.Command{ Use: "item ", Short: "Look up an item — sources, stats, and price", Long: `Searches for an item on the wiki and displays its details: infobox stats, drop sources, acquisition methods, and GE price. With --ironman, hides GE price and emphasizes self-sufficient acquisition (drops, shops, crafting). Examples: rsw osrs item "abyssal whip" rsw rs3 item "dragon bones" --ironman`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { name := args[0] wikiClient := wiki.NewClient(GameBaseURL()) results, err := wikiClient.Search(name, 5) if err != nil { return fmt.Errorf("search failed: %w", err) } if len(results) == 0 { return fmt.Errorf("no wiki page found for %q", name) } pageTitle := results[0].Title page, err := wikiClient.GetPage(pageTitle) if err != nil { return fmt.Errorf("failed to fetch page: %w", err) } if Raw() { fmt.Println(page.Wikitext) return nil } templates := extract.ParseTemplates(page.Wikitext) md := render.New() md.H1(pageTitle) // Extract infobox infobox := findItemInfobox(templates) if infobox != nil { md.H2("Item Details") md.KV("Examine", infobox.Params["examine"]) md.KV("Members", infobox.Params["members"]) md.KV("Tradeable", infobox.Params["tradeable"]) md.KV("Quest item", infobox.Params["quest"]) md.KV("Weight", infobox.Params["weight"]) md.KV("High Alch", formatAlchValue(infobox.Params["highalch"])) md.KV("Low Alch", formatAlchValue(infobox.Params["lowalch"])) md.KV("Destroy", infobox.Params["destroy"]) md.KV("Release", infobox.Params["release"]) md.Newline() } // Equipment bonuses bonuses := extract.FindTemplate(templates, "Infobox Bonuses") if bonuses != nil { md.H2("Equipment Bonuses") headers := []string{"Stat", "Value"} var rows [][]string for _, stat := range []string{"astab", "aslash", "acrush", "amagic", "arange", "dstab", "dslash", "dcrush", "dmagic", "drange", "str", "rstr", "mdmg", "prayer"} { if v, ok := bonuses.Params[stat]; ok && v != "" && v != "0" { rows = append(rows, []string{statName(stat), v}) } } if len(rows) > 0 { md.Table(headers, rows) } } // GE Price vs Ironman if !Ironman() { renderGEPrice(md, name, pageTitle) } else { md.H2("Ironman Acquisition") md.P("*GE prices hidden — showing self-sufficient acquisition info.*") if infobox != nil { md.KV("High Alch", formatAlchValue(infobox.Params["highalch"])) md.KV("Low Alch", formatAlchValue(infobox.Params["lowalch"])) md.KV("Store price", infobox.Params["store"]) } md.Newline() } // Drop sources renderDropSources(md, templates) // Acquisition section from the page renderSourcesSection(md, page, wikiClient, pageTitle) fmt.Print(md.String()) return nil }, } } func findItemInfobox(templates []extract.Infobox) *extract.Infobox { for _, name := range []string{"Infobox Item", "Infobox item", "Infobox Bonuses", "Infobox bonuses"} { if box := extract.FindTemplate(templates, name); box != nil { return box } } return nil } func renderGEPrice(md *render.Builder, name, pageTitle string) { if Game() == "rs3" { renderRS3GEPrice(md, pageTitle) return } priceClient := prices.NewClient(GamePriceBaseURL()) mc := prices.NewMappingCache(priceClient, Game()) if err := mc.Load(); err != nil { return } item := mc.LookupByName(name) if item == nil { item = mc.LookupByName(pageTitle) } if item == nil { return } latest, err := priceClient.GetLatestForItem(item.ID) if err != nil { return } md.H2("Grand Exchange") if latest.High != nil { md.KV("Buy price", render.FormatGP(*latest.High)) } if latest.Low != nil { md.KV("Sell price", render.FormatGP(*latest.Low)) } if item.Limit != nil { md.KV("Buy limit", render.FormatNumber(*item.Limit)) } md.Newline() } func renderRS3GEPrice(md *render.Builder, pageTitle string) { wikiClient := wiki.NewClient(GameBaseURL()) exchangeItem, err := wikiClient.GetExchangeModule(pageTitle) if err != nil || exchangeItem == nil { return } rs3Client := prices.NewRS3Client() detail, err := rs3Client.GetDetail(exchangeItem.ItemID) if err != nil { return } md.H2("Grand Exchange") md.KV("Current price", detail.CurrentPrice+" gp") if detail.TodayPrice != 0 { md.KV("Today's change", render.FormatGP(detail.TodayPrice)) } else { md.KV("Today's change", "0 gp") } if exchangeItem.Limit > 0 { md.KV("Buy limit", render.FormatNumber(exchangeItem.Limit)) } md.Newline() } func renderDropSources(md *render.Builder, templates []extract.Infobox) { drops := extract.FindAllTemplates(templates, "DropsLine") if len(drops) == 0 { drops = extract.FindAllTemplates(templates, "DropLine") } if len(drops) == 0 { return } md.H2("Drop Sources") headers := []string{"Monster", "Quantity", "Rarity"} var rows [][]string for _, d := range drops { monster := firstNonEmpty(d.Params["name"], d.Params["Name"], d.Params["monster"], d.Params["Monster"]) qty := firstNonEmpty(d.Params["quantity"], d.Params["Quantity"]) rarity := firstNonEmpty(d.Params["rarity"], d.Params["Rarity"]) if monster != "" { rows = append(rows, []string{monster, qty, rarity}) } } if len(rows) > 0 { md.Table(headers, rows) } } func renderSourcesSection(md *render.Builder, page *wiki.ParsedPage, client *wiki.Client, title string) { for _, s := range page.Sections { lower := strings.ToLower(s.Line) if lower == "item sources" || lower == "sources" || lower == "obtaining" || lower == "acquisition" { idx := 0 _, _ = fmt.Sscanf(s.Index, "%d", &idx) if idx > 0 { sectionPage, err := client.GetPageSection(title, idx) if err == nil { plain := extract.ExtractPlainText(sectionPage.Wikitext) cleaned := strings.TrimSpace(plain) // Skip if only whitespace/asterisks remain after cleanup stripped := strings.NewReplacer("*", "", " ", "").Replace(cleaned) if stripped != "" { md.H2("Sources") md.P(cleaned) } } } return } } } func firstNonEmpty(vals ...string) string { for _, v := range vals { if v != "" { return v } } return "" } func formatAlchValue(s string) string { if s == "" { return "" } return s + " gp" } func statName(s string) string { names := map[string]string{ "astab": "Stab Attack", "aslash": "Slash Attack", "acrush": "Crush Attack", "amagic": "Magic Attack", "arange": "Ranged Attack", "dstab": "Stab Defence", "dslash": "Slash Defence", "dcrush": "Crush Defence", "dmagic": "Magic Defence", "drange": "Ranged Defence", "str": "Melee Strength", "rstr": "Ranged Strength", "mdmg": "Magic Damage", "prayer": "Prayer", } if n, ok := names[s]; ok { return n } return s } func init() { RegisterCommand(newItemCmd) }