package cmd import ( "fmt" "time" "github.com/runescape-wiki/rsw/internal/prices" "github.com/runescape-wiki/rsw/internal/render" "github.com/spf13/cobra" ) func newPriceCmd() *cobra.Command { return &cobra.Command{ Use: "price ", Short: "Look up Grand Exchange prices for an item", Long: `Fetches real-time GE price data for an item: current buy/sell prices, trade volume, and recent trend. With --ironman, shows alch values and vendor prices instead of GE data. Examples: rsw osrs price "abyssal whip" rsw rs3 price "blue partyhat" rsw osrs price "dragon bones" --ironman`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { name := args[0] priceClient := prices.NewClient(GamePriceBaseURL()) mc := prices.NewMappingCache(priceClient) if err := mc.Load(); err != nil { return fmt.Errorf("failed to load item mapping: %w", err) } item := mc.LookupByName(name) if item == nil { matches := mc.SearchByName(name) if len(matches) == 0 { return fmt.Errorf("no item found matching %q", name) } if len(matches) > 1 { md := render.New() md.H2("Multiple items found") md.P("Did you mean one of these?") shown := 0 for _, m := range matches { md.Bullet(m.Name) shown++ if shown >= 15 { md.P(fmt.Sprintf("...and %d more", len(matches)-15)) break } } fmt.Print(md.String()) return nil } item = matches[0] } md := render.New() md.H1(item.Name) md.KV("Examine", item.Examine) md.KV("Members", fmt.Sprintf("%v", item.Members)) md.KV("Store value", render.FormatGP(item.Value)) if item.HighAlch != nil { md.KV("High Alch", render.FormatGP(*item.HighAlch)) } if item.LowAlch != nil { md.KV("Low Alch", render.FormatGP(*item.LowAlch)) } md.Newline() if Ironman() { md.H2("Ironman Info") md.P("*GE trading unavailable — showing self-sufficient values.*") md.KV("Store value", render.FormatGP(item.Value)) if item.HighAlch != nil { md.KV("High Alch value", render.FormatGP(*item.HighAlch)) } md.P("Use `rsw item` for drop sources and shop locations.") } else { latest, err := priceClient.GetLatestForItem(item.ID) if err != nil { md.P("*Price data unavailable.*") } else { md.H2("Grand Exchange Price") if latest.High != nil { md.KV("Instant buy", render.FormatGP(*latest.High)) if latest.HighTime != nil { md.KV("Last buy", timeAgo(*latest.HighTime)) } } if latest.Low != nil { md.KV("Instant sell", render.FormatGP(*latest.Low)) if latest.LowTime != nil { md.KV("Last sell", timeAgo(*latest.LowTime)) } } if item.Limit != nil { md.KV("Buy limit", render.FormatNumber(*item.Limit)) } md.Newline() } hourly, err := priceClient.Get1Hour(item.ID) if err == nil && len(hourly.Data) > 0 { recent := hourly.Data[len(hourly.Data)-1] md.H2("Recent Activity (1h)") if recent.AvgHighPrice != nil { md.KV("Avg buy price", render.FormatGP(*recent.AvgHighPrice)) } md.KV("Buy volume", render.FormatNumber(recent.HighVolume)) if recent.AvgLowPrice != nil { md.KV("Avg sell price", render.FormatGP(*recent.AvgLowPrice)) } md.KV("Sell volume", render.FormatNumber(recent.LowVolume)) md.Newline() } } fmt.Print(md.String()) return nil }, } } func timeAgo(unixTime int64) string { t := time.Unix(unixTime, 0) d := time.Since(t) switch { case d < time.Minute: return "just now" case d < time.Hour: return fmt.Sprintf("%d min ago", int(d.Minutes())) case d < 24*time.Hour: return fmt.Sprintf("%d hours ago", int(d.Hours())) default: return fmt.Sprintf("%d days ago", int(d.Hours()/24)) } } func init() { RegisterCommand(newPriceCmd) }