Files
rgoue/game/pack.go
sneak b940cfc41f Give item categories and subtypes real types (refactor step 2)
ObjectKind separates what an item is from how it draws: Object.Type
(a byte that doubled as the map character) becomes Kind ObjectKind,
with Glyph() producing the display character and objectKindForGlyph
converting back at the map boundary. KindWand covers the C 'stick'
category. The magic-missile bolt uses KindGold, matching C's literal
o_type='*' trick, with a comment.

PotionKind, ScrollKind, RingKind, WandKind, WeaponKind, ArmorKind and
TrapKind are now iota enums with Stringer (names come from the base
info tables); Object gains typed accessors (obj.RingKind(), ...) and
Launch is typed WeaponKind. beTrapped returns TrapKind. The
getItem/inventory/whatis prompt filters take ObjectKind, with
KindCallable/KindRingOrStick replacing the C CALLABLE/R_OR_S
sentinels; wizard.c's type_name table is subsumed by
ObjectKind.String(). IsRing/IsWearing/initWeapon/doPot signatures are
typed accordingly.

The save format version becomes 5.4.4-go2: the gob field rename would
otherwise silently zero Kind when reading old saves.

No behavior change; full suite green.
2026-07-06 22:00:43 +02:00

403 lines
8.8 KiB
Go

package game
// pack.c — routines to deal with the pack.
// addPack picks up an object and adds it to the pack; if obj is non-nil use
// it instead of getting it off the ground (pack.c add_pack).
func (g *RogueGame) addPack(obj *Object, silent bool) {
p := &g.Player
fromFloor := false
if obj == nil {
if obj = g.findObj(p.Pos.Y, p.Pos.X); obj == nil {
return
}
fromFloor = true
}
// Check for and deal with scare monster scrolls
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster && obj.Flags.Has(WasFound) {
detachObj(&g.Level.Objects, obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
}
g.msg("the scroll turns to dust as you pick it up")
return
}
if len(p.Pack) == 0 {
p.Pack = append(p.Pack, obj)
obj.PackCh = g.packChar()
p.Inpack++
} else {
// Walk the pack looking for the insertion point, keeping items of
// one type together and merging stackable/grouped items — a direct
// translation of the C linked-list walk. lp is the index to insert
// after; -1 after a merge means no insertion.
lp := -1
merged := false
for i := 0; i < len(p.Pack); i++ {
if p.Pack[i].Kind != obj.Kind {
lp = i
continue
}
// found the group of our type: scan for matching subtype
for p.Pack[i].Kind == obj.Kind && p.Pack[i].Which != obj.Which {
lp = i
if i+1 >= len(p.Pack) {
break
}
i++
}
op := p.Pack[i]
if op.Kind == obj.Kind && op.Which == obj.Which {
if op.Kind.MergesInPack() {
if !g.packRoom(fromFloor, obj) {
return
}
op.Count++
obj = op
lp = -1
merged = true
} else if obj.Group != 0 {
lp = i
for p.Pack[i].Kind == obj.Kind &&
p.Pack[i].Which == obj.Which &&
p.Pack[i].Group != obj.Group {
lp = i
if i+1 >= len(p.Pack) {
break
}
i++
}
op = p.Pack[i]
if op.Kind == obj.Kind && op.Which == obj.Which &&
op.Group == obj.Group {
op.Count += obj.Count
p.Inpack--
if !g.packRoom(fromFloor, obj) {
return
}
obj = op
lp = -1
merged = true
}
} else {
lp = i
}
}
break
}
if !merged && lp != -1 {
if !g.packRoom(fromFloor, obj) {
return
}
obj.PackCh = g.packChar()
p.Pack = append(p.Pack[:lp+1],
append([]*Object{obj}, p.Pack[lp+1:]...)...)
}
}
obj.Flags.Set(WasFound)
// If this was the object of something's desire, that monster will get
// mad and run at the hero.
for _, op := range g.Level.Monsters {
if op.Dest == &obj.Pos {
op.Dest = &p.Pos
}
}
if obj.Kind == KindAmulet {
g.HasAmulet = true
}
// Notify the user
if !silent {
if !g.Options.Terse {
g.addmsg("you now have ")
}
g.msg("%s (%c)", g.invName(obj, !g.Options.Terse), obj.PackCh)
}
}
// packRoom sees if there's room in the pack; if not, prints an appropriate
// message (pack.c pack_room).
func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
p := &g.Player
if p.Inpack++; p.Inpack > MaxPack {
if !g.Options.Terse {
g.addmsg("there's ")
}
g.addmsg("no room")
if !g.Options.Terse {
g.addmsg(" in your pack")
}
g.endmsg()
if fromFloor {
g.moveMsg(obj)
}
p.Inpack = MaxPack
return false
}
if fromFloor {
detachObj(&g.Level.Objects, obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
}
}
return true
}
// leavePack takes an item out of the pack (pack.c leave_pack).
func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object {
p := &g.Player
p.Inpack--
nobj := obj
if obj.Count > 1 && !all {
g.LastPick = obj
obj.Count--
if obj.Group != 0 {
p.Inpack++
}
if newobj {
copied := *obj
nobj = &copied
nobj.Count = 1
}
} else {
g.LastPick = nil
p.PackUsed[obj.PackCh-'a'] = false
detachObj(&p.Pack, obj)
}
return nobj
}
// packChar returns the next unused pack character (pack.c pack_char).
func (g *RogueGame) packChar() byte {
p := &g.Player
for i := range p.PackUsed {
if !p.PackUsed[i] {
p.PackUsed[i] = true
return byte(i) + 'a'
}
}
return byte(len(p.PackUsed)) + 'a' // C would walk off the array here
}
// inventory lists what is in the pack; returns true if there is something
// of the given type (pack.c inventory).
func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
g.NObjs = 0
for _, item := range list {
if kind != KindNone && kind != item.Kind &&
!(kind == KindCallable && item.Kind != KindFood &&
item.Kind != KindAmulet) &&
!(kind == KindRingOrStick &&
(item.Kind == KindRing || item.Kind == KindWand)) {
continue
}
g.NObjs++
g.Msgs.MsgEsc = true
line := string(item.PackCh) + ") " + g.invName(item, false)
if g.addLine("%s", line) == Escape {
g.Msgs.MsgEsc = false
g.msg("")
return true
}
g.Msgs.MsgEsc = false
}
if g.NObjs == 0 {
if g.Options.Terse {
if kind == KindNone {
g.msg("empty handed")
} else {
g.msg("nothing appropriate")
}
} else {
if kind == KindNone {
g.msg("you are empty handed")
} else {
g.msg("you don't have anything appropriate")
}
}
return false
}
g.endLine()
return true
}
// pickUp adds something to the character's pack (pack.c pick_up).
func (g *RogueGame) pickUp(ch byte) {
p := &g.Player
if p.On(Levitating) {
return
}
obj := g.findObj(p.Pos.Y, p.Pos.X)
if g.MoveOn {
g.moveMsg(obj)
return
}
switch ch {
case Gold:
if obj == nil {
return
}
g.money(obj.GoldVal())
detachObj(&g.Level.Objects, obj)
p.Room.GoldVal = 0
default:
g.addPack(nil, false)
}
}
// moveMsg prints the message if you are just moving onto an object
// (pack.c move_msg).
func (g *RogueGame) moveMsg(obj *Object) {
if !g.Options.Terse {
g.addmsg("you ")
}
g.msg("moved onto %s", g.invName(obj, true))
}
// pickyInven allows the player to inventory a single item (pack.c
// picky_inven).
func (g *RogueGame) pickyInven() {
p := &g.Player
if len(p.Pack) == 0 {
g.msg("you aren't carrying anything")
return
}
if len(p.Pack) == 1 {
g.msg("a) %s", g.invName(p.Pack[0], false))
return
}
g.msg("%s", g.chooseTerse("item: ", "which item do you wish to inventory: "))
g.Msgs.Mpos = 0
mch := g.readchar()
if mch == Escape {
g.msg("")
return
}
for _, obj := range p.Pack {
if mch == obj.PackCh {
g.msg("%c) %s", mch, g.invName(obj, false))
return
}
}
g.msg("'%s' not in pack", unctrl(mch))
}
// getItem picks something out of a pack for a purpose (pack.c get_item).
func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
p := &g.Player
if len(p.Pack) == 0 {
g.msg("you aren't carrying anything")
return nil
}
if g.Again {
if g.LastPick != nil {
return g.LastPick
}
g.msg("you ran out")
return nil
}
for {
if !g.Options.Terse {
g.addmsg("which object do you want to ")
}
g.addmsg("%s", purpose)
if g.Options.Terse {
g.addmsg(" what")
}
g.msg("? (* for list): ")
ch := g.readchar()
g.Msgs.Mpos = 0
// Give the poor player a chance to abort the command
if ch == Escape {
g.resetLast()
g.After = false
g.msg("")
return nil
}
g.NObjs = 1 // normal case: person types one char
if ch == '*' {
g.Msgs.Mpos = 0
if !g.inventory(p.Pack, kind) {
g.After = false
return nil
}
continue
}
for _, obj := range p.Pack {
if obj.PackCh == ch {
return obj
}
}
g.msg("'%s' is not a valid item", unctrl(ch))
}
}
// money adds or subtracts gold from the pack (pack.c money).
func (g *RogueGame) money(value int) {
p := &g.Player
p.Purse += value
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
}
if value > 0 {
if !g.Options.Terse {
g.addmsg("you found ")
}
g.msg("%d gold pieces", value)
}
}
// floorCh returns the appropriate floor character for her room
// (pack.c floor_ch).
func (g *RogueGame) floorCh() byte {
if g.Player.Room.Flags.Has(Gone) {
return Passage
}
if g.showFloor() {
return Floor
}
return ' '
}
// floorAt returns the character at the hero's position, taking see_floor
// into account (pack.c floor_at).
func (g *RogueGame) floorAt() byte {
ch := g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X)
if ch == Floor {
ch = g.floorCh()
}
return ch
}
// resetLast resets the last command when the current one is aborted
// (pack.c reset_last).
func (g *RogueGame) resetLast() {
g.LastComm = g.LLastComm
g.LastDir = g.LLastDir
g.LastPick = g.LLastPick
}
// chooseTerse picks the terse or verbose variant of a message.
func (g *RogueGame) chooseTerse(terse, verbose string) string {
if g.Options.Terse {
return terse
}
return verbose
}