Files
rgoue/game/things.go
sneak 6850c87ae7 Rename movement/world methods to idiomatic Go; remove all gotos
Refactor step 4. Renames (C breadcrumbs kept in doc comments):
doMove→moveHero, beTrapped→springTrap, rndmove→randomStep,
doRooms→digRooms, doPassages→digPassages, doMaze→digMaze,
chgStr→changeStrength, doRun→startRun, moveStuff→finishMove,
turnref→turnRefresh, moveMonst→moveMonster, doChase→chaseStep,
setOldch→setOldChar, cansee→canSee, roomin→roomIn, runto→runTo,
conn→connectRooms, putpass→putPassage, passnum→numberPassages,
numpass→numberPassage, rndPos→randomPos, rndRoom→randomRoom,
treasRoom→treasureRoom, accntMaze→accountMaze.

All goto/label flows are gone: moveHero uses a retry loop with the
PASSGO corner logic extracted into passageTurn; dispatch re-dispatches
via a loop; chaseStep re-checks via a loop; saveGame uses a labeled
prompt loop. Control flow and RNG call order are unchanged; suite
green.
2026-07-07 02:29:06 +02:00

631 lines
13 KiB
Go

package game
import (
"fmt"
"strings"
)
// things.c — item naming, random item creation, and the discovery list.
// invName returns the name of something as it would appear in an inventory
// (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, potionName, it.PotColors[which], &it.Potions[which], nullstr)
case KindRing:
g.nameit(&pb, obj, ringName, it.RingStones[which], &it.Rings[which], ringNum)
case KindWand:
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr)
case KindScroll:
if obj.Count == 1 {
pb.WriteString("A scroll ")
} else {
fmt.Fprintf(&pb, "%d scrolls ", obj.Count)
}
op := &it.Scrolls[which]
switch {
case op.Know:
fmt.Fprintf(&pb, "of %s", op.Name)
case op.Guess != "":
fmt.Fprintf(&pb, "called %s", op.Guess)
default:
fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which])
}
case KindFood:
if which == 1 {
if obj.Count == 1 {
fmt.Fprintf(&pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
} else {
fmt.Fprintf(&pb, "%d %ss", obj.Count, g.Fruit)
}
} else {
if obj.Count == 1 {
pb.WriteString("Some food")
} else {
fmt.Fprintf(&pb, "%d rations of food", obj.Count)
}
}
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)
}
case KindArmor:
sp := it.Armors[which].Name
if obj.Flags.Has(Known) {
fmt.Fprintf(&pb, "%s %s [", num(g.data.aClass[which]-obj.ArmorClass, 0, Armor), sp)
if !g.Options.Terse {
pb.WriteString("protection ")
}
fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass)
} else {
pb.WriteString(sp)
}
if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label)
}
case KindAmulet:
pb.WriteString("The Amulet of Yendor")
case KindGold:
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue)
}
out := pb.String()
if g.InvDescribe {
p := &g.Player
if obj == p.CurArmor {
out += " (being worn)"
}
if obj == p.CurWeapon {
out += " (weapon in hand)"
}
switch obj {
case p.CurRing[Left]:
out += " (on left hand)"
case p.CurRing[Right]:
out += " (on right hand)"
}
}
if out != "" {
if drop && isUpper(out[0]) {
out = string(toLower(out[0])) + out[1:]
} else if !drop && isLower(out[0]) {
out = string(toUpper(out[0])) + out[1:]
}
}
return out
}
// dropIt puts something down (things.c drop; renamed to avoid the
// 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))
}
// dropCheck does special checks for dropping or unwielding|unwearing|
// unringing (things.c dropcheck).
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
}
switch obj {
case p.CurWeapon:
p.CurWeapon = nil
case p.CurArmor:
g.wasteTime()
p.CurArmor = nil
default:
hand := Right
if obj == p.CurRing[Left] {
hand = Left
}
p.CurRing[hand] = nil
switch obj.RingKind() {
case RingAddStrength:
g.changeStrength(-obj.Bonus)
case RingSeeInvisible:
g.unsee(0)
g.Extinguish(DUnsee)
}
}
return true
}
// newThing returns a new random thing for the dungeon (things.c new_thing).
func (g *RogueGame) newThing() *Object {
cur := newObject()
cur.Damage = dice("0x0")
cur.HurlDmg = dice("0x0")
cur.ArmorClass = 11
cur.Count = 1
// Decide what kind of object it will be; if we haven't had food for a
// while, let it be food.
var kind int
if g.Player.NoFood > 3 {
kind = 2
} else {
kind = pickOne(g, g.Items.Things[:])
}
switch kind {
case 0:
cur.Kind = KindPotion
cur.Which = pickOne(g, g.Items.Potions[:])
case 1:
cur.Kind = KindScroll
cur.Which = pickOne(g, g.Items.Scrolls[:])
case 2:
cur.Kind = KindFood
g.Player.NoFood = 0
if g.rnd(10) != 0 {
cur.Which = 0
} else {
cur.Which = 1
}
case 3:
g.initWeapon(cur, WeaponKind(pickOne(g, g.Items.Weapons[:NumWeaponTypes])))
if r := g.rnd(100); r < 10 {
cur.Flags.Set(Cursed)
cur.HPlus -= g.rnd(3) + 1
} else if r < 15 {
cur.HPlus += g.rnd(3) + 1
}
case 4:
cur.Kind = KindArmor
cur.Which = pickOne(g, g.Items.Armors[:])
cur.ArmorClass = g.data.aClass[cur.Which]
if r := g.rnd(100); r < 20 {
cur.Flags.Set(Cursed)
cur.ArmorClass += g.rnd(3) + 1
} else if r < 28 {
cur.ArmorClass -= g.rnd(3) + 1
}
case 5:
cur.Kind = KindRing
cur.Which = pickOne(g, g.Items.Rings[:])
switch cur.RingKind() {
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
if cur.Bonus = g.rnd(3); cur.Bonus == 0 {
cur.Bonus = -1
cur.Flags.Set(Cursed)
}
case RingAggravateMonsters, RingTeleportation:
cur.Flags.Set(Cursed)
}
case 6:
cur.Kind = KindWand
cur.Which = pickOne(g, g.Items.Sticks[:])
g.fixStick(cur)
}
return cur
}
// pickOne picks an item out of a list of possible objects using their
// cumulative probabilities (things.c pick_one).
func pickOne(g *RogueGame, info []ObjInfo) int {
i := g.rnd(100)
for idx := range info {
if i < info[idx].Prob {
return idx
}
}
return 0 // bad pick_one: C resets to the start of the table
}
// invPage is the things.c static pagination state for the discovery/
// inventory list windows (line_cnt, newpage, lastfmt/lastarg, maxlen).
type invPage struct {
lineCnt int
newpage bool
lastLine string
maxlen int
init bool
}
// discovered lists what the player has found of a certain type
// (things.c discovered).
func (g *RogueGame) discovered() {
var ch byte
for {
discList := false
if !g.Options.Terse {
g.addmsgf("for ")
}
g.addmsgf("what type")
if !g.Options.Terse {
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
default:
if g.Options.Terse {
g.msg("Not a type")
} else {
g.msg("Please type one of %c%c%c%c (ESCAPE to quit)",
Potion, Scroll, Ring, Stick)
}
}
if discList {
break
}
}
if ch == '*' {
g.printDisc(Potion)
g.addLine("")
g.printDisc(Scroll)
g.addLine("")
g.printDisc(Ring)
g.addLine("")
g.printDisc(Stick)
g.endLine()
} else {
g.printDisc(ch)
g.endLine()
}
}
// printDisc prints what we've discovered of the given type
// (things.c print_disc).
func (g *RogueGame) printDisc(typ byte) {
var info []ObjInfo
switch typ {
case Scroll:
info = g.Items.Scrolls[:]
case Potion:
info = g.Items.Potions[:]
case Ring:
info = g.Items.Rings[:]
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))
}
}
// setOrder shuffles the display order for the discovery list
// (things.c set_order).
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]
}
}
// addLine adds a line to the list of discoveries (things.c add_line). A
// format of exactly "\x00" is the C fmt==NULL page-flush sentinel — use
// flushLine() for that.
const flushSentinel = "\x00"
func (g *RogueGame) addLine(format string, a ...any) int {
pg := &g.invPage
prompt := "--Press space to continue--"
isFlush := format == flushSentinel
var line string
if !isFlush {
line = fmt.Sprintf(format, a...)
}
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(' ')
g.scr.Std.CopyFrom(saved)
g.refresh()
} else {
g.scr.Hw.MvAddStr(NumLines-1, 0, prompt)
g.scr.RefreshWin(g.scr.Hw)
g.waitFor(' ')
g.scr.Hw.Clear()
g.refresh()
}
pg.newpage = true
pg.lineCnt = 0
pg.maxlen = len(prompt)
}
if !isFlush && (pg.lineCnt != 0 || line != "") {
g.scr.Hw.MvAddStr(pg.lineCnt, 0, line)
pg.lineCnt++
if pg.maxlen < len(line) {
pg.maxlen = len(line)
}
pg.lastLine = line
}
}
return ^Escape
}
// flushLine is add_line(NULL): force out the accumulated page.
func (g *RogueGame) flushLine() int { return g.addLine(flushSentinel) }
// endLine ends the list of lines (things.c end_line).
func (g *RogueGame) endLine() {
pg := &g.invPage
if g.Options.InvType != InvSlow {
if pg.lineCnt == 1 && !pg.newpage {
g.Msgs.Mpos = 0
g.msg("%s", pg.lastLine)
} else {
g.flushLine()
}
}
pg.lineCnt = 0
pg.newpage = false
}
// nothing builds the "nothing found" message for a type (things.c nothing).
func (g *RogueGame) nothing(typ byte) string {
var out string
if g.Options.Terse {
out = "Nothing"
} else {
out = "Haven't discovered anything"
}
if typ != '*' {
var tystr string
switch typ {
case Potion:
tystr = potionName
case Scroll:
tystr = scrollName
case Ring:
tystr = ringName
case Stick:
tystr = "stick"
}
out += fmt.Sprintf(" about any %ss", tystr)
}
return out
}
// nameit gives the proper name to a potion, stick, or ring
// (things.c nameit).
func (g *RogueGame) nameit(pb *strings.Builder, obj *Object, typ, which string,
op *ObjInfo, prfunc func(*RogueGame, *Object) string) {
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)
}
case obj.Count == 1:
fmt.Fprintf(pb, "A%s %s %s", vowelstr(which), which, typ)
default:
fmt.Fprintf(pb, "%d %s %ss", obj.Count, which, typ)
}
}
// nullstr returns an empty string (things.c nullstr).
func nullstr(*RogueGame, *Object) string { return "" }
// prList lists possible potions, scrolls, etc. for the wizard (things.c
// pr_list).
func (g *RogueGame) prList() {
if !g.Options.Terse {
g.addmsgf("for ")
}
g.addmsgf("what type")
if !g.Options.Terse {
g.addmsgf(" of object do you want a list")
}
g.msg("? ")
ch := g.readchar()
switch ch {
case Potion:
g.prSpec(g.Items.Potions[:])
case Scroll:
g.prSpec(g.Items.Scrolls[:])
case Ring:
g.prSpec(g.Items.Rings[:])
case Stick:
g.prSpec(g.Items.Sticks[:])
case Armor:
g.prSpec(g.Items.Armors[:])
case Weapon:
g.prSpec(g.Items.Weapons[:NumWeaponTypes])
}
}
// prSpec prints a specific list of possible items to choose from
// (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()
}