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

269 lines
7.1 KiB
Go

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 <name>",
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)
}