Initial commit
This commit is contained in:
237
scripts/rsw/internal/cmd/item.go
Normal file
237
scripts/rsw/internal/cmd/item.go
Normal file
@@ -0,0 +1,237 @@
|
||||
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) {
|
||||
priceClient := prices.NewClient(GamePriceBaseURL())
|
||||
mc := prices.NewMappingCache(priceClient)
|
||||
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 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)
|
||||
}
|
||||
Reference in New Issue
Block a user