package game import ( "fmt" "strings" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func (g *Game) doGet(sess *net.Session, input string) { p := sess.Player g.cancelAction(p) // Only stock this player may actually take counts: entries reserved // for another player are invisible to the pickup, so a blocked pickup // can never half-remove the ground stack. ground := g.World.AccessibleGround(p.RoomID, p.Name) if len(ground) == 0 { sess.WriteLine("There's nothing on the ground to pick up.") return } qty, itemName := parseQty(input) matches := g.findGroundMatches(itemName, p.RoomID) if len(matches) == 0 { sess.WriteLine(fmt.Sprintf("There's no '%s' here.", itemName)) return } if len(matches) > 1 { unique := uniqueItemNames(matches) if len(unique) > 1 { g.showWhichOne(sess, matches) return } } itemID := matches[0].ID if itemID == "credits" { g.pickupCredits(sess, p, itemID) return } def, _ := g.ItemStore.Load(itemID) name := itemID if def != nil { name = def.Name } available := 0 if gqty, ok := ground[itemID]; ok { available = gqty } if available <= 0 { sess.WriteLine("There's none of that here.") return } if qty == 0 { if def != nil && def.Stackable { qty = available } else { qty = 1 } } else if qty > available { qty = available } // No premature free-slot gate here: a stackable pickup may merge into an // existing stack even when every slot is occupied, so capacity is only // required when a new slot is actually needed (matching doGetAll and // 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 || 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 { sess.WriteLine("Your inventory is full.") return } if _, ok := g.World.RemoveReservedItem(p.RoomID, itemID, qty, p.Name); !ok { sess.WriteLine("That's not yours!") return } p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, qty)) g.saveCharacter(p) if qty == 1 { sess.WriteLine(fmt.Sprintf("You pick up a %s.", g.itemColorize(sess, def, def.Name))) } else { sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", qty, g.itemColorize(sess, def, def.Name))) } return } picked := 0 for picked < qty { fs := p.FirstFreeSlot() if fs == -1 { break } if _, ok := g.World.RemoveReservedItem(p.RoomID, itemID, 1, p.Name); !ok { break } p.SetInvSlot(fs, g.newInventorySlot(itemID, 1)) picked++ } g.saveCharacter(p) if picked == 0 { sess.WriteLine("Your inventory is full.") } else if picked == 1 { sess.WriteLine(fmt.Sprintf("You pick up a %s.", g.itemColorize(sess, def, name))) } else { sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", picked, g.itemColorize(sess, def, name))) } } func (g *Game) doGetAllNamed(sess *net.Session, input string) { p := sess.Player g.cancelAction(p) // Only takeable stock counts (see doGet). ground := g.World.AccessibleGround(p.RoomID, p.Name) if len(ground) == 0 { sess.WriteLine("There's nothing on the ground to pick up.") return } matches := g.findGroundMatches(input, p.RoomID) if len(matches) == 0 { sess.WriteLine(fmt.Sprintf("There's no '%s' here.", input)) return } if len(matches) > 1 { unique := uniqueItemNames(matches) if len(unique) > 1 { g.showWhichOne(sess, matches) return } } itemID := matches[0].ID if itemID == "credits" { g.pickupCredits(sess, p, itemID) return } def, _ := g.ItemStore.Load(itemID) available := 0 if gqty, ok := ground[itemID]; ok { available = gqty } if available <= 0 { sess.WriteLine("There's none of that here.") return } wanted := available if def != nil && !def.Stackable { free := p.FreeSlots() if wanted > free { wanted = free } } if wanted <= 0 { sess.WriteLine("Your inventory is full.") return } name := itemID if def != nil { name = def.Name } 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 || 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 { sess.WriteLine("Your inventory is full.") return } removed, ok := g.World.RemoveReservedItem(p.RoomID, itemID, wanted, p.Name) if !ok { sess.WriteLine("That's not yours!") return } p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, removed)) g.saveCharacter(p) if removed == 1 { sess.WriteLine(fmt.Sprintf("You pick up a %s.", coloredName)) } else { sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", removed, coloredName)) } return } picked := 0 for picked < wanted { fs := p.FirstFreeSlot() if fs == -1 { break } _, ok := g.World.RemoveReservedItem(p.RoomID, itemID, 1, p.Name) if !ok { break } p.SetInvSlot(fs, g.newInventorySlot(itemID, 1)) picked++ } g.saveCharacter(p) if picked == 0 { sess.WriteLine("Your inventory is full.") } else if picked == 1 { sess.WriteLine(fmt.Sprintf("You pick up a %s.", coloredName)) } else { sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", picked, coloredName)) } if picked < available { sess.WriteLine(fmt.Sprintf("You're only able to take %d x %s!", picked, coloredName)) } } func (g *Game) doGetAll(sess *net.Session) { p := sess.Player g.cancelAction(p) // Only takeable stock counts (see doGet); the all-or-nothing // RemoveReservedItem then never aborts a partial pickup. ground := g.World.AccessibleGround(p.RoomID, p.Name) if len(ground) == 0 { sess.WriteLine("There's nothing on the ground to pick up.") return } var picked []string creditsCapped := false for itemID, qty := range ground { if qty <= 0 { continue } if itemID == "credits" { // 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) if taken == 1 { picked = append(picked, "1 credit") } else { picked = append(picked, fmt.Sprintf("%d credits", taken)) } continue } def, _ := g.ItemStore.Load(itemID) 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 || 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 } freeSlot := p.FirstFreeSlot() if freeSlot == -1 { if len(picked) > 0 { sess.WriteLine("You can't hold everything but you picked up:") formatPickupList(sess, picked) } else { sess.WriteLine("Your inventory is full!") } g.saveCharacter(p) return } _, ok := g.World.RemoveReservedItem(p.RoomID, itemID, qty, p.Name) if !ok { break } p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, qty)) name := itemID if def != nil { name = def.Name } picked = append(picked, g.itemColorize(sess, def, name)) break } freeSlot := p.FirstFreeSlot() if freeSlot == -1 { if len(picked) > 0 { sess.WriteLine("You can't hold everything but you picked up:") formatPickupList(sess, picked) } else { sess.WriteLine("Your inventory is full!") } g.saveCharacter(p) return } take := 1 _, ok := g.World.RemoveReservedItem(p.RoomID, itemID, take, p.Name) if !ok { break } p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, take)) name := itemID if def != nil { name = def.Name } picked = append(picked, g.itemColorize(sess, def, name)) qty -= take } } g.saveCharacter(p) 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) { // 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 } g.grantCredits(p, qty) g.saveCharacter(p) if qty == 1 { sess.WriteLine(g.colorize(sess, "currency_pickup", "You pick up 1 credit.")) } else { sess.WriteLine(g.colorize(sess, "currency_pickup", fmt.Sprintf("You pick up %d credits. (total: %d)", qty, p.Credits))) } } // 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 { def, err := g.ItemStore.Load(itemID) if err != nil { return true } if def.MatchesName(input) { matches = append(matches, itemMatch{ID: itemID, Name: def.Name, Slot: slot}) } return true }) return matches } func (g *Game) findGroundMatches(input string, roomID int) []itemMatch { ground := g.World.GroundItems(roomID) return g.collectMatches(input, func(yield func(string, int) bool) { for itemID := range ground { if !yield(itemID, -1) { return } } }) } func (g *Game) findInventoryMatches(input string, p *player.Player) []itemMatch { return g.collectMatches(input, func(yield func(string, int) bool) { for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot == nil { continue } if !yield(slot.ItemID, i) { return } } }) } func (g *Game) executeGet(sess *net.Session, args []string, rawInput string) { if len(args) == 0 { sess.WriteLine("Get what?") } else if args[0] == "all" { if len(args) == 1 { g.doGetAll(sess) } else { g.doGetAllNamed(sess, strings.Join(args[1:], " ")) } } else { g.doGet(sess, strings.Join(args, " ")) } }