Merge refactor/effects-dispatch (refactor step 7)

This commit is contained in:
2026-07-22 22:18:11 +07:00
30 changed files with 5071 additions and 3525 deletions

View File

@@ -21,12 +21,13 @@ sneak's explicit permission. To disable a linter, ask, explaining what
the linter does; he approves specific exceptions, which are recorded
in the config's "Repo-specific exceptions" block with the approval
date. Approved so far: paralleltest (2026-07-06); testpackage,
exhaustive, and mnd (2026-07-07). Complexity linters (cyclop, gocognit,
nestif) stay enabled and red until refactor step 7 fixes the findings,
per sneak 2026-07-07. Line-level //nolint
with a reason is used sparingly for C-faithfulness (e.g. the authentic
"missle" message spellings) and provably-safe gosec conversions; each
needs a justifying comment.
exhaustive, and mnd (2026-07-07). The complexity linters (cyclop,
gocognit, nestif) are enabled and clean as of refactor step 7
(2026-07-07): the whole golangci-lint run is 0 issues, so keep it that
way — decompose new hot spots rather than reaching for a nolint.
Line-level //nolint with a reason is used sparingly for C-faithfulness
(e.g. the authentic "missle" message spellings) and provably-safe gosec
conversions; each needs a justifying comment.
## Faithfulness

40
TODO.md
View File

@@ -29,13 +29,27 @@ Refactor ground rules:
# Next Step
Refactor step 7: effects dispatch — the giant quaff/readScroll/doZap
switches become per-kind handler tables of small named methods,
keeping effect order and RNG call sequence identical. This step also
clears the outstanding cyclop/gocognit/nestif lint findings.
Refactor step 8: constructor and style pass per the house styleguide —
game.New(game.Params{...}) replacing NewGame(Config); replace the
gameEnd panic unwind with error-based turn results where feasible;
77-column wrap sweep.
# Completed Steps
- 2026-07-07 Refactor step 7 (refactor/effects-dispatch): effects
dispatch tables plus a full decomposition sweep — the quaff /
readScroll / doZap switches, the attack monster-power switch, the
be_trapped switch, the daemon d_func switch, and the command-key
switch all became handler tables on gameData (quaffHandlers,
readHandlers, zapHandlers, hitHandlers, trapHandlers, daemonHandlers,
commandHandlers), one small named method per case. Every remaining
cyclop/gocognit/nestif hot spot was split into named helpers across
fight, misc (look), command, chase, move, passages, options, pack,
things, save, daemons, rooms, score, monsters, rings, rip, io,
object, weapons, wizard, and term/tcell, plus three test functions.
Effect order and RNG call sequence preserved throughout; the whole
golangci-lint run is now 0 issues.
- 2026-07-07 Refactor step 6 (refactor/god-object-extraction):
MessageLine (was MsgLine) owns the msg/addmsg/endmsg machinery,
wired to its screen/look/input needs via attach(); RogueGame keeps
@@ -128,27 +142,23 @@ clears the outstanding cyclop/gocognit/nestif lint findings.
# Future Steps
1. Refactor step 8: constructor and style pass per the house
styleguide — game.New(game.Params{...}) replacing NewGame(Config);
replace the gameEnd panic unwind with error-based turn results where
feasible; 77-column wrap sweep.
2. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the
1. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the
post-refactor names; add the C name → Go name rename table.
3. Playtest hardening pass: play several full games with the tcell
2. Playtest hardening pass: play several full games with the tcell
binary and extend run_test.go to script a deeper multi-level
playthrough (descend past level 5, use potions, scrolls, zapping,
save/restore). Fix any panics, message mismatches, or divergences
from the C behavior that this uncovers, with regression tests.
4. Verify the seed-compatibility claim against the C reference on
3. Verify the seed-compatibility claim against the C reference on
c-master: same seed, same dungeon, same item tables, for several
seeds.
5. Broaden unit test coverage where playtesting finds thin spots
4. Broaden unit test coverage where playtesting finds thin spots
(rings, sticks, wizard commands).
6. Tag a release once a full game (Amulet retrieval and score entry)
5. Tag a release once a full game (Amulet retrieval and score entry)
completes without defects.
7. Full-terminal-size support (deferred by explicit decision
6. Full-terminal-size support (deferred by explicit decision
2026-07-06): per-game dungeon dimensions instead of the 80x24
constants; open design questions are resize policy, gameplay
tuning at larger sizes, and a --classic 80x24 mode.
8. Note: this repo is exempt from the standard policy scaffold. Do not
7. Note: this repo is exempt from the standard policy scaffold. Do not
add Makefile, Dockerfile, or REPO_POLICIES.md.

View File

@@ -9,17 +9,32 @@ const dragonShot = 5
func (g *RogueGame) runners(int) {
list := append([]*Monster(nil), g.Level.Monsters...)
for _, tp := range list {
if !tp.On(Held) && tp.On(Awake) {
g.runnerTurn(tp)
}
if g.HasHit {
g.endmsg()
g.HasHit = false
}
}
// runnerTurn gives one monster its motion for the turn; flying monsters
// far from the hero move twice (the loop body of chase.c runners).
func (g *RogueGame) runnerTurn(tp *Monster) {
if tp.On(Held) || !tp.On(Awake) {
return
}
origPos := tp.Pos
wastarget := tp.On(Targeted)
if removed := g.moveMonster(tp); removed {
continue
return
}
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
if removed := g.moveMonster(tp); removed {
continue
return
}
}
@@ -29,13 +44,6 @@ func (g *RogueGame) runners(int) {
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
@@ -87,13 +95,48 @@ func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
}
}
// 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
// chaseStep makes one thing chase another (chase.c do_chase). The
// result reports that the chaser died or left the level in the attempt
// (the C -1 return).
func (g *RogueGame) chaseStep(th *Monster) bool {
stoprun := false // true means we are there
mindist := 32767
rer, ree, door := g.chaseRooms(th)
this, shot := g.chaseGoal(th, rer, ree, door, 32767)
if shot {
return false
}
// 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 th.Type == 'F' {
return false
}
} else {
switch this {
case g.Player.Pos:
return g.attack(th)
case *th.Dest:
g.chaseTakeObject(th)
stoprun = th.Type != 'F'
}
}
g.relocate(th, g.chRet)
// And stop running if need be
if stoprun && th.Pos == *th.Dest {
th.Flags.Clear(Awake)
}
return false
}
// chaseRooms finds the rooms of the chaser and its desire; doors do not
// count as inside rooms here (the setup of chase.c do_chase).
func (g *RogueGame) chaseRooms(th *Monster) (*Room, *Room, bool) {
p := &g.Player
rer := th.Room // find room of chaser
if th.On(Greedy) && rer.GoldVal == 0 {
@@ -106,15 +149,26 @@ func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
} 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
return rer, ree, g.Level.Char(th.Pos.Y, th.Pos.X) == Door
}
// chaseGoal picks the spot the chaser runs toward this turn: the
// nearest exit toward its desire when it is in a different room, or the
// desire itself. shot means a dragon breathed flame instead of moving
// (the goal loop of chase.c do_chase).
func (g *RogueGame) chaseGoal(th *Monster, rer, ree *Room, door bool, mindist int) (Coord, bool) {
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 {
if rer == ree {
this = *th.Dest
return this, g.dragonBreath(th)
}
for i := range rer.Exits {
curdist := distCp(*th.Dest, rer.Exits[i])
if curdist < mindist {
@@ -123,21 +177,25 @@ func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
}
}
if door {
if !door {
return this, false
}
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
// 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 {
}
// dragonBreath checks whether a dragon shoots flame at the hero instead
// of moving, and shoots it (the D block of chase.c do_chase).
func (g *RogueGame) dragonBreath(th *Monster) bool {
if th.Type != 'D' || !g.dragonShoots(th) {
return false
}
p := &g.Player
g.Delta.Y = sign(p.Pos.Y - th.Pos.Y)
g.Delta.X = sign(p.Pos.X - th.Pos.X)
@@ -155,18 +213,27 @@ func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
g.Kamikaze = false
}
return false
}
return true
}
break
// dragonShoots decides whether the dragon takes the shot: the hero is
// on a straight line from it, within shooting distance but outside
// striking range, it is not cancelled, and the shot roll comes up
// (chase.c do_chase).
func (g *RogueGame) dragonShoots(th *Monster) bool {
p := &g.Player
if 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) {
return false
}
// 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 {
return distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
!th.On(Cancelled) && g.rnd(dragonShot) == 0
}
// chaseTakeObject has the monster pick up the object it was running to
// (the dest arm of chase.c do_chase).
func (g *RogueGame) chaseTakeObject(th *Monster) {
for _, obj := range g.Level.Objects {
if th.Dest == &obj.Pos {
g.Level.RemoveObject(obj)
@@ -183,34 +250,12 @@ func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
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
@@ -226,11 +271,27 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
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)
curdist = g.chaseBestSpot(tp, ee)
}
return curdist != 0 && g.chRet != g.Player.Pos
}
// chaseSearch is the scan state while chase looks for the step that
// gets a monster closest to its chasee.
type chaseSearch struct {
er Coord // where the chaser is
ee Coord // where it wants to go
curdist int
plcnt int
}
// chaseBestSpot finds the empty spot next to the chaser that is closest
// to the chasee, leaving it in g.chRet; if there is none, the chaser
// stays where it is (the search half of chase.c chase).
func (g *RogueGame) chaseBestSpot(tp *Monster, ee Coord) int {
er := tp.Pos
s := chaseSearch{er: er, ee: ee, curdist: distCp(er, ee), plcnt: 1}
g.chRet = er
ey := er.Y + 1
@@ -249,54 +310,59 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
}
for y := er.Y - 1; y <= ey; y++ {
g.chaseTry(&s, y, x)
}
}
return s.curdist
}
// chaseTry scores one candidate square, reservoir-sampling among ties
// (the scan body of chase.c chase).
func (g *RogueGame) chaseTry(s *chaseSearch, y, x int) {
tryp := Coord{X: x, Y: y}
if !g.diagOk(er, tryp) {
continue
if !g.diagOk(s.er, tryp) {
return
}
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
if !stepOk(ch) {
return
}
// 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 && g.scareScrollAt(y, x) {
return
}
// 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
return
}
// 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
// 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, s.ee.Y, s.ee.X)
if thisdist < s.curdist {
s.plcnt = 1
g.chRet = tryp
curdist = thisdist
} else if thisdist == curdist {
if plcnt++; g.rnd(plcnt) == 0 {
s.curdist = thisdist
} else if thisdist == s.curdist {
if s.plcnt++; g.rnd(s.plcnt) == 0 {
g.chRet = tryp
curdist = thisdist
}
}
}
s.curdist = thisdist
}
}
}
return curdist != 0 && g.chRet != p.Pos
// scareScrollAt reports whether the object lying at (y, x) is a scare
// monster scroll (chase.c chase).
func (g *RogueGame) scareScrollAt(y, x int) bool {
for _, obj := range g.Level.Objects {
if y == obj.Pos.Y && x == obj.Pos.X {
return obj.ScrollKind() == ScrollScareMonster
}
}
return false
}
// setOldChar sets the oldch character for the monster (chase.c set_oldch).
@@ -431,22 +497,23 @@ func (g *RogueGame) findDest(tp *Monster) *Coord {
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 {
if g.roomIn(obj.Pos) == tp.Room && g.rnd(100) < prob &&
!g.objectClaimed(obj) {
return &obj.Pos
}
}
}
return &g.Player.Pos
}
// objectClaimed reports whether some monster already runs toward this
// object (chase.c find_dest).
func (g *RogueGame) objectClaimed(obj *Object) bool {
for _, other := range g.Level.Monsters {
if other.Dest == &obj.Pos {
return true
}
}
return false
}

View File

@@ -16,6 +16,70 @@ func (g *RogueGame) command() {
g.DoFuses(Before)
for ; ntimes > 0; ntimes-- {
g.playTurn()
if !g.After {
ntimes++
}
}
g.DoDaemons(After)
g.DoFuses(After)
g.ringTurnEffects(Left)
g.ringTurnEffects(Right)
}
// ringTurnEffects fires the per-turn ring powers on one hand: searching
// searches, teleportation teleports on 1 turn in 50 (the tail of
// command.c command).
func (g *RogueGame) ringTurnEffects(hand int) {
p := &g.Player
if p.IsRing(hand, RingSearching) {
g.search()
} else if p.IsRing(hand, RingTeleportation) && g.rnd(50) == 0 {
g.teleport()
}
}
// playTurn runs one full player turn: upkeep, one command, and pickup
// (the body of the command.c command loop).
func (g *RogueGame) playTurn() {
p := &g.Player
g.turnUpkeep()
var ch byte
if g.NoCommand == 0 {
ch = g.readCommand()
} else {
ch = '.'
}
if g.NoCommand != 0 {
if g.NoCommand--; g.NoCommand == 0 {
p.Flags.Set(Awake)
g.msg("you can move again")
}
} else {
g.executeCommand(ch)
}
// If he ran into something to take, let him pick it up.
if g.Take != 0 {
g.pickUp(g.Take)
}
if !g.Running {
g.DoorStop = false
}
}
// turnUpkeep redraws and resets per-turn state before a command is read
// (the top of the command.c command loop).
func (g *RogueGame) turnUpkeep() {
p := &g.Player
g.Again = false
if g.HasHit {
g.endmsg()
@@ -47,59 +111,35 @@ func (g *RogueGame) command() {
if g.Wizard {
g.NoScore = true
}
}
var ch byte
if g.NoCommand == 0 {
// readCommand picks the next command character: run/count repeats or
// fresh input (the read block of command.c command).
func (g *RogueGame) readCommand() byte {
switch {
case g.Running || g.ToDeath:
ch = g.RunCh
return g.RunCh
case g.Count != 0:
ch = g.countCh
return g.countCh
default:
ch = g.readchar()
ch := g.readchar()
g.MoveOn = false
if g.Msgs.Mpos != 0 { // erase message if its there
g.msg("")
}
return ch
}
} else {
ch = '.'
}
if g.NoCommand != 0 {
if g.NoCommand--; g.NoCommand == 0 {
p.Flags.Set(Awake)
g.msg("you can move again")
}
} else {
// executeCommand runs one read command with the repeat-count prefix and
// the repeat-history bookkeeping (the execute block of command.c
// command).
func (g *RogueGame) executeCommand(ch byte) {
// check for prefixes
g.newCount = false
if isDigit(ch) {
g.Count = 0
g.newCount = true
for isDigit(ch) {
g.Count = min(g.Count*10+int(ch-'0'), 255)
ch = g.readchar()
}
g.countCh = ch
// turn off count for commands which don't make sense
// to repeat
switch ch {
case CTRL('B'), CTRL('H'), CTRL('J'), CTRL('K'),
CTRL('L'), CTRL('N'), CTRL('U'), CTRL('Y'),
'.', 'a', 'b', 'h', 'j', 'k', 'l', 'm', 'n', 'q',
'r', 's', 't', 'u', 'y', 'z', 'B', 'C', 'H', 'I',
'J', 'K', 'L', 'N', 'U', 'Y',
CTRL('D'), CTRL('A'):
default:
g.Count = 0
}
}
ch = g.countPrefix(ch)
// execute a command
if g.Count != 0 && !g.Running {
g.Count--
@@ -121,44 +161,136 @@ func (g *RogueGame) command() {
g.DoorStop = false
}
}
// If he ran into something to take, let him pick it up.
if g.Take != 0 {
g.pickUp(g.Take)
// countPrefix consumes a numeric repeat-count prefix and returns the
// command character that follows it (command.c).
func (g *RogueGame) countPrefix(ch byte) byte {
if !isDigit(ch) {
return ch
}
if !g.Running {
g.DoorStop = false
g.Count = 0
g.newCount = true
for isDigit(ch) {
g.Count = min(g.Count*10+int(ch-'0'), 255)
ch = g.readchar()
}
if !g.After {
ntimes++
}
g.countCh = ch
// turn off count for commands which don't make sense to repeat
switch ch {
case CTRL('B'), CTRL('H'), CTRL('J'), CTRL('K'),
CTRL('L'), CTRL('N'), CTRL('U'), CTRL('Y'),
'.', 'a', 'b', 'h', 'j', 'k', 'l', 'm', 'n', 'q',
'r', 's', 't', 'u', 'y', 'z', 'B', 'C', 'H', 'I',
'J', 'K', 'L', 'N', 'U', 'Y',
CTRL('D'), CTRL('A'):
default:
g.Count = 0
}
g.DoDaemons(After)
g.DoFuses(After)
if p.IsRing(Left, RingSearching) {
g.search()
} else if p.IsRing(Left, RingTeleportation) && g.rnd(50) == 0 {
g.teleport()
return ch
}
if p.IsRing(Right, RingSearching) {
g.search()
} else if p.IsRing(Right, RingTeleportation) && g.rnd(50) == 0 {
g.teleport()
}
}
// dispatch runs one command character (the big switch in command.c; the
// loop stands in for its `goto over` re-dispatch).
// dispatch runs one command character, re-running dispatchKey when it
// asks (the C `goto over` re-dispatch in command.c).
func (g *RogueGame) dispatch(ch byte) {
for {
newCh, again := g.dispatchKey(ch)
if !again {
return
}
ch = newCh
}
}
// dispatchKey runs one command character (the big switch of command.c).
// A true second result asks dispatch to run once more with the returned
// key.
func (g *RogueGame) dispatchKey(ch byte) (byte, bool) {
if h, ok := g.data.commandHandlers[ch]; ok {
h(g)
return 0, false
}
switch ch {
case CTRL('H'), CTRL('J'), CTRL('K'), CTRL('L'),
CTRL('Y'), CTRL('U'), CTRL('B'), CTRL('N'):
return g.runCommand(ch)
case 'F', 'f':
return g.fightCommand(ch)
case 'a':
return g.repeatCommand()
case 'm':
return g.moveOnCommand()
default:
g.After = false
if g.Wizard {
g.wizardCommand(ch)
} else {
g.illcom(ch)
}
}
return 0, false
}
// runCommand handles the ctrl-direction run-until-adjacent prefix; the
// returned key re-dispatches as the matching run command (command.c).
func (g *RogueGame) runCommand(ch byte) (byte, bool) {
if !g.Player.On(Blind) {
g.DoorStop = true
g.Firstmove = true
}
if g.Count != 0 && !g.newCount {
ch = g.direction
} else {
ch += 'A' - CTRL('A')
g.direction = ch
}
return ch, true // the C goto over: re-dispatch as the run command
}
// repeatCommand handles 'a': replay the last command by re-dispatching
// it (command.c).
func (g *RogueGame) repeatCommand() (byte, bool) {
if g.LastComm == 0 {
g.msg("you haven't typed a command yet")
g.After = false
return 0, false
}
g.Again = true
return g.LastComm, true // the C goto over: replay the last command
}
// moveOnCommand handles 'm': move without picking up; the direction key
// re-dispatches (command.c).
func (g *RogueGame) moveOnCommand() (byte, bool) {
g.MoveOn = true
if !g.promptDirection() {
g.After = false
return 0, false
}
g.countCh = g.DirCh
return g.DirCh, true // the C goto over: move onto it without pickup
}
// pickupCommand handles ',': pick up whatever is here (command.c).
func (g *RogueGame) pickupCommand() {
p := &g.Player
for {
switch ch {
case ',':
var found *Object
for _, obj := range g.Level.Objects {
@@ -173,7 +305,10 @@ func (g *RogueGame) dispatch(ch byte) {
if !g.levitCheck() {
g.pickUp(found.Kind.Glyph())
}
} else {
return
}
if !g.Options.Terse {
g.addmsgf("there is ")
}
@@ -186,56 +321,11 @@ func (g *RogueGame) dispatch(ch byte) {
g.endmsg()
}
case '!':
g.shell()
case 'h':
g.moveHero(0, -1)
case 'j':
g.moveHero(1, 0)
case 'k':
g.moveHero(-1, 0)
case 'l':
g.moveHero(0, 1)
case 'y':
g.moveHero(-1, -1)
case 'u':
g.moveHero(-1, 1)
case 'b':
g.moveHero(1, -1)
case 'n':
g.moveHero(1, 1)
case 'H':
g.startRun('h')
case 'J':
g.startRun('j')
case 'K':
g.startRun('k')
case 'L':
g.startRun('l')
case 'Y':
g.startRun('y')
case 'U':
g.startRun('u')
case 'B':
g.startRun('b')
case 'N':
g.startRun('n')
case CTRL('H'), CTRL('J'), CTRL('K'), CTRL('L'),
CTRL('Y'), CTRL('U'), CTRL('B'), CTRL('N'):
if !p.On(Blind) {
g.DoorStop = true
g.Firstmove = true
}
if g.Count != 0 && !g.newCount {
ch = g.direction
} else {
ch += 'A' - CTRL('A')
g.direction = ch
}
continue // the C goto over: re-dispatch as the run command
case 'F', 'f':
// fightCommand handles the f/F fight prefix; a true again asks dispatch
// to rerun with the direction key (command.c).
func (g *RogueGame) fightCommand(ch byte) (byte, bool) {
p := &g.Player
if ch == 'F' {
g.Kamikaze = true
}
@@ -243,7 +333,7 @@ func (g *RogueGame) dispatch(ch byte) {
if !g.promptDirection() {
g.After = false
break
return 0, false
}
g.Delta.Y += p.Pos.Y
@@ -264,103 +354,23 @@ func (g *RogueGame) dispatch(ch byte) {
mp.Flags.Set(Targeted)
g.RunCh = g.DirCh
ch = g.DirCh
continue // the C goto over: fight by running at it
return g.DirCh, true // the C goto over: fight by running at it
}
case 't':
return 0, false
}
// identifyTrapCommand handles '^': name the trap in a direction
// (command.c).
func (g *RogueGame) identifyTrapCommand() {
p := &g.Player
g.After = false
if !g.promptDirection() {
g.After = false
} else {
g.missile(g.Delta.Y, g.Delta.X)
return
}
case 'a':
if g.LastComm == 0 {
g.msg("you haven't typed a command yet")
g.After = false
} else {
ch = g.LastComm
g.Again = true
continue // the C goto over: replay the last command
}
case 'q':
g.quaff()
case 'Q':
g.After = false
g.QComm = true
g.quit(0)
g.QComm = false
case 'i':
g.After = false
g.inventory(p.Pack, 0)
case 'I':
g.After = false
g.pickyInven()
case 'd':
g.dropIt()
case 'r':
g.readScroll()
case 'e':
g.eat()
case 'w':
g.wield()
case 'W':
g.wear()
case 'T':
g.takeOff()
case 'P':
g.ringOn()
case 'R':
g.ringOff()
case 'o':
g.option()
g.After = false
case 'c':
g.call()
g.After = false
case '>':
g.After = false
g.dLevel()
case '<':
g.After = false
g.uLevel()
case '?':
g.After = false
g.help()
case '/':
g.After = false
g.identify()
case 's':
g.search()
case 'z':
if g.promptDirection() {
g.doZap()
} else {
g.After = false
}
case 'D':
g.After = false
g.discovered()
case CTRL('P'):
g.After = false
g.msg("%s", g.Msgs.Huh)
case CTRL('R'):
g.After = false
g.refresh()
case 'v':
g.After = false
g.msg("version %s. (mctesq was here)", Release)
case 'S':
g.After = false
g.saveGame()
case '.':
// rest command
case ' ':
g.After = false // "legal" illegal command
case '^':
g.After = false
if g.promptDirection() {
g.Delta.Y += p.Pos.Y
g.Delta.X += p.Pos.X
@@ -379,47 +389,6 @@ func (g *RogueGame) dispatch(ch byte) {
fp.Set(FSeen)
}
}
case Escape: // escape
g.DoorStop = false
g.Count = 0
g.After = false
g.Again = false
case 'm':
g.MoveOn = true
if !g.promptDirection() {
g.After = false
} else {
ch = g.DirCh
g.countCh = g.DirCh
continue // the C goto over: move onto it without pickup
}
case ')':
g.current(p.CurWeapon, "wielding", "")
case ']':
g.current(p.CurArmor, "wearing", "")
case '=':
g.current(p.CurRing[Left], "wearing",
g.chooseTerse("(L)", "on left hand"))
g.current(p.CurRing[Right], "wearing",
g.chooseTerse("(R)", "on right hand"))
case '@':
g.StatMsg = true
g.status()
g.StatMsg = false
g.After = false
default:
g.After = false
if g.Wizard {
g.wizardCommand(ch)
} else {
g.illcom(ch)
}
}
return
}
}
// wizardCommand handles the MASTER debug commands (command.c).
func (g *RogueGame) wizardCommand(ch byte) {
@@ -444,6 +413,17 @@ func (g *RogueGame) wizardCommand(ch byte) {
g.NewLevel()
case CTRL('F'):
g.showMap()
default:
g.wizardDebugCommand(ch)
}
}
// wizardDebugCommand handles the rest of the MASTER debug commands
// (command.c).
func (g *RogueGame) wizardDebugCommand(ch byte) {
p := &g.Player
switch ch {
case CTRL('T'):
g.teleport()
case CTRL('E'):
@@ -457,6 +437,19 @@ func (g *RogueGame) wizardCommand(ch byte) {
item.Charges = 10000
}
case CTRL('I'):
g.wizardKit()
case '*':
g.prList()
default:
g.illcom(ch)
}
}
// wizardKit levels the wizard up and equips him (the ^I case of the
// command.c wizard switch).
func (g *RogueGame) wizardKit() {
p := &g.Player
for range 9 {
g.raiseLevel()
}
@@ -476,11 +469,6 @@ func (g *RogueGame) wizardCommand(ch byte) {
obj.Count = 1
p.CurArmor = obj
g.addPack(obj, true)
case '*':
g.prList()
default:
g.illcom(ch)
}
}
// illcom reports an illegal command (command.c illcom).
@@ -514,9 +502,24 @@ func (g *RogueGame) search() {
continue
}
if g.searchSpot(y, x, probinc) {
found = true
}
}
}
if found {
g.look(false)
}
}
// searchSpot tries to reveal one hidden thing next to the hero: a
// secret door, a hidden trap, or a secret passage (the loop body of
// command.c search). It reports whether something was found.
func (g *RogueGame) searchSpot(y, x, probinc int) bool {
fp := g.Level.FlagsAt(y, x)
if fp.Has(FReal) {
continue
return false
}
foundone := false
@@ -532,24 +535,7 @@ func (g *RogueGame) search() {
foundone = true
case Floor:
if g.rnd(2+probinc) != 0 {
break
}
g.Level.SetChar(y, x, Trap)
if !g.Options.Terse {
g.addmsgf("you found ")
}
if p.On(Hallucinating) {
g.msg("%s", g.data.trName[g.rnd(NumTrapTypes)])
} else {
g.msg("%s", g.data.trName[*fp&FTrapMask])
fp.Set(FSeen)
}
foundone = true
foundone = g.searchFloor(fp, y, x, probinc)
case ' ':
if g.rnd(3+probinc) != 0 {
break
@@ -561,19 +547,36 @@ func (g *RogueGame) search() {
}
if foundone {
found = true
fp.Set(FReal)
g.Count = 0
g.Running = false
}
}
return foundone
}
if found {
g.look(false)
// searchFloor reveals a hidden trap underfoot and names it (the FLOOR
// arm of command.c search).
func (g *RogueGame) searchFloor(fp *PlaceFlags, y, x, probinc int) bool {
if g.rnd(2+probinc) != 0 {
return false
}
g.Level.SetChar(y, x, Trap)
if !g.Options.Terse {
g.addmsgf("you found ")
}
if g.Player.On(Hallucinating) {
g.msg("%s", g.data.trName[g.rnd(NumTrapTypes)])
} else {
g.msg("%s", g.data.trName[*fp&FTrapMask])
fp.Set(FSeen)
}
return true
}
// help gives single character help, or the whole mess if he wants it
@@ -585,6 +588,17 @@ func (g *RogueGame) help() {
// If it's not a *, print the right help string or an error if he
// typed a funny character.
if helpch != '*' {
g.helpOne(helpch)
return
}
g.helpAll()
}
// helpOne prints the help line for a single key, or an error for a
// funny character (command.c help).
func (g *RogueGame) helpOne(helpch byte) {
g.move(0, 0)
for _, strp := range g.data.helpStr {
@@ -598,27 +612,12 @@ func (g *RogueGame) help() {
}
g.msg("unknown character '%s'", unctrl(helpch))
return
}
// Here we print help for everything, then wait before we return to
// command mode.
numprint := 0
for _, strp := range g.data.helpStr {
if strp.Print {
numprint++
}
}
if numprint&1 == 1 { // round odd numbers up
numprint++
}
numprint /= 2
if numprint > NumLines-1 {
numprint = NumLines - 1
}
// helpAll prints help for everything in two columns, then waits before
// returning to command mode (command.c help).
func (g *RogueGame) helpAll() {
numprint := g.helpLines()
hw := g.scr.Hw
hw.Clear()
@@ -655,6 +654,29 @@ func (g *RogueGame) help() {
g.refresh()
}
// helpLines counts how many rows the two-column help listing needs
// (command.c help).
func (g *RogueGame) helpLines() int {
numprint := 0
for _, strp := range g.data.helpStr {
if strp.Print {
numprint++
}
}
if numprint&1 == 1 { // round odd numbers up
numprint++
}
numprint /= 2
if numprint > NumLines-1 {
numprint = NumLines - 1
}
return numprint
}
// identify tells the player what a certain thing is (command.c identify).
func (g *RogueGame) identify() {
g.msg("what do you want identified? ")
@@ -706,8 +728,18 @@ func (g *RogueGame) uLevel() {
return
}
if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) == Stairs {
if g.HasAmulet {
if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) != Stairs {
g.msg("I see no way up")
return
}
if !g.HasAmulet {
g.msg("your way is magically blocked")
return
}
g.Depth--
if g.Depth == 0 {
g.totalWinner()
@@ -715,12 +747,6 @@ func (g *RogueGame) uLevel() {
g.NewLevel()
g.msg("you feel a wrenching sensation in your gut")
} else {
g.msg("your way is magically blocked")
}
} else {
g.msg("I see no way up")
}
}
// levitCheck checks whether she's levitating, and if she is, prints an
@@ -744,63 +770,14 @@ func (g *RogueGame) call() {
return
}
var (
op *ObjInfo
elsewise string
know *bool
guess *string
)
it := &g.Items
switch obj.Kind {
case KindRing:
op = &it.Rings[obj.Which]
elsewise = it.RingStones[obj.Which]
case KindPotion:
op = &it.Potions[obj.Which]
elsewise = it.PotColors[obj.Which]
case KindScroll:
op = &it.Scrolls[obj.Which]
elsewise = it.ScrNames[obj.Which]
case KindWand:
op = &it.Sticks[obj.Which]
elsewise = it.WandMade[obj.Which]
case KindFood:
g.msg("you can't call that anything")
return
default:
guess = &obj.Label
elsewise = obj.Label
}
fromGuess := false
if op != nil {
know = &op.Know
guess = &op.Guess
if *guess != "" {
elsewise = *guess
fromGuess = true
}
} else {
fromGuess = elsewise != "" && elsewise == *guess
}
if know != nil && *know {
g.msg("that has already been identified")
op, elsewise, guess, ok := g.callTarget(obj)
if !ok {
return
}
if fromGuess {
if !g.Options.Terse {
g.addmsgf("Was ")
}
g.msg("called \"%s\"", elsewise)
elsewise, guess, ok = g.callPrelude(op, elsewise, guess)
if !ok {
return
}
g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? "))
@@ -811,10 +788,82 @@ func (g *RogueGame) call() {
}
}
// callTarget picks what a call names: the lore record for identifiable
// kinds, the object's own label otherwise; ok is false for food (the
// switch of command.c call).
func (g *RogueGame) callTarget(obj *Object) (*ObjInfo, string, *string, bool) {
it := &g.Items
switch obj.Kind {
case KindRing:
return &it.Rings[obj.Which], it.RingStones[obj.Which], nil, true
case KindPotion:
return &it.Potions[obj.Which], it.PotColors[obj.Which], nil, true
case KindScroll:
return &it.Scrolls[obj.Which], it.ScrNames[obj.Which], nil, true
case KindWand:
return &it.Sticks[obj.Which], it.WandMade[obj.Which], nil, true
case KindFood:
g.msg("you can't call that anything")
return nil, "", nil, false
default:
return nil, obj.Label, &obj.Label, true
}
}
// callPrelude resolves the item's current name, reporting a previous
// guess; ok is false when the item is already identified (command.c
// call).
func (g *RogueGame) callPrelude(op *ObjInfo, elsewise string, guess *string) (string, *string, bool) {
fromGuess := false
if op != nil {
if op.Know {
g.msg("that has already been identified")
return "", nil, false
}
guess = &op.Guess
if *guess != "" {
elsewise = *guess
fromGuess = true
}
} else {
fromGuess = elsewise != "" && elsewise == *guess
}
if fromGuess {
if !g.Options.Terse {
g.addmsgf("Was ")
}
g.msg("called \"%s\"", elsewise)
}
return elsewise, guess, true
}
// current prints the current weapon/armor (command.c current).
func (g *RogueGame) current(cur *Object, how, where string) {
g.After = false
if cur != nil {
if cur == nil {
if !g.Options.Terse {
g.addmsgf("you are ")
}
g.addmsgf("%s nothing", how)
if where != "" {
g.addmsgf(" %s", where)
}
g.endmsg()
return
}
if !g.Options.Terse {
g.addmsgf("you are %s (", how)
}
@@ -828,19 +877,6 @@ func (g *RogueGame) current(cur *Object, how, where string) {
}
g.endmsg()
} else {
if !g.Options.Terse {
g.addmsgf("you are ")
}
g.addmsgf("%s nothing", how)
if where != "" {
g.addmsgf(" %s", where)
}
g.endmsg()
}
}
// shell lets them escape for a while (main.c shell). The terminal decides

View File

@@ -5,38 +5,14 @@ package game
// runDaemon invokes the callback named by id (the call through d_func in C).
func (g *RogueGame) runDaemon(id DaemonID, arg int) {
switch id {
case DRollwand:
g.rollwand(arg)
case DDoctor:
g.doctor(arg)
case DStomach:
g.stomach(arg)
case DRunners:
g.runners(arg)
case DSwander:
g.swander(arg)
case DNohaste:
g.nohaste(arg)
case DUnconfuse:
g.unconfuse(arg)
case DUnsee:
g.unsee(arg)
case DSight:
g.sight(arg)
case DVisuals:
g.visuals(arg)
case DComeDown:
g.comeDown(arg)
case DLand:
g.land(arg)
case DTurnSee:
g.turnSee(arg != 0)
default:
// Callbacks are added to this switch as their subsystems are
// ported; reaching one that isn't here is a porting bug.
h := g.data.daemonHandlers[id]
if h == nil {
// Handlers are added to the table as their subsystems are
// ported; reaching one that isn't there is a porting bug.
panic("daemon not yet ported")
}
h(g, arg)
}
// doctor is the healing daemon that restores hit points after rest
@@ -141,6 +117,24 @@ func (g *RogueGame) stomach(int) {
origHungry := p.HungryState
if p.FoodLeft <= 0 {
g.stomachFaint()
} else {
g.stomachDigest()
}
if p.HungryState != origHungry {
p.Flags.Clear(Awake)
g.Running = false
g.ToDeath = false
g.Count = 0
}
}
// stomachFaint starves and possibly faints an empty-stomached hero (the
// no-food arm of daemons.c stomach).
func (g *RogueGame) stomachFaint() {
p := &g.Player
if p.FoodLeft--; p.FoodLeft < -StarveTime {
g.death('s')
}
@@ -159,7 +153,12 @@ func (g *RogueGame) stomach(int) {
}
g.msg("%s", g.chooseStr("You freak out", "You faint"))
} else {
}
// stomachDigest burns food and reports growing hunger (the fed arm of
// daemons.c stomach).
func (g *RogueGame) stomachDigest() {
p := &g.Player
oldfood := p.FoodLeft
amulet := 0
@@ -187,15 +186,6 @@ func (g *RogueGame) stomach(int) {
}
}
if p.HungryState != origHungry {
p.Flags.Clear(Awake)
g.Running = false
g.ToDeath = false
g.Count = 0
}
}
// comeDown takes the hero down off her acid trip (daemons.c come_down).
func (g *RogueGame) comeDown(int) {
p := &g.Player
@@ -242,7 +232,6 @@ func (g *RogueGame) comeDown(int) {
// visuals changes the characters for the player while hallucinating
// (daemons.c visuals).
func (g *RogueGame) visuals(int) {
p := &g.Player
if !g.After || (g.Running && g.Options.Jump) {
return
}
@@ -259,7 +248,13 @@ func (g *RogueGame) visuals(int) {
}
// change the monsters
seemonst := p.On(SenseMonsters)
g.visualMonsters()
}
// visualMonsters redraws the monsters through the hallucination (the
// monster loop of daemons.c visuals).
func (g *RogueGame) visualMonsters() {
seemonst := g.Player.On(SenseMonsters)
for _, tp := range g.Level.Monsters {
g.move(tp.Pos.Y, tp.Pos.X)

View File

@@ -48,8 +48,37 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
g.Count = 0
g.Quiet = 0
g.runTo(mp)
// Let him know it was really a xeroc (if it was one).
if tp.Type == 'X' && tp.Disguise != 'X' && !p.On(Blind) {
if g.revealXeroc(tp) && !thrown {
return false
}
mname := g.setMname(tp)
g.HasHit = g.Options.Terse && !g.ToDeath
if g.rollAttacks(&p.Creature, &tp.Creature, weap, thrown) {
g.heroHits(tp, mname, weap, thrown)
return true
}
if thrown {
g.bounce(weap, mname, g.Options.Terse)
} else {
g.miss("", mname, g.Options.Terse)
}
return false
}
// revealXeroc lets him know it was really a xeroc (if it was one); it
// reports whether one was unmasked (the X block of fight.c fight).
func (g *RogueGame) revealXeroc(tp *Monster) bool {
p := &g.Player
if tp.Type != 'X' || tp.Disguise == 'X' || p.On(Blind) {
return false
}
tp.Disguise = 'X'
if p.On(Hallucinating) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.randomMonsterLetter())
@@ -58,17 +87,14 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
"wait! That's a xeroc!"))
if !thrown {
return false
}
return true
}
mname := g.setMname(tp)
didHit := false
g.HasHit = g.Options.Terse && !g.ToDeath
if g.rollAttacks(&p.Creature, &tp.Creature, weap, thrown) {
didHit = false
// heroHits lands the hero's blow on a monster: messages, the confusing
// touch, and the kill check (the hit arm of fight.c fight).
func (g *RogueGame) heroHits(tp *Monster, mname string, weap *Object, thrown bool) {
p := &g.Player
confused := false
if thrown {
g.thunk(weap, mname, g.Options.Terse)
@@ -77,7 +103,7 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
}
if p.On(CanConfuse) {
didHit = true
confused = true
tp.Flags.Set(Confused)
p.Flags.Clear(CanConfuse)
@@ -88,26 +114,15 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
if tp.Stats.HP <= 0 {
g.killed(tp, true)
} else if didHit && !p.On(Blind) {
} else if confused && !p.On(Blind) {
g.msg("%s appears confused", mname)
}
didHit = true
} else {
if thrown {
g.bounce(weap, mname, g.Options.Terse)
} else {
g.miss("", mname, g.Options.Terse)
}
}
return didHit
}
// attack has the monster attack the player (fight.c attack). removed
// attack has the monster attack the player (fight.c attack). The result
// reports that the monster took itself off the level during its own
// attack (the C -1 return).
func (g *RogueGame) attack(mp *Monster) (removed bool) {
func (g *RogueGame) attack(mp *Monster) bool {
p := &g.Player
// Since this is an attack, stop running and any healing that was
// going on at the time.
@@ -129,8 +144,29 @@ func (g *RogueGame) attack(mp *Monster) (removed bool) {
mname := g.setMname(mp)
oldhp := p.Stats.HP
removed := false
if g.rollAttacks(&mp.Creature, &p.Creature, nil, false) {
removed = g.monsterHit(mp, mname, oldhp)
} else {
g.monsterMiss(mp, mname)
}
if g.Options.FightFlush && !g.ToDeath {
g.flushType()
}
g.Count = 0
g.status()
return removed
}
// monsterHit lands a monster's blow on the hero: messages, death and
// to-death bookkeeping, then the monster's special power (the hit arm
// of fight.c attack). It reports whether the monster removed itself.
func (g *RogueGame) monsterHit(mp *Monster, mname string, oldhp int) bool {
p := &g.Player
if mp.Type != 'I' {
if g.HasHit {
g.addmsgf(". ")
@@ -156,13 +192,53 @@ func (g *RogueGame) attack(mp *Monster) (removed bool) {
}
if !mp.On(Cancelled) {
switch mp.Type {
case 'A':
if h := g.data.hitHandlers[mp.Type-'A']; h != nil {
return h(g, mp, mname)
}
}
return false
}
// monsterMiss handles a monster's whiffed swing (the miss arm of
// fight.c attack); ice monsters miss silently.
func (g *RogueGame) monsterMiss(mp *Monster, mname string) {
if mp.Type == 'I' {
return
}
p := &g.Player
if g.HasHit {
g.addmsgf(". ")
g.HasHit = false
}
if mp.Type == 'F' {
p.Stats.HP -= p.VfHit
if p.Stats.HP <= 0 {
g.death(mp.Type) // Bye bye life ...
}
}
g.miss(mname, "", false)
}
// The monster special-power handlers, dispatched through
// gameData.hitHandlers when an uncancelled monster's hit lands. Each is
// one case of the C attack switch; a true return means the monster
// removed itself from the level.
func (g *RogueGame) hitAquator(*Monster, string) bool {
// If an aquator hits, you can lose armor class.
g.rustArmor(p.CurArmor)
case 'I':
g.rustArmor(g.Player.CurArmor)
return false
}
func (g *RogueGame) hitIceMonster(_ *Monster, mname string) bool {
// The ice monster freezes you
p.Flags.Clear(Awake)
g.Player.Flags.Clear(Awake)
if g.NoCommand == 0 {
g.addmsgf("you are frozen")
@@ -178,34 +254,41 @@ func (g *RogueGame) attack(mp *Monster) (removed bool) {
if g.NoCommand > BoreLevel {
g.death('h')
}
case 'R':
// Rattlesnakes have poisonous bites
if !g.save(VsPoison) {
if !p.IsWearing(RingSustainStrength) {
g.changeStrength(-1)
if !g.Options.Terse {
g.msg("you feel a bite in your leg and now feel weaker")
} else {
g.msg("a bite has weakened you")
return false
}
func (g *RogueGame) hitRattlesnake(*Monster, string) bool {
// Rattlesnakes have poisonous bites
if g.save(VsPoison) {
return false
}
if !g.Player.IsWearing(RingSustainStrength) {
g.changeStrength(-1)
g.msg("%s", g.chooseTerse("a bite has weakened you",
"you feel a bite in your leg and now feel weaker"))
} else if !g.ToDeath {
if !g.Options.Terse {
g.msg("a bite momentarily weakens you")
} else {
g.msg("bite has no effect")
g.msg("%s", g.chooseTerse("bite has no effect",
"a bite momentarily weakens you"))
}
return false
}
}
case 'W', 'V':
// Wraiths might drain energy levels, and Vampires can
// steal max_hp
func (g *RogueGame) hitLifeDrainer(mp *Monster, _ string) bool {
// Wraiths might drain energy levels, and Vampires can steal max_hp
p := &g.Player
chance := 30
if mp.Type == 'W' {
chance = 15
}
if g.rnd(100) < chance {
if g.rnd(100) >= chance {
return false
}
var fewer int
if mp.Type == 'W' {
@@ -237,9 +320,13 @@ func (g *RogueGame) attack(mp *Monster) (removed bool) {
}
g.msg("you suddenly feel weaker")
return false
}
case 'F':
func (g *RogueGame) hitFlytrap(*Monster, string) bool {
// Venus Flytrap stops the poor guy from moving
p := &g.Player
p.Flags.Set(Held)
p.VfHit++
@@ -247,8 +334,13 @@ func (g *RogueGame) attack(mp *Monster) (removed bool) {
if p.Stats.HP--; p.Stats.HP <= 0 {
g.death('F')
}
case 'L':
return false
}
func (g *RogueGame) hitLeprechaun(mp *Monster, _ string) bool {
// Leprechaun steals some gold
p := &g.Player
lastpurse := p.Purse
p.Purse -= g.goldCalc()
@@ -262,14 +354,18 @@ func (g *RogueGame) attack(mp *Monster) (removed bool) {
g.removeMon(mp.Pos, mp, false)
removed = true
if p.Purse != lastpurse {
g.msg("your purse feels lighter")
}
case 'N':
// Nymphs steal a magic item; look through the pack and
// pick out one we like.
return true
}
func (g *RogueGame) hitNymph(mp *Monster, _ string) bool {
// Nymphs steal a magic item; look through the pack and pick out one
// we like.
p := &g.Player
var steal *Object
nobj := 0
@@ -284,40 +380,15 @@ func (g *RogueGame) attack(mp *Monster) (removed bool) {
}
}
if steal != nil {
if steal == nil {
return false
}
g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false)
removed = true
g.leavePack(steal, false, false)
g.msg("she stole %s!", g.inventoryName(steal, true))
}
}
}
} else if mp.Type != 'I' {
if g.HasHit {
g.addmsgf(". ")
g.HasHit = false
}
if mp.Type == 'F' {
p.Stats.HP -= p.VfHit
if p.Stats.HP <= 0 {
g.death(mp.Type) // Bye bye life ...
}
}
g.miss(mname, "", false)
}
if g.Options.FightFlush && !g.ToDeath {
g.flushType()
}
g.Count = 0
g.status()
return removed
return true
}
// swing returns true if the swing hits (fight.c swing).
@@ -330,7 +401,6 @@ func (g *RogueGame) swing(atLvl, opArm, wplus int) bool {
// rollAttacks rolls several attacks (fight.c roll_em).
func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool) bool {
p := &g.Player
att := &thatt.Stats
def := &thdef.Stats
@@ -342,34 +412,7 @@ func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool)
if weap == nil {
attacks = att.Dmg
} else {
hplus = weap.HPlus
dplus = weap.DPlus
if weap == p.CurWeapon {
if p.IsRing(Left, RingIncreaseDamage) {
dplus += p.CurRing[Left].Bonus
} else if p.IsRing(Left, RingDexterity) {
hplus += p.CurRing[Left].Bonus
}
if p.IsRing(Right, RingIncreaseDamage) {
dplus += p.CurRing[Right].Bonus
} else if p.IsRing(Right, RingDexterity) {
hplus += p.CurRing[Right].Bonus
}
}
attacks = weap.Damage
if hurl {
if weap.Flags.Has(Missile) && p.CurWeapon != nil &&
WeaponKind(p.CurWeapon.Which) == weap.Launch {
attacks = weap.HurlDmg
hplus += p.CurWeapon.HPlus
dplus += p.CurWeapon.DPlus
} else if weap.Launch < 0 {
attacks = weap.HurlDmg
}
}
attacks, hplus, dplus = g.weaponAttack(weap, hurl)
}
// If the creature being attacked is not running (asleep or held) then
// the attacker gets a plus four bonus to hit.
@@ -377,21 +420,7 @@ func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool)
hplus += 4
}
defArm := def.ArmorClass
if def == &p.Stats {
if p.CurArmor != nil {
defArm = p.CurArmor.ArmorClass
}
if p.IsRing(Left, RingProtection) {
defArm -= p.CurRing[Left].Bonus
}
if p.IsRing(Right, RingProtection) {
defArm -= p.CurRing[Right].Bonus
}
}
defArm := g.defenderArmor(thdef)
didHit := false
for _, atk := range attacks {
@@ -410,6 +439,78 @@ func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool)
return didHit
}
// weaponAttack picks the dice and to-hit/damage bonuses a weapon swings
// with: ring bonuses when wielded, and launcher pairing for hurled
// missiles (the weapon preamble of fight.c roll_em).
func (g *RogueGame) weaponAttack(weap *Object, hurl bool) (DiceSpec, int, int) {
p := &g.Player
hplus := weap.HPlus
dplus := weap.DPlus
if weap == p.CurWeapon {
hplus, dplus = g.wieldedRingBonus(hplus, dplus)
}
attacks := weap.Damage
if hurl {
if weap.Flags.Has(Missile) && p.CurWeapon != nil &&
WeaponKind(p.CurWeapon.Which) == weap.Launch {
attacks = weap.HurlDmg
hplus += p.CurWeapon.HPlus
dplus += p.CurWeapon.DPlus
} else if weap.Launch < 0 {
attacks = weap.HurlDmg
}
}
return attacks, hplus, dplus
}
// wieldedRingBonus folds damage and dexterity ring bonuses into the
// wielded weapon's to-hit/damage pluses (fight.c roll_em).
func (g *RogueGame) wieldedRingBonus(hplus, dplus int) (int, int) {
p := &g.Player
if p.IsRing(Left, RingIncreaseDamage) {
dplus += p.CurRing[Left].Bonus
} else if p.IsRing(Left, RingDexterity) {
hplus += p.CurRing[Left].Bonus
}
if p.IsRing(Right, RingIncreaseDamage) {
dplus += p.CurRing[Right].Bonus
} else if p.IsRing(Right, RingDexterity) {
hplus += p.CurRing[Right].Bonus
}
return hplus, dplus
}
// defenderArmor computes the defender's effective armor class: worn
// armor and protection rings when the hero defends (the def_arm
// computation of fight.c roll_em).
func (g *RogueGame) defenderArmor(thdef *Creature) int {
p := &g.Player
def := &thdef.Stats
defArm := def.ArmorClass
if def == &p.Stats {
if p.CurArmor != nil {
defArm = p.CurArmor.ArmorClass
}
if p.IsRing(Left, RingProtection) {
defArm -= p.CurRing[Left].Bonus
}
if p.IsRing(Right, RingProtection) {
defArm -= p.CurRing[Right].Bonus
}
}
return defArm
}
// cAtoi parses a leading integer like C atoi: trailing non-digits are
// ignored rather than an error.
func cAtoi(s string) int {
@@ -566,30 +667,7 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
p := &g.Player
p.Stats.Exp += tp.Stats.Exp
// If the monster was a venus flytrap, un-hold him
switch tp.Type {
case 'F':
p.Flags.Clear(Held)
p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
case 'L':
pos, ok := g.fallpos(tp.Pos)
if ok {
tp.Room.Gold = pos
}
if ok && g.Depth >= g.MaxDepth {
gold := newObject()
gold.Kind = KindGold
gold.GoldValue = g.goldCalc()
if g.save(VsMagic) {
gold.GoldValue += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
}
attachObj(&tp.Pack, gold)
}
}
g.killedSpecial(tp)
// Get rid of the monster.
mname := g.setMname(tp)
g.removeMon(tp.Pos, tp, true)
@@ -616,6 +694,37 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
}
}
// killedSpecial handles deaths with side effects: a flytrap releases its
// grip and a leprechaun drops its gold (the switch of fight.c killed).
func (g *RogueGame) killedSpecial(tp *Monster) {
p := &g.Player
// If the monster was a venus flytrap, un-hold him
switch tp.Type {
case 'F':
p.Flags.Clear(Held)
p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
case 'L':
pos, ok := g.fallpos(tp.Pos)
if ok {
tp.Room.Gold = pos
}
if ok && g.Depth >= g.MaxDepth {
gold := newObject()
gold.Kind = KindGold
gold.GoldValue = g.goldCalc()
if g.save(VsMagic) {
gold.GoldValue += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
}
attachObj(&tp.Pack, gold)
}
}
}
// flushType flushes typeahead for the fight_flush option (mach_dep.c
// flush_type / curses flushinp).
func (g *RogueGame) flushType() {

View File

@@ -58,30 +58,9 @@ func (m *MessageLine) End() int {
m.Huh = m.buf.String()
}
if m.Mpos != 0 {
m.look(false)
m.scr.Std.MvAddStr(0, m.Mpos, "--More--")
m.scr.Refresh()
if !m.MsgEsc {
m.waitForSpace()
} else {
for {
ch := m.readChar()
if ch == ' ' {
break
}
if ch == Escape {
m.buf.Reset()
m.Mpos = 0
m.newpos = 0
if m.Mpos != 0 && m.promptMore() == Escape {
return Escape
}
}
}
}
// All messages should start with uppercase, except ones that start
// with a pack addressing character
out := m.buf.String()
@@ -101,6 +80,36 @@ func (m *MessageLine) End() int {
return ^Escape
}
// promptMore shows the --More-- prompt and waits for the reader to
// acknowledge; Escape means the player bailed out (the Mpos block of
// io.c endmsg).
func (m *MessageLine) promptMore() int {
m.look(false)
m.scr.Std.MvAddStr(0, m.Mpos, "--More--")
m.scr.Refresh()
if !m.MsgEsc {
m.waitForSpace()
return ^Escape
}
for {
ch := m.readChar()
if ch == ' ' {
return ^Escape
}
if ch == Escape {
m.buf.Reset()
m.Mpos = 0
m.newpos = 0
return Escape
}
}
}
// attach wires the message line to its display and input; NewGame and
// Restore call it once the screen and game exist.
func (m *MessageLine) attach(scr *Screen, look func(bool), readChar func() byte) {
@@ -193,9 +202,7 @@ func (g *RogueGame) status() {
temp = p.CurArmor.ArmorClass
}
if s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp &&
s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&
s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg {
if g.statusUnchanged(temp) {
return
}
@@ -238,6 +245,18 @@ func (g *RogueGame) status() {
g.move(oy, ox)
}
// statusUnchanged reports whether the status line still shows current
// values, so it need not be redrawn (the shadow-variable check of io.c
// status). temp is the effective armor class.
func (g *RogueGame) statusUnchanged(temp int) bool {
s := &g.statusCache
p := &g.Player
return s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp &&
s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&
s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg
}
// waitFor sits around until the guy types the right key (io.c wait_for).
func (g *RogueGame) waitFor(ch byte) {
if ch == '\n' {

View File

@@ -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,85 +29,126 @@ 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++ {
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 := sx; x <= ex; x++ {
for x := s.sx; x <= s.ex; x++ {
if x < 0 || x >= NumCols {
continue
}
if !p.On(Blind) {
if y == hero.Y && x == hero.X {
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)
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
}
if g.lookSkips(s, pp, y, x) {
return
}
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
ch, skip := g.lookCellChar(s, tp, y, x, pp.Ch)
if skip {
return
}
continue
default:
if wakeup {
g.wakeMonster(y, x)
if !g.lookShow(s, tp, ch, y, x) {
return
}
if g.seeMonst(tp) {
if p.On(Hallucinating) {
ch = g.randomMonsterLetter()
} else {
ch = tp.Disguise
}
if s.doorStop && g.Running {
g.lookRunCheck(s, ch, y, x)
}
}
if p.On(Blind) && (y != hero.Y || x != hero.X) {
continue
// 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)
@@ -108,66 +161,88 @@ func (g *RogueGame) look(wakeup bool) {
g.addch(ch)
}
if g.DoorStop && !g.Firstmove && g.Running {
switch g.RunCh {
case 'h':
if x == ex {
continue
return true
}
case 'j':
if y == sy {
continue
// 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
}
case 'k':
if y == ey {
continue
return ch, true
default:
if s.wakeup {
g.wakeMonster(y, x)
}
case 'l':
if x == sx {
continue
if g.seeMonst(tp) {
if p.On(Hallucinating) {
return g.randomMonsterLetter(), false
}
case 'y':
if (y+x)-sumhero >= 1 {
continue
return tp.Disguise, false
}
case 'u':
if (y-x)-diffhero >= 1 {
continue
return ch, false
}
case 'n':
if (y+x)-sumhero <= -1 {
continue
}
case 'b':
if (y-x)-diffhero <= -1 {
continue
}
// 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 == hero.X || y == hero.Y {
if x == s.hero.X || y == s.hero.Y {
g.Running = false
}
case Passage:
if x == hero.X || y == hero.Y {
passcount++
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
}
if g.DoorStop && !g.Firstmove && passcount > 1 {
g.Running = false
}
if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh)
}
return false
}
// tripCh returns the character for this space, taking into account whether
@@ -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 {

View File

@@ -116,7 +116,7 @@ func (g *RogueGame) wanderer() {
// wakeMonster is what to do when the hero steps next to a monster
// (monsters.c wake_monster).
func (g *RogueGame) wakeMonster(y, x int) *Monster {
func (g *RogueGame) wakeMonster(y, x int) {
p := &g.Player
tp := g.Level.MonsterAt(y, x)
@@ -124,22 +124,63 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
panic("can't find monster in wake_monster")
}
ch := tp.Type
// Every time he sees a mean monster, it might start chasing him
if !tp.On(Awake) && g.rnd(3) != 0 && tp.On(Mean) && !tp.On(Held) &&
!p.IsWearing(RingStealth) && !p.On(Levitating) {
if g.meanWakes(tp) {
tp.Dest = &p.Pos
tp.Flags.Set(Awake)
}
if ch == 'M' && !p.On(Blind) && !p.On(Hallucinating) &&
!tp.On(Found) && !tp.On(Cancelled) && tp.On(Awake) {
if g.medusaCatches(tp) {
g.medusaGaze(tp, y, x)
}
// Let greedy ones guard gold
if tp.On(Greedy) && !tp.On(Awake) {
tp.Flags.Set(Awake)
if p.Room.GoldVal != 0 {
tp.Dest = &p.Room.Gold
} else {
tp.Dest = &p.Pos
}
}
}
// meanWakes decides whether a sleeping mean monster starts the chase
// (monsters.c wake_monster). The waking roll happens for any sleeping
// monster, as in C.
func (g *RogueGame) meanWakes(tp *Monster) bool {
p := &g.Player
return !tp.On(Awake) && g.rnd(3) != 0 && tp.On(Mean) && !tp.On(Held) &&
!p.IsWearing(RingStealth) && !p.On(Levitating)
}
// medusaCatches reports an uncovered, awake medusa the hero can see
// (monsters.c wake_monster).
func (g *RogueGame) medusaCatches(tp *Monster) bool {
p := &g.Player
return tp.Type == 'M' && !p.On(Blind) && !p.On(Hallucinating) &&
!tp.On(Found) && !tp.On(Cancelled) && tp.On(Awake)
}
// medusaGaze confuses the hero when the medusa's gaze lands (the M
// block of monsters.c wake_monster).
func (g *RogueGame) medusaGaze(tp *Monster, y, x int) {
p := &g.Player
rp := p.Room
if (rp != nil && !rp.Flags.Has(Dark)) ||
distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
if (rp == nil || rp.Flags.Has(Dark)) &&
distance(y, x, p.Pos.Y, p.Pos.X) >= LampDist {
return
}
tp.Flags.Set(Found)
if !g.save(VsMagic) {
if g.save(VsMagic) {
return
}
if p.On(Confused) {
g.Lengthen(DUnconfuse, g.spread(HuhDuration))
} else {
@@ -157,21 +198,6 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
g.msg("s gaze has confused you")
}
}
}
// Let greedy ones guard gold
if tp.On(Greedy) && !tp.On(Awake) {
tp.Flags.Set(Awake)
if p.Room.GoldVal != 0 {
tp.Dest = &p.Room.Gold
} else {
tp.Dest = &p.Pos
}
}
return tp
}
// givePack gives a pack to a monster if it deserves one (monsters.c
// give_pack).

View File

@@ -38,21 +38,102 @@ func (g *RogueGame) moveHero(dy, dx int) {
}
for {
ch, fl, stop := g.moveTarget(nh)
if stop {
return
}
turned, ndy, ndx := g.moveResolve(nh, ch, fl, dy, dx)
if !turned {
return
}
// the C goto over: re-check the turned move
dy, dx = ndy, ndx
g.turnRefresh()
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
}
}
// moveResolve acts on the square the hero stepped at: a wall may turn a
// passage runner (reported with the new deltas); anything else completes
// or refuses the move (the switch of move.c do_move).
func (g *RogueGame) moveResolve(nh Coord, ch byte, fl PlaceFlags, dy, dx int) (bool, int, int) {
switch ch {
case ' ', '|', '-':
if turn, ndy, ndx := g.passageTurn(dy, dx); turn {
return true, ndy, ndx
}
g.Running = false
g.After = false
default:
g.moveEnter(nh, fl, ch)
}
return false, 0, 0
}
// moveEnter completes a step onto a walkable square: doors, traps,
// passages, floor, and things (the entry arms of the move.c do_move
// switch).
func (g *RogueGame) moveEnter(nh Coord, fl PlaceFlags, ch byte) {
p := &g.Player
switch ch {
case Door:
g.Running = false
if g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Has(FPassage) {
g.enterRoom(nh)
}
case Trap:
tr := g.springTrap(nh)
if tr == TrapDoor || tr == TrapTeleport {
return
}
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)
case Floor:
if !fl.Has(FReal) {
g.springTrap(p.Pos)
}
default:
g.moveOnto(nh, fl, ch)
return
}
g.finishMove(nh, fl)
}
// offMap reports coordinates outside the walkable map (move.c do_move).
func offMap(nh Coord) bool {
return nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1
}
// moveTarget inspects the square the hero is stepping onto: bounds and
// diagonal legality, hidden traps underfoot, and being held. stop means
// the move is refused (the checks of move.c do_move).
func (g *RogueGame) moveTarget(nh Coord) (byte, PlaceFlags, bool) {
p := &g.Player
// 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
if offMap(nh) {
return ' ', 0, false // fall into the wall case
}
var (
ch byte
fl PlaceFlags
)
if !hitBound {
if !g.diagOk(p.Pos, nh) {
g.After = false
g.Running = false
return
return 0, 0, true
}
if g.Running && p.Pos == nh {
@@ -60,9 +141,9 @@ func (g *RogueGame) moveHero(dy, dx int) {
g.Running = false
}
fl = *g.Level.FlagsAt(nh.Y, nh.X)
fl := *g.Level.FlagsAt(nh.Y, nh.X)
ch = g.Level.VisibleChar(nh.Y, nh.X)
ch := g.Level.VisibleChar(nh.Y, nh.X)
if !fl.Has(FReal) && ch == Floor {
if !p.On(Levitating) {
ch = Trap
@@ -72,56 +153,16 @@ func (g *RogueGame) moveHero(dy, dx int) {
} else if p.On(Held) && ch != 'F' {
g.msg("you are being held")
return
}
return 0, 0, true
}
if hitBound {
ch = ' ' // fall into the wall case below
return ch, fl, false
}
switch ch {
case ' ', '|', '-':
if turn, ndy, ndx := g.passageTurn(dy, dx); turn {
dy, dx = ndy, ndx
g.turnRefresh()
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
continue // the C goto over: re-check the turned move
}
g.Running = false
g.After = false
case Door:
g.Running = false
if g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Has(FPassage) {
g.enterRoom(nh)
}
g.finishMove(nh, fl)
case Trap:
tr := g.springTrap(nh)
if tr == TrapDoor || tr == TrapTeleport {
return
}
g.finishMove(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.finishMove(nh, fl)
case Floor:
if !fl.Has(FReal) {
g.springTrap(p.Pos)
}
g.finishMove(nh, fl)
default:
// moveOnto handles stepping at a monster or onto an item (the default
// arm of the move.c do_move switch).
func (g *RogueGame) moveOnto(nh Coord, fl PlaceFlags, ch byte) {
p := &g.Player
if ch == Stairs {
g.SeenStairs = true
}
@@ -138,10 +179,6 @@ func (g *RogueGame) moveHero(dy, dx int) {
}
}
return
}
}
// passageTurn checks whether a runner in a gone-room passage should turn
// the corner instead of stopping at a wall (the PASSGO block of move.c
// do_move). It reports whether to turn and the new deltas, updating RunCh.
@@ -152,44 +189,66 @@ func (g *RogueGame) passageTurn(dy, dx int) (bool, int, int) {
return false, dy, dx
}
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
}
return true, dy, 0
if turn, ndy := g.passageTurnVertical(); turn {
return true, ndy, 0
}
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
}
return true, 0, dx
if turn, ndx := g.passageTurnHorizontal(); turn {
return true, 0, ndx
}
}
return false, dy, dx
}
// passageTurnVertical decides whether a horizontal runner turns up or
// down at a corner (move.c do_move).
func (g *RogueGame) passageTurnVertical() (bool, int) {
p := &g.Player
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 {
return false, 0
}
if b1 {
g.RunCh = 'k'
return true, -1
}
g.RunCh = 'j'
return true, 1
}
// passageTurnHorizontal decides whether a vertical runner turns left or
// right at a corner (move.c do_move).
func (g *RogueGame) passageTurnHorizontal() (bool, int) {
p := &g.Player
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 {
return false, 0
}
if b1 {
g.RunCh = 'h'
return true, -1
}
g.RunCh = 'l'
return true, 1
}
// finishMove is the move_stuff label in do_move: complete the step.
func (g *RogueGame) finishMove(nh Coord, fl PlaceFlags) {
p := &g.Player
@@ -255,16 +314,32 @@ func (g *RogueGame) springTrap(tc Coord) TrapKind {
tr := TrapKind(pp.Flags & FTrapMask)
pp.Flags.Set(FSeen)
switch tr {
case TrapDoor:
if h := g.data.trapHandlers[tr]; h != nil {
h(g, tc)
}
g.flushType()
return tr
}
// The per-trap effect handlers, dispatched through
// gameData.trapHandlers. Each is one case of the C be_trapped switch.
func (g *RogueGame) trapFall(Coord) {
g.Depth++
g.NewLevel()
g.msg("you fell into a trap!")
case TrapBear:
}
func (g *RogueGame) trapBear(Coord) {
g.NoMove += g.spread(3) // BEARTIME
g.msg("you are caught in a bear trap")
case TrapMystery:
switch g.rnd(11) {
}
func (g *RogueGame) trapMystery(Coord) {
which := g.rnd(11)
switch which {
case 0:
g.msg("you are suddenly in a parallel dimension")
case 1:
@@ -277,6 +352,14 @@ func (g *RogueGame) springTrap(tc Coord) TrapKind {
g.msg("a %s light flashes in your eyes", g.data.rainbow[g.rnd(len(g.data.rainbow))])
case 5:
g.msg("a spike shoots past your ear!")
default:
g.trapMysteryMore(which)
}
}
// trapMysteryMore holds the back half of the mystery-trap messages.
func (g *RogueGame) trapMysteryMore(which int) {
switch which {
case 6:
g.msg("%s sparks dance across your armor", g.data.rainbow[g.rnd(len(g.data.rainbow))])
case 7:
@@ -288,12 +371,17 @@ func (g *RogueGame) springTrap(tc Coord) TrapKind {
case 10:
g.msg("you pack turns %s!", g.data.rainbow[g.rnd(len(g.data.rainbow))])
}
case TrapSleep:
}
func (g *RogueGame) trapSleep(Coord) {
g.NoCommand += g.spread(5) // SLEEPTIME
p.Flags.Clear(Awake)
g.Player.Flags.Clear(Awake)
g.msg("a strange white mist envelops you and you fall asleep")
case TrapArrow:
}
func (g *RogueGame) trapArrow(Coord) {
p := &g.Player
if g.swing(p.Stats.Lvl-1, p.Stats.ArmorClass, 1) {
p.Stats.HP -= g.roll(1, 6)
if p.Stats.HP <= 0 {
@@ -310,15 +398,23 @@ func (g *RogueGame) springTrap(tc Coord) TrapKind {
g.fall(arrow, false)
g.msg("an arrow shoots past you")
}
case TrapTeleport:
}
func (g *RogueGame) trapTeleport(tc Coord) {
// 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 TrapDart:
}
func (g *RogueGame) trapDart(Coord) {
p := &g.Player
if !g.swing(p.Stats.Lvl+1, p.Stats.ArmorClass, 1) {
g.msg("a small dart whizzes by your ear and vanishes")
} else {
return
}
p.Stats.HP -= g.roll(1, 4)
if p.Stats.HP <= 0 {
g.msg("a poisoned dart killed you")
@@ -331,14 +427,10 @@ func (g *RogueGame) springTrap(tc Coord) TrapKind {
g.msg("a small dart just hit you in the shoulder")
}
case TrapRust:
func (g *RogueGame) trapRust(Coord) {
g.msg("a gush of water hits you on the head")
g.rustArmor(p.CurArmor)
}
g.flushType()
return tr
g.rustArmor(g.Player.CurArmor)
}
// randomStep moves in a random direction if the monster/person is

View File

@@ -38,6 +38,19 @@ func TestNewLevelInvariants(t *testing.T) {
for _, seed := range []int32{1, 12345, 2026, 99999} {
g := genLevel(t, seed)
checkHeroPlacement(t, g, seed)
checkRoomsDrawn(t, g, seed)
checkMonstersPlaced(t, g, seed)
checkObjectsPlaced(t, g, seed)
checkStartingKit(t, g, seed)
}
}
// checkHeroPlacement verifies the staircase and hero landed on valid,
// unoccupied cells.
func checkHeroPlacement(t *testing.T, g *RogueGame, seed int32) {
t.Helper()
// The staircase is somewhere real.
st := g.Level.Stairs
if g.Level.Char(st.Y, st.X) != Stairs {
@@ -58,8 +71,12 @@ func TestNewLevelInvariants(t *testing.T) {
if g.Player.Room == nil {
t.Errorf("seed %d: hero not in any room", seed)
}
}
// checkRoomsDrawn verifies rooms and floor/passages appear on the map.
func checkRoomsDrawn(t *testing.T, g *RogueGame, seed int32) {
t.Helper()
// Some rooms exist and are drawn.
m := renderMap(g)
if !strings.Contains(m, "|") || !strings.Contains(m, "-") {
t.Errorf("seed %d: no room walls drawn", seed)
@@ -68,8 +85,13 @@ func TestNewLevelInvariants(t *testing.T) {
if !strings.Contains(m, ".") && !strings.Contains(m, "#") {
t.Errorf("seed %d: no floor or passages drawn", seed)
}
}
// checkMonstersPlaced verifies every monster is indexed on the map and
// placed in a room.
func checkMonstersPlaced(t *testing.T, g *RogueGame, seed int32) {
t.Helper()
// Every monster is indexed on the map and placed in a room.
for _, mon := range g.Level.Monsters {
if g.Level.MonsterAt(mon.Pos.Y, mon.Pos.X) != mon {
t.Errorf("seed %d: monster %c not indexed at its position",
@@ -80,9 +102,14 @@ func TestNewLevelInvariants(t *testing.T) {
t.Errorf("seed %d: monster %c has no room", seed, mon.Type)
}
}
}
// checkObjectsPlaced verifies every level object sits on a cell
// displaying its type (items can share cells only with monsters standing
// on them).
func checkObjectsPlaced(t *testing.T, g *RogueGame, seed int32) {
t.Helper()
// Every level object sits on a cell displaying its type (items can
// share cells only with monsters standing on them).
for _, obj := range g.Level.Objects {
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
if ch != obj.Kind.Glyph() &&
@@ -91,8 +118,13 @@ func TestNewLevelInvariants(t *testing.T) {
seed, obj.Kind, obj.Pos.Y, obj.Pos.X, ch)
}
}
}
// checkStartingKit verifies the player has her starting kit: food, armor,
// mace, bow, arrows.
func checkStartingKit(t *testing.T, g *RogueGame, seed int32) {
t.Helper()
// The player has her starting kit: food, armor, mace, bow, arrows.
if len(g.Player.Pack) != 5 {
t.Errorf("seed %d: starting pack has %d items, want 5",
seed, len(g.Player.Pack))
@@ -108,7 +140,6 @@ func TestNewLevelInvariants(t *testing.T) {
t.Errorf("seed %d: not wearing the starting ring mail", seed)
}
}
}
func TestNewLevelDeterministic(t *testing.T) {
a := renderMap(genLevel(t, 12345))

View File

@@ -77,17 +77,9 @@ func (k ObjectKind) String() string {
return "suit of armor"
case KindRing:
return ringName
case KindWand:
return "wand or staff"
case KindAmulet:
return "amulet"
case KindGold:
return goldName
case KindRingOrStick:
return "ring, wand or staff"
default:
return k.stringRest()
}
return "bizarre thing"
}
// objectKindForGlyph is the reverse of Glyph: what category of item does a
@@ -123,6 +115,23 @@ func (k ObjectKind) MergesInPack() bool {
return k == KindPotion || k == KindScroll || k == KindFood
}
// stringRest names the remaining kinds, including the ring-or-stick
// prompt pseudo-kind (the tail of the C type_name switch).
func (k ObjectKind) stringRest() string {
switch k {
case KindWand:
return "wand or staff"
case KindAmulet:
return "amulet"
case KindGold:
return goldName
case KindRingOrStick:
return "ring, wand or staff"
}
return "bizarre thing"
}
// Object is the _o arm of the C THING union: anything that can lie on the
// floor or ride in a pack.
type Object struct {

View File

@@ -205,55 +205,11 @@ func (g *RogueGame) getStr(opt *string, win *Window) int {
)
for {
c = g.readchar()
if c == '\n' || c == '\r' || c == Escape {
if endsInput(c) || (len(buf) == 0 && c == '-' && !onStd) {
break
}
if c == 8 || c == 0x7f { // erase character
if len(buf) > 0 {
buf = buf[:len(buf)-1]
win.Move(oy, ox+len(displayStr(buf)))
}
win.Clrtoeol()
g.scr.RefreshWin(win)
continue
}
if c == CTRL('U') { // kill character
buf = buf[:0]
win.Move(oy, ox)
win.Clrtoeol()
g.scr.RefreshWin(win)
continue
}
if len(buf) == 0 {
if c == '-' && !onStd {
break
}
if c == '~' {
buf = append(buf, g.Home...)
win.AddStr(g.Home)
win.Clrtoeol()
g.scr.RefreshWin(win)
continue
}
}
if len(buf) >= MaxInp || (!isPrint(c) && c != ' ') {
continue // C beeps here
}
buf = append(buf, c)
win.AddStr(unctrl(c))
win.Clrtoeol()
g.scr.RefreshWin(win)
buf = g.getStrEdit(win, buf, c, oy, ox)
}
if len(buf) > 0 { // only change option if something has been typed
@@ -267,6 +223,12 @@ func (g *RogueGame) getStr(opt *string, win *Window) int {
g.Msgs.Mpos += len(buf)
}
return getStrResult(c)
}
// getStrResult maps the terminating key to the C return code (options.c
// get_str).
func getStrResult(c byte) int {
switch c {
case '-':
return Minus
@@ -277,6 +239,52 @@ func (g *RogueGame) getStr(opt *string, win *Window) int {
}
}
// endsInput reports the keys that finish line input (options.c get_str).
func endsInput(c byte) bool {
return c == '\n' || c == '\r' || c == Escape
}
// getStrErase deletes the last character of the buffer (options.c
// get_str).
func getStrErase(win *Window, buf []byte, oy, ox int) []byte {
if len(buf) > 0 {
buf = buf[:len(buf)-1]
win.Move(oy, ox+len(displayStr(buf)))
}
win.Clrtoeol()
return buf
}
// getStrEdit applies one key to the line editor's buffer: erase, kill,
// home expansion, or a typed character (options.c get_str).
func (g *RogueGame) getStrEdit(win *Window, buf []byte, c byte, oy, ox int) []byte {
switch {
case c == 8 || c == 0x7f:
buf = getStrErase(win, buf, oy, ox)
case c == CTRL('U'): // kill character
buf = buf[:0]
win.Move(oy, ox)
win.Clrtoeol()
case len(buf) == 0 && c == '~':
buf = append(buf, g.Home...)
win.AddStr(g.Home)
win.Clrtoeol()
case len(buf) >= MaxInp || (!isPrint(c) && c != ' '):
return buf // C beeps here
default:
buf = append(buf, c)
win.AddStr(unctrl(c))
win.Clrtoeol()
}
g.scr.RefreshWin(win)
return buf
}
// displayStr renders a buffer the way the input echo did.
func displayStr(buf []byte) string {
var sb strings.Builder
@@ -337,20 +345,49 @@ func (g *RogueGame) ParseOpts(str string) {
i++
}
name := str[:i]
rest := str[i:]
matched := false
rest := g.parseOptName(optlist, str[:i], str[i:])
// skip to start of next option name
for rest != "" && !isAlpha(rest[0]) {
rest = rest[1:]
}
str = rest
}
}
// parseOptName applies one named option, returning the unconsumed
// remainder: "name" turns a boolean on, "noname" turns it off, and
// string options consume a value (the option scan of options.c
// parse_opts).
func (g *RogueGame) parseOptName(optlist []optDesc, name, rest string) string {
for oi := range optlist {
op := &optlist[oi]
isBoolOpt := op.kind == optBool || op.kind == optSeeFloor
if strings.HasPrefix(op.name, name) && name != "" {
matched = true
if isBoolOpt {
*op.boolP = true
} else {
return rest
}
return g.parseOptValue(op, rest)
}
if isBoolOpt && strings.HasPrefix(name, "no") &&
strings.HasPrefix(op.name, name[2:]) {
*op.boolP = false
return rest
}
}
return rest
}
// parseOptValue consumes an option's "=value" from rest, storing it,
// and returns the remainder (the string arm of options.c parse_opts).
func (g *RogueGame) parseOptValue(op *optDesc, rest string) string {
// Skip to start of string value
for rest != "" && rest[0] == '=' {
rest = rest[1:]
@@ -374,45 +411,31 @@ func (g *RogueGame) ParseOpts(str string) {
}
word := val[:end]
rest = val[end:]
if op.kind == optInvT {
g.parseInvType(op, word)
} else {
*op.strP = prefix + strucpy(word)
}
return val[end:]
}
// parseInvType matches an inventory-style name by prefix (options.c
// parse_opts).
func (g *RogueGame) parseInvType(op *optDesc, word string) {
// check for type of inventory
w := word
if w != "" {
w = string(toUpper(w[0])) + w[1:]
if word != "" {
word = string(toUpper(word[0])) + word[1:]
}
for ti, tn := range g.data.invTName {
if strings.HasPrefix(tn, w) {
if strings.HasPrefix(tn, word) {
*op.intP = ti
break
}
}
} else {
*op.strP = prefix + strucpy(word)
}
}
break
} else if isBoolOpt && strings.HasPrefix(name, "no") &&
strings.HasPrefix(op.name, name[2:]) {
matched = true
*op.boolP = false
break
}
}
_ = matched
// skip to start of next option name
for rest != "" && !isAlpha(rest[0]) {
rest = rest[1:]
}
str = rest
}
}
// strucpy copies a string keeping only printable characters, capped at

View File

@@ -17,107 +17,15 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
}
// Check for and deal with scare monster scrolls
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster && obj.Flags.Has(WasFound) {
g.Level.RemoveObject(obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
}
g.msg("the scroll turns to dust as you pick it up")
if g.pickupScareScroll(obj) {
return
}
if len(p.Pack) == 0 {
p.Pack = append(p.Pack, obj)
obj.PackCh = p.nextPackChar()
p.Inpack++
} else {
// Walk the pack looking for the insertion point, keeping items of
// one type together and merging stackable/grouped items — a direct
// translation of the C linked-list walk. lp is the index to insert
// after; -1 after a merge means no insertion.
lp := -1
merged := false
for i := 0; i < len(p.Pack); i++ {
if p.Pack[i].Kind != obj.Kind {
lp = i
continue
}
// found the group of our type: scan for matching subtype
for p.Pack[i].Kind == obj.Kind && p.Pack[i].Which != obj.Which {
lp = i
if i+1 >= len(p.Pack) {
break
}
i++
}
op := p.Pack[i]
if op.Kind == obj.Kind && op.Which == obj.Which {
switch {
case op.Kind.MergesInPack():
if !g.packRoom(fromFloor, obj) {
obj, ok := g.packInsert(obj, fromFloor)
if !ok {
return
}
op.Count++
obj = op
lp = -1
merged = true
case obj.Group != 0:
lp = i
for p.Pack[i].Kind == obj.Kind &&
p.Pack[i].Which == obj.Which &&
p.Pack[i].Group != obj.Group {
lp = i
if i+1 >= len(p.Pack) {
break
}
i++
}
op = p.Pack[i]
if op.Kind == obj.Kind && op.Which == obj.Which &&
op.Group == obj.Group {
op.Count += obj.Count
p.Inpack--
if !g.packRoom(fromFloor, obj) {
return
}
obj = op
lp = -1
merged = true
}
default:
lp = i
}
}
break
}
if !merged && lp != -1 {
if !g.packRoom(fromFloor, obj) {
return
}
obj.PackCh = p.nextPackChar()
p.Pack = append(p.Pack[:lp+1],
append([]*Object{obj}, p.Pack[lp+1:]...)...)
}
}
obj.Flags.Set(WasFound)
// If this was the object of something's desire, that monster will get
@@ -141,6 +49,163 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
}
}
// pickupScareScroll crumbles a found scare monster scroll when it is
// picked up again; it reports whether it did (pack.c add_pack).
func (g *RogueGame) pickupScareScroll(obj *Object) bool {
if obj.Kind != KindScroll || obj.ScrollKind() != ScrollScareMonster ||
!obj.Flags.Has(WasFound) {
return false
}
p := &g.Player
g.Level.RemoveObject(obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
}
g.msg("the scroll turns to dust as you pick it up")
return true
}
// packInsert places the object in the pack, keeping items of one type
// together and merging stackable and grouped items — a translation of
// the C linked-list walk in pack.c add_pack. It returns the pack entry
// the object ended up as; ok is false when the pack has no room.
func (g *RogueGame) packInsert(obj *Object, fromFloor bool) (*Object, bool) {
p := &g.Player
if len(p.Pack) == 0 {
p.Pack = append(p.Pack, obj)
obj.PackCh = p.nextPackChar()
p.Inpack++
return obj, true
}
merged := false
// lp is the index to insert after; -1 after a merge means no
// insertion.
i, lp := packScanKind(p.Pack, obj.Kind)
if i < len(p.Pack) {
i, lp = packScanWhich(p.Pack, obj, i, lp)
if op := p.Pack[i]; op.Kind == obj.Kind && op.Which == obj.Which {
var ok bool
obj, lp, merged, ok = g.packMatch(obj, op, i, fromFloor)
if !ok {
return nil, false
}
}
}
if !merged && lp != -1 {
if !g.packRoom(fromFloor, obj) {
return nil, false
}
obj.PackCh = p.nextPackChar()
p.Pack = append(p.Pack[:lp+1],
append([]*Object{obj}, p.Pack[lp+1:]...)...)
}
return obj, true
}
// packScanKind scans to the first pack entry of this kind, returning
// its index (or the pack length) and the entry to insert after (the
// outer scan of pack.c add_pack).
func packScanKind(pack []*Object, kind ObjectKind) (int, int) {
lp := -1
i := 0
for ; i < len(pack); i++ {
if pack[i].Kind == kind {
break
}
lp = i
}
return i, lp
}
// packScanWhich scans within the kind group for the matching subtype
// (the inner scan of pack.c add_pack).
func packScanWhich(pack []*Object, obj *Object, i, lp int) (int, int) {
for pack[i].Kind == obj.Kind && pack[i].Which != obj.Which {
lp = i
if i+1 >= len(pack) {
break
}
i++
}
return i, lp
}
// packMatch merges the object with a matching pack entry when possible:
// stackables merge counts, grouped missiles rejoin their bundle, and
// anything else marks the insertion point (the matched-subtype switch
// of pack.c add_pack). It returns the resulting entry, the insert-after
// index, whether a merge happened, and ok false when the pack is full.
func (g *RogueGame) packMatch(obj, op *Object, i int, fromFloor bool) (*Object, int, bool, bool) {
switch {
case op.Kind.MergesInPack():
if !g.packRoom(fromFloor, obj) {
return nil, 0, false, false
}
op.Count++
return op, -1, true, true
case obj.Group != 0:
return g.packMatchGroup(obj, i, fromFloor)
default:
return obj, i, false, true
}
}
// packMatchGroup rejoins a grouped missile bundle with its group entry
// (the o_group arm of pack.c add_pack).
func (g *RogueGame) packMatchGroup(obj *Object, i int, fromFloor bool) (*Object, int, bool, bool) {
p := &g.Player
lp := i
for p.Pack[i].Kind == obj.Kind &&
p.Pack[i].Which == obj.Which &&
p.Pack[i].Group != obj.Group {
lp = i
if i+1 >= len(p.Pack) {
break
}
i++
}
op := p.Pack[i]
if op.Kind == obj.Kind && op.Which == obj.Which &&
op.Group == obj.Group {
op.Count += obj.Count
p.Inpack--
if !g.packRoom(fromFloor, obj) {
return nil, 0, false, false
}
return op, -1, true, true
}
return obj, lp, false, true
}
// packRoom sees if there's room in the pack; if not, prints an appropriate
// message (pack.c pack_room).
func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
@@ -219,18 +284,11 @@ func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
}
if g.NObjs == 0 {
if g.Options.Terse {
if kind == KindNone {
g.msg("empty handed")
g.msg("%s", g.chooseTerse("empty handed", "you are empty handed"))
} else {
g.msg("nothing appropriate")
}
} else {
if kind == KindNone {
g.msg("you are empty handed")
} else {
g.msg("you don't have anything appropriate")
}
g.msg("%s", g.chooseTerse("nothing appropriate",
"you don't have anything appropriate"))
}
return false
@@ -335,8 +393,9 @@ func (g *RogueGame) pickyInven() {
}
// promptPackItem picks something out of a pack for a purpose (pack.c
// get_item); ok reports whether the player chose an item.
func (g *RogueGame) promptPackItem(purpose string, kind ObjectKind) (obj *Object, ok bool) {
// get_item); the second result reports whether the player chose an
// item.
func (g *RogueGame) promptPackItem(purpose string, kind ObjectKind) (*Object, bool) {
p := &g.Player
if len(p.Pack) == 0 {
g.msg("you aren't carrying anything")
@@ -345,27 +404,12 @@ func (g *RogueGame) promptPackItem(purpose string, kind ObjectKind) (obj *Object
}
if g.Again {
if g.LastPick != nil {
return g.LastPick, true
}
g.msg("you ran out")
return nil, false
return g.repeatLastItem()
}
for {
if !g.Options.Terse {
g.addmsgf("which object do you want to ")
}
g.promptItemPurpose(purpose)
g.addmsgf("%s", purpose)
if g.Options.Terse {
g.addmsgf(" what")
}
g.msg("? (* for list): ")
ch := g.readchar()
g.Msgs.Mpos = 0
// Give the poor player a chance to abort the command
@@ -399,6 +443,34 @@ func (g *RogueGame) promptPackItem(purpose string, kind ObjectKind) (obj *Object
}
}
// repeatLastItem replays the previous selection for the repeat command
// (pack.c get_item).
func (g *RogueGame) repeatLastItem() (*Object, bool) {
if g.LastPick != nil {
return g.LastPick, true
}
g.msg("you ran out")
return nil, false
}
// promptItemPurpose prints the "which object do you want to ...?"
// prompt (pack.c get_item).
func (g *RogueGame) promptItemPurpose(purpose string) {
if !g.Options.Terse {
g.addmsgf("which object do you want to ")
}
g.addmsgf("%s", purpose)
if g.Options.Terse {
g.addmsgf(" what")
}
g.msg("? (* for list): ")
}
// money adds or subtracts gold from the pack (pack.c money).
func (g *RogueGame) money(value int) {
p := &g.Player

View File

@@ -15,204 +15,234 @@ func (g *RogueGame) digPassages() {
r1 := g.rnd(MaxRooms)
ingraph[r1] = true
for {
for roomcount < MaxRooms {
// find a room to connect with
j := 0
r2 := -1
for i := range MaxRooms {
if g.data.rdesConn[r1][i] && !ingraph[i] {
if j++; g.rnd(j) == 0 {
r2 = i
}
}
}
if j == 0 {
// if no adjacent rooms are outside the graph, pick a new room
// to look from
r2 := g.pickNeighbor(r1, func(i int) bool { return !ingraph[i] })
if r2 < 0 {
// if no adjacent rooms are outside the graph, pick a new
// room to look from
for {
r1 = g.rnd(MaxRooms)
if ingraph[r1] {
break
}
}
} else {
continue
}
// otherwise, connect new room to the graph, and draw a tunnel
// to it
ingraph[r2] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
ingraph[r2] = true
g.connectRooms(r1, r2)
isconn[r1][r2] = true
isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
isconn[r2][r1] = true
roomcount++
}
if roomcount >= MaxRooms {
break
}
}
// attempt to add passages to the graph a random number of times so that
// there isn't always just one unique passage through it.
for roomcount = g.rnd(5); roomcount > 0; roomcount-- {
r1 = g.rnd(MaxRooms) // a random room to look from
// find an adjacent room not already connected
j := 0
r2 := -1
for i := range MaxRooms {
if g.data.rdesConn[r1][i] && !isconn[r1][i] {
if j++; g.rnd(j) == 0 {
r2 = i
}
}
}
// if there is one, connect it and look for the next added passage
if j != 0 {
// find an adjacent room not already connected; if there is one,
// connect it and look for the next added passage
r2 := g.pickNeighbor(r1, func(i int) bool { return !isconn[r1][i] })
if r2 >= 0 {
g.connectRooms(r1, r2)
isconn[r1][r2] = true
isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
isconn[r2][r1] = true
}
}
g.numberPassages()
}
// pickNeighbor reservoir-picks an adjacent room for which ok holds, or
// -1 when there is none (passages.c do_passages).
func (g *RogueGame) pickNeighbor(r1 int, ok func(int) bool) int {
j := 0
r2 := -1
for i := range MaxRooms {
if g.data.rdesConn[r1][i] && ok(i) {
if j++; g.rnd(j) == 0 {
r2 = i
}
}
}
return r2
}
// corridorPlan is the movement setup connectRooms computes before it
// digs (the local variables of passages.c conn).
type corridorPlan struct {
rpf, rpt *Room // the rooms being joined
del Coord // direction of move
turnDelta Coord // direction to turn
spos, epos Coord // start and end of move
distance, turnDistance int // how far to move and to turn
}
// connectRooms draws a corridor from a room in a certain direction
// (passages.c conn).
func (g *RogueGame) connectRooms(r1, r2 int) {
var (
rm int
direc byte
)
if r1 < r2 {
rm = r1
if r1+1 == r2 {
direc = 'r'
} else {
direc = 'd'
}
} else {
rm = r2
if r2+1 == r1 {
direc = 'r'
} else {
direc = 'd'
}
}
rpf := &g.Level.Rooms[rm]
// Set up the movement variables, in two cases: first drawing one down.
var (
rpt *Room
del, turnDelta, spos, epos Coord
distance, turnDistance int
)
rm, direc := connOrient(r1, r2)
var plan corridorPlan
if direc == 'd' {
rmt := rm + 3 // room # of dest
rpt = &g.Level.Rooms[rmt] // room pointer of dest
del = Coord{X: 0, Y: 1} // direction of move
spos = rpf.Pos // start of move
epos = rpt.Pos // end of move
if !rpf.Flags.Has(Gone) { // if not gone pick door pos
for {
spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1
spos.Y = rpf.Pos.Y + rpf.Max.Y - 1
if !rpf.Flags.Has(Maze) || g.Level.FlagsAt(spos.Y, spos.X).Has(FPassage) {
break
}
}
}
if !rpt.Flags.Has(Gone) {
for {
epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1
if !rpt.Flags.Has(Maze) || g.Level.FlagsAt(epos.Y, epos.X).Has(FPassage) {
break
}
}
}
distance = abs(spos.Y-epos.Y) - 1 // distance to move
turnDelta.Y = 0 // direction to turn
if spos.X < epos.X {
turnDelta.X = 1
plan = g.connPlanDown(rm)
} else {
turnDelta.X = -1
plan = g.connPlanRight(rm)
}
turnDistance = abs(spos.X - epos.X) // how far to turn
} else { // setup for moving right
rmt := rm + 1
rpt = &g.Level.Rooms[rmt]
del = Coord{X: 1, Y: 0}
spos = rpf.Pos
epos = rpt.Pos
if !rpf.Flags.Has(Gone) {
for {
spos.X = rpf.Pos.X + rpf.Max.X - 1
spos.Y = rpf.Pos.Y + g.rnd(rpf.Max.Y-2) + 1
if !rpf.Flags.Has(Maze) || g.Level.FlagsAt(spos.Y, spos.X).Has(FPassage) {
break
}
}
}
if !rpt.Flags.Has(Gone) {
for {
epos.Y = rpt.Pos.Y + g.rnd(rpt.Max.Y-2) + 1
if !rpt.Flags.Has(Maze) || g.Level.FlagsAt(epos.Y, epos.X).Has(FPassage) {
break
}
}
}
distance = abs(spos.X-epos.X) - 1
if spos.Y < epos.Y {
turnDelta.Y = 1
} else {
turnDelta.Y = -1
}
turnDelta.X = 0
turnDistance = abs(spos.Y - epos.Y)
}
turnSpot := g.rnd(distance-1) + 1 // where turn starts
turnSpot := g.rnd(plan.distance-1) + 1 // where turn starts
// Draw in the doors on either side of the passage or just put #'s if
// the rooms are gone.
if !rpf.Flags.Has(Gone) {
g.door(rpf, spos)
} else {
g.putPassage(spos)
g.connEnd(plan.rpf, plan.spos)
g.connEnd(plan.rpt, plan.epos)
g.digCorridor(plan, turnSpot)
}
// connOrient picks the upper-left room of the pair and the digging
// direction: right for horizontal neighbors, down otherwise (passages.c
// conn).
func connOrient(r1, r2 int) (int, byte) {
rm := min(r1, r2)
if abs(r1-r2) == 1 {
return rm, 'r'
}
return rm, 'd'
}
// connPlanDown sets up the movement variables for a corridor drawn
// downward (passages.c conn).
func (g *RogueGame) connPlanDown(rm int) corridorPlan {
rpf := &g.Level.Rooms[rm]
rpt := &g.Level.Rooms[rm+3] // room pointer of dest
plan := corridorPlan{
rpf: rpf,
rpt: rpt,
del: Coord{X: 0, Y: 1}, // direction of move
spos: rpf.Pos, // start of move
epos: rpt.Pos, // end of move
}
if !rpf.Flags.Has(Gone) { // if not gone pick door pos
for {
plan.spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1
plan.spos.Y = rpf.Pos.Y + rpf.Max.Y - 1
if !rpf.Flags.Has(Maze) ||
g.Level.FlagsAt(plan.spos.Y, plan.spos.X).Has(FPassage) {
break
}
}
}
if !rpt.Flags.Has(Gone) {
g.door(rpt, epos)
} else {
g.putPassage(epos)
for {
plan.epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1
if !rpt.Flags.Has(Maze) ||
g.Level.FlagsAt(plan.epos.Y, plan.epos.X).Has(FPassage) {
break
}
// Get ready to move...
curr := spos
}
}
plan.distance = abs(plan.spos.Y-plan.epos.Y) - 1 // distance to move
plan.turnDelta.Y = 0 // direction to turn
if plan.spos.X < plan.epos.X {
plan.turnDelta.X = 1
} else {
plan.turnDelta.X = -1
}
plan.turnDistance = abs(plan.spos.X - plan.epos.X) // how far to turn
return plan
}
// connPlanRight sets up the movement variables for a corridor drawn to
// the right (passages.c conn).
func (g *RogueGame) connPlanRight(rm int) corridorPlan {
rpf := &g.Level.Rooms[rm]
rpt := &g.Level.Rooms[rm+1]
plan := corridorPlan{
rpf: rpf,
rpt: rpt,
del: Coord{X: 1, Y: 0},
spos: rpf.Pos,
epos: rpt.Pos,
}
if !rpf.Flags.Has(Gone) {
for {
plan.spos.X = rpf.Pos.X + rpf.Max.X - 1
plan.spos.Y = rpf.Pos.Y + g.rnd(rpf.Max.Y-2) + 1
if !rpf.Flags.Has(Maze) ||
g.Level.FlagsAt(plan.spos.Y, plan.spos.X).Has(FPassage) {
break
}
}
}
if !rpt.Flags.Has(Gone) {
for {
plan.epos.Y = rpt.Pos.Y + g.rnd(rpt.Max.Y-2) + 1
if !rpt.Flags.Has(Maze) ||
g.Level.FlagsAt(plan.epos.Y, plan.epos.X).Has(FPassage) {
break
}
}
}
plan.distance = abs(plan.spos.X-plan.epos.X) - 1
if plan.spos.Y < plan.epos.Y {
plan.turnDelta.Y = 1
} else {
plan.turnDelta.Y = -1
}
plan.turnDelta.X = 0
plan.turnDistance = abs(plan.spos.Y - plan.epos.Y)
return plan
}
// connEnd draws a corridor end: a door on a real room, a passage square
// on a gone one (passages.c conn).
func (g *RogueGame) connEnd(rp *Room, pos Coord) {
if !rp.Flags.Has(Gone) {
g.door(rp, pos)
} else {
g.putPassage(pos)
}
}
// digCorridor digs from spos to epos, turning at turnSpot (the digging
// loop of passages.c conn).
func (g *RogueGame) digCorridor(plan corridorPlan, turnSpot int) {
curr := plan.spos
distance := plan.distance
turnDistance := plan.turnDistance
for distance > 0 {
// Move to new position
curr.X += del.X
curr.Y += del.Y
curr.X += plan.del.X
curr.Y += plan.del.Y
// Check if we are at the turn place, if so do the turn
if distance == turnSpot {
for ; turnDistance > 0; turnDistance-- {
g.putPassage(curr)
curr.X += turnDelta.X
curr.Y += turnDelta.Y
curr.X += plan.turnDelta.X
curr.Y += plan.turnDelta.Y
}
}
// Continue digging along
@@ -221,10 +251,10 @@ func (g *RogueGame) connectRooms(r1, r2 int) {
distance--
}
curr.X += del.X
curr.X += plan.del.X
curr.Y += del.Y
if curr != epos {
curr.Y += plan.del.Y
if curr != plan.epos {
g.msg("warning, connectivity problem on this level")
}
}
@@ -270,9 +300,18 @@ func (g *RogueGame) door(rm *Room, cp Coord) {
func (g *RogueGame) addPass() {
for y := 1; y < NumLines-1; y++ {
for x := range NumCols {
pp := g.Level.At(y, x)
if pp.Flags.Has(FPassage) || pp.Ch == Door ||
(!pp.Flags.Has(FReal) && (pp.Ch == '|' || pp.Ch == '-')) {
g.addPassSpot(g.Level.At(y, x), y, x)
}
}
}
// addPassSpot shows one passage or door square for the wizard (the loop
// body of passages.c add_pass).
func (g *RogueGame) addPassSpot(pp *Place, y, x int) {
if !pp.Flags.Has(FPassage) && !hiddenExit(pp.Flags, pp.Ch) {
return
}
ch := pp.Ch
if pp.Flags.Has(FPassage) {
ch = Passage
@@ -298,8 +337,11 @@ func (g *RogueGame) addPass() {
g.standend()
}
}
}
}
// hiddenExit reports a door, or a secret door still drawn as a wall
// (passages.c add_pass / numpass).
func hiddenExit(fp PlaceFlags, ch byte) bool {
return ch == Door || (!fp.Has(FReal) && (ch == '|' || ch == '-'))
}
// numberPassages assigns a number to each passageway (passages.c passnum).
@@ -338,8 +380,7 @@ func (g *RogueGame) numberPassage(y, x int) {
}
// check to see if it is a door or secret door, i.e., a new exit, or a
// numerable type of place
if ch := g.Level.Char(y, x); ch == Door ||
(!fp.Has(FReal) && (ch == '|' || ch == '-')) {
if hiddenExit(*fp, g.Level.Char(y, x)) {
rp := &g.Level.Passages[g.pnum]
rp.Exits = append(rp.Exits, Coord{Y: y, X: x})
} else if !fp.Has(FPassage) {

View File

@@ -41,19 +41,36 @@ func (g *RogueGame) quaff() {
g.leavePack(obj, false, false)
switch obj.PotionKind() {
case PotionConfusion:
if h := g.data.quaffHandlers[obj.PotionKind()]; h != nil {
h(g, trip)
}
g.status()
// Throw the item away
g.callIt(&g.Items.Potions[obj.Which])
}
// The per-potion effect handlers, dispatched through
// gameData.quaffHandlers. Each is one case of the C quaff switch.
func (g *RogueGame) quaffConfusion(trip bool) {
g.applyPotionFuse(PotionConfusion, !trip)
case PotionPoison:
}
func (g *RogueGame) quaffPoison(bool) {
g.Items.Potions[PotionPoison].Know = true
if p.IsWearing(RingSustainStrength) {
if g.Player.IsWearing(RingSustainStrength) {
g.msg("you feel momentarily sick")
} else {
g.changeStrength(-(g.rnd(3) + 1))
g.msg("you feel very sick now")
g.comeDown(0)
}
case PotionHealing:
}
func (g *RogueGame) quaffHealing(bool) {
p := &g.Player
g.Items.Potions[PotionHealing].Know = true
if p.Stats.HP += g.roll(p.Stats.Lvl, 4); p.Stats.HP > p.Stats.MaxHP {
p.Stats.MaxHP++
@@ -62,19 +79,25 @@ func (g *RogueGame) quaff() {
g.sight(0)
g.msg("you begin to feel better")
case PotionGainStrength:
}
func (g *RogueGame) quaffGainStrength(bool) {
g.Items.Potions[PotionGainStrength].Know = true
g.changeStrength(1)
g.msg("you feel stronger, now. What bulging muscles!")
case PotionDetectMonsters:
p.Flags.Set(SenseMonsters)
}
func (g *RogueGame) quaffDetectMonsters(bool) {
g.Player.Flags.Set(SenseMonsters)
g.Fuse(DTurnSee, 1, HuhDuration, After)
if !g.turnSee(false) {
g.msg("you have a %s feeling for a moment, then it passes",
g.chooseStr("normal", "strange"))
}
case PotionDetectMagic:
}
func (g *RogueGame) quaffDetectMagic(bool) {
// Potion of magic detection. Show the potions and scrolls
show := false
@@ -108,7 +131,10 @@ func (g *RogueGame) quaff() {
g.msg("you have a %s feeling for a moment, then it passes",
g.chooseStr("normal", "strange"))
}
case PotionLSD:
}
func (g *RogueGame) quaffLSD(trip bool) {
p := &g.Player
if !trip {
if p.On(SenseMonsters) {
g.turnSee(false)
@@ -119,8 +145,10 @@ func (g *RogueGame) quaff() {
}
g.applyPotionFuse(PotionLSD, true)
case PotionSeeInvisible:
show := p.On(CanSeeInvisible)
}
func (g *RogueGame) quaffSeeInvisible(bool) {
show := g.Player.On(CanSeeInvisible)
g.applyPotionFuse(PotionSeeInvisible, false)
@@ -129,11 +157,17 @@ func (g *RogueGame) quaff() {
}
g.sight(0)
case PotionRaiseLevel:
}
func (g *RogueGame) quaffRaiseLevel(bool) {
g.Items.Potions[PotionRaiseLevel].Know = true
g.msg("you suddenly feel much more skillful")
g.raiseLevel()
case PotionExtraHealing:
}
func (g *RogueGame) quaffExtraHealing(bool) {
p := &g.Player
g.Items.Potions[PotionExtraHealing].Know = true
if p.Stats.HP += g.roll(p.Stats.Lvl, 8); p.Stats.HP > p.Stats.MaxHP {
if p.Stats.HP > p.Stats.MaxHP+p.Stats.Lvl+1 {
@@ -147,14 +181,19 @@ func (g *RogueGame) quaff() {
g.sight(0)
g.comeDown(0)
g.msg("you begin to feel much better")
case PotionHaste:
}
func (g *RogueGame) quaffHaste(bool) {
g.Items.Potions[PotionHaste].Know = true
g.After = false
if g.addHaste(true) {
g.msg("you feel yourself moving much faster")
}
case PotionRestoreStrength:
}
func (g *RogueGame) quaffRestoreStrength(bool) {
p := &g.Player
if p.IsRing(Left, RingAddStrength) {
addStr(&p.Stats.Str, -p.CurRing[Left].Bonus)
}
@@ -176,15 +215,14 @@ func (g *RogueGame) quaff() {
}
g.msg("hey, this tastes great. It make you feel warm all over")
case PotionBlindness:
g.applyPotionFuse(PotionBlindness, true)
case PotionLevitation:
g.applyPotionFuse(PotionLevitation, true)
}
g.status()
// Throw the item away
g.callIt(&g.Items.Potions[obj.Which])
func (g *RogueGame) quaffBlindness(bool) {
g.applyPotionFuse(PotionBlindness, true)
}
func (g *RogueGame) quaffLevitation(bool) {
g.applyPotionFuse(PotionLevitation, true)
}
// raiseLevel: the guy just magically went up a level (potions.c
@@ -260,7 +298,24 @@ func (g *RogueGame) turnSee(turnOff bool) bool {
if !canSee {
g.addch(mp.OldCh)
}
} else if g.showSensed(mp, canSee) {
addNew = true
}
}
if turnOff {
g.Player.Flags.Clear(SenseMonsters)
} else {
g.Player.Flags.Set(SenseMonsters)
}
return addNew
}
// showSensed draws one monster for monster sense, standout when it is
// otherwise invisible; it reports whether the monster was newly revealed
// (the turn-on arm of the C turn_see loop).
func (g *RogueGame) showSensed(mp *Monster, canSee bool) bool {
if !canSee {
g.standout()
}
@@ -274,18 +329,10 @@ func (g *RogueGame) turnSee(turnOff bool) bool {
if !canSee {
g.standend()
addNew = true
}
}
return true
}
if turnOff {
g.Player.Flags.Clear(SenseMonsters)
} else {
g.Player.Flags.Set(SenseMonsters)
}
return addNew
return false
}
// seenStairs reports whether the player has seen the stairs (potions.c

View File

@@ -14,11 +14,8 @@ func (g *RogueGame) ringOn() {
}
if obj.Kind != KindRing {
if !g.Options.Terse {
g.msg("it would be difficult to wrap that around a finger")
} else {
g.msg("not a ring")
}
g.msg("%s", g.chooseTerse("not a ring",
"it would be difficult to wrap that around a finger"))
return
}
@@ -28,24 +25,8 @@ func (g *RogueGame) ringOn() {
return
}
var ring int
switch {
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
if ring = g.gethand(); ring < 0 {
return
}
case p.CurRing[Left] == nil:
ring = Left
case p.CurRing[Right] == nil:
ring = Right
default:
if !g.Options.Terse {
g.msg("you already have a ring on each hand")
} else {
g.msg("wearing two")
}
ring := g.pickRingHand()
if ring < 0 {
return
}
@@ -68,6 +49,26 @@ func (g *RogueGame) ringOn() {
g.msg("%s (%c)", g.inventoryName(obj, true), obj.PackCh)
}
// pickRingHand chooses the hand for a new ring, asking when both are
// free; negative aborts (rings.c ring_on).
func (g *RogueGame) pickRingHand() int {
p := &g.Player
switch {
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
return g.gethand()
case p.CurRing[Left] == nil:
return Left
case p.CurRing[Right] == nil:
return Right
default:
g.msg("%s", g.chooseTerse("wearing two",
"you already have a ring on each hand"))
return -1
}
}
// ringOff takes off a ring (rings.c ring_off).
func (g *RogueGame) ringOff() {
p := &g.Player

View File

@@ -115,8 +115,25 @@ func (g *RogueGame) totalWinner() {
line := 1
for _, obj := range p.Pack {
worth := 0
worth := g.objectWorth(obj)
g.scr.Std.MvPrintwf(line, 0, "%c) %5d %s", obj.PackCh, worth,
g.inventoryName(obj, false))
line++
p.Purse += worth
}
g.scr.Std.MvPrintwf(line, 0, " %5d Gold Pieces ", oldpurse)
g.refresh()
g.score(p.Purse, 2, ' ')
g.myExit()
}
// objectWorth appraises one pack item on the way out, marking it known
// (the switch of rip.c total_winner).
func (g *RogueGame) objectWorth(obj *Object) int {
it := &g.Items
worth := 0
switch obj.Kind {
case KindFood:
@@ -131,26 +148,42 @@ func (g *RogueGame) totalWinner() {
worth += 10 * (g.data.aClass[obj.Which] - obj.ArmorClass)
obj.Flags.Set(Known)
case KindScroll:
op := &it.Scrolls[obj.Which]
worth = op.Worth * obj.Count
if !op.Know {
worth /= 2
}
op.Know = true
worth = loreWorth(&it.Scrolls[obj.Which], obj.Count)
case KindPotion:
op := &it.Potions[obj.Which]
worth = loreWorth(&it.Potions[obj.Which], obj.Count)
case KindRing:
worth = g.ringWorth(obj)
case KindWand:
worth = g.wandWorth(obj)
case KindAmulet:
worth = 1000
}
worth = op.Worth * obj.Count
if worth < 0 {
worth = 0
}
return worth
}
// loreWorth appraises a scroll or potion, halved when unidentified, and
// identifies it (rip.c total_winner).
func loreWorth(op *ObjInfo, count int) int {
worth := op.Worth * count
if !op.Know {
worth /= 2
}
op.Know = true
case KindRing:
op := &it.Rings[obj.Which]
worth = op.Worth
return worth
}
// ringWorth appraises a ring: bonus rings gain by their bonus, cursed
// ones are junk (rip.c total_winner).
func (g *RogueGame) ringWorth(obj *Object) int {
op := &g.Items.Rings[obj.Which]
worth := op.Worth
if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage ||
obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity {
@@ -168,9 +201,15 @@ func (g *RogueGame) totalWinner() {
obj.Flags.Set(Known)
op.Know = true
case KindWand:
op := &it.Sticks[obj.Which]
worth = op.Worth
return worth
}
// wandWorth appraises a wand or staff by its charges (rip.c
// total_winner).
func (g *RogueGame) wandWorth(obj *Object) int {
op := &g.Items.Sticks[obj.Which]
worth := op.Worth
worth += 20 * obj.Charges
if !obj.Flags.Has(Known) {
@@ -180,24 +219,8 @@ func (g *RogueGame) totalWinner() {
obj.Flags.Set(Known)
op.Know = true
case KindAmulet:
worth = 1000
}
if worth < 0 {
worth = 0
}
g.scr.Std.MvPrintwf(line, 0, "%c) %5d %s", obj.PackCh, worth,
g.inventoryName(obj, false))
line++
p.Purse += worth
}
g.scr.Std.MvPrintwf(line, 0, " %5d Gold Pieces ", oldpurse)
g.refresh()
g.score(p.Purse, 2, ' ')
g.myExit()
return worth
}
// killname converts a code to a monster name (rip.c killname).

View File

@@ -38,25 +38,21 @@ func (g *RogueGame) digRooms() {
}
// dig and populate all the rooms on the level
for i := range g.Level.Rooms {
g.digRoom(i, bsze)
}
}
// digRoom digs and populates one room (the loop body of rooms.c
// do_rooms).
func (g *RogueGame) digRoom(i int, bsze Coord) {
rp := &g.Level.Rooms[i]
// Find upper left corner of box that this room goes in
top := Coord{X: (i%3)*bsze.X + 1, Y: (i / 3) * bsze.Y}
if rp.Flags.Has(Gone) {
// Place a gone room. Make certain that there is a blank line
// for passage drawing.
for {
rp.Pos.X = top.X + g.rnd(bsze.X-2) + 1
rp.Pos.Y = top.Y + g.rnd(bsze.Y-2) + 1
rp.Max.X = -NumCols
g.placeGoneRoom(rp, top, bsze)
rp.Max.Y = -NumLines
if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 {
break
}
}
continue
return
}
// set room type
if g.rnd(10) < g.Depth-1 {
@@ -68,6 +64,33 @@ func (g *RogueGame) digRooms() {
}
// Find a place and size for a random room
if rp.Flags.Has(Maze) {
placeMazeRoom(rp, top, bsze)
} else {
g.placeNormalRoom(rp, top, bsze)
}
g.drawRoom(rp)
g.roomGold(rp)
g.roomMonster(rp)
}
// placeGoneRoom places a gone room, making certain that there is a
// blank line for passage drawing (rooms.c do_rooms).
func (g *RogueGame) placeGoneRoom(rp *Room, top, bsze Coord) {
for {
rp.Pos.X = top.X + g.rnd(bsze.X-2) + 1
rp.Pos.Y = top.Y + g.rnd(bsze.Y-2) + 1
rp.Max.X = -NumCols
rp.Max.Y = -NumLines
if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 {
return
}
}
}
// placeMazeRoom sizes a maze room to fill its box (rooms.c do_rooms).
func placeMazeRoom(rp *Room, top, bsze Coord) {
rp.Max.X = bsze.X - 1
rp.Max.Y = bsze.Y - 1
@@ -79,7 +102,11 @@ func (g *RogueGame) digRooms() {
rp.Pos.Y++
rp.Max.Y--
}
} else {
}
// placeNormalRoom rolls a place and size for an ordinary room (rooms.c
// do_rooms).
func (g *RogueGame) placeNormalRoom(rp *Room, top, bsze Coord) {
for {
rp.Max.X = g.rnd(bsze.X-4) + 4
rp.Max.Y = g.rnd(bsze.Y-4) + 4
@@ -87,14 +114,17 @@ func (g *RogueGame) digRooms() {
rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y)
if rp.Pos.Y != 0 {
break
return
}
}
}
g.drawRoom(rp)
// Put the gold in
if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) {
// roomGold maybe puts a gold pile in the room (rooms.c do_rooms).
func (g *RogueGame) roomGold(rp *Room) {
if g.rnd(2) != 0 || (g.HasAmulet && g.Depth < g.MaxDepth) {
return
}
gold := newObject()
rp.GoldVal = g.goldCalc()
gold.GoldValue = rp.GoldVal
@@ -107,7 +137,10 @@ func (g *RogueGame) digRooms() {
gold.Kind = KindGold
g.Level.AddObject(gold)
}
// Put the monster in
// roomMonster maybe puts a monster in the room; gold attracts them
// (rooms.c do_rooms).
func (g *RogueGame) roomMonster(rp *Room) {
prob := 25
if rp.GoldVal > 0 {
prob = 80
@@ -120,7 +153,6 @@ func (g *RogueGame) digRooms() {
g.givePack(tp)
}
}
}
// drawRoom draws a box around a room and lays down the floor for normal
// rooms; for maze rooms, draws the maze (rooms.c draw_room).
@@ -182,9 +214,28 @@ func (g *RogueGame) digMaze(rp *Room) {
// dig digs out from around where we are now, if possible (rooms.c dig).
func (g *RogueGame) dig(y, x int) {
m := &g.maze
del := [4]Coord{{X: 2, Y: 0}, {X: -2, Y: 0}, {X: 0, Y: 2}, {X: 0, Y: -2}}
for {
nexty, nextx, ok := g.digPick(y, x)
if !ok {
return
}
g.accountMaze(y, x, nexty, nextx)
g.accountMaze(nexty, nextx, y, x)
g.putPassage(digWallGap(m, y, x, nexty, nextx))
g.putPassage(Coord{Y: nexty + m.starty, X: nextx + m.startx})
g.dig(nexty, nextx)
}
}
// digPick reservoir-picks the next unvisited maze cell; ok is false
// when the digger is boxed in (the candidate scan of rooms.c dig).
func (g *RogueGame) digPick(y, x int) (int, int, bool) {
m := &g.maze
del := [4]Coord{{X: 2, Y: 0}, {X: -2, Y: 0}, {X: 0, Y: 2}, {X: 0, Y: -2}}
cnt := 0
var nexty, nextx int
@@ -207,36 +258,27 @@ func (g *RogueGame) dig(y, x int) {
}
}
if cnt == 0 {
return
return nexty, nextx, cnt != 0
}
g.accountMaze(y, x, nexty, nextx)
g.accountMaze(nexty, nextx, y, x)
var pos Coord
// digWallGap picks the wall square to knock out between two maze cells
// (rooms.c dig).
func digWallGap(m *mazeState, y, x, nexty, nextx int) Coord {
if nexty == y {
pos.Y = y + m.starty
pos := Coord{Y: y + m.starty, X: nextx + m.startx - 1}
if nextx-x < 0 {
pos.X = nextx + m.startx + 1
} else {
pos.X = nextx + m.startx - 1
}
} else {
pos.X = x + m.startx
if nexty-y < 0 {
pos.Y = nexty + m.starty + 1
} else {
pos.Y = nexty + m.starty - 1
}
}
g.putPassage(pos)
pos.Y = nexty + m.starty
pos.X = nextx + m.startx
g.putPassage(pos)
g.dig(nexty, nextx)
return pos
}
pos := Coord{X: x + m.startx, Y: nexty + m.starty - 1}
if nexty-y < 0 {
pos.Y = nexty + m.starty + 1
}
return pos
}
// accountMaze accounts for maze exits (rooms.c accnt_maze).
@@ -278,13 +320,20 @@ func (g *RogueGame) findFloorIn(rp *Room, limit int, monst bool) (Coord, bool) {
return g.findFloorImpl(rp, limit, monst, false)
}
// floorChar is what an empty spot looks like in a room: passage in a
// maze, floor otherwise (rooms.c find_floor).
func floorChar(rp *Room) byte {
if rp.Flags.Has(Maze) {
return Passage
}
return Floor
}
func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Coord, bool) {
var compchar byte
if !pickroom {
compchar = Floor
if rp.Flags.Has(Maze) {
compchar = Passage
}
compchar = floorChar(rp)
}
cnt := limit
@@ -297,11 +346,7 @@ func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Co
if pickroom {
rp = &g.Level.Rooms[g.randomRoom()]
compchar = Floor
if rp.Flags.Has(Maze) {
compchar = Passage
}
compchar = floorChar(rp)
}
cp := g.randomPos(rp)
@@ -330,6 +375,15 @@ func (g *RogueGame) enterRoom(cp Coord) {
g.move(y, rp.Pos.X)
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
g.enterRoomCell(y, x)
}
}
}
}
// enterRoomCell draws one square of a room being lit on entry (the loop
// body of rooms.c enter_room).
func (g *RogueGame) enterRoomCell(y, x int) {
tp := g.Level.MonsterAt(y, x)
ch := g.Level.Char(y, x)
@@ -339,23 +393,24 @@ func (g *RogueGame) enterRoom(cp Coord) {
} else {
g.move(y, x+1)
}
} else {
return
}
tp.OldCh = ch
if !g.seeMonst(tp) {
if p.On(SenseMonsters) {
if g.seeMonst(tp) {
g.addch(tp.Disguise)
return
}
if g.Player.On(SenseMonsters) {
g.standout()
g.addch(tp.Disguise)
g.standend()
} else {
g.addch(ch)
}
} else {
g.addch(tp.Disguise)
}
}
}
}
}
}
// leaveRoom is the code for when we exit a room (rooms.c leave_room).
@@ -381,6 +436,16 @@ func (g *RogueGame) leaveRoom(cp Coord) {
p.Room = &g.Level.Passages[*g.Level.FlagsAt(cp.Y, cp.X)&FPassNum]
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
g.leaveRoomCell(floor, y, x)
}
}
g.doorOpen(rp)
}
// leaveRoomCell hides one square of a room being left (the loop body of
// rooms.c leave_room).
func (g *RogueGame) leaveRoomCell(floor byte, y, x int) {
g.move(y, x)
switch ch := g.inch(); ch {
@@ -391,13 +456,16 @@ func (g *RogueGame) leaveRoom(cp Coord) {
default:
// to check for monster, we have to strip out the standout
// bit (our Window returns the bare character already)
if isUpper(ch) {
if p.On(SenseMonsters) {
if !isUpper(ch) {
return
}
if g.Player.On(SenseMonsters) {
g.standout()
g.addch(ch)
g.standend()
break
return
}
pp := g.Level.At(y, x)
@@ -408,8 +476,3 @@ func (g *RogueGame) leaveRoom(cp Coord) {
}
}
}
}
}
g.doorOpen(rp)
}

View File

@@ -177,8 +177,48 @@ func (g *RogueGame) packIdx(obj *Object) int {
// snapshot captures the complete game state.
func (g *RogueGame) snapshot() *SaveState {
p := &g.Player
st := &SaveState{
st := g.snapshotHeader()
// the map, sans monster pointers (rebuilt on load)
st.Places = make([]savedPlace, len(g.Level.Places))
for i := range g.Level.Places {
st.Places[i] = savedPlace{
Ch: g.Level.Places[i].Ch,
Flags: g.Level.Places[i].Flags,
}
}
// level objects by value; remember their pointers for dest encoding
objAt := make(map[*Object]int, len(g.Level.Objects))
for i, o := range g.Level.Objects {
st.Objects = append(st.Objects, *o)
objAt[o] = i
}
st.Player = g.snapshotPlayer()
// monsters, with chase targets as (kind, index) references
for _, m := range g.Level.Monsters {
sc := savedCreature{
Pos: m.Pos, Turn: m.Turn, Type: m.Type, Disguise: m.Disguise,
OldCh: m.OldCh, Flags: m.Flags, Stats: m.Stats,
RoomIdx: g.roomIdx(m.Room),
}
for _, o := range m.Pack {
sc.Pack = append(sc.Pack, *o)
}
st.Monsters = append(st.Monsters, sc)
st.Dests = append(st.Dests, g.destRefFor(m, objAt))
}
return st
}
// snapshotHeader captures the scalar game state (the field list of
// state.c rs_save_file).
func (g *RogueGame) snapshotHeader() *SaveState {
return &SaveState{
Version: saveFormatVersion,
Seed: g.Rng.Seed,
Dnum: g.Dnum,
@@ -226,25 +266,14 @@ func (g *RogueGame) snapshot() *SaveState {
AllScore: g.AllScore,
Screen: g.scr.Std.Contents(),
}
// the map, sans monster pointers (rebuilt on load)
st.Places = make([]savedPlace, len(g.Level.Places))
for i := range g.Level.Places {
st.Places[i] = savedPlace{
Ch: g.Level.Places[i].Ch,
Flags: g.Level.Places[i].Flags,
}
}
// level objects by value; remember their pointers for dest encoding
objAt := make(map[*Object]int, len(g.Level.Objects))
for i, o := range g.Level.Objects {
st.Objects = append(st.Objects, *o)
objAt[o] = i
}
// snapshotPlayer captures the player, equipment as pack indices (the
// player half of snapshot).
func (g *RogueGame) snapshotPlayer() savedPlayer {
p := &g.Player
// the player
st.Player = savedPlayer{
sp := savedPlayer{
Body: savedCreature{
Pos: p.Pos, Turn: p.Turn, Type: p.Type, Disguise: p.Disguise,
OldCh: p.OldCh, Flags: p.Flags, Stats: p.Stats,
@@ -260,61 +289,72 @@ func (g *RogueGame) snapshot() *SaveState {
MaxStats: p.MaxStats, VfHit: p.VfHit,
}
for _, o := range p.Pack {
st.Player.Body.Pack = append(st.Player.Body.Pack, *o)
sp.Body.Pack = append(sp.Body.Pack, *o)
}
// monsters, with chase targets as (kind, index) references
for _, m := range g.Level.Monsters {
sc := savedCreature{
Pos: m.Pos, Turn: m.Turn, Type: m.Type, Disguise: m.Disguise,
OldCh: m.OldCh, Flags: m.Flags, Stats: m.Stats,
RoomIdx: g.roomIdx(m.Room),
}
for _, o := range m.Pack {
sc.Pack = append(sc.Pack, *o)
return sp
}
st.Monsters = append(st.Monsters, sc)
ref := destRef{}
// destRefFor encodes a monster's chase target as a (kind, index)
// reference: the hero, another monster, a level object, or room gold
// (state.c rs_write_thing).
func (g *RogueGame) destRefFor(m *Monster, objAt map[*Object]int) destRef {
switch {
case m.Dest == nil:
case m.Dest == &p.Pos:
ref = destRef{Kind: 1}
default:
return destRef{}
case m.Dest == &g.Player.Pos:
return destRef{Kind: 1}
}
for mi, om := range g.Level.Monsters {
if m.Dest == &om.Pos {
ref = destRef{Kind: 2, Idx: mi}
return destRef{Kind: 2, Idx: mi}
}
}
if ref.Kind == 0 {
for _, oo := range g.Level.Objects {
if m.Dest == &oo.Pos {
ref = destRef{Kind: 3, Idx: objAt[oo]}
}
return destRef{Kind: 3, Idx: objAt[oo]}
}
}
if ref.Kind == 0 {
for ri := range g.Level.Rooms {
if m.Dest == &g.Level.Rooms[ri].Gold {
ref = destRef{Kind: 4, Idx: ri}
}
}
return destRef{Kind: 4, Idx: ri}
}
}
st.Dests = append(st.Dests, ref)
}
return st
return destRef{}
}
// applySnapshot rebuilds live game state from a snapshot.
func (g *RogueGame) applySnapshot(st *SaveState) {
p := &g.Player
g.applyHeader(st)
for i := range g.Level.Places {
g.Level.Places[i] = Place{
Ch: st.Places[i].Ch,
Flags: st.Places[i].Flags,
}
}
// level objects
g.Level.Objects = nil
for i := range st.Objects {
o := st.Objects[i]
g.Level.Objects = append(g.Level.Objects, &o)
}
g.applyPlayer(st)
g.applyMonsters(st)
g.applyDests(st)
g.scr.Std.SetContents(st.Screen)
}
// applyHeader restores the scalar game state (the field list of
// applySnapshot).
func (g *RogueGame) applyHeader(st *SaveState) {
g.Rng.Seed = st.Seed
g.Dnum = st.Dnum
g.Whoami = st.Whoami
@@ -329,6 +369,12 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.Level.Passages = st.Passages
g.Level.Stairs = st.Stairs
g.Level.TrapCount = st.TrapCount
g.applyTurnState(st)
}
// applyTurnState restores the in-turn command state (the second half of
// applyHeader).
func (g *RogueGame) applyTurnState(st *SaveState) {
g.After = st.After
g.Again = st.Again
g.NoScore = st.NoScoreF
@@ -359,22 +405,12 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.LastScore = st.LastScore
g.AllScore = st.AllScore
g.Playing = true
for i := range g.Level.Places {
g.Level.Places[i] = Place{
Ch: st.Places[i].Ch,
Flags: st.Places[i].Flags,
}
}
// level objects
g.Level.Objects = nil
for i := range st.Objects {
o := st.Objects[i]
g.Level.Objects = append(g.Level.Objects, &o)
}
// the player
// applyPlayer restores the player, resolving equipment pack indices
// (the player half of applySnapshot).
func (g *RogueGame) applyPlayer(st *SaveState) {
p := &g.Player
sp := &st.Player
p.Pos = sp.Body.Pos
p.Turn = sp.Body.Turn
@@ -411,8 +447,11 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
p.NoFood = sp.NoFood
p.MaxStats = sp.MaxStats
p.VfHit = sp.VfHit
}
// monsters, their map index, and their chase targets
// applyMonsters rebuilds the monster list and its map index from a
// snapshot (the monster half of applySnapshot).
func (g *RogueGame) applyMonsters(st *SaveState) {
g.Level.Monsters = nil
for i := range st.Monsters {
sc := &st.Monsters[i]
@@ -430,13 +469,17 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.Level.Monsters = append(g.Level.Monsters, m)
g.Level.SetMonsterAt(m.Pos.Y, m.Pos.X, m)
}
}
// applyDests re-aims the monsters' chase targets from their (kind,
// index) references (the fixup half of applySnapshot).
func (g *RogueGame) applyDests(st *SaveState) {
for i, ref := range st.Dests {
m := g.Level.Monsters[i]
switch ref.Kind {
case 1:
m.Dest = &p.Pos
m.Dest = &g.Player.Pos
case 2:
m.Dest = &g.Level.Monsters[ref.Idx].Pos
case 3:
@@ -445,13 +488,20 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
m.Dest = &g.Level.Rooms[ref.Idx].Gold
}
}
g.scr.Std.SetContents(st.Screen)
}
// saveAnswer is a yes/no/escape prompt result in the save-game flow.
type saveAnswer int
// The saveGame prompt outcomes.
const (
saveYes saveAnswer = iota
saveNo
saveAbort
)
// saveGame implements the "save game" command (save.c save_game). The
// labeled prompt loop and the useDefault flag stand in for the C
// goto over/gotfile flow.
// labeled prompt loop stands in for the C goto over/gotfile flow.
func (g *RogueGame) saveGame() {
g.Msgs.Mpos = 0
@@ -460,80 +510,31 @@ prompt:
useDefault := false
if g.FileName != "" {
var c byte
for {
g.msg("save file (%s)? ", g.FileName)
c = g.readchar()
g.Msgs.Mpos = 0
if c == Escape {
g.msg("")
a := g.askDefaultSave()
if a == saveAbort {
return
}
if c == 'n' || c == 'N' || c == 'y' || c == 'Y' {
break
}
g.msg("please answer Y or N")
}
if c == 'y' || c == 'Y' {
g.addstr("Yes\n")
g.refresh()
useDefault = true
}
useDefault = a == saveYes
}
for {
var buf string
if useDefault {
buf = g.FileName
buf, ok := g.saveFileName(useDefault)
if !ok {
return
}
useDefault = false
} else {
g.Msgs.Mpos = 0
g.msg("file name: ")
if g.getStr(&buf, g.scr.Std) == Quit {
g.msg("")
a := g.saveCheckOverwrite(buf)
if a == saveAbort {
return
}
g.Msgs.Mpos = 0
}
// test to see if the file exists
_, statErr := os.Stat(buf)
if statErr == nil {
for {
g.msg("File exists. Do you wish to overwrite it?")
g.Msgs.Mpos = 0
c := g.readchar()
if c == Escape {
g.msg("")
return
}
if c == 'y' || c == 'Y' {
break
}
if c == 'n' || c == 'N' {
if a == saveNo {
continue prompt // the C goto over: start again
}
g.msg("Please answer Y or N")
}
g.msg("file name: %s", buf)
_ = os.Remove(g.FileName) // best effort, as in C (md_unlink)
}
g.FileName = buf
err := g.saveFile(g.FileName)
@@ -550,6 +551,97 @@ prompt:
g.myExit()
}
// askDefaultSave asks whether to save to the current file name (save.c
// save_game).
func (g *RogueGame) askDefaultSave() saveAnswer {
for {
g.msg("save file (%s)? ", g.FileName)
c := g.readchar()
g.Msgs.Mpos = 0
switch c {
case Escape:
g.msg("")
return saveAbort
case 'y', 'Y':
g.addstr("Yes\n")
g.refresh()
return saveYes
case 'n', 'N':
return saveNo
}
g.msg("please answer Y or N")
}
}
// saveFileName picks the save path: the default, or a prompted one; ok
// is false when the player quit the prompt (save.c save_game).
func (g *RogueGame) saveFileName(useDefault bool) (string, bool) {
if useDefault {
return g.FileName, true
}
g.Msgs.Mpos = 0
g.msg("file name: ")
buf := ""
if g.getStr(&buf, g.scr.Std) == Quit {
g.msg("")
return "", false
}
g.Msgs.Mpos = 0
return buf, true
}
// saveCheckOverwrite guards an existing file: saveNo restarts the whole
// prompt, saveAbort quits (save.c save_game).
func (g *RogueGame) saveCheckOverwrite(buf string) saveAnswer {
// test to see if the file exists
_, statErr := os.Stat(buf)
if statErr != nil {
return saveYes
}
answer := g.askOverwrite()
if answer != saveYes {
return answer
}
g.msg("file name: %s", buf)
_ = os.Remove(g.FileName) // best effort, as in C (md_unlink)
return saveYes
}
// askOverwrite asks whether to overwrite the existing file (save.c
// save_game).
func (g *RogueGame) askOverwrite() saveAnswer {
for {
g.msg("File exists. Do you wish to overwrite it?")
g.Msgs.Mpos = 0
switch g.readchar() {
case Escape:
g.msg("")
return saveAbort
case 'y', 'Y':
return saveYes
case 'n', 'N':
return saveNo
}
g.msg("Please answer Y or N")
}
}
// saveFile writes the saved game (save.c save_file). A failed write means
// a corrupt save, so the file is removed before reporting the error.
func (g *RogueGame) saveFile(path string) error {

View File

@@ -39,6 +39,15 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
t.Error("save file not deleted on restore (C anti-restart rule)")
}
checkRestoredState(t, g, h)
checkRestoredMonsters(t, g, h)
checkEquipmentAliasing(t, g, h)
}
// checkRestoredState verifies the scalar state survived the round trip.
func checkRestoredState(t *testing.T, g, h *RogueGame) {
t.Helper()
if h.Player.Purse != 123 || h.Player.FoodLeft != 777 {
t.Errorf("player state lost: purse=%d food=%d",
h.Player.Purse, h.Player.FoodLeft)
@@ -67,13 +76,22 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
if renderMap(h) != renderMap(g) {
t.Error("restored level map differs")
}
}
// checkRestoredMonsters verifies the monster list and its pointer fixups
// survived the round trip.
func checkRestoredMonsters(t *testing.T, g, h *RogueGame) {
t.Helper()
if len(h.Level.Monsters) != len(g.Level.Monsters) {
t.Fatalf("monster count %d != %d",
len(h.Level.Monsters), len(g.Level.Monsters))
}
if len(g.Level.Monsters) > 0 {
if len(g.Level.Monsters) == 0 {
return
}
m := h.Level.Monsters[0]
if m.Dest != &h.Player.Pos {
t.Error("monster chase target not re-aliased to the hero")
@@ -87,8 +105,12 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
t.Error("monster room pointer not rebuilt")
}
}
// Equipment aliasing: the wielded mace must be the same *Object as the
// one in the pack.
// checkEquipmentAliasing verifies the wielded mace is the same *Object as
// the one in the pack.
func checkEquipmentAliasing(t *testing.T, g, h *RogueGame) {
t.Helper()
st := g.snapshot()
t.Logf("snapshot indices: weapon=%d armor=%d rings=%v packlen=%d",
st.Player.CurWeapon, st.Player.CurArmor, st.Player.CurRing,

View File

@@ -107,26 +107,30 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
topTen := g.rdScore()
// Insert her in list if need be
ins := -1
if !g.NoScore && flags >= 0 {
ins = g.scoreInsert(topTen, amount, flags, monst)
}
lines, highlight := g.scoreLines(topTen, ins)
g.showScores(lines, highlight)
// Update the list file
if ins >= 0 {
g.wrScore(topTen)
}
}
// scoreInsert slots the new score into the top ten, honoring the
// one-score-per-losing-uid rule; -1 means it did not place (the
// insertion half of rip.c score).
func (g *RogueGame) scoreInsert(topTen []ScoreEnt, amount, flags int, monst byte) int {
uid := os.Getuid()
scp := len(topTen)
for i := range topTen {
if amount > topTen[i].Score {
scp = i
break
} else if !g.AllScore && flags != 2 &&
topTen[i].UID == uid && topTen[i].Flags != 2 {
// only one score per nowin uid
scp = len(topTen)
break
}
scp := g.scoreSlot(topTen, amount, flags, uid)
if scp >= len(topTen) {
return -1
}
if scp < len(topTen) {
sc2 := len(topTen) - 1
if flags != 2 && !g.AllScore {
for i := scp; i < len(topTen); i++ {
@@ -157,11 +161,32 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
Level: lvl,
Time: time.Now().Unix(),
}
ins = scp
return scp
}
// scoreSlot finds where the new score lands: len(topTen) when it does
// not place, or when this uid already holds a losing score (the scan of
// rip.c score).
func (g *RogueGame) scoreSlot(topTen []ScoreEnt, amount, flags, uid int) int {
for i := range topTen {
if amount > topTen[i].Score {
return i
}
if !g.AllScore && flags != 2 &&
topTen[i].UID == uid && topTen[i].Flags != 2 {
// only one score per nowin uid
return len(topTen)
}
}
// Build the list display
return len(topTen)
}
// scoreLines formats the scoreboard, noting which display line holds
// the freshly inserted score (the display half of rip.c score).
func (g *RogueGame) scoreLines(topTen []ScoreEnt, ins int) ([]string, int) {
label := "Rogueists"
if g.AllScore {
label = "Scores"
@@ -194,7 +219,20 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
lines = append(lines, line)
}
if g.scr != nil && g.scr.term != nil {
return lines, highlight
}
// showScores prints the scoreboard on the screen when there is one,
// else to standard output (rip.c score).
func (g *RogueGame) showScores(lines []string, highlight int) {
if g.scr == nil || g.scr.term == nil {
for _, line := range lines {
_, _ = fmt.Fprintln(os.Stdout, line) // CLI output
}
return
}
g.clear()
for i, line := range lines {
@@ -210,16 +248,6 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
}
g.refresh()
} else {
for _, line := range lines {
_, _ = fmt.Fprintln(os.Stdout, line) // CLI output
}
}
// Update the list file
if ins >= 0 {
g.wrScore(topTen)
}
}
// ShowScores implements the -s command line option: print the scoreboard

View File

@@ -28,20 +28,63 @@ func (g *RogueGame) readScroll() {
// Get rid of the thing
g.leavePack(obj, false, false)
switch obj.ScrollKind() {
case ScrollMonsterConfusion:
if h := g.data.readHandlers[obj.ScrollKind()]; h != nil {
h(g, obj)
}
g.look(true) // put the result of the scroll on the screen
g.status()
g.callIt(&g.Items.Scrolls[obj.Which])
}
// The per-scroll effect handlers, dispatched through
// gameData.readHandlers. Each is one case of the C read_scroll switch.
func (g *RogueGame) readMonsterConfusion(*Object) {
// Scroll of monster confusion. Give him that power.
p.Flags.Set(CanConfuse)
g.Player.Flags.Set(CanConfuse)
g.msg("your hands begin to glow %s", g.pickColor("red"))
case ScrollEnchantArmor:
}
func (g *RogueGame) readEnchantArmor(*Object) {
p := &g.Player
if p.CurArmor != nil {
p.CurArmor.ArmorClass--
p.CurArmor.Flags.Clear(Cursed)
g.msg("your armor glows %s for a moment", g.pickColor("silver"))
}
case ScrollHoldMonster:
}
func (g *RogueGame) readHoldMonster(*Object) {
// Hold monster scroll. Stop all monsters within two spaces from
// chasing after the hero.
held := g.holdMonstersNear()
if held > 0 {
g.addmsgf("the monster")
if held > 1 {
g.addmsgf("s around you")
}
g.addmsgf(" freeze")
if held == 1 {
g.addmsgf("s")
}
g.endmsg()
g.Items.Scrolls[ScrollHoldMonster].Know = true
} else {
g.msg("you feel a strange sense of loss")
}
}
// holdMonstersNear freezes every awake monster within two spaces of the
// hero and reports how many froze (the scan of the C S_HOLD case).
func (g *RogueGame) holdMonstersNear() int {
p := &g.Player
held := 0
for x := p.Pos.X - 2; x <= p.Pos.X+2; x++ {
@@ -63,34 +106,35 @@ func (g *RogueGame) readScroll() {
}
}
if held > 0 {
g.addmsgf("the monster")
if held > 1 {
g.addmsgf("s around you")
return held
}
g.addmsgf(" freeze")
if held == 1 {
g.addmsgf("s")
}
g.endmsg()
g.Items.Scrolls[ScrollHoldMonster].Know = true
} else {
g.msg("you feel a strange sense of loss")
}
case ScrollSleep:
func (g *RogueGame) readSleep(*Object) {
// Scroll which makes you fall asleep
g.Items.Scrolls[ScrollSleep].Know = true
g.NoCommand += g.rnd(g.spread(5)) + 4 // SLEEPTIME
p.Flags.Clear(Awake)
g.Player.Flags.Clear(Awake)
g.msg("you fall asleep")
case ScrollCreateMonster:
}
func (g *RogueGame) readCreateMonster(*Object) {
// Create a monster: first look in a circle around him, next try
// his room, otherwise give up
mp, ok := g.createMonsterSpot()
if !ok {
g.msg("you hear a faint cry of anguish in the distance")
} else {
tp := &Monster{}
g.newMonster(tp, g.randMonster(false), mp)
}
}
// createMonsterSpot reservoir-samples a legal spot around the hero for a
// created monster; ok is false when every neighbor is blocked (the scan
// of the C S_CREATE case).
func (g *RogueGame) createMonsterSpot() (Coord, bool) {
p := &g.Player
i := 0
var mp Coord
@@ -116,59 +160,63 @@ func (g *RogueGame) readScroll() {
}
}
if i == 0 {
g.msg("you hear a faint cry of anguish in the distance")
} else {
tp := &Monster{}
g.newMonster(tp, g.randMonster(false), mp)
return mp, i != 0
}
case ScrollIdentifyPotion, ScrollIdentifyScroll, ScrollIdentifyWeapon, ScrollIdentifyArmor, ScrollIdentifyRingOrStick:
func (g *RogueGame) readIdentify(obj *Object) {
// Identify, let him figure something out
g.Items.Scrolls[obj.Which].Know = true
g.msg("this scroll is an %s scroll", g.Items.Scrolls[obj.Which].Name)
g.whatis(true, g.data.idType[obj.ScrollKind()])
case ScrollMagicMapping:
}
func (g *RogueGame) readMagicMapping(*Object) {
// Scroll of magic mapping.
g.Items.Scrolls[ScrollMagicMapping].Know = true
g.msg("oh, now this scroll has a map on it")
// take all the things we want to keep hidden out of the window
for y := 1; y < NumLines-1; y++ {
for x := range NumCols {
g.revealSpot(y, x)
}
}
}
// revealSpot uncovers one map cell for magic mapping and draws it (the
// loop body of the C SCR_MAP case).
func (g *RogueGame) revealSpot(y, x int) {
pp := g.Level.At(y, x)
ch := revealChar(pp)
if ch != ' ' {
if tp := pp.Monst; tp != nil {
tp.OldCh = ch
if !g.Player.On(SenseMonsters) {
g.mvaddch(y, x, ch)
}
} else {
g.mvaddch(y, x, ch)
}
}
}
// revealChar decides what magic mapping shows at a cell, making secret
// doors, hidden passages, and hidden traps real as a side effect; ' '
// means show nothing (the switch of the C SCR_MAP loop).
func revealChar(pp *Place) byte {
ch := pp.Ch
pass := false
switch ch {
case Door, Stairs:
case '-', '|':
if !pp.Flags.Has(FReal) {
ch = Door
pp.Ch = Door
pp.Flags.Set(FReal)
}
ch = revealWall(pp)
case ' ':
if pp.Flags.Has(FReal) {
// def: hidden things in walls stay hidden
if pp.Flags.Has(FPassage) {
pass = true
} else {
ch = ' '
}
} else {
pp.Flags.Set(FReal)
pp.Ch = Passage
pass = true
}
pass = revealSolid(pp)
case Passage:
pass = true
case Floor:
if pp.Flags.Has(FReal) {
ch = ' '
} else {
ch = Trap
pp.Ch = Trap
pp.Flags.Set(FSeen | FReal)
}
ch = revealFloor(pp)
default:
if pp.Flags.Has(FPassage) {
pass = true
@@ -187,19 +235,50 @@ func (g *RogueGame) readScroll() {
ch = Passage
}
if ch != ' ' {
if tp := pp.Monst; tp != nil {
tp.OldCh = ch
if !p.On(SenseMonsters) {
g.mvaddch(y, x, ch)
return ch
}
} else {
g.mvaddch(y, x, ch)
// revealWall handles a wall cell for magic mapping: a secret door
// becomes a real door (the '-'/'|' arm).
func revealWall(pp *Place) byte {
if !pp.Flags.Has(FReal) {
pp.Ch = Door
pp.Flags.Set(FReal)
return Door
}
return pp.Ch
}
// revealSolid handles a blank cell for magic mapping, reporting whether
// it is passage: hidden passages become real; hidden things in walls
// stay hidden (the ' ' arm).
func revealSolid(pp *Place) bool {
if pp.Flags.Has(FReal) {
return pp.Flags.Has(FPassage)
}
pp.Flags.Set(FReal)
pp.Ch = Passage
return true
}
case ScrollFoodDetection:
// revealFloor handles a floor cell for magic mapping: a hidden trap
// becomes a real, seen trap; real floor shows nothing (the FLOOR arm).
func revealFloor(pp *Place) byte {
if pp.Flags.Has(FReal) {
return ' '
}
pp.Ch = Trap
pp.Flags.Set(FSeen | FReal)
return Trap
}
func (g *RogueGame) readFoodDetection(*Object) {
// Food detection
found := false
@@ -219,8 +298,11 @@ func (g *RogueGame) readScroll() {
} else {
g.msg("your nose tingles")
}
case ScrollTeleportation:
}
func (g *RogueGame) readTeleportation(*Object) {
// Scroll of teleportation: make him disappear and reappear
p := &g.Player
curRoom := p.Room
g.teleport()
@@ -228,7 +310,10 @@ func (g *RogueGame) readScroll() {
if curRoom != p.Room {
g.Items.Scrolls[ScrollTeleportation].Know = true
}
case ScrollEnchantWeapon:
}
func (g *RogueGame) readEnchantWeapon(*Object) {
p := &g.Player
if p.CurWeapon == nil || p.CurWeapon.Kind != KindWeapon {
g.msg("you feel a strange sense of loss")
} else {
@@ -243,23 +328,34 @@ func (g *RogueGame) readScroll() {
g.msg("your %s glows %s for a moment",
g.Items.Weapons[p.CurWeapon.Which].Name, g.pickColor("blue"))
}
case ScrollScareMonster:
}
func (g *RogueGame) readScareMonster(*Object) {
// Reading it is a mistake and produces laughter at her poor boo
// boo.
g.msg("you hear maniacal laughter in the distance")
case ScrollRemoveCurse:
}
func (g *RogueGame) readRemoveCurse(*Object) {
p := &g.Player
uncurse(p.CurArmor)
uncurse(p.CurWeapon)
uncurse(p.CurRing[Left])
uncurse(p.CurRing[Right])
g.msg("%s", g.chooseStr("you feel in touch with the Universal Onenes",
"you feel as if somebody is watching over you"))
case ScrollAggravateMonsters:
}
func (g *RogueGame) readAggravateMonsters(*Object) {
// This scroll aggravates all the monsters on the current level
// and sets them running towards the hero
g.aggravate()
g.msg("you hear a high pitched humming noise")
case ScrollProtectArmor:
}
func (g *RogueGame) readProtectArmor(*Object) {
p := &g.Player
if p.CurArmor != nil {
p.CurArmor.Flags.Set(Protected)
g.msg("your armor is covered by a shimmering %s shield",
@@ -269,12 +365,6 @@ func (g *RogueGame) readScroll() {
}
}
g.look(true) // put the result of the scroll on the screen
g.status()
g.callIt(&g.Items.Scrolls[obj.Which])
}
// uncurse uncurses an item (scrolls.c uncurse).
func uncurse(obj *Object) {
if obj != nil {

View File

@@ -12,8 +12,6 @@ const (
// doZap performs a zap with a wand (sticks.c do_zap).
func (g *RogueGame) doZap() {
p := &g.Player
obj, ok := g.promptPackItem("zap with", KindWand)
if !ok {
return
@@ -32,9 +30,51 @@ func (g *RogueGame) doZap() {
return
}
switch obj.WandKind() {
case WandLight:
if h := g.data.zapHandlers[obj.WandKind()]; h != nil {
if !h(g, obj) {
return // the zap aborted; no charge is used
}
}
obj.Charges--
}
// zapRayMonster walks the zap ray from the hero to the first blocking
// spot and returns the monster standing there, if any (the shared
// preamble of the C monster-affecting zap cases).
func (g *RogueGame) zapRayMonster() *Monster {
p := &g.Player
y := p.Pos.Y
x := p.Pos.X
for stepOk(g.Level.VisibleChar(y, x)) {
y += g.Delta.Y
x += g.Delta.X
}
return g.Level.MonsterAt(y, x)
}
// zapVictim is zapRayMonster plus the flytrap release the C code does
// before the invisibility-family effects.
func (g *RogueGame) zapVictim() *Monster {
tp := g.zapRayMonster()
if tp != nil && tp.Type == 'F' {
g.Player.Flags.Clear(Held)
}
return tp
}
// The per-wand effect handlers, dispatched through gameData.zapHandlers.
// Each is one case of the C do_zap switch; returning false aborts the
// zap without using a charge.
func (g *RogueGame) zapLight(*Object) bool {
// Reddy Kilowatt wand. Light up the room
p := &g.Player
g.Items.Sticks[WandLight].Know = true
if p.Room.Flags.Has(Gone) {
g.msg("the corridor glows and then fades")
@@ -50,40 +90,45 @@ func (g *RogueGame) doZap() {
g.endmsg()
}
case WandDrainLife:
return true
}
func (g *RogueGame) zapDrainLife(*Object) bool {
// take away 1/2 of hero's hit points, then take it away evenly
// from the monsters in the room (or next to hero if he is in a
// passage)
if p.Stats.HP < 2 {
if g.Player.Stats.HP < 2 {
g.msg("you are too weak to use it")
return
return false
}
g.drain()
case WandInvisibility, WandPolymorph, WandTeleportAway, WandTeleportTo, WandCancellation:
y := p.Pos.Y
x := p.Pos.X
for stepOk(g.Level.VisibleChar(y, x)) {
y += g.Delta.Y
x += g.Delta.X
return true
}
if tp := g.Level.MonsterAt(y, x); tp != nil {
monster := tp.Type
if monster == 'F' {
p.Flags.Clear(Held)
}
switch obj.WandKind() {
case WandInvisibility:
func (g *RogueGame) zapInvisibility(*Object) bool {
if tp := g.zapVictim(); tp != nil {
tp.Flags.Set(Invisible)
if g.canSee(y, x) {
g.mvaddch(y, x, tp.OldCh)
if g.canSee(tp.Pos.Y, tp.Pos.X) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.OldCh)
}
case WandPolymorph:
}
return true
}
func (g *RogueGame) zapPolymorph(*Object) bool {
tp := g.zapVictim()
if tp == nil {
return true
}
y, x := tp.Pos.Y, tp.Pos.X
pp := tp.Pack
g.Level.RemoveMonster(tp)
@@ -94,7 +139,7 @@ func (g *RogueGame) doZap() {
oldch := tp.OldCh
g.Delta.Y = y
g.Delta.X = x
monster = g.randomMonsterLetter()
monster := g.randomMonsterLetter()
g.newMonster(tp, monster, g.Delta)
if g.seeMonst(tp) {
@@ -107,15 +152,32 @@ func (g *RogueGame) doZap() {
if g.seeMonst(tp) {
g.Items.Sticks[WandPolymorph].Know = true
}
case WandCancellation:
return true
}
func (g *RogueGame) zapCancellation(*Object) bool {
if tp := g.zapVictim(); tp != nil {
tp.Flags.Set(Cancelled)
tp.Flags.Clear(Invisible | CanConfuse)
tp.Disguise = tp.Type
if g.seeMonst(tp) {
g.mvaddch(y, x, tp.Disguise)
g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Disguise)
}
case WandTeleportAway, WandTeleportTo:
}
return true
}
func (g *RogueGame) zapTeleport(obj *Object) bool {
p := &g.Player
tp := g.zapVictim()
if tp == nil {
return true
}
var newPos Coord
if obj.WandKind() == WandTeleportAway {
@@ -133,9 +195,13 @@ func (g *RogueGame) doZap() {
tp.Dest = &p.Pos
tp.Flags.Set(Awake)
g.relocate(tp, newPos)
return true
}
}
case WandMagicMissile:
func (g *RogueGame) zapMagicMissile(*Object) bool {
p := &g.Player
g.Items.Sticks[WandMagicMissile].Know = true
bolt := newObject()
bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon
@@ -158,23 +224,42 @@ func (g *RogueGame) doZap() {
} else {
g.msg("the missle vanishes with a puff of smoke") //nolint:misspell // C's spelling
}
case WandHasteMonster, WandSlowMonster:
y := p.Pos.Y
x := p.Pos.X
for stepOk(g.Level.VisibleChar(y, x)) {
y += g.Delta.Y
x += g.Delta.X
return true
}
func (g *RogueGame) zapSpeed(obj *Object) bool {
tp := g.zapRayMonster()
if tp == nil {
return true
}
if tp := g.Level.MonsterAt(y, x); tp != nil {
if obj.WandKind() == WandHasteMonster {
hasteTarget(tp)
} else {
slowTarget(tp)
}
g.Delta.Y = tp.Pos.Y
g.Delta.X = tp.Pos.X
g.runTo(g.Delta)
return true
}
// hasteTarget cancels a slow or applies a haste (the WS_HASTE_M arm of
// do_zap).
func hasteTarget(tp *Monster) {
if tp.On(Slowed) {
tp.Flags.Clear(Slowed)
} else {
tp.Flags.Set(Hasted)
}
} else {
}
// slowTarget cancels a haste or applies a slow (the WS_SLOW_M arm of
// do_zap).
func slowTarget(tp *Monster) {
if tp.On(Hasted) {
tp.Flags.Clear(Hasted)
} else {
@@ -184,11 +269,7 @@ func (g *RogueGame) doZap() {
tp.Turn = true
}
g.Delta.Y = y
g.Delta.X = x
g.runTo(g.Delta)
}
case WandLightning, WandFire, WandCold:
func (g *RogueGame) zapBolt(obj *Object) bool {
var name string
switch obj.WandKind() {
@@ -200,12 +281,10 @@ func (g *RogueGame) doZap() {
name = "ice"
}
g.fireBolt(p.Pos, &g.Delta, name)
g.fireBolt(g.Player.Pos, &g.Delta, name)
g.Items.Sticks[obj.Which].Know = true
case WandNothing:
}
obj.Charges--
return true
}
// drain does the drain-hit-points-from-player schtick (sticks.c drain).
@@ -222,9 +301,7 @@ func (g *RogueGame) drain() {
var drainee []*Monster
for _, mp := range g.Level.Monsters {
if mp.Room == p.Room || mp.Room == corp ||
(inpass && g.Level.Char(mp.Pos.Y, mp.Pos.X) == Door &&
&g.Level.Passages[*g.Level.FlagsAt(mp.Pos.Y, mp.Pos.X)&FPassNum] == p.Room) {
if g.drainReaches(mp, corp, inpass) {
drainee = append(drainee, mp)
}
}
@@ -248,6 +325,18 @@ func (g *RogueGame) drain() {
}
}
// drainReaches reports whether the drain-life wand reaches this monster:
// the hero's room, the passage behind the door he stands on, or — when
// he is in a passage — a door of that same passage (the drainee
// condition of sticks.c drain).
func (g *RogueGame) drainReaches(mp *Monster, corp *Room, inpass bool) bool {
p := &g.Player
return mp.Room == p.Room || mp.Room == corp ||
(inpass && g.Level.Char(mp.Pos.Y, mp.Pos.X) == Door &&
&g.Level.Passages[*g.Level.FlagsAt(mp.Pos.Y, mp.Pos.X)&FPassNum] == p.Room)
}
// fireBolt fires a bolt in a given direction from a specific starting
// place (sticks.c fire_bolt).
func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
@@ -262,21 +351,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
bolt.DPlus = 0
g.Items.Weapons[WeaponFlame].Name = name
var dirch byte
switch dir.Y + dir.X {
case 0:
dirch = '/'
case 1, -1:
if dir.Y == 0 {
dirch = '-'
} else {
dirch = '|'
}
case 2, -2:
dirch = '\\'
}
dirch := boltDirChar(*dir)
pos := start
hitHero := !fromHero
used := false
@@ -287,22 +362,9 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
pos.Y += dir.Y
pos.X += dir.X
spotpos = append(spotpos, pos)
ch := g.Level.VisibleChar(pos.Y, pos.X)
bounce := false
switch ch {
case Door:
// this code is necessary if the hero is on a door and he
// fires at the wall the door is in, it would otherwise loop
// infinitely
if p.Pos != pos {
bounce = true
}
case '|', '-', ' ':
bounce = true
}
if bounce {
if boltBounces(ch, p.Pos, pos) {
if !changed {
hitHero = !hitHero
}
@@ -320,11 +382,62 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
if tp := g.Level.MonsterAt(pos.Y, pos.X); !hitHero && tp != nil {
hitHero = true
changed = !changed
used = g.boltStrikesMonster(tp, bolt, pos, ch, name, fromHero)
} else if hitHero && pos == p.Pos {
hitHero = false
changed = !changed
used = g.boltStrikesHero(start, name, fromHero)
}
g.mvaddch(pos.Y, pos.X, dirch)
g.refresh()
}
// erase the bolt trail
for _, c2 := range spotpos {
g.mvaddch(c2.Y, c2.X, g.Level.Char(c2.Y, c2.X))
}
}
// boltDirChar picks the character a traveling bolt is drawn with for its
// direction (the dirch switch of sticks.c fire_bolt).
func boltDirChar(dir Coord) byte {
switch dir.Y + dir.X {
case 0:
return '/'
case 1, -1:
if dir.Y == 0 {
return '-'
}
return '|'
case 2, -2:
return '\\'
}
return 0 // unreachable for the eight legal directions, as in C
}
// boltBounces reports whether a bolt bounces off this spot: walls, and
// any door except the one the hero stands on (which would otherwise loop
// infinitely, per the C comment in fire_bolt).
func boltBounces(ch byte, heroPos, pos Coord) bool {
switch ch {
case Door:
return heroPos != pos
case '|', '-', ' ':
return true
}
return false
}
// boltStrikesMonster resolves a bolt arriving on a monster's square (the
// monster arm of the fire_bolt loop). It reports whether the bolt was
// used up.
func (g *RogueGame) boltStrikesMonster(tp *Monster, bolt *Object, pos Coord, ch byte, name string, fromHero bool) bool {
tp.OldCh = g.Level.Char(pos.Y, pos.X)
if !g.saveThrow(VsMagic, &tp.Stats) {
bolt.Pos = pos
used = true
if tp.Type == 'D' && name == "flame" {
g.addmsgf("the flame bounces")
@@ -337,7 +450,11 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
} else {
g.hitMonster(pos, bolt)
}
} else if ch != 'M' || tp.Disguise == 'M' {
return true
}
if ch != 'M' || tp.Disguise == 'M' {
if fromHero {
g.runTo(pos)
}
@@ -348,11 +465,20 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
g.msg("the %s whizzes past %s", name, g.setMname(tp))
}
}
} else if hitHero && pos == p.Pos {
hitHero = false
changed = !changed
if !g.save(VsMagic) {
return false
}
// boltStrikesHero resolves a bolt arriving on the hero (the hero arm of
// the fire_bolt loop). It reports whether the bolt was used up.
func (g *RogueGame) boltStrikesHero(start Coord, name string, fromHero bool) bool {
p := &g.Player
if g.save(VsMagic) {
g.msg("the %s whizzes by you", name)
return false
}
if p.Stats.HP -= g.roll(6, 6); p.Stats.HP <= 0 {
if fromHero {
g.death('b')
@@ -361,25 +487,13 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
}
}
used = true
if g.Options.Terse {
g.msg("the %s hits", name)
} else {
g.msg("you are hit by the %s", name)
}
} else {
g.msg("the %s whizzes by you", name)
}
}
g.mvaddch(pos.Y, pos.X, dirch)
g.refresh()
}
// erase the bolt trail
for _, c2 := range spotpos {
g.mvaddch(c2.Y, c2.X, g.Level.Char(c2.Y, c2.X))
}
return true
}
// fixStick sets up a new wand or staff (sticks.c fix_stick).

View File

@@ -122,6 +122,39 @@ type gameData struct {
// scoreReasons is the rip.c reason[] scoreboard strings.
scoreReasons [4]string
// quaffHandlers dispatches each potion kind to its effect method:
// the cases of the potions.c quaff switch. The bool is trip — was
// the hero hallucinating when the potion went down.
quaffHandlers [NumPotionTypes]func(g *RogueGame, trip bool)
// readHandlers dispatches each scroll kind to its effect method:
// the cases of the scrolls.c read_scroll switch. The handler gets
// the scroll being read (the identify family needs its subtype).
readHandlers [NumScrollTypes]func(g *RogueGame, obj *Object)
// zapHandlers dispatches each wand kind to its effect method: the
// cases of the sticks.c do_zap switch. A false return aborts the
// zap without using a charge (drain life on a too-weak hero).
zapHandlers [NumWandTypes]func(g *RogueGame, obj *Object) bool
// hitHandlers dispatches a monster's special power when its hit
// lands, indexed by monster letter - 'A': the cases of the fight.c
// attack switch. A true return means the monster removed itself.
hitHandlers [26]func(g *RogueGame, mp *Monster, mname string) bool
// commandHandlers dispatches the ordinary command keys: the simple
// cases of the big command.c switch. Keys that re-dispatch (runs,
// fight, repeat, move-on) and wizard keys stay in dispatchKey.
commandHandlers map[byte]func(g *RogueGame)
// trapHandlers dispatches each sprung trap to its effect method:
// the cases of the move.c be_trapped switch.
trapHandlers [NumTrapTypes]func(g *RogueGame, tc Coord)
// daemonHandlers dispatches DaemonIDs to their callbacks: the C
// d_func function pointers (daemons.c / daemon.c).
daemonHandlers [DTurnSee + 1]func(g *RogueGame, arg int)
}
// ripWall is the repeated blank wall line of the tombstone art.
@@ -587,6 +620,234 @@ func newGameData() *gameData {
"A total winner",
"killed with Amulet",
},
quaffHandlers: [NumPotionTypes]func(*RogueGame, bool){
PotionConfusion: (*RogueGame).quaffConfusion,
PotionLSD: (*RogueGame).quaffLSD,
PotionPoison: (*RogueGame).quaffPoison,
PotionGainStrength: (*RogueGame).quaffGainStrength,
PotionSeeInvisible: (*RogueGame).quaffSeeInvisible,
PotionHealing: (*RogueGame).quaffHealing,
PotionDetectMonsters: (*RogueGame).quaffDetectMonsters,
PotionDetectMagic: (*RogueGame).quaffDetectMagic,
PotionRaiseLevel: (*RogueGame).quaffRaiseLevel,
PotionExtraHealing: (*RogueGame).quaffExtraHealing,
PotionHaste: (*RogueGame).quaffHaste,
PotionRestoreStrength: (*RogueGame).quaffRestoreStrength,
PotionBlindness: (*RogueGame).quaffBlindness,
PotionLevitation: (*RogueGame).quaffLevitation,
},
readHandlers: [NumScrollTypes]func(*RogueGame, *Object){
ScrollMonsterConfusion: (*RogueGame).readMonsterConfusion,
ScrollMagicMapping: (*RogueGame).readMagicMapping,
ScrollHoldMonster: (*RogueGame).readHoldMonster,
ScrollSleep: (*RogueGame).readSleep,
ScrollEnchantArmor: (*RogueGame).readEnchantArmor,
ScrollIdentifyPotion: (*RogueGame).readIdentify,
ScrollIdentifyScroll: (*RogueGame).readIdentify,
ScrollIdentifyWeapon: (*RogueGame).readIdentify,
ScrollIdentifyArmor: (*RogueGame).readIdentify,
ScrollIdentifyRingOrStick: (*RogueGame).readIdentify,
ScrollScareMonster: (*RogueGame).readScareMonster,
ScrollFoodDetection: (*RogueGame).readFoodDetection,
ScrollTeleportation: (*RogueGame).readTeleportation,
ScrollEnchantWeapon: (*RogueGame).readEnchantWeapon,
ScrollCreateMonster: (*RogueGame).readCreateMonster,
ScrollRemoveCurse: (*RogueGame).readRemoveCurse,
ScrollAggravateMonsters: (*RogueGame).readAggravateMonsters,
ScrollProtectArmor: (*RogueGame).readProtectArmor,
},
zapHandlers: [NumWandTypes]func(*RogueGame, *Object) bool{
WandLight: (*RogueGame).zapLight,
WandInvisibility: (*RogueGame).zapInvisibility,
WandLightning: (*RogueGame).zapBolt,
WandFire: (*RogueGame).zapBolt,
WandCold: (*RogueGame).zapBolt,
WandPolymorph: (*RogueGame).zapPolymorph,
WandMagicMissile: (*RogueGame).zapMagicMissile,
WandHasteMonster: (*RogueGame).zapSpeed,
WandSlowMonster: (*RogueGame).zapSpeed,
WandDrainLife: (*RogueGame).zapDrainLife,
WandTeleportAway: (*RogueGame).zapTeleport,
WandTeleportTo: (*RogueGame).zapTeleport,
WandCancellation: (*RogueGame).zapCancellation,
},
hitHandlers: [26]func(*RogueGame, *Monster, string) bool{
0: (*RogueGame).hitAquator, // 'A' - 'A'
'F' - 'A': (*RogueGame).hitFlytrap,
'I' - 'A': (*RogueGame).hitIceMonster,
'L' - 'A': (*RogueGame).hitLeprechaun,
'N' - 'A': (*RogueGame).hitNymph,
'R' - 'A': (*RogueGame).hitRattlesnake,
'V' - 'A': (*RogueGame).hitLifeDrainer,
'W' - 'A': (*RogueGame).hitLifeDrainer,
},
commandHandlers: map[byte]func(*RogueGame){
',': (*RogueGame).pickupCommand,
'!': (*RogueGame).shell,
'h': func(g *RogueGame) { g.moveHero(0, -1) },
'j': func(g *RogueGame) { g.moveHero(1, 0) },
'k': func(g *RogueGame) { g.moveHero(-1, 0) },
'l': func(g *RogueGame) { g.moveHero(0, 1) },
'y': func(g *RogueGame) { g.moveHero(-1, -1) },
'u': func(g *RogueGame) { g.moveHero(-1, 1) },
'b': func(g *RogueGame) { g.moveHero(1, -1) },
'n': func(g *RogueGame) { g.moveHero(1, 1) },
'H': func(g *RogueGame) { g.startRun('h') },
'J': func(g *RogueGame) { g.startRun('j') },
'K': func(g *RogueGame) { g.startRun('k') },
'L': func(g *RogueGame) { g.startRun('l') },
'Y': func(g *RogueGame) { g.startRun('y') },
'U': func(g *RogueGame) { g.startRun('u') },
'B': func(g *RogueGame) { g.startRun('b') },
'N': func(g *RogueGame) { g.startRun('n') },
't': func(g *RogueGame) {
if !g.promptDirection() {
g.After = false
} else {
g.missile(g.Delta.Y, g.Delta.X)
}
},
'q': (*RogueGame).quaff,
'Q': func(g *RogueGame) {
g.After = false
g.QComm = true
g.quit(0)
g.QComm = false
},
'i': func(g *RogueGame) {
g.After = false
g.inventory(g.Player.Pack, 0)
},
'I': func(g *RogueGame) {
g.After = false
g.pickyInven()
},
'd': (*RogueGame).dropIt,
'r': (*RogueGame).readScroll,
'e': (*RogueGame).eat,
'w': (*RogueGame).wield,
'W': (*RogueGame).wear,
'T': (*RogueGame).takeOff,
'P': (*RogueGame).ringOn,
'R': (*RogueGame).ringOff,
'o': func(g *RogueGame) {
g.option()
g.After = false
},
'c': func(g *RogueGame) {
g.call()
g.After = false
},
'>': func(g *RogueGame) {
g.After = false
g.dLevel()
},
'<': func(g *RogueGame) {
g.After = false
g.uLevel()
},
'?': func(g *RogueGame) {
g.After = false
g.help()
},
'/': func(g *RogueGame) {
g.After = false
g.identify()
},
's': (*RogueGame).search,
'z': func(g *RogueGame) {
if g.promptDirection() {
g.doZap()
} else {
g.After = false
}
},
'D': func(g *RogueGame) {
g.After = false
g.discovered()
},
CTRL('P'): func(g *RogueGame) {
g.After = false
g.msg("%s", g.Msgs.Huh)
},
CTRL('R'): func(g *RogueGame) {
g.After = false
g.refresh()
},
'v': func(g *RogueGame) {
g.After = false
g.msg("version %s. (mctesq was here)", Release)
},
'S': func(g *RogueGame) {
g.After = false
g.saveGame()
},
'.': func(*RogueGame) {
// rest command
},
' ': func(g *RogueGame) {
g.After = false // "legal" illegal command
},
'^': (*RogueGame).identifyTrapCommand,
Escape: func(g *RogueGame) {
g.DoorStop = false
g.Count = 0
g.After = false
g.Again = false
},
')': func(g *RogueGame) {
g.current(g.Player.CurWeapon, "wielding", "")
},
']': func(g *RogueGame) {
g.current(g.Player.CurArmor, "wearing", "")
},
'=': func(g *RogueGame) {
g.current(g.Player.CurRing[Left], "wearing",
g.chooseTerse("(L)", "on left hand"))
g.current(g.Player.CurRing[Right], "wearing",
g.chooseTerse("(R)", "on right hand"))
},
'@': func(g *RogueGame) {
g.StatMsg = true
g.status()
g.StatMsg = false
g.After = false
},
},
trapHandlers: [NumTrapTypes]func(*RogueGame, Coord){
TrapDoor: (*RogueGame).trapFall,
TrapArrow: (*RogueGame).trapArrow,
TrapSleep: (*RogueGame).trapSleep,
TrapBear: (*RogueGame).trapBear,
TrapTeleport: (*RogueGame).trapTeleport,
TrapDart: (*RogueGame).trapDart,
TrapRust: (*RogueGame).trapRust,
TrapMystery: (*RogueGame).trapMystery,
},
daemonHandlers: [DTurnSee + 1]func(*RogueGame, int){
DRollwand: (*RogueGame).rollwand,
DDoctor: (*RogueGame).doctor,
DStomach: (*RogueGame).stomach,
DRunners: (*RogueGame).runners,
DSwander: (*RogueGame).swander,
DNohaste: (*RogueGame).nohaste,
DUnconfuse: (*RogueGame).unconfuse,
DUnsee: (*RogueGame).unsee,
DSight: (*RogueGame).sight,
DVisuals: (*RogueGame).visuals,
DComeDown: (*RogueGame).comeDown,
DLand: (*RogueGame).land,
DTurnSee: func(g *RogueGame, arg int) {
g.turnSee(arg != 0)
},
},
}
}

View File

@@ -48,6 +48,22 @@ func TestInitProbsCumulative(t *testing.T) {
func TestNewGameRandomizesAppearances(t *testing.T) {
g := NewGame(Config{Seed: 12345})
checkPotionColors(t, g)
checkScrollNames(t, g)
checkWandMaterials(t, g)
// Determinism: same seed, same appearances.
h := NewGame(Config{Seed: 12345})
if h.Items != g.Items {
t.Error("two games with the same seed produced different item lore")
}
}
// checkPotionColors verifies every potion has a distinct color.
func checkPotionColors(t *testing.T, g *RogueGame) {
t.Helper()
seen := map[string]bool{}
for i, c := range g.Items.PotColors {
@@ -61,6 +77,12 @@ func TestNewGameRandomizesAppearances(t *testing.T) {
seen[c] = true
}
}
// checkScrollNames verifies every scroll has a name within the C buffer
// limit.
func checkScrollNames(t *testing.T, g *RogueGame) {
t.Helper()
for i, n := range g.Items.ScrNames {
if n == "" {
@@ -71,9 +93,15 @@ func TestNewGameRandomizesAppearances(t *testing.T) {
t.Errorf("scroll name %q longer than C buffer allows", n)
}
}
}
// checkWandMaterials verifies every stick has a wand/staff type and a
// material.
func checkWandMaterials(t *testing.T, g *RogueGame) {
t.Helper()
for i := range g.Items.WandType {
if g.Items.WandType[i] != "wand" && g.Items.WandType[i] != "staff" {
if g.Items.WandType[i] != wandName && g.Items.WandType[i] != staffName {
t.Errorf("stick %d has type %q", i, g.Items.WandType[i])
}
@@ -81,12 +109,6 @@ func TestNewGameRandomizesAppearances(t *testing.T) {
t.Errorf("stick %d has no material", i)
}
}
// Determinism: same seed, same appearances.
h := NewGame(Config{Seed: 12345})
if h.Items != g.Items {
t.Error("two games with the same seed produced different item lore")
}
}
func TestMonsterTable(t *testing.T) {

View File

@@ -23,47 +23,74 @@ func (g *RogueGame) inventoryName(obj *Object, drop bool) string {
case KindWand:
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr)
case KindScroll:
g.nameScroll(&pb, obj)
case KindFood:
g.nameFood(&pb, obj)
case KindWeapon:
g.nameWeapon(&pb, obj)
case KindArmor:
g.nameArmor(&pb, obj)
case KindAmulet:
pb.WriteString("The Amulet of Yendor")
case KindGold:
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue)
}
return fixNameCase(g.describeWorn(obj, pb.String()), drop)
}
// nameScroll writes a scroll's inventory name (things.c inv_name).
func (g *RogueGame) nameScroll(pb *strings.Builder, obj *Object) {
if obj.Count == 1 {
pb.WriteString("A scroll ")
} else {
fmt.Fprintf(&pb, "%d scrolls ", obj.Count)
fmt.Fprintf(pb, "%d scrolls ", obj.Count)
}
op := &it.Scrolls[which]
op := &g.Items.Scrolls[obj.Which]
switch {
case op.Know:
fmt.Fprintf(&pb, "of %s", op.Name)
fmt.Fprintf(pb, "of %s", op.Name)
case op.Guess != "":
fmt.Fprintf(&pb, "called %s", op.Guess)
fmt.Fprintf(pb, "called %s", op.Guess)
default:
fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which])
fmt.Fprintf(pb, "titled '%s'", g.Items.ScrNames[obj.Which])
}
case KindFood:
if which == 1 {
}
// nameFood writes a food item's inventory name; which 1 is the fruit
// (things.c inv_name).
func (g *RogueGame) nameFood(pb *strings.Builder, obj *Object) {
if obj.Which == 1 {
if obj.Count == 1 {
fmt.Fprintf(&pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
fmt.Fprintf(pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
} else {
fmt.Fprintf(&pb, "%d %ss", obj.Count, g.Fruit)
fmt.Fprintf(pb, "%d %ss", obj.Count, g.Fruit)
}
} else {
return
}
if obj.Count == 1 {
pb.WriteString("Some food")
} else {
fmt.Fprintf(&pb, "%d rations of food", obj.Count)
fmt.Fprintf(pb, "%d rations of food", obj.Count)
}
}
case KindWeapon:
sp := it.Weapons[which].Name
// nameWeapon writes a weapon's inventory name (things.c inv_name).
func (g *RogueGame) nameWeapon(pb *strings.Builder, obj *Object) {
sp := g.Items.Weapons[obj.Which].Name
if obj.Count > 1 {
fmt.Fprintf(&pb, "%d ", obj.Count)
fmt.Fprintf(pb, "%d ", obj.Count)
} else {
fmt.Fprintf(&pb, "A%s ", vowelstr(sp))
fmt.Fprintf(pb, "A%s ", vowelstr(sp))
}
if obj.Flags.Has(Known) {
fmt.Fprintf(&pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
fmt.Fprintf(pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
} else {
pb.WriteString(sp)
}
@@ -73,34 +100,38 @@ func (g *RogueGame) inventoryName(obj *Object, drop bool) string {
}
if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label)
fmt.Fprintf(pb, " called %s", obj.Label)
}
case KindArmor:
sp := it.Armors[which].Name
}
// nameArmor writes an armor's inventory name (things.c inv_name).
func (g *RogueGame) nameArmor(pb *strings.Builder, obj *Object) {
sp := g.Items.Armors[obj.Which].Name
if obj.Flags.Has(Known) {
fmt.Fprintf(&pb, "%s %s [", num(g.data.aClass[which]-obj.ArmorClass, 0, Armor), sp)
fmt.Fprintf(pb, "%s %s [",
num(g.data.aClass[obj.Which]-obj.ArmorClass, 0, Armor), sp)
if !g.Options.Terse {
pb.WriteString("protection ")
}
fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass)
fmt.Fprintf(pb, "%d]", 10-obj.ArmorClass)
} else {
pb.WriteString(sp)
}
if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label)
fmt.Fprintf(pb, " called %s", obj.Label)
}
case KindAmulet:
pb.WriteString("The Amulet of Yendor")
case KindGold:
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue)
}
out := pb.String()
// describeWorn appends the equipped-status notes to an inventory name
// (things.c inv_name).
func (g *RogueGame) describeWorn(obj *Object, out string) string {
if !g.InvDescribe {
return out
}
if g.InvDescribe {
p := &g.Player
if obj == p.CurArmor {
out += " (being worn)"
@@ -116,14 +147,23 @@ func (g *RogueGame) inventoryName(obj *Object, drop bool) string {
case p.CurRing[Right]:
out += " (on right hand)"
}
return out
}
if out != "" {
if drop && isUpper(out[0]) {
out = string(toLower(out[0])) + out[1:]
} else if !drop && isLower(out[0]) {
out = string(toUpper(out[0])) + out[1:]
// fixNameCase upper- or lowercases the leading letter to suit the
// sentence it will land in (things.c inv_name).
func fixNameCase(out string, drop bool) string {
if out == "" {
return out
}
if drop && isUpper(out[0]) {
return string(toLower(out[0])) + out[1:]
}
if !drop && isLower(out[0]) {
return string(toUpper(out[0])) + out[1:]
}
return out
@@ -192,6 +232,17 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
p.CurArmor = nil
default:
g.dropRing(obj)
}
return true
}
// dropRing takes a worn ring off with its side effects (things.c
// dropcheck).
func (g *RogueGame) dropRing(obj *Object) {
p := &g.Player
hand := Right
if obj == p.CurRing[Left] {
hand = Left
@@ -208,9 +259,6 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
}
}
return true
}
// newThing returns a new random thing for the dungeon (things.c new_thing).
func (g *RogueGame) newThing() *Object {
cur := newObject()
@@ -236,6 +284,25 @@ func (g *RogueGame) newThing() *Object {
cur.Kind = KindScroll
cur.Which = pickOne(g, g.Items.Scrolls[:])
case 2:
g.newFoodThing(cur)
case 3:
g.newWeaponThing(cur)
case 4:
g.newArmorThing(cur)
case 5:
g.newRingThing(cur)
case 6:
cur.Kind = KindWand
cur.Which = pickOne(g, g.Items.Sticks[:])
g.fixStick(cur)
}
return cur
}
// newFoodThing rolls food, one time in ten the fruit (things.c
// new_thing).
func (g *RogueGame) newFoodThing(cur *Object) {
cur.Kind = KindFood
g.Player.NoFood = 0
@@ -244,7 +311,11 @@ func (g *RogueGame) newThing() *Object {
} else {
cur.Which = 1
}
case 3:
}
// newWeaponThing rolls a weapon, sometimes cursed or blessed (things.c
// new_thing).
func (g *RogueGame) newWeaponThing(cur *Object) {
g.initWeapon(cur, WeaponKind(pickOne(g, g.Items.Weapons[:NumWeaponTypes])))
if r := g.rnd(100); r < 10 {
@@ -253,7 +324,11 @@ func (g *RogueGame) newThing() *Object {
} else if r < 15 {
cur.HPlus += g.rnd(3) + 1
}
case 4:
}
// newArmorThing rolls armor, sometimes cursed or blessed (things.c
// new_thing).
func (g *RogueGame) newArmorThing(cur *Object) {
cur.Kind = KindArmor
cur.Which = pickOne(g, g.Items.Armors[:])
@@ -264,7 +339,10 @@ func (g *RogueGame) newThing() *Object {
} else if r < 28 {
cur.ArmorClass -= g.rnd(3) + 1
}
case 5:
}
// newRingThing rolls a ring, cursing the bad ones (things.c new_thing).
func (g *RogueGame) newRingThing(cur *Object) {
cur.Kind = KindRing
cur.Which = pickOne(g, g.Items.Rings[:])
@@ -277,13 +355,6 @@ func (g *RogueGame) newThing() *Object {
case RingAggravateMonsters, RingTeleportation:
cur.Flags.Set(Cursed)
}
case 6:
cur.Kind = KindWand
cur.Which = pickOne(g, g.Items.Sticks[:])
g.fixStick(cur)
}
return cur
}
// pickOne picks an item out of a list of possible objects using their
@@ -423,7 +494,6 @@ const flushSentinel = "\x00"
func (g *RogueGame) addLine(format string, a ...any) int {
pg := &g.invPage
prompt := "--Press space to continue--"
isFlush := format == flushSentinel
var line string
@@ -440,23 +510,80 @@ func (g *RogueGame) addLine(format string, a ...any) int {
}
if g.Options.InvType == InvSlow {
return g.addLineSlow(line, isFlush)
}
g.addLinePaged(line, isFlush)
return ^Escape
}
// addLineSlow shows one discovery line as a message (the slow-inventory
// arm of things.c add_line).
func (g *RogueGame) addLineSlow(line string, isFlush bool) int {
if !isFlush && line != "" {
if g.msg("%s", line) == Escape {
return Escape
}
}
pg.lineCnt++
} else {
g.invPage.lineCnt++
return ^Escape
}
// addLinePaged accumulates discovery lines into the paged window,
// prompting between full pages (the windowed arm of things.c add_line).
func (g *RogueGame) addLinePaged(line string, isFlush bool) {
pg := &g.invPage
prompt := "--Press space to continue--"
if !pg.init {
pg.maxlen = len(prompt)
pg.init = true
}
if pg.lineCnt >= NumLines-1 || isFlush {
g.addLinePageBreak(prompt, isFlush)
}
if !isFlush && (pg.lineCnt != 0 || line != "") {
g.scr.Hw.MvAddStr(pg.lineCnt, 0, line)
pg.lineCnt++
if pg.maxlen < len(line) {
pg.maxlen = len(line)
}
pg.lastLine = line
}
}
// addLinePageBreak prompts at a full page and starts a fresh one
// (things.c add_line).
func (g *RogueGame) addLinePageBreak(prompt string, isFlush bool) {
pg := &g.invPage
if g.Options.InvType == InvOver && isFlush && !pg.newpage {
// Overlay the accumulated list in a box at the top right
// of the screen, prompt, and restore what was beneath.
g.addLineOverlay(prompt)
} else {
g.scr.Hw.MvAddStr(NumLines-1, 0, prompt)
g.scr.RefreshWin(g.scr.Hw)
g.waitFor(' ')
g.scr.Hw.Clear()
g.refresh()
}
pg.newpage = true
pg.lineCnt = 0
pg.maxlen = len(prompt)
}
// addLineOverlay draws the accumulated list in a box at the top right
// of the screen, prompts, and restores what was beneath (things.c
// add_line).
func (g *RogueGame) addLineOverlay(prompt string) {
pg := &g.invPage
g.msg("")
g.refresh()
@@ -475,32 +602,6 @@ func (g *RogueGame) addLine(format string, a ...any) int {
g.waitFor(' ')
g.scr.Std.CopyFrom(saved)
g.refresh()
} else {
g.scr.Hw.MvAddStr(NumLines-1, 0, prompt)
g.scr.RefreshWin(g.scr.Hw)
g.waitFor(' ')
g.scr.Hw.Clear()
g.refresh()
}
pg.newpage = true
pg.lineCnt = 0
pg.maxlen = len(prompt)
}
if !isFlush && (pg.lineCnt != 0 || line != "") {
g.scr.Hw.MvAddStr(pg.lineCnt, 0, line)
pg.lineCnt++
if pg.maxlen < len(line) {
pg.maxlen = len(line)
}
pg.lastLine = line
}
}
return ^Escape
}
// flushLine is add_line(NULL): force out the accumulated page.

View File

@@ -35,8 +35,30 @@ func (g *RogueGame) doMotion(obj *Object, ydelta, xdelta int) {
// Come fly with us ...
obj.Pos = p.Pos
for {
// Erase the old one
if obj.Pos != p.Pos && g.canSee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
g.eraseFlight(obj, p.Pos)
// Get the new position
obj.Pos.Y += ydelta
obj.Pos.X += xdelta
ch := g.Level.VisibleChar(obj.Pos.Y, obj.Pos.X)
if !stepOk(ch) || ch == Door {
break
}
// It hasn't hit anything yet, so display it if it's alright.
if g.canSee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
g.mvaddch(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
g.refresh()
}
}
}
// eraseFlight erases a flying object from its current square, unless it
// still sits on the hero (the erase step of weapons.c do_motion).
func (g *RogueGame) eraseFlight(obj *Object, heroPos Coord) {
if obj.Pos == heroPos || !g.canSee(obj.Pos.Y, obj.Pos.X) || g.Options.Terse {
return
}
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
if ch == Floor && !g.showFloor() {
ch = ' '
@@ -44,24 +66,6 @@ func (g *RogueGame) doMotion(obj *Object, ydelta, xdelta int) {
g.mvaddch(obj.Pos.Y, obj.Pos.X, ch)
}
// Get the new position
obj.Pos.Y += ydelta
obj.Pos.X += xdelta
ch := g.Level.VisibleChar(obj.Pos.Y, obj.Pos.X)
if stepOk(ch) && ch != Door {
// It hasn't hit anything yet, so display it if it's alright.
if g.canSee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
g.mvaddch(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
g.refresh()
}
continue
}
break
}
}
// fall drops an item someplace around here (weapons.c fall).
func (g *RogueGame) fall(obj *Object, pr bool) {

View File

@@ -27,6 +27,26 @@ func (g *RogueGame) createObj() {
switch obj.Kind {
case KindWeapon, KindArmor:
g.createWeaponArmor(obj)
case KindRing:
g.createRing(obj)
case KindWand:
g.fixStick(obj)
case KindGold:
g.msg("how much?")
buf := ""
if g.getStr(&buf, g.scr.Std) == Norm {
obj.GoldValue = cAtoi(buf)
}
}
g.addPack(obj, false)
}
// createWeaponArmor sets up a wizard-created weapon or armor with an
// optional blessing (the weapon/armor arm of wizard.c create_obj).
func (g *RogueGame) createWeaponArmor(obj *Object) {
g.msg("blessing? (+,-,n)")
bless := g.readchar()
g.Msgs.Mpos = 0
@@ -45,7 +65,10 @@ func (g *RogueGame) createObj() {
if bless == '+' {
obj.HPlus += g.rnd(3) + 1
}
} else {
return
}
obj.ArmorClass = g.data.aClass[obj.Which]
if bless == '-' {
obj.ArmorClass += g.rnd(3) + 1
@@ -55,7 +78,10 @@ func (g *RogueGame) createObj() {
obj.ArmorClass -= g.rnd(3) + 1
}
}
case KindRing:
// createRing sets up a wizard-created ring, prompting for a bonus on
// the bonus rings (the ring arm of wizard.c create_obj).
func (g *RogueGame) createRing(obj *Object) {
switch obj.RingKind() {
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
g.msg("blessing? (+,-,n)")
@@ -71,18 +97,6 @@ func (g *RogueGame) createObj() {
case RingAggravateMonsters, RingTeleportation:
obj.Flags.Set(Cursed)
}
case KindWand:
g.fixStick(obj)
case KindGold:
g.msg("how much?")
buf := ""
if g.getStr(&buf, g.scr.Std) == Norm {
obj.GoldValue = cAtoi(buf)
}
}
g.addPack(obj, false)
}
// showMap prints out the whole map for the wizard (wizard.c show_map).
@@ -117,34 +131,8 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
return
}
var obj *Object
for {
obj, _ = g.promptPackItem("identify", kind)
if !insist {
break
}
if g.NObjs == 0 {
return
}
if obj == nil {
g.msg("you must identify something")
continue
}
if !matchesFilter(kind, obj) {
g.msg("you must identify a %s", kind)
continue
}
break
}
if obj == nil {
obj, ok := g.whatisPick(insist, kind)
if !ok {
return
}
@@ -164,6 +152,37 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
g.msg("%s", g.inventoryName(obj, false))
}
// whatisPick prompts for the item to identify, re-asking until a
// matching one is chosen when insist is set; ok is false when the
// player gives up (the prompt loop of wizard.c whatis).
func (g *RogueGame) whatisPick(insist bool, kind ObjectKind) (*Object, bool) {
for {
obj, _ := g.promptPackItem("identify", kind)
if !insist {
return obj, obj != nil
}
if g.NObjs == 0 {
return nil, false
}
if obj == nil {
g.msg("you must identify something")
continue
}
if !matchesFilter(kind, obj) {
g.msg("you must identify a %s", kind)
continue
}
return obj, true
}
}
// setKnow sets things up when we really know what a thing is (wizard.c
// set_know).
func setKnow(obj *Object, info []ObjInfo) {

View File

@@ -89,46 +89,86 @@ func (t *Tcell) ReadChar() byte {
t.Render(t.last)
}
case *tcell.EventKey:
switch ev.Key() {
case tcell.KeyUp:
return 'k'
case tcell.KeyDown:
return 'j'
case tcell.KeyLeft:
return 'h'
case tcell.KeyRight:
return 'l'
case tcell.KeyHome:
return 'y'
case tcell.KeyPgUp:
return 'u'
case tcell.KeyEnd:
return 'b'
case tcell.KeyPgDn:
return 'n'
case tcell.KeyEnter:
return '\n'
case tcell.KeyEscape:
return game.Escape
case tcell.KeyBackspace, tcell.KeyBackspace2:
return 8
case tcell.KeyDelete:
return 0x7f
case tcell.KeyTab:
return '\t'
case tcell.KeyCtrlC:
return 3
default:
if b, ok := translateKey(ev); ok {
return b
}
}
}
}
// translateKey converts a key event to a game input byte; ok is false
// for keys the C game does not understand.
func translateKey(ev *tcell.EventKey) (byte, bool) {
if b, ok := namedKey(ev.Key()); ok {
return b, true
}
if ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ {
return byte(ev.Key()) //nolint:gosec // G115: 1..26 fits
return byte(ev.Key()), true //nolint:gosec // G115: 1..26 fits
}
if r := ev.Rune(); r > 0 && r < 0x80 {
return byte(r)
return byte(r), true
}
return 0, false
}
// namedKey translates tcell's navigation and editing keys to the single
// bytes the C game reads (arrows become hjkl, etc.); ok is false for
// keys handled elsewhere.
func namedKey(k tcell.Key) (byte, bool) {
if b, ok := motionKey(k); ok {
return b, true
}
return editingKey(k)
}
// motionKey translates the arrow and paging keys to Rogue's movement
// letters (tcell.go ReadChar).
func motionKey(k tcell.Key) (byte, bool) {
switch k {
case tcell.KeyUp:
return 'k', true
case tcell.KeyDown:
return 'j', true
case tcell.KeyLeft:
return 'h', true
case tcell.KeyRight:
return 'l', true
case tcell.KeyHome:
return 'y', true
case tcell.KeyPgUp:
return 'u', true
case tcell.KeyEnd:
return 'b', true
case tcell.KeyPgDn:
return 'n', true
}
return 0, false
}
// editingKey translates the editing and control keys to their C0 codes
// (tcell.go ReadChar).
func editingKey(k tcell.Key) (byte, bool) {
switch k {
case tcell.KeyEnter:
return '\n', true
case tcell.KeyEscape:
return game.Escape, true
case tcell.KeyBackspace, tcell.KeyBackspace2:
return 8, true
case tcell.KeyDelete:
return 0x7f, true
case tcell.KeyTab:
return '\t', true
case tcell.KeyCtrlC:
return 3, true
}
return 0, false
}
// ShellEscape suspends the screen and runs the user's shell (main.c