package game import ( "fmt" "strconv" "strings" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/ui" ) func (g *Game) handleShopInput(sess *net.Session, input string) { if sess.Player == nil { sess.State = net.StateGame g.writePrompt(sess) return } input = strings.TrimSpace(input) parts := strings.Fields(strings.ToLower(input)) if len(parts) == 0 { g.showShopBrowse(sess) return } cmd := parts[0] switch cmd { case "buy": g.cmdShopBuy(sess, parts[1:]) case "sell": g.cmdShopSell(sess, parts[1:]) case "browse", "list": g.showShopBrowse(sess) case "leave", "bye", "exit", "quit": g.leaveShop(sess) return default: sess.WriteLine("Commands: buy , sell , browse, leave") } g.writeShopPrompt(sess) } func parseShopQty(args []string) (int, string, bool) { if len(args) == 0 { return 1, "", false } if strings.EqualFold(args[0], "all") { rest := strings.Join(args[1:], " ") return 0, rest, true } if n, err := strconv.Atoi(args[0]); err == nil && n > 0 { return n, strings.Join(args[1:], " "), false } return 1, strings.Join(args, " "), false } func (g *Game) cmdShopBuy(sess *net.Session, args []string) { cfg := g.shopConfig(sess) if cfg == nil { return } qty, itemName, all := parseShopQty(args) if itemName == "" { sess.WriteLine("Buy what? Use 'browse' to see available items.") return } g.doShopBuy(sess, itemName, qty, all) } func (g *Game) cmdShopSell(sess *net.Session, args []string) { cfg := g.shopConfig(sess) if cfg == nil { return } qty, itemName, all := parseShopQty(args) if itemName == "" { sess.WriteLine("Sell what?") return } g.doShopSell(sess, itemName, qty, all) } func (g *Game) writeShopPrompt(sess *net.Session) { sess.WritePrompt(g.colorize(sess, "dialog", "> ")) } func (g *Game) shopConfig(sess *net.Session) *behavior.ShopConfig { cfg := sess.Shop return cfg } type shopMatch struct { si *behavior.ShopItem name string def *item.ItemDef } func (g *Game) showShopBrowse(sess *net.Session) { cfg := g.shopConfig(sess) if cfg == nil { return } if cfg.Message != "" { sess.WriteLine(fmt.Sprintf("%s", g.colorize(sess, "dialog", cfg.Message))) } if len(cfg.Items) == 0 { sess.WriteLine("This shop has nothing for sale.") return } unicode := true if p := sess.Player; p != nil { unicode = p.OptionBool("unicode") } t := ui.Table{ Title: "Shop Inventory", Columns: []string{"Item", "Buy Price", "Sell Price"}, } for _, si := range cfg.Items { def, _ := g.ItemStore.Load(si.ItemID) itemName := si.ItemID if def != nil { itemName = def.Name } buyStr := fmt.Sprintf("%d cr", si.BuyPrice) sellStr := "\u2014" if si.SellPrice > 0 { sellStr = fmt.Sprintf("%d cr", si.SellPrice) } t.Rows = append(t.Rows, []string{ g.itemColorize(sess, def, itemName), g.colorize(sess, "credits_pickup", buyStr), g.colorize(sess, "credits_pickup", sellStr), }) } for _, line := range t.Render(unicode) { sess.WriteLine(line) } } func (g *Game) findShopMatches(input string, cfg *behavior.ShopConfig) []shopMatch { var matches []shopMatch for i := range cfg.Items { si := &cfg.Items[i] itemName := si.ItemID def, _ := g.ItemStore.Load(si.ItemID) if def != nil { itemName = def.Name } if behavior.WordPrefixMatch(input, itemName) { matches = append(matches, shopMatch{si: si, name: itemName, def: def}) } } return matches } func (g *Game) doShopBuy(sess *net.Session, input string, qty int, all bool) { cfg := g.shopConfig(sess) if cfg == nil { return } p := sess.Player matches := g.findShopMatches(input, cfg) if len(matches) == 0 { sess.WriteLine("That item isn't for sale here. Use 'browse' to see shop inventory.") return } if len(matches) > 1 { sess.WriteLine("That's ambiguous, which one?") for _, m := range matches { sess.WriteLine(fmt.Sprintf(" - %s", g.itemColorize(sess, m.def, m.name))) } return } si := matches[0].si def := matches[0].def actualQty := qty if all || qty == 0 { affordable := p.Credits / si.BuyPrice if def != nil && def.Stackable { actualQty = affordable } else { freeSlots := p.FreeSlots() actualQty = affordable if actualQty > freeSlots { actualQty = freeSlots } } } if actualQty <= 0 { if p.Credits < si.BuyPrice { sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits)) } else { sess.WriteLine("Your inventory is full.") } return } g.buyShopItem(sess, *si, actualQty) } func (g *Game) buyShopItem(sess *net.Session, si behavior.ShopItem, qty int) { p := sess.Player totalCost := si.BuyPrice * qty if p.Credits < totalCost { qty = p.Credits / si.BuyPrice if qty <= 0 { sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits)) return } totalCost = si.BuyPrice * qty } def, _ := g.ItemStore.Load(si.ItemID) displayName := si.ItemID if def != nil { displayName = def.Name } if def != nil && def.Stackable { for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot != nil && slot.ItemID == si.ItemID { p.Credits -= totalCost slot.Quantity += qty g.AccountStore.SaveCharacter(p) itemColor := g.itemColorize(sess, def, displayName) if qty == 1 { sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice)))) } else { sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", qty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCost)))) } return } } freeSlot := p.FirstFreeSlot() if freeSlot == -1 { sess.WriteLine("Your inventory is full.") return } p.Credits -= totalCost p.SetInvSlot(freeSlot, g.newInventorySlot(si.ItemID, qty)) g.AccountStore.SaveCharacter(p) itemColor := g.itemColorize(sess, def, displayName) if qty == 1 { sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice)))) } else { sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", qty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCost)))) } return } freeSlots := p.FreeSlots() if qty > freeSlots { if freeSlots == 0 { sess.WriteLine("Your inventory is full.") return } qty = freeSlots totalCost = si.BuyPrice * qty if p.Credits < totalCost { qty = p.Credits / si.BuyPrice totalCost = si.BuyPrice * qty } } if qty <= 0 { if p.Credits < si.BuyPrice { sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits)) } else { sess.WriteLine("Your inventory is full.") } return } p.Credits -= totalCost for j := 0; j < qty; j++ { freeSlot := p.FirstFreeSlot() p.SetInvSlot(freeSlot, g.newInventorySlot(si.ItemID, 1)) } g.AccountStore.SaveCharacter(p) itemColor := g.itemColorize(sess, def, displayName) if qty == 1 { sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice)))) } else { sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", qty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCost)))) } } func (g *Game) doShopSell(sess *net.Session, input string, qty int, all bool) { cfg := g.shopConfig(sess) if cfg == nil { return } p := sess.Player matches := g.findInventoryMatches(input, p) if len(matches) == 0 { sess.WriteLine("You don't have that item.") return } if len(matches) > 1 { sess.WriteLine("That's ambiguous, which one?") for _, m := range matches { def, _ := g.ItemStore.Load(m.ID) sess.WriteLine(fmt.Sprintf(" - %s", g.itemColorize(sess, def, m.Name))) } return } match := matches[0] var shopItem *behavior.ShopItem for i := range cfg.Items { si := &cfg.Items[i] if si.ItemID == match.ID && si.SellPrice > 0 { shopItem = si break } } if shopItem == nil { def, _ := g.ItemStore.Load(match.ID) displayName := match.ID if def != nil { displayName = def.Name } sess.WriteLine(fmt.Sprintf("The shop doesn't want to buy %s.", g.itemColorize(sess, def, displayName))) return } totalInInv := p.CountItem(match.ID) actualQty := qty if all || qty == 0 { actualQty = totalInInv } else if actualQty > totalInInv { actualQty = totalInInv } if actualQty <= 0 { sess.WriteLine("You don't have that item.") return } p.RemoveItem(match.ID, actualQty) p.Credits += shopItem.SellPrice * actualQty g.AccountStore.SaveCharacter(p) def, _ := g.ItemStore.Load(match.ID) displayName := match.Name itemColor := g.itemColorize(sess, def, displayName) if actualQty == 1 { sess.WriteLine(fmt.Sprintf("You sell a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", shopItem.SellPrice)))) } else { totalCredits := shopItem.SellPrice * actualQty sess.WriteLine(fmt.Sprintf("You sell %d %s for %s credits.", actualQty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCredits)))) } } func (g *Game) leaveShop(sess *net.Session) { sess.Shop = nil p := sess.Player if p == nil || p.Action == nil || p.Action.Type != "talk" { sess.State = net.StateGame if p != nil { g.cancelAction(p) } g.writePrompt(sess) return } td, ok := p.Action.Data.(*behavior.TalkData) if !ok || td.Cfg == nil { sess.State = net.StateGame g.cancelAction(p) g.writePrompt(sess) return } node, ok := td.Cfg.Nodes[td.Node] if !ok { sess.State = net.StateGame g.cancelAction(p) g.writePrompt(sess) return } for { visible := g.visibleOptions(sess, node.Options) if len(visible) > 0 { for i, opt := range visible { sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, opt.Text)) } sess.State = net.StateTalk g.reprompt(sess) return } if node.Goto != "" { next, ok := td.Cfg.Nodes[node.Goto] if !ok { break } td.Node = node.Goto node = next } else { break } } sess.State = net.StateGame g.cancelAction(p) g.writePrompt(sess) }