Adopt house golangci-lint config; fix all approved-linter findings
.golangci.yml is the prompts-repo standard verbatim plus one approved
exception (paralleltest, sneak 2026-07-06). Roughly 1,500 findings
fixed:
- autofix sweep (wsl_v5/nlreturn/intrange/modernize formatting), with
the misspell autofix REVERTED where it rewrote authentic C game text
("missle vanishes" stays, with nolint and a comment)
- error handling: errcheck sites get real handling — saveFile now
closes explicitly and removes corrupt saves, scoreboard writes are
documented best-effort, err113 sentinel errors (ErrSaveOutOfDate,
ErrScreenTooSmall); panics allowed for unrecoverable states per
MEMORY.md policy
- gosec: real fixes (ParseInt for SEED, 0600 scorefile) and justified
per-line nolints for provably-bounded conversions (randomMonsterLetter
helper collapses eight rnd(26)+'A' sites)
- API tidying from linters: pointer receivers on flag types
(recvcheck), msg helpers renamed addmsgf/doaddf/Printwf/MvPrintwf
(goprintffuncname), myExit()/findFloor(monst)/mkGameInput(t) drop
always-constant params (unparam), gameEnd carries no status
- gocritic/staticcheck: if-else chains to switches, the pack.c
inventory filter untangled into matchesFilter, main.go split so
defers run before exit (exitAfterDefer, funlen)
- revive doc comments on all exported flag types/consts/methods
Remaining findings are confined to linters pending sneak's exception
decision (mnd, gochecknoglobals, cyclop, nestif, gocognit, exhaustive,
goconst, testpackage); TODO.md records the state. MEMORY.md added at
repo root as the project's agent working notes.
This commit is contained in:
34
.golangci.yml
Normal file
34
.golangci.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
modules-download-mode: readonly
|
||||
|
||||
linters:
|
||||
default: all
|
||||
disable:
|
||||
# Genuinely incompatible with project patterns
|
||||
- exhaustruct # Requires all struct fields
|
||||
- depguard # Dependency allow/block lists
|
||||
- godot # Requires comments to end with periods
|
||||
- wsl # Deprecated, replaced by wsl_v5
|
||||
- wrapcheck # Too verbose for internal packages
|
||||
- varnamelen # Short names like db, id are idiomatic Go
|
||||
# Repo-specific exceptions approved by sneak (2026-07-06)
|
||||
- paralleltest # Requires t.Parallel() in every test
|
||||
|
||||
linters-settings:
|
||||
lll:
|
||||
line-length: 88
|
||||
funlen:
|
||||
lines: 80
|
||||
statements: 50
|
||||
cyclop:
|
||||
max-complexity: 15
|
||||
dupl:
|
||||
threshold: 100
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
40
MEMORY.md
Normal file
40
MEMORY.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Project Memory
|
||||
|
||||
Working notes for agents on this repo. Read this alongside TODO.md
|
||||
(which holds the step queue and workflow) before starting work.
|
||||
|
||||
## Error handling
|
||||
|
||||
Panicking on bad/unexpected errors is allowed and preferred over
|
||||
threading unlikely error returns through game code — e.g. write-side
|
||||
Close/encode failures where continuing would mean corrupt state. The
|
||||
game already unwinds C's exit() calls via a gameEnd panic recovered in
|
||||
Run. Return errors where a caller genuinely handles them (save-file
|
||||
prompts, restore validation). Reserve deliberate `_ =` discards for
|
||||
true best-effort paths (scorefile writes, signal-time autosave), always
|
||||
with a comment saying why.
|
||||
|
||||
## Linting
|
||||
|
||||
The .golangci.yml is the house standard and may only be modified with
|
||||
sneak's explicit permission. To disable a linter, ask, explaining what
|
||||
the linter does; he approves specific exceptions, which are recorded
|
||||
in the config's "Repo-specific exceptions" block with the approval
|
||||
date. Approved so far: paralleltest (2026-07-06). Line-level //nolint
|
||||
with a reason is used sparingly for C-faithfulness (e.g. the authentic
|
||||
"missle" message spellings) and provably-safe gosec conversions; each
|
||||
needs a justifying comment.
|
||||
|
||||
## Faithfulness
|
||||
|
||||
Behavior must not change during the idiomatic-Go refactor unless a
|
||||
TODO step says so. The 80x24 seed-compatible gameplay, message text
|
||||
(including original typos), RNG call order, and C quirks (documented
|
||||
in tests like TestHoldScrollGreedyMonsterQuirk) are contract. Doc
|
||||
comments keep their "(file.c func_name)" breadcrumbs.
|
||||
|
||||
## Debugging
|
||||
|
||||
Write real, committed test files with t.Logf output and run plain
|
||||
`go test -v`; no throwaway scratch scripts. Successful debug probes
|
||||
become regression tests.
|
||||
16
TODO.md
16
TODO.md
@@ -29,9 +29,19 @@ Refactor ground rules:
|
||||
|
||||
# Next Step
|
||||
|
||||
Adopt the house Go linting standards: copy .golangci.yml from the
|
||||
prompts repo and bring game/, term/, and cmd/ lint-clean (the port is
|
||||
greenfield code, so no exemptions apply).
|
||||
Finish adopting the house Go linting standards. Done so far (branch
|
||||
refactor/lint-adoption): .golangci.yml copied verbatim from the
|
||||
prompts repo (plus the sneak-approved paralleltest exception,
|
||||
2026-07-06); ~1,500 findings fixed (autofix formatting sweep, errcheck/
|
||||
err113/noinlineerr error handling, forbidigo, funcorder, recvcheck
|
||||
pointer receivers, goprintffuncname renames msg helpers to *f, revive
|
||||
doc comments, gocritic switch rewrites, gosec real fixes plus justified
|
||||
nolints, unparam signature tightening, C-faithful "missle" spellings
|
||||
restored after misspell autofix changed game text). Remaining findings
|
||||
are all in linters awaiting sneak's exception decision: mnd (288),
|
||||
gochecknoglobals (37), cyclop (36), nestif (30), gocognit (23),
|
||||
exhaustive (22), goconst (16), testpackage (9). Blocked on that
|
||||
decision; either disable with approval or scope the fixes.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
|
||||
@@ -18,61 +18,45 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(run())
|
||||
}
|
||||
|
||||
// run carries the real main so that deferred terminal restoration runs
|
||||
// before the process exits (os.Exit skips defers).
|
||||
func run() int {
|
||||
scores := flag.Bool("s", false, "print the scoreboard and exit")
|
||||
deathDemo := flag.Bool("d", false, "die a random death (demo)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
|
||||
// get options from environment (main.c)
|
||||
rogueOpts := os.Getenv("ROGUEOPTS")
|
||||
name := ""
|
||||
if u, err := user.Current(); err == nil {
|
||||
name = u.Username
|
||||
}
|
||||
|
||||
wizard := os.Getenv("ROGUE_WIZARD") != ""
|
||||
// dungeon number: SEED for reproducible dungeons (wizard mode in C),
|
||||
// else time+pid
|
||||
var seed int32
|
||||
if env := os.Getenv("SEED"); env != "" && wizard {
|
||||
n, _ := strconv.Atoi(env)
|
||||
seed = int32(n)
|
||||
} else {
|
||||
seed = int32(time.Now().Unix()) + int32(os.Getpid())
|
||||
}
|
||||
|
||||
cfg := game.Config{
|
||||
Seed: seed,
|
||||
Name: name,
|
||||
RogueOpts: rogueOpts,
|
||||
Home: home,
|
||||
ScorePath: home + "/.rogue.scores",
|
||||
Wizard: wizard,
|
||||
}
|
||||
cfg := loadConfig()
|
||||
|
||||
if *scores {
|
||||
g := game.NewGame(cfg)
|
||||
g.ShowScores()
|
||||
return
|
||||
game.NewGame(cfg).ShowScores()
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
t, err := term.New()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
|
||||
return 1
|
||||
}
|
||||
defer t.Fini()
|
||||
|
||||
cfg.Term = t
|
||||
|
||||
var g *game.RogueGame
|
||||
|
||||
if args := flag.Args(); len(args) == 1 && !*deathDemo {
|
||||
// restore a saved game
|
||||
g, err = game.Restore(args[0], cfg)
|
||||
if err != nil {
|
||||
t.Fini()
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
|
||||
return 1
|
||||
}
|
||||
} else {
|
||||
g = game.NewGame(cfg)
|
||||
@@ -80,22 +64,75 @@ func main() {
|
||||
|
||||
if *deathDemo {
|
||||
g.DeathDemo()
|
||||
return
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// SIGHUP/SIGTERM autosave (save.c auto_save)
|
||||
installAutosave(g, t)
|
||||
|
||||
runErr := g.Run()
|
||||
if runErr != nil {
|
||||
t.Fini()
|
||||
fmt.Fprintln(os.Stderr, runErr)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// loadConfig gathers the game configuration from the environment: home
|
||||
// directory, ROGUEOPTS, user name, wizard mode, and the dungeon seed
|
||||
// (main.c's startup).
|
||||
func loadConfig() game.Config {
|
||||
home, _ := os.UserHomeDir()
|
||||
|
||||
name := ""
|
||||
|
||||
u, userErr := user.Current()
|
||||
if userErr == nil {
|
||||
name = u.Username
|
||||
}
|
||||
|
||||
wizard := os.Getenv("ROGUE_WIZARD") != ""
|
||||
|
||||
return game.Config{
|
||||
Seed: chooseSeed(wizard),
|
||||
Name: name,
|
||||
RogueOpts: os.Getenv("ROGUEOPTS"),
|
||||
Home: home,
|
||||
ScorePath: home + "/.rogue.scores",
|
||||
Wizard: wizard,
|
||||
}
|
||||
}
|
||||
|
||||
// installAutosave saves the game and exits on SIGHUP/SIGTERM (save.c
|
||||
// auto_save).
|
||||
func installAutosave(g *game.RogueGame, t *term.Tcell) {
|
||||
sig := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(sig, syscall.SIGHUP, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sig
|
||||
g.AutoSave()
|
||||
t.Fini()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
if err := g.Run(); err != nil {
|
||||
t.Fini()
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// chooseSeed picks the dungeon number: SEED for reproducible dungeons
|
||||
// (wizard mode, as in the C game), else time+pid (main.c).
|
||||
func chooseSeed(wizard bool) int32 {
|
||||
if env := os.Getenv("SEED"); env != "" && wizard {
|
||||
n, err := strconv.ParseInt(env, 10, 32)
|
||||
if err == nil {
|
||||
return int32(n)
|
||||
}
|
||||
}
|
||||
|
||||
// The C game computed `lowtime + getpid()` in int; the truncation to
|
||||
// 32 bits is the same wraparound the C int arithmetic performed.
|
||||
return int32(time.Now().Unix()&0x7fffffff) + //nolint:gosec // G115: deliberate wrap
|
||||
int32(os.Getpid()&0x7fffffff) //nolint:gosec // G115: deliberate wrap
|
||||
}
|
||||
|
||||
@@ -5,36 +5,47 @@ package game
|
||||
// wear lets the player put armor on (armor.c wear).
|
||||
func (g *RogueGame) wear() {
|
||||
p := &g.Player
|
||||
|
||||
obj := g.getItem("wear", KindArmor)
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if p.CurArmor != nil {
|
||||
g.addmsg("you are already wearing some")
|
||||
g.addmsgf("you are already wearing some")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(". You'll have to take it off first")
|
||||
g.addmsgf(". You'll have to take it off first")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
g.After = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindArmor {
|
||||
g.msg("you can't wear that")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.wasteTime()
|
||||
obj.Flags.Set(Known)
|
||||
sp := g.invName(obj, true)
|
||||
p.CurArmor = obj
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you are now ")
|
||||
g.addmsgf("you are now ")
|
||||
}
|
||||
|
||||
g.msg("wearing %s", sp)
|
||||
}
|
||||
|
||||
// takeOff gets the armor off of the player's back (armor.c take_off).
|
||||
func (g *RogueGame) takeOff() {
|
||||
p := &g.Player
|
||||
|
||||
obj := p.CurArmor
|
||||
if obj == nil {
|
||||
g.After = false
|
||||
@@ -43,17 +54,22 @@ func (g *RogueGame) takeOff() {
|
||||
} else {
|
||||
g.msg("you aren't wearing any armor")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !g.dropCheck(p.CurArmor) {
|
||||
return
|
||||
}
|
||||
|
||||
p.CurArmor = nil
|
||||
|
||||
if g.Options.Terse {
|
||||
g.addmsg("was")
|
||||
g.addmsgf("was")
|
||||
} else {
|
||||
g.addmsg("you used to be")
|
||||
g.addmsgf("you used to be")
|
||||
}
|
||||
|
||||
g.msg(" wearing %c) %s", obj.PackCh, g.invName(obj, true))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,21 +11,26 @@ func (g *RogueGame) runners(int) {
|
||||
for _, tp := range list {
|
||||
if !tp.On(Held) && tp.On(Awake) {
|
||||
origPos := tp.Pos
|
||||
|
||||
wastarget := tp.On(Targeted)
|
||||
if g.moveMonst(tp) == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
|
||||
if g.moveMonst(tp) == -1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if wastarget && origPos != tp.Pos {
|
||||
tp.Flags.Clear(Targeted)
|
||||
|
||||
g.ToDeath = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
@@ -40,12 +45,15 @@ func (g *RogueGame) moveMonst(tp *Monster) int {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
if tp.On(Hasted) {
|
||||
if g.doChase(tp) == -1 {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
tp.Turn = !tp.Turn
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -62,10 +70,13 @@ func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
|
||||
if oroom != th.Room {
|
||||
th.Dest = g.findDest(th)
|
||||
}
|
||||
|
||||
th.Pos = newLoc
|
||||
g.Level.SetMonsterAt(newLoc.Y, newLoc.X, th)
|
||||
}
|
||||
|
||||
g.move(newLoc.Y, newLoc.X)
|
||||
|
||||
if g.seeMonst(th) {
|
||||
g.addch(th.Disguise)
|
||||
} else if g.Player.On(SenseMonsters) {
|
||||
@@ -86,6 +97,7 @@ func (g *RogueGame) doChase(th *Monster) int {
|
||||
if th.On(Greedy) && rer.GoldVal == 0 {
|
||||
th.Dest = &p.Pos // if gold has been taken, run after hero
|
||||
}
|
||||
|
||||
var ree *Room // find room of chasee
|
||||
if th.Dest == &p.Pos {
|
||||
ree = p.Room
|
||||
@@ -94,6 +106,7 @@ func (g *RogueGame) doChase(th *Monster) int {
|
||||
}
|
||||
// We don't count doors as inside rooms for this routine
|
||||
door := g.Level.Char(th.Pos.Y, th.Pos.X) == Door
|
||||
|
||||
var this Coord
|
||||
|
||||
over:
|
||||
@@ -107,9 +120,11 @@ over:
|
||||
mindist = curdist
|
||||
}
|
||||
}
|
||||
|
||||
if door {
|
||||
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum]
|
||||
door = false
|
||||
|
||||
goto over
|
||||
}
|
||||
} else {
|
||||
@@ -122,18 +137,22 @@ over:
|
||||
distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
|
||||
!th.On(Cancelled) && g.rnd(dragonShot) == 0 {
|
||||
g.Delta.Y = sign(p.Pos.Y - th.Pos.Y)
|
||||
|
||||
g.Delta.X = sign(p.Pos.X - th.Pos.X)
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.fireBolt(th.Pos, &g.Delta, "flame")
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
|
||||
g.Quiet = 0
|
||||
if g.ToDeath && !th.On(Targeted) {
|
||||
g.ToDeath = false
|
||||
g.Kamikaze = false
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -147,15 +166,19 @@ over:
|
||||
if th.Dest == &obj.Pos {
|
||||
detachObj(&g.Level.Objects, obj)
|
||||
attachObj(&th.Pack, obj)
|
||||
|
||||
if th.Room.Flags.Has(Gone) {
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor)
|
||||
}
|
||||
|
||||
th.Dest = g.findDest(th)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if th.Type != 'F' {
|
||||
stoprun = true
|
||||
}
|
||||
@@ -165,11 +188,13 @@ over:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
g.relocate(th, g.chRet)
|
||||
// And stop running if need be
|
||||
if stoprun && th.Pos == *th.Dest {
|
||||
th.Flags.Clear(Awake)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -180,6 +205,7 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
|
||||
p := &g.Player
|
||||
er := tp.Pos
|
||||
plcnt := 1
|
||||
|
||||
var curdist int
|
||||
|
||||
// If the thing is confused, let it move randomly. Invisible Stalkers
|
||||
@@ -206,6 +232,7 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
|
||||
if ey >= NumLines-1 {
|
||||
ey = NumLines - 2
|
||||
}
|
||||
|
||||
ex := er.X + 1
|
||||
if ex >= NumCols {
|
||||
ex = NumCols - 1
|
||||
@@ -215,11 +242,13 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
|
||||
if x < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for y := er.Y - 1; y <= ey; y++ {
|
||||
tryp := Coord{X: x, Y: y}
|
||||
if !g.diagOk(er, tryp) {
|
||||
continue
|
||||
}
|
||||
|
||||
ch := g.Level.VisibleChar(y, x)
|
||||
if stepOk(ch) {
|
||||
// If it is a scroll, it might be a scare monster
|
||||
@@ -227,12 +256,15 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
|
||||
// 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
|
||||
}
|
||||
@@ -258,6 +290,7 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return curdist != 0 && g.chRet != p.Pos
|
||||
}
|
||||
|
||||
@@ -266,7 +299,9 @@ func (g *RogueGame) setOldch(tp *Monster, cp Coord) {
|
||||
if tp.Pos == cp {
|
||||
return
|
||||
}
|
||||
|
||||
sch := tp.OldCh
|
||||
|
||||
tp.OldCh = g.mvinch(cp.Y, cp.X)
|
||||
if !g.Player.On(Blind) {
|
||||
if (sch == Floor || tp.OldCh == Floor) && tp.Room.Flags.Has(Dark) {
|
||||
@@ -284,20 +319,25 @@ func (g *RogueGame) seeMonst(mp *Monster) bool {
|
||||
if p.On(Blind) {
|
||||
return false
|
||||
}
|
||||
|
||||
if mp.On(Invisible) && !p.On(CanSeeInvisible) {
|
||||
return false
|
||||
}
|
||||
|
||||
y, x := mp.Pos.Y, mp.Pos.X
|
||||
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
||||
if y != p.Pos.Y && x != p.Pos.X &&
|
||||
!stepOk(g.Level.Char(y, p.Pos.X)) && !stepOk(g.Level.Char(p.Pos.Y, x)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if mp.Room != p.Room {
|
||||
return false
|
||||
}
|
||||
|
||||
return !mp.Room.Flags.Has(Dark)
|
||||
}
|
||||
|
||||
@@ -330,6 +370,7 @@ func (g *RogueGame) roomin(cp Coord) *Room {
|
||||
}
|
||||
|
||||
g.msg("in some bizarre place (%d, %d)", cp.Y, cp.X)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -338,9 +379,11 @@ func (g *RogueGame) diagOk(sp, ep Coord) bool {
|
||||
if ep.X < 0 || ep.X >= NumCols || ep.Y <= 0 || ep.Y >= NumLines-1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if ep.X == sp.X || ep.Y == sp.Y {
|
||||
return true
|
||||
}
|
||||
|
||||
return stepOk(g.Level.Char(ep.Y, sp.X)) && stepOk(g.Level.Char(sp.Y, ep.X))
|
||||
}
|
||||
|
||||
@@ -351,6 +394,7 @@ func (g *RogueGame) cansee(y, x int) bool {
|
||||
if p.On(Blind) {
|
||||
return false
|
||||
}
|
||||
|
||||
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
||||
if g.Level.FlagsAt(y, x).Has(FPassage) {
|
||||
if y != p.Pos.Y && x != p.Pos.X &&
|
||||
@@ -359,11 +403,13 @@ func (g *RogueGame) cansee(y, x int) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
// We can only see if the hero is in the same room as the coordinate
|
||||
// and the room is lit, or if it is close.
|
||||
rer := g.roomin(Coord{X: x, Y: y})
|
||||
|
||||
return rer == p.Room && !rer.Flags.Has(Dark)
|
||||
}
|
||||
|
||||
@@ -374,22 +420,28 @@ func (g *RogueGame) findDest(tp *Monster) *Coord {
|
||||
if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) {
|
||||
return &g.Player.Pos
|
||||
}
|
||||
|
||||
for _, obj := range g.Level.Objects {
|
||||
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster {
|
||||
continue
|
||||
}
|
||||
|
||||
if g.roomin(obj.Pos) == tp.Room && g.rnd(100) < prob {
|
||||
claimed := false
|
||||
|
||||
for _, other := range g.Level.Monsters {
|
||||
if other.Dest == &obj.Pos {
|
||||
claimed = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !claimed {
|
||||
return &obj.Pos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &g.Player.Pos
|
||||
}
|
||||
|
||||
166
game/command.go
166
game/command.go
@@ -6,6 +6,7 @@ package game
|
||||
// bracketing (command.c command).
|
||||
func (g *RogueGame) command() {
|
||||
p := &g.Player
|
||||
|
||||
ntimes := 1 // number of player moves
|
||||
if p.On(Hasted) {
|
||||
ntimes++
|
||||
@@ -13,6 +14,7 @@ func (g *RogueGame) command() {
|
||||
// Let the daemons start up
|
||||
g.DoDaemons(Before)
|
||||
g.DoFuses(Before)
|
||||
|
||||
for ; ntimes > 0; ntimes-- {
|
||||
g.Again = false
|
||||
if g.HasHit {
|
||||
@@ -26,29 +28,37 @@ func (g *RogueGame) command() {
|
||||
}
|
||||
|
||||
g.look(true)
|
||||
|
||||
if !g.Running {
|
||||
g.DoorStop = false
|
||||
}
|
||||
|
||||
g.status()
|
||||
g.LastScore = p.Purse
|
||||
g.move(p.Pos.Y, p.Pos.X)
|
||||
if !((g.Running || g.Count != 0) && g.Options.Jump) {
|
||||
|
||||
if (!g.Running && g.Count == 0) || !g.Options.Jump {
|
||||
g.refresh() // draw screen
|
||||
}
|
||||
|
||||
g.Take = 0
|
||||
g.After = true
|
||||
// Read command or continue run
|
||||
if g.Wizard {
|
||||
g.NoScore = true
|
||||
}
|
||||
|
||||
var ch byte
|
||||
|
||||
if g.NoCommand == 0 {
|
||||
if g.Running || g.ToDeath {
|
||||
switch {
|
||||
case g.Running || g.ToDeath:
|
||||
ch = g.RunCh
|
||||
} else if g.Count != 0 {
|
||||
case g.Count != 0:
|
||||
ch = g.countCh
|
||||
} else {
|
||||
default:
|
||||
ch = g.readchar()
|
||||
|
||||
g.MoveOn = false
|
||||
if g.Msgs.Mpos != 0 { // erase message if its there
|
||||
g.msg("")
|
||||
@@ -57,6 +67,7 @@ func (g *RogueGame) command() {
|
||||
} else {
|
||||
ch = '.'
|
||||
}
|
||||
|
||||
if g.NoCommand != 0 {
|
||||
if g.NoCommand--; g.NoCommand == 0 {
|
||||
p.Flags.Set(Awake)
|
||||
@@ -67,14 +78,14 @@ func (g *RogueGame) command() {
|
||||
g.newCount = false
|
||||
if isDigit(ch) {
|
||||
g.Count = 0
|
||||
|
||||
g.newCount = true
|
||||
for isDigit(ch) {
|
||||
g.Count = g.Count*10 + int(ch-'0')
|
||||
if g.Count > 255 {
|
||||
g.Count = 255
|
||||
}
|
||||
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
|
||||
@@ -93,8 +104,9 @@ func (g *RogueGame) command() {
|
||||
if g.Count != 0 && !g.Running {
|
||||
g.Count--
|
||||
}
|
||||
|
||||
if ch != 'a' && ch != Escape &&
|
||||
!(g.Running || g.Count != 0 || g.ToDeath) {
|
||||
!g.Running && g.Count == 0 && !g.ToDeath {
|
||||
g.LLastComm = g.LastComm
|
||||
g.LLastDir = g.LastDir
|
||||
g.LLastPick = g.LastPick
|
||||
@@ -102,6 +114,7 @@ func (g *RogueGame) command() {
|
||||
g.LastDir = 0
|
||||
g.LastPick = nil
|
||||
}
|
||||
|
||||
g.dispatch(ch)
|
||||
// turn off flags if no longer needed
|
||||
if !g.Running {
|
||||
@@ -112,20 +125,25 @@ func (g *RogueGame) command() {
|
||||
if g.Take != 0 {
|
||||
g.pickUp(g.Take)
|
||||
}
|
||||
|
||||
if !g.Running {
|
||||
g.DoorStop = false
|
||||
}
|
||||
|
||||
if !g.After {
|
||||
ntimes++
|
||||
}
|
||||
}
|
||||
|
||||
g.DoDaemons(After)
|
||||
g.DoFuses(After)
|
||||
|
||||
if p.IsRing(Left, RingSearching) {
|
||||
g.search()
|
||||
} else if p.IsRing(Left, RingTeleportation) && g.rnd(50) == 0 {
|
||||
g.teleport()
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingSearching) {
|
||||
g.search()
|
||||
} else if p.IsRing(Right, RingTeleportation) && g.rnd(50) == 0 {
|
||||
@@ -137,28 +155,35 @@ func (g *RogueGame) command() {
|
||||
// including its `goto over` re-dispatch).
|
||||
func (g *RogueGame) dispatch(ch byte) {
|
||||
p := &g.Player
|
||||
|
||||
over:
|
||||
switch ch {
|
||||
case ',':
|
||||
var found *Object
|
||||
|
||||
for _, obj := range g.Level.Objects {
|
||||
if obj.Pos.Y == p.Pos.Y && obj.Pos.X == p.Pos.X {
|
||||
found = obj
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found != nil {
|
||||
if !g.levitCheck() {
|
||||
g.pickUp(found.Kind.Glyph())
|
||||
}
|
||||
} else {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("there is ")
|
||||
g.addmsgf("there is ")
|
||||
}
|
||||
g.addmsg("nothing here")
|
||||
|
||||
g.addmsgf("nothing here")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" to pick up")
|
||||
g.addmsgf(" to pick up")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
}
|
||||
case '!':
|
||||
@@ -201,36 +226,46 @@ over:
|
||||
g.DoorStop = true
|
||||
g.Firstmove = true
|
||||
}
|
||||
|
||||
if g.Count != 0 && !g.newCount {
|
||||
ch = g.direction
|
||||
} else {
|
||||
ch += 'A' - CTRL('A')
|
||||
g.direction = ch
|
||||
}
|
||||
|
||||
goto over
|
||||
case 'F', 'f':
|
||||
if ch == 'F' {
|
||||
g.Kamikaze = true
|
||||
}
|
||||
|
||||
if !g.getDir() {
|
||||
g.After = false
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
g.Delta.Y += p.Pos.Y
|
||||
g.Delta.X += p.Pos.X
|
||||
|
||||
mp := g.Level.MonsterAt(g.Delta.Y, g.Delta.X)
|
||||
if mp == nil || (!g.seeMonst(mp) && !p.On(SenseMonsters)) {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("I see ")
|
||||
g.addmsgf("I see ")
|
||||
}
|
||||
|
||||
g.msg("no monster there")
|
||||
g.After = false
|
||||
} else if g.diagOk(p.Pos, g.Delta) {
|
||||
g.ToDeath = true
|
||||
g.MaxHit = 0
|
||||
|
||||
mp.Flags.Set(Targeted)
|
||||
|
||||
g.RunCh = g.DirCh
|
||||
ch = g.DirCh
|
||||
|
||||
goto over
|
||||
}
|
||||
case 't':
|
||||
@@ -246,6 +281,7 @@ over:
|
||||
} else {
|
||||
ch = g.LastComm
|
||||
g.Again = true
|
||||
|
||||
goto over
|
||||
}
|
||||
case 'q':
|
||||
@@ -327,15 +363,18 @@ over:
|
||||
if g.getDir() {
|
||||
g.Delta.Y += p.Pos.Y
|
||||
g.Delta.X += p.Pos.X
|
||||
|
||||
fp := g.Level.FlagsAt(g.Delta.Y, g.Delta.X)
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("You have found ")
|
||||
g.addmsgf("You have found ")
|
||||
}
|
||||
if g.Level.Char(g.Delta.Y, g.Delta.X) != Trap {
|
||||
|
||||
switch {
|
||||
case g.Level.Char(g.Delta.Y, g.Delta.X) != Trap:
|
||||
g.msg("no trap there")
|
||||
} else if p.On(Hallucinating) {
|
||||
case p.On(Hallucinating):
|
||||
g.msg("%s", trName[g.rnd(NumTrapTypes)])
|
||||
} else {
|
||||
default:
|
||||
g.msg("%s", trName[*fp&FTrapMask])
|
||||
fp.Set(FSeen)
|
||||
}
|
||||
@@ -352,6 +391,7 @@ over:
|
||||
} else {
|
||||
ch = g.DirCh
|
||||
g.countCh = g.DirCh
|
||||
|
||||
goto over
|
||||
}
|
||||
case ')':
|
||||
@@ -381,6 +421,7 @@ over:
|
||||
// wizardCommand handles the MASTER debug commands (command.c).
|
||||
func (g *RogueGame) wizardCommand(ch byte) {
|
||||
p := &g.Player
|
||||
|
||||
switch ch {
|
||||
case '|':
|
||||
g.msg("@ %d,%d", p.Pos.Y, p.Pos.X)
|
||||
@@ -452,62 +493,81 @@ func (g *RogueGame) search() {
|
||||
p := &g.Player
|
||||
ey := p.Pos.Y + 1
|
||||
ex := p.Pos.X + 1
|
||||
|
||||
probinc := 0
|
||||
if p.On(Hallucinating) {
|
||||
probinc = 3
|
||||
}
|
||||
|
||||
if p.On(Blind) {
|
||||
probinc += 2
|
||||
}
|
||||
|
||||
found := false
|
||||
|
||||
for y := p.Pos.Y - 1; y <= ey; y++ {
|
||||
for x := p.Pos.X - 1; x <= ex; x++ {
|
||||
if y == p.Pos.Y && x == p.Pos.X {
|
||||
continue
|
||||
}
|
||||
|
||||
fp := g.Level.FlagsAt(y, x)
|
||||
if fp.Has(FReal) {
|
||||
continue
|
||||
}
|
||||
|
||||
foundone := false
|
||||
|
||||
switch g.Level.Char(y, x) {
|
||||
case '|', '-':
|
||||
if g.rnd(5+probinc) != 0 {
|
||||
break
|
||||
}
|
||||
|
||||
g.Level.SetChar(y, x, Door)
|
||||
g.msg("a secret door")
|
||||
|
||||
foundone = true
|
||||
case Floor:
|
||||
if g.rnd(2+probinc) != 0 {
|
||||
break
|
||||
}
|
||||
|
||||
g.Level.SetChar(y, x, Trap)
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you found ")
|
||||
g.addmsgf("you found ")
|
||||
}
|
||||
|
||||
if p.On(Hallucinating) {
|
||||
g.msg("%s", trName[g.rnd(NumTrapTypes)])
|
||||
} else {
|
||||
g.msg("%s", trName[*fp&FTrapMask])
|
||||
fp.Set(FSeen)
|
||||
}
|
||||
|
||||
foundone = true
|
||||
case ' ':
|
||||
if g.rnd(3+probinc) != 0 {
|
||||
break
|
||||
}
|
||||
|
||||
g.Level.SetChar(y, x, Passage)
|
||||
|
||||
foundone = true
|
||||
}
|
||||
|
||||
if foundone {
|
||||
found = true
|
||||
|
||||
fp.Set(FReal)
|
||||
|
||||
g.Count = 0
|
||||
g.Running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
g.look(false)
|
||||
}
|
||||
@@ -523,28 +583,35 @@ func (g *RogueGame) help() {
|
||||
// typed a funny character.
|
||||
if helpch != '*' {
|
||||
g.move(0, 0)
|
||||
|
||||
for _, strp := range helpStr {
|
||||
if strp.Ch == helpch {
|
||||
g.Msgs.LowerMsg = true
|
||||
g.msg("%s%s", unctrl(strp.Ch), strp.Desc)
|
||||
g.Msgs.LowerMsg = false
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
g.msg("unknown character '%s'", unctrl(helpch))
|
||||
|
||||
return
|
||||
}
|
||||
// Here we print help for everything, then wait before we return to
|
||||
// command mode.
|
||||
numprint := 0
|
||||
|
||||
for _, strp := range helpStr {
|
||||
if strp.Print {
|
||||
numprint++
|
||||
}
|
||||
}
|
||||
|
||||
if numprint&1 == 1 { // round odd numbers up
|
||||
numprint++
|
||||
}
|
||||
|
||||
numprint /= 2
|
||||
if numprint > NumLines-1 {
|
||||
numprint = NumLines - 1
|
||||
@@ -552,24 +619,32 @@ func (g *RogueGame) help() {
|
||||
|
||||
hw := g.scr.Hw
|
||||
hw.Clear()
|
||||
|
||||
cnt := 0
|
||||
|
||||
for _, strp := range helpStr {
|
||||
if !strp.Print {
|
||||
continue
|
||||
}
|
||||
|
||||
x := 0
|
||||
if cnt >= numprint {
|
||||
x = NumCols / 2
|
||||
}
|
||||
|
||||
hw.Move(cnt%numprint, x)
|
||||
|
||||
if strp.Ch != 0 {
|
||||
hw.AddStr(unctrl(strp.Ch))
|
||||
}
|
||||
|
||||
hw.AddStr(strp.Desc)
|
||||
|
||||
if cnt++; cnt >= numprint*2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
hw.MvAddStr(NumLines-1, 0, "--Press space to continue--")
|
||||
g.scr.RefreshWin(hw)
|
||||
g.waitFor(' ')
|
||||
@@ -603,23 +678,29 @@ var identList = []helpEntry{
|
||||
func (g *RogueGame) identify() {
|
||||
g.msg("what do you want identified? ")
|
||||
ch := g.readchar()
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
if ch == Escape {
|
||||
g.msg("")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var str string
|
||||
if isUpper(ch) {
|
||||
str = g.Monsters[ch-'A'].Name
|
||||
} else {
|
||||
str = "unknown character"
|
||||
|
||||
for _, hp := range identList {
|
||||
if hp.Ch == ch {
|
||||
str = hp.Desc
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.msg("'%s': %s", unctrl(ch), str)
|
||||
}
|
||||
|
||||
@@ -628,6 +709,7 @@ func (g *RogueGame) dLevel() {
|
||||
if g.levitCheck() {
|
||||
return
|
||||
}
|
||||
|
||||
if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) != Stairs {
|
||||
g.msg("I see no way down")
|
||||
} else {
|
||||
@@ -642,12 +724,14 @@ func (g *RogueGame) uLevel() {
|
||||
if g.levitCheck() {
|
||||
return
|
||||
}
|
||||
|
||||
if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) == Stairs {
|
||||
if g.HasAmulet {
|
||||
g.Depth--
|
||||
if g.Depth == 0 {
|
||||
g.totalWinner()
|
||||
}
|
||||
|
||||
g.NewLevel()
|
||||
g.msg("you feel a wrenching sensation in your gut")
|
||||
} else {
|
||||
@@ -664,7 +748,9 @@ func (g *RogueGame) levitCheck() bool {
|
||||
if !g.Player.On(Levitating) {
|
||||
return false
|
||||
}
|
||||
|
||||
g.msg("You can't. You're floating off the ground!")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -676,11 +762,16 @@ func (g *RogueGame) call() {
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
var op *ObjInfo
|
||||
var elsewise string
|
||||
var know *bool
|
||||
var guess *string
|
||||
|
||||
var (
|
||||
op *ObjInfo
|
||||
elsewise string
|
||||
know *bool
|
||||
guess *string
|
||||
)
|
||||
|
||||
it := &g.Items
|
||||
|
||||
switch obj.Kind {
|
||||
case KindRing:
|
||||
op = &it.Rings[obj.Which]
|
||||
@@ -696,14 +787,18 @@ func (g *RogueGame) call() {
|
||||
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
|
||||
@@ -712,16 +807,21 @@ func (g *RogueGame) call() {
|
||||
} else {
|
||||
fromGuess = elsewise != "" && elsewise == *guess
|
||||
}
|
||||
|
||||
if know != nil && *know {
|
||||
g.msg("that has already been identified")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if fromGuess {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("Was ")
|
||||
g.addmsgf("Was ")
|
||||
}
|
||||
|
||||
g.msg("called \"%s\"", elsewise)
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? "))
|
||||
|
||||
buf := elsewise
|
||||
@@ -735,23 +835,29 @@ func (g *RogueGame) current(cur *Object, how, where string) {
|
||||
g.After = false
|
||||
if cur != nil {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you are %s (", how)
|
||||
g.addmsgf("you are %s (", how)
|
||||
}
|
||||
|
||||
g.InvDescribe = false
|
||||
g.addmsg("%c) %s", cur.PackCh, g.invName(cur, true))
|
||||
g.addmsgf("%c) %s", cur.PackCh, g.invName(cur, true))
|
||||
|
||||
g.InvDescribe = true
|
||||
if where != "" {
|
||||
g.addmsg(" %s", where)
|
||||
g.addmsgf(" %s", where)
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
} else {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you are ")
|
||||
g.addmsgf("you are ")
|
||||
}
|
||||
g.addmsg("%s nothing", how)
|
||||
|
||||
g.addmsgf("%s nothing", how)
|
||||
|
||||
if where != "" {
|
||||
g.addmsg(" %s", where)
|
||||
g.addmsgf(" %s", where)
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
}
|
||||
}
|
||||
@@ -762,7 +868,9 @@ func (g *RogueGame) shell() {
|
||||
g.After = false
|
||||
if se, ok := g.scr.term.(interface{ ShellEscape() }); ok {
|
||||
g.InShell = true
|
||||
|
||||
se.ShellEscape()
|
||||
|
||||
g.InShell = false
|
||||
g.refresh()
|
||||
} else {
|
||||
|
||||
@@ -23,6 +23,7 @@ type Monster struct {
|
||||
// in the C sources (cur_armor, purse, food_left, ...).
|
||||
type Player struct {
|
||||
Creature
|
||||
|
||||
CurArmor *Object // what he is wearing
|
||||
CurWeapon *Object // which weapon he is wielding
|
||||
CurRing [2]*Object // which rings are being worn (Left/Right)
|
||||
@@ -59,6 +60,7 @@ func detachMon(list *[]*Monster, item *Monster) {
|
||||
for i, m := range *list {
|
||||
if m == item {
|
||||
*list = append((*list)[:i], (*list)[i+1:]...)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ package game
|
||||
// function-pointer-to-int mapping in state.c rs_write_daemons.
|
||||
type DaemonID int
|
||||
|
||||
// Daemon and fuse callback identifiers. The first block's numeric values
|
||||
// match the function-pointer-to-int mapping in state.c rs_write_daemons;
|
||||
// the second block covers fuses state.c never saved (the Go save format
|
||||
// handles them all uniformly).
|
||||
const (
|
||||
DNone DaemonID = 0
|
||||
DRollwand DaemonID = 1
|
||||
@@ -21,8 +25,6 @@ const (
|
||||
DUnconfuse DaemonID = 7
|
||||
DUnsee DaemonID = 8
|
||||
DSight DaemonID = 9
|
||||
// Fuses beyond the C save map (state.c never saved these; the Go save
|
||||
// format handles them uniformly).
|
||||
DVisuals DaemonID = 10
|
||||
DComeDown DaemonID = 11
|
||||
DLand DaemonID = 12
|
||||
@@ -59,6 +61,7 @@ func (g *RogueGame) dSlot() *delayedAction {
|
||||
return &g.Daemons.List[i]
|
||||
}
|
||||
}
|
||||
|
||||
panic("ran out of fuse slots") // C: debug message in MASTER, NULL deref otherwise
|
||||
}
|
||||
|
||||
@@ -70,6 +73,7 @@ func (g *RogueGame) findSlot(f DaemonID) *delayedAction {
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ func (g *RogueGame) doctor(int) {
|
||||
p := &g.Player
|
||||
lv := p.Stats.Lvl
|
||||
ohp := p.Stats.HP
|
||||
|
||||
g.Quiet++
|
||||
if lv < 8 {
|
||||
if g.Quiet+(lv<<1) > 20 {
|
||||
@@ -53,16 +54,20 @@ func (g *RogueGame) doctor(int) {
|
||||
} else if g.Quiet >= 3 {
|
||||
p.Stats.HP += g.rnd(lv-7) + 1
|
||||
}
|
||||
|
||||
if p.IsRing(Left, RingRegeneration) {
|
||||
p.Stats.HP++
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingRegeneration) {
|
||||
p.Stats.HP++
|
||||
}
|
||||
|
||||
if ohp != p.Stats.HP {
|
||||
if p.Stats.HP > p.Stats.MaxHP {
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
|
||||
g.Quiet = 0
|
||||
}
|
||||
}
|
||||
@@ -82,6 +87,7 @@ func (g *RogueGame) rollwand(int) {
|
||||
g.KillDaemon(DRollwand)
|
||||
g.Fuse(DSwander, 0, wanderTime(g), Before)
|
||||
}
|
||||
|
||||
g.Daemons.Between = 0
|
||||
}
|
||||
}
|
||||
@@ -103,6 +109,7 @@ func (g *RogueGame) unsee(int) {
|
||||
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
|
||||
}
|
||||
}
|
||||
|
||||
g.Player.Flags.Clear(CanSeeInvisible)
|
||||
}
|
||||
|
||||
@@ -112,9 +119,11 @@ func (g *RogueGame) sight(int) {
|
||||
if p.On(Blind) {
|
||||
g.Extinguish(DSight)
|
||||
p.Flags.Clear(Blind)
|
||||
|
||||
if !p.Room.Flags.Has(Gone) {
|
||||
g.enterRoom(p.Pos)
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr("far out! Everything is all cosmic again",
|
||||
"the veil of darkness lifts"))
|
||||
}
|
||||
@@ -129,6 +138,7 @@ func (g *RogueGame) nohaste(int) {
|
||||
// stomach digests the hero's food (daemons.c stomach).
|
||||
func (g *RogueGame) stomach(int) {
|
||||
p := &g.Player
|
||||
|
||||
origHungry := p.HungryState
|
||||
if p.FoodLeft <= 0 {
|
||||
if p.FoodLeft--; p.FoodLeft < -StarveTime {
|
||||
@@ -138,29 +148,36 @@ func (g *RogueGame) stomach(int) {
|
||||
if g.NoCommand != 0 || g.rnd(5) != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
g.NoCommand += g.rnd(8) + 4
|
||||
p.HungryState = 3
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("%s", g.chooseStr(
|
||||
g.addmsgf("%s", g.chooseStr(
|
||||
"the munchies overpower your motor capabilities. ",
|
||||
"you feel too weak from lack of food. "))
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr("You freak out", "You faint"))
|
||||
} else {
|
||||
oldfood := p.FoodLeft
|
||||
|
||||
amulet := 0
|
||||
if g.HasAmulet {
|
||||
amulet = 1
|
||||
}
|
||||
|
||||
p.FoodLeft -= g.ringEat(Left) + g.ringEat(Right) + 1 - amulet
|
||||
|
||||
if p.FoodLeft < MoreTime && oldfood >= MoreTime {
|
||||
p.HungryState = 2
|
||||
|
||||
g.msg("%s", g.chooseStr(
|
||||
"the munchies are interfering with your motor capabilites",
|
||||
"the munchies are interfering with your motor capabilities",
|
||||
"you are starting to feel weak"))
|
||||
} else if p.FoodLeft < 2*MoreTime && oldfood >= 2*MoreTime {
|
||||
p.HungryState = 1
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("%s", g.chooseStr("getting the munchies", "getting hungry"))
|
||||
} else {
|
||||
@@ -169,8 +186,10 @@ func (g *RogueGame) stomach(int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.HungryState != origHungry {
|
||||
p.Flags.Clear(Awake)
|
||||
|
||||
g.Running = false
|
||||
g.ToDeath = false
|
||||
g.Count = 0
|
||||
@@ -200,8 +219,10 @@ func (g *RogueGame) comeDown(int) {
|
||||
|
||||
// undo the monsters
|
||||
seemonst := p.On(SenseMonsters)
|
||||
|
||||
for _, tp := range g.Level.Monsters {
|
||||
g.move(tp.Pos.Y, tp.Pos.X)
|
||||
|
||||
if g.cansee(tp.Pos.Y, tp.Pos.X) {
|
||||
if !tp.On(Invisible) || p.On(CanSeeInvisible) {
|
||||
g.addch(tp.Disguise)
|
||||
@@ -214,6 +235,7 @@ func (g *RogueGame) comeDown(int) {
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
|
||||
g.msg("Everything looks SO boring now.")
|
||||
}
|
||||
|
||||
@@ -238,17 +260,19 @@ func (g *RogueGame) visuals(int) {
|
||||
|
||||
// change the monsters
|
||||
seemonst := p.On(SenseMonsters)
|
||||
|
||||
for _, tp := range g.Level.Monsters {
|
||||
g.move(tp.Pos.Y, tp.Pos.X)
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
if tp.Type == 'X' && tp.Disguise != 'X' {
|
||||
g.addch(g.rndThing())
|
||||
} else {
|
||||
g.addch(byte(g.rnd(26) + 'A'))
|
||||
g.addch(g.randomMonsterLetter())
|
||||
}
|
||||
} else if seemonst {
|
||||
g.standout()
|
||||
g.addch(byte(g.rnd(26) + 'A'))
|
||||
g.addch(g.randomMonsterLetter())
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,20 +25,26 @@ type DiceSpec []DiceRoll
|
||||
// a single 0x0 attack, as it did in C.
|
||||
func ParseDice(s string) DiceSpec {
|
||||
var spec DiceSpec
|
||||
|
||||
for s != "" {
|
||||
count := cAtoi(s)
|
||||
|
||||
xi := strings.IndexByte(s, 'x')
|
||||
if xi < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
s = s[xi+1:]
|
||||
spec = append(spec, DiceRoll{Count: count, Sides: cAtoi(s)})
|
||||
|
||||
si := strings.IndexByte(s, '/')
|
||||
if si < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
s = s[si+1:]
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
@@ -48,11 +54,14 @@ func dice(s string) DiceSpec { return ParseDice(s) }
|
||||
// String renders the spec back in the classic "NxM/NxM" form.
|
||||
func (d DiceSpec) String() string {
|
||||
var sb strings.Builder
|
||||
|
||||
for i, r := range d {
|
||||
if i > 0 {
|
||||
sb.WriteByte('/')
|
||||
}
|
||||
|
||||
fmt.Fprintf(&sb, "%dx%d", r.Count, r.Sides)
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ func TestTablesHaveDice(t *testing.T) {
|
||||
t.Errorf("monster %c (%s) has no attacks", 'A'+i, m.Name)
|
||||
}
|
||||
}
|
||||
|
||||
for w, iw := range initWeaps {
|
||||
if len(iw.dam) == 0 || len(iw.hrl) == 0 {
|
||||
t.Errorf("weapon %v has empty dice", WeaponKind(w))
|
||||
|
||||
@@ -2,13 +2,16 @@ package game
|
||||
|
||||
import "testing"
|
||||
|
||||
// mkGameInput builds a headless game whose input plays the given script.
|
||||
func mkGameInput(t *testing.T, seed int32, input string) *RogueGame {
|
||||
// mkGameInput builds a headless game; tests script it via setInput. The
|
||||
// fixed seed keeps the scripted item/monster interactions stable.
|
||||
func mkGameInput(t *testing.T) *RogueGame {
|
||||
t.Helper()
|
||||
g := NewGame(Config{Seed: seed, Term: &testTerm{input: []byte(input)}})
|
||||
|
||||
g := NewGame(Config{Seed: 5, Term: &testTerm{}})
|
||||
g.NewLevel()
|
||||
g.Oldpos = g.Player.Pos
|
||||
g.Oldrp = g.roomin(g.Player.Pos)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
@@ -16,42 +19,61 @@ func mkGameInput(t *testing.T, seed int32, input string) *RogueGame {
|
||||
func give(g *RogueGame, obj *Object) byte {
|
||||
obj.Count = 1
|
||||
g.addPack(obj, true)
|
||||
|
||||
return obj.PackCh
|
||||
}
|
||||
|
||||
// setInput replaces the scripted terminal input.
|
||||
func setInput(t *testing.T, g *RogueGame, input ...byte) {
|
||||
t.Helper()
|
||||
|
||||
tt, ok := g.scr.term.(*testTerm)
|
||||
if !ok {
|
||||
t.Fatal("game terminal is not a testTerm")
|
||||
}
|
||||
|
||||
tt.input = input
|
||||
tt.pos = 0
|
||||
}
|
||||
|
||||
func TestQuaffHealingPotion(t *testing.T) {
|
||||
g := mkGameInput(t, 5, "")
|
||||
g := mkGameInput(t)
|
||||
pot := newObject()
|
||||
pot.Kind = KindPotion
|
||||
pot.Which = int(PotionHealing)
|
||||
ch := give(g, pot)
|
||||
g.scr.term.(*testTerm).input = []byte{ch}
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.Player.Stats.HP = 1
|
||||
g.quaff()
|
||||
|
||||
if g.Player.Stats.HP <= 1 {
|
||||
t.Error("healing potion did not heal")
|
||||
}
|
||||
|
||||
if !g.Items.Potions[PotionHealing].Know {
|
||||
t.Error("healing potion not identified after drinking")
|
||||
}
|
||||
|
||||
if len(g.Player.Pack) != 5 {
|
||||
t.Errorf("potion not consumed: %d items", len(g.Player.Pack))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuaffConfusionSetsFlagAndFuse(t *testing.T) {
|
||||
g := mkGameInput(t, 5, "")
|
||||
g := mkGameInput(t)
|
||||
pot := newObject()
|
||||
pot.Kind = KindPotion
|
||||
pot.Which = int(PotionConfusion)
|
||||
ch := give(g, pot)
|
||||
g.scr.term.(*testTerm).input = []byte{ch}
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.quaff()
|
||||
|
||||
if !g.Player.On(Confused) {
|
||||
t.Error("confusion potion did not confuse")
|
||||
}
|
||||
|
||||
if g.findSlot(DUnconfuse) == nil {
|
||||
t.Error("no unconfuse fuse pending")
|
||||
}
|
||||
@@ -59,21 +81,23 @@ func TestQuaffConfusionSetsFlagAndFuse(t *testing.T) {
|
||||
for range 30 {
|
||||
g.DoFuses(After)
|
||||
}
|
||||
|
||||
if g.Player.On(Confused) {
|
||||
t.Error("confusion never wore off")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadEnchantArmor(t *testing.T) {
|
||||
g := mkGameInput(t, 5, "")
|
||||
g := mkGameInput(t)
|
||||
scr := newObject()
|
||||
scr.Kind = KindScroll
|
||||
scr.Which = int(ScrollEnchantArmor)
|
||||
ch := give(g, scr)
|
||||
g.scr.term.(*testTerm).input = []byte{ch}
|
||||
setInput(t, g, ch)
|
||||
|
||||
before := g.Player.CurArmor.ArmorClass
|
||||
g.readScroll()
|
||||
|
||||
if g.Player.CurArmor.ArmorClass != before-1 {
|
||||
t.Errorf("enchant armor: AC %d -> %d, want %d",
|
||||
before, g.Player.CurArmor.ArmorClass, before-1)
|
||||
@@ -86,17 +110,19 @@ func TestReadHoldMonsterFreezesAdjacent(t *testing.T) {
|
||||
// look(TRUE) at the end of read_scroll immediately re-wakes greedy
|
||||
// monsters. The port reproduces that quirk faithfully — see
|
||||
// TestHoldScrollGreedyMonsterQuirk.
|
||||
g := mkGameInput(t, 5, "")
|
||||
g := mkGameInput(t)
|
||||
tp := spawnAdjacent(g, 'Z')
|
||||
tp.Flags.Set(Awake)
|
||||
|
||||
scr := newObject()
|
||||
scr.Kind = KindScroll
|
||||
scr.Which = int(ScrollHoldMonster)
|
||||
ch := give(g, scr)
|
||||
g.scr.term.(*testTerm).input = []byte{ch}
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.readScroll()
|
||||
t.Logf("after scroll: flags=%o huh=%q", tp.Flags, g.Msgs.Huh)
|
||||
|
||||
if tp.On(Awake) || !tp.On(Held) {
|
||||
t.Error("hold monster scroll did not hold the adjacent monster")
|
||||
}
|
||||
@@ -107,21 +133,24 @@ func TestReadHoldMonsterFreezesAdjacent(t *testing.T) {
|
||||
// (orc) held by a scroll is re-woken by the look(TRUE) that read_scroll
|
||||
// performs, ending up both held and running again.
|
||||
func TestHoldScrollGreedyMonsterQuirk(t *testing.T) {
|
||||
g := mkGameInput(t, 5, "")
|
||||
g := mkGameInput(t)
|
||||
tp := spawnAdjacent(g, 'O')
|
||||
tp.Flags.Set(Awake)
|
||||
|
||||
scr := newObject()
|
||||
scr.Kind = KindScroll
|
||||
scr.Which = int(ScrollHoldMonster)
|
||||
ch := give(g, scr)
|
||||
g.scr.term.(*testTerm).input = []byte{ch}
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.readScroll()
|
||||
t.Logf("orc after scroll: flags=%o (Awake=%v Held=%v)",
|
||||
tp.Flags, tp.On(Awake), tp.On(Held))
|
||||
|
||||
if !tp.On(Held) {
|
||||
t.Error("orc lost Held entirely")
|
||||
}
|
||||
|
||||
if !tp.On(Awake) {
|
||||
t.Error("quirk changed: greedy monster stayed held; if this is a " +
|
||||
"deliberate fix, update this test and ARCHITECTURE.md")
|
||||
@@ -129,21 +158,25 @@ func TestHoldScrollGreedyMonsterQuirk(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestZapSlowMonster(t *testing.T) {
|
||||
g := mkGameInput(t, 5, "")
|
||||
g := mkGameInput(t)
|
||||
tp := spawnAdjacent(g, 'Z')
|
||||
stick := newObject()
|
||||
stick.Kind = KindWand
|
||||
stick.Which = int(WandSlowMonster)
|
||||
g.fixStick(stick)
|
||||
ch := give(g, stick)
|
||||
g.scr.term.(*testTerm).input = []byte{ch}
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.Delta = Coord{X: 1, Y: 0} // aim at the monster
|
||||
|
||||
charges := stick.Charges
|
||||
|
||||
g.doZap()
|
||||
|
||||
if !tp.On(Slowed) {
|
||||
t.Error("slow monster wand did not slow")
|
||||
}
|
||||
|
||||
if stick.Charges != charges-1 {
|
||||
t.Error("zap did not use a charge")
|
||||
}
|
||||
@@ -152,18 +185,23 @@ func TestZapSlowMonster(t *testing.T) {
|
||||
func TestParseOpts(t *testing.T) {
|
||||
g := NewGame(Config{Seed: 1})
|
||||
g.ParseOpts("terse,nojump,name=Conan,fruit=mango,inven=slow")
|
||||
|
||||
if !g.Options.Terse {
|
||||
t.Error("terse not set")
|
||||
}
|
||||
|
||||
if g.Options.Jump {
|
||||
t.Error("nojump not honored")
|
||||
}
|
||||
|
||||
if g.Whoami != "Conan" {
|
||||
t.Errorf("name = %q", g.Whoami)
|
||||
}
|
||||
|
||||
if g.Fruit != "mango" {
|
||||
t.Errorf("fruit = %q", g.Fruit)
|
||||
}
|
||||
|
||||
if g.Options.InvType != InvSlow {
|
||||
t.Errorf("inven = %d", g.Options.InvType)
|
||||
}
|
||||
|
||||
160
game/fight.go
160
game/fight.go
@@ -48,20 +48,27 @@ func (g *RogueGame) setMname(tp *Monster) string {
|
||||
if g.Options.Terse {
|
||||
return "it"
|
||||
}
|
||||
|
||||
return "something"
|
||||
}
|
||||
|
||||
var mname string
|
||||
|
||||
if g.Player.On(Hallucinating) {
|
||||
ch := int(g.mvinch(tp.Pos.Y, tp.Pos.X))
|
||||
if !isUpper(byte(ch)) {
|
||||
ch = g.rnd(26)
|
||||
var idx int
|
||||
|
||||
ch := g.mvinch(tp.Pos.Y, tp.Pos.X)
|
||||
if isUpper(ch) {
|
||||
idx = int(ch - 'A')
|
||||
} else {
|
||||
ch -= 'A'
|
||||
idx = g.rnd(26)
|
||||
}
|
||||
mname = g.Monsters[ch].Name
|
||||
|
||||
mname = g.Monsters[idx].Name
|
||||
} else {
|
||||
mname = g.Monsters[tp.Type-'A'].Name
|
||||
}
|
||||
|
||||
return "the " + mname
|
||||
}
|
||||
|
||||
@@ -82,37 +89,46 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
|
||||
if tp.Type == 'X' && tp.Disguise != 'X' && !p.On(Blind) {
|
||||
tp.Disguise = 'X'
|
||||
if p.On(Hallucinating) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, byte(g.rnd(26)+'A'))
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.randomMonsterLetter())
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
|
||||
"wait! That's a xeroc!"))
|
||||
|
||||
if !thrown {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
mname := g.setMname(tp)
|
||||
didHit := false
|
||||
|
||||
g.HasHit = g.Options.Terse && !g.ToDeath
|
||||
if g.rollEm(&p.Creature, &tp.Creature, weap, thrown) {
|
||||
didHit = false
|
||||
|
||||
if thrown {
|
||||
g.thunk(weap, mname, g.Options.Terse)
|
||||
} else {
|
||||
g.hit("", mname, g.Options.Terse)
|
||||
}
|
||||
|
||||
if p.On(CanConfuse) {
|
||||
didHit = true
|
||||
|
||||
tp.Flags.Set(Confused)
|
||||
p.Flags.Clear(CanConfuse)
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
g.msg("your hands stop glowing %s", g.pickColor("red"))
|
||||
}
|
||||
|
||||
if tp.Stats.HP <= 0 {
|
||||
g.killed(tp, true)
|
||||
} else if didHit && !p.On(Blind) {
|
||||
g.msg("%s appears confused", mname)
|
||||
}
|
||||
|
||||
didHit = true
|
||||
} else {
|
||||
if thrown {
|
||||
@@ -121,6 +137,7 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
|
||||
g.miss("", mname, g.Options.Terse)
|
||||
}
|
||||
}
|
||||
|
||||
return didHit
|
||||
}
|
||||
|
||||
@@ -132,29 +149,35 @@ func (g *RogueGame) attack(mp *Monster) int {
|
||||
// going on at the time.
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
|
||||
g.Quiet = 0
|
||||
if g.ToDeath && !mp.On(Targeted) {
|
||||
g.ToDeath = false
|
||||
g.Kamikaze = false
|
||||
}
|
||||
|
||||
if mp.Type == 'X' && mp.Disguise != 'X' && !p.On(Blind) {
|
||||
mp.Disguise = 'X'
|
||||
if p.On(Hallucinating) {
|
||||
g.mvaddch(mp.Pos.Y, mp.Pos.X, byte(g.rnd(26)+'A'))
|
||||
g.mvaddch(mp.Pos.Y, mp.Pos.X, g.randomMonsterLetter())
|
||||
}
|
||||
}
|
||||
|
||||
mname := g.setMname(mp)
|
||||
oldhp := p.Stats.HP
|
||||
removed := false
|
||||
|
||||
if g.rollEm(&mp.Creature, &p.Creature, nil, false) {
|
||||
if mp.Type != 'I' {
|
||||
if g.HasHit {
|
||||
g.addmsg(". ")
|
||||
g.addmsgf(". ")
|
||||
}
|
||||
|
||||
g.hit(mname, "", false)
|
||||
} else if g.HasHit {
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.HasHit = false
|
||||
if p.Stats.HP <= 0 {
|
||||
g.death(mp.Type) // Bye bye life ...
|
||||
@@ -163,10 +186,12 @@ func (g *RogueGame) attack(mp *Monster) int {
|
||||
if oldhp > g.MaxHit {
|
||||
g.MaxHit = oldhp
|
||||
}
|
||||
|
||||
if p.Stats.HP <= g.MaxHit {
|
||||
g.ToDeath = false
|
||||
}
|
||||
}
|
||||
|
||||
if !mp.On(Cancelled) {
|
||||
switch mp.Type {
|
||||
case 'A':
|
||||
@@ -175,13 +200,17 @@ func (g *RogueGame) attack(mp *Monster) int {
|
||||
case 'I':
|
||||
// The ice monster freezes you
|
||||
p.Flags.Clear(Awake)
|
||||
|
||||
if g.NoCommand == 0 {
|
||||
g.addmsg("you are frozen")
|
||||
g.addmsgf("you are frozen")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" by the %s", mname)
|
||||
g.addmsgf(" by the %s", mname)
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.NoCommand += g.rnd(2) + 2
|
||||
if g.NoCommand > BoreLevel {
|
||||
g.death('h')
|
||||
@@ -191,6 +220,7 @@ func (g *RogueGame) attack(mp *Monster) int {
|
||||
if !g.save(VsPoison) {
|
||||
if !p.IsWearing(RingSustainStrength) {
|
||||
g.chgStr(-1)
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.msg("you feel a bite in your leg and now feel weaker")
|
||||
} else {
|
||||
@@ -211,36 +241,45 @@ func (g *RogueGame) attack(mp *Monster) int {
|
||||
if mp.Type == 'W' {
|
||||
chance = 15
|
||||
}
|
||||
|
||||
if g.rnd(100) < chance {
|
||||
var fewer int
|
||||
|
||||
if mp.Type == 'W' {
|
||||
if p.Stats.Exp == 0 {
|
||||
g.death('W') // All levels gone
|
||||
}
|
||||
|
||||
if p.Stats.Lvl--; p.Stats.Lvl == 0 {
|
||||
p.Stats.Exp = 0
|
||||
p.Stats.Lvl = 1
|
||||
} else {
|
||||
p.Stats.Exp = eLevels[p.Stats.Lvl-1] + 1
|
||||
}
|
||||
|
||||
fewer = g.roll(1, 10)
|
||||
} else {
|
||||
fewer = g.roll(1, 3)
|
||||
}
|
||||
|
||||
p.Stats.HP -= fewer
|
||||
|
||||
p.Stats.MaxHP -= fewer
|
||||
if p.Stats.HP <= 0 {
|
||||
p.Stats.HP = 1
|
||||
}
|
||||
|
||||
if p.Stats.MaxHP <= 0 {
|
||||
g.death(mp.Type)
|
||||
}
|
||||
|
||||
g.msg("you suddenly feel weaker")
|
||||
}
|
||||
case 'F':
|
||||
// Venus Flytrap stops the poor guy from moving
|
||||
p.Flags.Set(Held)
|
||||
p.VfHit++
|
||||
|
||||
g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}}
|
||||
if p.Stats.HP--; p.Stats.HP <= 0 {
|
||||
g.death('F')
|
||||
@@ -248,15 +287,20 @@ func (g *RogueGame) attack(mp *Monster) int {
|
||||
case 'L':
|
||||
// Leprechaun steals some gold
|
||||
lastpurse := p.Purse
|
||||
|
||||
p.Purse -= g.goldCalc()
|
||||
if !g.save(VsMagic) {
|
||||
p.Purse -= g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
|
||||
}
|
||||
|
||||
if p.Purse < 0 {
|
||||
p.Purse = 0
|
||||
}
|
||||
|
||||
g.removeMon(mp.Pos, mp, false)
|
||||
|
||||
removed = true
|
||||
|
||||
if p.Purse != lastpurse {
|
||||
g.msg("your purse feels lighter")
|
||||
}
|
||||
@@ -264,7 +308,9 @@ func (g *RogueGame) attack(mp *Monster) int {
|
||||
// Nymphs steal a magic item; look through the pack and
|
||||
// pick out one we like.
|
||||
var steal *Object
|
||||
|
||||
nobj := 0
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
if obj != p.CurArmor && obj != p.CurWeapon &&
|
||||
obj != p.CurRing[Left] && obj != p.CurRing[Right] &&
|
||||
@@ -274,9 +320,12 @@ func (g *RogueGame) attack(mp *Monster) int {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if steal != nil {
|
||||
g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false)
|
||||
|
||||
removed = true
|
||||
|
||||
g.leavePack(steal, false, false)
|
||||
g.msg("she stole %s!", g.invName(steal, true))
|
||||
}
|
||||
@@ -284,25 +333,31 @@ func (g *RogueGame) attack(mp *Monster) int {
|
||||
}
|
||||
} else if mp.Type != 'I' {
|
||||
if g.HasHit {
|
||||
g.addmsg(". ")
|
||||
g.addmsgf(". ")
|
||||
g.HasHit = false
|
||||
}
|
||||
|
||||
if mp.Type == 'F' {
|
||||
p.Stats.HP -= p.VfHit
|
||||
if p.Stats.HP <= 0 {
|
||||
g.death(mp.Type) // Bye bye life ...
|
||||
}
|
||||
}
|
||||
|
||||
g.miss(mname, "", false)
|
||||
}
|
||||
|
||||
if g.Options.FightFlush && !g.ToDeath {
|
||||
g.flushType()
|
||||
}
|
||||
|
||||
g.Count = 0
|
||||
g.status()
|
||||
|
||||
if removed {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -310,6 +365,7 @@ func (g *RogueGame) attack(mp *Monster) int {
|
||||
func (g *RogueGame) swing(atLvl, opArm, wplus int) bool {
|
||||
res := g.rnd(20)
|
||||
need := (20 - atLvl) - opArm
|
||||
|
||||
return res+wplus >= need
|
||||
}
|
||||
|
||||
@@ -318,12 +374,17 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
|
||||
p := &g.Player
|
||||
att := &thatt.Stats
|
||||
def := &thdef.Stats
|
||||
var attacks DiceSpec
|
||||
var hplus, dplus int
|
||||
|
||||
var (
|
||||
attacks DiceSpec
|
||||
hplus, dplus int
|
||||
)
|
||||
|
||||
if weap == nil {
|
||||
attacks = att.Dmg
|
||||
} else {
|
||||
hplus = weap.HPlus
|
||||
|
||||
dplus = weap.DPlus
|
||||
if weap == p.CurWeapon {
|
||||
if p.IsRing(Left, RingIncreaseDamage) {
|
||||
@@ -331,12 +392,14 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
|
||||
} 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 &&
|
||||
@@ -354,29 +417,37 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
|
||||
if !thdef.Flags.Has(Awake) {
|
||||
hplus += 4
|
||||
}
|
||||
|
||||
defArm := def.ArmorClass
|
||||
if def == &p.Stats {
|
||||
if p.CurArmor != nil {
|
||||
defArm = p.CurArmor.ArmorClass
|
||||
}
|
||||
|
||||
if p.IsRing(Left, RingProtection) {
|
||||
defArm -= p.CurRing[Left].Bonus
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingProtection) {
|
||||
defArm -= p.CurRing[Right].Bonus
|
||||
}
|
||||
}
|
||||
|
||||
didHit := false
|
||||
|
||||
for _, atk := range attacks {
|
||||
if g.swing(att.Lvl, defArm, hplus+strPlus[att.Str]) {
|
||||
proll := g.roll(atk.Count, atk.Sides)
|
||||
|
||||
damage := dplus + proll + addDam[att.Str]
|
||||
if damage > 0 {
|
||||
def.HP -= damage
|
||||
}
|
||||
|
||||
didHit = true
|
||||
}
|
||||
}
|
||||
|
||||
return didHit
|
||||
}
|
||||
|
||||
@@ -387,7 +458,9 @@ func cAtoi(s string) int {
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
|
||||
n, _ := strconv.Atoi(s[:i])
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -398,9 +471,11 @@ func prname(mname string, upper bool) string {
|
||||
if out == "" {
|
||||
out = "you"
|
||||
}
|
||||
|
||||
if upper {
|
||||
out = string(toUpper(out[0])) + out[1:]
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -409,12 +484,15 @@ func (g *RogueGame) thunk(weap *Object, mname string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
|
||||
if weap.Kind == KindWeapon {
|
||||
g.addmsg("the %s hits ", g.Items.Weapons[weap.Which].Name)
|
||||
g.addmsgf("the %s hits ", g.Items.Weapons[weap.Which].Name)
|
||||
} else {
|
||||
g.addmsg("you hit ")
|
||||
g.addmsgf("you hit ")
|
||||
}
|
||||
g.addmsg("%s", mname)
|
||||
|
||||
g.addmsgf("%s", mname)
|
||||
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
@@ -425,7 +503,9 @@ func (g *RogueGame) hit(er, ee string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
g.addmsg("%s", prname(er, true))
|
||||
|
||||
g.addmsgf("%s", prname(er, true))
|
||||
|
||||
var s string
|
||||
if g.Options.Terse {
|
||||
s = " hit"
|
||||
@@ -434,12 +514,16 @@ func (g *RogueGame) hit(er, ee string, noend bool) {
|
||||
if er != "" {
|
||||
i += 4
|
||||
}
|
||||
|
||||
s = hNames[i]
|
||||
}
|
||||
g.addmsg("%s", s)
|
||||
|
||||
g.addmsgf("%s", s)
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("%s", prname(ee, false))
|
||||
g.addmsgf("%s", prname(ee, false))
|
||||
}
|
||||
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
@@ -450,18 +534,24 @@ func (g *RogueGame) miss(er, ee string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
g.addmsg("%s", prname(er, true))
|
||||
|
||||
g.addmsgf("%s", prname(er, true))
|
||||
|
||||
i := 0
|
||||
if !g.Options.Terse {
|
||||
i = g.rnd(4)
|
||||
}
|
||||
|
||||
if er != "" {
|
||||
i += 4
|
||||
}
|
||||
g.addmsg("%s", mNames[i])
|
||||
|
||||
g.addmsgf("%s", mNames[i])
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" %s", prname(ee, false))
|
||||
g.addmsgf(" %s", prname(ee, false))
|
||||
}
|
||||
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
@@ -472,12 +562,15 @@ func (g *RogueGame) bounce(weap *Object, mname string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
|
||||
if weap.Kind == KindWeapon {
|
||||
g.addmsg("the %s misses ", g.Items.Weapons[weap.Which].Name)
|
||||
g.addmsgf("the %s misses ", g.Items.Weapons[weap.Which].Name)
|
||||
} else {
|
||||
g.addmsg("you missed ")
|
||||
g.addmsgf("you missed ")
|
||||
}
|
||||
g.addmsg("%s", mname)
|
||||
|
||||
g.addmsgf("%s", mname)
|
||||
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
@@ -489,15 +582,19 @@ func (g *RogueGame) removeMon(mp Coord, tp *Monster, waskill bool) {
|
||||
for _, obj := range pack {
|
||||
obj.Pos = tp.Pos
|
||||
detachObj(&tp.Pack, obj)
|
||||
|
||||
if waskill {
|
||||
g.fall(obj, false)
|
||||
}
|
||||
}
|
||||
|
||||
g.Level.SetMonsterAt(mp.Y, mp.X, nil)
|
||||
g.mvaddch(mp.Y, mp.X, tp.OldCh)
|
||||
detachMon(&g.Level.Monsters, tp)
|
||||
|
||||
if tp.On(Targeted) {
|
||||
g.Kamikaze = false
|
||||
|
||||
g.ToDeath = false
|
||||
if g.Options.FightFlush {
|
||||
g.flushType()
|
||||
@@ -521,33 +618,40 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
|
||||
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.
|
||||
mname := g.setMname(tp)
|
||||
g.removeMon(tp.Pos, tp, true)
|
||||
|
||||
if pr {
|
||||
if g.HasHit {
|
||||
g.addmsg(". Defeated ")
|
||||
g.addmsgf(". Defeated ")
|
||||
g.HasHit = false
|
||||
} else {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you have ")
|
||||
g.addmsgf("you have ")
|
||||
}
|
||||
g.addmsg("defeated ")
|
||||
|
||||
g.addmsgf("defeated ")
|
||||
}
|
||||
|
||||
g.msg("%s", mname)
|
||||
}
|
||||
// Do adjustments if he went up a level
|
||||
g.checkLevel()
|
||||
|
||||
if g.Options.FightFlush {
|
||||
g.flushType()
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import "testing"
|
||||
// look() state the way playit() does before the first command.
|
||||
func mkGame(t *testing.T, seed int32) *RogueGame {
|
||||
t.Helper()
|
||||
|
||||
g := NewGame(Config{Seed: seed, Term: &testTerm{}})
|
||||
g.NewLevel()
|
||||
g.Oldpos = g.Player.Pos
|
||||
g.Oldrp = g.roomin(g.Player.Pos)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
@@ -18,6 +20,7 @@ func spawnAdjacent(g *RogueGame, typ byte) *Monster {
|
||||
pos := Coord{X: g.Player.Pos.X + 1, Y: g.Player.Pos.Y}
|
||||
tp := &Monster{}
|
||||
g.newMonster(tp, typ, pos)
|
||||
|
||||
return tp
|
||||
}
|
||||
|
||||
@@ -32,6 +35,7 @@ func TestRollEmParsesMultiAttackDice(t *testing.T) {
|
||||
if !g.rollEm(att, def, nil, false) {
|
||||
t.Fatal("attack with guaranteed swing missed")
|
||||
}
|
||||
|
||||
dmg := 1000 - def.Stats.HP
|
||||
if dmg < 6 || dmg > 15 {
|
||||
t.Errorf("three 1x4+1 attacks dealt %d damage, want 6..15", dmg)
|
||||
@@ -45,12 +49,15 @@ func TestFightKillsMonster(t *testing.T) {
|
||||
g.Player.Stats.Lvl = 20 // always hits
|
||||
before := len(g.Level.Monsters)
|
||||
g.fight(tp.Pos, g.Player.CurWeapon, false)
|
||||
|
||||
if len(g.Level.Monsters) != before-1 {
|
||||
t.Error("monster not removed after fatal fight")
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(tp.Pos.Y, tp.Pos.X) != nil {
|
||||
t.Error("map still records dead monster")
|
||||
}
|
||||
|
||||
if g.Player.Stats.Exp == 0 {
|
||||
t.Error("no experience for the kill")
|
||||
}
|
||||
@@ -61,10 +68,12 @@ func TestAttackHurtsPlayer(t *testing.T) {
|
||||
tp := spawnAdjacent(g, 'T') // troll: 1x8/1x8/2x6
|
||||
tp.Stats.Lvl = 20 // always hits
|
||||
tp.Flags.Clear(Cancelled)
|
||||
|
||||
hpBefore := g.Player.Stats.HP
|
||||
g.Player.Stats.HP = 500
|
||||
g.Player.Stats.MaxHP = 500
|
||||
g.attack(tp)
|
||||
|
||||
if g.Player.Stats.HP >= 500 {
|
||||
t.Errorf("player HP unchanged (%d -> %d)", hpBefore, g.Player.Stats.HP)
|
||||
}
|
||||
@@ -72,15 +81,18 @@ func TestAttackHurtsPlayer(t *testing.T) {
|
||||
|
||||
func TestDeathUnwindsWithGameEnd(t *testing.T) {
|
||||
g := mkGame(t, 11)
|
||||
|
||||
defer func() {
|
||||
r := recover()
|
||||
if _, ok := r.(gameEnd); !ok {
|
||||
t.Fatalf("death did not unwind with gameEnd, got %v", r)
|
||||
}
|
||||
|
||||
if g.Playing {
|
||||
t.Error("still playing after death")
|
||||
}
|
||||
}()
|
||||
|
||||
g.Options.Tombstone = false
|
||||
g.death('K')
|
||||
}
|
||||
@@ -90,16 +102,20 @@ func TestRunnersChaseHero(t *testing.T) {
|
||||
// Place a hobgoblin a few squares away in the hero's room and set it
|
||||
// running at the hero.
|
||||
p := &g.Player
|
||||
|
||||
pos := Coord{X: p.Pos.X + 3, Y: p.Pos.Y}
|
||||
if !stepOk(g.Level.Char(pos.Y, pos.X)) || g.Level.MonsterAt(pos.Y, pos.X) != nil {
|
||||
t.Skip("no clear lane on this seed")
|
||||
}
|
||||
|
||||
tp := &Monster{}
|
||||
g.newMonster(tp, 'H', pos)
|
||||
tp.Flags.Set(Awake)
|
||||
tp.Dest = &p.Pos
|
||||
d0 := distCp(tp.Pos, p.Pos)
|
||||
|
||||
g.runners(0)
|
||||
|
||||
if d1 := distCp(tp.Pos, p.Pos); d1 >= d0 {
|
||||
t.Errorf("monster did not close distance: %d -> %d", d0, d1)
|
||||
}
|
||||
|
||||
25
game/game.go
25
game/game.go
@@ -166,15 +166,18 @@ func NewGame(cfg Config) *RogueGame {
|
||||
g.Msgs.SaveMsg = true
|
||||
g.scr = NewScreen(cfg.Term)
|
||||
g.FileName = cfg.Home + "/rogue.save"
|
||||
|
||||
g.rogueOpts = cfg.RogueOpts
|
||||
if cfg.Wizard {
|
||||
g.Player.Flags.Set(SenseMonsters)
|
||||
}
|
||||
|
||||
if cfg.RogueOpts != "" {
|
||||
g.ParseOpts(cfg.RogueOpts)
|
||||
}
|
||||
|
||||
g.Monsters = monsterTable
|
||||
|
||||
g.Items.Group = 2 // weapons.c: int group = 2
|
||||
for i := range g.Level.Passages {
|
||||
g.Level.Passages[i].Flags = Gone | Dark
|
||||
@@ -186,20 +189,26 @@ func NewGame(cfg Config) *RogueGame {
|
||||
g.initColors() // set up colors of potions
|
||||
g.initStones() // set up stone settings of rings
|
||||
g.initMaterials() // set up materials of wands
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// Run plays the game to its end: the back half of main.c main() plus
|
||||
// playit(). It returns after death, victory, quitting, or saving.
|
||||
func (g *RogueGame) Run() (err error) {
|
||||
func (g *RogueGame) Run() error {
|
||||
// A gameEnd panic is the port's my_exit(): recovering it here makes
|
||||
// Run return normally (zero values), restoring the terminal via the
|
||||
// caller's defers.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(gameEnd); ok {
|
||||
return // normal game over / save exit
|
||||
}
|
||||
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
if !g.restored {
|
||||
g.NewLevel() // draw current level
|
||||
// Start up daemons and fuses
|
||||
@@ -208,7 +217,9 @@ func (g *RogueGame) Run() (err error) {
|
||||
g.Fuse(DSwander, 0, wanderTime(g), After)
|
||||
g.StartDaemon(DStomach, 0, After)
|
||||
}
|
||||
|
||||
g.playit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -226,10 +237,12 @@ func (g *RogueGame) playit() {
|
||||
}
|
||||
|
||||
g.Oldpos = g.Player.Pos
|
||||
|
||||
g.Oldrp = g.roomin(g.Player.Pos)
|
||||
for g.Playing {
|
||||
g.command() // command execution
|
||||
}
|
||||
|
||||
g.endit()
|
||||
}
|
||||
|
||||
@@ -242,7 +255,7 @@ func (g *RogueGame) endit() {
|
||||
func (g *RogueGame) fatal(s string) {
|
||||
g.mvaddstr(NumLines-2, 0, s)
|
||||
g.refresh()
|
||||
g.myExit(0)
|
||||
g.myExit()
|
||||
}
|
||||
|
||||
// quit has the player make certain, then exits (main.c quit). The final
|
||||
@@ -252,17 +265,21 @@ func (g *RogueGame) quit(int) {
|
||||
if !g.QComm {
|
||||
g.Msgs.Mpos = 0
|
||||
}
|
||||
|
||||
oy, ox := g.scr.Std.GetYX()
|
||||
g.msg("really quit?")
|
||||
|
||||
if g.readchar() == 'y' {
|
||||
g.clear()
|
||||
g.scr.Std.MvPrintw(NumLines-2, 0, "You quit with %d gold pieces", g.Player.Purse)
|
||||
g.scr.Std.MvPrintwf(NumLines-2, 0, "You quit with %d gold pieces", g.Player.Purse)
|
||||
g.move(NumLines-1, 0)
|
||||
g.refresh()
|
||||
g.score(g.Player.Purse, 1, 0)
|
||||
g.myExit(0)
|
||||
g.myExit()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.move(0, 0)
|
||||
g.clrtoeol()
|
||||
g.status()
|
||||
|
||||
22
game/init.go
22
game/init.go
@@ -51,7 +51,8 @@ func (g *RogueGame) initPlayer() {
|
||||
// (init.c init_colors).
|
||||
func (g *RogueGame) initColors() {
|
||||
used := make([]bool, len(rainbow))
|
||||
for i := PotionKind(0); i < NumPotionTypes; i++ {
|
||||
|
||||
for i := range NumPotionTypes {
|
||||
var j int
|
||||
for {
|
||||
j = g.rnd(len(rainbow))
|
||||
@@ -59,6 +60,7 @@ func (g *RogueGame) initColors() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
used[j] = true
|
||||
g.Items.PotColors[i] = rainbow[j]
|
||||
}
|
||||
@@ -66,8 +68,9 @@ func (g *RogueGame) initColors() {
|
||||
|
||||
// initNames generates the names of the various scrolls (init.c init_names).
|
||||
func (g *RogueGame) initNames() {
|
||||
for i := ScrollKind(0); i < NumScrollTypes; i++ {
|
||||
for i := range NumScrollTypes {
|
||||
var cp strings.Builder
|
||||
|
||||
nwords := g.rnd(3) + 2
|
||||
for ; nwords > 0; nwords-- {
|
||||
nsyl := g.rnd(3) + 1
|
||||
@@ -76,10 +79,13 @@ func (g *RogueGame) initNames() {
|
||||
if cp.Len()+len(sp) > MaxNameLen {
|
||||
break
|
||||
}
|
||||
|
||||
cp.WriteString(sp)
|
||||
}
|
||||
|
||||
cp.WriteByte(' ')
|
||||
}
|
||||
|
||||
g.Items.ScrNames[i] = strings.TrimSuffix(cp.String(), " ")
|
||||
}
|
||||
}
|
||||
@@ -88,7 +94,8 @@ func (g *RogueGame) initNames() {
|
||||
// (init.c init_stones).
|
||||
func (g *RogueGame) initStones() {
|
||||
used := make([]bool, len(stoneTable))
|
||||
for i := RingKind(0); i < NumRingTypes; i++ {
|
||||
|
||||
for i := range NumRingTypes {
|
||||
var j int
|
||||
for {
|
||||
j = g.rnd(len(stoneTable))
|
||||
@@ -96,6 +103,7 @@ func (g *RogueGame) initStones() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
used[j] = true
|
||||
g.Items.RingStones[i] = stoneTable[j].Name
|
||||
g.Items.Rings[i].Worth += stoneTable[j].Value
|
||||
@@ -107,8 +115,10 @@ func (g *RogueGame) initStones() {
|
||||
func (g *RogueGame) initMaterials() {
|
||||
used := make([]bool, len(woods))
|
||||
metused := make([]bool, len(metals))
|
||||
for i := WandKind(0); i < NumWandTypes; i++ {
|
||||
|
||||
for i := range NumWandTypes {
|
||||
var str string
|
||||
|
||||
for {
|
||||
if g.rnd(2) == 0 {
|
||||
j := g.rnd(len(metals))
|
||||
@@ -116,6 +126,7 @@ func (g *RogueGame) initMaterials() {
|
||||
g.Items.WandType[i] = "wand"
|
||||
str = metals[j]
|
||||
metused[j] = true
|
||||
|
||||
break
|
||||
}
|
||||
} else {
|
||||
@@ -124,10 +135,12 @@ func (g *RogueGame) initMaterials() {
|
||||
g.Items.WandType[i] = "staff"
|
||||
str = woods[j]
|
||||
used[j] = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.Items.WandMade[i] = str
|
||||
}
|
||||
}
|
||||
@@ -166,5 +179,6 @@ func (g *RogueGame) pickColor(col string) string {
|
||||
if g.Player.On(Hallucinating) {
|
||||
return rainbow[g.rnd(len(rainbow))]
|
||||
}
|
||||
|
||||
return col
|
||||
}
|
||||
|
||||
40
game/io.go
40
game/io.go
@@ -31,16 +31,18 @@ func (g *RogueGame) msg(format string, a ...any) int {
|
||||
g.move(0, 0)
|
||||
g.clrtoeol()
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
return ^Escape
|
||||
}
|
||||
// otherwise add to the message and flush it out
|
||||
g.doadd(format, a...)
|
||||
g.doaddf(format, a...)
|
||||
|
||||
return g.endmsg()
|
||||
}
|
||||
|
||||
// addmsg adds things to the current message (io.c addmsg).
|
||||
func (g *RogueGame) addmsg(format string, a ...any) {
|
||||
g.doadd(format, a...)
|
||||
// addmsgf adds things to the current message (io.c addmsg).
|
||||
func (g *RogueGame) addmsgf(format string, a ...any) {
|
||||
g.doaddf(format, a...)
|
||||
}
|
||||
|
||||
// endmsg displays a new msg, giving the player a chance to see the previous
|
||||
@@ -50,10 +52,12 @@ func (g *RogueGame) endmsg() int {
|
||||
if m.SaveMsg {
|
||||
m.Huh = m.buf.String()
|
||||
}
|
||||
|
||||
if m.Mpos != 0 {
|
||||
g.look(false)
|
||||
g.mvaddstr(0, m.Mpos, "--More--")
|
||||
g.refresh()
|
||||
|
||||
if !m.MsgEsc {
|
||||
g.waitFor(' ')
|
||||
} else {
|
||||
@@ -62,10 +66,12 @@ func (g *RogueGame) endmsg() int {
|
||||
if ch == ' ' {
|
||||
break
|
||||
}
|
||||
|
||||
if ch == Escape {
|
||||
m.buf.Reset()
|
||||
m.Mpos = 0
|
||||
m.newpos = 0
|
||||
|
||||
return Escape
|
||||
}
|
||||
}
|
||||
@@ -75,25 +81,30 @@ func (g *RogueGame) endmsg() int {
|
||||
// with a pack addressing character
|
||||
out := m.buf.String()
|
||||
if len(out) > 0 && isLower(out[0]) && !m.LowerMsg &&
|
||||
!(len(out) > 1 && out[1] == ')') {
|
||||
(len(out) <= 1 || out[1] != ')') {
|
||||
out = string(toUpper(out[0])) + out[1:]
|
||||
}
|
||||
|
||||
g.mvaddstr(0, 0, out)
|
||||
g.clrtoeol()
|
||||
|
||||
m.Mpos = m.newpos
|
||||
m.newpos = 0
|
||||
m.buf.Reset()
|
||||
g.refresh()
|
||||
|
||||
return ^Escape
|
||||
}
|
||||
|
||||
// doadd performs an add onto the message buffer (io.c doadd).
|
||||
func (g *RogueGame) doadd(format string, a ...any) {
|
||||
// doaddf performs an add onto the message buffer (io.c doadd).
|
||||
func (g *RogueGame) doaddf(format string, a ...any) {
|
||||
m := &g.Msgs
|
||||
|
||||
s := fmt.Sprintf(format, a...)
|
||||
if len(s)+m.newpos >= maxMsg {
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
m.buf.WriteString(s)
|
||||
m.newpos = m.buf.Len()
|
||||
}
|
||||
@@ -114,8 +125,10 @@ func (g *RogueGame) readchar() byte {
|
||||
ch := g.scr.term.ReadChar()
|
||||
if ch == 3 { // ^C
|
||||
g.quit(0)
|
||||
|
||||
return 27
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
@@ -146,17 +159,21 @@ func (g *RogueGame) status() {
|
||||
if p.CurArmor != nil {
|
||||
temp = p.CurArmor.ArmorClass
|
||||
}
|
||||
|
||||
if s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp &&
|
||||
s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&
|
||||
s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg {
|
||||
return
|
||||
}
|
||||
|
||||
s.init = true
|
||||
s.arm = temp
|
||||
|
||||
oy, ox := g.scr.Std.GetYX()
|
||||
|
||||
if s.hp != p.Stats.MaxHP {
|
||||
s.hp = p.Stats.MaxHP
|
||||
|
||||
s.hpwidth = 0
|
||||
for t := p.Stats.MaxHP; t != 0; t /= 10 {
|
||||
s.hpwidth++
|
||||
@@ -183,6 +200,7 @@ func (g *RogueGame) status() {
|
||||
g.move(StatLine, 0)
|
||||
g.addstr(line)
|
||||
}
|
||||
|
||||
g.clrtoeol()
|
||||
g.move(oy, ox)
|
||||
}
|
||||
@@ -197,7 +215,11 @@ func (g *RogueGame) waitFor(ch byte) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for g.readchar() != ch {
|
||||
|
||||
for {
|
||||
if g.readchar() == ch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,11 +243,13 @@ func toUpper(c byte) byte {
|
||||
if isLower(c) {
|
||||
return c - 'a' + 'A'
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
func toLower(c byte) byte {
|
||||
if isUpper(c) {
|
||||
return c - 'A' + 'a'
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ func (l *Level) VisibleChar(y, x int) byte {
|
||||
if m := l.MonsterAt(y, x); m != nil {
|
||||
return m.Disguise
|
||||
}
|
||||
|
||||
return l.Char(y, x)
|
||||
}
|
||||
|
||||
|
||||
86
game/misc.go
86
game/misc.go
@@ -10,20 +10,24 @@ func (g *RogueGame) look(wakeup bool) {
|
||||
hero := p.Pos
|
||||
passcount := 0
|
||||
rp := p.Room
|
||||
|
||||
if g.Oldpos != hero {
|
||||
g.eraseLamp(g.Oldpos, g.Oldrp)
|
||||
g.Oldpos = hero
|
||||
g.Oldrp = rp
|
||||
}
|
||||
|
||||
ey := hero.Y + 1
|
||||
ex := hero.X + 1
|
||||
sx := hero.X - 1
|
||||
sy := hero.Y - 1
|
||||
|
||||
sumhero, diffhero := 0, 0
|
||||
if g.DoorStop && !g.Firstmove && g.Running {
|
||||
sumhero = hero.Y + hero.X
|
||||
diffhero = hero.Y - hero.X
|
||||
}
|
||||
|
||||
pp := g.Level.At(hero.Y, hero.X)
|
||||
pch := pp.Ch
|
||||
pfl := pp.Flags
|
||||
@@ -32,10 +36,12 @@ func (g *RogueGame) look(wakeup bool) {
|
||||
if y <= 0 || y >= NumLines-1 {
|
||||
continue
|
||||
}
|
||||
|
||||
for x := sx; x <= ex; x++ {
|
||||
if x < 0 || x >= NumCols {
|
||||
continue
|
||||
}
|
||||
|
||||
if !p.On(Blind) {
|
||||
if y == hero.Y && x == hero.X {
|
||||
continue
|
||||
@@ -43,16 +49,19 @@ func (g *RogueGame) look(wakeup bool) {
|
||||
}
|
||||
|
||||
pp := g.Level.At(y, x)
|
||||
|
||||
ch := pp.Ch
|
||||
if ch == ' ' { // nothing need be done with a ' '
|
||||
continue
|
||||
}
|
||||
|
||||
fp := &pp.Flags
|
||||
if pch != Door && ch != Door {
|
||||
if (pfl & FPassage) != (*fp & FPassage) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (fp.Has(FPassage) || ch == Door) && (pfl.Has(FPassage) || pch == Door) {
|
||||
if hero.X != x && hero.Y != y &&
|
||||
!stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) {
|
||||
@@ -61,25 +70,30 @@ func (g *RogueGame) look(wakeup bool) {
|
||||
}
|
||||
|
||||
tp := pp.Monst
|
||||
if tp == nil {
|
||||
|
||||
switch {
|
||||
case tp == nil:
|
||||
ch = g.tripCh(y, x, ch)
|
||||
} else if p.On(SenseMonsters) && tp.On(Invisible) {
|
||||
case p.On(SenseMonsters) && tp.On(Invisible):
|
||||
if g.DoorStop && !g.Firstmove {
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
continue
|
||||
} else {
|
||||
default:
|
||||
if wakeup {
|
||||
g.wakeMonster(y, x)
|
||||
}
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
if p.On(Hallucinating) {
|
||||
ch = byte(g.rnd(26) + 'A')
|
||||
ch = g.randomMonsterLetter()
|
||||
} else {
|
||||
ch = tp.Disguise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.On(Blind) && (y != hero.Y || x != hero.X) {
|
||||
continue
|
||||
}
|
||||
@@ -129,6 +143,7 @@ func (g *RogueGame) look(wakeup bool) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case Door:
|
||||
if x == hero.X || y == hero.Y {
|
||||
@@ -145,9 +160,11 @@ func (g *RogueGame) look(wakeup bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.DoorStop && !g.Firstmove && passcount > 1 {
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
if !g.Running || !g.Options.Jump {
|
||||
g.mvaddch(hero.Y, hero.X, PlayerCh)
|
||||
}
|
||||
@@ -165,26 +182,30 @@ func (g *RogueGame) tripCh(y, x int, ch byte) byte {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// eraseLamp erases the area shown by a lamp in a dark room
|
||||
// (misc.c erase_lamp).
|
||||
func (g *RogueGame) eraseLamp(pos Coord, rp *Room) {
|
||||
if !(g.Options.SeeFloor && rp.Flags&(Gone|Dark) == Dark &&
|
||||
!g.Player.On(Blind)) {
|
||||
if !g.Options.SeeFloor || rp.Flags&(Gone|Dark) != Dark ||
|
||||
g.Player.On(Blind) {
|
||||
return
|
||||
}
|
||||
|
||||
ey := pos.Y + 1
|
||||
ex := pos.X + 1
|
||||
|
||||
sy := pos.Y - 1
|
||||
for x := pos.X - 1; x <= ex; x++ {
|
||||
for y := sy; y <= ey; y++ {
|
||||
if y == g.Player.Pos.Y && x == g.Player.Pos.X {
|
||||
continue
|
||||
}
|
||||
|
||||
g.move(y, x)
|
||||
|
||||
if g.inch() == Floor {
|
||||
g.addch(' ')
|
||||
}
|
||||
@@ -198,6 +219,7 @@ func (g *RogueGame) showFloor() bool {
|
||||
if g.Player.Room.Flags&(Gone|Dark) == Dark && !g.Player.On(Blind) {
|
||||
return g.Options.SeeFloor
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -208,6 +230,7 @@ func (g *RogueGame) findObj(y, x int) *Object {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -217,34 +240,43 @@ func (g *RogueGame) eat() {
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindFood {
|
||||
if !g.Options.Terse {
|
||||
g.msg("ugh, you would get ill if you ate that")
|
||||
} else {
|
||||
g.msg("that's Inedible!")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
if p.FoodLeft < 0 {
|
||||
p.FoodLeft = 0
|
||||
}
|
||||
|
||||
if p.FoodLeft += HungerTime - 200 + g.rnd(400); p.FoodLeft > StomachSize {
|
||||
p.FoodLeft = StomachSize
|
||||
}
|
||||
|
||||
p.HungryState = 0
|
||||
if obj == p.CurWeapon {
|
||||
p.CurWeapon = nil
|
||||
}
|
||||
if obj.Which == 1 {
|
||||
|
||||
switch {
|
||||
case obj.Which == 1:
|
||||
g.msg("my, that was a yummy %s", g.Fruit)
|
||||
} else if g.rnd(100) > 70 {
|
||||
case g.rnd(100) > 70:
|
||||
p.Stats.Exp++
|
||||
|
||||
g.msg("%s, this food tastes awful", g.chooseStr("bummer", "yuk"))
|
||||
g.checkLevel()
|
||||
} else {
|
||||
default:
|
||||
g.msg("%s, that tasted good", g.chooseStr("oh, wow", "yum"))
|
||||
}
|
||||
|
||||
g.leavePack(obj, false, false)
|
||||
}
|
||||
|
||||
@@ -252,19 +284,23 @@ func (g *RogueGame) eat() {
|
||||
// check_level).
|
||||
func (g *RogueGame) checkLevel() {
|
||||
p := &g.Player
|
||||
|
||||
var i int
|
||||
for i = 0; eLevels[i] != 0; i++ {
|
||||
if eLevels[i] > p.Stats.Exp {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
i++
|
||||
olevel := p.Stats.Lvl
|
||||
|
||||
p.Stats.Lvl = i
|
||||
if i > olevel {
|
||||
add := g.roll(i-olevel, 10)
|
||||
p.Stats.MaxHP += add
|
||||
p.Stats.HP += add
|
||||
|
||||
g.msg("welcome to level %d", i)
|
||||
}
|
||||
}
|
||||
@@ -275,15 +311,19 @@ func (g *RogueGame) chgStr(amt int) {
|
||||
if amt == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
addStr(&p.Stats.Str, amt)
|
||||
|
||||
comp := p.Stats.Str
|
||||
if p.IsRing(Left, RingAddStrength) {
|
||||
addStr(&comp, -p.CurRing[Left].Bonus)
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingAddStrength) {
|
||||
addStr(&comp, -p.CurRing[Right].Bonus)
|
||||
}
|
||||
|
||||
if comp > p.MaxStats.Str {
|
||||
p.MaxStats.Str = comp
|
||||
}
|
||||
@@ -303,15 +343,20 @@ func (g *RogueGame) addHaste(potion bool) bool {
|
||||
p := &g.Player
|
||||
if p.On(Hasted) {
|
||||
g.NoCommand += g.rnd(8)
|
||||
|
||||
p.Flags.Clear(Awake | Hasted)
|
||||
g.Extinguish(DNohaste)
|
||||
g.msg("you faint from exhaustion")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
p.Flags.Set(Hasted)
|
||||
|
||||
if potion {
|
||||
g.Fuse(DNohaste, 0, g.rnd(4)+4, After)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -330,10 +375,12 @@ func vowelstr(str string) string {
|
||||
if str == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch str[0] {
|
||||
case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U':
|
||||
return "n"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -343,15 +390,19 @@ func (g *RogueGame) isCurrent(obj *Object) bool {
|
||||
if obj == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
if obj == p.CurArmor || obj == p.CurWeapon ||
|
||||
obj == p.CurRing[Left] || obj == p.CurRing[Right] {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("That's already ")
|
||||
g.addmsgf("That's already ")
|
||||
}
|
||||
|
||||
g.msg("in use")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -367,8 +418,10 @@ func (g *RogueGame) getDir() bool {
|
||||
prompt = "which direction? "
|
||||
g.msg("%s", prompt)
|
||||
}
|
||||
|
||||
for {
|
||||
gotit := true
|
||||
|
||||
switch g.DirCh = g.readchar(); g.DirCh {
|
||||
case 'h', 'H':
|
||||
g.Delta = Coord{X: -1, Y: 0}
|
||||
@@ -389,30 +442,38 @@ func (g *RogueGame) getDir() bool {
|
||||
case Escape:
|
||||
g.LastDir = 0
|
||||
g.resetLast()
|
||||
|
||||
return false
|
||||
default:
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("%s", prompt)
|
||||
|
||||
gotit = false
|
||||
}
|
||||
|
||||
if gotit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
g.DirCh = toLower(g.DirCh)
|
||||
g.LastDir = g.DirCh
|
||||
g.lastDelt = g.Delta
|
||||
}
|
||||
|
||||
if g.Player.On(Confused) && g.rnd(5) == 0 {
|
||||
for {
|
||||
g.Delta.Y = g.rnd(3) - 1
|
||||
|
||||
g.Delta.X = g.rnd(3) - 1
|
||||
if g.Delta.Y != 0 || g.Delta.X != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -422,6 +483,7 @@ func (g *RogueGame) callIt(info *ObjInfo) {
|
||||
info.Guess = ""
|
||||
} else if info.Guess == "" {
|
||||
g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? "))
|
||||
|
||||
buf := ""
|
||||
if g.getStr(&buf, g.scr.Std) == Norm {
|
||||
if buf != "" {
|
||||
@@ -445,6 +507,7 @@ func (g *RogueGame) rndThing() byte {
|
||||
} else {
|
||||
i = g.rnd(len(thingList) - 1)
|
||||
}
|
||||
|
||||
return thingList[i]
|
||||
}
|
||||
|
||||
@@ -454,6 +517,7 @@ func (g *RogueGame) chooseStr(ts, ns string) string {
|
||||
if g.Player.On(Hallucinating) {
|
||||
return ts
|
||||
}
|
||||
|
||||
return ns
|
||||
}
|
||||
|
||||
@@ -463,8 +527,10 @@ func unctrl(ch byte) string {
|
||||
if ch < ' ' {
|
||||
return "^" + string(ch+'@')
|
||||
}
|
||||
|
||||
if ch == 0x7f {
|
||||
return "^?"
|
||||
}
|
||||
|
||||
return string(ch)
|
||||
}
|
||||
|
||||
@@ -21,14 +21,17 @@ func (g *RogueGame) randMonster(wander bool) byte {
|
||||
if wander {
|
||||
mons = &wandMons
|
||||
}
|
||||
|
||||
for {
|
||||
d := g.Depth + (g.rnd(10) - 6)
|
||||
if d < 0 {
|
||||
d = g.rnd(5)
|
||||
}
|
||||
|
||||
if d > 25 {
|
||||
d = g.rnd(5) + 21
|
||||
}
|
||||
|
||||
if mons[d] != 0 {
|
||||
return mons[d]
|
||||
}
|
||||
@@ -38,10 +41,8 @@ func (g *RogueGame) randMonster(wander bool) byte {
|
||||
// newMonster picks a new monster and adds it to the list (monsters.c
|
||||
// new_monster).
|
||||
func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
|
||||
levAdd := g.Depth - AmuletLevel
|
||||
if levAdd < 0 {
|
||||
levAdd = 0
|
||||
}
|
||||
levAdd := max(g.Depth-AmuletLevel, 0)
|
||||
|
||||
attachMon(&g.Level.Monsters, tp)
|
||||
tp.Type = typ
|
||||
tp.Disguise = typ
|
||||
@@ -58,15 +59,19 @@ func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
|
||||
tp.Stats.Dmg = mp.Stats.Dmg
|
||||
tp.Stats.Str = mp.Stats.Str
|
||||
tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp)
|
||||
|
||||
tp.Flags = mp.Flags
|
||||
if g.Depth > 29 {
|
||||
tp.Flags.Set(Hasted)
|
||||
}
|
||||
|
||||
tp.Turn = true
|
||||
tp.Pack = nil
|
||||
|
||||
if g.Player.IsWearing(RingAggravateMonsters) {
|
||||
g.runto(cp)
|
||||
}
|
||||
|
||||
if typ == 'X' {
|
||||
tp.Disguise = g.rndThing()
|
||||
}
|
||||
@@ -81,11 +86,13 @@ func expAdd(tp *Monster) int {
|
||||
} else {
|
||||
mod = tp.Stats.MaxHP / 6
|
||||
}
|
||||
|
||||
if tp.Stats.Lvl > 9 {
|
||||
mod *= 20
|
||||
} else if tp.Stats.Lvl > 6 {
|
||||
mod *= 4
|
||||
}
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
@@ -93,23 +100,29 @@ func expAdd(tp *Monster) int {
|
||||
// (monsters.c wanderer).
|
||||
func (g *RogueGame) wanderer() {
|
||||
tp := &Monster{}
|
||||
|
||||
var cp Coord
|
||||
for {
|
||||
cp, _ = g.findFloor(nil, 0, true)
|
||||
cp, _ = g.findFloor(true)
|
||||
if g.roomin(cp) != g.Player.Room {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
g.newMonster(tp, g.randMonster(true), cp)
|
||||
|
||||
if g.Player.On(SenseMonsters) {
|
||||
g.standout()
|
||||
|
||||
if !g.Player.On(Hallucinating) {
|
||||
g.addch(tp.Type)
|
||||
} else {
|
||||
g.addch(byte(g.rnd(26) + 'A'))
|
||||
g.addch(g.randomMonsterLetter())
|
||||
}
|
||||
|
||||
g.standend()
|
||||
}
|
||||
|
||||
g.runto(tp.Pos)
|
||||
}
|
||||
|
||||
@@ -117,10 +130,12 @@ func (g *RogueGame) wanderer() {
|
||||
// (monsters.c wake_monster).
|
||||
func (g *RogueGame) wakeMonster(y, x int) *Monster {
|
||||
p := &g.Player
|
||||
|
||||
tp := g.Level.MonsterAt(y, x)
|
||||
if tp == nil {
|
||||
panic("can't find monster in wake_monster")
|
||||
}
|
||||
|
||||
ch := tp.Type
|
||||
// Every time he sees a mean monster, it might start chasing him
|
||||
if !tp.On(Awake) && g.rnd(3) != 0 && tp.On(Mean) && !tp.On(Held) &&
|
||||
@@ -128,24 +143,30 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
|
||||
tp.Dest = &p.Pos
|
||||
tp.Flags.Set(Awake)
|
||||
}
|
||||
|
||||
if ch == 'M' && !p.On(Blind) && !p.On(Hallucinating) &&
|
||||
!tp.On(Found) && !tp.On(Cancelled) && tp.On(Awake) {
|
||||
rp := p.Room
|
||||
if (rp != nil && !rp.Flags.Has(Dark)) ||
|
||||
distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
||||
tp.Flags.Set(Found)
|
||||
|
||||
if !g.save(VsMagic) {
|
||||
if p.On(Confused) {
|
||||
g.Lengthen(DUnconfuse, g.spread(HuhDuration))
|
||||
} else {
|
||||
g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After)
|
||||
}
|
||||
|
||||
p.Flags.Set(Confused)
|
||||
|
||||
mname := g.setMname(tp)
|
||||
g.addmsg("%s", mname)
|
||||
g.addmsgf("%s", mname)
|
||||
|
||||
if mname != "it" {
|
||||
g.addmsg("'")
|
||||
g.addmsgf("'")
|
||||
}
|
||||
|
||||
g.msg("s gaze has confused you")
|
||||
}
|
||||
}
|
||||
@@ -153,12 +174,14 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -174,6 +197,7 @@ func (g *RogueGame) givePack(tp *Monster) {
|
||||
// save_throw).
|
||||
func (g *RogueGame) saveThrow(which int, st *Stats) bool {
|
||||
need := 14 + which - st.Lvl/2
|
||||
|
||||
return g.roll(1, 20) >= need
|
||||
}
|
||||
|
||||
@@ -185,9 +209,17 @@ func (g *RogueGame) save(which int) bool {
|
||||
if p.IsRing(Left, RingProtection) {
|
||||
which -= p.CurRing[Left].Bonus
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingProtection) {
|
||||
which -= p.CurRing[Right].Bonus
|
||||
}
|
||||
}
|
||||
|
||||
return g.saveThrow(which, &p.Stats)
|
||||
}
|
||||
|
||||
// randomMonsterLetter picks a random monster display letter, used by the
|
||||
// hallucination effects (the C rnd(26)+'A' idiom).
|
||||
func (g *RogueGame) randomMonsterLetter() byte {
|
||||
return byte(g.rnd(26) + 'A') //nolint:gosec // G115: 'A'..'Z' fits a byte
|
||||
}
|
||||
|
||||
56
game/move.go
56
game/move.go
@@ -13,10 +13,12 @@ func (g *RogueGame) doRun(ch byte) {
|
||||
// fighting, picking up, etc. (move.c do_move).
|
||||
func (g *RogueGame) doMove(dy, dx int) {
|
||||
p := &g.Player
|
||||
|
||||
g.Firstmove = false
|
||||
if g.NoMove > 0 {
|
||||
g.NoMove--
|
||||
g.msg("you are still stuck in the bear trap")
|
||||
|
||||
return
|
||||
}
|
||||
// Do a confused move (maybe)
|
||||
@@ -27,6 +29,7 @@ func (g *RogueGame) doMove(dy, dx int) {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
g.ToDeath = false
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -37,19 +40,27 @@ over:
|
||||
// Check if he tried to move off the screen or make an illegal diagonal
|
||||
// move, and stop him if he did.
|
||||
hitBound := nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1
|
||||
var ch byte
|
||||
var fl PlaceFlags
|
||||
|
||||
var (
|
||||
ch byte
|
||||
fl PlaceFlags
|
||||
)
|
||||
|
||||
if !hitBound {
|
||||
if !g.diagOk(p.Pos, nh) {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if g.Running && p.Pos == nh {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
fl = *g.Level.FlagsAt(nh.Y, nh.X)
|
||||
|
||||
ch = g.Level.VisibleChar(nh.Y, nh.X)
|
||||
if !fl.Has(FReal) && ch == Floor {
|
||||
if !p.On(Levitating) {
|
||||
@@ -59,20 +70,25 @@ over:
|
||||
}
|
||||
} else if p.On(Held) && ch != 'F' {
|
||||
g.msg("you are being held")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if hitBound {
|
||||
ch = ' ' // fall into the wall case below
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case ' ', '|', '-':
|
||||
if g.Options.PassGo && g.Running && p.Room.Flags.Has(Gone) &&
|
||||
!p.On(Blind) {
|
||||
var b1, b2 bool
|
||||
|
||||
switch g.RunCh {
|
||||
case 'h', 'l':
|
||||
b1 = p.Pos.Y != 1 && g.turnOk(p.Pos.Y-1, p.Pos.X)
|
||||
|
||||
b2 = p.Pos.Y != NumLines-2 && g.turnOk(p.Pos.Y+1, p.Pos.X)
|
||||
if b1 != b2 {
|
||||
if b1 {
|
||||
@@ -82,13 +98,18 @@ over:
|
||||
g.RunCh = 'j'
|
||||
dy = 1
|
||||
}
|
||||
|
||||
dx = 0
|
||||
|
||||
g.turnref()
|
||||
|
||||
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
|
||||
|
||||
goto over
|
||||
}
|
||||
case 'j', 'k':
|
||||
b1 = p.Pos.X != 0 && g.turnOk(p.Pos.Y, p.Pos.X-1)
|
||||
|
||||
b2 = p.Pos.X != NumCols-1 && g.turnOk(p.Pos.Y, p.Pos.X+1)
|
||||
if b1 != b2 {
|
||||
if b1 {
|
||||
@@ -98,13 +119,18 @@ over:
|
||||
g.RunCh = 'l'
|
||||
dx = 1
|
||||
}
|
||||
|
||||
dy = 0
|
||||
|
||||
g.turnref()
|
||||
|
||||
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
|
||||
|
||||
goto over
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.Running = false
|
||||
g.After = false
|
||||
case Door:
|
||||
@@ -112,12 +138,14 @@ over:
|
||||
if g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Has(FPassage) {
|
||||
g.enterRoom(nh)
|
||||
}
|
||||
|
||||
g.moveStuff(nh, fl)
|
||||
case Trap:
|
||||
tr := g.beTrapped(nh)
|
||||
if tr == TrapDoor || tr == TrapTeleport {
|
||||
return
|
||||
}
|
||||
|
||||
g.moveStuff(nh, fl)
|
||||
case Passage:
|
||||
// when you're in a corridor, you don't know if you're in a maze
|
||||
@@ -130,11 +158,13 @@ over:
|
||||
if !fl.Has(FReal) {
|
||||
g.beTrapped(p.Pos)
|
||||
}
|
||||
|
||||
g.moveStuff(nh, fl)
|
||||
default:
|
||||
if ch == Stairs {
|
||||
g.SeenStairs = true
|
||||
}
|
||||
|
||||
g.Running = false
|
||||
if isUpper(ch) || g.Level.MonsterAt(nh.Y, nh.X) != nil {
|
||||
g.fight(nh, p.CurWeapon, false)
|
||||
@@ -142,6 +172,7 @@ over:
|
||||
if ch != Stairs {
|
||||
g.Take = ch
|
||||
}
|
||||
|
||||
g.moveStuff(nh, fl)
|
||||
}
|
||||
}
|
||||
@@ -151,9 +182,11 @@ over:
|
||||
func (g *RogueGame) moveStuff(nh Coord, fl PlaceFlags) {
|
||||
p := &g.Player
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
|
||||
|
||||
if fl.Has(FPassage) && g.Level.Char(g.Oldpos.Y, g.Oldpos.X) == Door {
|
||||
g.leaveRoom(nh)
|
||||
}
|
||||
|
||||
p.Pos = nh
|
||||
}
|
||||
|
||||
@@ -161,17 +194,20 @@ func (g *RogueGame) moveStuff(nh Coord, fl PlaceFlags) {
|
||||
// (move.c turn_ok).
|
||||
func (g *RogueGame) turnOk(y, x int) bool {
|
||||
pp := g.Level.At(y, x)
|
||||
|
||||
return pp.Ch == Door || pp.Flags&(FReal|FPassage) == (FReal|FPassage)
|
||||
}
|
||||
|
||||
// turnref decides whether to refresh at a passage turning (move.c turnref).
|
||||
func (g *RogueGame) turnref() {
|
||||
p := &g.Player
|
||||
|
||||
pp := g.Level.At(p.Pos.Y, p.Pos.X)
|
||||
if !pp.Flags.Has(FSeen) {
|
||||
if g.Options.Jump {
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
pp.Flags.Set(FSeen)
|
||||
}
|
||||
}
|
||||
@@ -182,6 +218,7 @@ func (g *RogueGame) doorOpen(rp *Room) {
|
||||
if rp.Flags.Has(Gone) {
|
||||
return
|
||||
}
|
||||
|
||||
for y := rp.Pos.Y; y < rp.Pos.Y+rp.Max.Y; y++ {
|
||||
for x := rp.Pos.X; x < rp.Pos.X+rp.Max.X; x++ {
|
||||
if isUpper(g.Level.VisibleChar(y, x)) {
|
||||
@@ -197,12 +234,14 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
|
||||
if p.On(Levitating) {
|
||||
return TrapRust // anything that's not a door or teleport
|
||||
}
|
||||
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
pp := g.Level.At(tc.Y, tc.X)
|
||||
pp.Ch = Trap
|
||||
tr := TrapKind(pp.Flags & FTrapMask)
|
||||
pp.Flags.Set(FSeen)
|
||||
|
||||
switch tr {
|
||||
case TrapDoor:
|
||||
g.Depth++
|
||||
@@ -238,6 +277,7 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
|
||||
}
|
||||
case TrapSleep:
|
||||
g.NoCommand += g.spread(5) // SLEEPTIME
|
||||
|
||||
p.Flags.Clear(Awake)
|
||||
g.msg("a strange white mist envelops you and you fall asleep")
|
||||
case TrapArrow:
|
||||
@@ -271,16 +311,20 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
|
||||
g.msg("a poisoned dart killed you")
|
||||
g.death('d')
|
||||
}
|
||||
|
||||
if !p.IsWearing(RingSustainStrength) && !g.save(VsPoison) {
|
||||
g.chgStr(-1)
|
||||
}
|
||||
|
||||
g.msg("a small dart just hit you in the shoulder")
|
||||
}
|
||||
case TrapRust:
|
||||
g.msg("a gush of water hits you on the head")
|
||||
g.rustArmor(p.CurArmor)
|
||||
}
|
||||
|
||||
g.flushType()
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
@@ -296,25 +340,32 @@ func (g *RogueGame) rndmove(who *Creature) Coord {
|
||||
if ret == who.Pos {
|
||||
return ret
|
||||
}
|
||||
|
||||
if !g.diagOk(who.Pos, ret) {
|
||||
return who.Pos
|
||||
}
|
||||
|
||||
ch := g.Level.VisibleChar(ret.Y, ret.X)
|
||||
if !stepOk(ch) {
|
||||
return who.Pos
|
||||
}
|
||||
|
||||
if ch == Scroll {
|
||||
var found *Object
|
||||
|
||||
for _, obj := range g.Level.Objects {
|
||||
if ret.Y == obj.Pos.Y && ret.X == obj.Pos.X {
|
||||
found = obj
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found != nil && found.ScrollKind() == ScrollScareMonster {
|
||||
return who.Pos
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -332,6 +383,7 @@ func (g *RogueGame) rustArmor(arm *Object) {
|
||||
}
|
||||
} else {
|
||||
arm.ArmorClass++
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.msg("your armor appears to be weaker now. Oh my!")
|
||||
} else {
|
||||
|
||||
@@ -13,6 +13,7 @@ const (
|
||||
func (g *RogueGame) NewLevel() {
|
||||
p := &g.Player
|
||||
p.Flags.Clear(Held) // unhold when you go down just in case
|
||||
|
||||
if g.Depth > g.MaxDepth {
|
||||
g.MaxDepth = g.Depth
|
||||
}
|
||||
@@ -20,6 +21,7 @@ func (g *RogueGame) NewLevel() {
|
||||
for i := range g.Level.Places {
|
||||
g.Level.Places[i] = Place{Ch: ' ', Flags: FReal}
|
||||
}
|
||||
|
||||
g.clear()
|
||||
// Free up the monsters on the last level; the objects and their packs
|
||||
// go with them (the garbage collector is our free_list).
|
||||
@@ -27,32 +29,33 @@ func (g *RogueGame) NewLevel() {
|
||||
g.Level.Objects = nil
|
||||
g.doRooms() // Draw rooms
|
||||
g.doPassages() // Draw passages
|
||||
|
||||
p.NoFood++
|
||||
|
||||
g.putThings() // Place objects (if any)
|
||||
// Place the traps
|
||||
if g.rnd(10) < g.Depth {
|
||||
g.Level.TrapCount = g.rnd(g.Depth/4) + 1
|
||||
if g.Level.TrapCount > MaxTraps {
|
||||
g.Level.TrapCount = MaxTraps
|
||||
}
|
||||
g.Level.TrapCount = min(g.rnd(g.Depth/4)+1, MaxTraps)
|
||||
|
||||
for i := g.Level.TrapCount; i > 0; i-- {
|
||||
// not only wouldn't it be NICE to have traps in mazes (not
|
||||
// that we care about being nice), since the trap number is
|
||||
// stored where the passage number is, we can't actually do it.
|
||||
var stairs Coord
|
||||
for {
|
||||
stairs, _ = g.findFloor(nil, 0, false)
|
||||
stairs, _ = g.findFloor(false)
|
||||
if g.Level.Char(stairs.Y, stairs.X) == Floor {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sp := g.Level.FlagsAt(stairs.Y, stairs.X)
|
||||
sp.Clear(FReal)
|
||||
*sp |= PlaceFlags(g.rnd(NumTrapTypes))
|
||||
*sp |= PlaceFlags(g.rnd(NumTrapTypes)) //nolint:gosec // G115: 0..7 fits
|
||||
}
|
||||
}
|
||||
// Place the staircase down.
|
||||
stairs, _ := g.findFloor(nil, 0, false)
|
||||
stairs, _ := g.findFloor(false)
|
||||
g.Level.Stairs = stairs
|
||||
g.Level.SetChar(stairs.Y, stairs.X, Stairs)
|
||||
g.SeenStairs = false
|
||||
@@ -61,13 +64,15 @@ func (g *RogueGame) NewLevel() {
|
||||
tp.Room = g.roomin(tp.Pos)
|
||||
}
|
||||
|
||||
hero, _ := g.findFloor(nil, 0, true)
|
||||
hero, _ := g.findFloor(true)
|
||||
p.Pos = hero
|
||||
g.enterRoom(hero)
|
||||
g.mvaddch(hero.Y, hero.X, PlayerCh)
|
||||
|
||||
if p.On(SenseMonsters) {
|
||||
g.turnSee(false)
|
||||
}
|
||||
|
||||
if p.On(Hallucinating) {
|
||||
g.visuals(0)
|
||||
}
|
||||
@@ -96,13 +101,13 @@ func (g *RogueGame) putThings() {
|
||||
g.treasRoom()
|
||||
}
|
||||
// Do MAXOBJ attempts to put things on a level
|
||||
for i := 0; i < MaxObj; i++ {
|
||||
for range MaxObj {
|
||||
if g.rnd(100) < 36 {
|
||||
// Pick a new object and link it in the list
|
||||
obj := g.newThing()
|
||||
attachObj(&g.Level.Objects, obj)
|
||||
// Put it somewhere
|
||||
obj.Pos, _ = g.findFloor(nil, 0, false)
|
||||
obj.Pos, _ = g.findFloor(false)
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
|
||||
}
|
||||
}
|
||||
@@ -116,7 +121,7 @@ func (g *RogueGame) putThings() {
|
||||
obj.ArmorClass = 11
|
||||
obj.Kind = KindAmulet
|
||||
// Put it somewhere
|
||||
obj.Pos, _ = g.findFloor(nil, 0, false)
|
||||
obj.Pos, _ = g.findFloor(false)
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Amulet)
|
||||
}
|
||||
}
|
||||
@@ -124,10 +129,9 @@ func (g *RogueGame) putThings() {
|
||||
// treasRoom adds a treasure room (new_level.c treas_room).
|
||||
func (g *RogueGame) treasRoom() {
|
||||
rp := &g.Level.Rooms[g.rndRoom()]
|
||||
spots := (rp.Max.Y-2)*(rp.Max.X-2) - minTreas
|
||||
if spots > maxTreas-minTreas {
|
||||
spots = maxTreas - minTreas
|
||||
}
|
||||
|
||||
spots := min((rp.Max.Y-2)*(rp.Max.X-2)-minTreas, maxTreas-minTreas)
|
||||
|
||||
numMonst := g.rnd(spots) + minTreas
|
||||
for nm := numMonst; nm > 0; nm-- {
|
||||
mp, _ := g.findFloorIn(rp, 2*maxTries, false)
|
||||
@@ -138,14 +142,13 @@ func (g *RogueGame) treasRoom() {
|
||||
}
|
||||
|
||||
// fill up room with monsters from the next level down
|
||||
nm := g.rnd(spots) + minTreas
|
||||
if nm < numMonst+2 {
|
||||
nm = numMonst + 2
|
||||
}
|
||||
nm := max(g.rnd(spots)+minTreas, numMonst+2)
|
||||
|
||||
spots = (rp.Max.Y - 2) * (rp.Max.X - 2)
|
||||
if nm > spots {
|
||||
nm = spots
|
||||
}
|
||||
|
||||
g.Depth++
|
||||
for ; nm > 0; nm-- {
|
||||
if mp, ok := g.findFloorIn(rp, maxTries, true); ok {
|
||||
@@ -155,5 +158,6 @@ func (g *RogueGame) treasRoom() {
|
||||
g.givePack(tp)
|
||||
}
|
||||
}
|
||||
|
||||
g.Depth--
|
||||
}
|
||||
|
||||
@@ -7,24 +7,30 @@ import (
|
||||
|
||||
func genLevel(t *testing.T, seed int32) *RogueGame {
|
||||
t.Helper()
|
||||
|
||||
g := NewGame(Config{Seed: seed})
|
||||
g.NewLevel()
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// renderMap draws the raw level map (not the screen) as text.
|
||||
func renderMap(g *RogueGame) string {
|
||||
var sb strings.Builder
|
||||
for y := 0; y < NumLines; y++ {
|
||||
for x := 0; x < NumCols; x++ {
|
||||
|
||||
for y := range NumLines {
|
||||
for x := range NumCols {
|
||||
ch := g.Level.Char(y, x)
|
||||
if m := g.Level.MonsterAt(y, x); m != nil {
|
||||
ch = m.Type
|
||||
}
|
||||
|
||||
sb.WriteByte(ch)
|
||||
}
|
||||
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -44,9 +50,11 @@ func TestNewLevelInvariants(t *testing.T) {
|
||||
t.Errorf("seed %d: hero on unwalkable cell %q", seed,
|
||||
g.Level.Char(hp.Y, hp.X))
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(hp.Y, hp.X) != nil {
|
||||
t.Errorf("seed %d: hero standing on a monster", seed)
|
||||
}
|
||||
|
||||
if g.Player.Room == nil {
|
||||
t.Errorf("seed %d: hero not in any room", seed)
|
||||
}
|
||||
@@ -56,6 +64,7 @@ func TestNewLevelInvariants(t *testing.T) {
|
||||
if !strings.Contains(m, "|") || !strings.Contains(m, "-") {
|
||||
t.Errorf("seed %d: no room walls drawn", seed)
|
||||
}
|
||||
|
||||
if !strings.Contains(m, ".") && !strings.Contains(m, "#") {
|
||||
t.Errorf("seed %d: no floor or passages drawn", seed)
|
||||
}
|
||||
@@ -66,6 +75,7 @@ func TestNewLevelInvariants(t *testing.T) {
|
||||
t.Errorf("seed %d: monster %c not indexed at its position",
|
||||
seed, mon.Type)
|
||||
}
|
||||
|
||||
if mon.Room == nil {
|
||||
t.Errorf("seed %d: monster %c has no room", seed, mon.Type)
|
||||
}
|
||||
@@ -87,10 +97,12 @@ func TestNewLevelInvariants(t *testing.T) {
|
||||
t.Errorf("seed %d: starting pack has %d items, want 5",
|
||||
seed, len(g.Player.Pack))
|
||||
}
|
||||
|
||||
if g.Player.CurWeapon == nil ||
|
||||
g.Player.CurWeapon.WeaponKind() != WeaponMace {
|
||||
t.Errorf("seed %d: not wielding the starting mace", seed)
|
||||
}
|
||||
|
||||
if g.Player.CurArmor == nil ||
|
||||
g.Player.CurArmor.ArmorKind() != ArmorRingMail {
|
||||
t.Errorf("seed %d: not wearing the starting ring mail", seed)
|
||||
@@ -100,10 +112,12 @@ func TestNewLevelInvariants(t *testing.T) {
|
||||
|
||||
func TestNewLevelDeterministic(t *testing.T) {
|
||||
a := renderMap(genLevel(t, 12345))
|
||||
|
||||
b := renderMap(genLevel(t, 12345))
|
||||
if a != b {
|
||||
t.Error("same seed produced different levels")
|
||||
}
|
||||
|
||||
c := renderMap(genLevel(t, 54321))
|
||||
if a == c {
|
||||
t.Error("different seeds produced identical levels")
|
||||
@@ -118,6 +132,7 @@ func TestDeeperLevels(t *testing.T) {
|
||||
for depth := 1; depth <= 30; depth++ {
|
||||
g.Depth = depth
|
||||
g.NewLevel()
|
||||
|
||||
st := g.Level.Stairs
|
||||
if g.Level.Char(st.Y, st.X) != Stairs {
|
||||
t.Fatalf("seed %d depth %d: missing staircase", seed, depth)
|
||||
|
||||
@@ -5,6 +5,7 @@ package game
|
||||
// category (ObjectKind) from its display character (Glyph).
|
||||
type ObjectKind int
|
||||
|
||||
// Item categories.
|
||||
const (
|
||||
KindNone ObjectKind = iota
|
||||
KindPotion
|
||||
@@ -44,6 +45,7 @@ func (k ObjectKind) Glyph() byte {
|
||||
if k < 0 || int(k) >= len(kindGlyphs) {
|
||||
return ' '
|
||||
}
|
||||
|
||||
return kindGlyphs[k]
|
||||
}
|
||||
|
||||
@@ -71,6 +73,7 @@ func (k ObjectKind) String() string {
|
||||
case KindRingOrStick:
|
||||
return "ring, wand or staff"
|
||||
}
|
||||
|
||||
return "bizarre thing"
|
||||
}
|
||||
|
||||
@@ -82,6 +85,7 @@ func objectKindForGlyph(ch byte) ObjectKind {
|
||||
return ObjectKind(k)
|
||||
}
|
||||
}
|
||||
|
||||
return KindNone
|
||||
}
|
||||
|
||||
@@ -148,6 +152,7 @@ func detachObj(list *[]*Object, item *Object) {
|
||||
for i, o := range *list {
|
||||
if o == item {
|
||||
*list = append((*list)[:i], (*list)[i+1:]...)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ type optDesc struct {
|
||||
// optList builds the options.c optlist for this game.
|
||||
func (g *RogueGame) optList() []optDesc {
|
||||
o := &g.Options
|
||||
|
||||
return []optDesc{
|
||||
{"terse", "Terse output", optBool, &o.Terse, nil, nil},
|
||||
{"flush", "Flush typeahead during battle", optBool, &o.FightFlush, nil, nil},
|
||||
@@ -46,6 +47,7 @@ func (g *RogueGame) optList() []optDesc {
|
||||
func (g *RogueGame) option() {
|
||||
hw := g.scr.Hw
|
||||
optlist := g.optList()
|
||||
|
||||
hw.Clear()
|
||||
// Display current values of options
|
||||
for i := range optlist {
|
||||
@@ -56,9 +58,11 @@ func (g *RogueGame) option() {
|
||||
}
|
||||
// Set values
|
||||
hw.Move(0, 0)
|
||||
|
||||
for i := 0; i < len(optlist); i++ {
|
||||
op := &optlist[i]
|
||||
g.prOptname(op)
|
||||
|
||||
retval := g.getOpt(op)
|
||||
if retval != Norm {
|
||||
if retval == Quit {
|
||||
@@ -70,6 +74,7 @@ func (g *RogueGame) option() {
|
||||
i -= 2
|
||||
} else { // trying to back up beyond the top
|
||||
hw.Move(0, 0)
|
||||
|
||||
i--
|
||||
}
|
||||
}
|
||||
@@ -84,13 +89,14 @@ func (g *RogueGame) option() {
|
||||
|
||||
// prOptname prints out the option name prompt (options.c pr_optname).
|
||||
func (g *RogueGame) prOptname(op *optDesc) {
|
||||
g.scr.Hw.Printw("%s (\"%s\"): ", op.prompt, op.name)
|
||||
g.scr.Hw.Printwf("%s (\"%s\"): ", op.prompt, op.name)
|
||||
}
|
||||
|
||||
// putOpt prints an option's current value (options.c put_bool/put_str/
|
||||
// put_inv_t).
|
||||
func (g *RogueGame) putOpt(op *optDesc) {
|
||||
hw := g.scr.Hw
|
||||
|
||||
switch op.kind {
|
||||
case optBool, optSeeFloor:
|
||||
hw.AddStr(boolStr(*op.boolP))
|
||||
@@ -105,6 +111,7 @@ func boolStr(b bool) string {
|
||||
if b {
|
||||
return "True"
|
||||
}
|
||||
|
||||
return "False"
|
||||
}
|
||||
|
||||
@@ -129,9 +136,11 @@ func (g *RogueGame) getBool(bp *bool) int {
|
||||
win := g.scr.Hw
|
||||
oy, ox := win.GetYX()
|
||||
win.AddStr(boolStr(*bp))
|
||||
|
||||
for {
|
||||
win.Move(oy, ox)
|
||||
g.scr.RefreshWin(win)
|
||||
|
||||
switch g.readchar() {
|
||||
case 't', 'T':
|
||||
*bp = true
|
||||
@@ -145,13 +154,17 @@ func (g *RogueGame) getBool(bp *bool) int {
|
||||
default:
|
||||
win.Move(oy, ox+10)
|
||||
win.AddStr("(T or F)")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
win.Move(oy, ox)
|
||||
win.AddStr(boolStr(*bp))
|
||||
win.AddCh('\n')
|
||||
|
||||
return Norm
|
||||
}
|
||||
|
||||
@@ -159,10 +172,12 @@ func (g *RogueGame) getBool(bp *bool) int {
|
||||
// get_sf).
|
||||
func (g *RogueGame) getSf(bp *bool) int {
|
||||
wasSf := g.Options.SeeFloor
|
||||
|
||||
retval := g.getBool(bp)
|
||||
if retval == Quit {
|
||||
return Quit
|
||||
}
|
||||
|
||||
if wasSf != g.Options.SeeFloor {
|
||||
if !g.Options.SeeFloor {
|
||||
g.Options.SeeFloor = true
|
||||
@@ -172,6 +187,7 @@ func (g *RogueGame) getSf(bp *bool) int {
|
||||
g.look(false)
|
||||
}
|
||||
}
|
||||
|
||||
return Norm
|
||||
}
|
||||
|
||||
@@ -183,57 +199,74 @@ func (g *RogueGame) getStr(opt *string, win *Window) int {
|
||||
oy, ox := win.GetYX()
|
||||
g.scr.RefreshWin(win)
|
||||
// loop reading in the string, and put it in a temporary buffer
|
||||
var buf []byte
|
||||
var c byte
|
||||
var (
|
||||
buf []byte
|
||||
c byte
|
||||
)
|
||||
for {
|
||||
c = g.readchar()
|
||||
if c == '\n' || c == '\r' || c == Escape {
|
||||
break
|
||||
}
|
||||
|
||||
if c == 8 || c == 0x7f { // erase character
|
||||
if len(buf) > 0 {
|
||||
buf = buf[:len(buf)-1]
|
||||
win.Move(oy, ox+len(displayStr(buf)))
|
||||
}
|
||||
|
||||
win.Clrtoeol()
|
||||
g.scr.RefreshWin(win)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if c == CTRL('U') { // kill character
|
||||
buf = buf[:0]
|
||||
|
||||
win.Move(oy, ox)
|
||||
win.Clrtoeol()
|
||||
g.scr.RefreshWin(win)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
if c == '-' && !onStd {
|
||||
break
|
||||
}
|
||||
|
||||
if c == '~' {
|
||||
buf = append(buf, g.Home...)
|
||||
win.AddStr(g.Home)
|
||||
win.Clrtoeol()
|
||||
g.scr.RefreshWin(win)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(buf) >= MaxInp || !(isPrint(c) || c == ' ') {
|
||||
|
||||
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
|
||||
*opt = strucpy(string(buf))
|
||||
}
|
||||
win.MvPrintw(oy, ox, "%s\n", *opt)
|
||||
|
||||
win.MvPrintwf(oy, ox, "%s\n", *opt)
|
||||
g.scr.RefreshWin(win)
|
||||
|
||||
if onStd {
|
||||
g.Msgs.Mpos += len(buf)
|
||||
}
|
||||
|
||||
switch c {
|
||||
case '-':
|
||||
return Minus
|
||||
@@ -250,6 +283,7 @@ func displayStr(buf []byte) string {
|
||||
for _, c := range buf {
|
||||
sb.WriteString(unctrl(c))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -258,9 +292,11 @@ func (g *RogueGame) getInvT(ip *int) int {
|
||||
win := g.scr.Hw
|
||||
oy, ox := win.GetYX()
|
||||
win.AddStr(invTName[*ip])
|
||||
|
||||
for {
|
||||
win.Move(oy, ox)
|
||||
g.scr.RefreshWin(win)
|
||||
|
||||
switch g.readchar() {
|
||||
case 'o', 'O':
|
||||
*ip = InvOver
|
||||
@@ -276,11 +312,15 @@ func (g *RogueGame) getInvT(ip *int) int {
|
||||
default:
|
||||
win.Move(oy, ox+15)
|
||||
win.AddStr("(O, S, or C)")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
win.MvPrintw(oy, ox, "%s\n", invTName[*ip])
|
||||
|
||||
win.MvPrintwf(oy, ox, "%s\n", invTName[*ip])
|
||||
|
||||
return Norm
|
||||
}
|
||||
|
||||
@@ -289,20 +329,25 @@ func (g *RogueGame) getInvT(ip *int) int {
|
||||
// "noname" (false), strings as "name=..." (options.c parse_opts).
|
||||
func (g *RogueGame) ParseOpts(str string) {
|
||||
optlist := g.optList()
|
||||
|
||||
for str != "" {
|
||||
// Get option name
|
||||
i := 0
|
||||
for i < len(str) && isAlpha(str[i]) {
|
||||
i++
|
||||
}
|
||||
|
||||
name := str[:i]
|
||||
rest := str[i:]
|
||||
matched := false
|
||||
|
||||
for oi := range optlist {
|
||||
op := &optlist[oi]
|
||||
|
||||
isBoolOpt := op.kind == optBool || op.kind == optSeeFloor
|
||||
if strings.HasPrefix(op.name, name) && name != "" {
|
||||
matched = true
|
||||
|
||||
if isBoolOpt {
|
||||
*op.boolP = true
|
||||
} else {
|
||||
@@ -310,10 +355,13 @@ func (g *RogueGame) ParseOpts(str string) {
|
||||
for rest != "" && rest[0] == '=' {
|
||||
rest = rest[1:]
|
||||
}
|
||||
|
||||
val := rest
|
||||
|
||||
var prefix string
|
||||
if val != "" && val[0] == '~' {
|
||||
prefix = g.Home
|
||||
|
||||
val = val[1:]
|
||||
for val != "" && val[0] == '/' {
|
||||
val = val[1:]
|
||||
@@ -324,17 +372,21 @@ func (g *RogueGame) ParseOpts(str string) {
|
||||
if end < 0 {
|
||||
end = len(val)
|
||||
}
|
||||
|
||||
word := val[:end]
|
||||
rest = val[end:]
|
||||
|
||||
if op.kind == optInvT {
|
||||
// check for type of inventory
|
||||
w := word
|
||||
if w != "" {
|
||||
w = string(toUpper(w[0])) + w[1:]
|
||||
}
|
||||
|
||||
for ti, tn := range invTName {
|
||||
if strings.HasPrefix(tn, w) {
|
||||
*op.intP = ti
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -342,19 +394,23 @@ func (g *RogueGame) ParseOpts(str string) {
|
||||
*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
|
||||
}
|
||||
}
|
||||
@@ -365,11 +421,14 @@ func strucpy(s string) string {
|
||||
if len(s) > MaxInp {
|
||||
s = s[:MaxInp]
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for i := 0; i < len(s); i++ {
|
||||
|
||||
for i := range len(s) {
|
||||
if isPrint(s[i]) || s[i] == ' ' {
|
||||
sb.WriteByte(s[i])
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
123
game/pack.go
123
game/pack.go
@@ -7,10 +7,12 @@ package game
|
||||
func (g *RogueGame) addPack(obj *Object, silent bool) {
|
||||
p := &g.Player
|
||||
fromFloor := false
|
||||
|
||||
if obj == nil {
|
||||
if obj = g.findObj(p.Pos.Y, p.Pos.X); obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fromFloor = true
|
||||
}
|
||||
|
||||
@@ -18,12 +20,15 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
|
||||
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster && obj.Flags.Has(WasFound) {
|
||||
detachObj(&g.Level.Objects, 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
|
||||
}
|
||||
|
||||
@@ -38,9 +43,11 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
|
||||
// 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
|
||||
@@ -49,19 +56,23 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
|
||||
if i+1 >= len(p.Pack) {
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
op := p.Pack[i]
|
||||
if op.Kind == obj.Kind && op.Which == obj.Which {
|
||||
if op.Kind.MergesInPack() {
|
||||
switch {
|
||||
case op.Kind.MergesInPack():
|
||||
if !g.packRoom(fromFloor, obj) {
|
||||
return
|
||||
}
|
||||
|
||||
op.Count++
|
||||
obj = op
|
||||
lp = -1
|
||||
merged = true
|
||||
} else if obj.Group != 0 {
|
||||
case obj.Group != 0:
|
||||
lp = i
|
||||
for p.Pack[i].Kind == obj.Kind &&
|
||||
p.Pack[i].Which == obj.Which &&
|
||||
@@ -70,24 +81,29 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
|
||||
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
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
lp = i
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -95,6 +111,7 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
|
||||
if !g.packRoom(fromFloor, obj) {
|
||||
return
|
||||
}
|
||||
|
||||
obj.PackCh = g.packChar()
|
||||
p.Pack = append(p.Pack[:lp+1],
|
||||
append([]*Object{obj}, p.Pack[lp+1:]...)...)
|
||||
@@ -117,8 +134,9 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
|
||||
// Notify the user
|
||||
if !silent {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you now have ")
|
||||
g.addmsgf("you now have ")
|
||||
}
|
||||
|
||||
g.msg("%s (%c)", g.invName(obj, !g.Options.Terse), obj.PackCh)
|
||||
}
|
||||
}
|
||||
@@ -129,29 +147,37 @@ func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
|
||||
p := &g.Player
|
||||
if p.Inpack++; p.Inpack > MaxPack {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("there's ")
|
||||
g.addmsgf("there's ")
|
||||
}
|
||||
g.addmsg("no room")
|
||||
|
||||
g.addmsgf("no room")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" in your pack")
|
||||
g.addmsgf(" in your pack")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
|
||||
if fromFloor {
|
||||
g.moveMsg(obj)
|
||||
}
|
||||
|
||||
p.Inpack = MaxPack
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if fromFloor {
|
||||
detachObj(&g.Level.Objects, 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)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -159,13 +185,16 @@ func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
|
||||
func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object {
|
||||
p := &g.Player
|
||||
p.Inpack--
|
||||
|
||||
nobj := obj
|
||||
if obj.Count > 1 && !all {
|
||||
g.LastPick = obj
|
||||
|
||||
obj.Count--
|
||||
if obj.Group != 0 {
|
||||
p.Inpack++
|
||||
}
|
||||
|
||||
if newobj {
|
||||
copied := *obj
|
||||
nobj = &copied
|
||||
@@ -176,6 +205,7 @@ func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object {
|
||||
p.PackUsed[obj.PackCh-'a'] = false
|
||||
detachObj(&p.Pack, obj)
|
||||
}
|
||||
|
||||
return nobj
|
||||
}
|
||||
|
||||
@@ -185,9 +215,11 @@ func (g *RogueGame) packChar() byte {
|
||||
for i := range p.PackUsed {
|
||||
if !p.PackUsed[i] {
|
||||
p.PackUsed[i] = true
|
||||
|
||||
return byte(i) + 'a'
|
||||
}
|
||||
}
|
||||
|
||||
return byte(len(p.PackUsed)) + 'a' // C would walk off the array here
|
||||
}
|
||||
|
||||
@@ -195,24 +227,26 @@ func (g *RogueGame) packChar() byte {
|
||||
// of the given type (pack.c inventory).
|
||||
func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
|
||||
g.NObjs = 0
|
||||
|
||||
for _, item := range list {
|
||||
if kind != KindNone && kind != item.Kind &&
|
||||
!(kind == KindCallable && item.Kind != KindFood &&
|
||||
item.Kind != KindAmulet) &&
|
||||
!(kind == KindRingOrStick &&
|
||||
(item.Kind == KindRing || item.Kind == KindWand)) {
|
||||
if !matchesFilter(kind, item) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.NObjs++
|
||||
g.Msgs.MsgEsc = true
|
||||
|
||||
line := string(item.PackCh) + ") " + g.invName(item, false)
|
||||
if g.addLine("%s", line) == Escape {
|
||||
g.Msgs.MsgEsc = false
|
||||
g.msg("")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
g.Msgs.MsgEsc = false
|
||||
}
|
||||
|
||||
if g.NObjs == 0 {
|
||||
if g.Options.Terse {
|
||||
if kind == KindNone {
|
||||
@@ -227,9 +261,12 @@ func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
|
||||
g.msg("you don't have anything appropriate")
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
g.endLine()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -243,27 +280,49 @@ func (g *RogueGame) pickUp(ch byte) {
|
||||
obj := g.findObj(p.Pos.Y, p.Pos.X)
|
||||
if g.MoveOn {
|
||||
g.moveMsg(obj)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case Gold:
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
g.money(obj.GoldValue)
|
||||
detachObj(&g.Level.Objects, obj)
|
||||
|
||||
p.Room.GoldVal = 0
|
||||
default:
|
||||
g.addPack(nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
// matchesFilter reports whether an item passes a get_item/inventory kind
|
||||
// filter (the C condition in pack.c inventory, untangled): KindNone takes
|
||||
// everything, KindCallable takes anything nameable (not food, not the
|
||||
// amulet), KindRingOrStick takes rings and wands.
|
||||
func matchesFilter(kind ObjectKind, item *Object) bool {
|
||||
switch kind {
|
||||
case KindNone:
|
||||
return true
|
||||
case KindCallable:
|
||||
return item.Kind != KindFood && item.Kind != KindAmulet
|
||||
case KindRingOrStick:
|
||||
return item.Kind == KindRing || item.Kind == KindWand
|
||||
default:
|
||||
return item.Kind == kind
|
||||
}
|
||||
}
|
||||
|
||||
// moveMsg prints the message if you are just moving onto an object
|
||||
// (pack.c move_msg).
|
||||
func (g *RogueGame) moveMsg(obj *Object) {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you ")
|
||||
g.addmsgf("you ")
|
||||
}
|
||||
|
||||
g.msg("moved onto %s", g.invName(obj, true))
|
||||
}
|
||||
|
||||
@@ -273,25 +332,34 @@ func (g *RogueGame) pickyInven() {
|
||||
p := &g.Player
|
||||
if len(p.Pack) == 0 {
|
||||
g.msg("you aren't carrying anything")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(p.Pack) == 1 {
|
||||
g.msg("a) %s", g.invName(p.Pack[0], false))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseTerse("item: ", "which item do you wish to inventory: "))
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
mch := g.readchar()
|
||||
if mch == Escape {
|
||||
g.msg("")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
if mch == obj.PackCh {
|
||||
g.msg("%c) %s", mch, g.invName(obj, false))
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
g.msg("'%s' not in pack", unctrl(mch))
|
||||
}
|
||||
|
||||
@@ -300,23 +368,31 @@ func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
|
||||
p := &g.Player
|
||||
if len(p.Pack) == 0 {
|
||||
g.msg("you aren't carrying anything")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if g.Again {
|
||||
if g.LastPick != nil {
|
||||
return g.LastPick
|
||||
}
|
||||
|
||||
g.msg("you ran out")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("which object do you want to ")
|
||||
g.addmsgf("which object do you want to ")
|
||||
}
|
||||
g.addmsg("%s", purpose)
|
||||
|
||||
g.addmsgf("%s", purpose)
|
||||
|
||||
if g.Options.Terse {
|
||||
g.addmsg(" what")
|
||||
g.addmsgf(" what")
|
||||
}
|
||||
|
||||
g.msg("? (* for list): ")
|
||||
ch := g.readchar()
|
||||
g.Msgs.Mpos = 0
|
||||
@@ -325,22 +401,28 @@ func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
|
||||
g.resetLast()
|
||||
g.After = false
|
||||
g.msg("")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
g.NObjs = 1 // normal case: person types one char
|
||||
if ch == '*' {
|
||||
g.Msgs.Mpos = 0
|
||||
if !g.inventory(p.Pack, kind) {
|
||||
g.After = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
if obj.PackCh == ch {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
g.msg("'%s' is not a valid item", unctrl(ch))
|
||||
}
|
||||
}
|
||||
@@ -350,15 +432,18 @@ func (g *RogueGame) money(value int) {
|
||||
p := &g.Player
|
||||
p.Purse += value
|
||||
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)
|
||||
}
|
||||
|
||||
if value > 0 {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you found ")
|
||||
g.addmsgf("you found ")
|
||||
}
|
||||
|
||||
g.msg("%d gold pieces", value)
|
||||
}
|
||||
}
|
||||
@@ -369,9 +454,11 @@ func (g *RogueGame) floorCh() byte {
|
||||
if g.Player.Room.Flags.Has(Gone) {
|
||||
return Passage
|
||||
}
|
||||
|
||||
if g.showFloor() {
|
||||
return Floor
|
||||
}
|
||||
|
||||
return ' '
|
||||
}
|
||||
|
||||
@@ -382,6 +469,7 @@ func (g *RogueGame) floorAt() byte {
|
||||
if ch == Floor {
|
||||
ch = g.floorCh()
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
@@ -398,5 +486,6 @@ func (g *RogueGame) chooseTerse(terse, verbose string) string {
|
||||
if g.Options.Terse {
|
||||
return terse
|
||||
}
|
||||
|
||||
return verbose
|
||||
}
|
||||
|
||||
@@ -17,25 +17,30 @@ var rdesConn = [MaxRooms][MaxRooms]bool{
|
||||
|
||||
// doPassages draws all the passages on a level (passages.c do_passages).
|
||||
func (g *RogueGame) doPassages() {
|
||||
var isconn [MaxRooms][MaxRooms]bool
|
||||
var ingraph [MaxRooms]bool
|
||||
var (
|
||||
isconn [MaxRooms][MaxRooms]bool
|
||||
ingraph [MaxRooms]bool
|
||||
)
|
||||
|
||||
// starting with one room, connect it to a random adjacent room and
|
||||
// then pick a new room to start with.
|
||||
roomcount := 1
|
||||
r1 := g.rnd(MaxRooms)
|
||||
ingraph[r1] = true
|
||||
|
||||
for {
|
||||
// find a room to connect with
|
||||
j := 0
|
||||
r2 := -1
|
||||
for i := 0; i < MaxRooms; i++ {
|
||||
|
||||
for i := range MaxRooms {
|
||||
if 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
|
||||
@@ -48,12 +53,13 @@ func (g *RogueGame) doPassages() {
|
||||
} else {
|
||||
// otherwise, connect new room to the graph, and draw a tunnel
|
||||
// to it
|
||||
ingraph[r2] = true
|
||||
ingraph[r2] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
|
||||
g.conn(r1, r2)
|
||||
isconn[r1][r2] = true
|
||||
isconn[r2][r1] = true
|
||||
isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
|
||||
roomcount++
|
||||
}
|
||||
|
||||
if roomcount >= MaxRooms {
|
||||
break
|
||||
}
|
||||
@@ -66,7 +72,8 @@ func (g *RogueGame) doPassages() {
|
||||
// find an adjacent room not already connected
|
||||
j := 0
|
||||
r2 := -1
|
||||
for i := 0; i < MaxRooms; i++ {
|
||||
|
||||
for i := range MaxRooms {
|
||||
if rdesConn[r1][i] && !isconn[r1][i] {
|
||||
if j++; g.rnd(j) == 0 {
|
||||
r2 = i
|
||||
@@ -77,17 +84,21 @@ func (g *RogueGame) doPassages() {
|
||||
if j != 0 {
|
||||
g.conn(r1, r2)
|
||||
isconn[r1][r2] = true
|
||||
isconn[r2][r1] = true
|
||||
isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
|
||||
}
|
||||
}
|
||||
|
||||
g.passnum()
|
||||
}
|
||||
|
||||
// conn draws a corridor from a room in a certain direction (passages.c
|
||||
// conn).
|
||||
func (g *RogueGame) conn(r1, r2 int) {
|
||||
var rm int
|
||||
var direc byte
|
||||
var (
|
||||
rm int
|
||||
direc byte
|
||||
)
|
||||
|
||||
if r1 < r2 {
|
||||
rm = r1
|
||||
if r1+1 == r2 {
|
||||
@@ -103,42 +114,52 @@ func (g *RogueGame) conn(r1, r2 int) {
|
||||
direc = 'd'
|
||||
}
|
||||
}
|
||||
|
||||
rpf := &g.Level.Rooms[rm]
|
||||
|
||||
// Set up the movement variables, in two cases: first drawing one down.
|
||||
var rpt *Room
|
||||
var del, turnDelta, spos, epos Coord
|
||||
var distance, turnDistance int
|
||||
var (
|
||||
rpt *Room
|
||||
del, turnDelta, spos, epos Coord
|
||||
distance, turnDistance int
|
||||
)
|
||||
|
||||
if direc == 'd' {
|
||||
rmt := rm + 3 // room # of dest
|
||||
rpt = &g.Level.Rooms[rmt] // room pointer of dest
|
||||
del = Coord{X: 0, Y: 1} // direction of move
|
||||
spos = rpf.Pos // start of move
|
||||
epos = rpt.Pos // end of move
|
||||
|
||||
if !rpf.Flags.Has(Gone) { // if not gone pick door pos
|
||||
for {
|
||||
spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1
|
||||
|
||||
spos.Y = rpf.Pos.Y + rpf.Max.Y - 1
|
||||
if !(rpf.Flags.Has(Maze) && !g.Level.FlagsAt(spos.Y, spos.X).Has(FPassage)) {
|
||||
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)) {
|
||||
if !rpt.Flags.Has(Maze) || g.Level.FlagsAt(epos.Y, epos.X).Has(FPassage) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
distance = abs(spos.Y-epos.Y) - 1 // distance to move
|
||||
|
||||
turnDelta.Y = 0 // direction to turn
|
||||
if spos.X < epos.X {
|
||||
turnDelta.X = 1
|
||||
} else {
|
||||
turnDelta.X = -1
|
||||
}
|
||||
|
||||
turnDistance = abs(spos.X - epos.X) // how far to turn
|
||||
} else { // setup for moving right
|
||||
rmt := rm + 1
|
||||
@@ -146,29 +167,34 @@ func (g *RogueGame) conn(r1, r2 int) {
|
||||
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)) {
|
||||
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)) {
|
||||
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)
|
||||
}
|
||||
@@ -182,6 +208,7 @@ func (g *RogueGame) conn(r1, r2 int) {
|
||||
} else {
|
||||
g.putpass(spos)
|
||||
}
|
||||
|
||||
if !rpt.Flags.Has(Gone) {
|
||||
g.door(rpt, epos)
|
||||
} else {
|
||||
@@ -203,9 +230,12 @@ func (g *RogueGame) conn(r1, r2 int) {
|
||||
}
|
||||
// Continue digging along
|
||||
g.putpass(curr)
|
||||
|
||||
distance--
|
||||
}
|
||||
|
||||
curr.X += del.X
|
||||
|
||||
curr.Y += del.Y
|
||||
if curr != epos {
|
||||
g.msg("warning, connectivity problem on this level")
|
||||
@@ -217,6 +247,7 @@ func (g *RogueGame) conn(r1, r2 int) {
|
||||
func (g *RogueGame) putpass(cp Coord) {
|
||||
pp := g.Level.At(cp.Y, cp.X)
|
||||
pp.Flags.Set(FPassage)
|
||||
|
||||
if g.rnd(10)+1 < g.Depth && g.rnd(40) == 0 {
|
||||
pp.Flags.Clear(FReal)
|
||||
} else {
|
||||
@@ -240,6 +271,7 @@ func (g *RogueGame) door(rm *Room, cp Coord) {
|
||||
} else {
|
||||
pp.Ch = '|'
|
||||
}
|
||||
|
||||
pp.Flags.Clear(FReal)
|
||||
} else {
|
||||
pp.Ch = Door
|
||||
@@ -250,7 +282,7 @@ func (g *RogueGame) door(rm *Room, cp Coord) {
|
||||
// (passages.c add_pass).
|
||||
func (g *RogueGame) addPass() {
|
||||
for y := 1; y < NumLines-1; y++ {
|
||||
for x := 0; x < NumCols; x++ {
|
||||
for x := range NumCols {
|
||||
pp := g.Level.At(y, x)
|
||||
if pp.Flags.Has(FPassage) || pp.Ch == Door ||
|
||||
(!pp.Flags.Has(FReal) && (pp.Ch == '|' || pp.Ch == '-')) {
|
||||
@@ -258,19 +290,24 @@ func (g *RogueGame) addPass() {
|
||||
if pp.Flags.Has(FPassage) {
|
||||
ch = Passage
|
||||
}
|
||||
|
||||
pp.Flags.Set(FSeen)
|
||||
g.move(y, x)
|
||||
if pp.Monst != nil {
|
||||
|
||||
switch {
|
||||
case pp.Monst != nil:
|
||||
pp.Monst.OldCh = pp.Ch
|
||||
} else if pp.Flags.Has(FReal) {
|
||||
case pp.Flags.Has(FReal):
|
||||
g.addch(ch)
|
||||
} else {
|
||||
default:
|
||||
g.standout()
|
||||
|
||||
if pp.Flags.Has(FPassage) {
|
||||
g.addch(Passage)
|
||||
} else {
|
||||
g.addch(Door)
|
||||
}
|
||||
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
@@ -281,10 +318,12 @@ func (g *RogueGame) addPass() {
|
||||
// passnum assigns a number to each passageway (passages.c passnum).
|
||||
func (g *RogueGame) passnum() {
|
||||
g.pnum = 0
|
||||
|
||||
g.newpnum = false
|
||||
for i := range g.Level.Passages {
|
||||
g.Level.Passages[i].Exits = g.Level.Passages[i].Exits[:0]
|
||||
}
|
||||
|
||||
for i := range g.Level.Rooms {
|
||||
rp := &g.Level.Rooms[i]
|
||||
for j := range rp.Exits {
|
||||
@@ -300,10 +339,12 @@ func (g *RogueGame) numpass(y, x int) {
|
||||
if x >= NumCols || x < 0 || y >= NumLines || y <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fp := g.Level.FlagsAt(y, x)
|
||||
if fp.Has(FPassNum) {
|
||||
return
|
||||
}
|
||||
|
||||
if g.newpnum {
|
||||
g.pnum++
|
||||
g.newpnum = false
|
||||
@@ -317,7 +358,8 @@ func (g *RogueGame) numpass(y, x int) {
|
||||
} else if !fp.Has(FPassage) {
|
||||
return
|
||||
}
|
||||
*fp |= PlaceFlags(g.pnum)
|
||||
|
||||
*fp |= PlaceFlags(g.pnum) //nolint:gosec // G115: pnum < MaxPass=13
|
||||
// recurse on the surrounding places
|
||||
g.numpass(y+1, x)
|
||||
g.numpass(y-1, x)
|
||||
@@ -330,5 +372,6 @@ func abs(n int) int {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -39,21 +39,26 @@ func (g *RogueGame) quaff() {
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindPotion {
|
||||
if !g.Options.Terse {
|
||||
g.msg("yuk! Why would you want to drink that?")
|
||||
} else {
|
||||
g.msg("that's undrinkable")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if obj == p.CurWeapon {
|
||||
p.CurWeapon = nil
|
||||
}
|
||||
|
||||
// Calculate the effect it has on the poor guy.
|
||||
trip := p.On(Hallucinating)
|
||||
|
||||
g.leavePack(obj, false, false)
|
||||
|
||||
switch obj.PotionKind() {
|
||||
case PotionConfusion:
|
||||
g.doPot(PotionConfusion, !trip)
|
||||
@@ -72,6 +77,7 @@ func (g *RogueGame) quaff() {
|
||||
p.Stats.MaxHP++
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
g.msg("you begin to feel better")
|
||||
case PotionGainStrength:
|
||||
@@ -81,6 +87,7 @@ func (g *RogueGame) quaff() {
|
||||
case PotionDetectMonsters:
|
||||
p.Flags.Set(SenseMonsters)
|
||||
g.Fuse(DTurnSee, 1, HuhDuration, After)
|
||||
|
||||
if !g.turnSee(false) {
|
||||
g.msg("you have a %s feeling for a moment, then it passes",
|
||||
g.chooseStr("normal", "strange"))
|
||||
@@ -88,24 +95,30 @@ func (g *RogueGame) quaff() {
|
||||
case PotionDetectMagic:
|
||||
// Potion of magic detection. Show the potions and scrolls
|
||||
show := false
|
||||
|
||||
if len(g.Level.Objects) > 0 {
|
||||
g.scr.Hw.Clear()
|
||||
|
||||
for _, tp := range g.Level.Objects {
|
||||
if tp.isMagic() {
|
||||
show = true
|
||||
|
||||
g.scr.Hw.MvAddCh(tp.Pos.Y, tp.Pos.X, Magic)
|
||||
g.Items.Potions[PotionDetectMagic].Know = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, mp := range g.Level.Monsters {
|
||||
for _, tp := range mp.Pack {
|
||||
if tp.isMagic() {
|
||||
show = true
|
||||
|
||||
g.scr.Hw.MvAddCh(mp.Pos.Y, mp.Pos.X, Magic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if show {
|
||||
g.Items.Potions[PotionDetectMagic].Know = true
|
||||
g.showWin("You sense the presence of magic on this level.--More--")
|
||||
@@ -118,16 +131,21 @@ func (g *RogueGame) quaff() {
|
||||
if p.On(SenseMonsters) {
|
||||
g.turnSee(false)
|
||||
}
|
||||
|
||||
g.StartDaemon(DVisuals, 0, Before)
|
||||
g.SeenStairs = g.seenStairs()
|
||||
}
|
||||
|
||||
g.doPot(PotionLSD, true)
|
||||
case PotionSeeInvisible:
|
||||
show := p.On(CanSeeInvisible)
|
||||
|
||||
g.doPot(PotionSeeInvisible, false)
|
||||
|
||||
if !show {
|
||||
g.invisOn()
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
case PotionRaiseLevel:
|
||||
g.Items.Potions[PotionRaiseLevel].Know = true
|
||||
@@ -139,14 +157,17 @@ func (g *RogueGame) quaff() {
|
||||
if p.Stats.HP > p.Stats.MaxHP+p.Stats.Lvl+1 {
|
||||
p.Stats.MaxHP++
|
||||
}
|
||||
|
||||
p.Stats.MaxHP++
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
g.comeDown(0)
|
||||
g.msg("you begin to feel much better")
|
||||
case PotionHaste:
|
||||
g.Items.Potions[PotionHaste].Know = true
|
||||
|
||||
g.After = false
|
||||
if g.addHaste(true) {
|
||||
g.msg("you feel yourself moving much faster")
|
||||
@@ -155,24 +176,30 @@ func (g *RogueGame) quaff() {
|
||||
if p.IsRing(Left, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, -p.CurRing[Left].Bonus)
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, -p.CurRing[Right].Bonus)
|
||||
}
|
||||
|
||||
if p.Stats.Str < p.MaxStats.Str {
|
||||
p.Stats.Str = p.MaxStats.Str
|
||||
}
|
||||
|
||||
if p.IsRing(Left, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, p.CurRing[Left].Bonus)
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, p.CurRing[Right].Bonus)
|
||||
}
|
||||
|
||||
g.msg("hey, this tastes great. It make you feel warm all over")
|
||||
case PotionBlindness:
|
||||
g.doPot(PotionBlindness, true)
|
||||
case PotionLevitation:
|
||||
g.doPot(PotionLevitation, true)
|
||||
}
|
||||
|
||||
g.status()
|
||||
// Throw the item away
|
||||
g.callIt(&g.Items.Potions[obj.Which])
|
||||
@@ -192,6 +219,7 @@ func (g *RogueGame) doPot(kind PotionKind, knowit bool) {
|
||||
if !g.Items.Potions[kind].Know {
|
||||
g.Items.Potions[kind].Know = knowit
|
||||
}
|
||||
|
||||
t := g.spread(pp.time)
|
||||
if !g.Player.On(pp.flags) {
|
||||
g.Player.Flags.Set(pp.flags)
|
||||
@@ -200,11 +228,14 @@ func (g *RogueGame) doPot(kind PotionKind, knowit bool) {
|
||||
} else {
|
||||
g.Lengthen(pp.daemon, t)
|
||||
}
|
||||
|
||||
high, straight := pp.high, pp.straight
|
||||
|
||||
if kind == PotionSeeInvisible {
|
||||
s := fmt.Sprintf("this potion tastes like %s juice", g.Fruit)
|
||||
high, straight = s, s
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr(high, straight))
|
||||
}
|
||||
|
||||
@@ -218,6 +249,7 @@ func (o *Object) isMagic() bool {
|
||||
case KindPotion, KindScroll, KindWand, KindRing, KindAmulet:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -225,6 +257,7 @@ func (o *Object) isMagic() bool {
|
||||
// invis_on).
|
||||
func (g *RogueGame) invisOn() {
|
||||
g.Player.Flags.Set(CanSeeInvisible)
|
||||
|
||||
for _, mp := range g.Level.Monsters {
|
||||
if mp.On(Invisible) && g.seeMonst(mp) && !g.Player.On(Hallucinating) {
|
||||
g.mvaddch(mp.Pos.Y, mp.Pos.X, mp.Disguise)
|
||||
@@ -236,8 +269,10 @@ func (g *RogueGame) invisOn() {
|
||||
// turn_see).
|
||||
func (g *RogueGame) turnSee(turnOff bool) bool {
|
||||
addNew := false
|
||||
|
||||
for _, mp := range g.Level.Monsters {
|
||||
g.move(mp.Pos.Y, mp.Pos.X)
|
||||
|
||||
canSee := g.seeMonst(mp)
|
||||
if turnOff {
|
||||
if !canSee {
|
||||
@@ -247,22 +282,27 @@ func (g *RogueGame) turnSee(turnOff bool) bool {
|
||||
if !canSee {
|
||||
g.standout()
|
||||
}
|
||||
|
||||
if !g.Player.On(Hallucinating) {
|
||||
g.addch(mp.Type)
|
||||
} else {
|
||||
g.addch(byte(g.rnd(26) + 'A'))
|
||||
g.addch(g.randomMonsterLetter())
|
||||
}
|
||||
|
||||
if !canSee {
|
||||
g.standend()
|
||||
|
||||
addNew = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if turnOff {
|
||||
g.Player.Flags.Clear(SenseMonsters)
|
||||
} else {
|
||||
g.Player.Flags.Set(SenseMonsters)
|
||||
}
|
||||
|
||||
return addNew
|
||||
}
|
||||
|
||||
@@ -271,9 +311,11 @@ func (g *RogueGame) turnSee(turnOff bool) bool {
|
||||
func (g *RogueGame) seenStairs() bool {
|
||||
st := g.Level.Stairs
|
||||
g.move(st.Y, st.X)
|
||||
|
||||
if g.inch() == Stairs { // it's on the map
|
||||
return true
|
||||
}
|
||||
|
||||
if g.Player.Pos == st { // it's under him
|
||||
return true
|
||||
}
|
||||
@@ -282,9 +324,11 @@ func (g *RogueGame) seenStairs() bool {
|
||||
if g.seeMonst(tp) && tp.On(Awake) { // visible and awake:
|
||||
return true // it must have moved there
|
||||
}
|
||||
|
||||
if g.Player.On(SenseMonsters) && tp.OldCh == Stairs {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ func (g *RogueGame) ringOn() {
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindRing {
|
||||
if !g.Options.Terse {
|
||||
g.msg("it would be difficult to wrap that around a finger")
|
||||
} else {
|
||||
g.msg("not a ring")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -27,6 +29,7 @@ func (g *RogueGame) ringOn() {
|
||||
}
|
||||
|
||||
var ring int
|
||||
|
||||
switch {
|
||||
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
|
||||
if ring = g.gethand(); ring < 0 {
|
||||
@@ -42,8 +45,10 @@ func (g *RogueGame) ringOn() {
|
||||
} else {
|
||||
g.msg("wearing two")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p.CurRing[ring] = obj
|
||||
|
||||
// Calculate the effect it has on the poor guy.
|
||||
@@ -57,15 +62,18 @@ func (g *RogueGame) ringOn() {
|
||||
}
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you are now wearing ")
|
||||
g.addmsgf("you are now wearing ")
|
||||
}
|
||||
|
||||
g.msg("%s (%c)", g.invName(obj, true), obj.PackCh)
|
||||
}
|
||||
|
||||
// ringOff takes off a ring (rings.c ring_off).
|
||||
func (g *RogueGame) ringOff() {
|
||||
p := &g.Player
|
||||
|
||||
var ring int
|
||||
|
||||
switch {
|
||||
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
|
||||
if g.Options.Terse {
|
||||
@@ -73,6 +81,7 @@ func (g *RogueGame) ringOff() {
|
||||
} else {
|
||||
g.msg("you aren't wearing any rings")
|
||||
}
|
||||
|
||||
return
|
||||
case p.CurRing[Left] == nil:
|
||||
ring = Right
|
||||
@@ -83,12 +92,16 @@ func (g *RogueGame) ringOff() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
obj := p.CurRing[ring]
|
||||
if obj == nil {
|
||||
g.msg("not wearing such a ring")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if g.dropCheck(obj) {
|
||||
g.msg("was wearing %s(%c)", g.invName(obj, true), obj.PackCh)
|
||||
}
|
||||
@@ -102,17 +115,22 @@ func (g *RogueGame) gethand() int {
|
||||
} else {
|
||||
g.msg("left hand or right hand? ")
|
||||
}
|
||||
|
||||
c := g.readchar()
|
||||
if c == Escape {
|
||||
return -1
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
if c == 'l' || c == 'L' {
|
||||
return Left
|
||||
}
|
||||
|
||||
if c == 'r' || c == 'R' {
|
||||
return Right
|
||||
}
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("L or R")
|
||||
} else {
|
||||
@@ -147,6 +165,7 @@ func (g *RogueGame) ringEat(hand int) int {
|
||||
if ring == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
eat := ringUses[ring.RingKind()]
|
||||
if eat < 0 {
|
||||
if g.rnd(-eat) == 0 {
|
||||
@@ -155,20 +174,25 @@ func (g *RogueGame) ringEat(hand int) int {
|
||||
eat = 0
|
||||
}
|
||||
}
|
||||
|
||||
if ring.RingKind() == RingSlowDigestion {
|
||||
eat = -eat
|
||||
}
|
||||
|
||||
return eat
|
||||
}
|
||||
|
||||
// ringNum prints ring bonuses (rings.c ring_num).
|
||||
func ringNum(g *RogueGame, obj *Object) string {
|
||||
// ringNum prints ring bonuses (rings.c ring_num). The unused game
|
||||
// parameter keeps the nameit prfunc signature.
|
||||
func ringNum(_ *RogueGame, obj *Object) string {
|
||||
if !obj.Flags.Has(Known) {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch obj.RingKind() {
|
||||
case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity:
|
||||
return fmt.Sprintf(" [%s]", num(obj.Bonus, 0, Ring))
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
64
game/rip.go
64
game/rip.go
@@ -11,12 +11,14 @@ import (
|
||||
// that Run recovers, so the terminal is restored by normal unwinding.
|
||||
|
||||
// gameEnd is the sentinel carried by the panic that replaces my_exit().
|
||||
type gameEnd struct{ status int }
|
||||
type gameEnd struct{}
|
||||
|
||||
// myExit leaves the process properly (main.c my_exit): it unwinds to Run.
|
||||
func (g *RogueGame) myExit(st int) {
|
||||
// Every C caller exited with status 0; abnormal exits panic for real.
|
||||
func (g *RogueGame) myExit() {
|
||||
g.Playing = false
|
||||
panic(gameEnd{status: st})
|
||||
|
||||
panic(gameEnd{})
|
||||
}
|
||||
|
||||
var ripArt = []string{
|
||||
@@ -39,39 +41,51 @@ var ripArt = []string{
|
||||
func (g *RogueGame) death(monst byte) {
|
||||
p := &g.Player
|
||||
p.Purse -= p.Purse / 10
|
||||
|
||||
g.clear()
|
||||
|
||||
killer := g.killname(monst, false)
|
||||
if !g.Options.Tombstone {
|
||||
g.scr.Std.MvPrintw(NumLines-2, 0, "Killed by ")
|
||||
g.scr.Std.MvPrintwf(NumLines-2, 0, "Killed by ")
|
||||
|
||||
if monst != 's' && monst != 'h' {
|
||||
g.printw("a%s ", vowelstr(killer))
|
||||
}
|
||||
|
||||
g.printw("%s with %d gold", killer, p.Purse)
|
||||
} else {
|
||||
year := time.Now().Year()
|
||||
|
||||
for i, line := range ripArt {
|
||||
g.scr.Std.MvAddStr(8+i, 0, line)
|
||||
}
|
||||
|
||||
g.mvaddstr(17, center(killer), killer)
|
||||
|
||||
if monst == 's' || monst == 'h' {
|
||||
g.mvaddch(16, 32, ' ')
|
||||
} else {
|
||||
g.mvaddstr(16, 33, vowelstr(killer))
|
||||
}
|
||||
|
||||
g.mvaddstr(14, center(g.Whoami), g.Whoami)
|
||||
|
||||
au := fmt.Sprintf("%d Au", p.Purse)
|
||||
g.mvaddstr(15, center(au), au)
|
||||
g.mvaddstr(18, 26, fmt.Sprintf("%4d", year))
|
||||
}
|
||||
|
||||
g.mvaddstr(NumLines-1, 0, "[Press return to continue]")
|
||||
g.refresh()
|
||||
|
||||
flags := 0
|
||||
if g.HasAmulet {
|
||||
flags = 3
|
||||
}
|
||||
|
||||
g.score(p.Purse, flags, monst)
|
||||
g.waitFor('\n')
|
||||
g.myExit(0)
|
||||
g.myExit()
|
||||
}
|
||||
|
||||
// center returns the column to center the given string on the tombstone
|
||||
@@ -85,6 +99,7 @@ func (g *RogueGame) totalWinner() {
|
||||
p := &g.Player
|
||||
g.clear()
|
||||
g.standout()
|
||||
|
||||
banner := []string{
|
||||
" ",
|
||||
" @ @ @ @ @ @@@ @ @ ",
|
||||
@@ -100,6 +115,7 @@ func (g *RogueGame) totalWinner() {
|
||||
for i, line := range banner {
|
||||
g.scr.Std.MvAddStr(i, 0, line)
|
||||
}
|
||||
|
||||
g.standend()
|
||||
g.scr.Std.MvAddStr(10, 0, "You have joined the elite ranks of those who have escaped the")
|
||||
g.scr.Std.MvAddStr(11, 0, "Dungeons of Doom alive. You journey home and sell all your loot at")
|
||||
@@ -110,11 +126,14 @@ func (g *RogueGame) totalWinner() {
|
||||
g.clear()
|
||||
g.mvaddstr(0, 0, " Worth Item")
|
||||
g.move(1, 0)
|
||||
|
||||
oldpurse := p.Purse
|
||||
line := 1
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
worth := 0
|
||||
it := &g.Items
|
||||
|
||||
switch obj.Kind {
|
||||
case KindFood:
|
||||
worth = 2 * obj.Count
|
||||
@@ -129,21 +148,26 @@ func (g *RogueGame) totalWinner() {
|
||||
obj.Flags.Set(Known)
|
||||
case KindScroll:
|
||||
op := &it.Scrolls[obj.Which]
|
||||
|
||||
worth = op.Worth * obj.Count
|
||||
if !op.Know {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
op.Know = true
|
||||
case KindPotion:
|
||||
op := &it.Potions[obj.Which]
|
||||
|
||||
worth = op.Worth * obj.Count
|
||||
if !op.Know {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
op.Know = true
|
||||
case KindRing:
|
||||
op := &it.Rings[obj.Which]
|
||||
worth = op.Worth
|
||||
|
||||
if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage ||
|
||||
obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity {
|
||||
if obj.Bonus > 0 {
|
||||
@@ -152,35 +176,44 @@ func (g *RogueGame) totalWinner() {
|
||||
worth = 10
|
||||
}
|
||||
}
|
||||
|
||||
if !obj.Flags.Has(Known) {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
obj.Flags.Set(Known)
|
||||
|
||||
op.Know = true
|
||||
case KindWand:
|
||||
op := &it.Sticks[obj.Which]
|
||||
worth = op.Worth
|
||||
|
||||
worth += 20 * obj.Charges
|
||||
if !obj.Flags.Has(Known) {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
obj.Flags.Set(Known)
|
||||
|
||||
op.Know = true
|
||||
case KindAmulet:
|
||||
worth = 1000
|
||||
}
|
||||
|
||||
if worth < 0 {
|
||||
worth = 0
|
||||
}
|
||||
g.scr.Std.MvPrintw(line, 0, "%c) %5d %s", obj.PackCh, worth,
|
||||
|
||||
g.scr.Std.MvPrintwf(line, 0, "%c) %5d %s", obj.PackCh, worth,
|
||||
g.invName(obj, false))
|
||||
line++
|
||||
p.Purse += worth
|
||||
}
|
||||
g.scr.Std.MvPrintw(line, 0, " %5d Gold Pieces ", oldpurse)
|
||||
|
||||
g.scr.Std.MvPrintwf(line, 0, " %5d Gold Pieces ", oldpurse)
|
||||
g.refresh()
|
||||
g.score(p.Purse, 2, ' ')
|
||||
g.myExit(0)
|
||||
g.myExit()
|
||||
}
|
||||
|
||||
// killnameTable is the rip.c nlist[]: special death causes.
|
||||
@@ -194,25 +227,32 @@ var killnameTable = []helpEntry{
|
||||
|
||||
// killname converts a code to a monster name (rip.c killname).
|
||||
func (g *RogueGame) killname(monst byte, doart bool) string {
|
||||
var sp string
|
||||
var article bool
|
||||
var (
|
||||
sp string
|
||||
article bool
|
||||
)
|
||||
|
||||
if isUpper(monst) {
|
||||
sp = g.Monsters[monst-'A'].Name
|
||||
article = true
|
||||
} else {
|
||||
sp = "Wally the Wonder Badger"
|
||||
article = false
|
||||
|
||||
for _, hp := range killnameTable {
|
||||
if hp.Ch == monst {
|
||||
sp = hp.Desc
|
||||
article = hp.Print
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if doart && article {
|
||||
return "a" + vowelstr(sp) + " " + sp
|
||||
}
|
||||
|
||||
return sp
|
||||
}
|
||||
|
||||
@@ -224,13 +264,16 @@ func (g *RogueGame) DeathDemo() {
|
||||
if _, ok := r.(gameEnd); ok {
|
||||
return
|
||||
}
|
||||
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
dnum := g.rnd(100)
|
||||
for dnum--; dnum > 0; dnum-- {
|
||||
g.rnd(100)
|
||||
}
|
||||
|
||||
g.Player.Purse = g.rnd(100) + 1
|
||||
g.Depth = g.rnd(100) + 1
|
||||
g.death(g.deathMonst())
|
||||
@@ -245,5 +288,6 @@ func (g *RogueGame) deathMonst() byte {
|
||||
'Y', 'Z', 'a', 'b', 'h', 'd', 's',
|
||||
' ', // generates the "Wally the Wonder Badger" message
|
||||
}
|
||||
|
||||
return poss[g.rnd(len(poss))]
|
||||
}
|
||||
|
||||
16
game/rng.go
16
game/rng.go
@@ -11,21 +11,17 @@ type Rng struct {
|
||||
Seed int32
|
||||
}
|
||||
|
||||
// next steps the generator and returns the next raw value (the RN macro).
|
||||
func (r *Rng) next() int {
|
||||
r.Seed = r.Seed*11109 + 13849
|
||||
return int(r.Seed>>16) & 0xffff
|
||||
}
|
||||
|
||||
// Rnd picks a very random number in [0, rng) (main.c rnd).
|
||||
func (r *Rng) Rnd(rng int) int {
|
||||
if rng == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
v := r.next()
|
||||
if v < 0 {
|
||||
v = -v
|
||||
}
|
||||
|
||||
return v % rng
|
||||
}
|
||||
|
||||
@@ -35,9 +31,17 @@ func (r *Rng) Roll(number, sides int) int {
|
||||
for ; number > 0; number-- {
|
||||
dtotal += r.Rnd(sides) + 1
|
||||
}
|
||||
|
||||
return dtotal
|
||||
}
|
||||
|
||||
// next steps the generator and returns the next raw value (the RN macro).
|
||||
func (r *Rng) next() int {
|
||||
r.Seed = r.Seed*11109 + 13849
|
||||
|
||||
return int(r.Seed>>16) & 0xffff
|
||||
}
|
||||
|
||||
// rnd is the ported code's spelling of C rnd(): every call site in the C
|
||||
// sources reads rnd(x), and keeping that shape makes cross-checking easy.
|
||||
func (g *RogueGame) rnd(rng int) int { return g.Rng.Rnd(rng) }
|
||||
|
||||
@@ -46,6 +46,7 @@ func TestRndZeroDoesNotStep(t *testing.T) {
|
||||
if got := r.Rnd(0); got != 0 {
|
||||
t.Fatalf("rnd(0) = %d, want 0", got)
|
||||
}
|
||||
|
||||
if r.Seed != 42 {
|
||||
t.Fatalf("rnd(0) stepped the generator: seed = %d, want 42", r.Seed)
|
||||
}
|
||||
@@ -58,6 +59,7 @@ func TestSpreadSmallValues(t *testing.T) {
|
||||
if got := g.spread(1); got != 1 {
|
||||
t.Errorf("spread(1) = %d, want 1", got)
|
||||
}
|
||||
|
||||
if got := g.spread(2); got != 2 {
|
||||
t.Errorf("spread(2) = %d, want 2", got)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ const goldGrp = 1
|
||||
// do_rooms).
|
||||
func (g *RogueGame) doRooms() {
|
||||
var bsze Coord // maximum room size
|
||||
|
||||
bsze.X = NumCols / 3
|
||||
bsze.Y = NumLines / 3
|
||||
// Clear things for a new level
|
||||
@@ -32,7 +33,7 @@ func (g *RogueGame) doRooms() {
|
||||
}
|
||||
// Put the gone rooms, if any, on the level
|
||||
leftOut := g.rnd(4)
|
||||
for i := 0; i < leftOut; i++ {
|
||||
for range leftOut {
|
||||
g.Level.Rooms[g.rndRoom()].Flags.Set(Gone)
|
||||
}
|
||||
// dig and populate all the rooms on the level
|
||||
@@ -40,6 +41,7 @@ func (g *RogueGame) doRooms() {
|
||||
rp := &g.Level.Rooms[i]
|
||||
// Find upper left corner of box that this room goes in
|
||||
top := Coord{X: (i%3)*bsze.X + 1, Y: (i / 3) * bsze.Y}
|
||||
|
||||
if rp.Flags.Has(Gone) {
|
||||
// Place a gone room. Make certain that there is a blank line
|
||||
// for passage drawing.
|
||||
@@ -47,16 +49,19 @@ func (g *RogueGame) doRooms() {
|
||||
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 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
// set room type
|
||||
if g.rnd(10) < g.Depth-1 {
|
||||
rp.Flags.Set(Dark) // dark room
|
||||
|
||||
if g.rnd(15) == 0 {
|
||||
rp.Flags = Maze // maze room
|
||||
}
|
||||
@@ -64,10 +69,12 @@ func (g *RogueGame) doRooms() {
|
||||
// Find a place and size for a random room
|
||||
if rp.Flags.Has(Maze) {
|
||||
rp.Max.X = bsze.X - 1
|
||||
|
||||
rp.Max.Y = bsze.Y - 1
|
||||
if rp.Pos.X = top.X; rp.Pos.X == 1 {
|
||||
rp.Pos.X = 0
|
||||
}
|
||||
|
||||
if rp.Pos.Y = top.Y; rp.Pos.Y == 0 {
|
||||
rp.Pos.Y++
|
||||
rp.Max.Y--
|
||||
@@ -77,12 +84,14 @@ func (g *RogueGame) doRooms() {
|
||||
rp.Max.X = g.rnd(bsze.X-4) + 4
|
||||
rp.Max.Y = g.rnd(bsze.Y-4) + 4
|
||||
rp.Pos.X = top.X + g.rnd(bsze.X-rp.Max.X)
|
||||
|
||||
rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y)
|
||||
if rp.Pos.Y != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.drawRoom(rp)
|
||||
// Put the gold in
|
||||
if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) {
|
||||
@@ -92,6 +101,7 @@ func (g *RogueGame) doRooms() {
|
||||
rp.Gold, _ = g.findFloorIn(rp, 0, false)
|
||||
gold.Pos = rp.Gold
|
||||
g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold)
|
||||
|
||||
gold.Flags = Stackable
|
||||
gold.Group = goldGrp
|
||||
gold.Kind = KindGold
|
||||
@@ -102,6 +112,7 @@ func (g *RogueGame) doRooms() {
|
||||
if rp.GoldVal > 0 {
|
||||
prob = 80
|
||||
}
|
||||
|
||||
if g.rnd(100) < prob {
|
||||
tp := &Monster{}
|
||||
mp, _ := g.findFloorIn(rp, 0, true)
|
||||
@@ -116,8 +127,10 @@ func (g *RogueGame) doRooms() {
|
||||
func (g *RogueGame) drawRoom(rp *Room) {
|
||||
if rp.Flags.Has(Maze) {
|
||||
g.doMaze(rp)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.vert(rp, rp.Pos.X) // Draw left side
|
||||
g.vert(rp, rp.Pos.X+rp.Max.X-1) // Draw right side
|
||||
g.horiz(rp, rp.Pos.Y) // Draw top
|
||||
@@ -173,26 +186,34 @@ func (g *RogueGame) dig(y, x int) {
|
||||
|
||||
for {
|
||||
cnt := 0
|
||||
|
||||
var nexty, nextx int
|
||||
|
||||
for _, cp := range del {
|
||||
newy := y + cp.Y
|
||||
|
||||
newx := x + cp.X
|
||||
if newy < 0 || newy > m.maxy || newx < 0 || newx > m.maxx {
|
||||
continue
|
||||
}
|
||||
|
||||
if g.Level.FlagsAt(newy+m.starty, newx+m.startx).Has(FPassage) {
|
||||
continue
|
||||
}
|
||||
|
||||
if cnt++; g.rnd(cnt) == 0 {
|
||||
nexty = newy
|
||||
nextx = newx
|
||||
}
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
g.accntMaze(y, x, nexty, nextx)
|
||||
g.accntMaze(nexty, nextx, y, x)
|
||||
|
||||
var pos Coord
|
||||
if nexty == y {
|
||||
pos.Y = y + m.starty
|
||||
@@ -209,6 +230,7 @@ func (g *RogueGame) dig(y, x int) {
|
||||
pos.Y = nexty + m.starty - 1
|
||||
}
|
||||
}
|
||||
|
||||
g.putpass(pos)
|
||||
pos.Y = nexty + m.starty
|
||||
pos.X = nextx + m.startx
|
||||
@@ -220,7 +242,7 @@ func (g *RogueGame) dig(y, x int) {
|
||||
// accntMaze accounts for maze exits (rooms.c accnt_maze).
|
||||
func (g *RogueGame) accntMaze(y, x, ny, nx int) {
|
||||
sp := &g.maze.maze[y][x]
|
||||
for i := 0; i < sp.nexits; i++ {
|
||||
for i := range sp.nexits {
|
||||
if sp.exits[i].Y == ny && sp.exits[i].X == nx {
|
||||
return
|
||||
}
|
||||
@@ -236,15 +258,18 @@ func (g *RogueGame) accntMaze(y, x, ny, nx int) {
|
||||
// rndPos picks a random spot in a room (rooms.c rnd_pos).
|
||||
func (g *RogueGame) rndPos(rp *Room) Coord {
|
||||
var cp Coord
|
||||
|
||||
cp.X = rp.Pos.X + g.rnd(rp.Max.X-2) + 1
|
||||
cp.Y = rp.Pos.Y + g.rnd(rp.Max.Y-2) + 1
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
// findFloor finds a valid floor spot, picking a new random room each time
|
||||
// around the loop (rooms.c find_floor with rp == NULL).
|
||||
func (g *RogueGame) findFloor(rp *Room, limit int, monst bool) (Coord, bool) {
|
||||
return g.findFloorImpl(rp, limit, monst, rp == nil)
|
||||
// around the loop; it retries forever (rooms.c find_floor with rp == NULL
|
||||
// — every such C call site passed FALSE for the limit).
|
||||
func (g *RogueGame) findFloor(monst bool) (Coord, bool) {
|
||||
return g.findFloorImpl(nil, 0, monst, true)
|
||||
}
|
||||
|
||||
// findFloorIn finds a valid floor spot in this room (rooms.c find_floor
|
||||
@@ -261,6 +286,7 @@ func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Co
|
||||
compchar = Passage
|
||||
}
|
||||
}
|
||||
|
||||
cnt := limit
|
||||
for {
|
||||
if limit != 0 {
|
||||
@@ -268,14 +294,18 @@ func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Co
|
||||
return Coord{}, false
|
||||
}
|
||||
}
|
||||
|
||||
if pickroom {
|
||||
rp = &g.Level.Rooms[g.rndRoom()]
|
||||
|
||||
compchar = Floor
|
||||
if rp.Flags.Has(Maze) {
|
||||
compchar = Passage
|
||||
}
|
||||
}
|
||||
|
||||
cp := g.rndPos(rp)
|
||||
|
||||
pp := g.Level.At(cp.Y, cp.X)
|
||||
if monst {
|
||||
if pp.Monst == nil && stepOk(pp.Ch) {
|
||||
@@ -294,11 +324,14 @@ func (g *RogueGame) enterRoom(cp Coord) {
|
||||
rp := g.roomin(cp)
|
||||
p.Room = rp
|
||||
g.doorOpen(rp)
|
||||
|
||||
if !rp.Flags.Has(Dark) && !p.On(Blind) {
|
||||
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
|
||||
g.move(y, rp.Pos.X)
|
||||
|
||||
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
|
||||
tp := g.Level.MonsterAt(y, x)
|
||||
|
||||
ch := g.Level.Char(y, x)
|
||||
if tp == nil {
|
||||
if g.inch() != ch {
|
||||
@@ -335,6 +368,7 @@ func (g *RogueGame) leaveRoom(cp Coord) {
|
||||
}
|
||||
|
||||
var floor byte
|
||||
|
||||
switch {
|
||||
case rp.Flags.Has(Gone):
|
||||
floor = Passage
|
||||
@@ -348,6 +382,7 @@ func (g *RogueGame) leaveRoom(cp Coord) {
|
||||
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
|
||||
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
|
||||
g.move(y, x)
|
||||
|
||||
switch ch := g.inch(); ch {
|
||||
case Floor:
|
||||
if floor == ' ' {
|
||||
@@ -361,8 +396,10 @@ func (g *RogueGame) leaveRoom(cp Coord) {
|
||||
g.standout()
|
||||
g.addch(ch)
|
||||
g.standend()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
pp := g.Level.At(y, x)
|
||||
if pp.Ch == Door {
|
||||
g.addch(Door)
|
||||
@@ -373,5 +410,6 @@ func (g *RogueGame) leaveRoom(cp Coord) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.doorOpen(rp)
|
||||
}
|
||||
|
||||
@@ -9,21 +9,27 @@ import (
|
||||
// moves, a rest, an inventory, then Q-quit answered yes.
|
||||
func TestRunScriptedSession(t *testing.T) {
|
||||
tt := &testTerm{input: []byte("hjkl.i Qy")}
|
||||
|
||||
g := NewGame(Config{Seed: 99, Term: tt})
|
||||
if err := g.Run(); err != nil {
|
||||
|
||||
err := g.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
|
||||
if g.Playing {
|
||||
t.Error("still playing after quit")
|
||||
}
|
||||
// After quitting, the scoreboard is the last thing shown (in C it went
|
||||
// to stdout after endwin; here it is drawn on the screen).
|
||||
found := false
|
||||
for y := 0; y < NumLines; y++ {
|
||||
|
||||
for y := range NumLines {
|
||||
if strings.Contains(g.scr.Std.Line(y), "Top Ten") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("score list not on screen after quit")
|
||||
}
|
||||
@@ -36,17 +42,23 @@ func TestRunManyTurns(t *testing.T) {
|
||||
// Spaces between commands double as answers to any --More-- prompts;
|
||||
// without them a single prompt would swallow the rest of the script
|
||||
// (wait_for eats everything that isn't a space).
|
||||
var script []byte
|
||||
moves := []byte("h h j j k k l l y u b n s s . . ")
|
||||
moves := []byte("h j k l y u b n s .")
|
||||
script := make([]byte, 0, len(moves)*200+7)
|
||||
|
||||
for range 200 {
|
||||
script = append(script, moves...)
|
||||
}
|
||||
|
||||
script = append(script, " Q y Qy"...)
|
||||
tt := &testTerm{input: script}
|
||||
|
||||
g := NewGame(Config{Seed: 31337, Term: tt})
|
||||
if err := g.Run(); err != nil {
|
||||
|
||||
err := g.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
|
||||
if g.Playing {
|
||||
t.Error("session did not end")
|
||||
}
|
||||
@@ -65,9 +77,12 @@ func TestRunDownStairs(t *testing.T) {
|
||||
g.StartDaemon(DDoctor, 0, After)
|
||||
g.Fuse(DSwander, 0, wanderTime(g), After)
|
||||
g.StartDaemon(DStomach, 0, After)
|
||||
if err := g.Run(); err != nil {
|
||||
|
||||
err := g.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
|
||||
if g.Depth != 2 {
|
||||
t.Errorf("depth = %d after descending, want 2", g.Depth)
|
||||
}
|
||||
@@ -79,26 +94,33 @@ func TestSaveCommandRoundTrip(t *testing.T) {
|
||||
// The C get_str caps input at MAXINP=50 characters, so the save path
|
||||
// must be short: work from the temp directory.
|
||||
t.Chdir(t.TempDir())
|
||||
|
||||
path := "cmd.save"
|
||||
// 'S' with no default file name goes straight to the name prompt.
|
||||
script := "S" + path + "\n"
|
||||
tt := &testTerm{input: []byte(script)}
|
||||
g := NewGame(Config{Seed: 55, Term: tt})
|
||||
|
||||
g.FileName = "" // force the name prompt
|
||||
if err := g.Run(); err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
|
||||
runErr := g.Run()
|
||||
if runErr != nil {
|
||||
t.Fatalf("Run: %v", runErr)
|
||||
}
|
||||
|
||||
h, err := Restore(path, Config{Term: &testTerm{}})
|
||||
if err != nil {
|
||||
t.Fatalf("Restore: %v", err)
|
||||
}
|
||||
|
||||
if h.Depth != g.Depth || h.Player.Purse != g.Player.Purse {
|
||||
t.Error("restored game does not match saved game")
|
||||
}
|
||||
// The restored game must be playable.
|
||||
h.scr.term.(*testTerm).input = []byte("..Qy")
|
||||
if err := h.Run(); err != nil {
|
||||
t.Fatalf("restored Run: %v", err)
|
||||
setInput(t, h, []byte("..Qy")...)
|
||||
|
||||
restoredErr := h.Run()
|
||||
if restoredErr != nil {
|
||||
t.Fatalf("restored Run: %v", restoredErr)
|
||||
}
|
||||
}
|
||||
|
||||
106
game/save.go
106
game/save.go
@@ -2,6 +2,7 @@ package game
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
@@ -131,16 +132,19 @@ func (g *RogueGame) roomIdx(rp *Room) int {
|
||||
if rp == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
for i := range g.Level.Rooms {
|
||||
if rp == &g.Level.Rooms[i] {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
for i := range g.Level.Passages {
|
||||
if rp == &g.Level.Passages[i] {
|
||||
return 100 + i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -152,6 +156,7 @@ func (g *RogueGame) roomAt(i int) *Room {
|
||||
case i >= 0:
|
||||
return &g.Level.Rooms[i]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -160,11 +165,13 @@ func (g *RogueGame) packIdx(obj *Object) int {
|
||||
if obj == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
for i, o := range g.Player.Pack {
|
||||
if o == obj {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -266,9 +273,11 @@ func (g *RogueGame) snapshot() *SaveState {
|
||||
for _, o := range m.Pack {
|
||||
sc.Pack = append(sc.Pack, *o)
|
||||
}
|
||||
|
||||
st.Monsters = append(st.Monsters, sc)
|
||||
|
||||
ref := destRef{}
|
||||
|
||||
switch {
|
||||
case m.Dest == nil:
|
||||
case m.Dest == &p.Pos:
|
||||
@@ -279,6 +288,7 @@ func (g *RogueGame) snapshot() *SaveState {
|
||||
ref = destRef{Kind: 2, Idx: mi}
|
||||
}
|
||||
}
|
||||
|
||||
if ref.Kind == 0 {
|
||||
for _, oo := range g.Level.Objects {
|
||||
if m.Dest == &oo.Pos {
|
||||
@@ -286,6 +296,7 @@ func (g *RogueGame) snapshot() *SaveState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ref.Kind == 0 {
|
||||
for ri := range g.Level.Rooms {
|
||||
if m.Dest == &g.Level.Rooms[ri].Gold {
|
||||
@@ -294,8 +305,10 @@ func (g *RogueGame) snapshot() *SaveState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
st.Dests = append(st.Dests, ref)
|
||||
}
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
@@ -371,15 +384,18 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
|
||||
p.Flags = sp.Body.Flags
|
||||
p.Stats = sp.Body.Stats
|
||||
p.Room = g.roomAt(sp.Body.RoomIdx)
|
||||
|
||||
p.Pack = nil
|
||||
for i := range sp.Body.Pack {
|
||||
o := sp.Body.Pack[i]
|
||||
p.Pack = append(p.Pack, &o)
|
||||
}
|
||||
|
||||
pick := func(i int) *Object {
|
||||
if i < 0 || i >= len(p.Pack) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.Pack[i]
|
||||
}
|
||||
p.CurArmor = pick(sp.CurArmor)
|
||||
@@ -400,6 +416,7 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
|
||||
g.Level.Monsters = nil
|
||||
for i := range st.Monsters {
|
||||
sc := &st.Monsters[i]
|
||||
|
||||
m := &Monster{Creature: Creature{
|
||||
Pos: sc.Pos, Turn: sc.Turn, Type: sc.Type, Disguise: sc.Disguise,
|
||||
OldCh: sc.OldCh, Flags: sc.Flags, Stats: sc.Stats,
|
||||
@@ -409,11 +426,14 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
|
||||
o := sc.Pack[j]
|
||||
m.Pack = append(m.Pack, &o)
|
||||
}
|
||||
|
||||
g.Level.Monsters = append(g.Level.Monsters, m)
|
||||
g.Level.SetMonsterAt(m.Pos.Y, m.Pos.X, m)
|
||||
}
|
||||
|
||||
for i, ref := range st.Dests {
|
||||
m := g.Level.Monsters[i]
|
||||
|
||||
switch ref.Kind {
|
||||
case 1:
|
||||
m.Dest = &p.Pos
|
||||
@@ -433,26 +453,35 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
|
||||
// goto over/gotfile flow becomes the useDefault flag.
|
||||
func (g *RogueGame) saveGame() {
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
over:
|
||||
useDefault := false
|
||||
|
||||
if g.FileName != "" {
|
||||
var c byte
|
||||
|
||||
for {
|
||||
g.msg("save file (%s)? ", g.FileName)
|
||||
c = g.readchar()
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
if c == Escape {
|
||||
g.msg("")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if c == 'n' || c == 'N' || c == 'y' || c == 'Y' {
|
||||
break
|
||||
}
|
||||
|
||||
g.msg("please answer Y or N")
|
||||
}
|
||||
|
||||
if c == 'y' || c == 'Y' {
|
||||
g.addstr("Yes\n")
|
||||
g.refresh()
|
||||
|
||||
useDefault = true
|
||||
}
|
||||
}
|
||||
@@ -465,80 +494,115 @@ over:
|
||||
} else {
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("file name: ")
|
||||
|
||||
if g.getStr(&buf, g.scr.Std) == Quit {
|
||||
g.msg("")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
}
|
||||
// test to see if the file exists
|
||||
if _, err := os.Stat(buf); err == nil {
|
||||
_, 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' {
|
||||
goto over
|
||||
}
|
||||
|
||||
g.msg("Please answer Y or N")
|
||||
}
|
||||
|
||||
g.msg("file name: %s", buf)
|
||||
os.Remove(g.FileName)
|
||||
_ = os.Remove(g.FileName) // best effort, as in C (md_unlink)
|
||||
}
|
||||
|
||||
g.FileName = buf
|
||||
if err := g.saveFile(g.FileName); err != nil {
|
||||
|
||||
err := g.saveFile(g.FileName)
|
||||
if err != nil {
|
||||
g.msg("%s", err.Error())
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
g.myExit(0)
|
||||
|
||||
g.myExit()
|
||||
}
|
||||
|
||||
// saveFile writes the saved game (save.c save_file).
|
||||
// saveFile writes the saved game (save.c save_file). A failed write means
|
||||
// a corrupt save, so the file is removed before reporting the error.
|
||||
func (g *RogueGame) saveFile(path string) error {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o400)
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o400) //nolint:gosec,lll // G304: user-chosen save path
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if err := gob.NewEncoder(f).Encode(g.snapshot()); err != nil {
|
||||
os.Remove(path)
|
||||
return err
|
||||
|
||||
encErr := gob.NewEncoder(f).Encode(g.snapshot())
|
||||
closeErr := f.Close()
|
||||
|
||||
if encErr != nil || closeErr != nil {
|
||||
_ = os.Remove(path) // don't leave a corrupt save behind
|
||||
|
||||
if encErr != nil {
|
||||
return encErr
|
||||
}
|
||||
|
||||
return closeErr
|
||||
}
|
||||
|
||||
return os.Chmod(path, 0o400)
|
||||
}
|
||||
|
||||
// AutoSave silently saves to the current file name; used on SIGHUP/SIGTERM
|
||||
// (save.c auto_save).
|
||||
// (save.c auto_save). Best effort by design: it runs on the way out of a
|
||||
// dying process.
|
||||
func (g *RogueGame) AutoSave() {
|
||||
if g.FileName != "" {
|
||||
os.Remove(g.FileName)
|
||||
g.saveFile(g.FileName)
|
||||
_ = os.Remove(g.FileName)
|
||||
_ = g.saveFile(g.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrSaveOutOfDate reports a save file from an incompatible version.
|
||||
var ErrSaveOutOfDate = errors.New("sorry, saved game is out of date")
|
||||
|
||||
// Restore restores a saved game from a file (save.c restore). The file is
|
||||
// deleted, as in C, to defeat restarting from the same save.
|
||||
func Restore(path string, cfg Config) (*RogueGame, error) {
|
||||
f, err := os.Open(path)
|
||||
f, err := os.Open(path) //nolint:gosec // G304: the save path is user-chosen by design
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }() // read-only handle
|
||||
|
||||
var st SaveState
|
||||
if err := gob.NewDecoder(f).Decode(&st); err != nil {
|
||||
return nil, fmt.Errorf("%s: corrupt or incompatible save file: %w", path, err)
|
||||
|
||||
decErr := gob.NewDecoder(f).Decode(&st)
|
||||
if decErr != nil {
|
||||
return nil, fmt.Errorf("%s: corrupt or incompatible save file: %w",
|
||||
path, decErr)
|
||||
}
|
||||
|
||||
if st.Version != saveFormatVersion {
|
||||
return nil, fmt.Errorf("sorry, saved game is out of date")
|
||||
return nil, ErrSaveOutOfDate
|
||||
}
|
||||
|
||||
g := &RogueGame{
|
||||
@@ -553,8 +617,10 @@ func Restore(path string, cfg Config) (*RogueGame, error) {
|
||||
g.applySnapshot(&st)
|
||||
|
||||
// defeat multiple restarting from the same place
|
||||
if err := os.Remove(path); err != nil {
|
||||
return nil, fmt.Errorf("cannot unlink file: %w", err)
|
||||
rmErr := os.Remove(path)
|
||||
if rmErr != nil {
|
||||
return nil, fmt.Errorf("cannot unlink file: %w", rmErr)
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
|
||||
g.HasAmulet = true
|
||||
g.Items.Potions[PotionHealing].Know = true
|
||||
g.Items.Scrolls[ScrollMagicMapping].Guess = "map???"
|
||||
|
||||
g.Monsters['F'-'A'].Stats.Dmg = dice("3x1") // mutated bestiary must survive
|
||||
if len(g.Level.Monsters) > 0 {
|
||||
g.Level.Monsters[0].Flags.Set(Awake)
|
||||
@@ -22,8 +23,10 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
|
||||
}
|
||||
|
||||
path := filepath.Join(t.TempDir(), "rogue.save")
|
||||
if err := g.saveFile(path); err != nil {
|
||||
t.Fatalf("saveFile: %v", err)
|
||||
|
||||
saveErr := g.saveFile(path)
|
||||
if saveErr != nil {
|
||||
t.Fatalf("saveFile: %v", saveErr)
|
||||
}
|
||||
|
||||
h, err := Restore(path, Config{Term: &testTerm{}})
|
||||
@@ -31,7 +34,8 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
|
||||
t.Fatalf("Restore: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
_, statErr := os.Stat(path)
|
||||
if !os.IsNotExist(statErr) {
|
||||
t.Error("save file not deleted on restore (C anti-restart rule)")
|
||||
}
|
||||
|
||||
@@ -39,36 +43,46 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
|
||||
t.Errorf("player state lost: purse=%d food=%d",
|
||||
h.Player.Purse, h.Player.FoodLeft)
|
||||
}
|
||||
|
||||
if !h.HasAmulet {
|
||||
t.Error("amulet flag lost")
|
||||
}
|
||||
|
||||
if !h.Items.Potions[PotionHealing].Know {
|
||||
t.Error("potion identification lost")
|
||||
}
|
||||
|
||||
if h.Items.Scrolls[ScrollMagicMapping].Guess != "map???" {
|
||||
t.Error("scroll guess lost")
|
||||
}
|
||||
|
||||
if h.Monsters['F'-'A'].Stats.Dmg.String() != "3x1" {
|
||||
t.Error("mutated bestiary lost")
|
||||
}
|
||||
|
||||
if h.Rng.Seed != g.Rng.Seed {
|
||||
t.Error("RNG state lost")
|
||||
}
|
||||
|
||||
if renderMap(h) != renderMap(g) {
|
||||
t.Error("restored level map differs")
|
||||
}
|
||||
|
||||
if len(h.Level.Monsters) != len(g.Level.Monsters) {
|
||||
t.Fatalf("monster count %d != %d",
|
||||
len(h.Level.Monsters), len(g.Level.Monsters))
|
||||
}
|
||||
|
||||
if len(g.Level.Monsters) > 0 {
|
||||
m := h.Level.Monsters[0]
|
||||
if m.Dest != &h.Player.Pos {
|
||||
t.Error("monster chase target not re-aliased to the hero")
|
||||
}
|
||||
|
||||
if h.Level.MonsterAt(m.Pos.Y, m.Pos.X) != m {
|
||||
t.Error("map monster index not rebuilt")
|
||||
}
|
||||
|
||||
if m.Room == nil {
|
||||
t.Error("monster room pointer not rebuilt")
|
||||
}
|
||||
@@ -81,13 +95,17 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
|
||||
len(st.Player.Body.Pack))
|
||||
t.Logf("restored: CurWeapon=%p pack has %d items", h.Player.CurWeapon,
|
||||
len(h.Player.Pack))
|
||||
|
||||
found := false
|
||||
|
||||
for i, o := range h.Player.Pack {
|
||||
t.Logf(" pack[%d]=%p type=%v which=%d", i, o, o.Kind, o.Which)
|
||||
|
||||
if o == h.Player.CurWeapon {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("restored CurWeapon is not aliased into the pack")
|
||||
}
|
||||
@@ -98,15 +116,24 @@ func TestRestoreRejectsWrongVersion(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "rogue.save")
|
||||
st := g.snapshot()
|
||||
st.Version = "0.0.0"
|
||||
f, err := os.Create(path)
|
||||
|
||||
f, err := os.Create(path) //nolint:gosec // G304: test temp path
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gob.NewEncoder(f).Encode(st); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
encErr := gob.NewEncoder(f).Encode(st)
|
||||
if encErr != nil {
|
||||
t.Fatal(encErr)
|
||||
}
|
||||
f.Close()
|
||||
if _, err := Restore(path, Config{}); err == nil {
|
||||
|
||||
closeErr := f.Close()
|
||||
if closeErr != nil {
|
||||
t.Fatal(closeErr)
|
||||
}
|
||||
|
||||
_, restoreErr := Restore(path, Config{})
|
||||
if restoreErr == nil {
|
||||
t.Error("restore accepted an out-of-date save")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,16 +38,22 @@ func (g *RogueGame) rdScore() []ScoreEnt {
|
||||
if g.ScorePath == "" {
|
||||
return topTen
|
||||
}
|
||||
|
||||
f, err := os.Open(g.ScorePath)
|
||||
if err != nil {
|
||||
return topTen
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }() // read-only handle
|
||||
|
||||
var onDisk []ScoreEnt
|
||||
if err := gob.NewDecoder(f).Decode(&onDisk); err != nil {
|
||||
return topTen
|
||||
|
||||
decErr := gob.NewDecoder(f).Decode(&onDisk)
|
||||
if decErr != nil {
|
||||
return topTen // unreadable scoreboard reads as empty, as in C
|
||||
}
|
||||
|
||||
copy(topTen, onDisk)
|
||||
|
||||
return topTen
|
||||
}
|
||||
|
||||
@@ -57,29 +63,45 @@ func (g *RogueGame) wrScore(topTen []ScoreEnt) {
|
||||
return
|
||||
}
|
||||
// lock_sc/unlock_sc: exclusive-create lock file with stale takeover.
|
||||
// The whole scoreboard write is best effort, as it was in C: a shared
|
||||
// scoreboard must never take the game down.
|
||||
lock := g.ScorePath + ".lck"
|
||||
for range 5 {
|
||||
lf, err := os.OpenFile(lock, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
|
||||
lf, err := os.OpenFile(lock, //nolint:gosec // G304: configured path
|
||||
os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
||||
if err == nil {
|
||||
lf.Close()
|
||||
defer os.Remove(lock)
|
||||
_ = lf.Close()
|
||||
|
||||
defer func() { _ = os.Remove(lock) }()
|
||||
|
||||
break
|
||||
}
|
||||
if fi, serr := os.Stat(lock); serr == nil &&
|
||||
time.Since(fi.ModTime()) > 10*time.Second {
|
||||
os.Remove(lock)
|
||||
|
||||
fi, statErr := os.Stat(lock)
|
||||
if statErr == nil && time.Since(fi.ModTime()) > staleLockAge {
|
||||
_ = os.Remove(lock)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
f, err := os.OpenFile(g.ScorePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
|
||||
|
||||
f, err := os.OpenFile(g.ScorePath,
|
||||
os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
gob.NewEncoder(f).Encode(topTen)
|
||||
|
||||
_ = gob.NewEncoder(f).Encode(topTen)
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
// staleLockAge is how old a scoreboard lock file may be before another
|
||||
// process assumes its owner died and takes it over (mach_dep.c lock_sc
|
||||
// aged its lock the same way).
|
||||
const staleLockAge = 10 * time.Second
|
||||
|
||||
// score figures the score and posts it (rip.c score). flags -1 means just
|
||||
// display the list (the -s command line option).
|
||||
func (g *RogueGame) score(amount, flags int, monst byte) {
|
||||
@@ -92,38 +114,47 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
|
||||
topTen := g.rdScore()
|
||||
// Insert her in list if need be
|
||||
ins := -1
|
||||
|
||||
if !g.NoScore && flags >= 0 {
|
||||
uid := os.Getuid()
|
||||
|
||||
scp := len(topTen)
|
||||
for i := range topTen {
|
||||
if amount > topTen[i].Score {
|
||||
scp = i
|
||||
|
||||
break
|
||||
} else if !g.AllScore && flags != 2 &&
|
||||
topTen[i].UID == uid && topTen[i].Flags != 2 {
|
||||
// only one score per nowin uid
|
||||
scp = len(topTen)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if scp < len(topTen) {
|
||||
sc2 := len(topTen) - 1
|
||||
if flags != 2 && !g.AllScore {
|
||||
for i := scp; i < len(topTen); i++ {
|
||||
if topTen[i].UID == uid && topTen[i].Flags != 2 {
|
||||
sc2 = i
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for sc2 > scp {
|
||||
topTen[sc2] = topTen[sc2-1]
|
||||
sc2--
|
||||
}
|
||||
|
||||
lvl := g.Depth
|
||||
if flags == 2 {
|
||||
lvl = g.MaxDepth
|
||||
}
|
||||
|
||||
topTen[scp] = ScoreEnt{
|
||||
UID: uid,
|
||||
Score: amount,
|
||||
@@ -142,43 +173,53 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
|
||||
if g.AllScore {
|
||||
label = "Scores"
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
fmt.Sprintf("Top Ten %s:", label),
|
||||
" Score Name",
|
||||
}
|
||||
highlight := -1
|
||||
|
||||
for i := range topTen {
|
||||
scp := &topTen[i]
|
||||
if scp.Score == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("%2d %5d %s: %s on level %d", i+1,
|
||||
scp.Score, scp.Name, scoreReasons[scp.Flags], scp.Level)
|
||||
if scp.Flags == 0 || scp.Flags == 3 {
|
||||
line += fmt.Sprintf(" by %s", g.killname(scp.Monster, true))
|
||||
line += " by " + g.killname(scp.Monster, true)
|
||||
}
|
||||
|
||||
line += "."
|
||||
|
||||
if i == ins {
|
||||
highlight = len(lines)
|
||||
}
|
||||
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
if g.scr != nil && g.scr.term != nil {
|
||||
g.clear()
|
||||
|
||||
for i, line := range lines {
|
||||
if i == highlight {
|
||||
g.standout()
|
||||
}
|
||||
|
||||
g.mvaddstr(i, 0, line)
|
||||
|
||||
if i == highlight {
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
|
||||
g.refresh()
|
||||
} else {
|
||||
for _, line := range lines {
|
||||
fmt.Println(line)
|
||||
_, _ = fmt.Fprintln(os.Stdout, line) // CLI output
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,26 +37,28 @@ type Window struct {
|
||||
func NewWindow(rows, cols int) *Window {
|
||||
w := &Window{rows: rows, cols: cols, cells: make([]cell, rows*cols)}
|
||||
w.Clear()
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *Window) at(y, x int) *cell { return &w.cells[y*w.cols+x] }
|
||||
|
||||
// Move positions the cursor (curses move/wmove).
|
||||
func (w *Window) Move(y, x int) { w.cy, w.cx = y, x }
|
||||
|
||||
// GetYX reports the cursor position (curses getyx).
|
||||
func (w *Window) GetYX() (y, x int) { return w.cy, w.cx }
|
||||
func (w *Window) GetYX() (int, int) { return w.cy, w.cx }
|
||||
|
||||
// AddCh writes a character at the cursor and advances it (curses addch).
|
||||
func (w *Window) AddCh(ch byte) {
|
||||
if ch == '\n' {
|
||||
w.cy, w.cx = w.cy+1, 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
|
||||
return
|
||||
}
|
||||
|
||||
*w.at(w.cy, w.cx) = cell{ch: ch, standout: w.standout}
|
||||
if w.cx++; w.cx >= w.cols {
|
||||
w.cx = 0
|
||||
@@ -68,7 +70,7 @@ func (w *Window) AddCh(ch byte) {
|
||||
|
||||
// AddStr writes a string at the cursor (curses addstr).
|
||||
func (w *Window) AddStr(s string) {
|
||||
for i := 0; i < len(s); i++ {
|
||||
for i := range len(s) {
|
||||
w.AddCh(s[i])
|
||||
}
|
||||
}
|
||||
@@ -85,15 +87,15 @@ func (w *Window) MvAddStr(y, x int, s string) {
|
||||
w.AddStr(s)
|
||||
}
|
||||
|
||||
// Printw writes formatted text at the cursor (curses printw).
|
||||
func (w *Window) Printw(format string, a ...any) {
|
||||
// Printwf writes formatted text at the cursor (curses printw).
|
||||
func (w *Window) Printwf(format string, a ...any) {
|
||||
w.AddStr(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// MvPrintw moves then writes formatted text (curses mvprintw).
|
||||
func (w *Window) MvPrintw(y, x int, format string, a ...any) {
|
||||
// MvPrintwf moves then writes formatted text (curses mvprintw).
|
||||
func (w *Window) MvPrintwf(y, x int, format string, a ...any) {
|
||||
w.Move(y, x)
|
||||
w.Printw(format, a...)
|
||||
w.Printwf(format, a...)
|
||||
}
|
||||
|
||||
// Inch returns the character under the cursor (curses inch, sans
|
||||
@@ -102,12 +104,14 @@ func (w *Window) Inch() byte {
|
||||
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
|
||||
return ' '
|
||||
}
|
||||
|
||||
return w.at(w.cy, w.cx).ch
|
||||
}
|
||||
|
||||
// MvInch moves then reads (curses mvinch).
|
||||
func (w *Window) MvInch(y, x int) byte {
|
||||
w.Move(y, x)
|
||||
|
||||
return w.Inch()
|
||||
}
|
||||
|
||||
@@ -120,6 +124,7 @@ func (w *Window) Clear() {
|
||||
for i := range w.cells {
|
||||
w.cells[i] = cell{ch: ' '}
|
||||
}
|
||||
|
||||
w.cy, w.cx = 0, 0
|
||||
}
|
||||
|
||||
@@ -128,6 +133,7 @@ func (w *Window) Clrtoeol() {
|
||||
if w.cy < 0 || w.cy >= w.rows {
|
||||
return
|
||||
}
|
||||
|
||||
for x := w.cx; x < w.cols; x++ {
|
||||
*w.at(w.cy, x) = cell{ch: ' '}
|
||||
}
|
||||
@@ -138,13 +144,14 @@ func (w *Window) CopyFrom(src *Window) {
|
||||
copy(w.cells, src.cells)
|
||||
}
|
||||
|
||||
// Size reports the window dimensions.
|
||||
func (w *Window) Size() (rows, cols int) { return w.rows, w.cols }
|
||||
// Size reports the window dimensions as rows, columns.
|
||||
func (w *Window) Size() (int, int) { return w.rows, w.cols }
|
||||
|
||||
// CellAt reports the character and standout attribute at a position; used
|
||||
// by Terminal implementations to render the window.
|
||||
func (w *Window) CellAt(y, x int) (ch byte, standout bool) {
|
||||
func (w *Window) CellAt(y, x int) (byte, bool) {
|
||||
c := w.at(y, x)
|
||||
|
||||
return c.ch, c.standout
|
||||
}
|
||||
|
||||
@@ -155,6 +162,7 @@ func (w *Window) Contents() []byte {
|
||||
for i, c := range w.cells {
|
||||
out[i] = c.ch
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -171,12 +179,16 @@ func (w *Window) SetContents(data []byte) {
|
||||
// victory screens.
|
||||
func (w *Window) Line(y int) string {
|
||||
buf := make([]byte, w.cols)
|
||||
for x := 0; x < w.cols; x++ {
|
||||
for x := range w.cols {
|
||||
buf[x] = w.at(y, x).ch
|
||||
}
|
||||
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// at addresses the cell at (y, x) in the backing array.
|
||||
func (w *Window) at(y, x int) *cell { return &w.cells[y*w.cols+x] }
|
||||
|
||||
// Screen bundles the two windows the game draws on with the device that
|
||||
// shows them.
|
||||
type Screen struct {
|
||||
@@ -217,7 +229,7 @@ func (g *RogueGame) mvaddch(y, x int, c byte) { g.scr.Std.MvAddCh(y, x, c) }
|
||||
func (g *RogueGame) mvaddstr(y, x int, s string) {
|
||||
g.scr.Std.MvAddStr(y, x, s)
|
||||
}
|
||||
func (g *RogueGame) printw(f string, a ...any) { g.scr.Std.Printw(f, a...) }
|
||||
func (g *RogueGame) printw(f string, a ...any) { g.scr.Std.Printwf(f, a...) }
|
||||
func (g *RogueGame) inch() byte { return g.scr.Std.Inch() }
|
||||
func (g *RogueGame) mvinch(y, x int) byte { return g.scr.Std.MvInch(y, x) }
|
||||
func (g *RogueGame) standout() { g.scr.Std.Standout(true) }
|
||||
|
||||
@@ -16,16 +16,19 @@ var idType = [ScrollIdentifyRingOrStick + 1]ObjectKind{
|
||||
// (scrolls.c read_scroll).
|
||||
func (g *RogueGame) readScroll() {
|
||||
p := &g.Player
|
||||
|
||||
obj := g.getItem("read", KindScroll)
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindScroll {
|
||||
if !g.Options.Terse {
|
||||
g.msg("there is nothing on it to read")
|
||||
} else {
|
||||
g.msg("nothing to read")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
// Calculate the effect it has on the poor guy.
|
||||
@@ -50,30 +53,39 @@ func (g *RogueGame) readScroll() {
|
||||
// Hold monster scroll. Stop all monsters within two spaces from
|
||||
// chasing after the hero.
|
||||
held := 0
|
||||
|
||||
for x := p.Pos.X - 2; x <= p.Pos.X+2; x++ {
|
||||
if x < 0 || x >= NumCols {
|
||||
continue
|
||||
}
|
||||
|
||||
for y := p.Pos.Y - 2; y <= p.Pos.Y+2; y++ {
|
||||
if y < 0 || y > NumLines-1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if mp := g.Level.MonsterAt(y, x); mp != nil && mp.On(Awake) {
|
||||
mp.Flags.Clear(Awake)
|
||||
mp.Flags.Set(Held)
|
||||
|
||||
held++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if held > 0 {
|
||||
g.addmsg("the monster")
|
||||
g.addmsgf("the monster")
|
||||
|
||||
if held > 1 {
|
||||
g.addmsg("s around you")
|
||||
g.addmsgf("s around you")
|
||||
}
|
||||
g.addmsg(" freeze")
|
||||
|
||||
g.addmsgf(" freeze")
|
||||
|
||||
if held == 1 {
|
||||
g.addmsg("s")
|
||||
g.addmsgf("s")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
g.Items.Scrolls[ScrollHoldMonster].Know = true
|
||||
} else {
|
||||
@@ -83,13 +95,16 @@ func (g *RogueGame) readScroll() {
|
||||
// Scroll which makes you fall asleep
|
||||
g.Items.Scrolls[ScrollSleep].Know = true
|
||||
g.NoCommand += g.rnd(g.spread(5)) + 4 // SLEEPTIME
|
||||
|
||||
p.Flags.Clear(Awake)
|
||||
g.msg("you fall asleep")
|
||||
case ScrollCreateMonster:
|
||||
// Create a monster: first look in a circle around him, next try
|
||||
// his room, otherwise give up
|
||||
i := 0
|
||||
|
||||
var mp Coord
|
||||
|
||||
for y := p.Pos.Y - 1; y <= p.Pos.Y+1; y++ {
|
||||
for x := p.Pos.X - 1; x <= p.Pos.X+1; x++ {
|
||||
// Don't put a monster on top of the player.
|
||||
@@ -103,12 +118,14 @@ func (g *RogueGame) readScroll() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if i++; g.rnd(i) == 0 {
|
||||
mp = Coord{Y: y, X: x}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
g.msg("you hear a faint cry of anguish in the distance")
|
||||
} else {
|
||||
@@ -126,10 +143,11 @@ func (g *RogueGame) readScroll() {
|
||||
g.msg("oh, now this scroll has a map on it")
|
||||
// take all the things we want to keep hidden out of the window
|
||||
for y := 1; y < NumLines-1; y++ {
|
||||
for x := 0; x < NumCols; x++ {
|
||||
for x := range NumCols {
|
||||
pp := g.Level.At(y, x)
|
||||
ch := pp.Ch
|
||||
pass := false
|
||||
|
||||
switch ch {
|
||||
case Door, Stairs:
|
||||
case '-', '|':
|
||||
@@ -168,13 +186,17 @@ func (g *RogueGame) readScroll() {
|
||||
ch = ' '
|
||||
}
|
||||
}
|
||||
|
||||
if pass {
|
||||
if !pp.Flags.Has(FReal) {
|
||||
pp.Ch = Passage
|
||||
}
|
||||
|
||||
pp.Flags.Set(FSeen | FReal)
|
||||
|
||||
ch = Passage
|
||||
}
|
||||
|
||||
if ch != ' ' {
|
||||
if tp := pp.Monst; tp != nil {
|
||||
tp.OldCh = ch
|
||||
@@ -190,13 +212,17 @@ func (g *RogueGame) readScroll() {
|
||||
case ScrollFoodDetection:
|
||||
// Food detection
|
||||
found := false
|
||||
|
||||
g.scr.Hw.Clear()
|
||||
|
||||
for _, fo := range g.Level.Objects {
|
||||
if fo.Kind == KindFood {
|
||||
found = true
|
||||
|
||||
g.scr.Hw.MvAddCh(fo.Pos.Y, fo.Pos.X, Food)
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
g.Items.Scrolls[ScrollFoodDetection].Know = true
|
||||
g.showWin("Your nose tingles and you smell food.--More--")
|
||||
@@ -206,7 +232,9 @@ func (g *RogueGame) readScroll() {
|
||||
case ScrollTeleportation:
|
||||
// Scroll of teleportation: make him disappear and reappear
|
||||
curRoom := p.Room
|
||||
|
||||
g.teleport()
|
||||
|
||||
if curRoom != p.Room {
|
||||
g.Items.Scrolls[ScrollTeleportation].Know = true
|
||||
}
|
||||
@@ -215,11 +243,13 @@ func (g *RogueGame) readScroll() {
|
||||
g.msg("you feel a strange sense of loss")
|
||||
} else {
|
||||
p.CurWeapon.Flags.Clear(Cursed)
|
||||
|
||||
if g.rnd(2) == 0 {
|
||||
p.CurWeapon.HPlus++
|
||||
} else {
|
||||
p.CurWeapon.DPlus++
|
||||
}
|
||||
|
||||
g.msg("your %s glows %s for a moment",
|
||||
g.Items.Weapons[p.CurWeapon.Which].Name, g.pickColor("blue"))
|
||||
}
|
||||
@@ -248,6 +278,7 @@ func (g *RogueGame) readScroll() {
|
||||
g.msg("you feel a strange sense of loss")
|
||||
}
|
||||
}
|
||||
|
||||
g.look(true) // put the result of the scroll on the screen
|
||||
g.status()
|
||||
|
||||
|
||||
@@ -7,19 +7,25 @@ import "fmt"
|
||||
// doZap performs a zap with a wand (sticks.c do_zap).
|
||||
func (g *RogueGame) doZap() {
|
||||
p := &g.Player
|
||||
|
||||
obj := g.getItem("zap with", KindWand)
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindWand {
|
||||
g.After = false
|
||||
g.msg("you can't zap with that!")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Charges == 0 {
|
||||
g.msg("nothing happens")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch obj.WandKind() {
|
||||
case WandLight:
|
||||
// Reddy Kilowatt wand. Light up the room
|
||||
@@ -30,10 +36,12 @@ func (g *RogueGame) doZap() {
|
||||
p.Room.Flags.Clear(Dark)
|
||||
// Light the room and put the player back up
|
||||
g.enterRoom(p.Pos)
|
||||
g.addmsg("the room is lit")
|
||||
g.addmsgf("the room is lit")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" by a shimmering %s light", g.pickColor("blue"))
|
||||
g.addmsgf(" by a shimmering %s light", g.pickColor("blue"))
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
}
|
||||
case WandDrainLife:
|
||||
@@ -42,42 +50,53 @@ func (g *RogueGame) doZap() {
|
||||
// passage)
|
||||
if p.Stats.HP < 2 {
|
||||
g.msg("you are too weak to use it")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.drain()
|
||||
case WandInvisibility, WandPolymorph, WandTeleportAway, WandTeleportTo, WandCancellation:
|
||||
y := p.Pos.Y
|
||||
|
||||
x := p.Pos.X
|
||||
for stepOk(g.Level.VisibleChar(y, x)) {
|
||||
y += g.Delta.Y
|
||||
x += g.Delta.X
|
||||
}
|
||||
|
||||
if tp := g.Level.MonsterAt(y, x); tp != nil {
|
||||
monster := tp.Type
|
||||
if monster == 'F' {
|
||||
p.Flags.Clear(Held)
|
||||
}
|
||||
|
||||
switch obj.WandKind() {
|
||||
case WandInvisibility:
|
||||
tp.Flags.Set(Invisible)
|
||||
|
||||
if g.cansee(y, x) {
|
||||
g.mvaddch(y, x, tp.OldCh)
|
||||
}
|
||||
case WandPolymorph:
|
||||
pp := tp.Pack
|
||||
detachMon(&g.Level.Monsters, tp)
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, g.Level.Char(y, x))
|
||||
}
|
||||
|
||||
oldch := tp.OldCh
|
||||
g.Delta.Y = y
|
||||
g.Delta.X = x
|
||||
monster = byte(g.rnd(26) + 'A')
|
||||
monster = g.randomMonsterLetter()
|
||||
g.newMonster(tp, monster, g.Delta)
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, monster)
|
||||
}
|
||||
|
||||
tp.OldCh = oldch
|
||||
|
||||
tp.Pack = pp
|
||||
if g.seeMonst(tp) {
|
||||
g.Items.Sticks[WandPolymorph].Know = true
|
||||
@@ -85,15 +104,17 @@ func (g *RogueGame) doZap() {
|
||||
case WandCancellation:
|
||||
tp.Flags.Set(Cancelled)
|
||||
tp.Flags.Clear(Invisible | CanConfuse)
|
||||
|
||||
tp.Disguise = tp.Type
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, tp.Disguise)
|
||||
}
|
||||
case WandTeleportAway, WandTeleportTo:
|
||||
var newPos Coord
|
||||
|
||||
if obj.WandKind() == WandTeleportAway {
|
||||
for {
|
||||
newPos, _ = g.findFloor(nil, 0, true)
|
||||
newPos, _ = g.findFloor(true)
|
||||
if newPos != p.Pos {
|
||||
break
|
||||
}
|
||||
@@ -102,6 +123,7 @@ func (g *RogueGame) doZap() {
|
||||
newPos.Y = p.Pos.Y + g.Delta.Y
|
||||
newPos.X = p.Pos.X + g.Delta.X
|
||||
}
|
||||
|
||||
tp.Dest = &p.Pos
|
||||
tp.Flags.Set(Awake)
|
||||
g.relocate(tp, newPos)
|
||||
@@ -114,26 +136,31 @@ func (g *RogueGame) doZap() {
|
||||
bolt.HurlDmg = dice("1x4")
|
||||
bolt.HPlus = 100
|
||||
bolt.DPlus = 1
|
||||
|
||||
bolt.Flags = Missile
|
||||
if p.CurWeapon != nil {
|
||||
bolt.Launch = WeaponKind(p.CurWeapon.Which)
|
||||
}
|
||||
|
||||
g.doMotion(bolt, g.Delta.Y, g.Delta.X)
|
||||
|
||||
if tp := g.Level.MonsterAt(bolt.Pos.Y, bolt.Pos.X); tp != nil &&
|
||||
!g.saveThrow(VsMagic, &tp.Stats) {
|
||||
g.hitMonster(bolt.Pos, bolt)
|
||||
} else if g.Options.Terse {
|
||||
g.msg("missle vanishes")
|
||||
g.msg("missle vanishes") //nolint:misspell // C's spelling, kept faithfully
|
||||
} else {
|
||||
g.msg("the missle vanishes with a puff of smoke")
|
||||
g.msg("the missle vanishes with a puff of smoke") //nolint:misspell // C's spelling
|
||||
}
|
||||
case WandHasteMonster, WandSlowMonster:
|
||||
y := p.Pos.Y
|
||||
|
||||
x := p.Pos.X
|
||||
for stepOk(g.Level.VisibleChar(y, x)) {
|
||||
y += g.Delta.Y
|
||||
x += g.Delta.X
|
||||
}
|
||||
|
||||
if tp := g.Level.MonsterAt(y, x); tp != nil {
|
||||
if obj.WandKind() == WandHasteMonster {
|
||||
if tp.On(Slowed) {
|
||||
@@ -147,14 +174,17 @@ func (g *RogueGame) doZap() {
|
||||
} else {
|
||||
tp.Flags.Set(Slowed)
|
||||
}
|
||||
|
||||
tp.Turn = true
|
||||
}
|
||||
|
||||
g.Delta.Y = y
|
||||
g.Delta.X = x
|
||||
g.runto(g.Delta)
|
||||
}
|
||||
case WandLightning, WandFire, WandCold:
|
||||
var name string
|
||||
|
||||
switch obj.WandKind() {
|
||||
case WandLightning:
|
||||
name = "bolt"
|
||||
@@ -163,10 +193,12 @@ func (g *RogueGame) doZap() {
|
||||
default:
|
||||
name = "ice"
|
||||
}
|
||||
|
||||
g.fireBolt(p.Pos, &g.Delta, name)
|
||||
g.Items.Sticks[obj.Which].Know = true
|
||||
case WandNothing:
|
||||
}
|
||||
|
||||
obj.Charges--
|
||||
}
|
||||
|
||||
@@ -178,8 +210,11 @@ func (g *RogueGame) drain() {
|
||||
if g.Level.Char(p.Pos.Y, p.Pos.X) == Door {
|
||||
corp = &g.Level.Passages[*g.Level.FlagsAt(p.Pos.Y, p.Pos.X)&FPassNum]
|
||||
}
|
||||
|
||||
inpass := p.Room.Flags.Has(Gone)
|
||||
|
||||
var drainee []*Monster
|
||||
|
||||
for _, mp := range g.Level.Monsters {
|
||||
if mp.Room == p.Room || mp.Room == corp ||
|
||||
(inpass && g.Level.Char(mp.Pos.Y, mp.Pos.X) == Door &&
|
||||
@@ -187,11 +222,14 @@ func (g *RogueGame) drain() {
|
||||
drainee = append(drainee, mp)
|
||||
}
|
||||
}
|
||||
|
||||
cnt := len(drainee)
|
||||
if cnt == 0 {
|
||||
g.msg("you have a tingling feeling")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p.Stats.HP /= 2
|
||||
cnt = p.Stats.HP / cnt
|
||||
// Now zot all of the monsters
|
||||
@@ -217,7 +255,9 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
bolt.HPlus = 100
|
||||
bolt.DPlus = 0
|
||||
g.Items.Weapons[WeaponFlame].Name = name
|
||||
|
||||
var dirch byte
|
||||
|
||||
switch dir.Y + dir.X {
|
||||
case 0:
|
||||
dirch = '/'
|
||||
@@ -230,10 +270,12 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
case 2, -2:
|
||||
dirch = '\\'
|
||||
}
|
||||
|
||||
pos := start
|
||||
hitHero := !fromHero
|
||||
used := false
|
||||
changed := false
|
||||
|
||||
var spotpos []Coord
|
||||
for len(spotpos) < BoltLength && !used {
|
||||
pos.Y += dir.Y
|
||||
@@ -241,6 +283,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
spotpos = append(spotpos, pos)
|
||||
ch := g.Level.VisibleChar(pos.Y, pos.X)
|
||||
bounce := false
|
||||
|
||||
switch ch {
|
||||
case Door:
|
||||
// this code is necessary if the hero is on a door and he
|
||||
@@ -252,29 +295,38 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
case '|', '-', ' ':
|
||||
bounce = true
|
||||
}
|
||||
|
||||
if bounce {
|
||||
if !changed {
|
||||
hitHero = !hitHero
|
||||
}
|
||||
|
||||
changed = false
|
||||
dir.Y = -dir.Y
|
||||
dir.X = -dir.X
|
||||
spotpos = spotpos[:len(spotpos)-1]
|
||||
|
||||
g.msg("the %s bounces", name)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if tp := g.Level.MonsterAt(pos.Y, pos.X); !hitHero && tp != nil {
|
||||
hitHero = true
|
||||
changed = !changed
|
||||
|
||||
tp.OldCh = g.Level.Char(pos.Y, pos.X)
|
||||
if !g.saveThrow(VsMagic, &tp.Stats) {
|
||||
bolt.Pos = pos
|
||||
used = true
|
||||
|
||||
if tp.Type == 'D' && name == "flame" {
|
||||
g.addmsg("the flame bounces")
|
||||
g.addmsgf("the flame bounces")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" off the dragon")
|
||||
g.addmsgf(" off the dragon")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
} else {
|
||||
g.hitMonster(pos, bolt)
|
||||
@@ -283,6 +335,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
if fromHero {
|
||||
g.runto(pos)
|
||||
}
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("%s misses", name)
|
||||
} else {
|
||||
@@ -292,6 +345,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
} else if hitHero && pos == p.Pos {
|
||||
hitHero = false
|
||||
changed = !changed
|
||||
|
||||
if !g.save(VsMagic) {
|
||||
if p.Stats.HP -= g.roll(6, 6); p.Stats.HP <= 0 {
|
||||
if fromHero {
|
||||
@@ -300,7 +354,9 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
g.death(g.Level.MonsterAt(start.Y, start.X).Type)
|
||||
}
|
||||
}
|
||||
|
||||
used = true
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("the %s hits", name)
|
||||
} else {
|
||||
@@ -310,6 +366,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
g.msg("the %s whizzes by you", name)
|
||||
}
|
||||
}
|
||||
|
||||
g.mvaddch(pos.Y, pos.X, dirch)
|
||||
g.refresh()
|
||||
}
|
||||
@@ -326,6 +383,7 @@ func (g *RogueGame) fixStick(cur *Object) {
|
||||
} else {
|
||||
cur.Damage = dice("1x1")
|
||||
}
|
||||
|
||||
cur.HurlDmg = dice("1x1")
|
||||
|
||||
switch cur.WandKind() {
|
||||
@@ -342,8 +400,10 @@ func chargeStr(g *RogueGame, obj *Object) string {
|
||||
if !obj.Flags.Has(Known) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if g.Options.Terse {
|
||||
return fmt.Sprintf(" [%d]", obj.Charges)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(" [%d charges]", obj.Charges)
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ var sylls = []string{
|
||||
"prok", "re", "rea", "rhov", "ri", "ro", "rog", "rok", "rol", "sa",
|
||||
"san", "sat", "sef", "seh", "shu", "ski", "sna", "sne", "snik",
|
||||
"sno", "so", "sol", "sri", "sta", "sun", "ta", "tab", "tem",
|
||||
"ther", "ti", "tox", "trol", "tue", "turs", "u", "ulk", "um", "un",
|
||||
"there", "ti", "tox", "trol", "tue", "turs", "u", "ulk", "um", "un",
|
||||
"uni", "ur", "val", "viv", "vly", "vom", "wah", "wed", "werg",
|
||||
"wex", "whon", "wun", "xo", "y", "yot", "yu", "zant", "zeb", "zim",
|
||||
"zok", "zon", "zum",
|
||||
|
||||
@@ -9,8 +9,10 @@ func TestProbabilitiesSumTo100(t *testing.T) {
|
||||
for _, oi := range info {
|
||||
s += oi.Prob
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
tables := map[string][]ObjInfo{
|
||||
"things": baseThings[:],
|
||||
"potions": basePotInfo[:],
|
||||
@@ -29,10 +31,12 @@ func TestProbabilitiesSumTo100(t *testing.T) {
|
||||
|
||||
func TestInitProbsCumulative(t *testing.T) {
|
||||
g := NewGame(Config{Seed: 1})
|
||||
|
||||
last := g.Items.Potions[NumPotionTypes-1].Prob
|
||||
if last != 100 {
|
||||
t.Errorf("cumulative potion probability ends at %d, want 100", last)
|
||||
}
|
||||
|
||||
for i := PotionKind(1); i < NumPotionTypes; i++ {
|
||||
if g.Items.Potions[i].Prob < g.Items.Potions[i-1].Prob {
|
||||
t.Errorf("potion probs not nondecreasing at %d", i)
|
||||
@@ -43,27 +47,34 @@ func TestInitProbsCumulative(t *testing.T) {
|
||||
func TestNewGameRandomizesAppearances(t *testing.T) {
|
||||
g := NewGame(Config{Seed: 12345})
|
||||
seen := map[string]bool{}
|
||||
|
||||
for i, c := range g.Items.PotColors {
|
||||
if c == "" {
|
||||
t.Fatalf("potion %d has no color", i)
|
||||
}
|
||||
|
||||
if seen[c] {
|
||||
t.Errorf("potion color %q assigned twice", c)
|
||||
}
|
||||
|
||||
seen[c] = true
|
||||
}
|
||||
|
||||
for i, n := range g.Items.ScrNames {
|
||||
if n == "" {
|
||||
t.Fatalf("scroll %d has no name", i)
|
||||
}
|
||||
|
||||
if len(n) > MaxNameLen+1 {
|
||||
t.Errorf("scroll name %q longer than C buffer allows", n)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range g.Items.WandType {
|
||||
if g.Items.WandType[i] != "wand" && g.Items.WandType[i] != "staff" {
|
||||
t.Errorf("stick %d has type %q", i, g.Items.WandType[i])
|
||||
}
|
||||
|
||||
if g.Items.WandMade[i] == "" {
|
||||
t.Errorf("stick %d has no material", i)
|
||||
}
|
||||
@@ -80,6 +91,7 @@ func TestMonsterTable(t *testing.T) {
|
||||
if monsterTable[0].Name != "aquator" || monsterTable[25].Name != "zombie" {
|
||||
t.Error("monster table order broken")
|
||||
}
|
||||
|
||||
if monsterTable['D'-'A'].Name != "dragon" {
|
||||
t.Error("letter indexing broken")
|
||||
}
|
||||
|
||||
@@ -15,11 +15,14 @@ func (t *testTerm) ReadChar() byte {
|
||||
if t.pos < len(t.input) {
|
||||
c := t.input[t.pos]
|
||||
t.pos++
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
t.tick++
|
||||
if t.tick%2 == 0 {
|
||||
return '\n'
|
||||
}
|
||||
|
||||
return ' '
|
||||
}
|
||||
|
||||
120
game/things.go
120
game/things.go
@@ -11,8 +11,10 @@ import (
|
||||
// (things.c inv_name).
|
||||
func (g *RogueGame) invName(obj *Object, drop bool) string {
|
||||
var pb strings.Builder
|
||||
|
||||
which := obj.Which
|
||||
it := &g.Items
|
||||
|
||||
switch obj.Kind {
|
||||
case KindPotion:
|
||||
g.nameit(&pb, obj, "potion", it.PotColors[which], &it.Potions[which], nullstr)
|
||||
@@ -26,12 +28,15 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "%d scrolls ", obj.Count)
|
||||
}
|
||||
|
||||
op := &it.Scrolls[which]
|
||||
if op.Know {
|
||||
|
||||
switch {
|
||||
case op.Know:
|
||||
fmt.Fprintf(&pb, "of %s", op.Name)
|
||||
} else if op.Guess != "" {
|
||||
case op.Guess != "":
|
||||
fmt.Fprintf(&pb, "called %s", op.Guess)
|
||||
} else {
|
||||
default:
|
||||
fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which])
|
||||
}
|
||||
case KindFood:
|
||||
@@ -50,19 +55,23 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
|
||||
}
|
||||
case KindWeapon:
|
||||
sp := it.Weapons[which].Name
|
||||
|
||||
if obj.Count > 1 {
|
||||
fmt.Fprintf(&pb, "%d ", obj.Count)
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "A%s ", vowelstr(sp))
|
||||
}
|
||||
|
||||
if obj.Flags.Has(Known) {
|
||||
fmt.Fprintf(&pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
|
||||
} else {
|
||||
pb.WriteString(sp)
|
||||
}
|
||||
|
||||
if obj.Count > 1 {
|
||||
pb.WriteString("s")
|
||||
}
|
||||
|
||||
if obj.Label != "" {
|
||||
fmt.Fprintf(&pb, " called %s", obj.Label)
|
||||
}
|
||||
@@ -70,13 +79,16 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
|
||||
sp := it.Armors[which].Name
|
||||
if obj.Flags.Has(Known) {
|
||||
fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.ArmorClass, 0, Armor), sp)
|
||||
|
||||
if !g.Options.Terse {
|
||||
pb.WriteString("protection ")
|
||||
}
|
||||
|
||||
fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass)
|
||||
} else {
|
||||
pb.WriteString(sp)
|
||||
}
|
||||
|
||||
if obj.Label != "" {
|
||||
fmt.Fprintf(&pb, " called %s", obj.Label)
|
||||
}
|
||||
@@ -87,20 +99,25 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
|
||||
}
|
||||
|
||||
out := pb.String()
|
||||
|
||||
if g.InvDescribe {
|
||||
p := &g.Player
|
||||
if obj == p.CurArmor {
|
||||
out += " (being worn)"
|
||||
}
|
||||
|
||||
if obj == p.CurWeapon {
|
||||
out += " (weapon in hand)"
|
||||
}
|
||||
if obj == p.CurRing[Left] {
|
||||
|
||||
switch obj {
|
||||
case p.CurRing[Left]:
|
||||
out += " (on left hand)"
|
||||
} else if obj == p.CurRing[Right] {
|
||||
case p.CurRing[Right]:
|
||||
out += " (on right hand)"
|
||||
}
|
||||
}
|
||||
|
||||
if out != "" {
|
||||
if drop && isUpper(out[0]) {
|
||||
out = string(toLower(out[0])) + out[1:]
|
||||
@@ -108,6 +125,7 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
|
||||
out = string(toUpper(out[0])) + out[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -115,28 +133,35 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
|
||||
// leavePack/detach vocabulary collision).
|
||||
func (g *RogueGame) dropIt() {
|
||||
p := &g.Player
|
||||
|
||||
ch := g.Level.Char(p.Pos.Y, p.Pos.X)
|
||||
if ch != Floor && ch != Passage {
|
||||
g.After = false
|
||||
g.msg("there is something there already")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
obj := g.getItem("drop", KindNone)
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !g.dropCheck(obj) {
|
||||
return
|
||||
}
|
||||
|
||||
obj = g.leavePack(obj, true, !obj.Kind.MergesInPack())
|
||||
// Link it into the level object list
|
||||
attachObj(&g.Level.Objects, obj)
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Kind.Glyph())
|
||||
g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped)
|
||||
|
||||
obj.Pos = p.Pos
|
||||
if obj.Kind == KindAmulet {
|
||||
g.HasAmulet = false
|
||||
}
|
||||
|
||||
g.msg("dropped %s", g.invName(obj, true))
|
||||
}
|
||||
|
||||
@@ -146,26 +171,34 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
|
||||
if obj == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
if obj != p.CurArmor && obj != p.CurWeapon &&
|
||||
obj != p.CurRing[Left] && obj != p.CurRing[Right] {
|
||||
return true
|
||||
}
|
||||
|
||||
if obj.Flags.Has(Cursed) {
|
||||
g.msg("you can't. It appears to be cursed")
|
||||
|
||||
return false
|
||||
}
|
||||
if obj == p.CurWeapon {
|
||||
|
||||
switch obj {
|
||||
case p.CurWeapon:
|
||||
p.CurWeapon = nil
|
||||
} else if obj == p.CurArmor {
|
||||
case p.CurArmor:
|
||||
g.wasteTime()
|
||||
|
||||
p.CurArmor = nil
|
||||
} else {
|
||||
default:
|
||||
hand := Right
|
||||
if obj == p.CurRing[Left] {
|
||||
hand = Left
|
||||
}
|
||||
|
||||
p.CurRing[hand] = nil
|
||||
|
||||
switch obj.RingKind() {
|
||||
case RingAddStrength:
|
||||
g.chgStr(-obj.Bonus)
|
||||
@@ -174,6 +207,7 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
|
||||
g.Extinguish(DUnsee)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -193,6 +227,7 @@ func (g *RogueGame) newThing() *Object {
|
||||
} else {
|
||||
kind = pickOne(g, g.Items.Things[:])
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case 0:
|
||||
cur.Kind = KindPotion
|
||||
@@ -202,6 +237,7 @@ func (g *RogueGame) newThing() *Object {
|
||||
cur.Which = pickOne(g, g.Items.Scrolls[:])
|
||||
case 2:
|
||||
cur.Kind = KindFood
|
||||
|
||||
g.Player.NoFood = 0
|
||||
if g.rnd(10) != 0 {
|
||||
cur.Which = 0
|
||||
@@ -210,6 +246,7 @@ func (g *RogueGame) newThing() *Object {
|
||||
}
|
||||
case 3:
|
||||
g.initWeapon(cur, WeaponKind(pickOne(g, g.Items.Weapons[:NumWeaponTypes])))
|
||||
|
||||
if r := g.rnd(100); r < 10 {
|
||||
cur.Flags.Set(Cursed)
|
||||
cur.HPlus -= g.rnd(3) + 1
|
||||
@@ -219,6 +256,7 @@ func (g *RogueGame) newThing() *Object {
|
||||
case 4:
|
||||
cur.Kind = KindArmor
|
||||
cur.Which = pickOne(g, g.Items.Armors[:])
|
||||
|
||||
cur.ArmorClass = aClass[cur.Which]
|
||||
if r := g.rnd(100); r < 20 {
|
||||
cur.Flags.Set(Cursed)
|
||||
@@ -228,6 +266,7 @@ func (g *RogueGame) newThing() *Object {
|
||||
}
|
||||
case 5:
|
||||
cur.Kind = KindRing
|
||||
|
||||
cur.Which = pickOne(g, g.Items.Rings[:])
|
||||
switch cur.RingKind() {
|
||||
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
|
||||
@@ -243,6 +282,7 @@ func (g *RogueGame) newThing() *Object {
|
||||
cur.Which = pickOne(g, g.Items.Sticks[:])
|
||||
g.fixStick(cur)
|
||||
}
|
||||
|
||||
return cur
|
||||
}
|
||||
|
||||
@@ -255,6 +295,7 @@ func pickOne(g *RogueGame, info []ObjInfo) int {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
return 0 // bad pick_one: C resets to the start of the table
|
||||
}
|
||||
|
||||
@@ -272,20 +313,27 @@ type invPage struct {
|
||||
// (things.c discovered).
|
||||
func (g *RogueGame) discovered() {
|
||||
var ch byte
|
||||
|
||||
for {
|
||||
discList := false
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("for ")
|
||||
g.addmsgf("for ")
|
||||
}
|
||||
g.addmsg("what type")
|
||||
|
||||
g.addmsgf("what type")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" of object do you want a list")
|
||||
g.addmsgf(" of object do you want a list")
|
||||
}
|
||||
|
||||
g.msg("? (* for all)")
|
||||
|
||||
ch = g.readchar()
|
||||
switch ch {
|
||||
case Escape:
|
||||
g.msg("")
|
||||
|
||||
return
|
||||
case Potion, Scroll, Ring, Stick, '*':
|
||||
discList = true
|
||||
@@ -297,10 +345,12 @@ func (g *RogueGame) discovered() {
|
||||
Potion, Scroll, Ring, Stick)
|
||||
}
|
||||
}
|
||||
|
||||
if discList {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '*' {
|
||||
g.printDisc(Potion)
|
||||
g.addLine("")
|
||||
@@ -320,6 +370,7 @@ func (g *RogueGame) discovered() {
|
||||
// (things.c print_disc).
|
||||
func (g *RogueGame) printDisc(typ byte) {
|
||||
var info []ObjInfo
|
||||
|
||||
switch typ {
|
||||
case Scroll:
|
||||
info = g.Items.Scrolls[:]
|
||||
@@ -330,18 +381,23 @@ func (g *RogueGame) printDisc(typ byte) {
|
||||
case Stick:
|
||||
info = g.Items.Sticks[:]
|
||||
}
|
||||
|
||||
order := make([]int, len(info))
|
||||
g.setOrder(order)
|
||||
|
||||
obj := Object{Count: 1}
|
||||
numFound := 0
|
||||
|
||||
for i := range info {
|
||||
if info[order[i]].Know || info[order[i]].Guess != "" {
|
||||
obj.Kind = objectKindForGlyph(typ)
|
||||
obj.Which = order[i]
|
||||
g.addLine("%s", g.invName(&obj, false))
|
||||
|
||||
numFound++
|
||||
}
|
||||
}
|
||||
|
||||
if numFound == 0 {
|
||||
g.addLine("%s", g.nothing(typ))
|
||||
}
|
||||
@@ -353,6 +409,7 @@ func (g *RogueGame) setOrder(order []int) {
|
||||
for i := range order {
|
||||
order[i] = i
|
||||
}
|
||||
|
||||
for i := len(order); i > 0; i-- {
|
||||
r := g.rnd(i)
|
||||
order[i-1], order[r] = order[r], order[i-1]
|
||||
@@ -368,6 +425,7 @@ func (g *RogueGame) addLine(format string, a ...any) int {
|
||||
pg := &g.invPage
|
||||
prompt := "--Press space to continue--"
|
||||
isFlush := format == flushSentinel
|
||||
|
||||
var line string
|
||||
if !isFlush {
|
||||
line = fmt.Sprintf(format, a...)
|
||||
@@ -375,36 +433,43 @@ func (g *RogueGame) addLine(format string, a ...any) int {
|
||||
|
||||
if pg.lineCnt == 0 {
|
||||
g.scr.Hw.Clear()
|
||||
|
||||
if g.Options.InvType == InvSlow {
|
||||
g.Msgs.Mpos = 0
|
||||
}
|
||||
}
|
||||
|
||||
if g.Options.InvType == InvSlow {
|
||||
if !isFlush && line != "" {
|
||||
if g.msg("%s", line) == Escape {
|
||||
return Escape
|
||||
}
|
||||
}
|
||||
|
||||
pg.lineCnt++
|
||||
} else {
|
||||
if !pg.init {
|
||||
pg.maxlen = len(prompt)
|
||||
pg.init = true
|
||||
}
|
||||
|
||||
if pg.lineCnt >= NumLines-1 || isFlush {
|
||||
if g.Options.InvType == InvOver && isFlush && !pg.newpage {
|
||||
// Overlay the accumulated list in a box at the top right
|
||||
// of the screen, prompt, and restore what was beneath.
|
||||
g.msg("")
|
||||
g.refresh()
|
||||
|
||||
saved := NewWindow(NumLines, NumCols)
|
||||
saved.CopyFrom(g.scr.Std)
|
||||
|
||||
lx := NumCols - pg.maxlen - 2
|
||||
for y := 0; y <= pg.lineCnt; y++ {
|
||||
for x := 0; x <= pg.maxlen; x++ {
|
||||
g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x))
|
||||
}
|
||||
}
|
||||
|
||||
g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt)
|
||||
g.refresh()
|
||||
g.waitFor(' ')
|
||||
@@ -417,19 +482,24 @@ func (g *RogueGame) addLine(format string, a ...any) int {
|
||||
g.scr.Hw.Clear()
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
pg.newpage = true
|
||||
pg.lineCnt = 0
|
||||
pg.maxlen = len(prompt)
|
||||
}
|
||||
if !isFlush && !(pg.lineCnt == 0 && line == "") {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -447,6 +517,7 @@ func (g *RogueGame) endLine() {
|
||||
g.flushLine()
|
||||
}
|
||||
}
|
||||
|
||||
pg.lineCnt = 0
|
||||
pg.newpage = false
|
||||
}
|
||||
@@ -459,8 +530,10 @@ func (g *RogueGame) nothing(typ byte) string {
|
||||
} else {
|
||||
out = "Haven't discovered anything"
|
||||
}
|
||||
|
||||
if typ != '*' {
|
||||
var tystr string
|
||||
|
||||
switch typ {
|
||||
case Potion:
|
||||
tystr = "potion"
|
||||
@@ -471,8 +544,10 @@ func (g *RogueGame) nothing(typ byte) string {
|
||||
case Stick:
|
||||
tystr = "stick"
|
||||
}
|
||||
|
||||
out += fmt.Sprintf(" about any %ss", tystr)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -480,20 +555,22 @@ func (g *RogueGame) nothing(typ byte) string {
|
||||
// (things.c nameit).
|
||||
func (g *RogueGame) nameit(pb *strings.Builder, obj *Object, typ, which string,
|
||||
op *ObjInfo, prfunc func(*RogueGame, *Object) string) {
|
||||
if op.Know || op.Guess != "" {
|
||||
switch {
|
||||
case op.Know || op.Guess != "":
|
||||
if obj.Count == 1 {
|
||||
fmt.Fprintf(pb, "A %s ", typ)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "%d %ss ", obj.Count, typ)
|
||||
}
|
||||
|
||||
if op.Know {
|
||||
fmt.Fprintf(pb, "of %s%s(%s)", op.Name, prfunc(g, obj), which)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "called %s%s(%s)", op.Guess, prfunc(g, obj), which)
|
||||
}
|
||||
} else if obj.Count == 1 {
|
||||
case obj.Count == 1:
|
||||
fmt.Fprintf(pb, "A%s %s %s", vowelstr(which), which, typ)
|
||||
} else {
|
||||
default:
|
||||
fmt.Fprintf(pb, "%d %s %ss", obj.Count, which, typ)
|
||||
}
|
||||
}
|
||||
@@ -505,13 +582,17 @@ func nullstr(*RogueGame, *Object) string { return "" }
|
||||
// pr_list).
|
||||
func (g *RogueGame) prList() {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("for ")
|
||||
g.addmsgf("for ")
|
||||
}
|
||||
g.addmsg("what type")
|
||||
|
||||
g.addmsgf("what type")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" of object do you want a list")
|
||||
g.addmsgf(" of object do you want a list")
|
||||
}
|
||||
|
||||
g.msg("? ")
|
||||
|
||||
ch := g.readchar()
|
||||
switch ch {
|
||||
case Potion:
|
||||
@@ -533,14 +614,17 @@ func (g *RogueGame) prList() {
|
||||
// (things.c pr_spec).
|
||||
func (g *RogueGame) prSpec(info []ObjInfo) {
|
||||
lastprob := 0
|
||||
|
||||
i := byte('0')
|
||||
for idx := range info {
|
||||
if i == '9'+1 {
|
||||
i = 'a'
|
||||
}
|
||||
|
||||
g.addLine("%c: %s (%d%%)", i, info[idx].Name, info[idx].Prob-lastprob)
|
||||
lastprob = info[idx].Prob
|
||||
i++
|
||||
}
|
||||
|
||||
g.endLine()
|
||||
}
|
||||
|
||||
@@ -90,22 +90,29 @@ const (
|
||||
VsMagic = 3
|
||||
)
|
||||
|
||||
// Flags for rooms (rogue.h)
|
||||
// RoomFlags are the room state bits (rogue.h room flags).
|
||||
type RoomFlags int16
|
||||
|
||||
// Room state bits (rogue.h ISDARK/ISGONE/ISMAZE).
|
||||
const (
|
||||
Dark RoomFlags = 1 << iota // room is dark
|
||||
Gone // room is gone (a corridor)
|
||||
Maze // room is a maze
|
||||
)
|
||||
|
||||
func (f RoomFlags) Has(b RoomFlags) bool { return f&b != 0 }
|
||||
// Has reports whether any of the given bits are set.
|
||||
func (f *RoomFlags) Has(b RoomFlags) bool { return *f&b != 0 }
|
||||
|
||||
// Set turns the given bits on.
|
||||
func (f *RoomFlags) Set(b RoomFlags) { *f |= b }
|
||||
|
||||
// Clear turns the given bits off.
|
||||
func (f *RoomFlags) Clear(b RoomFlags) { *f &^= b }
|
||||
|
||||
// Flags for objects (rogue.h)
|
||||
// ObjFlags are the object state bits (rogue.h object flags).
|
||||
type ObjFlags int32
|
||||
|
||||
// Object state bits (rogue.h).
|
||||
const (
|
||||
Cursed ObjFlags = 1 << iota // ISCURSED: object is cursed
|
||||
Known // ISKNOW: player knows details about the object
|
||||
@@ -115,15 +122,22 @@ const (
|
||||
Protected // ISPROT: armor is permanently protected
|
||||
)
|
||||
|
||||
func (f ObjFlags) Has(b ObjFlags) bool { return f&b != 0 }
|
||||
// Has reports whether any of the given bits are set.
|
||||
func (f *ObjFlags) Has(b ObjFlags) bool { return *f&b != 0 }
|
||||
|
||||
// Set turns the given bits on.
|
||||
func (f *ObjFlags) Set(b ObjFlags) { *f |= b }
|
||||
|
||||
// Clear turns the given bits off.
|
||||
func (f *ObjFlags) Clear(b ObjFlags) { *f &^= b }
|
||||
|
||||
// Flags for creatures (rogue.h). The C bit collisions are deliberate and
|
||||
// preserved: one name of each pair applies to monsters, the other to the
|
||||
// hero, and they never coexist on one creature.
|
||||
// CreatureFlags are the creature state bits (rogue.h creature flags). The
|
||||
// C bit collisions are deliberate and preserved: one name of each pair
|
||||
// applies to monsters, the other to the hero, and they never coexist on
|
||||
// one creature.
|
||||
type CreatureFlags int32
|
||||
|
||||
// Creature state bits (rogue.h).
|
||||
const (
|
||||
CanConfuse CreatureFlags = 0o000001 // CANHUH: creature can confuse
|
||||
CanSeeInvisible CreatureFlags = 0o000002 // CANSEE: creature can see invisible creatures
|
||||
@@ -146,13 +160,20 @@ const (
|
||||
Slowed CreatureFlags = 0o100000 // ISSLOW: creature has been slowed
|
||||
)
|
||||
|
||||
func (f CreatureFlags) Has(b CreatureFlags) bool { return f&b != 0 }
|
||||
// Has reports whether any of the given bits are set.
|
||||
func (f *CreatureFlags) Has(b CreatureFlags) bool { return *f&b != 0 }
|
||||
|
||||
// Set turns the given bits on.
|
||||
func (f *CreatureFlags) Set(b CreatureFlags) { *f |= b }
|
||||
|
||||
// Clear turns the given bits off.
|
||||
func (f *CreatureFlags) Clear(b CreatureFlags) { *f &^= b }
|
||||
|
||||
// Flags for the level map (rogue.h)
|
||||
// PlaceFlags are the per-map-cell bits (rogue.h level map flags). The low
|
||||
// bits double as the passage number (FPassNum) or trap kind (FTrapMask).
|
||||
type PlaceFlags uint8
|
||||
|
||||
// Map cell bits (rogue.h).
|
||||
const (
|
||||
FPassage PlaceFlags = 0x80 // F_PASS: is a passageway
|
||||
FSeen PlaceFlags = 0x40 // have seen this spot before
|
||||
@@ -163,14 +184,20 @@ const (
|
||||
FTrapMask PlaceFlags = 0x07 // F_TMASK: trap number mask
|
||||
)
|
||||
|
||||
func (f PlaceFlags) Has(b PlaceFlags) bool { return f&b != 0 }
|
||||
// Has reports whether any of the given bits are set.
|
||||
func (f *PlaceFlags) Has(b PlaceFlags) bool { return *f&b != 0 }
|
||||
|
||||
// Set turns the given bits on.
|
||||
func (f *PlaceFlags) Set(b PlaceFlags) { *f |= b }
|
||||
|
||||
// Clear turns the given bits off.
|
||||
func (f *PlaceFlags) Clear(b PlaceFlags) { *f &^= b }
|
||||
|
||||
// TrapKind identifies a trap (rogue.h trap types). The kind is stored in
|
||||
// the low bits of a map cell's PlaceFlags (FTrapMask).
|
||||
type TrapKind int
|
||||
|
||||
// Trap kinds (rogue.h T_* constants).
|
||||
const (
|
||||
TrapDoor TrapKind = 0
|
||||
TrapArrow TrapKind = 1
|
||||
@@ -189,6 +216,7 @@ func (t TrapKind) String() string {
|
||||
if t < 0 || t >= NumTrapTypes {
|
||||
return "a bizarre trap"
|
||||
}
|
||||
|
||||
return trName[t]
|
||||
}
|
||||
|
||||
@@ -219,6 +247,7 @@ func (p PotionKind) String() string {
|
||||
if p < 0 || p >= NumPotionTypes {
|
||||
return "strange potion"
|
||||
}
|
||||
|
||||
return basePotInfo[p].Name
|
||||
}
|
||||
|
||||
@@ -253,6 +282,7 @@ func (s ScrollKind) String() string {
|
||||
if s < 0 || s >= NumScrollTypes {
|
||||
return "strange scroll"
|
||||
}
|
||||
|
||||
return baseScrInfo[s].Name
|
||||
}
|
||||
|
||||
@@ -271,14 +301,18 @@ const (
|
||||
WeaponShuriken
|
||||
WeaponSpear
|
||||
WeaponFlame // fake entry for dragon breath (ick)
|
||||
NumWeaponTypes = WeaponFlame
|
||||
)
|
||||
|
||||
// NumWeaponTypes counts the real weapons; the flame pseudo-weapon sits
|
||||
// just past them in the tables (C's MAXWEAPONS == FLAME).
|
||||
const NumWeaponTypes = WeaponFlame
|
||||
|
||||
// String returns the weapon's name ("mace", "two handed sword", ...).
|
||||
func (w WeaponKind) String() string {
|
||||
if w < 0 || w > WeaponFlame {
|
||||
return "strange weapon"
|
||||
}
|
||||
|
||||
return baseWeapInfo[w].Name
|
||||
}
|
||||
|
||||
@@ -303,6 +337,7 @@ func (a ArmorKind) String() string {
|
||||
if a < 0 || a >= NumArmorTypes {
|
||||
return "strange armor"
|
||||
}
|
||||
|
||||
return baseArmInfo[a].Name
|
||||
}
|
||||
|
||||
@@ -333,6 +368,7 @@ func (r RingKind) String() string {
|
||||
if r < 0 || r >= NumRingTypes {
|
||||
return "strange ring"
|
||||
}
|
||||
|
||||
return baseRingInfo[r].Name
|
||||
}
|
||||
|
||||
@@ -363,6 +399,7 @@ func (w WandKind) String() string {
|
||||
if w < 0 || w >= NumWandTypes {
|
||||
return "strange stick"
|
||||
}
|
||||
|
||||
return baseWsInfo[w].Name
|
||||
}
|
||||
|
||||
@@ -425,6 +462,7 @@ func CTRL(c byte) byte { return c & 0o37 }
|
||||
func distance(y1, x1, y2, x2 int) int {
|
||||
dx := x2 - x1
|
||||
dy := y2 - y1
|
||||
|
||||
return dx*dx + dy*dy
|
||||
}
|
||||
|
||||
@@ -439,5 +477,6 @@ func sign(nm int) int {
|
||||
case nm > 0:
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ func (g *RogueGame) missile(ydelta, xdelta int) {
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !g.dropCheck(obj) || g.isCurrent(obj) {
|
||||
return
|
||||
}
|
||||
|
||||
obj = g.leavePack(obj, true, false)
|
||||
g.doMotion(obj, ydelta, xdelta)
|
||||
// AHA! Here it has hit something. If it is a wall or a door, or if
|
||||
@@ -39,11 +41,13 @@ func (g *RogueGame) doMotion(obj *Object, ydelta, xdelta int) {
|
||||
if ch == Floor && !g.showFloor() {
|
||||
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.
|
||||
@@ -51,8 +55,10 @@ func (g *RogueGame) doMotion(obj *Object, ydelta, xdelta int) {
|
||||
g.mvaddch(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -62,6 +68,7 @@ func (g *RogueGame) fall(obj *Object, pr bool) {
|
||||
if fpos, ok := g.fallpos(obj.Pos); ok {
|
||||
pp := g.Level.At(fpos.Y, fpos.X)
|
||||
pp.Ch = obj.Kind.Glyph()
|
||||
|
||||
obj.Pos = fpos
|
||||
if g.cansee(fpos.Y, fpos.X) {
|
||||
if pp.Monst != nil {
|
||||
@@ -70,14 +77,18 @@ func (g *RogueGame) fall(obj *Object, pr bool) {
|
||||
g.mvaddch(fpos.Y, fpos.X, obj.Kind.Glyph())
|
||||
}
|
||||
}
|
||||
|
||||
attachObj(&g.Level.Objects, obj)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if pr {
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
}
|
||||
|
||||
g.msg("the %s vanishes as it hits the ground",
|
||||
g.Items.Weapons[obj.Which].Name)
|
||||
}
|
||||
@@ -92,32 +103,43 @@ func (g *RogueGame) hitMonster(mp Coord, obj *Object) bool {
|
||||
// wield pulls out a certain weapon (weapons.c wield).
|
||||
func (g *RogueGame) wield() {
|
||||
p := &g.Player
|
||||
|
||||
oweapon := p.CurWeapon
|
||||
if !g.dropCheck(p.CurWeapon) {
|
||||
p.CurWeapon = oweapon
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p.CurWeapon = oweapon
|
||||
|
||||
obj := g.getItem("wield", KindWeapon)
|
||||
if obj == nil {
|
||||
g.After = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind == KindArmor {
|
||||
g.msg("you can't wield armor")
|
||||
g.After = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if g.isCurrent(obj) {
|
||||
g.After = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sp := g.invName(obj, true)
|
||||
p.CurWeapon = obj
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you are now ")
|
||||
g.addmsgf("you are now ")
|
||||
}
|
||||
|
||||
g.msg("wielding %s (%c)", sp, obj.PackCh)
|
||||
}
|
||||
|
||||
@@ -149,16 +171,19 @@ func (g *RogueGame) initWeapon(weap *Object, which WeaponKind) {
|
||||
weap.Launch = iwp.launch
|
||||
weap.Flags = iwp.flags
|
||||
weap.HPlus = 0
|
||||
|
||||
weap.DPlus = 0
|
||||
if which == WeaponDagger {
|
||||
|
||||
switch {
|
||||
case which == WeaponDagger:
|
||||
weap.Count = g.rnd(4) + 2
|
||||
weap.Group = g.Items.Group
|
||||
g.Items.Group++
|
||||
} else if weap.Flags.Has(Stackable) {
|
||||
case weap.Flags.Has(Stackable):
|
||||
weap.Count = g.rnd(8) + 8
|
||||
weap.Group = g.Items.Group
|
||||
g.Items.Group++
|
||||
} else {
|
||||
default:
|
||||
weap.Count = 1
|
||||
weap.Group = 0
|
||||
}
|
||||
@@ -170,6 +195,7 @@ func num(n1, n2 int, typ byte) string {
|
||||
if typ == Weapon {
|
||||
out += fmt.Sprintf(",%+d", n2)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -177,7 +203,9 @@ func num(n1, n2 int, typ byte) string {
|
||||
// (weapons.c fallpos).
|
||||
func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
|
||||
var newpos Coord
|
||||
|
||||
cnt := 0
|
||||
|
||||
for y := pos.Y - 1; y <= pos.Y+1; y++ {
|
||||
for x := pos.X - 1; x <= pos.X+1; x++ {
|
||||
// check to make certain the spot is empty, if it is, put the
|
||||
@@ -187,6 +215,7 @@ func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
|
||||
y < 0 || x < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ch := g.Level.Char(y, x)
|
||||
if ch == Floor || ch == Passage {
|
||||
if cnt++; g.rnd(cnt) == 0 {
|
||||
@@ -196,5 +225,6 @@ func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newpos, cnt != 0
|
||||
}
|
||||
|
||||
@@ -8,32 +8,40 @@ package game
|
||||
// create_obj).
|
||||
func (g *RogueGame) createObj() {
|
||||
obj := newObject()
|
||||
|
||||
g.msg("type of item: ")
|
||||
obj.Kind = objectKindForGlyph(g.readchar())
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("which %c do you want? (0-f)", obj.Kind.Glyph())
|
||||
|
||||
ch := g.readchar()
|
||||
if isDigit(ch) {
|
||||
obj.Which = int(ch - '0')
|
||||
} else {
|
||||
obj.Which = int(ch-'a') + 10
|
||||
}
|
||||
|
||||
obj.Group = 0
|
||||
obj.Count = 1
|
||||
g.Msgs.Mpos = 0
|
||||
switch {
|
||||
case obj.Kind == KindWeapon || obj.Kind == KindArmor:
|
||||
|
||||
switch obj.Kind {
|
||||
case KindWeapon, KindArmor:
|
||||
g.msg("blessing? (+,-,n)")
|
||||
bless := g.readchar()
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
if bless == '-' {
|
||||
obj.Flags.Set(Cursed)
|
||||
}
|
||||
|
||||
if obj.Kind == KindWeapon {
|
||||
g.initWeapon(obj, WeaponKind(obj.Which))
|
||||
|
||||
if bless == '-' {
|
||||
obj.HPlus -= g.rnd(3) + 1
|
||||
}
|
||||
|
||||
if bless == '+' {
|
||||
obj.HPlus += g.rnd(3) + 1
|
||||
}
|
||||
@@ -42,16 +50,18 @@ func (g *RogueGame) createObj() {
|
||||
if bless == '-' {
|
||||
obj.ArmorClass += g.rnd(3) + 1
|
||||
}
|
||||
|
||||
if bless == '+' {
|
||||
obj.ArmorClass -= g.rnd(3) + 1
|
||||
}
|
||||
}
|
||||
case obj.Kind == KindRing:
|
||||
case KindRing:
|
||||
switch obj.RingKind() {
|
||||
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
|
||||
g.msg("blessing? (+,-,n)")
|
||||
bless := g.readchar()
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
if bless == '-' {
|
||||
obj.Flags.Set(Cursed)
|
||||
obj.Bonus = -1
|
||||
@@ -61,15 +71,17 @@ func (g *RogueGame) createObj() {
|
||||
case RingAggravateMonsters, RingTeleportation:
|
||||
obj.Flags.Set(Cursed)
|
||||
}
|
||||
case obj.Kind == KindWand:
|
||||
case KindWand:
|
||||
g.fixStick(obj)
|
||||
case obj.Kind == KindGold:
|
||||
case KindGold:
|
||||
g.msg("how much?")
|
||||
|
||||
buf := ""
|
||||
if g.getStr(&buf, g.scr.Std) == Norm {
|
||||
obj.GoldValue = cAtoi(buf)
|
||||
}
|
||||
}
|
||||
|
||||
g.addPack(obj, false)
|
||||
}
|
||||
|
||||
@@ -77,18 +89,22 @@ func (g *RogueGame) createObj() {
|
||||
func (g *RogueGame) showMap() {
|
||||
hw := g.scr.Hw
|
||||
hw.Clear()
|
||||
|
||||
for y := 1; y < NumLines-1; y++ {
|
||||
for x := 0; x < NumCols; x++ {
|
||||
real := g.Level.FlagsAt(y, x).Has(FReal)
|
||||
if !real {
|
||||
for x := range NumCols {
|
||||
isReal := g.Level.FlagsAt(y, x).Has(FReal)
|
||||
if !isReal {
|
||||
hw.Standout(true)
|
||||
}
|
||||
|
||||
hw.MvAddCh(y, x, g.Level.Char(y, x))
|
||||
if !real {
|
||||
|
||||
if !isReal {
|
||||
hw.Standout(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.showWin("---More (level map)---")
|
||||
}
|
||||
|
||||
@@ -97,27 +113,35 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
|
||||
p := &g.Player
|
||||
if len(p.Pack) == 0 {
|
||||
g.msg("you don't have anything in your pack to identify")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var obj *Object
|
||||
for {
|
||||
obj = g.getItem("identify", kind)
|
||||
|
||||
if !insist {
|
||||
break
|
||||
}
|
||||
|
||||
if g.NObjs == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if obj == nil {
|
||||
g.msg("you must identify something")
|
||||
} else if kind != KindNone && obj.Kind != kind &&
|
||||
!(kind == KindRingOrStick &&
|
||||
(obj.Kind == KindRing || obj.Kind == KindWand)) {
|
||||
g.msg("you must identify a %s", kind)
|
||||
} else {
|
||||
break
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !matchesFilter(kind, obj) {
|
||||
g.msg("you must identify a %s", kind)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if obj == nil {
|
||||
@@ -136,6 +160,7 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
|
||||
case KindRing:
|
||||
setKnow(obj, g.Items.Rings[:])
|
||||
}
|
||||
|
||||
g.msg("%s", g.invName(obj, false))
|
||||
}
|
||||
|
||||
@@ -154,15 +179,18 @@ func setKnow(obj *Object, info []ObjInfo) {
|
||||
func (g *RogueGame) teleport() {
|
||||
p := &g.Player
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
|
||||
c, _ := g.findFloor(nil, 0, true)
|
||||
|
||||
c, _ := g.findFloor(true)
|
||||
if g.roomin(c) != p.Room {
|
||||
g.leaveRoom(p.Pos)
|
||||
p.Pos = c
|
||||
g.enterRoom(p.Pos)
|
||||
} else {
|
||||
p.Pos = c
|
||||
|
||||
g.look(true)
|
||||
}
|
||||
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, PlayerCh)
|
||||
// turn off ISHELD in case teleportation was done while fighting a
|
||||
// Flytrap
|
||||
@@ -171,6 +199,7 @@ func (g *RogueGame) teleport() {
|
||||
p.VfHit = 0
|
||||
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
|
||||
}
|
||||
|
||||
g.NoMove = 0
|
||||
g.Count = 0
|
||||
g.Running = false
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -21,6 +23,9 @@ type Tcell struct {
|
||||
last *game.Window // last rendered window, for resize redraws
|
||||
}
|
||||
|
||||
// ErrScreenTooSmall reports a terminal below the required 80x24.
|
||||
var ErrScreenTooSmall = errors.New("screen too small")
|
||||
|
||||
// New initializes the terminal. The screen must be at least 80x24, as the
|
||||
// C game required.
|
||||
func New() (*Tcell, error) {
|
||||
@@ -28,16 +33,22 @@ func New() (*Tcell, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.Init(); err != nil {
|
||||
return nil, err
|
||||
|
||||
initErr := s.Init()
|
||||
if initErr != nil {
|
||||
return nil, initErr
|
||||
}
|
||||
|
||||
w, h := s.Size()
|
||||
if h < game.NumLines || w < game.NumCols {
|
||||
s.Fini()
|
||||
return nil, fmt.Errorf("sorry, the screen must be at least %dx%d",
|
||||
game.NumLines, game.NumCols)
|
||||
|
||||
return nil, fmt.Errorf("sorry, %w: %dx%d required",
|
||||
ErrScreenTooSmall, game.NumCols, game.NumLines)
|
||||
}
|
||||
|
||||
s.HideCursor()
|
||||
|
||||
return &Tcell{screen: s}, nil
|
||||
}
|
||||
|
||||
@@ -49,17 +60,21 @@ func (t *Tcell) Fini() {
|
||||
// Render blits a game window to the terminal (curses refresh).
|
||||
func (t *Tcell) Render(w *game.Window) {
|
||||
t.last = w
|
||||
|
||||
rows, cols := w.Size()
|
||||
for y := 0; y < rows; y++ {
|
||||
for x := 0; x < cols; x++ {
|
||||
for y := range rows {
|
||||
for x := range cols {
|
||||
ch, standout := w.CellAt(y, x)
|
||||
|
||||
style := tcell.StyleDefault
|
||||
if standout {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
|
||||
t.screen.SetContent(x, y, rune(ch), nil, style)
|
||||
}
|
||||
}
|
||||
|
||||
t.screen.Show()
|
||||
}
|
||||
|
||||
@@ -105,8 +120,9 @@ func (t *Tcell) ReadChar() byte {
|
||||
return 3
|
||||
default:
|
||||
if ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ {
|
||||
return byte(ev.Key())
|
||||
return byte(ev.Key()) //nolint:gosec // G115: 1..26 fits
|
||||
}
|
||||
|
||||
if r := ev.Rune(); r > 0 && r < 0x80 {
|
||||
return byte(r)
|
||||
}
|
||||
@@ -118,18 +134,29 @@ func (t *Tcell) ReadChar() byte {
|
||||
// ShellEscape suspends the screen and runs the user's shell (main.c
|
||||
// shell + md_shellescape).
|
||||
func (t *Tcell) ShellEscape() {
|
||||
if err := t.screen.Suspend(); err != nil {
|
||||
err := t.screen.Suspend()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
shell := os.Getenv("SHELL")
|
||||
if shell == "" {
|
||||
shell = "/bin/sh"
|
||||
}
|
||||
fmt.Println("[Entering shell; exit to return to the game]")
|
||||
cmd := exec.Command(shell)
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stdout,
|
||||
"[Entering shell; exit to return to the game]")
|
||||
|
||||
// The shell session has no deadline by design; Background context.
|
||||
cmd := exec.CommandContext(context.Background(), //nolint:gosec // G204: the user's own $SHELL
|
||||
shell)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Run()
|
||||
t.screen.Resume()
|
||||
_ = cmd.Run() // best effort: the shell is the user's business
|
||||
|
||||
resumeErr := t.screen.Resume()
|
||||
if resumeErr != nil {
|
||||
panic(resumeErr) // terminal resume failure is unrecoverable
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user