Merge refactor/effects-dispatch (refactor step 7)
This commit is contained in:
13
MEMORY.md
13
MEMORY.md
@@ -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
40
TODO.md
@@ -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.
|
||||
|
||||
465
game/chase.go
465
game/chase.go
@@ -9,26 +9,7 @@ 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) {
|
||||
origPos := tp.Pos
|
||||
|
||||
wastarget := tp.On(Targeted)
|
||||
if removed := g.moveMonster(tp); removed {
|
||||
continue
|
||||
}
|
||||
|
||||
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
|
||||
if removed := g.moveMonster(tp); removed {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if wastarget && origPos != tp.Pos {
|
||||
tp.Flags.Clear(Targeted)
|
||||
|
||||
g.ToDeath = false
|
||||
}
|
||||
}
|
||||
g.runnerTurn(tp)
|
||||
}
|
||||
|
||||
if g.HasHit {
|
||||
@@ -37,6 +18,33 @@ func (g *RogueGame) runners(int) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
|
||||
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
|
||||
if removed := g.moveMonster(tp); removed {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if wastarget && origPos != tp.Pos {
|
||||
tp.Flags.Clear(Targeted)
|
||||
|
||||
g.ToDeath = false
|
||||
}
|
||||
}
|
||||
|
||||
// moveMonster executes a single turn of running for a monster (chase.c
|
||||
// move_monst). The result reports that the monster died or left the
|
||||
// level (the C -1 return).
|
||||
@@ -87,111 +95,33 @@ 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 := th.Room // find room of chaser
|
||||
if th.On(Greedy) && rer.GoldVal == 0 {
|
||||
th.Dest = &p.Pos // if gold has been taken, run after hero
|
||||
}
|
||||
rer, ree, door := g.chaseRooms(th)
|
||||
|
||||
var ree *Room // find room of chasee
|
||||
if th.Dest == &p.Pos {
|
||||
ree = p.Room
|
||||
} else {
|
||||
ree = g.roomIn(*th.Dest)
|
||||
}
|
||||
// We don't count doors as inside rooms for this routine
|
||||
door := g.Level.Char(th.Pos.Y, th.Pos.X) == Door
|
||||
|
||||
var this Coord
|
||||
|
||||
for {
|
||||
// If the object of our desire is in a different room, and we are
|
||||
// not in a corridor, run to the door nearest to our goal.
|
||||
if rer != ree {
|
||||
for i := range rer.Exits {
|
||||
curdist := distCp(*th.Dest, rer.Exits[i])
|
||||
if curdist < mindist {
|
||||
this = rer.Exits[i]
|
||||
mindist = curdist
|
||||
}
|
||||
}
|
||||
|
||||
if door {
|
||||
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum]
|
||||
door = false
|
||||
|
||||
continue // the C goto over: redo with the passage as room
|
||||
}
|
||||
} else {
|
||||
this = *th.Dest
|
||||
// For dragons check and see if (a) the hero is on a straight
|
||||
// line from it, and (b) that it is within shooting distance,
|
||||
// but outside of striking range.
|
||||
if th.Type == 'D' && (th.Pos.Y == p.Pos.Y || th.Pos.X == p.Pos.X ||
|
||||
abs(th.Pos.Y-p.Pos.Y) == abs(th.Pos.X-p.Pos.X)) &&
|
||||
distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
|
||||
!th.On(Cancelled) && g.rnd(dragonShot) == 0 {
|
||||
g.Delta.Y = sign(p.Pos.Y - th.Pos.Y)
|
||||
|
||||
g.Delta.X = sign(p.Pos.X - th.Pos.X)
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.fireBolt(th.Pos, &g.Delta, "flame")
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
|
||||
g.Quiet = 0
|
||||
if g.ToDeath && !th.On(Targeted) {
|
||||
g.ToDeath = false
|
||||
g.Kamikaze = false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
this, 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 this == p.Pos {
|
||||
return g.attack(th)
|
||||
} else if this == *th.Dest {
|
||||
for _, obj := range g.Level.Objects {
|
||||
if th.Dest == &obj.Pos {
|
||||
g.Level.RemoveObject(obj)
|
||||
attachObj(&th.Pack, obj)
|
||||
|
||||
if th.Room.Flags.Has(Gone) {
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor)
|
||||
}
|
||||
|
||||
th.Dest = g.findDest(th)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if th.Type != 'F' {
|
||||
stoprun = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if 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)
|
||||
@@ -203,14 +133,129 @@ func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
|
||||
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 {
|
||||
th.Dest = &p.Pos // if gold has been taken, run after hero
|
||||
}
|
||||
|
||||
var ree *Room // find room of chasee
|
||||
if th.Dest == &p.Pos {
|
||||
ree = p.Room
|
||||
} else {
|
||||
ree = g.roomIn(*th.Dest)
|
||||
}
|
||||
|
||||
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 {
|
||||
this = *th.Dest
|
||||
|
||||
return this, g.dragonBreath(th)
|
||||
}
|
||||
|
||||
for i := range rer.Exits {
|
||||
curdist := distCp(*th.Dest, rer.Exits[i])
|
||||
if curdist < mindist {
|
||||
this = rer.Exits[i]
|
||||
mindist = curdist
|
||||
}
|
||||
}
|
||||
|
||||
if !door {
|
||||
return this, false
|
||||
}
|
||||
|
||||
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum]
|
||||
door = false
|
||||
// the C goto over: redo with the passage as room
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.fireBolt(th.Pos, &g.Delta, "flame")
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
|
||||
g.Quiet = 0
|
||||
if g.ToDeath && !th.On(Targeted) {
|
||||
g.ToDeath = false
|
||||
g.Kamikaze = false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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)
|
||||
attachObj(&th.Pack, obj)
|
||||
|
||||
if th.Room.Flags.Has(Gone) {
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor)
|
||||
}
|
||||
|
||||
th.Dest = g.findDest(th)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,77 +271,98 @@ 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)
|
||||
g.chRet = er
|
||||
curdist = g.chaseBestSpot(tp, ee)
|
||||
}
|
||||
|
||||
ey := er.Y + 1
|
||||
if ey >= NumLines-1 {
|
||||
ey = NumLines - 2
|
||||
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
|
||||
if ey >= NumLines-1 {
|
||||
ey = NumLines - 2
|
||||
}
|
||||
|
||||
ex := er.X + 1
|
||||
if ex >= NumCols {
|
||||
ex = NumCols - 1
|
||||
}
|
||||
|
||||
for x := er.X - 1; x <= ex; x++ {
|
||||
if x < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ex := er.X + 1
|
||||
if ex >= NumCols {
|
||||
ex = NumCols - 1
|
||||
}
|
||||
|
||||
for x := er.X - 1; x <= ex; x++ {
|
||||
if x < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for y := er.Y - 1; y <= ey; y++ {
|
||||
tryp := Coord{X: x, Y: y}
|
||||
if !g.diagOk(er, tryp) {
|
||||
continue
|
||||
}
|
||||
|
||||
ch := g.Level.VisibleChar(y, x)
|
||||
if stepOk(ch) {
|
||||
// If it is a scroll, it might be a scare monster
|
||||
// scroll so we need to look it up to see what type
|
||||
// it is.
|
||||
if ch == Scroll {
|
||||
var found *Object
|
||||
|
||||
for _, obj := range g.Level.Objects {
|
||||
if y == obj.Pos.Y && x == obj.Pos.X {
|
||||
found = obj
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found != nil && found.ScrollKind() == ScrollScareMonster {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// It can also be a Xeroc, which we shouldn't step on
|
||||
if m := g.Level.MonsterAt(y, x); m != nil && m.Type == 'X' {
|
||||
continue
|
||||
}
|
||||
// If we didn't find any scrolls at this place or it
|
||||
// wasn't a scare scroll, then this place counts
|
||||
thisdist := distance(y, x, ee.Y, ee.X)
|
||||
if thisdist < curdist {
|
||||
plcnt = 1
|
||||
g.chRet = tryp
|
||||
curdist = thisdist
|
||||
} else if thisdist == curdist {
|
||||
if plcnt++; g.rnd(plcnt) == 0 {
|
||||
g.chRet = tryp
|
||||
curdist = thisdist
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for y := er.Y - 1; y <= ey; y++ {
|
||||
g.chaseTry(&s, y, x)
|
||||
}
|
||||
}
|
||||
|
||||
return curdist != 0 && g.chRet != p.Pos
|
||||
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(s.er, tryp) {
|
||||
return
|
||||
}
|
||||
|
||||
ch := g.Level.VisibleChar(y, x)
|
||||
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' {
|
||||
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, s.ee.Y, s.ee.X)
|
||||
if thisdist < s.curdist {
|
||||
s.plcnt = 1
|
||||
g.chRet = tryp
|
||||
s.curdist = thisdist
|
||||
} else if thisdist == s.curdist {
|
||||
if s.plcnt++; g.rnd(s.plcnt) == 0 {
|
||||
g.chRet = tryp
|
||||
s.curdist = thisdist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return &obj.Pos
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
1114
game/command.go
1114
game/command.go
File diff suppressed because it is too large
Load Diff
145
game/daemons.go
145
game/daemons.go
@@ -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,50 +117,9 @@ func (g *RogueGame) stomach(int) {
|
||||
|
||||
origHungry := p.HungryState
|
||||
if p.FoodLeft <= 0 {
|
||||
if p.FoodLeft--; p.FoodLeft < -StarveTime {
|
||||
g.death('s')
|
||||
}
|
||||
// the hero is fainting
|
||||
if g.NoCommand != 0 || g.rnd(5) != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
g.NoCommand += g.rnd(8) + 4
|
||||
p.HungryState = 3
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("%s", g.chooseStr(
|
||||
"the munchies overpower your motor capabilities. ",
|
||||
"you feel too weak from lack of food. "))
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr("You freak out", "You faint"))
|
||||
g.stomachFaint()
|
||||
} else {
|
||||
oldfood := p.FoodLeft
|
||||
|
||||
amulet := 0
|
||||
if g.HasAmulet {
|
||||
amulet = 1
|
||||
}
|
||||
|
||||
p.FoodLeft -= g.ringEat(Left) + g.ringEat(Right) + 1 - amulet
|
||||
|
||||
if p.FoodLeft < MoreTime && oldfood >= MoreTime {
|
||||
p.HungryState = 2
|
||||
|
||||
g.msg("%s", g.chooseStr(
|
||||
"the munchies are interfering with your motor capabilities",
|
||||
"you are starting to feel weak"))
|
||||
} else if p.FoodLeft < 2*MoreTime && oldfood >= 2*MoreTime {
|
||||
p.HungryState = 1
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("%s", g.chooseStr("getting the munchies", "getting hungry"))
|
||||
} else {
|
||||
g.msg("%s", g.chooseStr("you are getting the munchies",
|
||||
"you are starting to get hungry"))
|
||||
}
|
||||
}
|
||||
g.stomachDigest()
|
||||
}
|
||||
|
||||
if p.HungryState != origHungry {
|
||||
@@ -196,6 +131,61 @@ func (g *RogueGame) stomach(int) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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')
|
||||
}
|
||||
// the hero is fainting
|
||||
if g.NoCommand != 0 || g.rnd(5) != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
g.NoCommand += g.rnd(8) + 4
|
||||
p.HungryState = 3
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("%s", g.chooseStr(
|
||||
"the munchies overpower your motor capabilities. ",
|
||||
"you feel too weak from lack of food. "))
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr("You freak out", "You faint"))
|
||||
}
|
||||
|
||||
// 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
|
||||
if g.HasAmulet {
|
||||
amulet = 1
|
||||
}
|
||||
|
||||
p.FoodLeft -= g.ringEat(Left) + g.ringEat(Right) + 1 - amulet
|
||||
|
||||
if p.FoodLeft < MoreTime && oldfood >= MoreTime {
|
||||
p.HungryState = 2
|
||||
|
||||
g.msg("%s", g.chooseStr(
|
||||
"the munchies are interfering with your motor capabilities",
|
||||
"you are starting to feel weak"))
|
||||
} else if p.FoodLeft < 2*MoreTime && oldfood >= 2*MoreTime {
|
||||
p.HungryState = 1
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("%s", g.chooseStr("getting the munchies", "getting hungry"))
|
||||
} else {
|
||||
g.msg("%s", g.chooseStr("you are getting the munchies",
|
||||
"you are starting to get hungry"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
691
game/fight.go
691
game/fight.go
@@ -48,66 +48,81 @@ 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) {
|
||||
tp.Disguise = 'X'
|
||||
if p.On(Hallucinating) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.randomMonsterLetter())
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
|
||||
"wait! That's a xeroc!"))
|
||||
|
||||
if !thrown {
|
||||
return false
|
||||
}
|
||||
if g.revealXeroc(tp) && !thrown {
|
||||
return false
|
||||
}
|
||||
|
||||
mname := g.setMname(tp)
|
||||
didHit := false
|
||||
|
||||
g.HasHit = g.Options.Terse && !g.ToDeath
|
||||
if g.rollAttacks(&p.Creature, &tp.Creature, weap, thrown) {
|
||||
didHit = false
|
||||
g.heroHits(tp, mname, weap, thrown)
|
||||
|
||||
if thrown {
|
||||
g.thunk(weap, mname, g.Options.Terse)
|
||||
} else {
|
||||
g.hit("", mname, g.Options.Terse)
|
||||
}
|
||||
|
||||
if p.On(CanConfuse) {
|
||||
didHit = true
|
||||
|
||||
tp.Flags.Set(Confused)
|
||||
p.Flags.Clear(CanConfuse)
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
g.msg("your hands stop glowing %s", g.pickColor("red"))
|
||||
}
|
||||
|
||||
if tp.Stats.HP <= 0 {
|
||||
g.killed(tp, true)
|
||||
} else if didHit && !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 true
|
||||
}
|
||||
|
||||
return didHit
|
||||
if thrown {
|
||||
g.bounce(weap, mname, g.Options.Terse)
|
||||
} else {
|
||||
g.miss("", mname, g.Options.Terse)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// attack has the monster attack the player (fight.c attack). removed
|
||||
// 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())
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
|
||||
"wait! That's a xeroc!"))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
g.hit("", mname, g.Options.Terse)
|
||||
}
|
||||
|
||||
if p.On(CanConfuse) {
|
||||
confused = true
|
||||
|
||||
tp.Flags.Set(Confused)
|
||||
p.Flags.Clear(CanConfuse)
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
g.msg("your hands stop glowing %s", g.pickColor("red"))
|
||||
}
|
||||
|
||||
if tp.Stats.HP <= 0 {
|
||||
g.killed(tp, true)
|
||||
} else if confused && !p.On(Blind) {
|
||||
g.msg("%s appears confused", mname)
|
||||
}
|
||||
}
|
||||
|
||||
// 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,185 +144,12 @@ 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) {
|
||||
if mp.Type != 'I' {
|
||||
if g.HasHit {
|
||||
g.addmsgf(". ")
|
||||
}
|
||||
|
||||
g.hit(mname, "", false)
|
||||
} else if g.HasHit {
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.HasHit = false
|
||||
if p.Stats.HP <= 0 {
|
||||
g.death(mp.Type) // Bye bye life ...
|
||||
} else if !g.Kamikaze {
|
||||
oldhp -= p.Stats.HP
|
||||
if oldhp > g.MaxHit {
|
||||
g.MaxHit = oldhp
|
||||
}
|
||||
|
||||
if p.Stats.HP <= g.MaxHit {
|
||||
g.ToDeath = false
|
||||
}
|
||||
}
|
||||
|
||||
if !mp.On(Cancelled) {
|
||||
switch mp.Type {
|
||||
case 'A':
|
||||
// If an aquator hits, you can lose armor class.
|
||||
g.rustArmor(p.CurArmor)
|
||||
case 'I':
|
||||
// The ice monster freezes you
|
||||
p.Flags.Clear(Awake)
|
||||
|
||||
if g.NoCommand == 0 {
|
||||
g.addmsgf("you are frozen")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" by the %s", mname)
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.NoCommand += g.rnd(2) + 2
|
||||
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")
|
||||
}
|
||||
} else if !g.ToDeath {
|
||||
if !g.Options.Terse {
|
||||
g.msg("a bite momentarily weakens you")
|
||||
} else {
|
||||
g.msg("bite has no effect")
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'W', 'V':
|
||||
// Wraiths might drain energy levels, and Vampires can
|
||||
// steal max_hp
|
||||
chance := 30
|
||||
if mp.Type == 'W' {
|
||||
chance = 15
|
||||
}
|
||||
|
||||
if g.rnd(100) < chance {
|
||||
var fewer int
|
||||
|
||||
if mp.Type == 'W' {
|
||||
if p.Stats.Exp == 0 {
|
||||
g.death('W') // All levels gone
|
||||
}
|
||||
|
||||
if p.Stats.Lvl--; p.Stats.Lvl == 0 {
|
||||
p.Stats.Exp = 0
|
||||
p.Stats.Lvl = 1
|
||||
} else {
|
||||
p.Stats.Exp = g.data.eLevels[p.Stats.Lvl-1] + 1
|
||||
}
|
||||
|
||||
fewer = g.roll(1, 10)
|
||||
} else {
|
||||
fewer = g.roll(1, 3)
|
||||
}
|
||||
|
||||
p.Stats.HP -= fewer
|
||||
|
||||
p.Stats.MaxHP -= fewer
|
||||
if p.Stats.HP <= 0 {
|
||||
p.Stats.HP = 1
|
||||
}
|
||||
|
||||
if p.Stats.MaxHP <= 0 {
|
||||
g.death(mp.Type)
|
||||
}
|
||||
|
||||
g.msg("you suddenly feel weaker")
|
||||
}
|
||||
case 'F':
|
||||
// Venus Flytrap stops the poor guy from moving
|
||||
p.Flags.Set(Held)
|
||||
p.VfHit++
|
||||
|
||||
g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}}
|
||||
if p.Stats.HP--; p.Stats.HP <= 0 {
|
||||
g.death('F')
|
||||
}
|
||||
case 'L':
|
||||
// Leprechaun steals some gold
|
||||
lastpurse := p.Purse
|
||||
|
||||
p.Purse -= g.goldCalc()
|
||||
if !g.save(VsMagic) {
|
||||
p.Purse -= g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
|
||||
}
|
||||
|
||||
if p.Purse < 0 {
|
||||
p.Purse = 0
|
||||
}
|
||||
|
||||
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.
|
||||
var steal *Object
|
||||
|
||||
nobj := 0
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
if obj != p.CurArmor && obj != p.CurWeapon &&
|
||||
obj != p.CurRing[Left] && obj != p.CurRing[Right] &&
|
||||
g.isMagic(obj) {
|
||||
if nobj++; g.rnd(nobj) == 0 {
|
||||
steal = obj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if steal != nil {
|
||||
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)
|
||||
removed = g.monsterHit(mp, mname, oldhp)
|
||||
} else {
|
||||
g.monsterMiss(mp, mname)
|
||||
}
|
||||
|
||||
if g.Options.FightFlush && !g.ToDeath {
|
||||
@@ -320,6 +162,235 @@ func (g *RogueGame) attack(mp *Monster) (removed bool) {
|
||||
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(". ")
|
||||
}
|
||||
|
||||
g.hit(mname, "", false)
|
||||
} else if g.HasHit {
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.HasHit = false
|
||||
if p.Stats.HP <= 0 {
|
||||
g.death(mp.Type) // Bye bye life ...
|
||||
} else if !g.Kamikaze {
|
||||
oldhp -= p.Stats.HP
|
||||
if oldhp > g.MaxHit {
|
||||
g.MaxHit = oldhp
|
||||
}
|
||||
|
||||
if p.Stats.HP <= g.MaxHit {
|
||||
g.ToDeath = false
|
||||
}
|
||||
}
|
||||
|
||||
if !mp.On(Cancelled) {
|
||||
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(g.Player.CurArmor)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *RogueGame) hitIceMonster(_ *Monster, mname string) bool {
|
||||
// The ice monster freezes you
|
||||
g.Player.Flags.Clear(Awake)
|
||||
|
||||
if g.NoCommand == 0 {
|
||||
g.addmsgf("you are frozen")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" by the %s", mname)
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.NoCommand += g.rnd(2) + 2
|
||||
if g.NoCommand > BoreLevel {
|
||||
g.death('h')
|
||||
}
|
||||
|
||||
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 {
|
||||
g.msg("%s", g.chooseTerse("bite has no effect",
|
||||
"a bite momentarily weakens you"))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
|
||||
var fewer int
|
||||
|
||||
if mp.Type == 'W' {
|
||||
if p.Stats.Exp == 0 {
|
||||
g.death('W') // All levels gone
|
||||
}
|
||||
|
||||
if p.Stats.Lvl--; p.Stats.Lvl == 0 {
|
||||
p.Stats.Exp = 0
|
||||
p.Stats.Lvl = 1
|
||||
} else {
|
||||
p.Stats.Exp = g.data.eLevels[p.Stats.Lvl-1] + 1
|
||||
}
|
||||
|
||||
fewer = g.roll(1, 10)
|
||||
} else {
|
||||
fewer = g.roll(1, 3)
|
||||
}
|
||||
|
||||
p.Stats.HP -= fewer
|
||||
|
||||
p.Stats.MaxHP -= fewer
|
||||
if p.Stats.HP <= 0 {
|
||||
p.Stats.HP = 1
|
||||
}
|
||||
|
||||
if p.Stats.MaxHP <= 0 {
|
||||
g.death(mp.Type)
|
||||
}
|
||||
|
||||
g.msg("you suddenly feel weaker")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *RogueGame) hitFlytrap(*Monster, string) bool {
|
||||
// Venus Flytrap stops the poor guy from moving
|
||||
p := &g.Player
|
||||
p.Flags.Set(Held)
|
||||
p.VfHit++
|
||||
|
||||
g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}}
|
||||
if p.Stats.HP--; p.Stats.HP <= 0 {
|
||||
g.death('F')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *RogueGame) hitLeprechaun(mp *Monster, _ string) bool {
|
||||
// Leprechaun steals some gold
|
||||
p := &g.Player
|
||||
lastpurse := p.Purse
|
||||
|
||||
p.Purse -= g.goldCalc()
|
||||
if !g.save(VsMagic) {
|
||||
p.Purse -= g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
|
||||
}
|
||||
|
||||
if p.Purse < 0 {
|
||||
p.Purse = 0
|
||||
}
|
||||
|
||||
g.removeMon(mp.Pos, mp, false)
|
||||
|
||||
if p.Purse != lastpurse {
|
||||
g.msg("your purse feels lighter")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
if obj != p.CurArmor && obj != p.CurWeapon &&
|
||||
obj != p.CurRing[Left] && obj != p.CurRing[Right] &&
|
||||
g.isMagic(obj) {
|
||||
if nobj++; g.rnd(nobj) == 0 {
|
||||
steal = obj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if steal == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false)
|
||||
g.leavePack(steal, false, false)
|
||||
g.msg("she stole %s!", g.inventoryName(steal, true))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// swing returns true if the swing hits (fight.c swing).
|
||||
func (g *RogueGame) swing(atLvl, opArm, wplus int) bool {
|
||||
res := g.rnd(20)
|
||||
@@ -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() {
|
||||
|
||||
71
game/io.go
71
game/io.go
@@ -58,29 +58,8 @@ 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
|
||||
|
||||
return Escape
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.Mpos != 0 && m.promptMore() == Escape {
|
||||
return Escape
|
||||
}
|
||||
// All messages should start with uppercase, except ones that start
|
||||
// with a pack addressing character
|
||||
@@ -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' {
|
||||
|
||||
436
game/misc.go
436
game/misc.go
@@ -4,11 +4,23 @@ package game
|
||||
// and small utilities. call_it arrives with the scroll/potion phase (it
|
||||
// needs the get_str line editor).
|
||||
|
||||
// lookScan carries the state of one look() glance while it examines the
|
||||
// nine squares around the hero.
|
||||
type lookScan struct {
|
||||
hero Coord
|
||||
pch byte // map character under the hero
|
||||
pfl PlaceFlags // map flags under the hero
|
||||
wakeup bool
|
||||
doorStop bool // door-stop checking applies (mid-run)
|
||||
sy, sx, ey, ex int
|
||||
sumhero, diffhero int
|
||||
passcount int
|
||||
}
|
||||
|
||||
// look takes a quick glance all around the player (misc.c look).
|
||||
func (g *RogueGame) look(wakeup bool) {
|
||||
p := &g.Player
|
||||
hero := p.Pos
|
||||
passcount := 0
|
||||
rp := p.Room
|
||||
|
||||
if g.Oldpos != hero {
|
||||
@@ -17,151 +29,28 @@ func (g *RogueGame) look(wakeup bool) {
|
||||
g.Oldrp = rp
|
||||
}
|
||||
|
||||
ey := hero.Y + 1
|
||||
ex := hero.X + 1
|
||||
sx := hero.X - 1
|
||||
sy := hero.Y - 1
|
||||
s := lookScan{
|
||||
hero: hero,
|
||||
wakeup: wakeup,
|
||||
sy: hero.Y - 1,
|
||||
sx: hero.X - 1,
|
||||
ey: hero.Y + 1,
|
||||
ex: hero.X + 1,
|
||||
}
|
||||
|
||||
sumhero, diffhero := 0, 0
|
||||
if g.DoorStop && !g.Firstmove && g.Running {
|
||||
sumhero = hero.Y + hero.X
|
||||
diffhero = hero.Y - hero.X
|
||||
s.doorStop = g.DoorStop && !g.Firstmove
|
||||
if s.doorStop && g.Running {
|
||||
s.sumhero = hero.Y + hero.X
|
||||
s.diffhero = hero.Y - hero.X
|
||||
}
|
||||
|
||||
pp := g.Level.At(hero.Y, hero.X)
|
||||
pch := pp.Ch
|
||||
pfl := pp.Flags
|
||||
s.pch = pp.Ch
|
||||
s.pfl = pp.Flags
|
||||
|
||||
for y := sy; y <= ey; y++ {
|
||||
if y <= 0 || y >= NumLines-1 {
|
||||
continue
|
||||
}
|
||||
g.lookAround(&s)
|
||||
|
||||
for x := sx; x <= ex; x++ {
|
||||
if x < 0 || x >= NumCols {
|
||||
continue
|
||||
}
|
||||
|
||||
if !p.On(Blind) {
|
||||
if y == hero.Y && x == hero.X {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
pp := g.Level.At(y, x)
|
||||
|
||||
ch := pp.Ch
|
||||
if ch == ' ' { // nothing need be done with a ' '
|
||||
continue
|
||||
}
|
||||
|
||||
fp := &pp.Flags
|
||||
if pch != Door && ch != Door {
|
||||
if (pfl & FPassage) != (*fp & FPassage) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (fp.Has(FPassage) || ch == Door) && (pfl.Has(FPassage) || pch == Door) {
|
||||
if hero.X != x && hero.Y != y &&
|
||||
!stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
tp := pp.Monst
|
||||
|
||||
switch {
|
||||
case tp == nil:
|
||||
ch = g.tripCh(y, x, ch)
|
||||
case p.On(SenseMonsters) && tp.On(Invisible):
|
||||
if g.DoorStop && !g.Firstmove {
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
continue
|
||||
default:
|
||||
if wakeup {
|
||||
g.wakeMonster(y, x)
|
||||
}
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
if p.On(Hallucinating) {
|
||||
ch = g.randomMonsterLetter()
|
||||
} else {
|
||||
ch = tp.Disguise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.On(Blind) && (y != hero.Y || x != hero.X) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.move(y, x)
|
||||
|
||||
if p.Room.Flags.Has(Dark) && !g.Options.SeeFloor && ch == Floor {
|
||||
ch = ' '
|
||||
}
|
||||
|
||||
if tp != nil || ch != g.inch() {
|
||||
g.addch(ch)
|
||||
}
|
||||
|
||||
if g.DoorStop && !g.Firstmove && g.Running {
|
||||
switch g.RunCh {
|
||||
case 'h':
|
||||
if x == ex {
|
||||
continue
|
||||
}
|
||||
case 'j':
|
||||
if y == sy {
|
||||
continue
|
||||
}
|
||||
case 'k':
|
||||
if y == ey {
|
||||
continue
|
||||
}
|
||||
case 'l':
|
||||
if x == sx {
|
||||
continue
|
||||
}
|
||||
case 'y':
|
||||
if (y+x)-sumhero >= 1 {
|
||||
continue
|
||||
}
|
||||
case 'u':
|
||||
if (y-x)-diffhero >= 1 {
|
||||
continue
|
||||
}
|
||||
case 'n':
|
||||
if (y+x)-sumhero <= -1 {
|
||||
continue
|
||||
}
|
||||
case 'b':
|
||||
if (y-x)-diffhero <= -1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case Door:
|
||||
if x == hero.X || y == hero.Y {
|
||||
g.Running = false
|
||||
}
|
||||
case Passage:
|
||||
if x == hero.X || y == hero.Y {
|
||||
passcount++
|
||||
}
|
||||
case Floor, '|', '-', ' ':
|
||||
default:
|
||||
g.Running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.DoorStop && !g.Firstmove && passcount > 1 {
|
||||
if s.doorStop && s.passcount > 1 {
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
@@ -170,6 +59,192 @@ func (g *RogueGame) look(wakeup bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// lookAround runs the nine-square scan of look().
|
||||
func (g *RogueGame) lookAround(s *lookScan) {
|
||||
for y := s.sy; y <= s.ey; y++ {
|
||||
if y <= 0 || y >= NumLines-1 {
|
||||
continue
|
||||
}
|
||||
|
||||
for x := s.sx; x <= s.ex; x++ {
|
||||
if x < 0 || x >= NumCols {
|
||||
continue
|
||||
}
|
||||
|
||||
g.lookCell(s, y, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lookCell examines one square around the hero: visibility rules, trip
|
||||
// and monster rendering, drawing, and run-stop checks (the loop body of
|
||||
// misc.c look).
|
||||
func (g *RogueGame) lookCell(s *lookScan, y, x int) {
|
||||
pp := g.Level.At(y, x)
|
||||
if g.lookSkips(s, pp, y, x) {
|
||||
return
|
||||
}
|
||||
|
||||
tp := pp.Monst
|
||||
|
||||
ch, skip := g.lookCellChar(s, tp, y, x, pp.Ch)
|
||||
if skip {
|
||||
return
|
||||
}
|
||||
|
||||
if !g.lookShow(s, tp, ch, y, x) {
|
||||
return
|
||||
}
|
||||
|
||||
if s.doorStop && g.Running {
|
||||
g.lookRunCheck(s, ch, y, x)
|
||||
}
|
||||
}
|
||||
|
||||
// lookSkips reports whether look ignores this square entirely: the
|
||||
// hero's own square when sighted, blank rock, passage squares of
|
||||
// another network, and diagonals the hero could not step to (the guard
|
||||
// chain of the misc.c look loop).
|
||||
func (g *RogueGame) lookSkips(s *lookScan, pp *Place, y, x int) bool {
|
||||
if !g.Player.On(Blind) && y == s.hero.Y && x == s.hero.X {
|
||||
return true
|
||||
}
|
||||
|
||||
if pp.Ch == ' ' { // nothing need be done with a ' '
|
||||
return true
|
||||
}
|
||||
|
||||
return lookForeignPassage(s, pp.Flags, pp.Ch) ||
|
||||
g.lookDiagonalBlocked(s, pp.Flags, pp.Ch, y, x)
|
||||
}
|
||||
|
||||
// lookForeignPassage hides passage squares belonging to a different
|
||||
// passage network than the hero's (misc.c look).
|
||||
func lookForeignPassage(s *lookScan, fp PlaceFlags, ch byte) bool {
|
||||
if s.pch != Door && ch != Door {
|
||||
return (s.pfl & FPassage) != (fp & FPassage)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// lookDiagonalBlocked hides diagonal door/passage squares the hero could
|
||||
// not actually step to (misc.c look).
|
||||
func (g *RogueGame) lookDiagonalBlocked(s *lookScan, fp PlaceFlags, ch byte, y, x int) bool {
|
||||
if !fp.Has(FPassage) && ch != Door {
|
||||
return false
|
||||
}
|
||||
|
||||
if !s.pfl.Has(FPassage) && s.pch != Door {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.hero.X != x && s.hero.Y != y &&
|
||||
!stepOk(g.Level.Char(y, s.hero.X)) && !stepOk(g.Level.Char(s.hero.Y, x))
|
||||
}
|
||||
|
||||
// lookShow draws the square if it changed; it reports false when a
|
||||
// blind hero cannot see it at all (the draw part of the look loop).
|
||||
func (g *RogueGame) lookShow(s *lookScan, tp *Monster, ch byte, y, x int) bool {
|
||||
p := &g.Player
|
||||
if p.On(Blind) && (y != s.hero.Y || x != s.hero.X) {
|
||||
return false
|
||||
}
|
||||
|
||||
g.move(y, x)
|
||||
|
||||
if p.Room.Flags.Has(Dark) && !g.Options.SeeFloor && ch == Floor {
|
||||
ch = ' '
|
||||
}
|
||||
|
||||
if tp != nil || ch != g.inch() {
|
||||
g.addch(ch)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// lookCellChar picks what the square shows: trip rendering for empty
|
||||
// squares, waking and disguises for monsters. skip means the square is
|
||||
// not drawn at all (the monster switch of the look loop).
|
||||
func (g *RogueGame) lookCellChar(s *lookScan, tp *Monster, y, x int, ch byte) (byte, bool) {
|
||||
p := &g.Player
|
||||
|
||||
switch {
|
||||
case tp == nil:
|
||||
return g.tripCh(y, x, ch), false
|
||||
case p.On(SenseMonsters) && tp.On(Invisible):
|
||||
if g.DoorStop && !g.Firstmove {
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
return ch, true
|
||||
default:
|
||||
if s.wakeup {
|
||||
g.wakeMonster(y, x)
|
||||
}
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
if p.On(Hallucinating) {
|
||||
return g.randomMonsterLetter(), false
|
||||
}
|
||||
|
||||
return tp.Disguise, false
|
||||
}
|
||||
|
||||
return ch, false
|
||||
}
|
||||
}
|
||||
|
||||
// lookRunCheck decides whether what this square shows should stop a run
|
||||
// (the DoorStop tail of the misc.c look loop). Squares on the running
|
||||
// edge are ignored.
|
||||
func (g *RogueGame) lookRunCheck(s *lookScan, ch byte, y, x int) {
|
||||
if s.atRunEdge(g.RunCh, y, x) {
|
||||
return
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case Door:
|
||||
if x == s.hero.X || y == s.hero.Y {
|
||||
g.Running = false
|
||||
}
|
||||
case Passage:
|
||||
if x == s.hero.X || y == s.hero.Y {
|
||||
s.passcount++
|
||||
}
|
||||
case Floor, '|', '-', ' ':
|
||||
default:
|
||||
g.Running = false
|
||||
}
|
||||
}
|
||||
|
||||
// atRunEdge reports whether (y, x) sits on the leading edge of the run
|
||||
// direction, where door-stop checking does not apply (the first RunCh
|
||||
// switch of the misc.c look loop).
|
||||
func (s *lookScan) atRunEdge(runCh byte, y, x int) bool {
|
||||
switch runCh {
|
||||
case 'h':
|
||||
return x == s.ex
|
||||
case 'j':
|
||||
return y == s.sy
|
||||
case 'k':
|
||||
return y == s.ey
|
||||
case 'l':
|
||||
return x == s.sx
|
||||
case 'y':
|
||||
return (y+x)-s.sumhero >= 1
|
||||
case 'u':
|
||||
return (y-x)-s.diffhero >= 1
|
||||
case 'n':
|
||||
return (y+x)-s.sumhero <= -1
|
||||
case 'b':
|
||||
return (y-x)-s.diffhero <= -1
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// tripCh returns the character for this space, taking into account whether
|
||||
// or not the player is tripping (misc.c trip_ch).
|
||||
func (g *RogueGame) tripCh(y, x int, ch byte) byte {
|
||||
@@ -409,40 +484,22 @@ func (g *RogueGame) promptDirection() bool {
|
||||
}
|
||||
|
||||
for {
|
||||
gotit := true
|
||||
|
||||
switch g.DirCh = g.readchar(); g.DirCh {
|
||||
case 'h', 'H':
|
||||
g.Delta = Coord{X: -1, Y: 0}
|
||||
case 'j', 'J':
|
||||
g.Delta = Coord{X: 0, Y: 1}
|
||||
case 'k', 'K':
|
||||
g.Delta = Coord{X: 0, Y: -1}
|
||||
case 'l', 'L':
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
case 'y', 'Y':
|
||||
g.Delta = Coord{X: -1, Y: -1}
|
||||
case 'u', 'U':
|
||||
g.Delta = Coord{X: 1, Y: -1}
|
||||
case 'b', 'B':
|
||||
g.Delta = Coord{X: -1, Y: 1}
|
||||
case 'n', 'N':
|
||||
g.Delta = Coord{X: 1, Y: 1}
|
||||
case Escape:
|
||||
g.DirCh = g.readchar()
|
||||
if g.DirCh == Escape {
|
||||
g.LastDir = 0
|
||||
g.resetLast()
|
||||
|
||||
return false
|
||||
default:
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("%s", prompt)
|
||||
|
||||
gotit = false
|
||||
}
|
||||
|
||||
if gotit {
|
||||
if d, ok := deltaFor(g.DirCh); ok {
|
||||
g.Delta = d
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("%s", prompt)
|
||||
}
|
||||
|
||||
g.DirCh = toLower(g.DirCh)
|
||||
@@ -451,14 +508,7 @@ func (g *RogueGame) promptDirection() bool {
|
||||
}
|
||||
|
||||
if g.Player.On(Confused) && g.rnd(5) == 0 {
|
||||
for {
|
||||
g.Delta.Y = g.rnd(3) - 1
|
||||
|
||||
g.Delta.X = g.rnd(3) - 1
|
||||
if g.Delta.Y != 0 || g.Delta.X != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
g.confuseDirection()
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
@@ -466,6 +516,44 @@ func (g *RogueGame) promptDirection() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// confuseDirection randomizes the chosen direction for a confused hero
|
||||
// (the ISHUH tail of misc.c get_dir).
|
||||
func (g *RogueGame) confuseDirection() {
|
||||
for {
|
||||
g.Delta.Y = g.rnd(3) - 1
|
||||
|
||||
g.Delta.X = g.rnd(3) - 1
|
||||
if g.Delta.Y != 0 || g.Delta.X != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deltaFor maps a direction key to its movement delta; ok is false for
|
||||
// keys that are not directions (the switch of misc.c get_dir).
|
||||
func deltaFor(ch byte) (Coord, bool) {
|
||||
switch ch {
|
||||
case 'h', 'H':
|
||||
return Coord{X: -1, Y: 0}, true
|
||||
case 'j', 'J':
|
||||
return Coord{X: 0, Y: 1}, true
|
||||
case 'k', 'K':
|
||||
return Coord{X: 0, Y: -1}, true
|
||||
case 'l', 'L':
|
||||
return Coord{X: 1, Y: 0}, true
|
||||
case 'y', 'Y':
|
||||
return Coord{X: -1, Y: -1}, true
|
||||
case 'u', 'U':
|
||||
return Coord{X: 1, Y: -1}, true
|
||||
case 'b', 'B':
|
||||
return Coord{X: -1, Y: 1}, true
|
||||
case 'n', 'N':
|
||||
return Coord{X: 1, Y: 1}, true
|
||||
}
|
||||
|
||||
return Coord{}, false
|
||||
}
|
||||
|
||||
// callIt calls an object something after use (misc.c call_it).
|
||||
func (g *RogueGame) callIt(info *ObjInfo) {
|
||||
if info.Know {
|
||||
|
||||
@@ -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,40 +124,14 @@ 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) {
|
||||
rp := p.Room
|
||||
if (rp != nil && !rp.Flags.Has(Dark)) ||
|
||||
distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
||||
tp.Flags.Set(Found)
|
||||
|
||||
if !g.save(VsMagic) {
|
||||
if p.On(Confused) {
|
||||
g.Lengthen(DUnconfuse, g.spread(HuhDuration))
|
||||
} else {
|
||||
g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After)
|
||||
}
|
||||
|
||||
p.Flags.Set(Confused)
|
||||
|
||||
mname := g.setMname(tp)
|
||||
g.addmsgf("%s", mname)
|
||||
|
||||
if mname != "it" {
|
||||
g.addmsgf("'")
|
||||
}
|
||||
|
||||
g.msg("s gaze has confused you")
|
||||
}
|
||||
}
|
||||
if g.medusaCatches(tp) {
|
||||
g.medusaGaze(tp, y, x)
|
||||
}
|
||||
// Let greedy ones guard gold
|
||||
if tp.On(Greedy) && !tp.On(Awake) {
|
||||
@@ -169,8 +143,60 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
|
||||
tp.Dest = &p.Pos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tp
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
|
||||
tp.Flags.Set(Found)
|
||||
|
||||
if g.save(VsMagic) {
|
||||
return
|
||||
}
|
||||
|
||||
if p.On(Confused) {
|
||||
g.Lengthen(DUnconfuse, g.spread(HuhDuration))
|
||||
} else {
|
||||
g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After)
|
||||
}
|
||||
|
||||
p.Flags.Set(Confused)
|
||||
|
||||
mname := g.setMname(tp)
|
||||
g.addmsgf("%s", mname)
|
||||
|
||||
if mname != "it" {
|
||||
g.addmsgf("'")
|
||||
}
|
||||
|
||||
g.msg("s gaze has confused you")
|
||||
}
|
||||
|
||||
// givePack gives a pack to a monster if it deserves one (monsters.c
|
||||
|
||||
488
game/move.go
488
game/move.go
@@ -38,108 +38,145 @@ func (g *RogueGame) moveHero(dy, dx int) {
|
||||
}
|
||||
|
||||
for {
|
||||
// Check if he tried to move off the screen or make an illegal
|
||||
// diagonal move, and stop him if he did.
|
||||
hitBound := nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1
|
||||
|
||||
var (
|
||||
ch byte
|
||||
fl PlaceFlags
|
||||
)
|
||||
|
||||
if !hitBound {
|
||||
if !g.diagOk(p.Pos, nh) {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if g.Running && p.Pos == nh {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
fl = *g.Level.FlagsAt(nh.Y, nh.X)
|
||||
|
||||
ch = g.Level.VisibleChar(nh.Y, nh.X)
|
||||
if !fl.Has(FReal) && ch == Floor {
|
||||
if !p.On(Levitating) {
|
||||
ch = Trap
|
||||
g.Level.SetChar(nh.Y, nh.X, Trap)
|
||||
g.Level.FlagsAt(nh.Y, nh.X).Set(FReal)
|
||||
}
|
||||
} else if p.On(Held) && ch != 'F' {
|
||||
g.msg("you are being held")
|
||||
|
||||
return
|
||||
}
|
||||
ch, fl, stop := g.moveTarget(nh)
|
||||
if stop {
|
||||
return
|
||||
}
|
||||
|
||||
if hitBound {
|
||||
ch = ' ' // fall into the wall case below
|
||||
turned, ndy, ndx := g.moveResolve(nh, ch, fl, dy, dx)
|
||||
if !turned {
|
||||
return
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case ' ', '|', '-':
|
||||
if turn, ndy, ndx := g.passageTurn(dy, dx); turn {
|
||||
dy, dx = ndy, ndx
|
||||
// the C goto over: re-check the turned move
|
||||
dy, dx = ndy, ndx
|
||||
|
||||
g.turnRefresh()
|
||||
g.turnRefresh()
|
||||
|
||||
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
|
||||
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:
|
||||
if ch == Stairs {
|
||||
g.SeenStairs = true
|
||||
}
|
||||
|
||||
g.Running = false
|
||||
if isUpper(ch) || g.Level.MonsterAt(nh.Y, nh.X) != nil {
|
||||
g.fight(nh, p.CurWeapon, false)
|
||||
} else {
|
||||
if ch != Stairs {
|
||||
g.Take = ch
|
||||
}
|
||||
|
||||
g.finishMove(nh, fl)
|
||||
}
|
||||
// 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.
|
||||
if offMap(nh) {
|
||||
return ' ', 0, false // fall into the wall case
|
||||
}
|
||||
|
||||
if !g.diagOk(p.Pos, nh) {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
|
||||
return 0, 0, true
|
||||
}
|
||||
|
||||
if g.Running && p.Pos == nh {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
fl := *g.Level.FlagsAt(nh.Y, nh.X)
|
||||
|
||||
ch := g.Level.VisibleChar(nh.Y, nh.X)
|
||||
if !fl.Has(FReal) && ch == Floor {
|
||||
if !p.On(Levitating) {
|
||||
ch = Trap
|
||||
g.Level.SetChar(nh.Y, nh.X, Trap)
|
||||
g.Level.FlagsAt(nh.Y, nh.X).Set(FReal)
|
||||
}
|
||||
} else if p.On(Held) && ch != 'F' {
|
||||
g.msg("you are being held")
|
||||
|
||||
return 0, 0, true
|
||||
}
|
||||
|
||||
return ch, fl, false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
g.Running = false
|
||||
if isUpper(ch) || g.Level.MonsterAt(nh.Y, nh.X) != nil {
|
||||
g.fight(nh, p.CurWeapon, false)
|
||||
} else {
|
||||
if ch != Stairs {
|
||||
g.Take = ch
|
||||
}
|
||||
|
||||
g.finishMove(nh, fl)
|
||||
}
|
||||
}
|
||||
|
||||
// passageTurn checks whether a runner in a gone-room passage should turn
|
||||
@@ -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,85 +314,8 @@ func (g *RogueGame) springTrap(tc Coord) TrapKind {
|
||||
tr := TrapKind(pp.Flags & FTrapMask)
|
||||
pp.Flags.Set(FSeen)
|
||||
|
||||
switch tr {
|
||||
case TrapDoor:
|
||||
g.Depth++
|
||||
g.NewLevel()
|
||||
g.msg("you fell into a trap!")
|
||||
case TrapBear:
|
||||
g.NoMove += g.spread(3) // BEARTIME
|
||||
g.msg("you are caught in a bear trap")
|
||||
case TrapMystery:
|
||||
switch g.rnd(11) {
|
||||
case 0:
|
||||
g.msg("you are suddenly in a parallel dimension")
|
||||
case 1:
|
||||
g.msg("the light in here suddenly seems %s", g.data.rainbow[g.rnd(len(g.data.rainbow))])
|
||||
case 2:
|
||||
g.msg("you feel a sting in the side of your neck")
|
||||
case 3:
|
||||
g.msg("multi-colored lines swirl around you, then fade")
|
||||
case 4:
|
||||
g.msg("a %s light flashes in your eyes", g.data.rainbow[g.rnd(len(g.data.rainbow))])
|
||||
case 5:
|
||||
g.msg("a spike shoots past your ear!")
|
||||
case 6:
|
||||
g.msg("%s sparks dance across your armor", g.data.rainbow[g.rnd(len(g.data.rainbow))])
|
||||
case 7:
|
||||
g.msg("you suddenly feel very thirsty")
|
||||
case 8:
|
||||
g.msg("you feel time speed up suddenly")
|
||||
case 9:
|
||||
g.msg("time now seems to be going slower")
|
||||
case 10:
|
||||
g.msg("you pack turns %s!", g.data.rainbow[g.rnd(len(g.data.rainbow))])
|
||||
}
|
||||
case TrapSleep:
|
||||
g.NoCommand += g.spread(5) // SLEEPTIME
|
||||
|
||||
p.Flags.Clear(Awake)
|
||||
g.msg("a strange white mist envelops you and you fall asleep")
|
||||
case TrapArrow:
|
||||
if g.swing(p.Stats.Lvl-1, p.Stats.ArmorClass, 1) {
|
||||
p.Stats.HP -= g.roll(1, 6)
|
||||
if p.Stats.HP <= 0 {
|
||||
g.msg("an arrow killed you")
|
||||
g.death('a')
|
||||
} else {
|
||||
g.msg("oh no! An arrow shot you")
|
||||
}
|
||||
} else {
|
||||
arrow := newObject()
|
||||
g.initWeapon(arrow, WeaponArrow)
|
||||
arrow.Count = 1
|
||||
arrow.Pos = p.Pos
|
||||
g.fall(arrow, false)
|
||||
g.msg("an arrow shoots past you")
|
||||
}
|
||||
case TrapTeleport:
|
||||
// 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:
|
||||
if !g.swing(p.Stats.Lvl+1, p.Stats.ArmorClass, 1) {
|
||||
g.msg("a small dart whizzes by your ear and vanishes")
|
||||
} else {
|
||||
p.Stats.HP -= g.roll(1, 4)
|
||||
if p.Stats.HP <= 0 {
|
||||
g.msg("a poisoned dart killed you")
|
||||
g.death('d')
|
||||
}
|
||||
|
||||
if !p.IsWearing(RingSustainStrength) && !g.save(VsPoison) {
|
||||
g.changeStrength(-1)
|
||||
}
|
||||
|
||||
g.msg("a small dart just hit you in the shoulder")
|
||||
}
|
||||
case TrapRust:
|
||||
g.msg("a gush of water hits you on the head")
|
||||
g.rustArmor(p.CurArmor)
|
||||
if h := g.data.trapHandlers[tr]; h != nil {
|
||||
h(g, tc)
|
||||
}
|
||||
|
||||
g.flushType()
|
||||
@@ -341,6 +323,116 @@ func (g *RogueGame) springTrap(tc Coord) TrapKind {
|
||||
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!")
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapBear(Coord) {
|
||||
g.NoMove += g.spread(3) // BEARTIME
|
||||
g.msg("you are caught in a bear trap")
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapMystery(Coord) {
|
||||
which := g.rnd(11)
|
||||
switch which {
|
||||
case 0:
|
||||
g.msg("you are suddenly in a parallel dimension")
|
||||
case 1:
|
||||
g.msg("the light in here suddenly seems %s", g.data.rainbow[g.rnd(len(g.data.rainbow))])
|
||||
case 2:
|
||||
g.msg("you feel a sting in the side of your neck")
|
||||
case 3:
|
||||
g.msg("multi-colored lines swirl around you, then fade")
|
||||
case 4:
|
||||
g.msg("a %s light flashes in your eyes", 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:
|
||||
g.msg("you suddenly feel very thirsty")
|
||||
case 8:
|
||||
g.msg("you feel time speed up suddenly")
|
||||
case 9:
|
||||
g.msg("time now seems to be going slower")
|
||||
case 10:
|
||||
g.msg("you pack turns %s!", g.data.rainbow[g.rnd(len(g.data.rainbow))])
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapSleep(Coord) {
|
||||
g.NoCommand += g.spread(5) // SLEEPTIME
|
||||
|
||||
g.Player.Flags.Clear(Awake)
|
||||
g.msg("a strange white mist envelops you and you fall asleep")
|
||||
}
|
||||
|
||||
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 {
|
||||
g.msg("an arrow killed you")
|
||||
g.death('a')
|
||||
} else {
|
||||
g.msg("oh no! An arrow shot you")
|
||||
}
|
||||
} else {
|
||||
arrow := newObject()
|
||||
g.initWeapon(arrow, WeaponArrow)
|
||||
arrow.Count = 1
|
||||
arrow.Pos = p.Pos
|
||||
g.fall(arrow, false)
|
||||
g.msg("an arrow shoots past you")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p.Stats.HP -= g.roll(1, 4)
|
||||
if p.Stats.HP <= 0 {
|
||||
g.msg("a poisoned dart killed you")
|
||||
g.death('d')
|
||||
}
|
||||
|
||||
if !p.IsWearing(RingSustainStrength) && !g.save(VsPoison) {
|
||||
g.changeStrength(-1)
|
||||
}
|
||||
|
||||
g.msg("a small dart just hit you in the shoulder")
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapRust(Coord) {
|
||||
g.msg("a gush of water hits you on the head")
|
||||
g.rustArmor(g.Player.CurArmor)
|
||||
}
|
||||
|
||||
// randomStep moves in a random direction if the monster/person is
|
||||
// confused (move.c rndmove).
|
||||
func (g *RogueGame) randomStep(who *Creature) Coord {
|
||||
|
||||
@@ -38,75 +38,106 @@ func TestNewLevelInvariants(t *testing.T) {
|
||||
for _, seed := range []int32{1, 12345, 2026, 99999} {
|
||||
g := genLevel(t, seed)
|
||||
|
||||
// The staircase is somewhere real.
|
||||
st := g.Level.Stairs
|
||||
if g.Level.Char(st.Y, st.X) != Stairs {
|
||||
t.Errorf("seed %d: no staircase at recorded stairs position", 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 {
|
||||
t.Errorf("seed %d: no staircase at recorded stairs position", seed)
|
||||
}
|
||||
|
||||
// The hero stands on a walkable, monster-free cell.
|
||||
hp := g.Player.Pos
|
||||
if !stepOk(g.Level.Char(hp.Y, hp.X)) {
|
||||
t.Errorf("seed %d: hero on unwalkable cell %q", seed,
|
||||
g.Level.Char(hp.Y, hp.X))
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(hp.Y, hp.X) != nil {
|
||||
t.Errorf("seed %d: hero standing on a monster", seed)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
m := renderMap(g)
|
||||
if !strings.Contains(m, "|") || !strings.Contains(m, "-") {
|
||||
t.Errorf("seed %d: no room walls drawn", seed)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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",
|
||||
seed, mon.Type)
|
||||
}
|
||||
|
||||
// The hero stands on a walkable, monster-free cell.
|
||||
hp := g.Player.Pos
|
||||
if !stepOk(g.Level.Char(hp.Y, hp.X)) {
|
||||
t.Errorf("seed %d: hero on unwalkable cell %q", seed,
|
||||
g.Level.Char(hp.Y, hp.X))
|
||||
if mon.Room == nil {
|
||||
t.Errorf("seed %d: monster %c has no room", seed, mon.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(hp.Y, hp.X) != nil {
|
||||
t.Errorf("seed %d: hero standing on a monster", seed)
|
||||
// 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()
|
||||
|
||||
for _, obj := range g.Level.Objects {
|
||||
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
|
||||
if ch != obj.Kind.Glyph() &&
|
||||
g.Level.MonsterAt(obj.Pos.Y, obj.Pos.X) == nil {
|
||||
t.Errorf("seed %d: object %v at (%d,%d) but map shows %q",
|
||||
seed, obj.Kind, obj.Pos.Y, obj.Pos.X, ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.Player.Room == nil {
|
||||
t.Errorf("seed %d: hero not in any room", seed)
|
||||
}
|
||||
// checkStartingKit verifies the player has her starting kit: food, armor,
|
||||
// mace, bow, arrows.
|
||||
func checkStartingKit(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)
|
||||
}
|
||||
if len(g.Player.Pack) != 5 {
|
||||
t.Errorf("seed %d: starting pack has %d items, want 5",
|
||||
seed, len(g.Player.Pack))
|
||||
}
|
||||
|
||||
if !strings.Contains(m, ".") && !strings.Contains(m, "#") {
|
||||
t.Errorf("seed %d: no floor or passages drawn", seed)
|
||||
}
|
||||
if g.Player.CurWeapon == nil ||
|
||||
g.Player.CurWeapon.WeaponKind() != WeaponMace {
|
||||
t.Errorf("seed %d: not wielding the starting mace", seed)
|
||||
}
|
||||
|
||||
// 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",
|
||||
seed, mon.Type)
|
||||
}
|
||||
|
||||
if mon.Room == nil {
|
||||
t.Errorf("seed %d: monster %c has no room", seed, mon.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// 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() &&
|
||||
g.Level.MonsterAt(obj.Pos.Y, obj.Pos.X) == nil {
|
||||
t.Errorf("seed %d: object %v at (%d,%d) but map shows %q",
|
||||
seed, obj.Kind, obj.Pos.Y, obj.Pos.X, ch)
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
if g.Player.CurWeapon == nil ||
|
||||
g.Player.CurWeapon.WeaponKind() != WeaponMace {
|
||||
t.Errorf("seed %d: not wielding the starting mace", seed)
|
||||
}
|
||||
|
||||
if g.Player.CurArmor == nil ||
|
||||
g.Player.CurArmor.ArmorKind() != ArmorRingMail {
|
||||
t.Errorf("seed %d: not wearing the starting ring mail", seed)
|
||||
}
|
||||
if g.Player.CurArmor == nil ||
|
||||
g.Player.CurArmor.ArmorKind() != ArmorRingMail {
|
||||
t.Errorf("seed %d: not wearing the starting ring mail", seed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
253
game/options.go
253
game/options.go
@@ -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,75 +345,7 @@ func (g *RogueGame) ParseOpts(str string) {
|
||||
i++
|
||||
}
|
||||
|
||||
name := str[:i]
|
||||
rest := str[i:]
|
||||
matched := false
|
||||
|
||||
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 {
|
||||
// Skip to start of string value
|
||||
for rest != "" && rest[0] == '=' {
|
||||
rest = rest[1:]
|
||||
}
|
||||
|
||||
val := rest
|
||||
|
||||
var prefix string
|
||||
if val != "" && val[0] == '~' {
|
||||
prefix = g.Home
|
||||
|
||||
val = val[1:]
|
||||
for val != "" && val[0] == '/' {
|
||||
val = val[1:]
|
||||
}
|
||||
}
|
||||
// Skip to end of string value
|
||||
end := strings.IndexByte(val, ',')
|
||||
if end < 0 {
|
||||
end = len(val)
|
||||
}
|
||||
|
||||
word := val[:end]
|
||||
rest = val[end:]
|
||||
|
||||
if op.kind == optInvT {
|
||||
// check for type of inventory
|
||||
w := word
|
||||
if w != "" {
|
||||
w = string(toUpper(w[0])) + w[1:]
|
||||
}
|
||||
|
||||
for ti, tn := range g.data.invTName {
|
||||
if strings.HasPrefix(tn, w) {
|
||||
*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
|
||||
rest := g.parseOptName(optlist, str[:i], str[i:])
|
||||
// skip to start of next option name
|
||||
for rest != "" && !isAlpha(rest[0]) {
|
||||
rest = rest[1:]
|
||||
@@ -415,6 +355,89 @@ func (g *RogueGame) ParseOpts(str string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 != "" {
|
||||
if isBoolOpt {
|
||||
*op.boolP = true
|
||||
|
||||
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:]
|
||||
}
|
||||
|
||||
val := rest
|
||||
|
||||
var prefix string
|
||||
if val != "" && val[0] == '~' {
|
||||
prefix = g.Home
|
||||
|
||||
val = val[1:]
|
||||
for val != "" && val[0] == '/' {
|
||||
val = val[1:]
|
||||
}
|
||||
}
|
||||
// Skip to end of string value
|
||||
end := strings.IndexByte(val, ',')
|
||||
if end < 0 {
|
||||
end = len(val)
|
||||
}
|
||||
|
||||
word := 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
|
||||
if word != "" {
|
||||
word = string(toUpper(word[0])) + word[1:]
|
||||
}
|
||||
|
||||
for ti, tn := range g.data.invTName {
|
||||
if strings.HasPrefix(tn, word) {
|
||||
*op.intP = ti
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// strucpy copies a string keeping only printable characters, capped at
|
||||
// MAXINP (options.c strucpy).
|
||||
func strucpy(s string) string {
|
||||
|
||||
324
game/pack.go
324
game/pack.go
@@ -17,105 +17,13 @@ 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) {
|
||||
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, ok := g.packInsert(obj, fromFloor)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
obj.Flags.Set(WasFound)
|
||||
@@ -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")
|
||||
} else {
|
||||
g.msg("nothing appropriate")
|
||||
}
|
||||
if kind == KindNone {
|
||||
g.msg("%s", g.chooseTerse("empty handed", "you are empty handed"))
|
||||
} 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
|
||||
|
||||
417
game/passages.go
417
game/passages.go
@@ -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 {
|
||||
// otherwise, connect new room to the graph, and draw a tunnel
|
||||
// to it
|
||||
ingraph[r2] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
|
||||
g.connectRooms(r1, r2)
|
||||
isconn[r1][r2] = true
|
||||
isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
|
||||
roomcount++
|
||||
}
|
||||
|
||||
if roomcount >= MaxRooms {
|
||||
break
|
||||
continue
|
||||
}
|
||||
// otherwise, connect new room to the graph, and draw a tunnel
|
||||
// to it
|
||||
ingraph[r2] = true
|
||||
g.connectRooms(r1, r2)
|
||||
isconn[r1][r2] = true
|
||||
isconn[r2][r1] = true
|
||||
roomcount++
|
||||
}
|
||||
|
||||
// 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
|
||||
} else {
|
||||
turnDelta.X = -1
|
||||
}
|
||||
|
||||
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)
|
||||
plan = g.connPlanDown(rm)
|
||||
} else {
|
||||
plan = g.connPlanRight(rm)
|
||||
}
|
||||
|
||||
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,38 +300,50 @@ 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 == '-')) {
|
||||
ch := pp.Ch
|
||||
if pp.Flags.Has(FPassage) {
|
||||
ch = Passage
|
||||
}
|
||||
|
||||
pp.Flags.Set(FSeen)
|
||||
g.move(y, x)
|
||||
|
||||
switch {
|
||||
case pp.Monst != nil:
|
||||
pp.Monst.OldCh = pp.Ch
|
||||
case pp.Flags.Has(FReal):
|
||||
g.addch(ch)
|
||||
default:
|
||||
g.standout()
|
||||
|
||||
if pp.Flags.Has(FPassage) {
|
||||
g.addch(Passage)
|
||||
} else {
|
||||
g.addch(Door)
|
||||
}
|
||||
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
pp.Flags.Set(FSeen)
|
||||
g.move(y, x)
|
||||
|
||||
switch {
|
||||
case pp.Monst != nil:
|
||||
pp.Monst.OldCh = pp.Ch
|
||||
case pp.Flags.Has(FReal):
|
||||
g.addch(ch)
|
||||
default:
|
||||
g.standout()
|
||||
|
||||
if pp.Flags.Has(FPassage) {
|
||||
g.addch(Passage)
|
||||
} else {
|
||||
g.addch(Door)
|
||||
}
|
||||
|
||||
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).
|
||||
func (g *RogueGame) numberPassages() {
|
||||
g.pnum = 0
|
||||
@@ -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) {
|
||||
|
||||
357
game/potions.go
357
game/potions.go
@@ -41,145 +41,8 @@ func (g *RogueGame) quaff() {
|
||||
|
||||
g.leavePack(obj, false, false)
|
||||
|
||||
switch obj.PotionKind() {
|
||||
case PotionConfusion:
|
||||
g.applyPotionFuse(PotionConfusion, !trip)
|
||||
case PotionPoison:
|
||||
g.Items.Potions[PotionPoison].Know = true
|
||||
if p.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:
|
||||
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++
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
g.msg("you begin to feel better")
|
||||
case PotionGainStrength:
|
||||
g.Items.Potions[PotionGainStrength].Know = true
|
||||
g.changeStrength(1)
|
||||
g.msg("you feel stronger, now. What bulging muscles!")
|
||||
case PotionDetectMonsters:
|
||||
p.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:
|
||||
// Potion of magic detection. Show the potions and scrolls
|
||||
show := false
|
||||
|
||||
if len(g.Level.Objects) > 0 {
|
||||
g.scr.Hw.Clear()
|
||||
|
||||
for _, tp := range g.Level.Objects {
|
||||
if g.isMagic(tp) {
|
||||
show = true
|
||||
|
||||
g.scr.Hw.MvAddCh(tp.Pos.Y, tp.Pos.X, Magic)
|
||||
g.Items.Potions[PotionDetectMagic].Know = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, mp := range g.Level.Monsters {
|
||||
for _, tp := range mp.Pack {
|
||||
if g.isMagic(tp) {
|
||||
show = true
|
||||
|
||||
g.scr.Hw.MvAddCh(mp.Pos.Y, mp.Pos.X, Magic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if show {
|
||||
g.Items.Potions[PotionDetectMagic].Know = true
|
||||
g.showWin("You sense the presence of magic on this level.--More--")
|
||||
} else {
|
||||
g.msg("you have a %s feeling for a moment, then it passes",
|
||||
g.chooseStr("normal", "strange"))
|
||||
}
|
||||
case PotionLSD:
|
||||
if !trip {
|
||||
if p.On(SenseMonsters) {
|
||||
g.turnSee(false)
|
||||
}
|
||||
|
||||
g.StartDaemon(DVisuals, 0, Before)
|
||||
g.SeenStairs = g.seenStairs()
|
||||
}
|
||||
|
||||
g.applyPotionFuse(PotionLSD, true)
|
||||
case PotionSeeInvisible:
|
||||
show := p.On(CanSeeInvisible)
|
||||
|
||||
g.applyPotionFuse(PotionSeeInvisible, false)
|
||||
|
||||
if !show {
|
||||
g.invisOn()
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
case PotionRaiseLevel:
|
||||
g.Items.Potions[PotionRaiseLevel].Know = true
|
||||
g.msg("you suddenly feel much more skillful")
|
||||
g.raiseLevel()
|
||||
case PotionExtraHealing:
|
||||
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 {
|
||||
p.Stats.MaxHP++
|
||||
}
|
||||
|
||||
p.Stats.MaxHP++
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
g.comeDown(0)
|
||||
g.msg("you begin to feel much better")
|
||||
case PotionHaste:
|
||||
g.Items.Potions[PotionHaste].Know = true
|
||||
|
||||
g.After = false
|
||||
if g.addHaste(true) {
|
||||
g.msg("you feel yourself moving much faster")
|
||||
}
|
||||
case PotionRestoreStrength:
|
||||
if p.IsRing(Left, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, -p.CurRing[Left].Bonus)
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, -p.CurRing[Right].Bonus)
|
||||
}
|
||||
|
||||
if p.Stats.Str < p.MaxStats.Str {
|
||||
p.Stats.Str = p.MaxStats.Str
|
||||
}
|
||||
|
||||
if p.IsRing(Left, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, p.CurRing[Left].Bonus)
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, p.CurRing[Right].Bonus)
|
||||
}
|
||||
|
||||
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)
|
||||
if h := g.data.quaffHandlers[obj.PotionKind()]; h != nil {
|
||||
h(g, trip)
|
||||
}
|
||||
|
||||
g.status()
|
||||
@@ -187,6 +50,181 @@ func (g *RogueGame) quaff() {
|
||||
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)
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffPoison(bool) {
|
||||
g.Items.Potions[PotionPoison].Know = true
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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++
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
g.msg("you begin to feel better")
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffGainStrength(bool) {
|
||||
g.Items.Potions[PotionGainStrength].Know = true
|
||||
g.changeStrength(1)
|
||||
g.msg("you feel stronger, now. What bulging muscles!")
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffDetectMagic(bool) {
|
||||
// Potion of magic detection. Show the potions and scrolls
|
||||
show := false
|
||||
|
||||
if len(g.Level.Objects) > 0 {
|
||||
g.scr.Hw.Clear()
|
||||
|
||||
for _, tp := range g.Level.Objects {
|
||||
if g.isMagic(tp) {
|
||||
show = true
|
||||
|
||||
g.scr.Hw.MvAddCh(tp.Pos.Y, tp.Pos.X, Magic)
|
||||
g.Items.Potions[PotionDetectMagic].Know = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, mp := range g.Level.Monsters {
|
||||
for _, tp := range mp.Pack {
|
||||
if g.isMagic(tp) {
|
||||
show = true
|
||||
|
||||
g.scr.Hw.MvAddCh(mp.Pos.Y, mp.Pos.X, Magic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if show {
|
||||
g.Items.Potions[PotionDetectMagic].Know = true
|
||||
g.showWin("You sense the presence of magic on this level.--More--")
|
||||
} else {
|
||||
g.msg("you have a %s feeling for a moment, then it passes",
|
||||
g.chooseStr("normal", "strange"))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffLSD(trip bool) {
|
||||
p := &g.Player
|
||||
if !trip {
|
||||
if p.On(SenseMonsters) {
|
||||
g.turnSee(false)
|
||||
}
|
||||
|
||||
g.StartDaemon(DVisuals, 0, Before)
|
||||
g.SeenStairs = g.seenStairs()
|
||||
}
|
||||
|
||||
g.applyPotionFuse(PotionLSD, true)
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffSeeInvisible(bool) {
|
||||
show := g.Player.On(CanSeeInvisible)
|
||||
|
||||
g.applyPotionFuse(PotionSeeInvisible, false)
|
||||
|
||||
if !show {
|
||||
g.invisOn()
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffRaiseLevel(bool) {
|
||||
g.Items.Potions[PotionRaiseLevel].Know = true
|
||||
g.msg("you suddenly feel much more skillful")
|
||||
g.raiseLevel()
|
||||
}
|
||||
|
||||
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 {
|
||||
p.Stats.MaxHP++
|
||||
}
|
||||
|
||||
p.Stats.MaxHP++
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
g.comeDown(0)
|
||||
g.msg("you begin to feel much better")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffRestoreStrength(bool) {
|
||||
p := &g.Player
|
||||
if p.IsRing(Left, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, -p.CurRing[Left].Bonus)
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, -p.CurRing[Right].Bonus)
|
||||
}
|
||||
|
||||
if p.Stats.Str < p.MaxStats.Str {
|
||||
p.Stats.Str = p.MaxStats.Str
|
||||
}
|
||||
|
||||
if p.IsRing(Left, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, p.CurRing[Left].Bonus)
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, p.CurRing[Right].Bonus)
|
||||
}
|
||||
|
||||
g.msg("hey, this tastes great. It make you feel warm all over")
|
||||
}
|
||||
|
||||
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
|
||||
// raise_level).
|
||||
func (g *RogueGame) raiseLevel() {
|
||||
@@ -260,22 +298,8 @@ func (g *RogueGame) turnSee(turnOff bool) bool {
|
||||
if !canSee {
|
||||
g.addch(mp.OldCh)
|
||||
}
|
||||
} else {
|
||||
if !canSee {
|
||||
g.standout()
|
||||
}
|
||||
|
||||
if !g.Player.On(Hallucinating) {
|
||||
g.addch(mp.Type)
|
||||
} else {
|
||||
g.addch(g.randomMonsterLetter())
|
||||
}
|
||||
|
||||
if !canSee {
|
||||
g.standend()
|
||||
|
||||
addNew = true
|
||||
}
|
||||
} else if g.showSensed(mp, canSee) {
|
||||
addNew = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +312,29 @@ func (g *RogueGame) turnSee(turnOff bool) bool {
|
||||
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()
|
||||
}
|
||||
|
||||
if !g.Player.On(Hallucinating) {
|
||||
g.addch(mp.Type)
|
||||
} else {
|
||||
g.addch(g.randomMonsterLetter())
|
||||
}
|
||||
|
||||
if !canSee {
|
||||
g.standend()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// seenStairs reports whether the player has seen the stairs (potions.c
|
||||
// seen_stairs).
|
||||
func (g *RogueGame) seenStairs() bool {
|
||||
|
||||
@@ -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
|
||||
|
||||
167
game/rip.go
167
game/rip.go
@@ -115,78 +115,7 @@ func (g *RogueGame) totalWinner() {
|
||||
line := 1
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
worth := 0
|
||||
it := &g.Items
|
||||
|
||||
switch obj.Kind {
|
||||
case KindFood:
|
||||
worth = 2 * obj.Count
|
||||
case KindWeapon:
|
||||
worth = it.Weapons[obj.Which].Worth
|
||||
worth *= 3*(obj.HPlus+obj.DPlus) + obj.Count
|
||||
obj.Flags.Set(Known)
|
||||
case KindArmor:
|
||||
worth = it.Armors[obj.Which].Worth
|
||||
worth += (9 - obj.ArmorClass) * 100
|
||||
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
|
||||
case KindPotion:
|
||||
op := &it.Potions[obj.Which]
|
||||
|
||||
worth = op.Worth * obj.Count
|
||||
if !op.Know {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
op.Know = true
|
||||
case KindRing:
|
||||
op := &it.Rings[obj.Which]
|
||||
worth = op.Worth
|
||||
|
||||
if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage ||
|
||||
obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity {
|
||||
if obj.Bonus > 0 {
|
||||
worth += obj.Bonus * 100
|
||||
} else {
|
||||
worth = 10
|
||||
}
|
||||
}
|
||||
|
||||
if !obj.Flags.Has(Known) {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
obj.Flags.Set(Known)
|
||||
|
||||
op.Know = true
|
||||
case KindWand:
|
||||
op := &it.Sticks[obj.Which]
|
||||
worth = op.Worth
|
||||
|
||||
worth += 20 * obj.Charges
|
||||
if !obj.Flags.Has(Known) {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
obj.Flags.Set(Known)
|
||||
|
||||
op.Know = true
|
||||
case KindAmulet:
|
||||
worth = 1000
|
||||
}
|
||||
|
||||
if worth < 0 {
|
||||
worth = 0
|
||||
}
|
||||
worth := g.objectWorth(obj)
|
||||
|
||||
g.scr.Std.MvPrintwf(line, 0, "%c) %5d %s", obj.PackCh, worth,
|
||||
g.inventoryName(obj, false))
|
||||
@@ -200,6 +129,100 @@ func (g *RogueGame) totalWinner() {
|
||||
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:
|
||||
worth = 2 * obj.Count
|
||||
case KindWeapon:
|
||||
worth = it.Weapons[obj.Which].Worth
|
||||
worth *= 3*(obj.HPlus+obj.DPlus) + obj.Count
|
||||
obj.Flags.Set(Known)
|
||||
case KindArmor:
|
||||
worth = it.Armors[obj.Which].Worth
|
||||
worth += (9 - obj.ArmorClass) * 100
|
||||
worth += 10 * (g.data.aClass[obj.Which] - obj.ArmorClass)
|
||||
obj.Flags.Set(Known)
|
||||
case KindScroll:
|
||||
worth = loreWorth(&it.Scrolls[obj.Which], obj.Count)
|
||||
case KindPotion:
|
||||
worth = loreWorth(&it.Potions[obj.Which], obj.Count)
|
||||
case KindRing:
|
||||
worth = g.ringWorth(obj)
|
||||
case KindWand:
|
||||
worth = g.wandWorth(obj)
|
||||
case KindAmulet:
|
||||
worth = 1000
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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 {
|
||||
if obj.Bonus > 0 {
|
||||
worth += obj.Bonus * 100
|
||||
} else {
|
||||
worth = 10
|
||||
}
|
||||
}
|
||||
|
||||
if !obj.Flags.Has(Known) {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
obj.Flags.Set(Known)
|
||||
|
||||
op.Know = true
|
||||
|
||||
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) {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
obj.Flags.Set(Known)
|
||||
|
||||
op.Know = true
|
||||
|
||||
return worth
|
||||
}
|
||||
|
||||
// killname converts a code to a monster name (rip.c killname).
|
||||
func (g *RogueGame) killname(monst byte, doart bool) string {
|
||||
var (
|
||||
|
||||
407
game/rooms.go
407
game/rooms.go
@@ -38,87 +38,119 @@ func (g *RogueGame) digRooms() {
|
||||
}
|
||||
// dig and populate all the rooms on the level
|
||||
for i := range g.Level.Rooms {
|
||||
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}
|
||||
g.digRoom(i, bsze)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// 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}
|
||||
|
||||
rp.Max.Y = -NumLines
|
||||
if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if rp.Flags.Has(Gone) {
|
||||
g.placeGoneRoom(rp, top, bsze)
|
||||
|
||||
continue
|
||||
return
|
||||
}
|
||||
// set room type
|
||||
if g.rnd(10) < g.Depth-1 {
|
||||
rp.Flags.Set(Dark) // dark room
|
||||
|
||||
if g.rnd(15) == 0 {
|
||||
rp.Flags = Maze // maze room
|
||||
}
|
||||
// set room type
|
||||
if g.rnd(10) < g.Depth-1 {
|
||||
rp.Flags.Set(Dark) // dark room
|
||||
}
|
||||
// Find a place and size for a random room
|
||||
if rp.Flags.Has(Maze) {
|
||||
placeMazeRoom(rp, top, bsze)
|
||||
} else {
|
||||
g.placeNormalRoom(rp, top, bsze)
|
||||
}
|
||||
|
||||
if g.rnd(15) == 0 {
|
||||
rp.Flags = Maze // maze room
|
||||
}
|
||||
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
|
||||
}
|
||||
// Find a place and size for a random room
|
||||
if rp.Flags.Has(Maze) {
|
||||
rp.Max.X = bsze.X - 1
|
||||
}
|
||||
}
|
||||
|
||||
rp.Max.Y = bsze.Y - 1
|
||||
if rp.Pos.X = top.X; rp.Pos.X == 1 {
|
||||
rp.Pos.X = 0
|
||||
}
|
||||
// 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
|
||||
|
||||
if rp.Pos.Y = top.Y; rp.Pos.Y == 0 {
|
||||
rp.Pos.Y++
|
||||
rp.Max.Y--
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
rp.Max.X = g.rnd(bsze.X-4) + 4
|
||||
rp.Max.Y = g.rnd(bsze.Y-4) + 4
|
||||
rp.Pos.X = top.X + g.rnd(bsze.X-rp.Max.X)
|
||||
rp.Max.Y = bsze.Y - 1
|
||||
if rp.Pos.X = top.X; rp.Pos.X == 1 {
|
||||
rp.Pos.X = 0
|
||||
}
|
||||
|
||||
rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y)
|
||||
if rp.Pos.Y != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if rp.Pos.Y = top.Y; rp.Pos.Y == 0 {
|
||||
rp.Pos.Y++
|
||||
rp.Max.Y--
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
rp.Pos.X = top.X + g.rnd(bsze.X-rp.Max.X)
|
||||
|
||||
rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y)
|
||||
if rp.Pos.Y != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.drawRoom(rp)
|
||||
// Put the gold in
|
||||
if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) {
|
||||
gold := newObject()
|
||||
rp.GoldVal = g.goldCalc()
|
||||
gold.GoldValue = rp.GoldVal
|
||||
rp.Gold, _ = g.findFloorIn(rp, 0, false)
|
||||
gold.Pos = rp.Gold
|
||||
g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold)
|
||||
// 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.Flags = Stackable
|
||||
gold.Group = goldGrp
|
||||
gold.Kind = KindGold
|
||||
g.Level.AddObject(gold)
|
||||
}
|
||||
// Put the monster in
|
||||
prob := 25
|
||||
if rp.GoldVal > 0 {
|
||||
prob = 80
|
||||
}
|
||||
gold := newObject()
|
||||
rp.GoldVal = g.goldCalc()
|
||||
gold.GoldValue = rp.GoldVal
|
||||
rp.Gold, _ = g.findFloorIn(rp, 0, false)
|
||||
gold.Pos = rp.Gold
|
||||
g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold)
|
||||
|
||||
if g.rnd(100) < prob {
|
||||
tp := &Monster{}
|
||||
mp, _ := g.findFloorIn(rp, 0, true)
|
||||
g.newMonster(tp, g.randMonster(false), mp)
|
||||
g.givePack(tp)
|
||||
}
|
||||
gold.Flags = Stackable
|
||||
gold.Group = goldGrp
|
||||
gold.Kind = KindGold
|
||||
g.Level.AddObject(gold)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if g.rnd(100) < prob {
|
||||
tp := &Monster{}
|
||||
mp, _ := g.findFloorIn(rp, 0, true)
|
||||
g.newMonster(tp, g.randMonster(false), mp)
|
||||
g.givePack(tp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,63 +214,73 @@ 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 {
|
||||
cnt := 0
|
||||
|
||||
var nexty, nextx int
|
||||
|
||||
for _, cp := range del {
|
||||
newy := y + cp.Y
|
||||
|
||||
newx := x + cp.X
|
||||
if newy < 0 || newy > m.maxy || newx < 0 || newx > m.maxx {
|
||||
continue
|
||||
}
|
||||
|
||||
if g.Level.FlagsAt(newy+m.starty, newx+m.startx).Has(FPassage) {
|
||||
continue
|
||||
}
|
||||
|
||||
if cnt++; g.rnd(cnt) == 0 {
|
||||
nexty = newy
|
||||
nextx = newx
|
||||
}
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
nexty, nextx, ok := g.digPick(y, x)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
g.accountMaze(y, x, nexty, nextx)
|
||||
g.accountMaze(nexty, nextx, y, x)
|
||||
|
||||
var pos Coord
|
||||
if nexty == y {
|
||||
pos.Y = y + m.starty
|
||||
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.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
|
||||
|
||||
for _, cp := range del {
|
||||
newy := y + cp.Y
|
||||
|
||||
newx := x + cp.X
|
||||
if newy < 0 || newy > m.maxy || newx < 0 || newx > m.maxx {
|
||||
continue
|
||||
}
|
||||
|
||||
if g.Level.FlagsAt(newy+m.starty, newx+m.startx).Has(FPassage) {
|
||||
continue
|
||||
}
|
||||
|
||||
if cnt++; g.rnd(cnt) == 0 {
|
||||
nexty = newy
|
||||
nextx = newx
|
||||
}
|
||||
}
|
||||
|
||||
return nexty, nextx, cnt != 0
|
||||
}
|
||||
|
||||
// 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 := Coord{Y: y + m.starty, X: nextx + m.startx - 1}
|
||||
if nextx-x < 0 {
|
||||
pos.X = nextx + m.startx + 1
|
||||
}
|
||||
|
||||
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).
|
||||
func (g *RogueGame) accountMaze(y, x, ny, nx int) {
|
||||
sp := &g.maze.maze[y][x]
|
||||
@@ -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,34 +375,44 @@ 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++ {
|
||||
tp := g.Level.MonsterAt(y, x)
|
||||
|
||||
ch := g.Level.Char(y, x)
|
||||
if tp == nil {
|
||||
if g.inch() != ch {
|
||||
g.addch(ch)
|
||||
} else {
|
||||
g.move(y, x+1)
|
||||
}
|
||||
} else {
|
||||
tp.OldCh = ch
|
||||
if !g.seeMonst(tp) {
|
||||
if p.On(SenseMonsters) {
|
||||
g.standout()
|
||||
g.addch(tp.Disguise)
|
||||
g.standend()
|
||||
} else {
|
||||
g.addch(ch)
|
||||
}
|
||||
} else {
|
||||
g.addch(tp.Disguise)
|
||||
}
|
||||
}
|
||||
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)
|
||||
if tp == nil {
|
||||
if g.inch() != ch {
|
||||
g.addch(ch)
|
||||
} else {
|
||||
g.move(y, x+1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tp.OldCh = ch
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// leaveRoom is the code for when we exit a room (rooms.c leave_room).
|
||||
func (g *RogueGame) leaveRoom(cp Coord) {
|
||||
p := &g.Player
|
||||
@@ -381,35 +436,43 @@ 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.move(y, x)
|
||||
|
||||
switch ch := g.inch(); ch {
|
||||
case Floor:
|
||||
if floor == ' ' {
|
||||
g.addch(' ')
|
||||
}
|
||||
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) {
|
||||
g.standout()
|
||||
g.addch(ch)
|
||||
g.standend()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
pp := g.Level.At(y, x)
|
||||
if pp.Ch == Door {
|
||||
g.addch(Door)
|
||||
} else {
|
||||
g.addch(floor)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
case Floor:
|
||||
if floor == ' ' {
|
||||
g.addch(' ')
|
||||
}
|
||||
default:
|
||||
// to check for monster, we have to strip out the standout
|
||||
// bit (our Window returns the bare character already)
|
||||
if !isUpper(ch) {
|
||||
return
|
||||
}
|
||||
|
||||
if g.Player.On(SenseMonsters) {
|
||||
g.standout()
|
||||
g.addch(ch)
|
||||
g.standend()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
pp := g.Level.At(y, x)
|
||||
if pp.Ch == Door {
|
||||
g.addch(Door)
|
||||
} else {
|
||||
g.addch(floor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
390
game/save.go
390
game/save.go
@@ -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,
|
||||
}
|
||||
}
|
||||
// snapshotPlayer captures the player, equipment as pack indices (the
|
||||
// player half of snapshot).
|
||||
func (g *RogueGame) snapshotPlayer() savedPlayer {
|
||||
p := &g.Player
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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{}
|
||||
|
||||
switch {
|
||||
case m.Dest == nil:
|
||||
case m.Dest == &p.Pos:
|
||||
ref = destRef{Kind: 1}
|
||||
default:
|
||||
for mi, om := range g.Level.Monsters {
|
||||
if m.Dest == &om.Pos {
|
||||
ref = 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]}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ref.Kind == 0 {
|
||||
for ri := range g.Level.Rooms {
|
||||
if m.Dest == &g.Level.Rooms[ri].Gold {
|
||||
ref = destRef{Kind: 4, Idx: ri}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
st.Dests = append(st.Dests, ref)
|
||||
// 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:
|
||||
return destRef{}
|
||||
case m.Dest == &g.Player.Pos:
|
||||
return destRef{Kind: 1}
|
||||
}
|
||||
|
||||
return st
|
||||
for mi, om := range g.Level.Monsters {
|
||||
if m.Dest == &om.Pos {
|
||||
return destRef{Kind: 2, Idx: mi}
|
||||
}
|
||||
}
|
||||
|
||||
for _, oo := range g.Level.Objects {
|
||||
if m.Dest == &oo.Pos {
|
||||
return destRef{Kind: 3, Idx: objAt[oo]}
|
||||
}
|
||||
}
|
||||
|
||||
for ri := range g.Level.Rooms {
|
||||
if m.Dest == &g.Level.Rooms[ri].Gold {
|
||||
return destRef{Kind: 4, Idx: ri}
|
||||
}
|
||||
}
|
||||
|
||||
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,78 +510,29 @@ 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("")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if c == 'n' || c == 'N' || c == 'y' || c == 'Y' {
|
||||
break
|
||||
}
|
||||
|
||||
g.msg("please answer Y or N")
|
||||
a := g.askDefaultSave()
|
||||
if a == saveAbort {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
useDefault = false
|
||||
} else {
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("file name: ")
|
||||
|
||||
if g.getStr(&buf, g.scr.Std) == Quit {
|
||||
g.msg("")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
buf, ok := g.saveFileName(useDefault)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// 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("")
|
||||
useDefault = false
|
||||
|
||||
return
|
||||
}
|
||||
a := g.saveCheckOverwrite(buf)
|
||||
if a == saveAbort {
|
||||
return
|
||||
}
|
||||
|
||||
if c == 'y' || c == 'Y' {
|
||||
break
|
||||
}
|
||||
|
||||
if c == 'n' || c == 'N' {
|
||||
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)
|
||||
if a == saveNo {
|
||||
continue prompt // the C goto over: start again
|
||||
}
|
||||
|
||||
g.FileName = buf
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,28 +76,41 @@ 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 {
|
||||
m := h.Level.Monsters[0]
|
||||
if m.Dest != &h.Player.Pos {
|
||||
t.Error("monster chase target not re-aliased to the hero")
|
||||
}
|
||||
|
||||
if h.Level.MonsterAt(m.Pos.Y, m.Pos.X) != m {
|
||||
t.Error("map monster index not rebuilt")
|
||||
}
|
||||
|
||||
if m.Room == nil {
|
||||
t.Error("monster room pointer not rebuilt")
|
||||
}
|
||||
if len(g.Level.Monsters) == 0 {
|
||||
return
|
||||
}
|
||||
// Equipment aliasing: the wielded mace must be the same *Object as the
|
||||
// one in the pack.
|
||||
|
||||
m := h.Level.Monsters[0]
|
||||
if m.Dest != &h.Player.Pos {
|
||||
t.Error("monster chase target not re-aliased to the hero")
|
||||
}
|
||||
|
||||
if h.Level.MonsterAt(m.Pos.Y, m.Pos.X) != m {
|
||||
t.Error("map monster index not rebuilt")
|
||||
}
|
||||
|
||||
if m.Room == nil {
|
||||
t.Error("monster room pointer not rebuilt")
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
158
game/score.go
158
game/score.go
@@ -107,61 +107,86 @@ 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 {
|
||||
uid := os.Getuid()
|
||||
ins = g.scoreInsert(topTen, amount, flags, monst)
|
||||
}
|
||||
|
||||
scp := len(topTen)
|
||||
for i := range topTen {
|
||||
if amount > topTen[i].Score {
|
||||
scp = i
|
||||
lines, highlight := g.scoreLines(topTen, ins)
|
||||
g.showScores(lines, highlight)
|
||||
|
||||
break
|
||||
} else if !g.AllScore && flags != 2 &&
|
||||
topTen[i].UID == uid && topTen[i].Flags != 2 {
|
||||
// only one score per nowin uid
|
||||
scp = len(topTen)
|
||||
// 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 := g.scoreSlot(topTen, amount, flags, uid)
|
||||
if scp >= len(topTen) {
|
||||
return -1
|
||||
}
|
||||
|
||||
sc2 := len(topTen) - 1
|
||||
if flags != 2 && !g.AllScore {
|
||||
for i := scp; i < len(topTen); i++ {
|
||||
if topTen[i].UID == uid && topTen[i].Flags != 2 {
|
||||
sc2 = i
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if scp < len(topTen) {
|
||||
sc2 := len(topTen) - 1
|
||||
if flags != 2 && !g.AllScore {
|
||||
for i := scp; i < len(topTen); i++ {
|
||||
if topTen[i].UID == uid && topTen[i].Flags != 2 {
|
||||
sc2 = i
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for sc2 > scp {
|
||||
topTen[sc2] = topTen[sc2-1]
|
||||
sc2--
|
||||
}
|
||||
|
||||
lvl := g.Depth
|
||||
if flags == 2 {
|
||||
lvl = g.MaxDepth
|
||||
}
|
||||
|
||||
topTen[scp] = ScoreEnt{
|
||||
UID: uid,
|
||||
Score: amount,
|
||||
Flags: flags,
|
||||
Monster: monst,
|
||||
Name: g.Whoami,
|
||||
Level: lvl,
|
||||
Time: time.Now().Unix(),
|
||||
}
|
||||
ins = scp
|
||||
}
|
||||
}
|
||||
|
||||
// Build the list display
|
||||
for sc2 > scp {
|
||||
topTen[sc2] = topTen[sc2-1]
|
||||
sc2--
|
||||
}
|
||||
|
||||
lvl := g.Depth
|
||||
if flags == 2 {
|
||||
lvl = g.MaxDepth
|
||||
}
|
||||
|
||||
topTen[scp] = ScoreEnt{
|
||||
UID: uid,
|
||||
Score: amount,
|
||||
Flags: flags,
|
||||
Monster: monst,
|
||||
Name: g.Whoami,
|
||||
Level: lvl,
|
||||
Time: time.Now().Unix(),
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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,32 +219,35 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
if g.scr != nil && g.scr.term != nil {
|
||||
g.clear()
|
||||
return lines, highlight
|
||||
}
|
||||
|
||||
for i, line := range lines {
|
||||
if i == highlight {
|
||||
g.standout()
|
||||
}
|
||||
|
||||
g.mvaddstr(i, 0, line)
|
||||
|
||||
if i == highlight {
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
|
||||
g.refresh()
|
||||
} else {
|
||||
// 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
|
||||
}
|
||||
|
||||
// Update the list file
|
||||
if ins >= 0 {
|
||||
g.wrScore(topTen)
|
||||
g.clear()
|
||||
|
||||
for i, line := range lines {
|
||||
if i == highlight {
|
||||
g.standout()
|
||||
}
|
||||
|
||||
g.mvaddstr(i, 0, line)
|
||||
|
||||
if i == highlight {
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
// ShowScores implements the -s command line option: print the scoreboard
|
||||
|
||||
568
game/scrolls.go
568
game/scrolls.go
@@ -28,245 +28,8 @@ func (g *RogueGame) readScroll() {
|
||||
// Get rid of the thing
|
||||
g.leavePack(obj, false, false)
|
||||
|
||||
switch obj.ScrollKind() {
|
||||
case ScrollMonsterConfusion:
|
||||
// Scroll of monster confusion. Give him that power.
|
||||
p.Flags.Set(CanConfuse)
|
||||
g.msg("your hands begin to glow %s", g.pickColor("red"))
|
||||
case ScrollEnchantArmor:
|
||||
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:
|
||||
// Hold monster scroll. Stop all monsters within two spaces from
|
||||
// chasing after the hero.
|
||||
held := 0
|
||||
|
||||
for x := p.Pos.X - 2; x <= p.Pos.X+2; x++ {
|
||||
if x < 0 || x >= NumCols {
|
||||
continue
|
||||
}
|
||||
|
||||
for y := p.Pos.Y - 2; y <= p.Pos.Y+2; y++ {
|
||||
if y < 0 || y > NumLines-1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if mp := g.Level.MonsterAt(y, x); mp != nil && mp.On(Awake) {
|
||||
mp.Flags.Clear(Awake)
|
||||
mp.Flags.Set(Held)
|
||||
|
||||
held++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
case ScrollSleep:
|
||||
// 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.msg("you fall asleep")
|
||||
case ScrollCreateMonster:
|
||||
// Create a monster: first look in a circle around him, next try
|
||||
// his room, otherwise give up
|
||||
i := 0
|
||||
|
||||
var mp Coord
|
||||
|
||||
for y := p.Pos.Y - 1; y <= p.Pos.Y+1; y++ {
|
||||
for x := p.Pos.X - 1; x <= p.Pos.X+1; x++ {
|
||||
// Don't put a monster on top of the player.
|
||||
if y == p.Pos.Y && x == p.Pos.X {
|
||||
continue
|
||||
}
|
||||
// Or anything else nasty
|
||||
if ch := g.Level.VisibleChar(y, x); stepOk(ch) {
|
||||
if ch == Scroll {
|
||||
if fo := g.Level.ObjectAt(y, x); fo != nil && fo.ScrollKind() == ScrollScareMonster {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if i++; g.rnd(i) == 0 {
|
||||
mp = Coord{Y: y, X: x}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
case ScrollIdentifyPotion, ScrollIdentifyScroll, ScrollIdentifyWeapon, ScrollIdentifyArmor, ScrollIdentifyRingOrStick:
|
||||
// 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:
|
||||
// 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 {
|
||||
pp := g.Level.At(y, x)
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
case Passage:
|
||||
pass = true
|
||||
case Floor:
|
||||
if pp.Flags.Has(FReal) {
|
||||
ch = ' '
|
||||
} else {
|
||||
ch = Trap
|
||||
pp.Ch = Trap
|
||||
pp.Flags.Set(FSeen | FReal)
|
||||
}
|
||||
default:
|
||||
if pp.Flags.Has(FPassage) {
|
||||
pass = true
|
||||
} else {
|
||||
ch = ' '
|
||||
}
|
||||
}
|
||||
|
||||
if pass {
|
||||
if !pp.Flags.Has(FReal) {
|
||||
pp.Ch = Passage
|
||||
}
|
||||
|
||||
pp.Flags.Set(FSeen | FReal)
|
||||
|
||||
ch = Passage
|
||||
}
|
||||
|
||||
if ch != ' ' {
|
||||
if tp := pp.Monst; tp != nil {
|
||||
tp.OldCh = ch
|
||||
if !p.On(SenseMonsters) {
|
||||
g.mvaddch(y, x, ch)
|
||||
}
|
||||
} else {
|
||||
g.mvaddch(y, x, ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case ScrollFoodDetection:
|
||||
// Food detection
|
||||
found := false
|
||||
|
||||
g.scr.Hw.Clear()
|
||||
|
||||
for _, fo := range g.Level.Objects {
|
||||
if fo.Kind == KindFood {
|
||||
found = true
|
||||
|
||||
g.scr.Hw.MvAddCh(fo.Pos.Y, fo.Pos.X, Food)
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
g.Items.Scrolls[ScrollFoodDetection].Know = true
|
||||
g.showWin("Your nose tingles and you smell food.--More--")
|
||||
} else {
|
||||
g.msg("your nose tingles")
|
||||
}
|
||||
case ScrollTeleportation:
|
||||
// Scroll of teleportation: make him disappear and reappear
|
||||
curRoom := p.Room
|
||||
|
||||
g.teleport()
|
||||
|
||||
if curRoom != p.Room {
|
||||
g.Items.Scrolls[ScrollTeleportation].Know = true
|
||||
}
|
||||
case ScrollEnchantWeapon:
|
||||
if p.CurWeapon == nil || p.CurWeapon.Kind != KindWeapon {
|
||||
g.msg("you feel a strange sense of loss")
|
||||
} else {
|
||||
p.CurWeapon.Flags.Clear(Cursed)
|
||||
|
||||
if g.rnd(2) == 0 {
|
||||
p.CurWeapon.HPlus++
|
||||
} else {
|
||||
p.CurWeapon.DPlus++
|
||||
}
|
||||
|
||||
g.msg("your %s glows %s for a moment",
|
||||
g.Items.Weapons[p.CurWeapon.Which].Name, g.pickColor("blue"))
|
||||
}
|
||||
case ScrollScareMonster:
|
||||
// Reading it is a mistake and produces laughter at her poor boo
|
||||
// boo.
|
||||
g.msg("you hear maniacal laughter in the distance")
|
||||
case ScrollRemoveCurse:
|
||||
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:
|
||||
// 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:
|
||||
if p.CurArmor != nil {
|
||||
p.CurArmor.Flags.Set(Protected)
|
||||
g.msg("your armor is covered by a shimmering %s shield",
|
||||
g.pickColor("gold"))
|
||||
} else {
|
||||
g.msg("you feel a strange sense of loss")
|
||||
}
|
||||
if h := g.data.readHandlers[obj.ScrollKind()]; h != nil {
|
||||
h(g, obj)
|
||||
}
|
||||
|
||||
g.look(true) // put the result of the scroll on the screen
|
||||
@@ -275,6 +38,333 @@ func (g *RogueGame) readScroll() {
|
||||
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.
|
||||
g.Player.Flags.Set(CanConfuse)
|
||||
g.msg("your hands begin to glow %s", g.pickColor("red"))
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
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++ {
|
||||
if x < 0 || x >= NumCols {
|
||||
continue
|
||||
}
|
||||
|
||||
for y := p.Pos.Y - 2; y <= p.Pos.Y+2; y++ {
|
||||
if y < 0 || y > NumLines-1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if mp := g.Level.MonsterAt(y, x); mp != nil && mp.On(Awake) {
|
||||
mp.Flags.Clear(Awake)
|
||||
mp.Flags.Set(Held)
|
||||
|
||||
held++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return held
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
g.Player.Flags.Clear(Awake)
|
||||
g.msg("you fall asleep")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
for y := p.Pos.Y - 1; y <= p.Pos.Y+1; y++ {
|
||||
for x := p.Pos.X - 1; x <= p.Pos.X+1; x++ {
|
||||
// Don't put a monster on top of the player.
|
||||
if y == p.Pos.Y && x == p.Pos.X {
|
||||
continue
|
||||
}
|
||||
// Or anything else nasty
|
||||
if ch := g.Level.VisibleChar(y, x); stepOk(ch) {
|
||||
if ch == Scroll {
|
||||
if fo := g.Level.ObjectAt(y, x); fo != nil && fo.ScrollKind() == ScrollScareMonster {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if i++; g.rnd(i) == 0 {
|
||||
mp = Coord{Y: y, X: x}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mp, i != 0
|
||||
}
|
||||
|
||||
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()])
|
||||
}
|
||||
|
||||
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 '-', '|':
|
||||
ch = revealWall(pp)
|
||||
case ' ':
|
||||
pass = revealSolid(pp)
|
||||
case Passage:
|
||||
pass = true
|
||||
case Floor:
|
||||
ch = revealFloor(pp)
|
||||
default:
|
||||
if pp.Flags.Has(FPassage) {
|
||||
pass = true
|
||||
} else {
|
||||
ch = ' '
|
||||
}
|
||||
}
|
||||
|
||||
if pass {
|
||||
if !pp.Flags.Has(FReal) {
|
||||
pp.Ch = Passage
|
||||
}
|
||||
|
||||
pp.Flags.Set(FSeen | FReal)
|
||||
|
||||
ch = Passage
|
||||
}
|
||||
|
||||
return 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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
g.scr.Hw.Clear()
|
||||
|
||||
for _, fo := range g.Level.Objects {
|
||||
if fo.Kind == KindFood {
|
||||
found = true
|
||||
|
||||
g.scr.Hw.MvAddCh(fo.Pos.Y, fo.Pos.X, Food)
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
g.Items.Scrolls[ScrollFoodDetection].Know = true
|
||||
g.showWin("Your nose tingles and you smell food.--More--")
|
||||
} else {
|
||||
g.msg("your nose tingles")
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) readTeleportation(*Object) {
|
||||
// Scroll of teleportation: make him disappear and reappear
|
||||
p := &g.Player
|
||||
curRoom := p.Room
|
||||
|
||||
g.teleport()
|
||||
|
||||
if curRoom != p.Room {
|
||||
g.Items.Scrolls[ScrollTeleportation].Know = true
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
p.CurWeapon.Flags.Clear(Cursed)
|
||||
|
||||
if g.rnd(2) == 0 {
|
||||
p.CurWeapon.HPlus++
|
||||
} else {
|
||||
p.CurWeapon.DPlus++
|
||||
}
|
||||
|
||||
g.msg("your %s glows %s for a moment",
|
||||
g.Items.Weapons[p.CurWeapon.Which].Name, g.pickColor("blue"))
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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",
|
||||
g.pickColor("gold"))
|
||||
} else {
|
||||
g.msg("you feel a strange sense of loss")
|
||||
}
|
||||
}
|
||||
|
||||
// uncurse uncurses an item (scrolls.c uncurse).
|
||||
func uncurse(obj *Object) {
|
||||
if obj != nil {
|
||||
|
||||
620
game/sticks.go
620
game/sticks.go
@@ -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,182 +30,263 @@ func (g *RogueGame) doZap() {
|
||||
return
|
||||
}
|
||||
|
||||
switch obj.WandKind() {
|
||||
case WandLight:
|
||||
// Reddy Kilowatt wand. Light up the room
|
||||
g.Items.Sticks[WandLight].Know = true
|
||||
if p.Room.Flags.Has(Gone) {
|
||||
g.msg("the corridor glows and then fades")
|
||||
} else {
|
||||
p.Room.Flags.Clear(Dark)
|
||||
// Light the room and put the player back up
|
||||
g.enterRoom(p.Pos)
|
||||
g.addmsgf("the room is lit")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" by a shimmering %s light", g.pickColor("blue"))
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
if h := g.data.zapHandlers[obj.WandKind()]; h != nil {
|
||||
if !h(g, obj) {
|
||||
return // the zap aborted; no charge is used
|
||||
}
|
||||
case WandDrainLife:
|
||||
// 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 {
|
||||
g.msg("you are too weak to use it")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if tp := g.Level.MonsterAt(y, x); tp != nil {
|
||||
monster := tp.Type
|
||||
if monster == 'F' {
|
||||
p.Flags.Clear(Held)
|
||||
}
|
||||
|
||||
switch obj.WandKind() {
|
||||
case WandInvisibility:
|
||||
tp.Flags.Set(Invisible)
|
||||
|
||||
if g.canSee(y, x) {
|
||||
g.mvaddch(y, x, tp.OldCh)
|
||||
}
|
||||
case WandPolymorph:
|
||||
pp := tp.Pack
|
||||
g.Level.RemoveMonster(tp)
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, g.Level.Char(y, x))
|
||||
}
|
||||
|
||||
oldch := tp.OldCh
|
||||
g.Delta.Y = y
|
||||
g.Delta.X = x
|
||||
monster = g.randomMonsterLetter()
|
||||
g.newMonster(tp, monster, g.Delta)
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, monster)
|
||||
}
|
||||
|
||||
tp.OldCh = oldch
|
||||
|
||||
tp.Pack = pp
|
||||
if g.seeMonst(tp) {
|
||||
g.Items.Sticks[WandPolymorph].Know = true
|
||||
}
|
||||
case WandCancellation:
|
||||
tp.Flags.Set(Cancelled)
|
||||
tp.Flags.Clear(Invisible | CanConfuse)
|
||||
|
||||
tp.Disguise = tp.Type
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, tp.Disguise)
|
||||
}
|
||||
case WandTeleportAway, WandTeleportTo:
|
||||
var newPos Coord
|
||||
|
||||
if obj.WandKind() == WandTeleportAway {
|
||||
for {
|
||||
newPos, _ = g.findFloor(true)
|
||||
if newPos != p.Pos {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newPos.Y = p.Pos.Y + g.Delta.Y
|
||||
newPos.X = p.Pos.X + g.Delta.X
|
||||
}
|
||||
|
||||
tp.Dest = &p.Pos
|
||||
tp.Flags.Set(Awake)
|
||||
g.relocate(tp, newPos)
|
||||
}
|
||||
}
|
||||
case WandMagicMissile:
|
||||
g.Items.Sticks[WandMagicMissile].Know = true
|
||||
bolt := newObject()
|
||||
bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon
|
||||
bolt.HurlDmg = dice("1x4")
|
||||
bolt.HPlus = 100
|
||||
bolt.DPlus = 1
|
||||
|
||||
bolt.Flags = Missile
|
||||
if p.CurWeapon != nil {
|
||||
bolt.Launch = WeaponKind(p.CurWeapon.Which)
|
||||
}
|
||||
|
||||
g.doMotion(bolt, g.Delta.Y, g.Delta.X)
|
||||
|
||||
if tp := g.Level.MonsterAt(bolt.Pos.Y, bolt.Pos.X); tp != nil &&
|
||||
!g.saveThrow(VsMagic, &tp.Stats) {
|
||||
g.hitMonster(bolt.Pos, bolt)
|
||||
} else if g.Options.Terse {
|
||||
g.msg("missle vanishes") //nolint:misspell // C's spelling, kept faithfully
|
||||
} 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
|
||||
}
|
||||
|
||||
if tp := g.Level.MonsterAt(y, x); tp != nil {
|
||||
if obj.WandKind() == WandHasteMonster {
|
||||
if tp.On(Slowed) {
|
||||
tp.Flags.Clear(Slowed)
|
||||
} else {
|
||||
tp.Flags.Set(Hasted)
|
||||
}
|
||||
} else {
|
||||
if tp.On(Hasted) {
|
||||
tp.Flags.Clear(Hasted)
|
||||
} else {
|
||||
tp.Flags.Set(Slowed)
|
||||
}
|
||||
|
||||
tp.Turn = true
|
||||
}
|
||||
|
||||
g.Delta.Y = y
|
||||
g.Delta.X = x
|
||||
g.runTo(g.Delta)
|
||||
}
|
||||
case WandLightning, WandFire, WandCold:
|
||||
var name string
|
||||
|
||||
switch obj.WandKind() {
|
||||
case WandLightning:
|
||||
name = "bolt"
|
||||
case WandFire:
|
||||
name = "flame"
|
||||
default:
|
||||
name = "ice"
|
||||
}
|
||||
|
||||
g.fireBolt(p.Pos, &g.Delta, name)
|
||||
g.Items.Sticks[obj.Which].Know = true
|
||||
case WandNothing:
|
||||
}
|
||||
|
||||
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")
|
||||
} else {
|
||||
p.Room.Flags.Clear(Dark)
|
||||
// Light the room and put the player back up
|
||||
g.enterRoom(p.Pos)
|
||||
g.addmsgf("the room is lit")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" by a shimmering %s light", g.pickColor("blue"))
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
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 g.Player.Stats.HP < 2 {
|
||||
g.msg("you are too weak to use it")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
g.drain()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapInvisibility(*Object) bool {
|
||||
if tp := g.zapVictim(); tp != nil {
|
||||
tp.Flags.Set(Invisible)
|
||||
|
||||
if g.canSee(tp.Pos.Y, tp.Pos.X) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.OldCh)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, g.Level.Char(y, x))
|
||||
}
|
||||
|
||||
oldch := tp.OldCh
|
||||
g.Delta.Y = y
|
||||
g.Delta.X = x
|
||||
monster := g.randomMonsterLetter()
|
||||
g.newMonster(tp, monster, g.Delta)
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, monster)
|
||||
}
|
||||
|
||||
tp.OldCh = oldch
|
||||
|
||||
tp.Pack = pp
|
||||
if g.seeMonst(tp) {
|
||||
g.Items.Sticks[WandPolymorph].Know = true
|
||||
}
|
||||
|
||||
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(tp.Pos.Y, tp.Pos.X, tp.Disguise)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
for {
|
||||
newPos, _ = g.findFloor(true)
|
||||
if newPos != p.Pos {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newPos.Y = p.Pos.Y + g.Delta.Y
|
||||
newPos.X = p.Pos.X + g.Delta.X
|
||||
}
|
||||
|
||||
tp.Dest = &p.Pos
|
||||
tp.Flags.Set(Awake)
|
||||
g.relocate(tp, newPos)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
bolt.HurlDmg = dice("1x4")
|
||||
bolt.HPlus = 100
|
||||
bolt.DPlus = 1
|
||||
|
||||
bolt.Flags = Missile
|
||||
if p.CurWeapon != nil {
|
||||
bolt.Launch = WeaponKind(p.CurWeapon.Which)
|
||||
}
|
||||
|
||||
g.doMotion(bolt, g.Delta.Y, g.Delta.X)
|
||||
|
||||
if tp := g.Level.MonsterAt(bolt.Pos.Y, bolt.Pos.X); tp != nil &&
|
||||
!g.saveThrow(VsMagic, &tp.Stats) {
|
||||
g.hitMonster(bolt.Pos, bolt)
|
||||
} else if g.Options.Terse {
|
||||
g.msg("missle vanishes") //nolint:misspell // C's spelling, kept faithfully
|
||||
} else {
|
||||
g.msg("the missle vanishes with a puff of smoke") //nolint:misspell // C's spelling
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapSpeed(obj *Object) bool {
|
||||
tp := g.zapRayMonster()
|
||||
if tp == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
tp.Flags.Set(Slowed)
|
||||
}
|
||||
|
||||
tp.Turn = true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapBolt(obj *Object) bool {
|
||||
var name string
|
||||
|
||||
switch obj.WandKind() {
|
||||
case WandLightning:
|
||||
name = "bolt"
|
||||
case WandFire:
|
||||
name = "flame"
|
||||
default:
|
||||
name = "ice"
|
||||
}
|
||||
|
||||
g.fireBolt(g.Player.Pos, &g.Delta, name)
|
||||
g.Items.Sticks[obj.Which].Know = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// drain does the drain-hit-points-from-player schtick (sticks.c drain).
|
||||
func (g *RogueGame) drain() {
|
||||
p := &g.Player
|
||||
@@ -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,57 +382,11 @@ 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
|
||||
|
||||
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")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" off the dragon")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
} else {
|
||||
g.hitMonster(pos, bolt)
|
||||
}
|
||||
} else if ch != 'M' || tp.Disguise == 'M' {
|
||||
if fromHero {
|
||||
g.runTo(pos)
|
||||
}
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("%s misses", name)
|
||||
} else {
|
||||
g.msg("the %s whizzes past %s", name, g.setMname(tp))
|
||||
}
|
||||
}
|
||||
used = g.boltStrikesMonster(tp, bolt, pos, ch, name, fromHero)
|
||||
} else if hitHero && pos == p.Pos {
|
||||
hitHero = false
|
||||
changed = !changed
|
||||
|
||||
if !g.save(VsMagic) {
|
||||
if p.Stats.HP -= g.roll(6, 6); p.Stats.HP <= 0 {
|
||||
if fromHero {
|
||||
g.death('b')
|
||||
} else {
|
||||
g.death(g.Level.MonsterAt(start.Y, start.X).Type)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
used = g.boltStrikesHero(start, name, fromHero)
|
||||
}
|
||||
|
||||
g.mvaddch(pos.Y, pos.X, dirch)
|
||||
@@ -382,6 +398,104 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
if tp.Type == 'D' && name == "flame" {
|
||||
g.addmsgf("the flame bounces")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" off the dragon")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
} else {
|
||||
g.hitMonster(pos, bolt)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if ch != 'M' || tp.Disguise == 'M' {
|
||||
if fromHero {
|
||||
g.runTo(pos)
|
||||
}
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("%s misses", name)
|
||||
} else {
|
||||
g.msg("the %s whizzes past %s", name, g.setMname(tp))
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
} else {
|
||||
g.death(g.Level.MonsterAt(start.Y, start.X).Type)
|
||||
}
|
||||
}
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("the %s hits", name)
|
||||
} else {
|
||||
g.msg("you are hit by the %s", name)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// fixStick sets up a new wand or staff (sticks.c fix_stick).
|
||||
func (g *RogueGame) fixStick(cur *Object) {
|
||||
if g.Items.WandType[cur.Which] == staffName {
|
||||
|
||||
261
game/tables.go
261
game/tables.go
@@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
491
game/things.go
491
game/things.go
@@ -23,107 +23,147 @@ 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:
|
||||
if obj.Count == 1 {
|
||||
pb.WriteString("A scroll ")
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "%d scrolls ", obj.Count)
|
||||
}
|
||||
|
||||
op := &it.Scrolls[which]
|
||||
|
||||
switch {
|
||||
case op.Know:
|
||||
fmt.Fprintf(&pb, "of %s", op.Name)
|
||||
case op.Guess != "":
|
||||
fmt.Fprintf(&pb, "called %s", op.Guess)
|
||||
default:
|
||||
fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which])
|
||||
}
|
||||
g.nameScroll(&pb, obj)
|
||||
case KindFood:
|
||||
if which == 1 {
|
||||
if obj.Count == 1 {
|
||||
fmt.Fprintf(&pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "%d %ss", obj.Count, g.Fruit)
|
||||
}
|
||||
} else {
|
||||
if obj.Count == 1 {
|
||||
pb.WriteString("Some food")
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "%d rations of food", obj.Count)
|
||||
}
|
||||
}
|
||||
g.nameFood(&pb, obj)
|
||||
case KindWeapon:
|
||||
sp := it.Weapons[which].Name
|
||||
|
||||
if obj.Count > 1 {
|
||||
fmt.Fprintf(&pb, "%d ", obj.Count)
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "A%s ", vowelstr(sp))
|
||||
}
|
||||
|
||||
if obj.Flags.Has(Known) {
|
||||
fmt.Fprintf(&pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
|
||||
} else {
|
||||
pb.WriteString(sp)
|
||||
}
|
||||
|
||||
if obj.Count > 1 {
|
||||
pb.WriteString("s")
|
||||
}
|
||||
|
||||
if obj.Label != "" {
|
||||
fmt.Fprintf(&pb, " called %s", obj.Label)
|
||||
}
|
||||
g.nameWeapon(&pb, obj)
|
||||
case KindArmor:
|
||||
sp := it.Armors[which].Name
|
||||
if obj.Flags.Has(Known) {
|
||||
fmt.Fprintf(&pb, "%s %s [", num(g.data.aClass[which]-obj.ArmorClass, 0, Armor), sp)
|
||||
|
||||
if !g.Options.Terse {
|
||||
pb.WriteString("protection ")
|
||||
}
|
||||
|
||||
fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass)
|
||||
} else {
|
||||
pb.WriteString(sp)
|
||||
}
|
||||
|
||||
if obj.Label != "" {
|
||||
fmt.Fprintf(&pb, " called %s", obj.Label)
|
||||
}
|
||||
g.nameArmor(&pb, obj)
|
||||
case KindAmulet:
|
||||
pb.WriteString("The Amulet of Yendor")
|
||||
case KindGold:
|
||||
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue)
|
||||
}
|
||||
|
||||
out := pb.String()
|
||||
return fixNameCase(g.describeWorn(obj, pb.String()), drop)
|
||||
}
|
||||
|
||||
if g.InvDescribe {
|
||||
p := &g.Player
|
||||
if obj == p.CurArmor {
|
||||
out += " (being worn)"
|
||||
}
|
||||
|
||||
if obj == p.CurWeapon {
|
||||
out += " (weapon in hand)"
|
||||
}
|
||||
|
||||
switch obj {
|
||||
case p.CurRing[Left]:
|
||||
out += " (on left hand)"
|
||||
case p.CurRing[Right]:
|
||||
out += " (on right hand)"
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
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:]
|
||||
op := &g.Items.Scrolls[obj.Which]
|
||||
|
||||
switch {
|
||||
case op.Know:
|
||||
fmt.Fprintf(pb, "of %s", op.Name)
|
||||
case op.Guess != "":
|
||||
fmt.Fprintf(pb, "called %s", op.Guess)
|
||||
default:
|
||||
fmt.Fprintf(pb, "titled '%s'", g.Items.ScrNames[obj.Which])
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "%d %ss", obj.Count, g.Fruit)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Count == 1 {
|
||||
pb.WriteString("Some food")
|
||||
} else {
|
||||
fmt.Fprintf(pb, "%d rations of food", obj.Count)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "A%s ", vowelstr(sp))
|
||||
}
|
||||
|
||||
if obj.Flags.Has(Known) {
|
||||
fmt.Fprintf(pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
|
||||
} else {
|
||||
pb.WriteString(sp)
|
||||
}
|
||||
|
||||
if obj.Count > 1 {
|
||||
pb.WriteString("s")
|
||||
}
|
||||
|
||||
if obj.Label != "" {
|
||||
fmt.Fprintf(pb, " called %s", obj.Label)
|
||||
}
|
||||
}
|
||||
|
||||
// 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[obj.Which]-obj.ArmorClass, 0, Armor), sp)
|
||||
|
||||
if !g.Options.Terse {
|
||||
pb.WriteString("protection ")
|
||||
}
|
||||
|
||||
fmt.Fprintf(pb, "%d]", 10-obj.ArmorClass)
|
||||
} else {
|
||||
pb.WriteString(sp)
|
||||
}
|
||||
|
||||
if obj.Label != "" {
|
||||
fmt.Fprintf(pb, " called %s", obj.Label)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
if obj == p.CurArmor {
|
||||
out += " (being worn)"
|
||||
}
|
||||
|
||||
if obj == p.CurWeapon {
|
||||
out += " (weapon in hand)"
|
||||
}
|
||||
|
||||
switch obj {
|
||||
case p.CurRing[Left]:
|
||||
out += " (on left hand)"
|
||||
case p.CurRing[Right]:
|
||||
out += " (on right hand)"
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// 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,25 +232,33 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
|
||||
|
||||
p.CurArmor = nil
|
||||
default:
|
||||
hand := Right
|
||||
if obj == p.CurRing[Left] {
|
||||
hand = Left
|
||||
}
|
||||
|
||||
p.CurRing[hand] = nil
|
||||
|
||||
switch obj.RingKind() {
|
||||
case RingAddStrength:
|
||||
g.changeStrength(-obj.Bonus)
|
||||
case RingSeeInvisible:
|
||||
g.unsee(0)
|
||||
g.Extinguish(DUnsee)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
p.CurRing[hand] = nil
|
||||
|
||||
switch obj.RingKind() {
|
||||
case RingAddStrength:
|
||||
g.changeStrength(-obj.Bonus)
|
||||
case RingSeeInvisible:
|
||||
g.unsee(0)
|
||||
g.Extinguish(DUnsee)
|
||||
}
|
||||
}
|
||||
|
||||
// newThing returns a new random thing for the dungeon (things.c new_thing).
|
||||
func (g *RogueGame) newThing() *Object {
|
||||
cur := newObject()
|
||||
@@ -236,47 +284,13 @@ func (g *RogueGame) newThing() *Object {
|
||||
cur.Kind = KindScroll
|
||||
cur.Which = pickOne(g, g.Items.Scrolls[:])
|
||||
case 2:
|
||||
cur.Kind = KindFood
|
||||
|
||||
g.Player.NoFood = 0
|
||||
if g.rnd(10) != 0 {
|
||||
cur.Which = 0
|
||||
} else {
|
||||
cur.Which = 1
|
||||
}
|
||||
g.newFoodThing(cur)
|
||||
case 3:
|
||||
g.initWeapon(cur, WeaponKind(pickOne(g, g.Items.Weapons[:NumWeaponTypes])))
|
||||
|
||||
if r := g.rnd(100); r < 10 {
|
||||
cur.Flags.Set(Cursed)
|
||||
cur.HPlus -= g.rnd(3) + 1
|
||||
} else if r < 15 {
|
||||
cur.HPlus += g.rnd(3) + 1
|
||||
}
|
||||
g.newWeaponThing(cur)
|
||||
case 4:
|
||||
cur.Kind = KindArmor
|
||||
cur.Which = pickOne(g, g.Items.Armors[:])
|
||||
|
||||
cur.ArmorClass = g.data.aClass[cur.Which]
|
||||
if r := g.rnd(100); r < 20 {
|
||||
cur.Flags.Set(Cursed)
|
||||
cur.ArmorClass += g.rnd(3) + 1
|
||||
} else if r < 28 {
|
||||
cur.ArmorClass -= g.rnd(3) + 1
|
||||
}
|
||||
g.newArmorThing(cur)
|
||||
case 5:
|
||||
cur.Kind = KindRing
|
||||
|
||||
cur.Which = pickOne(g, g.Items.Rings[:])
|
||||
switch cur.RingKind() {
|
||||
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
|
||||
if cur.Bonus = g.rnd(3); cur.Bonus == 0 {
|
||||
cur.Bonus = -1
|
||||
cur.Flags.Set(Cursed)
|
||||
}
|
||||
case RingAggravateMonsters, RingTeleportation:
|
||||
cur.Flags.Set(Cursed)
|
||||
}
|
||||
g.newRingThing(cur)
|
||||
case 6:
|
||||
cur.Kind = KindWand
|
||||
cur.Which = pickOne(g, g.Items.Sticks[:])
|
||||
@@ -286,6 +300,63 @@ func (g *RogueGame) newThing() *Object {
|
||||
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
|
||||
if g.rnd(10) != 0 {
|
||||
cur.Which = 0
|
||||
} else {
|
||||
cur.Which = 1
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
cur.Flags.Set(Cursed)
|
||||
cur.HPlus -= g.rnd(3) + 1
|
||||
} else if r < 15 {
|
||||
cur.HPlus += g.rnd(3) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// 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[:])
|
||||
|
||||
cur.ArmorClass = g.data.aClass[cur.Which]
|
||||
if r := g.rnd(100); r < 20 {
|
||||
cur.Flags.Set(Cursed)
|
||||
cur.ArmorClass += g.rnd(3) + 1
|
||||
} else if r < 28 {
|
||||
cur.ArmorClass -= g.rnd(3) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// 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[:])
|
||||
switch cur.RingKind() {
|
||||
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
|
||||
if cur.Bonus = g.rnd(3); cur.Bonus == 0 {
|
||||
cur.Bonus = -1
|
||||
cur.Flags.Set(Cursed)
|
||||
}
|
||||
case RingAggravateMonsters, RingTeleportation:
|
||||
cur.Flags.Set(Cursed)
|
||||
}
|
||||
}
|
||||
|
||||
// pickOne picks an item out of a list of possible objects using their
|
||||
// cumulative probabilities (things.c pick_one).
|
||||
func pickOne(g *RogueGame, info []ObjInfo) int {
|
||||
@@ -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,69 +510,100 @@ func (g *RogueGame) addLine(format string, a ...any) int {
|
||||
}
|
||||
|
||||
if g.Options.InvType == InvSlow {
|
||||
if !isFlush && line != "" {
|
||||
if g.msg("%s", line) == Escape {
|
||||
return Escape
|
||||
}
|
||||
}
|
||||
return g.addLineSlow(line, isFlush)
|
||||
}
|
||||
|
||||
pg.lineCnt++
|
||||
} else {
|
||||
if !pg.init {
|
||||
pg.maxlen = len(prompt)
|
||||
pg.init = true
|
||||
}
|
||||
g.addLinePaged(line, isFlush)
|
||||
|
||||
if pg.lineCnt >= NumLines-1 || isFlush {
|
||||
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.msg("")
|
||||
g.refresh()
|
||||
return ^Escape
|
||||
}
|
||||
|
||||
saved := NewWindow(NumLines, NumCols)
|
||||
saved.CopyFrom(g.scr.Std)
|
||||
|
||||
lx := NumCols - pg.maxlen - 2
|
||||
for y := 0; y <= pg.lineCnt; y++ {
|
||||
for x := 0; x <= pg.maxlen; x++ {
|
||||
g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x))
|
||||
}
|
||||
}
|
||||
|
||||
g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt)
|
||||
g.refresh()
|
||||
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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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()
|
||||
|
||||
saved := NewWindow(NumLines, NumCols)
|
||||
saved.CopyFrom(g.scr.Std)
|
||||
|
||||
lx := NumCols - pg.maxlen - 2
|
||||
for y := 0; y <= pg.lineCnt; y++ {
|
||||
for x := 0; x <= pg.maxlen; x++ {
|
||||
g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x))
|
||||
}
|
||||
}
|
||||
|
||||
g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt)
|
||||
g.refresh()
|
||||
g.waitFor(' ')
|
||||
g.scr.Std.CopyFrom(saved)
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
// flushLine is add_line(NULL): force out the accumulated page.
|
||||
func (g *RogueGame) flushLine() int { return g.addLine(flushSentinel) }
|
||||
|
||||
|
||||
@@ -35,34 +35,38 @@ 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 {
|
||||
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
|
||||
if ch == Floor && !g.showFloor() {
|
||||
ch = ' '
|
||||
}
|
||||
|
||||
g.mvaddch(obj.Pos.Y, obj.Pos.X, ch)
|
||||
}
|
||||
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 {
|
||||
// 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
|
||||
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()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = ' '
|
||||
}
|
||||
|
||||
g.mvaddch(obj.Pos.Y, obj.Pos.X, ch)
|
||||
}
|
||||
|
||||
// fall drops an item someplace around here (weapons.c fall).
|
||||
func (g *RogueGame) fall(obj *Object, pr bool) {
|
||||
if fpos, ok := g.fallpos(obj.Pos); ok {
|
||||
|
||||
161
game/wizard.go
161
game/wizard.go
@@ -27,50 +27,9 @@ func (g *RogueGame) createObj() {
|
||||
|
||||
switch obj.Kind {
|
||||
case KindWeapon, KindArmor:
|
||||
g.msg("blessing? (+,-,n)")
|
||||
bless := g.readchar()
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
if bless == '-' {
|
||||
obj.Flags.Set(Cursed)
|
||||
}
|
||||
|
||||
if obj.Kind == KindWeapon {
|
||||
g.initWeapon(obj, WeaponKind(obj.Which))
|
||||
|
||||
if bless == '-' {
|
||||
obj.HPlus -= g.rnd(3) + 1
|
||||
}
|
||||
|
||||
if bless == '+' {
|
||||
obj.HPlus += g.rnd(3) + 1
|
||||
}
|
||||
} else {
|
||||
obj.ArmorClass = g.data.aClass[obj.Which]
|
||||
if bless == '-' {
|
||||
obj.ArmorClass += g.rnd(3) + 1
|
||||
}
|
||||
|
||||
if bless == '+' {
|
||||
obj.ArmorClass -= g.rnd(3) + 1
|
||||
}
|
||||
}
|
||||
g.createWeaponArmor(obj)
|
||||
case KindRing:
|
||||
switch obj.RingKind() {
|
||||
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
|
||||
g.msg("blessing? (+,-,n)")
|
||||
bless := g.readchar()
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
if bless == '-' {
|
||||
obj.Flags.Set(Cursed)
|
||||
obj.Bonus = -1
|
||||
} else {
|
||||
obj.Bonus = g.rnd(2) + 1
|
||||
}
|
||||
case RingAggravateMonsters, RingTeleportation:
|
||||
obj.Flags.Set(Cursed)
|
||||
}
|
||||
g.createRing(obj)
|
||||
case KindWand:
|
||||
g.fixStick(obj)
|
||||
case KindGold:
|
||||
@@ -85,6 +44,61 @@ func (g *RogueGame) createObj() {
|
||||
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
|
||||
|
||||
if bless == '-' {
|
||||
obj.Flags.Set(Cursed)
|
||||
}
|
||||
|
||||
if obj.Kind == KindWeapon {
|
||||
g.initWeapon(obj, WeaponKind(obj.Which))
|
||||
|
||||
if bless == '-' {
|
||||
obj.HPlus -= g.rnd(3) + 1
|
||||
}
|
||||
|
||||
if bless == '+' {
|
||||
obj.HPlus += g.rnd(3) + 1
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
obj.ArmorClass = g.data.aClass[obj.Which]
|
||||
if bless == '-' {
|
||||
obj.ArmorClass += g.rnd(3) + 1
|
||||
}
|
||||
|
||||
if bless == '+' {
|
||||
obj.ArmorClass -= g.rnd(3) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// 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)")
|
||||
bless := g.readchar()
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
if bless == '-' {
|
||||
obj.Flags.Set(Cursed)
|
||||
obj.Bonus = -1
|
||||
} else {
|
||||
obj.Bonus = g.rnd(2) + 1
|
||||
}
|
||||
case RingAggravateMonsters, RingTeleportation:
|
||||
obj.Flags.Set(Cursed)
|
||||
}
|
||||
}
|
||||
|
||||
// showMap prints out the whole map for the wizard (wizard.c show_map).
|
||||
func (g *RogueGame) showMap() {
|
||||
hw := g.scr.Hw
|
||||
@@ -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) {
|
||||
|
||||
114
term/tcell.go
114
term/tcell.go
@@ -89,48 +89,88 @@ 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 ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ {
|
||||
return byte(ev.Key()) //nolint:gosec // G115: 1..26 fits
|
||||
}
|
||||
|
||||
if r := ev.Rune(); r > 0 && r < 0x80 {
|
||||
return byte(r)
|
||||
}
|
||||
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()), true //nolint:gosec // G115: 1..26 fits
|
||||
}
|
||||
|
||||
if r := ev.Rune(); r > 0 && r < 0x80 {
|
||||
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
|
||||
// shell + md_shellescape).
|
||||
func (t *Tcell) ShellEscape() {
|
||||
|
||||
Reference in New Issue
Block a user