rooms.c, passages.c, new_level.c ported in full: room/maze layout, corridor spanning tree with extra connections, passage numbering flood fill, trap/stairs/object placement, treasure rooms. Careful RNG-call ordering throughout keeps generation seed-faithful to C. Supporting subsystems this depends on, also ported: - screen.go: curses replaced by in-memory Window cell buffers behind a Terminal interface (tcell arrives with the UI phase; tests run headless) - io.go: msg/addmsg/endmsg message-line protocol, status line, step_ok - pack.c in full (add_pack sorting/stacking walk, get_item, inventory) - things.c in full (inv_name, new_thing, discovery lists, pagination) - monsters.c in full; chase.c navigation half (roomin/cansee/see_monst/ find_dest/runto/set_oldch); rings.c, armor.c in full - weapons.c/sticks.c creation half (init_weapon, fix_stick, num) - misc.c look() display update, eat, level-up, direction input - daemons.c callbacks except stomach (needs death()) and the runners chase driver (combat phase) - init.c init_player with the starting kit Tests: level invariants across seeds, determinism, 30-depth sweep.
401 lines
8.7 KiB
Go
401 lines
8.7 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.Type == Scroll && obj.Which == SScare && obj.Flags.Has(ObjIsFound) {
|
|
detachObj(&g.Level.Objects, obj)
|
|
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
|
|
if p.Room.Flags.Has(IsGone) {
|
|
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].Type != obj.Type {
|
|
lp = i
|
|
continue
|
|
}
|
|
// found the group of our type: scan for matching subtype
|
|
for p.Pack[i].Type == obj.Type && p.Pack[i].Which != obj.Which {
|
|
lp = i
|
|
if i+1 >= len(p.Pack) {
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
op := p.Pack[i]
|
|
if op.Type == obj.Type && op.Which == obj.Which {
|
|
if IsMult(op.Type) {
|
|
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].Type == obj.Type &&
|
|
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.Type == obj.Type && 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(ObjIsFound)
|
|
|
|
// 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.Type == Amulet {
|
|
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(IsGone) {
|
|
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, typ int) bool {
|
|
g.NObjs = 0
|
|
for _, item := range list {
|
|
if typ != 0 && typ != int(item.Type) &&
|
|
!(typ == Callable && item.Type != Food && item.Type != Amulet) &&
|
|
!(typ == RorS && (item.Type == Ring || item.Type == Stick)) {
|
|
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 typ == 0 {
|
|
g.msg("empty handed")
|
|
} else {
|
|
g.msg("nothing appropriate")
|
|
}
|
|
} else {
|
|
if typ == 0 {
|
|
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(IsLevit) {
|
|
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, typ int) *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, typ) {
|
|
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(IsGone) {
|
|
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(IsGone) {
|
|
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
|
|
}
|