Files
rgoue/game/move.go
sneak 3c5add87cd go: port combat, chase driver, traps, zapping, death and scores
- fight.c in full: fight/attack with all eight special monster attacks
  (aquator rust, ice freeze, rattlesnake poison, wraith/vampire drains,
  flytrap hold, leprechaun/nymph theft), swing, roll_em dice parsing
  (with a C-semantics atoi — Go's strconv rejects '1x4/...'), hit/miss
  message variants, killed, remove_mon
- chase.c completed: runners, move_monst, relocate, do_chase with
  dragon breath, chase target selection (C's dead oroom comparison in
  relocate is preserved as-is)
- move.c in full: do_move with passgo turning, all eight traps,
  rndmove, rust_armor
- sticks.c completed: do_zap (all 14 sticks), drain, fire_bolt with
  wall bounces; weapons.c completed: missile/do_motion/fall/wield
- rip.c: death tombstone, total_winner, killname; C exit() becomes a
  gameEnd panic that Run will recover
- score.go: top-ten scoreboard as a gob file with lock-file protocol
  replacing the XOR-encrypted C format
- wizard.c: whatis/set_know/teleport; daemons.c stomach
- monster bestiary moved to per-game state (C mutates the flytrap
  damage string during play)

Tests: combat math, kill/removal bookkeeping, monster chase pursuit,
death unwinding, scripted-input headless terminal.
2026-07-06 19:23:45 +02:00

342 lines
8.0 KiB
Go

package game
// move.c — hero movement commands.
// doRun starts the hero running (move.c do_run).
func (g *RogueGame) doRun(ch byte) {
g.Running = true
g.After = false
g.RunCh = ch
}
// doMove checks that a move is legal and handles the consequences —
// fighting, picking up, etc. (move.c do_move).
func (g *RogueGame) doMove(dy, dx int) {
p := &g.Player
g.Firstmove = false
if g.NoMove > 0 {
g.NoMove--
g.msg("you are still stuck in the bear trap")
return
}
// Do a confused move (maybe)
var nh Coord
if p.On(IsHuh) && g.rnd(5) != 0 {
nh = g.rndmove(&p.Creature)
if nh == p.Pos {
g.After = false
g.Running = false
g.ToDeath = false
return
}
} else {
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
}
over:
// Check if he tried to move off the screen or make an illegal diagonal
// move, and stop him if he did.
hitBound := nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1
var ch byte
var fl PlaceFlags
if !hitBound {
if !g.diagOk(p.Pos, nh) {
g.After = false
g.Running = false
return
}
if g.Running && p.Pos == nh {
g.After = false
g.Running = false
}
fl = *g.Level.FlagsAt(nh.Y, nh.X)
ch = g.Level.VisibleChar(nh.Y, nh.X)
if !fl.Has(FReal) && ch == Floor {
if !p.On(IsLevit) {
ch = Trap
g.Level.SetChar(nh.Y, nh.X, Trap)
g.Level.FlagsAt(nh.Y, nh.X).Set(FReal)
}
} else if p.On(IsHeld) && ch != 'F' {
g.msg("you are being held")
return
}
}
if hitBound {
ch = ' ' // fall into the wall case below
}
switch ch {
case ' ', '|', '-':
if g.Options.PassGo && g.Running && p.Room.Flags.Has(IsGone) &&
!p.On(IsBlind) {
var b1, b2 bool
switch g.RunCh {
case 'h', 'l':
b1 = p.Pos.Y != 1 && g.turnOk(p.Pos.Y-1, p.Pos.X)
b2 = p.Pos.Y != NumLines-2 && g.turnOk(p.Pos.Y+1, p.Pos.X)
if b1 != b2 {
if b1 {
g.RunCh = 'k'
dy = -1
} else {
g.RunCh = 'j'
dy = 1
}
dx = 0
g.turnref()
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
goto over
}
case 'j', 'k':
b1 = p.Pos.X != 0 && g.turnOk(p.Pos.Y, p.Pos.X-1)
b2 = p.Pos.X != NumCols-1 && g.turnOk(p.Pos.Y, p.Pos.X+1)
if b1 != b2 {
if b1 {
g.RunCh = 'h'
dx = -1
} else {
g.RunCh = 'l'
dx = 1
}
dy = 0
g.turnref()
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
goto over
}
}
}
g.Running = false
g.After = false
case Door:
g.Running = false
if g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Has(FPass) {
g.enterRoom(nh)
}
g.moveStuff(nh, fl)
case Trap:
tr := g.beTrapped(nh)
if tr == TDoor || tr == TTelep {
return
}
g.moveStuff(nh, fl)
case Passage:
// when you're in a corridor, you don't know if you're in a maze
// room or not, and there ain't no way to find out if you're
// leaving a maze room, so it is necessary to always recalculate
// proom.
p.Room = g.roomin(p.Pos)
g.moveStuff(nh, fl)
case Floor:
if !fl.Has(FReal) {
g.beTrapped(p.Pos)
}
g.moveStuff(nh, fl)
default:
if ch == Stairs {
g.SeenStairs = true
}
g.Running = false
if isUpper(ch) || g.Level.MonsterAt(nh.Y, nh.X) != nil {
g.fight(nh, p.CurWeapon, false)
} else {
if ch != Stairs {
g.Take = ch
}
g.moveStuff(nh, fl)
}
}
}
// moveStuff is the move_stuff label in do_move: complete the step.
func (g *RogueGame) moveStuff(nh Coord, fl PlaceFlags) {
p := &g.Player
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
if fl.Has(FPass) && g.Level.Char(g.Oldpos.Y, g.Oldpos.X) == Door {
g.leaveRoom(nh)
}
p.Pos = nh
}
// turnOk decides whether it is legal to turn onto the given space
// (move.c turn_ok).
func (g *RogueGame) turnOk(y, x int) bool {
pp := g.Level.At(y, x)
return pp.Ch == Door || pp.Flags&(FReal|FPass) == (FReal|FPass)
}
// turnref decides whether to refresh at a passage turning (move.c turnref).
func (g *RogueGame) turnref() {
p := &g.Player
pp := g.Level.At(p.Pos.Y, p.Pos.X)
if !pp.Flags.Has(FSeen) {
if g.Options.Jump {
g.refresh()
}
pp.Flags.Set(FSeen)
}
}
// doorOpen is called to wake up things in a room that might move when the
// hero enters (move.c door_open).
func (g *RogueGame) doorOpen(rp *Room) {
if rp.Flags.Has(IsGone) {
return
}
for y := rp.Pos.Y; y < rp.Pos.Y+rp.Max.Y; y++ {
for x := rp.Pos.X; x < rp.Pos.X+rp.Max.X; x++ {
if isUpper(g.Level.VisibleChar(y, x)) {
g.wakeMonster(y, x)
}
}
}
}
// beTrapped makes him pay for stepping on a trap (move.c be_trapped).
func (g *RogueGame) beTrapped(tc Coord) int {
p := &g.Player
if p.On(IsLevit) {
return TRust // anything that's not a door or teleport
}
g.Running = false
g.Count = 0
pp := g.Level.At(tc.Y, tc.X)
pp.Ch = Trap
tr := int(pp.Flags & FTMask)
pp.Flags.Set(FSeen)
switch tr {
case TDoor:
g.Depth++
g.NewLevel()
g.msg("you fell into a trap!")
case TBear:
g.NoMove += g.spread(3) // BEARTIME
g.msg("you are caught in a bear trap")
case TMyst:
switch g.rnd(11) {
case 0:
g.msg("you are suddenly in a parallel dimension")
case 1:
g.msg("the light in here suddenly seems %s", rainbow[g.rnd(len(rainbow))])
case 2:
g.msg("you feel a sting in the side of your neck")
case 3:
g.msg("multi-colored lines swirl around you, then fade")
case 4:
g.msg("a %s light flashes in your eyes", rainbow[g.rnd(len(rainbow))])
case 5:
g.msg("a spike shoots past your ear!")
case 6:
g.msg("%s sparks dance across your armor", rainbow[g.rnd(len(rainbow))])
case 7:
g.msg("you suddenly feel very thirsty")
case 8:
g.msg("you feel time speed up suddenly")
case 9:
g.msg("time now seems to be going slower")
case 10:
g.msg("you pack turns %s!", rainbow[g.rnd(len(rainbow))])
}
case TSleep:
g.NoCommand += g.spread(5) // SLEEPTIME
p.Flags.Clear(IsRun)
g.msg("a strange white mist envelops you and you fall asleep")
case TArrow:
if g.swing(p.Stats.Lvl-1, p.Stats.Arm, 1) {
p.Stats.HP -= g.roll(1, 6)
if p.Stats.HP <= 0 {
g.msg("an arrow killed you")
g.death('a')
} else {
g.msg("oh no! An arrow shot you")
}
} else {
arrow := newObject()
g.initWeapon(arrow, Arrow)
arrow.Count = 1
arrow.Pos = p.Pos
g.fall(arrow, false)
g.msg("an arrow shoots past you")
}
case TTelep:
// since the hero's leaving, look() won't put a TRAP down for us,
// so we have to do it ourself
g.teleport()
g.mvaddch(tc.Y, tc.X, Trap)
case TDart:
if !g.swing(p.Stats.Lvl+1, p.Stats.Arm, 1) {
g.msg("a small dart whizzes by your ear and vanishes")
} else {
p.Stats.HP -= g.roll(1, 4)
if p.Stats.HP <= 0 {
g.msg("a poisoned dart killed you")
g.death('d')
}
if !p.IsWearing(RSustStr) && !g.save(VsPoison) {
g.chgStr(-1)
}
g.msg("a small dart just hit you in the shoulder")
}
case TRust:
g.msg("a gush of water hits you on the head")
g.rustArmor(p.CurArmor)
}
g.flushType()
return tr
}
// rndmove moves in a random direction if the monster/person is confused
// (move.c rndmove).
func (g *RogueGame) rndmove(who *Creature) Coord {
ret := Coord{
Y: who.Pos.Y + g.rnd(3) - 1,
X: who.Pos.X + g.rnd(3) - 1,
}
// Now check to see if that's a legal move. If not, don't move.
// (I.e., bump into the wall or whatever)
if ret == who.Pos {
return ret
}
if !g.diagOk(who.Pos, ret) {
return who.Pos
}
ch := g.Level.VisibleChar(ret.Y, ret.X)
if !stepOk(ch) {
return who.Pos
}
if ch == Scroll {
var found *Object
for _, obj := range g.Level.Objects {
if ret.Y == obj.Pos.Y && ret.X == obj.Pos.X {
found = obj
break
}
}
if found != nil && found.Which == SScare {
return who.Pos
}
}
return ret
}
// rustArmor rusts the given armor, if it is a legal kind to rust, and we
// aren't wearing a magic ring (move.c rust_armor).
func (g *RogueGame) rustArmor(arm *Object) {
if arm == nil || arm.Type != Armor || arm.Which == Leather ||
arm.Arm >= 9 {
return
}
if arm.Flags.Has(IsProt) || g.Player.IsWearing(RSustArm) {
if !g.ToDeath {
g.msg("the rust vanishes instantly")
}
} else {
arm.Arm++
if !g.Options.Terse {
g.msg("your armor appears to be weaker now. Oh my!")
} else {
g.msg("your armor weakens")
}
}
}