createObj stored the raw 0-f nibble as Object.Which with no bounds check, so wizard mode -> C -> / -> f made a wand numbered 15 against a 14-entry table and panicked in fixStick. Input outside 0-f overshoots much further rather than going negative: readchar returns a byte, so the int(ch-'a') + 10 branch is byte arithmetic and wraps, giving 234 for 'A' and 202 for '!', and panicked the same way. C's create_obj() was equally unchecked, but its consumers were either switches (defined for any value) or static-array reads past the end (undefined, and survivable in practice). Since one game is now one process, the Go panic kills the game outright and leaves the terminal in raw mode. Reject at the two boundaries a bad Which can enter through. createObj now refuses an out-of-range choice with a message drawn from C's own type_name() vocabulary and adds nothing to the pack, a deliberate divergence recorded in a comment because C had no defined behavior here to be faithful to. Restore refuses a snapshot describing such an object (ErrSaveCorrupt) rather than loading a game that would explode later. A decoded snapshot is also the only source of a genuinely negative Which, Which being a plain int off the wire, so it is what the Which >= 0 arm of hasValidWhich defends against. Behind those, whichLimit/hasValidWhich back defensive guards at every dispatch the issue names: the quaffHandler/readHandler/zapHandler accessors return no handler instead of indexing (for wands that is exactly what non-MASTER C did, matching no case and still running o_charges--), the callIt lore lookups, identifyType, armorClass for the a_class[] reads, initWeapon against the missing init_dam[] row for WeaponFlame, fixStick's ws_type[] read, and inventoryName and objectWorth, hoisted so one check each covers the whole family of per-kind name and appraisal tables. identifyType's bound is defensive rather than live: readHandlers registers readIdentify only for the identify scrolls, all of which sit inside the shorter idType table. No in-range input changes behavior and no guard consumes a random number: the rejection precedes every rnd() call. TestSeedCompatItemTables stays green untouched. New game/wizard_test.go covers the exact reproducer, a rejection sweep over every indexed kind including the wrapped values from input outside 0-f, an acceptance sweep proving valid choices still build the right item, one no-panic test per guarded family, the fixStick crash site, the corrupt-save rejection over both the wrapped values and a negative Which, and a check that whichLimit still agrees with the table sizes. Each guard was confirmed load-bearing by reverting it and watching the test fail. TODO.md records the step; Next Step is deliberately left alone, since this arrived out of band via an issue.
747 lines
16 KiB
Go
747 lines
16 KiB
Go
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
|
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) inventoryName(obj *Object, drop bool) string {
|
|
var pb strings.Builder
|
|
|
|
// Every arm below reaches into a per-kind name table at obj.Which:
|
|
// the potion colors, ring stones, wand material/type, scroll titles,
|
|
// and the weapon and armor name tables. An object with an out-of-range
|
|
// Which cannot reach here (createObj and Restore both reject one), but
|
|
// if one ever did, naming it must not take the process down — so it
|
|
// falls back to the bare category name from C's type_name() vocabulary.
|
|
if !obj.hasValidWhich() {
|
|
return obj.Kind.String()
|
|
}
|
|
|
|
which := obj.Which
|
|
it := &g.Items
|
|
|
|
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
|
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:
|
|
g.nameScroll(&pb, obj)
|
|
case KindFood:
|
|
g.nameFood(&pb, obj)
|
|
case KindWeapon:
|
|
g.nameWeapon(&pb, obj)
|
|
case KindArmor:
|
|
g.nameArmor(&pb, obj)
|
|
case KindAmulet:
|
|
pb.WriteString("The Amulet of Yendor")
|
|
case KindGold:
|
|
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue)
|
|
}
|
|
|
|
return fixNameCase(g.describeWorn(obj, pb.String()), drop)
|
|
}
|
|
|
|
// nameScroll writes a scroll's inventory name (things.c inv_name).
|
|
func (g *RogueGame) nameScroll(pb *strings.Builder, obj *Object) {
|
|
if obj.Count == 1 {
|
|
pb.WriteString("A scroll ")
|
|
} else {
|
|
fmt.Fprintf(pb, "%d scrolls ", obj.Count)
|
|
}
|
|
|
|
op := &g.Items.Scrolls[obj.Which]
|
|
|
|
switch {
|
|
case op.Know:
|
|
fmt.Fprintf(pb, "of %s", op.Name)
|
|
case op.Guess != "":
|
|
fmt.Fprintf(pb, "called %s", op.Guess)
|
|
default:
|
|
fmt.Fprintf(pb, "titled '%s'", g.Items.ScrNames[obj.Which])
|
|
}
|
|
}
|
|
|
|
// nameFood writes a food item's inventory name; which 1 is the fruit
|
|
// (things.c inv_name).
|
|
func (g *RogueGame) nameFood(pb *strings.Builder, obj *Object) {
|
|
if obj.Which == 1 {
|
|
if obj.Count == 1 {
|
|
fmt.Fprintf(pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
|
|
} else {
|
|
fmt.Fprintf(pb, "%d %ss", obj.Count, g.Fruit)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if obj.Count == 1 {
|
|
pb.WriteString("Some food")
|
|
} else {
|
|
fmt.Fprintf(pb, "%d rations of food", obj.Count)
|
|
}
|
|
}
|
|
|
|
// nameWeapon writes a weapon's inventory name (things.c inv_name).
|
|
func (g *RogueGame) nameWeapon(pb *strings.Builder, obj *Object) {
|
|
sp := g.Items.Weapons[obj.Which].Name
|
|
|
|
if obj.Count > 1 {
|
|
fmt.Fprintf(pb, "%d ", obj.Count)
|
|
} else {
|
|
fmt.Fprintf(pb, "A%s ", vowelstr(sp))
|
|
}
|
|
|
|
if obj.Flags.Has(Known) {
|
|
fmt.Fprintf(pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
|
|
} else {
|
|
pb.WriteString(sp)
|
|
}
|
|
|
|
if obj.Count > 1 {
|
|
pb.WriteString("s")
|
|
}
|
|
|
|
if obj.Label != "" {
|
|
fmt.Fprintf(pb, " called %s", obj.Label)
|
|
}
|
|
}
|
|
|
|
// nameArmor writes an armor's inventory name (things.c inv_name).
|
|
func (g *RogueGame) nameArmor(pb *strings.Builder, obj *Object) {
|
|
sp := g.Items.Armors[obj.Which].Name
|
|
if obj.Flags.Has(Known) {
|
|
fmt.Fprintf(pb, "%s %s [",
|
|
num(g.data.armorClass(obj.Which)-obj.ArmorClass, 0, Armor), sp)
|
|
|
|
if !g.Options.Terse {
|
|
pb.WriteString("protection ")
|
|
}
|
|
|
|
fmt.Fprintf(pb, "%d]", 10-obj.ArmorClass)
|
|
} else {
|
|
pb.WriteString(sp)
|
|
}
|
|
|
|
if obj.Label != "" {
|
|
fmt.Fprintf(pb, " called %s", obj.Label)
|
|
}
|
|
}
|
|
|
|
// describeWorn appends the equipped-status notes to an inventory name
|
|
// (things.c inv_name).
|
|
func (g *RogueGame) describeWorn(obj *Object, out string) string {
|
|
if !g.InvDescribe {
|
|
return out
|
|
}
|
|
|
|
p := &g.Player
|
|
if obj == p.CurArmor {
|
|
out += " (being worn)"
|
|
}
|
|
|
|
if obj == p.CurWeapon {
|
|
out += " (weapon in hand)"
|
|
}
|
|
|
|
switch obj {
|
|
case p.CurRing[Left]:
|
|
out += " (on left hand)"
|
|
case p.CurRing[Right]:
|
|
out += " (on right hand)"
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// fixNameCase upper- or lowercases the leading letter to suit the
|
|
// sentence it will land in (things.c inv_name).
|
|
func fixNameCase(out string, drop bool) string {
|
|
if out == "" {
|
|
return out
|
|
}
|
|
|
|
if drop && isUpper(out[0]) {
|
|
return string(toLower(out[0])) + out[1:]
|
|
}
|
|
|
|
if !drop && isLower(out[0]) {
|
|
return string(toUpper(out[0])) + out[1:]
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// 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, ok := g.promptPackItem("drop", KindNone)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if !g.dropCheck(obj) {
|
|
return
|
|
}
|
|
|
|
obj = g.leavePack(obj, true, !obj.Kind.MergesInPack())
|
|
// Link it into the level object list
|
|
g.Level.AddObject(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.inventoryName(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:
|
|
g.dropRing(obj)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// dropRing takes a worn ring off with its side effects (things.c
|
|
// dropcheck).
|
|
func (g *RogueGame) dropRing(obj *Object) {
|
|
p := &g.Player
|
|
|
|
hand := Right
|
|
if obj == p.CurRing[Left] {
|
|
hand = Left
|
|
}
|
|
|
|
p.CurRing[hand] = nil
|
|
|
|
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
|
switch obj.RingKind() {
|
|
case RingAddStrength:
|
|
g.changeStrength(-obj.Bonus)
|
|
case RingSeeInvisible:
|
|
g.unsee(0)
|
|
g.Extinguish(DUnsee)
|
|
}
|
|
}
|
|
|
|
// newThing returns a new random thing for the dungeon (things.c new_thing).
|
|
func (g *RogueGame) newThing() *Object {
|
|
cur := newObject()
|
|
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:
|
|
g.newFoodThing(cur)
|
|
case 3:
|
|
g.newWeaponThing(cur)
|
|
case 4:
|
|
g.newArmorThing(cur)
|
|
case 5:
|
|
g.newRingThing(cur)
|
|
case 6:
|
|
cur.Kind = KindWand
|
|
cur.Which = pickOne(g, g.Items.Sticks[:])
|
|
g.fixStick(cur)
|
|
}
|
|
|
|
return cur
|
|
}
|
|
|
|
// newFoodThing rolls food, one time in ten the fruit (things.c
|
|
// new_thing).
|
|
func (g *RogueGame) newFoodThing(cur *Object) {
|
|
cur.Kind = KindFood
|
|
|
|
g.Player.NoFood = 0
|
|
if g.rnd(10) != 0 {
|
|
cur.Which = 0
|
|
} else {
|
|
cur.Which = 1
|
|
}
|
|
}
|
|
|
|
// newWeaponThing rolls a weapon, sometimes cursed or blessed (things.c
|
|
// new_thing).
|
|
func (g *RogueGame) newWeaponThing(cur *Object) {
|
|
g.initWeapon(cur, WeaponKind(pickOne(g, g.Items.Weapons[:NumWeaponTypes])))
|
|
|
|
if r := g.rnd(100); r < 10 {
|
|
cur.Flags.Set(Cursed)
|
|
cur.HPlus -= g.rnd(3) + 1
|
|
} else if r < 15 {
|
|
cur.HPlus += g.rnd(3) + 1
|
|
}
|
|
}
|
|
|
|
// newArmorThing rolls armor, sometimes cursed or blessed (things.c
|
|
// new_thing).
|
|
func (g *RogueGame) newArmorThing(cur *Object) {
|
|
cur.Kind = KindArmor
|
|
cur.Which = pickOne(g, g.Items.Armors[:])
|
|
|
|
cur.ArmorClass = g.data.aClass[cur.Which]
|
|
if r := g.rnd(100); r < 20 {
|
|
cur.Flags.Set(Cursed)
|
|
cur.ArmorClass += g.rnd(3) + 1
|
|
} else if r < 28 {
|
|
cur.ArmorClass -= g.rnd(3) + 1
|
|
}
|
|
}
|
|
|
|
// newRingThing rolls a ring, cursing the bad ones (things.c new_thing).
|
|
func (g *RogueGame) newRingThing(cur *Object) {
|
|
cur.Kind = KindRing
|
|
|
|
cur.Which = pickOne(g, g.Items.Rings[:])
|
|
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
|
switch cur.RingKind() {
|
|
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
|
|
if cur.Bonus = g.rnd(3); cur.Bonus == 0 {
|
|
cur.Bonus = -1
|
|
cur.Flags.Set(Cursed)
|
|
}
|
|
case RingAggravateMonsters, RingTeleportation:
|
|
cur.Flags.Set(Cursed)
|
|
}
|
|
}
|
|
|
|
// pickOne picks an item out of a list of possible objects using their
|
|
// cumulative probabilities (things.c pick_one).
|
|
func pickOne(g *RogueGame, info []ObjInfo) int {
|
|
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.inventoryName(&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
|
|
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 {
|
|
return g.addLineSlow(line, isFlush)
|
|
}
|
|
|
|
g.addLinePaged(line, isFlush)
|
|
|
|
return ^Escape
|
|
}
|
|
|
|
// addLineSlow shows one discovery line as a message (the slow-inventory
|
|
// arm of things.c add_line).
|
|
func (g *RogueGame) addLineSlow(line string, isFlush bool) int {
|
|
if !isFlush && line != "" {
|
|
if g.msg("%s", line) == Escape {
|
|
return Escape
|
|
}
|
|
}
|
|
|
|
g.invPage.lineCnt++
|
|
|
|
return ^Escape
|
|
}
|
|
|
|
// addLinePaged accumulates discovery lines into the paged window,
|
|
// prompting between full pages (the windowed arm of things.c add_line).
|
|
func (g *RogueGame) addLinePaged(line string, isFlush bool) {
|
|
pg := &g.invPage
|
|
prompt := "--Press space to continue--"
|
|
|
|
if !pg.init {
|
|
pg.maxlen = len(prompt)
|
|
pg.init = true
|
|
}
|
|
|
|
if pg.lineCnt >= NumLines-1 || isFlush {
|
|
g.addLinePageBreak(prompt, isFlush)
|
|
}
|
|
|
|
if !isFlush && (pg.lineCnt != 0 || line != "") {
|
|
g.scr.Hw.MvAddStr(pg.lineCnt, 0, line)
|
|
|
|
pg.lineCnt++
|
|
if pg.maxlen < len(line) {
|
|
pg.maxlen = len(line)
|
|
}
|
|
|
|
pg.lastLine = line
|
|
}
|
|
}
|
|
|
|
// addLinePageBreak prompts at a full page and starts a fresh one
|
|
// (things.c add_line).
|
|
func (g *RogueGame) addLinePageBreak(prompt string, isFlush bool) {
|
|
pg := &g.invPage
|
|
if g.Options.InvType == InvOver && isFlush && !pg.newpage {
|
|
g.addLineOverlay(prompt)
|
|
} else {
|
|
g.scr.Hw.MvAddStr(NumLines-1, 0, prompt)
|
|
g.scr.RefreshWin(g.scr.Hw)
|
|
g.waitFor(' ')
|
|
g.scr.Hw.Clear()
|
|
g.refresh()
|
|
}
|
|
|
|
pg.newpage = true
|
|
pg.lineCnt = 0
|
|
pg.maxlen = len(prompt)
|
|
}
|
|
|
|
// addLineOverlay draws the accumulated list in a box at the top right
|
|
// of the screen, prompts, and restores what was beneath (things.c
|
|
// add_line).
|
|
func (g *RogueGame) addLineOverlay(prompt string) {
|
|
pg := &g.invPage
|
|
|
|
g.msg("")
|
|
g.refresh()
|
|
|
|
saved := NewWindow(NumLines, NumCols)
|
|
saved.CopyFrom(g.scr.Std)
|
|
|
|
lx := NumCols - pg.maxlen - 2
|
|
for y := 0; y <= pg.lineCnt; y++ {
|
|
for x := 0; x <= pg.maxlen; x++ {
|
|
g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x))
|
|
}
|
|
}
|
|
|
|
g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt)
|
|
g.refresh()
|
|
g.waitFor(' ')
|
|
g.scr.Std.CopyFrom(saved)
|
|
g.refresh()
|
|
}
|
|
|
|
// flushLine is add_line(NULL): force out the accumulated page.
|
|
func (g *RogueGame) flushLine() int { return g.addLine(flushSentinel) }
|
|
|
|
// 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()
|
|
}
|