aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_get.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 05:31:09 -0400
committerhistoria <[not public]>2026-09-18 05:31:09 -0400
commit153acb7f644d755c45533b7c5311e3ffa47e8973 (patch)
tree7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/cmd_get.go
parent87d08f0151e643951769e323995f1c5ed89ee200 (diff)
downloadthehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/game/cmd_get.go')
-rw-r--r--internal/game/cmd_get.go132
1 files changed, 89 insertions, 43 deletions
diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go
index bc48394..a2ad137 100644
--- a/internal/game/cmd_get.go
+++ b/internal/game/cmd_get.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
+ "thehouseoficarus/internal/casino"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
@@ -74,19 +75,21 @@ func (g *Game) doGet(sess *net.Session, input string) {
// doGetAllNamed). See TestPickupMergesIntoFullInventory.
if def != nil && def.Stackable {
+ quality, maxQuality := g.pickedStackProps(itemID)
for i := 0; i < 28; i++ {
slot := p.InvSlot(i)
- if slot != nil && slot.ItemID == itemID {
- removed, ok := g.World.RemoveReservedItem(p.RoomID, itemID, qty, p.Name)
- if !ok {
- sess.WriteLine("That's not yours!")
- return
- }
- slot.Quantity += removed
- g.saveCharacter(p)
- sess.WriteLine(fmt.Sprintf("You pick up %d x %s. (now %d)", qty, g.itemColorize(sess, def, def.Name), slot.Quantity))
+ if slot == nil || slot.ItemID != itemID || slot.Quality != quality || slot.MaxQuality != maxQuality {
+ continue
+ }
+ removed, ok := g.World.RemoveReservedItem(p.RoomID, itemID, qty, p.Name)
+ if !ok {
+ sess.WriteLine("That's not yours!")
return
}
+ slot.Quantity += removed
+ g.saveCharacter(p)
+ sess.WriteLine(fmt.Sprintf("You pick up %d x %s. (now %d)", qty, g.itemColorize(sess, def, def.Name), slot.Quantity))
+ return
}
freeSlot := p.FirstFreeSlot()
if freeSlot == -1 {
@@ -116,7 +119,7 @@ func (g *Game) doGet(sess *net.Session, input string) {
if _, ok := g.World.RemoveReservedItem(p.RoomID, itemID, 1, p.Name); !ok {
break
}
- p.SetInvSlot(fs, &player.InventorySlot{ItemID: itemID, Quantity: 1})
+ p.SetInvSlot(fs, g.newInventorySlot(itemID, 1))
picked++
}
@@ -192,19 +195,21 @@ func (g *Game) doGetAllNamed(sess *net.Session, input string) {
coloredName := g.itemColorize(sess, def, name)
if def != nil && def.Stackable {
+ quality, maxQuality := g.pickedStackProps(itemID)
for i := 0; i < 28; i++ {
slot := p.InvSlot(i)
- if slot != nil && slot.ItemID == itemID {
- removed, ok := g.World.RemoveReservedItem(p.RoomID, itemID, wanted, p.Name)
- if !ok {
- sess.WriteLine("That's not yours!")
- return
- }
- slot.Quantity += removed
- g.saveCharacter(p)
- sess.WriteLine(fmt.Sprintf("You pick up %d x %s. (now %d)", removed, coloredName, slot.Quantity))
+ if slot == nil || slot.ItemID != itemID || slot.Quality != quality || slot.MaxQuality != maxQuality {
+ continue
+ }
+ removed, ok := g.World.RemoveReservedItem(p.RoomID, itemID, wanted, p.Name)
+ if !ok {
+ sess.WriteLine("That's not yours!")
return
}
+ slot.Quantity += removed
+ g.saveCharacter(p)
+ sess.WriteLine(fmt.Sprintf("You pick up %d x %s. (now %d)", removed, coloredName, slot.Quantity))
+ return
}
freeSlot := p.FirstFreeSlot()
if freeSlot == -1 {
@@ -216,7 +221,7 @@ func (g *Game) doGetAllNamed(sess *net.Session, input string) {
sess.WriteLine("That's not yours!")
return
}
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: itemID, Quantity: removed})
+ p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, removed))
g.saveCharacter(p)
if removed == 1 {
sess.WriteLine(fmt.Sprintf("You pick up a %s.", coloredName))
@@ -236,7 +241,7 @@ func (g *Game) doGetAllNamed(sess *net.Session, input string) {
if !ok {
break
}
- p.SetInvSlot(fs, &player.InventorySlot{ItemID: itemID, Quantity: 1})
+ p.SetInvSlot(fs, g.newInventorySlot(itemID, 1))
picked++
}
@@ -265,15 +270,24 @@ func (g *Game) doGetAll(sess *net.Session) {
}
var picked []string
+ creditsCapped := false
for itemID, qty := range ground {
if qty <= 0 {
continue
}
if itemID == "credits" {
- // Same capped-removal rule as pickupCredits: the request is a
- // sentinel above any possible stock, not an exact quantity.
- taken := g.World.RemoveAvailableItem(p.RoomID, itemID, 999999, p.Name)
+ // Same capped-removal rule as pickupCredits: only the balance
+ // headroom is taken, so credits at the cap stay on the ground
+ // instead of being destroyed by the clamped grant.
+ room := creditHeadroom(p)
+ taken := 0
+ if room > 0 {
+ taken = g.World.RemoveAvailableItem(p.RoomID, itemID, room, p.Name)
+ }
if taken <= 0 {
+ if room <= 0 {
+ creditsCapped = true
+ }
continue
}
g.grantCredits(p, taken)
@@ -289,21 +303,23 @@ func (g *Game) doGetAll(sess *net.Session) {
for qty > 0 {
if def != nil && def.Stackable {
stacked := false
+ quality, maxQuality := g.pickedStackProps(itemID)
for i := 0; i < 28; i++ {
slot := p.InvSlot(i)
- if slot != nil && slot.ItemID == itemID {
- _, ok := g.World.RemoveReservedItem(p.RoomID, itemID, qty, p.Name)
- if !ok {
- qty = 0
- stacked = true
- break
- }
- slot.Quantity += qty
- picked = append(picked, fmt.Sprintf("%s (now %d)", g.itemColorize(sess, def, def.Name), slot.Quantity))
- stacked = true
+ if slot == nil || slot.ItemID != itemID || slot.Quality != quality || slot.MaxQuality != maxQuality {
+ continue
+ }
+ _, ok := g.World.RemoveReservedItem(p.RoomID, itemID, qty, p.Name)
+ if !ok {
qty = 0
+ stacked = true
break
}
+ slot.Quantity += qty
+ picked = append(picked, fmt.Sprintf("%s (now %d)", g.itemColorize(sess, def, def.Name), slot.Quantity))
+ stacked = true
+ qty = 0
+ break
}
if stacked {
break
@@ -349,7 +365,7 @@ func (g *Game) doGetAll(sess *net.Session) {
if !ok {
break
}
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: itemID, Quantity: take})
+ p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, take))
name := itemID
if def != nil {
name = def.Name
@@ -361,20 +377,28 @@ func (g *Game) doGetAll(sess *net.Session) {
g.saveCharacter(p)
- if len(picked) == 0 {
- sess.WriteLine("Your inventory is full.")
- } else {
+ if creditsCapped {
+ sess.WriteLine(g.colorize(sess, "currency_pickup", "Your purse can't hold any more credits."))
+ }
+ switch {
+ case len(picked) > 0:
sess.Write("You picked up: ")
formatPickupList(sess, picked)
+ case !creditsCapped:
+ sess.WriteLine("Your inventory is full.")
}
}
func (g *Game) pickupCredits(sess *net.Session, p *player.Player, itemID string) {
- // Take whatever credits are accessible: a sentinel quantity far above
- // any possible stock. RemoveReservedItem is all-or-nothing and would
- // reject a request larger than the stack; RemoveAvailableItem caps the
- // removal at what is actually on the ground.
- qty := g.World.RemoveAvailableItem(p.RoomID, itemID, 999999, p.Name)
+ // The balance cap is a hard limit: credits the purse cannot absorb must
+ // stay on the ground instead of being removed and then clamped away by
+ // grantCredits. Only the headroom below the cap is taken.
+ room := creditHeadroom(p)
+ if room <= 0 {
+ sess.WriteLine(g.colorize(sess, "currency_pickup", "Your purse can't hold any more credits."))
+ return
+ }
+ qty := g.World.RemoveAvailableItem(p.RoomID, itemID, room, p.Name)
if qty <= 0 {
return
}
@@ -387,6 +411,28 @@ func (g *Game) pickupCredits(sess *net.Session, p *player.Player, itemID string)
}
}
+// creditHeadroom is how many credits p's balance can still absorb before the
+// economy cap (casino.MaxCredits) is reached.
+func creditHeadroom(p *player.Player) int {
+ room := casino.MaxCredits - p.Credits
+ if room < 0 {
+ return 0
+ }
+ return room
+}
+
+// pickedStackProps returns the Quality and MaxQuality a freshly picked-up
+// stack receives (newInventorySlot stamps full quality onto quality-bearing
+// items). A pickup only merges into a slot with matching properties, since
+// merging a differently qualified stack would silently destroy the picked-up
+// stack's quality - the same rule withdrawBankItem applies in cmd_bank.go.
+// It reads the rule through stackProps so every other stack-creation site
+// (craft, harvest, ammo removal) agrees on the same values.
+func (g *Game) pickedStackProps(itemID string) (quality, maxQuality int) {
+ def, _ := g.ItemStore.Load(itemID)
+ return stackProps(def)
+}
+
func (g *Game) collectMatches(input string, each func(func(itemID string, slot int) bool)) []itemMatch {
var matches []itemMatch
each(func(itemID string, slot int) bool {