look's nine-square scan splits into lookAround/lookCell with a lookScan state struct and guard helpers (lookSkips, lookForeignPassage, lookDiagonalBlocked, lookCellChar, lookShow, lookRunCheck, atRunEdge); promptDirection gains deltaFor and confuseDirection. misc.go is complexity-clean. Behavior and RNG call order unchanged.
609 lines
13 KiB
Go
609 lines
13 KiB
Go
package game
|
|
|
|
// misc.c — look() display maintenance, direction input, eating, level-ups,
|
|
// and small utilities. call_it arrives with the scroll/potion phase (it
|
|
// needs the get_str line editor).
|
|
|
|
// lookScan carries the state of one look() glance while it examines the
|
|
// nine squares around the hero.
|
|
type lookScan struct {
|
|
hero Coord
|
|
pch byte // map character under the hero
|
|
pfl PlaceFlags // map flags under the hero
|
|
wakeup bool
|
|
doorStop bool // door-stop checking applies (mid-run)
|
|
sy, sx, ey, ex int
|
|
sumhero, diffhero int
|
|
passcount int
|
|
}
|
|
|
|
// look takes a quick glance all around the player (misc.c look).
|
|
func (g *RogueGame) look(wakeup bool) {
|
|
p := &g.Player
|
|
hero := p.Pos
|
|
rp := p.Room
|
|
|
|
if g.Oldpos != hero {
|
|
g.eraseLamp(g.Oldpos, g.Oldrp)
|
|
g.Oldpos = hero
|
|
g.Oldrp = rp
|
|
}
|
|
|
|
s := lookScan{
|
|
hero: hero,
|
|
wakeup: wakeup,
|
|
sy: hero.Y - 1,
|
|
sx: hero.X - 1,
|
|
ey: hero.Y + 1,
|
|
ex: hero.X + 1,
|
|
}
|
|
|
|
s.doorStop = g.DoorStop && !g.Firstmove
|
|
if s.doorStop && g.Running {
|
|
s.sumhero = hero.Y + hero.X
|
|
s.diffhero = hero.Y - hero.X
|
|
}
|
|
|
|
pp := g.Level.At(hero.Y, hero.X)
|
|
s.pch = pp.Ch
|
|
s.pfl = pp.Flags
|
|
|
|
g.lookAround(&s)
|
|
|
|
if s.doorStop && s.passcount > 1 {
|
|
g.Running = false
|
|
}
|
|
|
|
if !g.Running || !g.Options.Jump {
|
|
g.mvaddch(hero.Y, hero.X, PlayerCh)
|
|
}
|
|
}
|
|
|
|
// lookAround runs the nine-square scan of look().
|
|
func (g *RogueGame) lookAround(s *lookScan) {
|
|
for y := s.sy; y <= s.ey; y++ {
|
|
if y <= 0 || y >= NumLines-1 {
|
|
continue
|
|
}
|
|
|
|
for x := s.sx; x <= s.ex; x++ {
|
|
if x < 0 || x >= NumCols {
|
|
continue
|
|
}
|
|
|
|
g.lookCell(s, y, x)
|
|
}
|
|
}
|
|
}
|
|
|
|
// lookCell examines one square around the hero: visibility rules, trip
|
|
// and monster rendering, drawing, and run-stop checks (the loop body of
|
|
// misc.c look).
|
|
func (g *RogueGame) lookCell(s *lookScan, y, x int) {
|
|
pp := g.Level.At(y, x)
|
|
if g.lookSkips(s, pp, y, x) {
|
|
return
|
|
}
|
|
|
|
tp := pp.Monst
|
|
|
|
ch, skip := g.lookCellChar(s, tp, y, x, pp.Ch)
|
|
if skip {
|
|
return
|
|
}
|
|
|
|
if !g.lookShow(s, tp, ch, y, x) {
|
|
return
|
|
}
|
|
|
|
if s.doorStop && g.Running {
|
|
g.lookRunCheck(s, ch, y, x)
|
|
}
|
|
}
|
|
|
|
// lookSkips reports whether look ignores this square entirely: the
|
|
// hero's own square when sighted, blank rock, passage squares of
|
|
// another network, and diagonals the hero could not step to (the guard
|
|
// chain of the misc.c look loop).
|
|
func (g *RogueGame) lookSkips(s *lookScan, pp *Place, y, x int) bool {
|
|
if !g.Player.On(Blind) && y == s.hero.Y && x == s.hero.X {
|
|
return true
|
|
}
|
|
|
|
if pp.Ch == ' ' { // nothing need be done with a ' '
|
|
return true
|
|
}
|
|
|
|
return lookForeignPassage(s, pp.Flags, pp.Ch) ||
|
|
g.lookDiagonalBlocked(s, pp.Flags, pp.Ch, y, x)
|
|
}
|
|
|
|
// lookForeignPassage hides passage squares belonging to a different
|
|
// passage network than the hero's (misc.c look).
|
|
func lookForeignPassage(s *lookScan, fp PlaceFlags, ch byte) bool {
|
|
if s.pch != Door && ch != Door {
|
|
return (s.pfl & FPassage) != (fp & FPassage)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// lookDiagonalBlocked hides diagonal door/passage squares the hero could
|
|
// not actually step to (misc.c look).
|
|
func (g *RogueGame) lookDiagonalBlocked(s *lookScan, fp PlaceFlags, ch byte, y, x int) bool {
|
|
if !fp.Has(FPassage) && ch != Door {
|
|
return false
|
|
}
|
|
|
|
if !s.pfl.Has(FPassage) && s.pch != Door {
|
|
return false
|
|
}
|
|
|
|
return s.hero.X != x && s.hero.Y != y &&
|
|
!stepOk(g.Level.Char(y, s.hero.X)) && !stepOk(g.Level.Char(s.hero.Y, x))
|
|
}
|
|
|
|
// lookShow draws the square if it changed; it reports false when a
|
|
// blind hero cannot see it at all (the draw part of the look loop).
|
|
func (g *RogueGame) lookShow(s *lookScan, tp *Monster, ch byte, y, x int) bool {
|
|
p := &g.Player
|
|
if p.On(Blind) && (y != s.hero.Y || x != s.hero.X) {
|
|
return false
|
|
}
|
|
|
|
g.move(y, x)
|
|
|
|
if p.Room.Flags.Has(Dark) && !g.Options.SeeFloor && ch == Floor {
|
|
ch = ' '
|
|
}
|
|
|
|
if tp != nil || ch != g.inch() {
|
|
g.addch(ch)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// lookCellChar picks what the square shows: trip rendering for empty
|
|
// squares, waking and disguises for monsters. skip means the square is
|
|
// not drawn at all (the monster switch of the look loop).
|
|
func (g *RogueGame) lookCellChar(s *lookScan, tp *Monster, y, x int, ch byte) (byte, bool) {
|
|
p := &g.Player
|
|
|
|
switch {
|
|
case tp == nil:
|
|
return g.tripCh(y, x, ch), false
|
|
case p.On(SenseMonsters) && tp.On(Invisible):
|
|
if g.DoorStop && !g.Firstmove {
|
|
g.Running = false
|
|
}
|
|
|
|
return ch, true
|
|
default:
|
|
if s.wakeup {
|
|
g.wakeMonster(y, x)
|
|
}
|
|
|
|
if g.seeMonst(tp) {
|
|
if p.On(Hallucinating) {
|
|
return g.randomMonsterLetter(), false
|
|
}
|
|
|
|
return tp.Disguise, false
|
|
}
|
|
|
|
return ch, false
|
|
}
|
|
}
|
|
|
|
// lookRunCheck decides whether what this square shows should stop a run
|
|
// (the DoorStop tail of the misc.c look loop). Squares on the running
|
|
// edge are ignored.
|
|
func (g *RogueGame) lookRunCheck(s *lookScan, ch byte, y, x int) {
|
|
if s.atRunEdge(g.RunCh, y, x) {
|
|
return
|
|
}
|
|
|
|
switch ch {
|
|
case Door:
|
|
if x == s.hero.X || y == s.hero.Y {
|
|
g.Running = false
|
|
}
|
|
case Passage:
|
|
if x == s.hero.X || y == s.hero.Y {
|
|
s.passcount++
|
|
}
|
|
case Floor, '|', '-', ' ':
|
|
default:
|
|
g.Running = false
|
|
}
|
|
}
|
|
|
|
// atRunEdge reports whether (y, x) sits on the leading edge of the run
|
|
// direction, where door-stop checking does not apply (the first RunCh
|
|
// switch of the misc.c look loop).
|
|
func (s *lookScan) atRunEdge(runCh byte, y, x int) bool {
|
|
switch runCh {
|
|
case 'h':
|
|
return x == s.ex
|
|
case 'j':
|
|
return y == s.sy
|
|
case 'k':
|
|
return y == s.ey
|
|
case 'l':
|
|
return x == s.sx
|
|
case 'y':
|
|
return (y+x)-s.sumhero >= 1
|
|
case 'u':
|
|
return (y-x)-s.diffhero >= 1
|
|
case 'n':
|
|
return (y+x)-s.sumhero <= -1
|
|
case 'b':
|
|
return (y-x)-s.diffhero <= -1
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// tripCh returns the character for this space, taking into account whether
|
|
// or not the player is tripping (misc.c trip_ch).
|
|
func (g *RogueGame) tripCh(y, x int, ch byte) byte {
|
|
if g.Player.On(Hallucinating) && g.After {
|
|
switch ch {
|
|
case Floor, ' ', Passage, '-', '|', Door, Trap:
|
|
default:
|
|
if y != g.Level.Stairs.Y || x != g.Level.Stairs.X || !g.SeenStairs {
|
|
ch = g.rndThing()
|
|
}
|
|
}
|
|
}
|
|
|
|
return ch
|
|
}
|
|
|
|
// eraseLamp erases the area shown by a lamp in a dark room
|
|
// (misc.c erase_lamp).
|
|
func (g *RogueGame) eraseLamp(pos Coord, rp *Room) {
|
|
if !g.Options.SeeFloor || rp.Flags&(Gone|Dark) != Dark ||
|
|
g.Player.On(Blind) {
|
|
return
|
|
}
|
|
|
|
ey := pos.Y + 1
|
|
ex := pos.X + 1
|
|
|
|
sy := pos.Y - 1
|
|
for x := pos.X - 1; x <= ex; x++ {
|
|
for y := sy; y <= ey; y++ {
|
|
if y == g.Player.Pos.Y && x == g.Player.Pos.X {
|
|
continue
|
|
}
|
|
|
|
g.move(y, x)
|
|
|
|
if g.inch() == Floor {
|
|
g.addch(' ')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// showFloor reports whether we show the floor in her room at this time
|
|
// (misc.c show_floor).
|
|
func (g *RogueGame) showFloor() bool {
|
|
if g.Player.Room.Flags&(Gone|Dark) == Dark && !g.Player.On(Blind) {
|
|
return g.Options.SeeFloor
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// eat lets her try to eat something (misc.c eat).
|
|
func (g *RogueGame) eat() {
|
|
obj, ok := g.promptPackItem("eat", KindFood)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if obj.Kind != KindFood {
|
|
if !g.Options.Terse {
|
|
g.msg("ugh, you would get ill if you ate that")
|
|
} else {
|
|
g.msg("that's Inedible!")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
p := &g.Player
|
|
if p.FoodLeft < 0 {
|
|
p.FoodLeft = 0
|
|
}
|
|
|
|
if p.FoodLeft += HungerTime - 200 + g.rnd(400); p.FoodLeft > StomachSize {
|
|
p.FoodLeft = StomachSize
|
|
}
|
|
|
|
p.HungryState = 0
|
|
if obj == p.CurWeapon {
|
|
p.CurWeapon = nil
|
|
}
|
|
|
|
switch {
|
|
case obj.Which == 1:
|
|
g.msg("my, that was a yummy %s", g.Fruit)
|
|
case g.rnd(100) > 70:
|
|
p.Stats.Exp++
|
|
|
|
g.msg("%s, this food tastes awful", g.chooseStr("bummer", "yuk"))
|
|
g.checkLevel()
|
|
default:
|
|
g.msg("%s, that tasted good", g.chooseStr("oh, wow", "yum"))
|
|
}
|
|
|
|
g.leavePack(obj, false, false)
|
|
}
|
|
|
|
// checkLevel checks to see if the guy has gone up a level (misc.c
|
|
// check_level).
|
|
func (g *RogueGame) checkLevel() {
|
|
p := &g.Player
|
|
|
|
var i int
|
|
for i = 0; g.data.eLevels[i] != 0; i++ {
|
|
if g.data.eLevels[i] > p.Stats.Exp {
|
|
break
|
|
}
|
|
}
|
|
|
|
i++
|
|
olevel := p.Stats.Lvl
|
|
|
|
p.Stats.Lvl = i
|
|
if i > olevel {
|
|
add := g.roll(i-olevel, 10)
|
|
p.Stats.MaxHP += add
|
|
p.Stats.HP += add
|
|
|
|
g.msg("welcome to level %d", i)
|
|
}
|
|
}
|
|
|
|
// changeStrength modifies the player's strength, keeping track of the
|
|
// highest it has been (misc.c chg_str).
|
|
func (g *RogueGame) changeStrength(amt int) {
|
|
if amt == 0 {
|
|
return
|
|
}
|
|
|
|
p := &g.Player
|
|
addStr(&p.Stats.Str, amt)
|
|
|
|
comp := p.Stats.Str
|
|
if p.IsRing(Left, RingAddStrength) {
|
|
addStr(&comp, -p.CurRing[Left].Bonus)
|
|
}
|
|
|
|
if p.IsRing(Right, RingAddStrength) {
|
|
addStr(&comp, -p.CurRing[Right].Bonus)
|
|
}
|
|
|
|
if comp > p.MaxStats.Str {
|
|
p.MaxStats.Str = comp
|
|
}
|
|
}
|
|
|
|
// addStr performs the actual strength add, checking bounds (misc.c add_str).
|
|
func addStr(sp *int, amt int) {
|
|
if *sp += amt; *sp < 3 {
|
|
*sp = 3
|
|
} else if *sp > 31 {
|
|
*sp = 31
|
|
}
|
|
}
|
|
|
|
// addHaste adds a haste to the player (misc.c add_haste).
|
|
func (g *RogueGame) addHaste(potion bool) bool {
|
|
p := &g.Player
|
|
if p.On(Hasted) {
|
|
g.NoCommand += g.rnd(8)
|
|
|
|
p.Flags.Clear(Awake | Hasted)
|
|
g.Extinguish(DNohaste)
|
|
g.msg("you faint from exhaustion")
|
|
|
|
return false
|
|
}
|
|
|
|
p.Flags.Set(Hasted)
|
|
|
|
if potion {
|
|
g.Fuse(DNohaste, 0, g.rnd(4)+4, After)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// aggravate aggravates all the monsters on this level (misc.c aggravate).
|
|
func (g *RogueGame) aggravate() {
|
|
// runTo() can splice the monster list while we walk it, so iterate a copy.
|
|
monsters := append([]*Monster(nil), g.Level.Monsters...)
|
|
for _, mp := range monsters {
|
|
g.runTo(mp.Pos)
|
|
}
|
|
}
|
|
|
|
// vowelstr returns "n" if the string starts with a vowel, for "a"/"an"
|
|
// (misc.c vowelstr).
|
|
func vowelstr(str string) string {
|
|
if str == "" {
|
|
return ""
|
|
}
|
|
|
|
switch str[0] {
|
|
case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U':
|
|
return "n"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// isCurrent sees if the object is one of the currently used items
|
|
// (misc.c is_current).
|
|
func (g *RogueGame) isCurrent(obj *Object) bool {
|
|
if obj == nil {
|
|
return false
|
|
}
|
|
|
|
p := &g.Player
|
|
if obj == p.CurArmor || obj == p.CurWeapon ||
|
|
obj == p.CurRing[Left] || obj == p.CurRing[Right] {
|
|
if !g.Options.Terse {
|
|
g.addmsgf("That's already ")
|
|
}
|
|
|
|
g.msg("in use")
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// promptDirection sets up the direction coordinate for use in various
|
|
// "prefix" commands (misc.c get_dir).
|
|
func (g *RogueGame) promptDirection() bool {
|
|
if g.Again && g.LastDir != 0 {
|
|
g.Delta = g.lastDelt
|
|
g.DirCh = g.LastDir
|
|
} else {
|
|
prompt := "direction: "
|
|
if !g.Options.Terse {
|
|
prompt = "which direction? "
|
|
g.msg("%s", prompt)
|
|
}
|
|
|
|
for {
|
|
g.DirCh = g.readchar()
|
|
if g.DirCh == Escape {
|
|
g.LastDir = 0
|
|
g.resetLast()
|
|
|
|
return false
|
|
}
|
|
|
|
if d, ok := deltaFor(g.DirCh); ok {
|
|
g.Delta = d
|
|
|
|
break
|
|
}
|
|
|
|
g.Msgs.Mpos = 0
|
|
g.msg("%s", prompt)
|
|
}
|
|
|
|
g.DirCh = toLower(g.DirCh)
|
|
g.LastDir = g.DirCh
|
|
g.lastDelt = g.Delta
|
|
}
|
|
|
|
if g.Player.On(Confused) && g.rnd(5) == 0 {
|
|
g.confuseDirection()
|
|
}
|
|
|
|
g.Msgs.Mpos = 0
|
|
|
|
return true
|
|
}
|
|
|
|
// confuseDirection randomizes the chosen direction for a confused hero
|
|
// (the ISHUH tail of misc.c get_dir).
|
|
func (g *RogueGame) confuseDirection() {
|
|
for {
|
|
g.Delta.Y = g.rnd(3) - 1
|
|
|
|
g.Delta.X = g.rnd(3) - 1
|
|
if g.Delta.Y != 0 || g.Delta.X != 0 {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// deltaFor maps a direction key to its movement delta; ok is false for
|
|
// keys that are not directions (the switch of misc.c get_dir).
|
|
func deltaFor(ch byte) (Coord, bool) {
|
|
switch ch {
|
|
case 'h', 'H':
|
|
return Coord{X: -1, Y: 0}, true
|
|
case 'j', 'J':
|
|
return Coord{X: 0, Y: 1}, true
|
|
case 'k', 'K':
|
|
return Coord{X: 0, Y: -1}, true
|
|
case 'l', 'L':
|
|
return Coord{X: 1, Y: 0}, true
|
|
case 'y', 'Y':
|
|
return Coord{X: -1, Y: -1}, true
|
|
case 'u', 'U':
|
|
return Coord{X: 1, Y: -1}, true
|
|
case 'b', 'B':
|
|
return Coord{X: -1, Y: 1}, true
|
|
case 'n', 'N':
|
|
return Coord{X: 1, Y: 1}, true
|
|
}
|
|
|
|
return Coord{}, false
|
|
}
|
|
|
|
// callIt calls an object something after use (misc.c call_it).
|
|
func (g *RogueGame) callIt(info *ObjInfo) {
|
|
if info.Know {
|
|
info.Guess = ""
|
|
} else if info.Guess == "" {
|
|
g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? "))
|
|
|
|
buf := ""
|
|
if g.getStr(&buf, g.scr.Std) == Norm {
|
|
if buf != "" {
|
|
info.Guess = buf
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// rndThing picks a random thing appropriate for this level (misc.c
|
|
// rnd_thing).
|
|
func (g *RogueGame) rndThing() byte {
|
|
var i int
|
|
if g.Depth >= AmuletLevel {
|
|
i = g.rnd(len(g.data.thingList))
|
|
} else {
|
|
i = g.rnd(len(g.data.thingList) - 1)
|
|
}
|
|
|
|
return g.data.thingList[i]
|
|
}
|
|
|
|
// chooseStr picks the first or second string depending on whether the
|
|
// player is tripping (misc.c choose_str).
|
|
func (g *RogueGame) chooseStr(ts, ns string) string {
|
|
if g.Player.On(Hallucinating) {
|
|
return ts
|
|
}
|
|
|
|
return ns
|
|
}
|
|
|
|
// unctrl gives a printable representation of a character, like curses
|
|
// unctrl(): control characters display as ^X.
|
|
func unctrl(ch byte) string {
|
|
if ch < ' ' {
|
|
return "^" + string(ch+'@')
|
|
}
|
|
|
|
if ch == 0x7f {
|
|
return "^?"
|
|
}
|
|
|
|
return string(ch)
|
|
}
|