Compare commits

33 Commits

Author SHA1 Message Date
88f18fc635 Merge refactor/effects-dispatch (refactor step 7) 2026-07-22 22:18:11 +07:00
ad098f9d99 Rotate TODO to step 8; note complexity linters clean (step 7 done)
Refactor step 7 is complete: the whole golangci-lint run is 0 issues.
MEMORY.md updated to reflect the complexity linters are enabled and
clean.
2026-07-22 22:18:11 +07:00
5b7e258195 Decompose test functions to clear complexity findings (step 7)
TestSaveRestoreRoundTrip, TestNewGameRandomizesAppearances, and
TestNewLevelInvariants split their assertion blocks into t.Helper()
sub-checks. The lint run is now completely clean (0 issues).
2026-07-22 22:11:10 +07:00
5c14a829aa Decompose tcell ReadChar key translation (refactor step 7)
ReadChar's key switch moves into translateKey with namedKey split into
motionKey/editingKey halves. term is complexity-clean. Behavior
unchanged.
2026-07-22 22:08:59 +07:00
b68836dde0 Decompose wizard.go createObj and whatis (refactor step 7)
createObj gains createWeaponArmor/createRing; whatis gains whatisPick
for its prompt loop. wizard.go is complexity-clean. Behavior unchanged.
2026-07-22 22:07:31 +07:00
730d91d160 Decompose ObjectKind.String and doMotion (refactor step 7)
String's tail moves into stringRest; doMotion's erase step moves into
eraseFlight and the inverted loop drops a nesting level. Behavior
unchanged.
2026-07-22 22:06:48 +07:00
71713d68b7 Decompose io.go End and status (refactor step 7)
End's --More-- handling moves into promptMore; status's redraw-skip
check moves into statusUnchanged. io.go is complexity-clean. Behavior
unchanged.
2026-07-22 22:05:06 +07:00
444bc30f2c Decompose ringOn and totalWinner (refactor step 7)
ringOn gains pickRingHand and chooseTerse messages; totalWinner's
appraisal switch becomes objectWorth with loreWorth/ringWorth/
wandWorth. Behavior unchanged.
2026-07-22 22:03:31 +07:00
ff7ee95395 Finish score/wakeMonster tidy (scoreSlot; drop unused return) 2026-07-22 22:02:02 +07:00
8895db530d Decompose score and wakeMonster (refactor step 7)
score splits into scoreInsert/scoreLines/showScores; wakeMonster gains
meanWakes/medusaCatches/medusaGaze predicates and effect. Behavior and
RNG call order unchanged.
2026-07-22 22:01:03 +07:00
3e1c30c787 Flatten digWallGap 2026-07-22 21:59:44 +07:00
432ea4f019 Decompose rooms.go (refactor step 7)
digRooms splits into digRoom/placeGoneRoom/placeMazeRoom/
placeNormalRoom/roomGold/roomMonster; dig gains digPick/digWallGap;
findFloorImpl gains floorChar; enterRoom and leaveRoom gain per-cell
helpers. rooms.go is complexity-clean. Behavior and RNG call order
unchanged.
2026-07-22 21:59:24 +07:00
bac9e361bc Decompose daemons.go; daemon dispatch becomes a table (step 7)
runDaemon's switch becomes gameData.daemonHandlers (the C d_func
function pointers restored as method expressions); stomach splits into
stomachFaint/stomachDigest; visuals gains visualMonsters. daemons.go
is complexity-clean. Behavior and RNG call order unchanged.
2026-07-22 21:57:22 +07:00
9083967ed3 Split applyHeader turn-state half; drop inline stat err 2026-07-22 21:55:04 +07:00
80484bcd31 Finish save.go split: header/player halves, drop inline err 2026-07-22 21:54:41 +07:00
43b4fbe746 Decompose save.go (refactor step 7)
saveGame splits into askDefaultSave/saveFileName/saveCheckOverwrite/
askOverwrite around a saveAnswer tri-state; snapshot gains destRefFor;
applySnapshot gains applyMonsters/applyDests. save.go is
complexity-clean. Behavior unchanged.
2026-07-22 21:53:02 +07:00
389db14bbf Finish addLine split (addLinePageBreak) 2026-07-22 21:51:06 +07:00
e1f065e783 Decompose things.go (refactor step 7)
inventoryName splits into nameScroll/nameFood/nameWeapon/nameArmor/
describeWorn/fixNameCase; newThing gains newFoodThing/newWeaponThing/
newArmorThing/newRingThing; dropCheck gains dropRing; addLine splits
into addLineSlow/addLinePaged/addLineOverlay. things.go is
complexity-clean. Behavior and RNG call order unchanged.
2026-07-22 21:50:44 +07:00
0b798c9c82 Tidy pack.go split: drop unused lp param and named returns 2026-07-07 03:31:53 +02:00
0274460e62 Decompose pack.go (refactor step 7)
addPack splits into pickupScareScroll and packInsert with
packScanKind/packScanWhich/packMatch/packMatchGroup for the C
linked-list walk; promptPackItem gains repeatLastItem and
promptItemPurpose; inventory's empty-handed messages flatten via
chooseTerse. pack.go is complexity-clean. Behavior unchanged.
2026-07-07 03:31:23 +02:00
c6dae3cf3d Finish getStr split (getStrResult) 2026-07-07 03:28:30 +02:00
5849dddcf0 Decompose options.go (refactor step 7)
ParseOpts splits into parseOptName/parseOptValue/parseInvType; getStr
gains endsInput/getStrErase/getStrEdit. options.go is complexity-clean.
Behavior unchanged.
2026-07-07 03:27:56 +02:00
cc2efb86e8 Decompose passages.go (refactor step 7)
digPassages gains pickNeighbor; connectRooms splits into
connOrient/connPlanDown/connPlanRight/connEnd/digCorridor around a
corridorPlan struct; addPass gains addPassSpot; the shared door/
secret-door predicate becomes hiddenExit. passages.go is
complexity-clean. Behavior and RNG call order unchanged.
2026-07-07 03:25:46 +02:00
ea68df32f0 Decompose move.go; traps become a handler table (refactor step 7)
The be_trapped switch becomes gameData.trapHandlers with one trap*
method per trap kind (mystery messages split in two); moveHero splits
into moveTarget/moveResolve/moveEnter/moveOnto/offMap; passageTurn
gains per-axis passageTurnVertical/Horizontal. move.go is
complexity-clean. Behavior and RNG call order unchanged.
2026-07-07 03:22:54 +02:00
fec79b939a Decompose chase.go (refactor step 7)
chase splits into chaseBestSpot/chaseTry/scareScrollAt with a
chaseSearch state struct; chaseStep gains chaseRooms, chaseGoal,
dragonBreath/dragonShoots, and chaseTakeObject; runners gains
runnerTurn; findDest gains objectClaimed. chase.go is complexity-clean.
Behavior and RNG call order unchanged.
2026-07-07 03:19:31 +02:00
aa57349c34 Decompose command.go; command keys become a handler table (step 7)
The ordinary command keys move into gameData.commandHandlers (method
expressions and small literals); dispatchKey keeps only re-dispatching
prefixes (runCommand/fightCommand/repeatCommand/moveOnCommand) and the
wizard fallthrough. command() splits into playTurn/turnUpkeep/
readCommand/executeCommand/countPrefix/ringTurnEffects; search gains
searchSpot/searchFloor; help gains helpOne/helpAll/helpLines; call
gains callTarget/callPrelude; wizardCommand splits in two plus
wizardKit; uLevel and current flatten to early returns. command.go is
complexity-clean. Behavior and RNG call order unchanged.
2026-07-07 03:15:27 +02:00
4a248eb392 Decompose look and promptDirection (refactor step 7)
look's nine-square scan splits into lookAround/lookCell with a
lookScan state struct and guard helpers (lookSkips,
lookForeignPassage, lookDiagonalBlocked, lookCellChar, lookShow,
lookRunCheck, atRunEdge); promptDirection gains deltaFor and
confuseDirection. misc.go is complexity-clean. Behavior and RNG call
order unchanged.
2026-07-07 03:03:50 +02:00
a20f500655 Decompose fight.go: hit-handler table, weapon/armor helpers (step 7)
The monster special-power switch in attack becomes
gameData.hitHandlers (indexed by monster letter); attack splits into
monsterHit/monsterMiss; fight gains revealXeroc and heroHits;
rollAttacks gains weaponAttack, wieldedRingBonus, and defenderArmor;
killed gains killedSpecial. fight.go is complexity-clean. Behavior and
RNG call order unchanged.
2026-07-07 02:58:44 +02:00
ebe477ba28 Decompose remaining effects-file hot spots (refactor step 7)
drain gains drainReaches; zapSpeed gains hasteTarget/slowTarget;
readHoldMonster gains holdMonstersNear; readCreateMonster gains
createMonsterSpot; revealSpot splits into revealChar/revealWall/
revealSolid/revealFloor; turnSee gains showSensed. potions.go,
scrolls.go, and sticks.go are complexity-clean. Behavior and RNG call
order unchanged.
2026-07-07 02:53:09 +02:00
1a25beead8 Decompose fireBolt (refactor step 7)
The fire_bolt loop splits into boltDirChar, boltBounces,
boltStrikesMonster, and boltStrikesHero; loop state (hitHero/changed/
used) stays in fireBolt. Effect order and RNG calls unchanged.
2026-07-07 02:49:37 +02:00
8e2915f60d Convert doZap to a per-wand handler table (refactor step 7)
The do_zap switch becomes gameData.zapHandlers, indexed by WandKind;
the shared monster-ray preamble is zapRayMonster/zapVictim, teleport
away/to and the three bolt wands share handlers, and a false return
aborts the zap without spending a charge (drain life on a too-weak
hero, as in C). Effect order and RNG call sequence unchanged.
2026-07-07 02:48:01 +02:00
3047f729aa Convert readScroll to a per-scroll handler table (refactor step 7)
The read_scroll switch becomes gameData.readHandlers, indexed by
ScrollKind; the five identify scrolls share one handler. The magic
mapping cell logic is extracted into revealSpot. Effect order and RNG
call sequence unchanged.
2026-07-07 02:46:09 +02:00
cc025eb808 Convert quaff to a per-potion handler table (refactor step 7)
The quaff switch becomes gameData.quaffHandlers, a method-expression
table indexed by PotionKind; each case body moved verbatim into a
quaff* method. Effect order and RNG call sequence unchanged.
2026-07-07 02:44:33 +02:00
30 changed files with 5071 additions and 3525 deletions

View File

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

40
TODO.md
View File

@@ -29,13 +29,27 @@ Refactor ground rules:
# Next Step # Next Step
Refactor step 7: effects dispatch — the giant quaff/readScroll/doZap Refactor step 8: constructor and style pass per the house styleguide —
switches become per-kind handler tables of small named methods, game.New(game.Params{...}) replacing NewGame(Config); replace the
keeping effect order and RNG call sequence identical. This step also gameEnd panic unwind with error-based turn results where feasible;
clears the outstanding cyclop/gocognit/nestif lint findings. 77-column wrap sweep.
# Completed Steps # 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): - 2026-07-07 Refactor step 6 (refactor/god-object-extraction):
MessageLine (was MsgLine) owns the msg/addmsg/endmsg machinery, MessageLine (was MsgLine) owns the msg/addmsg/endmsg machinery,
wired to its screen/look/input needs via attach(); RogueGame keeps 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 # Future Steps
1. Refactor step 8: constructor and style pass per the house 1. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the
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
post-refactor names; add the C name → Go name rename table. 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 binary and extend run_test.go to script a deeper multi-level
playthrough (descend past level 5, use potions, scrolls, zapping, playthrough (descend past level 5, use potions, scrolls, zapping,
save/restore). Fix any panics, message mismatches, or divergences save/restore). Fix any panics, message mismatches, or divergences
from the C behavior that this uncovers, with regression tests. 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 c-master: same seed, same dungeon, same item tables, for several
seeds. 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). (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. 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 2026-07-06): per-game dungeon dimensions instead of the 80x24
constants; open design questions are resize policy, gameplay constants; open design questions are resize policy, gameplay
tuning at larger sizes, and a --classic 80x24 mode. 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. add Makefile, Dockerfile, or REPO_POLICIES.md.

View File

@@ -9,17 +9,32 @@ const dragonShot = 5
func (g *RogueGame) runners(int) { func (g *RogueGame) runners(int) {
list := append([]*Monster(nil), g.Level.Monsters...) list := append([]*Monster(nil), g.Level.Monsters...)
for _, tp := range list { for _, tp := range list {
if !tp.On(Held) && tp.On(Awake) { g.runnerTurn(tp)
}
if g.HasHit {
g.endmsg()
g.HasHit = false
}
}
// runnerTurn gives one monster its motion for the turn; flying monsters
// far from the hero move twice (the loop body of chase.c runners).
func (g *RogueGame) runnerTurn(tp *Monster) {
if tp.On(Held) || !tp.On(Awake) {
return
}
origPos := tp.Pos origPos := tp.Pos
wastarget := tp.On(Targeted) wastarget := tp.On(Targeted)
if removed := g.moveMonster(tp); removed { if removed := g.moveMonster(tp); removed {
continue return
} }
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 { if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
if removed := g.moveMonster(tp); removed { if removed := g.moveMonster(tp); removed {
continue return
} }
} }
@@ -28,13 +43,6 @@ func (g *RogueGame) runners(int) {
g.ToDeath = false g.ToDeath = false
} }
}
}
if g.HasHit {
g.endmsg()
g.HasHit = false
}
} }
// moveMonster executes a single turn of running for a monster (chase.c // moveMonster executes a single turn of running for a monster (chase.c
@@ -87,13 +95,48 @@ func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
} }
} }
// chaseStep makes one thing chase another (chase.c do_chase). removed // chaseStep makes one thing chase another (chase.c do_chase). The
// reports that the chaser died or left the level in the attempt (the C // result reports that the chaser died or left the level in the attempt
// -1 return). // (the C -1 return).
func (g *RogueGame) chaseStep(th *Monster) (removed bool) { func (g *RogueGame) chaseStep(th *Monster) bool {
p := &g.Player
stoprun := false // true means we are there stoprun := false // true means we are there
mindist := 32767
rer, ree, door := g.chaseRooms(th)
this, shot := g.chaseGoal(th, rer, ree, door, 32767)
if shot {
return false
}
// This now contains what we want to run to this time so we run to it.
// If we hit it we either want to fight it or stop running
if g.chase(th, this) {
if th.Type == 'F' {
return false
}
} else {
switch this {
case g.Player.Pos:
return g.attack(th)
case *th.Dest:
g.chaseTakeObject(th)
stoprun = th.Type != 'F'
}
}
g.relocate(th, g.chRet)
// And stop running if need be
if stoprun && th.Pos == *th.Dest {
th.Flags.Clear(Awake)
}
return false
}
// chaseRooms finds the rooms of the chaser and its desire; doors do not
// count as inside rooms here (the setup of chase.c do_chase).
func (g *RogueGame) chaseRooms(th *Monster) (*Room, *Room, bool) {
p := &g.Player
rer := th.Room // find room of chaser rer := th.Room // find room of chaser
if th.On(Greedy) && rer.GoldVal == 0 { if th.On(Greedy) && rer.GoldVal == 0 {
@@ -106,15 +149,26 @@ func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
} else { } else {
ree = g.roomIn(*th.Dest) ree = g.roomIn(*th.Dest)
} }
// We don't count doors as inside rooms for this routine
door := g.Level.Char(th.Pos.Y, th.Pos.X) == Door
return rer, ree, g.Level.Char(th.Pos.Y, th.Pos.X) == Door
}
// chaseGoal picks the spot the chaser runs toward this turn: the
// nearest exit toward its desire when it is in a different room, or the
// desire itself. shot means a dragon breathed flame instead of moving
// (the goal loop of chase.c do_chase).
func (g *RogueGame) chaseGoal(th *Monster, rer, ree *Room, door bool, mindist int) (Coord, bool) {
var this Coord var this Coord
for { for {
// If the object of our desire is in a different room, and we are // 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. // not in a corridor, run to the door nearest to our goal.
if rer != ree { if rer == ree {
this = *th.Dest
return this, g.dragonBreath(th)
}
for i := range rer.Exits { for i := range rer.Exits {
curdist := distCp(*th.Dest, rer.Exits[i]) curdist := distCp(*th.Dest, rer.Exits[i])
if curdist < mindist { if curdist < mindist {
@@ -123,21 +177,25 @@ func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
} }
} }
if door { if !door {
return this, false
}
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum] rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum]
door = false door = false
// the C goto over: redo with the passage as room
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 // dragonBreath checks whether a dragon shoots flame at the hero instead
// line from it, and (b) that it is within shooting distance, // of moving, and shoots it (the D block of chase.c do_chase).
// but outside of striking range. func (g *RogueGame) dragonBreath(th *Monster) bool {
if th.Type == 'D' && (th.Pos.Y == p.Pos.Y || th.Pos.X == p.Pos.X || if th.Type != 'D' || !g.dragonShoots(th) {
abs(th.Pos.Y-p.Pos.Y) == abs(th.Pos.X-p.Pos.X)) && return false
distCp(th.Pos, p.Pos) <= BoltLength*BoltLength && }
!th.On(Cancelled) && g.rnd(dragonShot) == 0 {
p := &g.Player
g.Delta.Y = sign(p.Pos.Y - th.Pos.Y) g.Delta.Y = sign(p.Pos.Y - th.Pos.Y)
g.Delta.X = sign(p.Pos.X - th.Pos.X) g.Delta.X = sign(p.Pos.X - th.Pos.X)
@@ -155,18 +213,27 @@ func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
g.Kamikaze = false 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 false
} }
}
break return distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
} !th.On(Cancelled) && g.rnd(dragonShot) == 0
// 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) { // chaseTakeObject has the monster pick up the object it was running to
if this == p.Pos { // (the dest arm of chase.c do_chase).
return g.attack(th) func (g *RogueGame) chaseTakeObject(th *Monster) {
} else if this == *th.Dest {
for _, obj := range g.Level.Objects { for _, obj := range g.Level.Objects {
if th.Dest == &obj.Pos { if th.Dest == &obj.Pos {
g.Level.RemoveObject(obj) g.Level.RemoveObject(obj)
@@ -183,34 +250,12 @@ func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
break break
} }
} }
if th.Type != 'F' {
stoprun = true
}
}
} else {
if th.Type == 'F' {
return false
}
}
g.relocate(th, g.chRet)
// And stop running if need be
if stoprun && th.Pos == *th.Dest {
th.Flags.Clear(Awake)
}
return false
} }
// chase finds the spot for the chaser to move closer to the chasee // chase 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 // (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. // if we reach the goal. The chosen spot lands in g.chRet.
func (g *RogueGame) chase(tp *Monster, ee Coord) bool { func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
p := &g.Player
er := tp.Pos
plcnt := 1
var curdist int var curdist int
// If the thing is confused, let it move randomly. Invisible Stalkers // If the thing is confused, let it move randomly. Invisible Stalkers
@@ -226,11 +271,27 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
tp.Flags.Clear(Confused) tp.Flags.Clear(Confused)
} }
} else { } else {
// Otherwise, find the empty spot next to the chaser that is curdist = g.chaseBestSpot(tp, ee)
// 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. return curdist != 0 && g.chRet != g.Player.Pos
curdist = distCp(er, ee) }
// 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 g.chRet = er
ey := er.Y + 1 ey := er.Y + 1
@@ -249,54 +310,59 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
} }
for y := er.Y - 1; y <= ey; y++ { for y := er.Y - 1; y <= ey; y++ {
g.chaseTry(&s, y, x)
}
}
return s.curdist
}
// chaseTry scores one candidate square, reservoir-sampling among ties
// (the scan body of chase.c chase).
func (g *RogueGame) chaseTry(s *chaseSearch, y, x int) {
tryp := Coord{X: x, Y: y} tryp := Coord{X: x, Y: y}
if !g.diagOk(er, tryp) { if !g.diagOk(s.er, tryp) {
continue return
} }
ch := g.Level.VisibleChar(y, x) ch := g.Level.VisibleChar(y, x)
if stepOk(ch) { if !stepOk(ch) {
// If it is a scroll, it might be a scare monster return
// scroll so we need to look it up to see what type
// it is.
if ch == Scroll {
var found *Object
for _, obj := range g.Level.Objects {
if y == obj.Pos.Y && x == obj.Pos.X {
found = obj
break
}
}
if found != nil && found.ScrollKind() == ScrollScareMonster {
continue
} }
// If 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 // It can also be a Xeroc, which we shouldn't step on
if m := g.Level.MonsterAt(y, x); m != nil && m.Type == 'X' { if m := g.Level.MonsterAt(y, x); m != nil && m.Type == 'X' {
continue return
} }
// If we didn't find any scrolls at this place or it // If we didn't find any scrolls at this place or it wasn't a scare
// wasn't a scare scroll, then this place counts // scroll, then this place counts
thisdist := distance(y, x, ee.Y, ee.X) thisdist := distance(y, x, s.ee.Y, s.ee.X)
if thisdist < curdist { if thisdist < s.curdist {
plcnt = 1 s.plcnt = 1
g.chRet = tryp g.chRet = tryp
curdist = thisdist s.curdist = thisdist
} else if thisdist == curdist { } else if thisdist == s.curdist {
if plcnt++; g.rnd(plcnt) == 0 { if s.plcnt++; g.rnd(s.plcnt) == 0 {
g.chRet = tryp g.chRet = tryp
curdist = thisdist 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 curdist != 0 && g.chRet != p.Pos return false
} }
// setOldChar sets the oldch character for the monster (chase.c set_oldch). // setOldChar sets the oldch character for the monster (chase.c set_oldch).
@@ -431,22 +497,23 @@ func (g *RogueGame) findDest(tp *Monster) *Coord {
continue continue
} }
if g.roomIn(obj.Pos) == tp.Room && g.rnd(100) < prob { if g.roomIn(obj.Pos) == tp.Room && g.rnd(100) < prob &&
claimed := false !g.objectClaimed(obj) {
for _, other := range g.Level.Monsters {
if other.Dest == &obj.Pos {
claimed = true
break
}
}
if !claimed {
return &obj.Pos return &obj.Pos
} }
} }
}
return &g.Player.Pos return &g.Player.Pos
} }
// objectClaimed reports whether some monster already runs toward this
// object (chase.c find_dest).
func (g *RogueGame) objectClaimed(obj *Object) bool {
for _, other := range g.Level.Monsters {
if other.Dest == &obj.Pos {
return true
}
}
return false
}

View File

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

View File

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

View File

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

View File

@@ -58,30 +58,9 @@ func (m *MessageLine) End() int {
m.Huh = m.buf.String() m.Huh = m.buf.String()
} }
if m.Mpos != 0 { if m.Mpos != 0 && m.promptMore() == Escape {
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 return Escape
} }
}
}
}
// All messages should start with uppercase, except ones that start // All messages should start with uppercase, except ones that start
// with a pack addressing character // with a pack addressing character
out := m.buf.String() out := m.buf.String()
@@ -101,6 +80,36 @@ func (m *MessageLine) End() int {
return ^Escape 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 // attach wires the message line to its display and input; NewGame and
// Restore call it once the screen and game exist. // Restore call it once the screen and game exist.
func (m *MessageLine) attach(scr *Screen, look func(bool), readChar func() byte) { func (m *MessageLine) attach(scr *Screen, look func(bool), readChar func() byte) {
@@ -193,9 +202,7 @@ func (g *RogueGame) status() {
temp = p.CurArmor.ArmorClass temp = p.CurArmor.ArmorClass
} }
if s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp && if g.statusUnchanged(temp) {
s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&
s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg {
return return
} }
@@ -238,6 +245,18 @@ func (g *RogueGame) status() {
g.move(oy, ox) 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). // waitFor sits around until the guy types the right key (io.c wait_for).
func (g *RogueGame) waitFor(ch byte) { func (g *RogueGame) waitFor(ch byte) {
if ch == '\n' { if ch == '\n' {

View File

@@ -4,11 +4,23 @@ package game
// and small utilities. call_it arrives with the scroll/potion phase (it // and small utilities. call_it arrives with the scroll/potion phase (it
// needs the get_str line editor). // 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). // look takes a quick glance all around the player (misc.c look).
func (g *RogueGame) look(wakeup bool) { func (g *RogueGame) look(wakeup bool) {
p := &g.Player p := &g.Player
hero := p.Pos hero := p.Pos
passcount := 0
rp := p.Room rp := p.Room
if g.Oldpos != hero { if g.Oldpos != hero {
@@ -17,85 +29,126 @@ func (g *RogueGame) look(wakeup bool) {
g.Oldrp = rp g.Oldrp = rp
} }
ey := hero.Y + 1 s := lookScan{
ex := hero.X + 1 hero: hero,
sx := hero.X - 1 wakeup: wakeup,
sy := hero.Y - 1 sy: hero.Y - 1,
sx: hero.X - 1,
ey: hero.Y + 1,
ex: hero.X + 1,
}
sumhero, diffhero := 0, 0 s.doorStop = g.DoorStop && !g.Firstmove
if g.DoorStop && !g.Firstmove && g.Running { if s.doorStop && g.Running {
sumhero = hero.Y + hero.X s.sumhero = hero.Y + hero.X
diffhero = hero.Y - hero.X s.diffhero = hero.Y - hero.X
} }
pp := g.Level.At(hero.Y, hero.X) pp := g.Level.At(hero.Y, hero.X)
pch := pp.Ch s.pch = pp.Ch
pfl := pp.Flags s.pfl = pp.Flags
for y := sy; y <= ey; y++ { g.lookAround(&s)
if s.doorStop && s.passcount > 1 {
g.Running = false
}
if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh)
}
}
// lookAround runs the nine-square scan of look().
func (g *RogueGame) lookAround(s *lookScan) {
for y := s.sy; y <= s.ey; y++ {
if y <= 0 || y >= NumLines-1 { if y <= 0 || y >= NumLines-1 {
continue continue
} }
for x := sx; x <= ex; x++ { for x := s.sx; x <= s.ex; x++ {
if x < 0 || x >= NumCols { if x < 0 || x >= NumCols {
continue continue
} }
if !p.On(Blind) { g.lookCell(s, y, x)
if y == hero.Y && x == hero.X {
continue
} }
} }
}
// 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) pp := g.Level.At(y, x)
if g.lookSkips(s, pp, y, x) {
ch := pp.Ch return
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 tp := pp.Monst
switch { ch, skip := g.lookCellChar(s, tp, y, x, pp.Ch)
case tp == nil: if skip {
ch = g.tripCh(y, x, ch) return
case p.On(SenseMonsters) && tp.On(Invisible):
if g.DoorStop && !g.Firstmove {
g.Running = false
} }
continue if !g.lookShow(s, tp, ch, y, x) {
default: return
if wakeup {
g.wakeMonster(y, x)
} }
if g.seeMonst(tp) { if s.doorStop && g.Running {
if p.On(Hallucinating) { g.lookRunCheck(s, ch, y, x)
ch = g.randomMonsterLetter()
} else {
ch = tp.Disguise
}
} }
}
// 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 p.On(Blind) && (y != hero.Y || x != hero.X) { if pp.Ch == ' ' { // nothing need be done with a ' '
continue 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) g.move(y, x)
@@ -108,66 +161,88 @@ func (g *RogueGame) look(wakeup bool) {
g.addch(ch) g.addch(ch)
} }
if g.DoorStop && !g.Firstmove && g.Running { return true
switch g.RunCh { }
case 'h':
if x == ex { // lookCellChar picks what the square shows: trip rendering for empty
continue // squares, waking and disguises for monsters. skip means the square is
// not drawn at all (the monster switch of the look loop).
func (g *RogueGame) lookCellChar(s *lookScan, tp *Monster, y, x int, ch byte) (byte, bool) {
p := &g.Player
switch {
case tp == nil:
return g.tripCh(y, x, ch), false
case p.On(SenseMonsters) && tp.On(Invisible):
if g.DoorStop && !g.Firstmove {
g.Running = false
} }
case 'j':
if y == sy { return ch, true
continue default:
if s.wakeup {
g.wakeMonster(y, x)
} }
case 'k':
if y == ey { if g.seeMonst(tp) {
continue if p.On(Hallucinating) {
return g.randomMonsterLetter(), false
} }
case 'l':
if x == sx { return tp.Disguise, false
continue
} }
case 'y':
if (y+x)-sumhero >= 1 { return ch, false
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
} }
}
// 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 { switch ch {
case Door: case Door:
if x == hero.X || y == hero.Y { if x == s.hero.X || y == s.hero.Y {
g.Running = false g.Running = false
} }
case Passage: case Passage:
if x == hero.X || y == hero.Y { if x == s.hero.X || y == s.hero.Y {
passcount++ s.passcount++
} }
case Floor, '|', '-', ' ': case Floor, '|', '-', ' ':
default: default:
g.Running = false g.Running = false
} }
} }
}
// atRunEdge reports whether (y, x) sits on the leading edge of the run
// direction, where door-stop checking does not apply (the first RunCh
// switch of the misc.c look loop).
func (s *lookScan) atRunEdge(runCh byte, y, x int) bool {
switch runCh {
case 'h':
return x == s.ex
case 'j':
return y == s.sy
case 'k':
return y == s.ey
case 'l':
return x == s.sx
case 'y':
return (y+x)-s.sumhero >= 1
case 'u':
return (y-x)-s.diffhero >= 1
case 'n':
return (y+x)-s.sumhero <= -1
case 'b':
return (y-x)-s.diffhero <= -1
} }
if g.DoorStop && !g.Firstmove && passcount > 1 { return false
g.Running = false
}
if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh)
}
} }
// tripCh returns the character for this space, taking into account whether // tripCh returns the character for this space, taking into account whether
@@ -409,40 +484,22 @@ func (g *RogueGame) promptDirection() bool {
} }
for { for {
gotit := true g.DirCh = g.readchar()
if g.DirCh == Escape {
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.LastDir = 0 g.LastDir = 0
g.resetLast() g.resetLast()
return false 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 break
} }
g.Msgs.Mpos = 0
g.msg("%s", prompt)
} }
g.DirCh = toLower(g.DirCh) g.DirCh = toLower(g.DirCh)
@@ -451,14 +508,7 @@ func (g *RogueGame) promptDirection() bool {
} }
if g.Player.On(Confused) && g.rnd(5) == 0 { if g.Player.On(Confused) && g.rnd(5) == 0 {
for { g.confuseDirection()
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.Msgs.Mpos = 0 g.Msgs.Mpos = 0
@@ -466,6 +516,44 @@ func (g *RogueGame) promptDirection() bool {
return true 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). // callIt calls an object something after use (misc.c call_it).
func (g *RogueGame) callIt(info *ObjInfo) { func (g *RogueGame) callIt(info *ObjInfo) {
if info.Know { if info.Know {

View File

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

View File

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

View File

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

View File

@@ -77,17 +77,9 @@ func (k ObjectKind) String() string {
return "suit of armor" return "suit of armor"
case KindRing: case KindRing:
return ringName return ringName
case KindWand: default:
return "wand or staff" return k.stringRest()
case KindAmulet:
return "amulet"
case KindGold:
return goldName
case KindRingOrStick:
return "ring, wand or staff"
} }
return "bizarre thing"
} }
// objectKindForGlyph is the reverse of Glyph: what category of item does a // 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 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 // Object is the _o arm of the C THING union: anything that can lie on the
// floor or ride in a pack. // floor or ride in a pack.
type Object struct { type Object struct {

View File

@@ -205,55 +205,11 @@ func (g *RogueGame) getStr(opt *string, win *Window) int {
) )
for { for {
c = g.readchar() c = g.readchar()
if c == '\n' || c == '\r' || c == Escape { if endsInput(c) || (len(buf) == 0 && c == '-' && !onStd) {
break break
} }
if c == 8 || c == 0x7f { // erase character buf = g.getStrEdit(win, buf, c, oy, ox)
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)
} }
if len(buf) > 0 { // only change option if something has been typed 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) 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 { switch c {
case '-': case '-':
return Minus 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. // displayStr renders a buffer the way the input echo did.
func displayStr(buf []byte) string { func displayStr(buf []byte) string {
var sb strings.Builder var sb strings.Builder
@@ -337,20 +345,49 @@ func (g *RogueGame) ParseOpts(str string) {
i++ i++
} }
name := str[:i] rest := g.parseOptName(optlist, str[:i], str[i:])
rest := str[i:] // skip to start of next option name
matched := false for rest != "" && !isAlpha(rest[0]) {
rest = rest[1:]
}
str = rest
}
}
// parseOptName applies one named option, returning the unconsumed
// remainder: "name" turns a boolean on, "noname" turns it off, and
// string options consume a value (the option scan of options.c
// parse_opts).
func (g *RogueGame) parseOptName(optlist []optDesc, name, rest string) string {
for oi := range optlist { for oi := range optlist {
op := &optlist[oi] op := &optlist[oi]
isBoolOpt := op.kind == optBool || op.kind == optSeeFloor isBoolOpt := op.kind == optBool || op.kind == optSeeFloor
if strings.HasPrefix(op.name, name) && name != "" { if strings.HasPrefix(op.name, name) && name != "" {
matched = true
if isBoolOpt { if isBoolOpt {
*op.boolP = true *op.boolP = true
} else {
return rest
}
return g.parseOptValue(op, rest)
}
if isBoolOpt && strings.HasPrefix(name, "no") &&
strings.HasPrefix(op.name, name[2:]) {
*op.boolP = false
return rest
}
}
return rest
}
// parseOptValue consumes an option's "=value" from rest, storing it,
// and returns the remainder (the string arm of options.c parse_opts).
func (g *RogueGame) parseOptValue(op *optDesc, rest string) string {
// Skip to start of string value // Skip to start of string value
for rest != "" && rest[0] == '=' { for rest != "" && rest[0] == '=' {
rest = rest[1:] rest = rest[1:]
@@ -374,45 +411,31 @@ func (g *RogueGame) ParseOpts(str string) {
} }
word := val[:end] word := val[:end]
rest = val[end:]
if op.kind == optInvT { 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 // check for type of inventory
w := word if word != "" {
if w != "" { word = string(toUpper(word[0])) + word[1:]
w = string(toUpper(w[0])) + w[1:]
} }
for ti, tn := range g.data.invTName { for ti, tn := range g.data.invTName {
if strings.HasPrefix(tn, w) { if strings.HasPrefix(tn, word) {
*op.intP = ti *op.intP = ti
break break
} }
} }
} else {
*op.strP = prefix + strucpy(word)
}
}
break
} else if isBoolOpt && strings.HasPrefix(name, "no") &&
strings.HasPrefix(op.name, name[2:]) {
matched = true
*op.boolP = false
break
}
}
_ = matched
// skip to start of next option name
for rest != "" && !isAlpha(rest[0]) {
rest = rest[1:]
}
str = rest
}
} }
// strucpy copies a string keeping only printable characters, capped at // strucpy copies a string keeping only printable characters, capped at

View File

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

View File

@@ -15,204 +15,234 @@ func (g *RogueGame) digPassages() {
r1 := g.rnd(MaxRooms) r1 := g.rnd(MaxRooms)
ingraph[r1] = true ingraph[r1] = true
for { for roomcount < MaxRooms {
// find a room to connect with // find a room to connect with
j := 0 r2 := g.pickNeighbor(r1, func(i int) bool { return !ingraph[i] })
r2 := -1 if r2 < 0 {
// if no adjacent rooms are outside the graph, pick a new
for i := range MaxRooms { // room to look from
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
for { for {
r1 = g.rnd(MaxRooms) r1 = g.rnd(MaxRooms)
if ingraph[r1] { if ingraph[r1] {
break break
} }
} }
} else {
continue
}
// otherwise, connect new room to the graph, and draw a tunnel // otherwise, connect new room to the graph, and draw a tunnel
// to it // to it
ingraph[r2] = true //nolint:gosec // G602: rnd(MaxRooms) bounded ingraph[r2] = true
g.connectRooms(r1, r2) g.connectRooms(r1, r2)
isconn[r1][r2] = true isconn[r1][r2] = true
isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded isconn[r2][r1] = true
roomcount++ roomcount++
} }
if roomcount >= MaxRooms {
break
}
}
// attempt to add passages to the graph a random number of times so that // attempt to add passages to the graph a random number of times so that
// there isn't always just one unique passage through it. // there isn't always just one unique passage through it.
for roomcount = g.rnd(5); roomcount > 0; roomcount-- { for roomcount = g.rnd(5); roomcount > 0; roomcount-- {
r1 = g.rnd(MaxRooms) // a random room to look from r1 = g.rnd(MaxRooms) // a random room to look from
// find an adjacent room not already connected // find an adjacent room not already connected; if there is one,
j := 0 // connect it and look for the next added passage
r2 := -1 r2 := g.pickNeighbor(r1, func(i int) bool { return !isconn[r1][i] })
if r2 >= 0 {
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 {
g.connectRooms(r1, r2) g.connectRooms(r1, r2)
isconn[r1][r2] = true isconn[r1][r2] = true
isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded isconn[r2][r1] = true
} }
} }
g.numberPassages() 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 // connectRooms draws a corridor from a room in a certain direction
// (passages.c conn). // (passages.c conn).
func (g *RogueGame) connectRooms(r1, r2 int) { func (g *RogueGame) connectRooms(r1, r2 int) {
var ( rm, direc := connOrient(r1, r2)
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
)
var plan corridorPlan
if direc == 'd' { if direc == 'd' {
rmt := rm + 3 // room # of dest plan = g.connPlanDown(rm)
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 { } else {
turnDelta.X = -1 plan = g.connPlanRight(rm)
} }
turnDistance = abs(spos.X - epos.X) // how far to turn turnSpot := g.rnd(plan.distance-1) + 1 // where turn starts
} else { // setup for moving right
rmt := rm + 1
rpt = &g.Level.Rooms[rmt]
del = Coord{X: 1, Y: 0}
spos = rpf.Pos
epos = rpt.Pos
if !rpf.Flags.Has(Gone) {
for {
spos.X = rpf.Pos.X + rpf.Max.X - 1
spos.Y = rpf.Pos.Y + g.rnd(rpf.Max.Y-2) + 1
if !rpf.Flags.Has(Maze) || g.Level.FlagsAt(spos.Y, spos.X).Has(FPassage) {
break
}
}
}
if !rpt.Flags.Has(Gone) {
for {
epos.Y = rpt.Pos.Y + g.rnd(rpt.Max.Y-2) + 1
if !rpt.Flags.Has(Maze) || g.Level.FlagsAt(epos.Y, epos.X).Has(FPassage) {
break
}
}
}
distance = abs(spos.X-epos.X) - 1
if spos.Y < epos.Y {
turnDelta.Y = 1
} else {
turnDelta.Y = -1
}
turnDelta.X = 0
turnDistance = abs(spos.Y - epos.Y)
}
turnSpot := g.rnd(distance-1) + 1 // where turn starts
// Draw in the doors on either side of the passage or just put #'s if // Draw in the doors on either side of the passage or just put #'s if
// the rooms are gone. // the rooms are gone.
if !rpf.Flags.Has(Gone) { g.connEnd(plan.rpf, plan.spos)
g.door(rpf, spos) g.connEnd(plan.rpt, plan.epos)
} else {
g.putPassage(spos) 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) { if !rpt.Flags.Has(Gone) {
g.door(rpt, epos) for {
} else { plan.epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1
g.putPassage(epos) 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 { for distance > 0 {
// Move to new position // Move to new position
curr.X += del.X curr.X += plan.del.X
curr.Y += del.Y curr.Y += plan.del.Y
// Check if we are at the turn place, if so do the turn // Check if we are at the turn place, if so do the turn
if distance == turnSpot { if distance == turnSpot {
for ; turnDistance > 0; turnDistance-- { for ; turnDistance > 0; turnDistance-- {
g.putPassage(curr) g.putPassage(curr)
curr.X += turnDelta.X curr.X += plan.turnDelta.X
curr.Y += turnDelta.Y curr.Y += plan.turnDelta.Y
} }
} }
// Continue digging along // Continue digging along
@@ -221,10 +251,10 @@ func (g *RogueGame) connectRooms(r1, r2 int) {
distance-- distance--
} }
curr.X += del.X curr.X += plan.del.X
curr.Y += del.Y curr.Y += plan.del.Y
if curr != epos { if curr != plan.epos {
g.msg("warning, connectivity problem on this level") g.msg("warning, connectivity problem on this level")
} }
} }
@@ -270,9 +300,18 @@ func (g *RogueGame) door(rm *Room, cp Coord) {
func (g *RogueGame) addPass() { func (g *RogueGame) addPass() {
for y := 1; y < NumLines-1; y++ { for y := 1; y < NumLines-1; y++ {
for x := range NumCols { for x := range NumCols {
pp := g.Level.At(y, x) g.addPassSpot(g.Level.At(y, x), y, x)
if pp.Flags.Has(FPassage) || pp.Ch == Door || }
(!pp.Flags.Has(FReal) && (pp.Ch == '|' || pp.Ch == '-')) { }
}
// 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 ch := pp.Ch
if pp.Flags.Has(FPassage) { if pp.Flags.Has(FPassage) {
ch = Passage ch = Passage
@@ -297,9 +336,12 @@ func (g *RogueGame) addPass() {
g.standend() 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). // numberPassages assigns a number to each passageway (passages.c passnum).
@@ -338,8 +380,7 @@ func (g *RogueGame) numberPassage(y, x int) {
} }
// check to see if it is a door or secret door, i.e., a new exit, or a // check to see if it is a door or secret door, i.e., a new exit, or a
// numerable type of place // numerable type of place
if ch := g.Level.Char(y, x); ch == Door || if hiddenExit(*fp, g.Level.Char(y, x)) {
(!fp.Has(FReal) && (ch == '|' || ch == '-')) {
rp := &g.Level.Passages[g.pnum] rp := &g.Level.Passages[g.pnum]
rp.Exits = append(rp.Exits, Coord{Y: y, X: x}) rp.Exits = append(rp.Exits, Coord{Y: y, X: x})
} else if !fp.Has(FPassage) { } else if !fp.Has(FPassage) {

View File

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

View File

@@ -14,11 +14,8 @@ func (g *RogueGame) ringOn() {
} }
if obj.Kind != KindRing { if obj.Kind != KindRing {
if !g.Options.Terse { g.msg("%s", g.chooseTerse("not a ring",
g.msg("it would be difficult to wrap that around a finger") "it would be difficult to wrap that around a finger"))
} else {
g.msg("not a ring")
}
return return
} }
@@ -28,24 +25,8 @@ func (g *RogueGame) ringOn() {
return return
} }
var ring int ring := g.pickRingHand()
if ring < 0 {
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")
}
return return
} }
@@ -68,6 +49,26 @@ func (g *RogueGame) ringOn() {
g.msg("%s (%c)", g.inventoryName(obj, true), obj.PackCh) 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). // ringOff takes off a ring (rings.c ring_off).
func (g *RogueGame) ringOff() { func (g *RogueGame) ringOff() {
p := &g.Player p := &g.Player

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -122,6 +122,39 @@ type gameData struct {
// scoreReasons is the rip.c reason[] scoreboard strings. // scoreReasons is the rip.c reason[] scoreboard strings.
scoreReasons [4]string 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. // ripWall is the repeated blank wall line of the tombstone art.
@@ -587,6 +620,234 @@ func newGameData() *gameData {
"A total winner", "A total winner",
"killed with Amulet", "killed with Amulet",
}, },
quaffHandlers: [NumPotionTypes]func(*RogueGame, bool){
PotionConfusion: (*RogueGame).quaffConfusion,
PotionLSD: (*RogueGame).quaffLSD,
PotionPoison: (*RogueGame).quaffPoison,
PotionGainStrength: (*RogueGame).quaffGainStrength,
PotionSeeInvisible: (*RogueGame).quaffSeeInvisible,
PotionHealing: (*RogueGame).quaffHealing,
PotionDetectMonsters: (*RogueGame).quaffDetectMonsters,
PotionDetectMagic: (*RogueGame).quaffDetectMagic,
PotionRaiseLevel: (*RogueGame).quaffRaiseLevel,
PotionExtraHealing: (*RogueGame).quaffExtraHealing,
PotionHaste: (*RogueGame).quaffHaste,
PotionRestoreStrength: (*RogueGame).quaffRestoreStrength,
PotionBlindness: (*RogueGame).quaffBlindness,
PotionLevitation: (*RogueGame).quaffLevitation,
},
readHandlers: [NumScrollTypes]func(*RogueGame, *Object){
ScrollMonsterConfusion: (*RogueGame).readMonsterConfusion,
ScrollMagicMapping: (*RogueGame).readMagicMapping,
ScrollHoldMonster: (*RogueGame).readHoldMonster,
ScrollSleep: (*RogueGame).readSleep,
ScrollEnchantArmor: (*RogueGame).readEnchantArmor,
ScrollIdentifyPotion: (*RogueGame).readIdentify,
ScrollIdentifyScroll: (*RogueGame).readIdentify,
ScrollIdentifyWeapon: (*RogueGame).readIdentify,
ScrollIdentifyArmor: (*RogueGame).readIdentify,
ScrollIdentifyRingOrStick: (*RogueGame).readIdentify,
ScrollScareMonster: (*RogueGame).readScareMonster,
ScrollFoodDetection: (*RogueGame).readFoodDetection,
ScrollTeleportation: (*RogueGame).readTeleportation,
ScrollEnchantWeapon: (*RogueGame).readEnchantWeapon,
ScrollCreateMonster: (*RogueGame).readCreateMonster,
ScrollRemoveCurse: (*RogueGame).readRemoveCurse,
ScrollAggravateMonsters: (*RogueGame).readAggravateMonsters,
ScrollProtectArmor: (*RogueGame).readProtectArmor,
},
zapHandlers: [NumWandTypes]func(*RogueGame, *Object) bool{
WandLight: (*RogueGame).zapLight,
WandInvisibility: (*RogueGame).zapInvisibility,
WandLightning: (*RogueGame).zapBolt,
WandFire: (*RogueGame).zapBolt,
WandCold: (*RogueGame).zapBolt,
WandPolymorph: (*RogueGame).zapPolymorph,
WandMagicMissile: (*RogueGame).zapMagicMissile,
WandHasteMonster: (*RogueGame).zapSpeed,
WandSlowMonster: (*RogueGame).zapSpeed,
WandDrainLife: (*RogueGame).zapDrainLife,
WandTeleportAway: (*RogueGame).zapTeleport,
WandTeleportTo: (*RogueGame).zapTeleport,
WandCancellation: (*RogueGame).zapCancellation,
},
hitHandlers: [26]func(*RogueGame, *Monster, string) bool{
0: (*RogueGame).hitAquator, // 'A' - 'A'
'F' - 'A': (*RogueGame).hitFlytrap,
'I' - 'A': (*RogueGame).hitIceMonster,
'L' - 'A': (*RogueGame).hitLeprechaun,
'N' - 'A': (*RogueGame).hitNymph,
'R' - 'A': (*RogueGame).hitRattlesnake,
'V' - 'A': (*RogueGame).hitLifeDrainer,
'W' - 'A': (*RogueGame).hitLifeDrainer,
},
commandHandlers: map[byte]func(*RogueGame){
',': (*RogueGame).pickupCommand,
'!': (*RogueGame).shell,
'h': func(g *RogueGame) { g.moveHero(0, -1) },
'j': func(g *RogueGame) { g.moveHero(1, 0) },
'k': func(g *RogueGame) { g.moveHero(-1, 0) },
'l': func(g *RogueGame) { g.moveHero(0, 1) },
'y': func(g *RogueGame) { g.moveHero(-1, -1) },
'u': func(g *RogueGame) { g.moveHero(-1, 1) },
'b': func(g *RogueGame) { g.moveHero(1, -1) },
'n': func(g *RogueGame) { g.moveHero(1, 1) },
'H': func(g *RogueGame) { g.startRun('h') },
'J': func(g *RogueGame) { g.startRun('j') },
'K': func(g *RogueGame) { g.startRun('k') },
'L': func(g *RogueGame) { g.startRun('l') },
'Y': func(g *RogueGame) { g.startRun('y') },
'U': func(g *RogueGame) { g.startRun('u') },
'B': func(g *RogueGame) { g.startRun('b') },
'N': func(g *RogueGame) { g.startRun('n') },
't': func(g *RogueGame) {
if !g.promptDirection() {
g.After = false
} else {
g.missile(g.Delta.Y, g.Delta.X)
}
},
'q': (*RogueGame).quaff,
'Q': func(g *RogueGame) {
g.After = false
g.QComm = true
g.quit(0)
g.QComm = false
},
'i': func(g *RogueGame) {
g.After = false
g.inventory(g.Player.Pack, 0)
},
'I': func(g *RogueGame) {
g.After = false
g.pickyInven()
},
'd': (*RogueGame).dropIt,
'r': (*RogueGame).readScroll,
'e': (*RogueGame).eat,
'w': (*RogueGame).wield,
'W': (*RogueGame).wear,
'T': (*RogueGame).takeOff,
'P': (*RogueGame).ringOn,
'R': (*RogueGame).ringOff,
'o': func(g *RogueGame) {
g.option()
g.After = false
},
'c': func(g *RogueGame) {
g.call()
g.After = false
},
'>': func(g *RogueGame) {
g.After = false
g.dLevel()
},
'<': func(g *RogueGame) {
g.After = false
g.uLevel()
},
'?': func(g *RogueGame) {
g.After = false
g.help()
},
'/': func(g *RogueGame) {
g.After = false
g.identify()
},
's': (*RogueGame).search,
'z': func(g *RogueGame) {
if g.promptDirection() {
g.doZap()
} else {
g.After = false
}
},
'D': func(g *RogueGame) {
g.After = false
g.discovered()
},
CTRL('P'): func(g *RogueGame) {
g.After = false
g.msg("%s", g.Msgs.Huh)
},
CTRL('R'): func(g *RogueGame) {
g.After = false
g.refresh()
},
'v': func(g *RogueGame) {
g.After = false
g.msg("version %s. (mctesq was here)", Release)
},
'S': func(g *RogueGame) {
g.After = false
g.saveGame()
},
'.': func(*RogueGame) {
// rest command
},
' ': func(g *RogueGame) {
g.After = false // "legal" illegal command
},
'^': (*RogueGame).identifyTrapCommand,
Escape: func(g *RogueGame) {
g.DoorStop = false
g.Count = 0
g.After = false
g.Again = false
},
')': func(g *RogueGame) {
g.current(g.Player.CurWeapon, "wielding", "")
},
']': func(g *RogueGame) {
g.current(g.Player.CurArmor, "wearing", "")
},
'=': func(g *RogueGame) {
g.current(g.Player.CurRing[Left], "wearing",
g.chooseTerse("(L)", "on left hand"))
g.current(g.Player.CurRing[Right], "wearing",
g.chooseTerse("(R)", "on right hand"))
},
'@': func(g *RogueGame) {
g.StatMsg = true
g.status()
g.StatMsg = false
g.After = false
},
},
trapHandlers: [NumTrapTypes]func(*RogueGame, Coord){
TrapDoor: (*RogueGame).trapFall,
TrapArrow: (*RogueGame).trapArrow,
TrapSleep: (*RogueGame).trapSleep,
TrapBear: (*RogueGame).trapBear,
TrapTeleport: (*RogueGame).trapTeleport,
TrapDart: (*RogueGame).trapDart,
TrapRust: (*RogueGame).trapRust,
TrapMystery: (*RogueGame).trapMystery,
},
daemonHandlers: [DTurnSee + 1]func(*RogueGame, int){
DRollwand: (*RogueGame).rollwand,
DDoctor: (*RogueGame).doctor,
DStomach: (*RogueGame).stomach,
DRunners: (*RogueGame).runners,
DSwander: (*RogueGame).swander,
DNohaste: (*RogueGame).nohaste,
DUnconfuse: (*RogueGame).unconfuse,
DUnsee: (*RogueGame).unsee,
DSight: (*RogueGame).sight,
DVisuals: (*RogueGame).visuals,
DComeDown: (*RogueGame).comeDown,
DLand: (*RogueGame).land,
DTurnSee: func(g *RogueGame, arg int) {
g.turnSee(arg != 0)
},
},
} }
} }

View File

@@ -48,6 +48,22 @@ func TestInitProbsCumulative(t *testing.T) {
func TestNewGameRandomizesAppearances(t *testing.T) { func TestNewGameRandomizesAppearances(t *testing.T) {
g := NewGame(Config{Seed: 12345}) 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{} seen := map[string]bool{}
for i, c := range g.Items.PotColors { for i, c := range g.Items.PotColors {
@@ -61,6 +77,12 @@ func TestNewGameRandomizesAppearances(t *testing.T) {
seen[c] = true 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 { for i, n := range g.Items.ScrNames {
if n == "" { if n == "" {
@@ -71,9 +93,15 @@ func TestNewGameRandomizesAppearances(t *testing.T) {
t.Errorf("scroll name %q longer than C buffer allows", n) 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 { 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]) 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) 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) { func TestMonsterTable(t *testing.T) {

View File

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

View File

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

View File

@@ -27,6 +27,26 @@ func (g *RogueGame) createObj() {
switch obj.Kind { switch obj.Kind {
case KindWeapon, KindArmor: case KindWeapon, KindArmor:
g.createWeaponArmor(obj)
case KindRing:
g.createRing(obj)
case KindWand:
g.fixStick(obj)
case KindGold:
g.msg("how much?")
buf := ""
if g.getStr(&buf, g.scr.Std) == Norm {
obj.GoldValue = cAtoi(buf)
}
}
g.addPack(obj, false)
}
// createWeaponArmor sets up a wizard-created weapon or armor with an
// optional blessing (the weapon/armor arm of wizard.c create_obj).
func (g *RogueGame) createWeaponArmor(obj *Object) {
g.msg("blessing? (+,-,n)") g.msg("blessing? (+,-,n)")
bless := g.readchar() bless := g.readchar()
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
@@ -45,7 +65,10 @@ func (g *RogueGame) createObj() {
if bless == '+' { if bless == '+' {
obj.HPlus += g.rnd(3) + 1 obj.HPlus += g.rnd(3) + 1
} }
} else {
return
}
obj.ArmorClass = g.data.aClass[obj.Which] obj.ArmorClass = g.data.aClass[obj.Which]
if bless == '-' { if bless == '-' {
obj.ArmorClass += g.rnd(3) + 1 obj.ArmorClass += g.rnd(3) + 1
@@ -54,8 +77,11 @@ func (g *RogueGame) createObj() {
if bless == '+' { if bless == '+' {
obj.ArmorClass -= g.rnd(3) + 1 obj.ArmorClass -= g.rnd(3) + 1
} }
} }
case KindRing:
// createRing sets up a wizard-created ring, prompting for a bonus on
// the bonus rings (the ring arm of wizard.c create_obj).
func (g *RogueGame) createRing(obj *Object) {
switch obj.RingKind() { switch obj.RingKind() {
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage: case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
g.msg("blessing? (+,-,n)") g.msg("blessing? (+,-,n)")
@@ -71,18 +97,6 @@ func (g *RogueGame) createObj() {
case RingAggravateMonsters, RingTeleportation: case RingAggravateMonsters, RingTeleportation:
obj.Flags.Set(Cursed) obj.Flags.Set(Cursed)
} }
case KindWand:
g.fixStick(obj)
case KindGold:
g.msg("how much?")
buf := ""
if g.getStr(&buf, g.scr.Std) == Norm {
obj.GoldValue = cAtoi(buf)
}
}
g.addPack(obj, false)
} }
// showMap prints out the whole map for the wizard (wizard.c show_map). // showMap prints out the whole map for the wizard (wizard.c show_map).
@@ -117,34 +131,8 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
return return
} }
var obj *Object obj, ok := g.whatisPick(insist, kind)
for { if !ok {
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 {
return return
} }
@@ -164,6 +152,37 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
g.msg("%s", g.inventoryName(obj, false)) 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 // setKnow sets things up when we really know what a thing is (wizard.c
// set_know). // set_know).
func setKnow(obj *Object, info []ObjInfo) { func setKnow(obj *Object, info []ObjInfo) {

View File

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