Files
claude-plugin-runescape/scripts/rsw/internal/cmd/price.go
2026-03-05 22:45:55 -06:00

203 lines
5.4 KiB
Go

package cmd
import (
"fmt"
"time"
"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 newPriceCmd() *cobra.Command {
return &cobra.Command{
Use: "price <item name>",
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]
if Game() == "rs3" {
return runRS3Price(name)
}
priceClient := prices.NewClient(GamePriceBaseURL())
mc := prices.NewMappingCache(priceClient, Game())
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 {
md.H2("Recent Activity (1h)")
if hourly.AvgHighPrice != nil {
md.KV("Avg buy price", render.FormatGP(*hourly.AvgHighPrice))
}
md.KV("Buy volume", render.FormatNumber(hourly.HighVolume))
if hourly.AvgLowPrice != nil {
md.KV("Avg sell price", render.FormatGP(*hourly.AvgLowPrice))
}
md.KV("Sell volume", render.FormatNumber(hourly.LowVolume))
md.Newline()
}
}
fmt.Print(md.String())
return nil
},
}
}
func runRS3Price(name string) error {
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
exchangeItem, err := wikiClient.GetExchangeModule(pageTitle)
if err != nil {
return fmt.Errorf("failed to look up exchange data: %w", err)
}
if exchangeItem == nil {
return fmt.Errorf("%q is not tradeable on the Grand Exchange", pageTitle)
}
rs3Client := prices.NewRS3Client()
detail, err := rs3Client.GetDetail(exchangeItem.ItemID)
if err != nil {
return fmt.Errorf("failed to fetch RS3 price data: %w", err)
}
md := render.New()
md.H1(detail.Name)
md.KV("Examine", exchangeItem.Examine)
md.KV("Members", fmt.Sprintf("%v", exchangeItem.Members))
if exchangeItem.Limit > 0 {
md.KV("Buy limit", render.FormatNumber(exchangeItem.Limit))
}
md.KV("Store value", render.FormatGP(exchangeItem.Value))
md.Newline()
md.H2("Grand Exchange Price")
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")
}
md.KV("30-day change", fmt.Sprintf("%s (%s)", detail.Day30Change, detail.Day30Trend))
md.KV("90-day change", fmt.Sprintf("%s (%s)", detail.Day90Change, detail.Day90Trend))
md.KV("180-day change", fmt.Sprintf("%s (%s)", detail.Day180Change, detail.Day180Trend))
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)
}