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) }