Files
rgoue/game/misc.go
sneak cdf9bf73a9 go: port item effects — potions, scrolls, options, call_it
- potions.c completed: quaff with all 14 potion effects, the PACT
  standard-fuse table (do_pot), raise_level; the see-invisible fruit-
  juice message is computed at quaff time as in C
- scrolls.c in full: read_scroll with all 18 scroll effects including
  the magic-mapping map rewrite and identify dispatch, uncurse
- options.c in full: the interactive options screen, get_bool/get_str/
  get_inv_t/get_sf editors, ROGUEOPTS parsing (prefix matching, no-
  prefixed booleans, ~ expansion), strucpy
- misc.c call_it via the get_str line editor
- turn_see gets a DaemonID (C casts it to a fuse callback for the
  monster-detection potion)

Tests: healing/confusion potions with fuse burn-down, enchant armor,
hold monster, slow-monster zap, ROGUEOPTS parsing, and a regression
test documenting the C wake_monster ISGREED/ISHELD quirk the port
keeps faithfully.
2026-07-06 19:34:36 +02:00

471 lines
9.9 KiB
Go

package game
// misc.c — look() display maintenance, direction input, eating, level-ups,
// and small utilities. call_it arrives with the scroll/potion phase (it
// needs the get_str line editor).
// look takes a quick glance all around the player (misc.c look).
func (g *RogueGame) look(wakeup bool) {
p := &g.Player
hero := p.Pos
passcount := 0
rp := p.Room
if g.Oldpos != hero {
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
for y := sy; y <= ey; y++ {
if y <= 0 || y >= NumLines-1 {
continue
}
for x := sx; x <= ex; x++ {
if x < 0 || x >= NumCols {
continue
}
if !p.On(IsBlind) {
if y == hero.Y && x == hero.X {
continue
}
}
pp := g.Level.At(y, x)
ch := pp.Ch
if ch == ' ' { // nothing need be done with a ' '
continue
}
fp := &pp.Flags
if pch != Door && ch != Door {
if (pfl & FPass) != (*fp & FPass) {
continue
}
}
if (fp.Has(FPass) || ch == Door) && (pfl.Has(FPass) || pch == Door) {
if hero.X != x && hero.Y != y &&
!stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) {
continue
}
}
tp := pp.Monst
if tp == nil {
ch = g.tripCh(y, x, ch)
} else if p.On(SeeMonst) && tp.On(IsInvis) {
if g.DoorStop && !g.Firstmove {
g.Running = false
}
continue
} else {
if wakeup {
g.wakeMonster(y, x)
}
if g.seeMonst(tp) {
if p.On(IsHalu) {
ch = byte(g.rnd(26) + 'A')
} else {
ch = tp.Disguise
}
}
}
if p.On(IsBlind) && (y != hero.Y || x != hero.X) {
continue
}
g.move(y, x)
if p.Room.Flags.Has(IsDark) && !g.Options.SeeFloor && ch == Floor {
ch = ' '
}
if tp != nil || ch != g.inch() {
g.addch(ch)
}
if g.DoorStop && !g.Firstmove && g.Running {
switch g.RunCh {
case 'h':
if x == ex {
continue
}
case 'j':
if y == sy {
continue
}
case 'k':
if y == ey {
continue
}
case 'l':
if x == sx {
continue
}
case 'y':
if (y+x)-sumhero >= 1 {
continue
}
case 'u':
if (y-x)-diffhero >= 1 {
continue
}
case 'n':
if (y+x)-sumhero <= -1 {
continue
}
case 'b':
if (y-x)-diffhero <= -1 {
continue
}
}
switch ch {
case Door:
if x == hero.X || y == hero.Y {
g.Running = false
}
case Passage:
if x == hero.X || y == hero.Y {
passcount++
}
case Floor, '|', '-', ' ':
default:
g.Running = false
}
}
}
}
if g.DoorStop && !g.Firstmove && passcount > 1 {
g.Running = false
}
if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh)
}
}
// tripCh returns the character for this space, taking into account whether
// or not the player is tripping (misc.c trip_ch).
func (g *RogueGame) tripCh(y, x int, ch byte) byte {
if g.Player.On(IsHalu) && g.After {
switch ch {
case Floor, ' ', Passage, '-', '|', Door, Trap:
default:
if y != g.Level.Stairs.Y || x != g.Level.Stairs.X || !g.SeenStairs {
ch = g.rndThing()
}
}
}
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&(IsGone|IsDark) == IsDark &&
!g.Player.On(IsBlind)) {
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(' ')
}
}
}
}
// showFloor reports whether we show the floor in her room at this time
// (misc.c show_floor).
func (g *RogueGame) showFloor() bool {
if g.Player.Room.Flags&(IsGone|IsDark) == IsDark && !g.Player.On(IsBlind) {
return g.Options.SeeFloor
}
return true
}
// findObj finds the unclaimed object at (y, x) (misc.c find_obj).
func (g *RogueGame) findObj(y, x int) *Object {
for _, obj := range g.Level.Objects {
if obj.Pos.Y == y && obj.Pos.X == x {
return obj
}
}
return nil
}
// eat lets her try to eat something (misc.c eat).
func (g *RogueGame) eat() {
obj := g.getItem("eat", int(Food))
if obj == nil {
return
}
if obj.Type != Food {
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 {
g.msg("my, that was a yummy %s", g.Fruit)
} else if g.rnd(100) > 70 {
p.Stats.Exp++
g.msg("%s, this food tastes awful", g.chooseStr("bummer", "yuk"))
g.checkLevel()
} else {
g.msg("%s, that tasted good", g.chooseStr("oh, wow", "yum"))
}
g.leavePack(obj, false, false)
}
// checkLevel checks to see if the guy has gone up a level (misc.c
// 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)
}
}
// chgStr modifies the player's strength, keeping track of the highest it
// has been (misc.c chg_str).
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, RAddStr) {
addStr(&comp, -p.CurRing[Left].Arm)
}
if p.IsRing(Right, RAddStr) {
addStr(&comp, -p.CurRing[Right].Arm)
}
if comp > p.MaxStats.Str {
p.MaxStats.Str = comp
}
}
// addStr performs the actual strength add, checking bounds (misc.c add_str).
func addStr(sp *int, amt int) {
if *sp += amt; *sp < 3 {
*sp = 3
} else if *sp > 31 {
*sp = 31
}
}
// addHaste adds a haste to the player (misc.c add_haste).
func (g *RogueGame) addHaste(potion bool) bool {
p := &g.Player
if p.On(IsHaste) {
g.NoCommand += g.rnd(8)
p.Flags.Clear(IsRun | IsHaste)
g.Extinguish(DNohaste)
g.msg("you faint from exhaustion")
return false
}
p.Flags.Set(IsHaste)
if potion {
g.Fuse(DNohaste, 0, g.rnd(4)+4, After)
}
return true
}
// aggravate aggravates all the monsters on this level (misc.c aggravate).
func (g *RogueGame) aggravate() {
// runto() can splice the monster list while we walk it, so iterate a copy.
monsters := append([]*Monster(nil), g.Level.Monsters...)
for _, mp := range monsters {
g.runto(mp.Pos)
}
}
// vowelstr returns "n" if the string starts with a vowel, for "a"/"an"
// (misc.c vowelstr).
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 ""
}
// isCurrent sees if the object is one of the currently used items
// (misc.c is_current).
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.msg("in use")
return true
}
return false
}
// getDir sets up the direction coordinate for use in various "prefix"
// commands (misc.c get_dir).
func (g *RogueGame) getDir() bool {
if g.Again && g.LastDir != 0 {
g.Delta = g.lastDelt
g.DirCh = g.LastDir
} else {
prompt := "direction: "
if !g.Options.Terse {
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}
case 'j', 'J':
g.Delta = Coord{X: 0, Y: 1}
case 'k', 'K':
g.Delta = Coord{X: 0, Y: -1}
case 'l', 'L':
g.Delta = Coord{X: 1, Y: 0}
case 'y', 'Y':
g.Delta = Coord{X: -1, Y: -1}
case 'u', 'U':
g.Delta = Coord{X: 1, Y: -1}
case 'b', 'B':
g.Delta = Coord{X: -1, Y: 1}
case 'n', 'N':
g.Delta = Coord{X: 1, Y: 1}
case Escape:
g.LastDir = 0
g.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(IsHuh) && 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
}
// callIt calls an object something after use (misc.c call_it).
func (g *RogueGame) callIt(info *ObjInfo) {
if info.Know {
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 != "" {
info.Guess = buf
}
}
}
}
// thingList is misc.c rnd_thing()'s static table.
var thingList = []byte{
Potion, Scroll, Ring, Stick, Food, Weapon, Armor, Stairs, Gold, Amulet,
}
// rndThing picks a random thing appropriate for this level (misc.c
// rnd_thing).
func (g *RogueGame) rndThing() byte {
var i int
if g.Depth >= AmuletLevel {
i = g.rnd(len(thingList))
} else {
i = g.rnd(len(thingList) - 1)
}
return thingList[i]
}
// chooseStr picks the first or second string depending on whether the
// player is tripping (misc.c choose_str).
func (g *RogueGame) chooseStr(ts, ns string) string {
if g.Player.On(IsHalu) {
return ts
}
return ns
}
// unctrl gives a printable representation of a character, like curses
// unctrl(): control characters display as ^X.
func unctrl(ch byte) string {
if ch < ' ' {
return "^" + string(ch+'@')
}
if ch == 0x7f {
return "^?"
}
return string(ch)
}