aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_shop.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-19 18:20:26 -0400
committerhistoria <[not public]>2026-06-19 18:20:26 -0400
commit0ea4eec5dd2122c6704216971eb1249276297867 (patch)
tree430fbbef04e837820f1d031c190f52ecd6112e25 /internal/game/cmd_shop.go
parent3a58125d14bb307f861b38c6c5b0a63babde7e08 (diff)
downloadthehouseoficarus-0ea4eec5dd2122c6704216971eb1249276297867.tar.gz
shop fixes, 'toggle' completely removed, movement speed increased
Diffstat (limited to 'internal/game/cmd_shop.go')
-rw-r--r--internal/game/cmd_shop.go276
1 files changed, 205 insertions, 71 deletions
diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go
index a3f7613..2742151 100644
--- a/internal/game/cmd_shop.go
+++ b/internal/game/cmd_shop.go
@@ -2,11 +2,12 @@ package game
import (
"fmt"
+ "strconv"
"strings"
"thehouseoficarus/internal/action"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/object"
)
func (g *Game) handleShopInput(sess *net.Session, input string) {
@@ -27,17 +28,9 @@ func (g *Game) handleShopInput(sess *net.Session, input string) {
switch cmd {
case "buy":
- if len(parts) < 2 {
- sess.WriteLine("Buy what? Use 'browse' to see available items.")
- } else {
- g.doShopBuy(sess, strings.Join(parts[1:], " "))
- }
+ g.cmdShopBuy(sess, parts[1:])
case "sell":
- if len(parts) < 2 {
- sess.WriteLine("Sell what?")
- } else {
- g.doShopSell(sess, strings.Join(parts[1:], " "))
- }
+ g.cmdShopSell(sess, parts[1:])
case "browse", "list":
g.showShopBrowse(sess)
case "leave", "bye", "exit", "quit":
@@ -49,15 +42,65 @@ func (g *Game) handleShopInput(sess *net.Session, input string) {
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.Write(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", "> ")))
}
func (g *Game) shopConfig(sess *net.Session) *action.ShopConfig {
- cfg, _ := sess.Shop.(*action.ShopConfig)
+ cfg := sess.Shop
return cfg
}
+type shopMatch struct {
+ si *action.ShopItem
+ name string
+ def *object.ItemDef
+}
+
func (g *Game) showShopBrowse(sess *net.Session) {
cfg := g.shopConfig(sess)
if cfg == nil {
@@ -74,16 +117,16 @@ func (g *Game) showShopBrowse(sess *net.Session) {
}
unicode := true
- if p, ok := sess.Player.(*player.Player); ok {
+ if p := sess.Player; p != nil {
unicode = p.OptionBool("unicode")
}
t := Table{
Title: "Shop Inventory",
- Columns: []string{"#", "Item", "Buy Price", "Sell Price"},
+ Columns: []string{"Item", "Buy Price", "Sell Price"},
}
- for i, si := range cfg.Items {
+ for _, si := range cfg.Items {
def, _ := g.ItemStore.Load(si.ItemID)
itemName := si.ItemID
if def != nil {
@@ -95,7 +138,6 @@ func (g *Game) showShopBrowse(sess *net.Session) {
sellStr = fmt.Sprintf("%d cr", si.SellPrice)
}
t.Rows = append(t.Rows, []string{
- fmt.Sprintf("%d", i+1),
g.colorize(sess, "item", itemName),
g.colorize(sess, "credits_pickup", buyStr),
g.colorize(sess, "credits_pickup", sellStr),
@@ -107,55 +149,84 @@ func (g *Game) showShopBrowse(sess *net.Session) {
}
}
-func (g *Game) doShopBuy(sess *net.Session, input string) {
+func (g *Game) findShopMatches(input string, cfg *action.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 action.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
}
- idx, err := parseChoiceIndex(input)
- if err == nil && idx > 0 && idx <= len(cfg.Items) {
- si := cfg.Items[idx-1]
- g.buyShopItem(sess, si)
+ 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
}
- var match *action.ShopItem
- for i := range cfg.Items {
- si := &cfg.Items[i]
- def, _ := g.ItemStore.Load(si.ItemID)
- itemName := si.ItemID
- if def != nil {
- itemName = def.Name
+ if len(matches) > 1 {
+ sess.WriteLine("That's ambiguous, which one?")
+ for _, m := range matches {
+ sess.WriteLine(fmt.Sprintf(" - %s", g.colorize(sess, "item", m.name)))
}
+ return
+ }
- invMatches := g.collectMatches(input, func(yield func(string, int) bool) {
- yield(itemName, -1)
- })
+ si := matches[0].si
+ def := matches[0].def
- if len(invMatches) > 0 {
- if match != nil {
- sess.WriteLine(fmt.Sprintf("That's ambiguous, which one? (use #, e.g. buy %s 1)", si.ItemID))
- return
+ 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
}
- match = si
}
}
- if match != nil {
- g.buyShopItem(sess, *match)
+ 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
}
- sess.WriteLine("That item isn't for sale here. Use 'browse' to see shop inventory.")
+ g.buyShopItem(sess, *si, actualQty)
}
-func (g *Game) buyShopItem(sess *net.Session, si action.ShopItem) {
- p, _ := sess.Player.(*player.Player)
+func (g *Game) buyShopItem(sess *net.Session, si action.ShopItem, qty int) {
+ p := sess.Player
- if p.Credits < si.BuyPrice {
- sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits))
- return
+ 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)
@@ -168,37 +239,83 @@ func (g *Game) buyShopItem(sess *net.Session, si action.ShopItem) {
for i := 0; i < 28; i++ {
slot := p.InvSlot(i)
if slot != nil && slot.ItemID == si.ItemID {
- p.Credits -= si.BuyPrice
- slot.Quantity++
+ p.Credits -= totalCost
+ slot.Quantity += qty
g.AccountStore.SaveCharacter(p)
itemColor := g.itemColorize(sess, def, displayName)
- sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice))))
+ 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
+ }
}
- freeSlot := p.FirstFreeSlot()
- if freeSlot == -1 {
- sess.WriteLine("Your inventory is full.")
+ 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 -= si.BuyPrice
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: si.ItemID, Quantity: 1})
+ 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)
- sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice))))
+ 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) {
+func (g *Game) doShopSell(sess *net.Session, input string, qty int, all bool) {
cfg := g.shopConfig(sess)
if cfg == nil {
return
}
- p, _ := sess.Player.(*player.Player)
+ p := sess.Player
matches := g.findInventoryMatches(input, p)
if len(matches) == 0 {
@@ -208,6 +325,9 @@ func (g *Game) doShopSell(sess *net.Session, input string) {
if len(matches) > 1 {
sess.WriteLine("That's ambiguous, which one?")
+ for _, m := range uniqueItemNames(matches) {
+ sess.WriteLine(fmt.Sprintf(" - %s", g.colorize(sess, "item", m)))
+ }
return
}
@@ -232,38 +352,52 @@ func (g *Game) doShopSell(sess *net.Session, input string) {
return
}
- p.RemoveItem(match.ID, 1)
- p.Credits += shopItem.SellPrice
+ 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.ID
- if def != nil {
- displayName = def.Name
- }
+ displayName := match.Name
itemColor := g.itemColorize(sess, def, displayName)
- sess.WriteLine(fmt.Sprintf("You sell a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", shopItem.SellPrice))))
+ 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, ok := sess.Player.(*player.Player)
- if !ok || p == nil || p.Action == nil || p.Action.Type != "talk" {
+ p := sess.Player
+ if p == nil || p.Action == nil || p.Action.Type != "talk" {
sess.State = net.StateGame
if p != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.writePrompt(sess)
return
}
- behaviorID, _ := p.Action.Data["behavior_id"].(string)
nodeKey, _ := p.Action.Data["node"].(string)
- cfg, err := g.BehaviorStore.LoadTalk(behaviorID)
- if err != nil {
+ cfg, ok := p.Action.Data["talk_cfg"].(*action.TalkConfig)
+ if !ok || cfg == nil {
sess.State = net.StateGame
- g.CancelAction(p)
+ g.cancelAction(p)
g.writePrompt(sess)
return
}
@@ -271,7 +405,7 @@ func (g *Game) leaveShop(sess *net.Session) {
node, ok := cfg.Nodes[nodeKey]
if !ok {
sess.State = net.StateGame
- g.CancelAction(p)
+ g.cancelAction(p)
g.writePrompt(sess)
return
}
@@ -287,7 +421,7 @@ func (g *Game) leaveShop(sess *net.Session) {
}
if visible == 0 {
sess.State = net.StateGame
- g.CancelAction(p)
+ g.cancelAction(p)
g.writePrompt(sess)
return
}