aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_shop.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/cmd_shop.go')
-rw-r--r--internal/game/cmd_shop.go525
1 files changed, 244 insertions, 281 deletions
diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go
index 69de750..e47c84d 100644
--- a/internal/game/cmd_shop.go
+++ b/internal/game/cmd_shop.go
@@ -8,39 +8,72 @@ import (
"thehouseoficarus/internal/behavior"
"thehouseoficarus/internal/item"
"thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
"thehouseoficarus/internal/ui"
+ "thehouseoficarus/internal/world"
)
-func (g *Game) handleShopInput(sess *net.Session, input string) {
- if sess.Player == nil {
- sess.State = net.StateGame
- g.writePrompt(sess)
+// ---- Command entry points ----
+
+func (g *Game) executeShopList(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ if p == nil {
return
}
-
- input = strings.TrimSpace(input)
- parts := strings.Fields(strings.ToLower(input))
- if len(parts) == 0 {
- g.showShopBrowse(sess)
+ mob := g.resolveShopMob(sess, p, strings.Join(args, " "))
+ if mob == nil {
return
}
+ g.showShopInventory(sess, mob)
+}
- cmd := parts[0]
+func (g *Game) executeShopBuy(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ if p == nil {
+ return
+ }
+ itemArgs, mobName := splitOnKeyword(args, "from")
+ qty, itemName, all := parseShopQty(itemArgs)
+ if itemName == "" {
+ sess.WriteLine("Buy what? Use 'list' to see what's for sale.")
+ return
+ }
+ mob := g.resolveShopMob(sess, p, mobName)
+ if mob == nil {
+ return
+ }
+ g.doShopBuy(sess, mob, itemName, qty, all)
+}
- 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)
+func (g *Game) executeShopSell(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ if p == nil {
+ return
+ }
+ itemArgs, mobName := splitOnKeyword(args, "to")
+ qty, itemName, all := parseShopQty(itemArgs)
+ if itemName == "" {
+ sess.WriteLine("Sell what?")
+ return
+ }
+ mob := g.resolveShopMob(sess, p, mobName)
+ if mob == nil {
return
- default:
- sess.WriteLine("Commands: buy <item>, sell <item>, browse, leave")
}
- g.writeShopPrompt(sess)
+ g.doShopSell(sess, mob, itemName, qty, all)
+}
+
+// ---- Helpers ----
+
+// splitOnKeyword splits args around the first standalone keyword (e.g. "from",
+// "to"), returning the words before it and the joined words after it.
+func splitOnKeyword(args []string, kw string) (before []string, after string) {
+ for i, a := range args {
+ if strings.EqualFold(a, kw) {
+ return args[:i], strings.Join(args[i+1:], " ")
+ }
+ }
+ return args, ""
}
func parseShopQty(args []string) (int, string, bool) {
@@ -48,8 +81,7 @@ func parseShopQty(args []string) (int, string, bool) {
return 1, "", false
}
if strings.EqualFold(args[0], "all") {
- rest := strings.Join(args[1:], " ")
- return 0, rest, true
+ return 0, strings.Join(args[1:], " "), true
}
if n, err := strconv.Atoi(args[0]); err == nil && n > 0 {
return n, strings.Join(args[1:], " "), false
@@ -57,63 +89,100 @@ func parseShopQty(args []string) (int, string, bool) {
return 1, strings.Join(args, " "), false
}
-func (g *Game) cmdShopBuy(sess *net.Session, args []string) {
- cfg := g.shopConfig(sess)
- if cfg == nil {
- return
+// resolveShopMob finds the shop-owning mob in the player's room. If name is
+// empty and exactly one shop exists it is returned; multiple shops prompt
+// "Which shop?". A non-empty name matches a specific mob. Errors are written to
+// the session and nil is returned when resolution fails.
+func (g *Game) resolveShopMob(sess *net.Session, p *player.Player, name string) *world.MobInstance {
+ var shops []*world.MobInstance
+ for _, m := range g.MobStore.MobsInRoom(p.RoomID) {
+ if m.HasShop() {
+ shops = append(shops, m)
+ }
}
-
- qty, itemName, all := parseShopQty(args)
- if itemName == "" {
- sess.WriteLine("Buy what? Use 'browse' to see available items.")
- return
+ if len(shops) == 0 {
+ sess.WriteLine("There is no shop here.")
+ return nil
}
- g.doShopBuy(sess, itemName, qty, all)
-}
-
-func (g *Game) cmdShopSell(sess *net.Session, args []string) {
- cfg := g.shopConfig(sess)
- if cfg == nil {
- return
+ name = strings.TrimSpace(name)
+ if name != "" {
+ var matched []*world.MobInstance
+ for _, m := range shops {
+ if m.MatchQuality(name) > world.MatchNone {
+ matched = append(matched, m)
+ }
+ }
+ if len(matched) == 0 {
+ sess.WriteLine(fmt.Sprintf("There is no shop here run by \"%s\".", name))
+ return nil
+ }
+ if len(matched) > 1 {
+ g.promptWhichShop(sess, matched)
+ return nil
+ }
+ return matched[0]
}
- qty, itemName, all := parseShopQty(args)
- if itemName == "" {
- sess.WriteLine("Sell what?")
- return
+ if len(shops) == 1 {
+ return shops[0]
}
-
- g.doShopSell(sess, itemName, qty, all)
-}
-
-func (g *Game) writeShopPrompt(sess *net.Session) {
- sess.WritePrompt(g.colorize(sess, "dialog", "> "))
+ g.promptWhichShop(sess, shops)
+ return nil
}
-func (g *Game) shopConfig(sess *net.Session) *behavior.ShopConfig {
- cfg := sess.Shop
- return cfg
+func (g *Game) promptWhichShop(sess *net.Session, shops []*world.MobInstance) {
+ sess.WriteLine("Which shop? Try again naming one of:")
+ for _, m := range shops {
+ sess.WriteLine(" - " + m.Name)
+ }
}
-type shopMatch struct {
- si *behavior.ShopItem
- name string
- def *item.ItemDef
+type shopCandidate struct {
+ itemID string
+ def *item.ItemDef
+ name string
+ stock int
}
-func (g *Game) showShopBrowse(sess *net.Session) {
- cfg := g.shopConfig(sess)
- if cfg == nil {
- return
+// shopCandidates lists everything the shop currently offers for sale: its
+// configured items (even when out of stock) plus any dynamically-held items
+// with stock remaining.
+func (g *Game) shopCandidates(mob *world.MobInstance) []shopCandidate {
+ stock := g.MobStore.ShopStockSnapshot(mob.InstanceID)
+ seen := map[string]bool{}
+ var out []shopCandidate
+ add := func(id string) {
+ if id == "" || seen[id] {
+ return
+ }
+ seen[id] = true
+ def, _ := g.ItemStore.Load(id)
+ name := id
+ if def != nil {
+ name = def.Name
+ }
+ out = append(out, shopCandidate{itemID: id, def: def, name: name, stock: stock[id]})
+ }
+ for i := range mob.Shop.Items {
+ add(mob.Shop.Items[i].ItemID)
}
+ for id, n := range stock {
+ if n > 0 {
+ add(id)
+ }
+ }
+ return out
+}
- if cfg.Message != "" {
- sess.WriteLine(fmt.Sprintf("%s", g.colorize(sess, "dialog", cfg.Message)))
+func (g *Game) showShopInventory(sess *net.Session, mob *world.MobInstance) {
+ if mob.Shop.Message != "" {
+ sess.WriteLine(g.colorize(sess, "dialog", mob.Shop.Message))
}
- if len(cfg.Items) == 0 {
- sess.WriteLine("This shop has nothing for sale.")
+ candidates := g.shopCandidates(mob)
+ if len(candidates) == 0 {
+ sess.WriteLine(fmt.Sprintf("%s has nothing for sale.", mob.Name))
return
}
@@ -125,199 +194,113 @@ func (g *Game) showShopBrowse(sess *net.Session) {
}
t := ui.Table{
- Title: "Shop Inventory",
- Columns: []string{"Item", "Buy Price", "Sell Price"},
+ Title: mob.Name,
+ Columns: []string{"Item", "Stock", "Buy", "Sell"},
}
-
- for _, si := range cfg.Items {
- def, _ := g.ItemStore.Load(si.ItemID)
- itemName := si.ItemID
- if def != nil {
- itemName = def.Name
+ for _, c := range candidates {
+ value := 0
+ if c.def != nil {
+ value = c.def.Value
}
- buyStr := fmt.Sprintf("%d cr", si.BuyPrice)
sellStr := "\u2014"
- if si.SellPrice > 0 {
- sellStr = fmt.Sprintf("%d cr", si.SellPrice)
+ if mob.Shop.FindItem(c.itemID) != nil || mob.Shop.Buys() {
+ sellStr = fmt.Sprintf("%d cr", mob.Shop.SellPrice(value, c.stock))
}
t.Rows = append(t.Rows, []string{
- g.itemColorize(sess, def, itemName),
- g.colorize(sess, "credits_pickup", buyStr),
+ g.itemColorize(sess, c.def, c.name),
+ fmt.Sprintf("%d", c.stock),
+ g.colorize(sess, "credits_pickup", fmt.Sprintf("%d cr", value)),
g.colorize(sess, "credits_pickup", sellStr),
})
}
-
for _, line := range t.Render(unicode, wrapWidth) {
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
+func (g *Game) doShopBuy(sess *net.Session, mob *world.MobInstance, input string, qty int, all bool) {
+ var matches []shopCandidate
+ for _, c := range g.shopCandidates(mob) {
+ if behavior.WordPrefixMatch(input, c.name) {
+ matches = append(matches, c)
}
- 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.")
+ sess.WriteLine("That item isn't for sale here. Use 'list' to see the 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)))
+ sess.WriteLine(" - " + 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
- }
- }
+ c := matches[0]
+ p := sess.Player
+ price := 0
+ if c.def != nil {
+ price = c.def.Value
}
- 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.")
- }
+ stock := g.MobStore.ShopStockOf(mob.InstanceID, c.itemID)
+ if stock <= 0 {
+ sess.WriteLine(fmt.Sprintf("%s is out of stock.", g.itemColorize(sess, c.def, c.name)))
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
+ const unlimited = 1 << 30
+ want := qty
+ if all || qty == 0 {
+ want = unlimited
}
-
- def, _ := g.ItemStore.Load(si.ItemID)
- displayName := si.ItemID
- if def != nil {
- displayName = def.Name
+ if want > stock {
+ want = stock
}
-
- 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
+ capacity := g.shopInvCapacity(p, c.itemID, c.def)
+ if want > capacity {
+ want = capacity
}
-
- 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 price > 0 {
+ if afford := p.Credits / price; want > afford {
+ want = afford
}
}
- 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 {
+ if want <= 0 {
+ switch {
+ case capacity <= 0:
sess.WriteLine("Your inventory is full.")
+ case price > 0 && p.Credits < price:
+ sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", price, p.Credits))
+ default:
+ sess.WriteLine("You can't buy that right now.")
}
return
}
- p.Credits -= totalCost
- for j := 0; j < qty; j++ {
- freeSlot := p.FirstFreeSlot()
- p.SetInvSlot(freeSlot, g.newInventorySlot(si.ItemID, 1))
+ taken := g.MobStore.ShopTake(mob.InstanceID, c.itemID, want)
+ if taken <= 0 {
+ sess.WriteLine(fmt.Sprintf("%s is out of stock.", g.itemColorize(sess, c.def, c.name)))
+ return
}
+
+ total := price * taken
+ p.Credits -= total
+ g.giveShopItems(p, c.itemID, taken, c.def)
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))))
+ itemColor := g.itemColorize(sess, c.def, c.name)
+ credits := g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", total))
+ if taken == 1 {
+ sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, credits))
} else {
- sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", qty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCost))))
+ sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", taken, itemColor, credits))
}
}
-func (g *Game) doShopSell(sess *net.Session, input string, qty int, all bool) {
- cfg := g.shopConfig(sess)
- if cfg == nil {
- return
- }
-
+func (g *Game) doShopSell(sess *net.Session, mob *world.MobInstance, input string, qty int, all bool) {
p := sess.Player
matches := g.findInventoryMatches(input, p)
@@ -325,35 +308,28 @@ func (g *Game) doShopSell(sess *net.Session, input string, qty int, all bool) {
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)))
+ sess.WriteLine(" - " + g.itemColorize(sess, def, m.Name))
}
return
}
match := matches[0]
+ def, _ := g.ItemStore.Load(match.ID)
+ displayName := match.Name
+ itemColor := g.itemColorize(sess, def, displayName)
- 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 mob.Shop.FindItem(match.ID) == nil && !mob.Shop.Buys() {
+ sess.WriteLine(fmt.Sprintf("%s doesn't want to buy %s.", mob.Name, itemColor))
+ return
}
- 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
+ value := 0
+ if def != nil {
+ value = def.Value
}
totalInInv := p.CountItem(match.ID)
@@ -363,79 +339,66 @@ func (g *Game) doShopSell(sess *net.Session, input string, qty int, all bool) {
} else if actualQty > totalInInv {
actualQty = totalInInv
}
-
if actualQty <= 0 {
sess.WriteLine("You don't have that item.")
return
}
+ stock := g.MobStore.ShopStockOf(mob.InstanceID, match.ID)
+ total := 0
+ for k := 0; k < actualQty; k++ {
+ total += mob.Shop.SellPrice(value, stock+k)
+ }
+
p.RemoveItem(match.ID, actualQty)
- p.Credits += shopItem.SellPrice * actualQty
+ p.Credits += total
+ g.MobStore.ShopAdd(mob.InstanceID, match.ID, actualQty)
g.AccountStore.SaveCharacter(p)
- def, _ := g.ItemStore.Load(match.ID)
- displayName := match.Name
- itemColor := g.itemColorize(sess, def, displayName)
+ credits := g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", total))
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))))
+ sess.WriteLine(fmt.Sprintf("You sell a %s for %s credits.", itemColor, credits))
} 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))))
+ sess.WriteLine(fmt.Sprintf("You sell %d %s for %s credits.", actualQty, itemColor, credits))
}
}
-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)
+// shopInvCapacity reports how many of an item the player can still carry.
+func (g *Game) shopInvCapacity(p *player.Player, itemID string, def *item.ItemDef) int {
+ if def != nil && def.Stackable {
+ for i := 0; i < 28; i++ {
+ if slot := p.InvSlot(i); slot != nil && slot.ItemID == itemID {
+ return 1 << 30
+ }
}
- 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
+ if p.FirstFreeSlot() != -1 {
+ return 1 << 30
+ }
+ return 0
}
+ return p.FreeSlots()
+}
- node, ok := td.Cfg.Nodes[td.Node]
- if !ok {
- sess.State = net.StateGame
- g.cancelAction(p)
- g.writePrompt(sess)
+// giveShopItems places qty of an item into the player's inventory, stacking when
+// possible. Callers must ensure capacity via shopInvCapacity first.
+func (g *Game) giveShopItems(p *player.Player, itemID string, qty int, def *item.ItemDef) {
+ if def != nil && def.Stackable {
+ for i := 0; i < 28; i++ {
+ if slot := p.InvSlot(i); slot != nil && slot.ItemID == itemID {
+ slot.Quantity += qty
+ return
+ }
+ }
+ if slot := p.FirstFreeSlot(); slot != -1 {
+ p.SetInvSlot(slot, g.newInventorySlot(itemID, qty))
+ }
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)
+ for j := 0; j < qty; j++ {
+ slot := p.FirstFreeSlot()
+ if slot == -1 {
return
}
-
- if node.Goto != "" {
- next, ok := td.Cfg.Nodes[node.Goto]
- if !ok {
- break
- }
- td.Node = node.Goto
- node = next
- } else {
- break
- }
+ p.SetInvSlot(slot, g.newInventorySlot(itemID, 1))
}
-
- sess.State = net.StateGame
- g.cancelAction(p)
- g.writePrompt(sess)
}