diff --git a/game/misc.go b/game/misc.go index f9ad13f..75a01df 100644 --- a/game/misc.go +++ b/game/misc.go @@ -4,11 +4,23 @@ package game // 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 - passcount := 0 rp := p.Room if g.Oldpos != hero { @@ -17,151 +29,28 @@ func (g *RogueGame) look(wakeup bool) { g.Oldrp = rp } - ey := hero.Y + 1 - ex := hero.X + 1 - sx := hero.X - 1 - sy := hero.Y - 1 + s := lookScan{ + hero: hero, + wakeup: wakeup, + sy: hero.Y - 1, + sx: hero.X - 1, + ey: hero.Y + 1, + ex: hero.X + 1, + } - sumhero, diffhero := 0, 0 - if g.DoorStop && !g.Firstmove && g.Running { - sumhero = hero.Y + hero.X - diffhero = hero.Y - hero.X + 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) - pch := pp.Ch - pfl := pp.Flags + s.pch = pp.Ch + s.pfl = pp.Flags - for y := sy; y <= ey; y++ { - if y <= 0 || y >= NumLines-1 { - continue - } + g.lookAround(&s) - for x := sx; x <= ex; x++ { - if x < 0 || x >= NumCols { - continue - } - - if !p.On(Blind) { - if y == hero.Y && x == hero.X { - continue - } - } - - pp := g.Level.At(y, x) - - ch := pp.Ch - if ch == ' ' { // nothing need be done with a ' ' - continue - } - - fp := &pp.Flags - if pch != Door && ch != Door { - if (pfl & FPassage) != (*fp & FPassage) { - continue - } - } - - if (fp.Has(FPassage) || ch == Door) && (pfl.Has(FPassage) || pch == Door) { - if hero.X != x && hero.Y != y && - !stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) { - continue - } - } - - tp := pp.Monst - - switch { - case tp == nil: - ch = g.tripCh(y, x, ch) - case p.On(SenseMonsters) && tp.On(Invisible): - if g.DoorStop && !g.Firstmove { - g.Running = false - } - - continue - default: - if wakeup { - g.wakeMonster(y, x) - } - - if g.seeMonst(tp) { - if p.On(Hallucinating) { - ch = g.randomMonsterLetter() - } else { - ch = tp.Disguise - } - } - } - - if p.On(Blind) && (y != hero.Y || x != hero.X) { - continue - } - - 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) - } - - if g.DoorStop && !g.Firstmove && g.Running { - switch g.RunCh { - case 'h': - if x == ex { - continue - } - case 'j': - if y == sy { - continue - } - case 'k': - if y == ey { - continue - } - case 'l': - if x == sx { - continue - } - case 'y': - if (y+x)-sumhero >= 1 { - continue - } - case 'u': - if (y-x)-diffhero >= 1 { - continue - } - case 'n': - if (y+x)-sumhero <= -1 { - continue - } - case 'b': - if (y-x)-diffhero <= -1 { - continue - } - } - - switch ch { - case Door: - if x == hero.X || y == hero.Y { - g.Running = false - } - case Passage: - if x == hero.X || y == hero.Y { - passcount++ - } - case Floor, '|', '-', ' ': - default: - g.Running = false - } - } - } - } - - if g.DoorStop && !g.Firstmove && passcount > 1 { + if s.doorStop && s.passcount > 1 { g.Running = false } @@ -170,6 +59,192 @@ func (g *RogueGame) look(wakeup bool) { } } +// 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 { @@ -409,40 +484,22 @@ func (g *RogueGame) promptDirection() bool { } for { - gotit := true - - switch g.DirCh = g.readchar(); g.DirCh { - case 'h', 'H': - g.Delta = Coord{X: -1, Y: 0} - case 'j', 'J': - g.Delta = Coord{X: 0, Y: 1} - case 'k', 'K': - g.Delta = Coord{X: 0, Y: -1} - case 'l', 'L': - g.Delta = Coord{X: 1, Y: 0} - case 'y', 'Y': - g.Delta = Coord{X: -1, Y: -1} - case 'u', 'U': - g.Delta = Coord{X: 1, Y: -1} - case 'b', 'B': - g.Delta = Coord{X: -1, Y: 1} - case 'n', 'N': - g.Delta = Coord{X: 1, Y: 1} - case Escape: + g.DirCh = g.readchar() + if g.DirCh == Escape { g.LastDir = 0 g.resetLast() return false - default: - g.Msgs.Mpos = 0 - g.msg("%s", prompt) - - gotit = false } - if gotit { + if d, ok := deltaFor(g.DirCh); ok { + g.Delta = d + break } + + g.Msgs.Mpos = 0 + g.msg("%s", prompt) } g.DirCh = toLower(g.DirCh) @@ -451,14 +508,7 @@ func (g *RogueGame) promptDirection() bool { } if g.Player.On(Confused) && g.rnd(5) == 0 { - for { - g.Delta.Y = g.rnd(3) - 1 - - g.Delta.X = g.rnd(3) - 1 - if g.Delta.Y != 0 || g.Delta.X != 0 { - break - } - } + g.confuseDirection() } g.Msgs.Mpos = 0 @@ -466,6 +516,44 @@ func (g *RogueGame) promptDirection() bool { 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 {