269 lines
7.1 KiB
Go
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)
|
|
}
|