Refactor step 6. MessageLine (was MsgLine) owns the msg/addmsg/endmsg machinery, wired to its screen, pre---More-- redraw, and input via attach(); RogueGame keeps one-line msg/addmsgf/endmsg shorthands so the ~400 call sites are unchanged. Player gains nextPackChar and removeFromPack (the state half of pack.c leave_pack); leavePack keeps only the LastPick repeat-command tracking. Level gains ObjectAt (misc.c find_obj) and AddObject/RemoveObject/AddMonster/RemoveMonster, replacing direct attach/detach calls on the level lists. Inventory and pickup UI flows stay on RogueGame: display and orchestration, not state surgery. Behavior and RNG order unchanged; suite green.
453 lines
10 KiB
Go
453 lines
10 KiB
Go
package game
|
|
|
|
// chase.c — code for one creature to chase another.
|
|
|
|
// dragonShot: one chance in DRAGONSHOT that a dragon will flame.
|
|
const dragonShot = 5
|
|
|
|
// runners makes all the running monsters move (chase.c runners).
|
|
func (g *RogueGame) runners(int) {
|
|
list := append([]*Monster(nil), g.Level.Monsters...)
|
|
for _, tp := range list {
|
|
if !tp.On(Held) && tp.On(Awake) {
|
|
origPos := tp.Pos
|
|
|
|
wastarget := tp.On(Targeted)
|
|
if removed := g.moveMonster(tp); removed {
|
|
continue
|
|
}
|
|
|
|
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
|
|
if removed := g.moveMonster(tp); removed {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if wastarget && origPos != tp.Pos {
|
|
tp.Flags.Clear(Targeted)
|
|
|
|
g.ToDeath = false
|
|
}
|
|
}
|
|
}
|
|
|
|
if g.HasHit {
|
|
g.endmsg()
|
|
g.HasHit = false
|
|
}
|
|
}
|
|
|
|
// moveMonster executes a single turn of running for a monster (chase.c
|
|
// move_monst). The result reports that the monster died or left the
|
|
// level (the C -1 return).
|
|
func (g *RogueGame) moveMonster(tp *Monster) bool {
|
|
if !tp.On(Slowed) || tp.Turn {
|
|
if g.chaseStep(tp) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
if tp.On(Hasted) {
|
|
if g.chaseStep(tp) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
tp.Turn = !tp.Turn
|
|
|
|
return false
|
|
}
|
|
|
|
// relocate makes the monster's new location be the specified one, updating
|
|
// all the relevant state (chase.c relocate).
|
|
func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
|
|
if newLoc != th.Pos {
|
|
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
|
|
th.Room = g.roomIn(newLoc)
|
|
g.setOldChar(th, newLoc)
|
|
oroom := th.Room
|
|
g.Level.SetMonsterAt(th.Pos.Y, th.Pos.X, nil)
|
|
|
|
if oroom != th.Room {
|
|
th.Dest = g.findDest(th)
|
|
}
|
|
|
|
th.Pos = newLoc
|
|
g.Level.SetMonsterAt(newLoc.Y, newLoc.X, th)
|
|
}
|
|
|
|
g.move(newLoc.Y, newLoc.X)
|
|
|
|
if g.seeMonst(th) {
|
|
g.addch(th.Disguise)
|
|
} else if g.Player.On(SenseMonsters) {
|
|
g.standout()
|
|
g.addch(th.Type)
|
|
g.standend()
|
|
}
|
|
}
|
|
|
|
// chaseStep makes one thing chase another (chase.c do_chase). removed
|
|
// reports that the chaser died or left the level in the attempt (the C
|
|
// -1 return).
|
|
func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
|
|
p := &g.Player
|
|
stoprun := false // true means we are there
|
|
mindist := 32767
|
|
|
|
rer := th.Room // find room of chaser
|
|
if th.On(Greedy) && rer.GoldVal == 0 {
|
|
th.Dest = &p.Pos // if gold has been taken, run after hero
|
|
}
|
|
|
|
var ree *Room // find room of chasee
|
|
if th.Dest == &p.Pos {
|
|
ree = p.Room
|
|
} else {
|
|
ree = g.roomIn(*th.Dest)
|
|
}
|
|
// We don't count doors as inside rooms for this routine
|
|
door := g.Level.Char(th.Pos.Y, th.Pos.X) == Door
|
|
|
|
var this Coord
|
|
|
|
for {
|
|
// If the object of our desire is in a different room, and we are
|
|
// not in a corridor, run to the door nearest to our goal.
|
|
if rer != ree {
|
|
for i := range rer.Exits {
|
|
curdist := distCp(*th.Dest, rer.Exits[i])
|
|
if curdist < mindist {
|
|
this = rer.Exits[i]
|
|
mindist = curdist
|
|
}
|
|
}
|
|
|
|
if door {
|
|
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum]
|
|
door = false
|
|
|
|
continue // the C goto over: redo with the passage as room
|
|
}
|
|
} else {
|
|
this = *th.Dest
|
|
// For dragons check and see if (a) the hero is on a straight
|
|
// line from it, and (b) that it is within shooting distance,
|
|
// but outside of striking range.
|
|
if th.Type == 'D' && (th.Pos.Y == p.Pos.Y || th.Pos.X == p.Pos.X ||
|
|
abs(th.Pos.Y-p.Pos.Y) == abs(th.Pos.X-p.Pos.X)) &&
|
|
distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
|
|
!th.On(Cancelled) && g.rnd(dragonShot) == 0 {
|
|
g.Delta.Y = sign(p.Pos.Y - th.Pos.Y)
|
|
|
|
g.Delta.X = sign(p.Pos.X - th.Pos.X)
|
|
if g.HasHit {
|
|
g.endmsg()
|
|
}
|
|
|
|
g.fireBolt(th.Pos, &g.Delta, "flame")
|
|
g.Running = false
|
|
g.Count = 0
|
|
|
|
g.Quiet = 0
|
|
if g.ToDeath && !th.On(Targeted) {
|
|
g.ToDeath = false
|
|
g.Kamikaze = false
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
// This now contains what we want to run to this time so we run to it.
|
|
// If we hit it we either want to fight it or stop running
|
|
if !g.chase(th, this) {
|
|
if this == p.Pos {
|
|
return g.attack(th)
|
|
} else if this == *th.Dest {
|
|
for _, obj := range g.Level.Objects {
|
|
if th.Dest == &obj.Pos {
|
|
g.Level.RemoveObject(obj)
|
|
attachObj(&th.Pack, obj)
|
|
|
|
if th.Room.Flags.Has(Gone) {
|
|
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage)
|
|
} else {
|
|
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor)
|
|
}
|
|
|
|
th.Dest = g.findDest(th)
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if th.Type != 'F' {
|
|
stoprun = true
|
|
}
|
|
}
|
|
} else {
|
|
if th.Type == 'F' {
|
|
return false
|
|
}
|
|
}
|
|
|
|
g.relocate(th, g.chRet)
|
|
// And stop running if need be
|
|
if stoprun && th.Pos == *th.Dest {
|
|
th.Flags.Clear(Awake)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// chase finds the spot for the chaser to move closer to the chasee
|
|
// (chase.c chase). Returns true if we want to keep on chasing later, false
|
|
// if we reach the goal. The chosen spot lands in g.chRet.
|
|
func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
|
|
p := &g.Player
|
|
er := tp.Pos
|
|
plcnt := 1
|
|
|
|
var curdist int
|
|
|
|
// If the thing is confused, let it move randomly. Invisible Stalkers
|
|
// are slightly confused all of the time, and bats are quite confused
|
|
// all the time
|
|
if (tp.On(Confused) && g.rnd(5) != 0) || (tp.Type == 'P' && g.rnd(5) == 0) ||
|
|
(tp.Type == 'B' && g.rnd(2) == 0) {
|
|
// get a valid random move
|
|
g.chRet = g.randomStep(&tp.Creature)
|
|
curdist = distCp(g.chRet, ee)
|
|
// Small chance that it will become un-confused
|
|
if g.rnd(20) == 0 {
|
|
tp.Flags.Clear(Confused)
|
|
}
|
|
} else {
|
|
// Otherwise, find the empty spot next to the chaser that is
|
|
// closest to the chasee. This will eventually hold where we move
|
|
// to get closer. If we can't find an empty spot, we stay where we
|
|
// are.
|
|
curdist = distCp(er, ee)
|
|
g.chRet = er
|
|
|
|
ey := er.Y + 1
|
|
if ey >= NumLines-1 {
|
|
ey = NumLines - 2
|
|
}
|
|
|
|
ex := er.X + 1
|
|
if ex >= NumCols {
|
|
ex = NumCols - 1
|
|
}
|
|
|
|
for x := er.X - 1; x <= ex; x++ {
|
|
if x < 0 {
|
|
continue
|
|
}
|
|
|
|
for y := er.Y - 1; y <= ey; y++ {
|
|
tryp := Coord{X: x, Y: y}
|
|
if !g.diagOk(er, tryp) {
|
|
continue
|
|
}
|
|
|
|
ch := g.Level.VisibleChar(y, x)
|
|
if stepOk(ch) {
|
|
// If it is a scroll, it might be a scare monster
|
|
// scroll so we need to look it up to see what type
|
|
// it is.
|
|
if ch == Scroll {
|
|
var found *Object
|
|
|
|
for _, obj := range g.Level.Objects {
|
|
if y == obj.Pos.Y && x == obj.Pos.X {
|
|
found = obj
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if found != nil && found.ScrollKind() == ScrollScareMonster {
|
|
continue
|
|
}
|
|
}
|
|
// It can also be a Xeroc, which we shouldn't step on
|
|
if m := g.Level.MonsterAt(y, x); m != nil && m.Type == 'X' {
|
|
continue
|
|
}
|
|
// If we didn't find any scrolls at this place or it
|
|
// wasn't a scare scroll, then this place counts
|
|
thisdist := distance(y, x, ee.Y, ee.X)
|
|
if thisdist < curdist {
|
|
plcnt = 1
|
|
g.chRet = tryp
|
|
curdist = thisdist
|
|
} else if thisdist == curdist {
|
|
if plcnt++; g.rnd(plcnt) == 0 {
|
|
g.chRet = tryp
|
|
curdist = thisdist
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return curdist != 0 && g.chRet != p.Pos
|
|
}
|
|
|
|
// setOldChar sets the oldch character for the monster (chase.c set_oldch).
|
|
func (g *RogueGame) setOldChar(tp *Monster, cp Coord) {
|
|
if tp.Pos == cp {
|
|
return
|
|
}
|
|
|
|
sch := tp.OldCh
|
|
|
|
tp.OldCh = g.mvinch(cp.Y, cp.X)
|
|
if !g.Player.On(Blind) {
|
|
if (sch == Floor || tp.OldCh == Floor) && tp.Room.Flags.Has(Dark) {
|
|
tp.OldCh = ' '
|
|
} else if distCp(cp, g.Player.Pos) <= LampDist && g.Options.SeeFloor {
|
|
tp.OldCh = g.Level.Char(cp.Y, cp.X)
|
|
}
|
|
}
|
|
}
|
|
|
|
// seeMonst returns true if the hero can see the monster (chase.c
|
|
// see_monst).
|
|
func (g *RogueGame) seeMonst(mp *Monster) bool {
|
|
p := &g.Player
|
|
if p.On(Blind) {
|
|
return false
|
|
}
|
|
|
|
if mp.On(Invisible) && !p.On(CanSeeInvisible) {
|
|
return false
|
|
}
|
|
|
|
y, x := mp.Pos.Y, mp.Pos.X
|
|
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
|
if y != p.Pos.Y && x != p.Pos.X &&
|
|
!stepOk(g.Level.Char(y, p.Pos.X)) && !stepOk(g.Level.Char(p.Pos.Y, x)) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
if mp.Room != p.Room {
|
|
return false
|
|
}
|
|
|
|
return !mp.Room.Flags.Has(Dark)
|
|
}
|
|
|
|
// runTo sets a monster running after the hero (chase.c runto).
|
|
func (g *RogueGame) runTo(runner Coord) {
|
|
tp := g.Level.MonsterAt(runner.Y, runner.X)
|
|
if tp == nil {
|
|
return
|
|
}
|
|
// Start the beastie running
|
|
tp.Flags.Set(Awake)
|
|
tp.Flags.Clear(Held)
|
|
tp.Dest = g.findDest(tp)
|
|
}
|
|
|
|
// roomIn finds what room some coordinates are in; nil means they aren't in
|
|
// any room (chase.c roomin).
|
|
func (g *RogueGame) roomIn(cp Coord) *Room {
|
|
fp := *g.Level.FlagsAt(cp.Y, cp.X)
|
|
if fp.Has(FPassage) {
|
|
return &g.Level.Passages[fp&FPassNum]
|
|
}
|
|
|
|
for i := range g.Level.Rooms {
|
|
rp := &g.Level.Rooms[i]
|
|
if cp.X <= rp.Pos.X+rp.Max.X && rp.Pos.X <= cp.X &&
|
|
cp.Y <= rp.Pos.Y+rp.Max.Y && rp.Pos.Y <= cp.Y {
|
|
return rp
|
|
}
|
|
}
|
|
|
|
g.msg("in some bizarre place (%d, %d)", cp.Y, cp.X)
|
|
|
|
return nil
|
|
}
|
|
|
|
// diagOk checks to see if a diagonal move is legal (chase.c diag_ok).
|
|
func (g *RogueGame) diagOk(sp, ep Coord) bool {
|
|
if ep.X < 0 || ep.X >= NumCols || ep.Y <= 0 || ep.Y >= NumLines-1 {
|
|
return false
|
|
}
|
|
|
|
if ep.X == sp.X || ep.Y == sp.Y {
|
|
return true
|
|
}
|
|
|
|
return stepOk(g.Level.Char(ep.Y, sp.X)) && stepOk(g.Level.Char(sp.Y, ep.X))
|
|
}
|
|
|
|
// canSee returns true if the hero can see a certain coordinate (chase.c
|
|
// cansee).
|
|
func (g *RogueGame) canSee(y, x int) bool {
|
|
p := &g.Player
|
|
if p.On(Blind) {
|
|
return false
|
|
}
|
|
|
|
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
|
if g.Level.FlagsAt(y, x).Has(FPassage) {
|
|
if y != p.Pos.Y && x != p.Pos.X &&
|
|
!stepOk(g.Level.Char(y, p.Pos.X)) &&
|
|
!stepOk(g.Level.Char(p.Pos.Y, x)) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
// We can only see if the hero is in the same room as the coordinate
|
|
// and the room is lit, or if it is close.
|
|
rer := g.roomIn(Coord{X: x, Y: y})
|
|
|
|
return rer == p.Room && !rer.Flags.Has(Dark)
|
|
}
|
|
|
|
// findDest finds the proper destination for the monster (chase.c
|
|
// find_dest).
|
|
func (g *RogueGame) findDest(tp *Monster) *Coord {
|
|
prob := g.Monsters[tp.Type-'A'].Carry
|
|
if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) {
|
|
return &g.Player.Pos
|
|
}
|
|
|
|
for _, obj := range g.Level.Objects {
|
|
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster {
|
|
continue
|
|
}
|
|
|
|
if g.roomIn(obj.Pos) == tp.Room && g.rnd(100) < prob {
|
|
claimed := false
|
|
|
|
for _, other := range g.Level.Monsters {
|
|
if other.Dest == &obj.Pos {
|
|
claimed = true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !claimed {
|
|
return &obj.Pos
|
|
}
|
|
}
|
|
}
|
|
|
|
return &g.Player.Pos
|
|
}
|