The port is complete, so the C sources and their build system (autoconf/automake inputs, Makefiles, Visual Studio project, doc templates, RPM spec, C build/modernization notes) leave this branch; they live on in master/modern-rogue for reference. Also ports the last missing wizard command (things.c pr_list/pr_spec, the '*' item-probability listing), rewrites README.md for the Go port, and updates ARCHITECTURE.md's layout notes (game/ has no third-party imports; tcell lives in term/). Kept: LICENSE.TXT, rogue.png, rogue.desktop, ARCHITECTURE.md.
547 lines
12 KiB
Go
547 lines
12 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.Type {
|
|
case Potion:
|
|
g.nameit(&pb, obj, "potion", it.PotColors[which], &it.Potions[which], nullstr)
|
|
case Ring:
|
|
g.nameit(&pb, obj, "ring", it.RingStones[which], &it.Rings[which], ringNum)
|
|
case Stick:
|
|
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr)
|
|
case Scroll:
|
|
if obj.Count == 1 {
|
|
pb.WriteString("A scroll ")
|
|
} else {
|
|
fmt.Fprintf(&pb, "%d scrolls ", obj.Count)
|
|
}
|
|
op := &it.Scrolls[which]
|
|
if op.Know {
|
|
fmt.Fprintf(&pb, "of %s", op.Name)
|
|
} else if op.Guess != "" {
|
|
fmt.Fprintf(&pb, "called %s", op.Guess)
|
|
} else {
|
|
fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which])
|
|
}
|
|
case Food:
|
|
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 Weapon:
|
|
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(IsKnow) {
|
|
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 Armor:
|
|
sp := it.Armors[which].Name
|
|
if obj.Flags.Has(IsKnow) {
|
|
fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.Arm, 0, Armor), sp)
|
|
if !g.Options.Terse {
|
|
pb.WriteString("protection ")
|
|
}
|
|
fmt.Fprintf(&pb, "%d]", 10-obj.Arm)
|
|
} else {
|
|
pb.WriteString(sp)
|
|
}
|
|
if obj.Label != "" {
|
|
fmt.Fprintf(&pb, " called %s", obj.Label)
|
|
}
|
|
case Amulet:
|
|
pb.WriteString("The Amulet of Yendor")
|
|
case Gold:
|
|
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldVal())
|
|
}
|
|
|
|
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] {
|
|
out += " (on left hand)"
|
|
} else if obj == 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", 0)
|
|
if obj == nil {
|
|
return
|
|
}
|
|
if !g.dropCheck(obj) {
|
|
return
|
|
}
|
|
obj = g.leavePack(obj, true, !IsMult(obj.Type))
|
|
// Link it into the level object list
|
|
attachObj(&g.Level.Objects, obj)
|
|
g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Type)
|
|
g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped)
|
|
obj.Pos = p.Pos
|
|
if obj.Type == Amulet {
|
|
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(IsCursed) {
|
|
g.msg("you can't. It appears to be cursed")
|
|
return false
|
|
}
|
|
if obj == p.CurWeapon {
|
|
p.CurWeapon = nil
|
|
} else if obj == p.CurArmor {
|
|
g.wasteTime()
|
|
p.CurArmor = nil
|
|
} else {
|
|
hand := Right
|
|
if obj == p.CurRing[Left] {
|
|
hand = Left
|
|
}
|
|
p.CurRing[hand] = nil
|
|
switch obj.Which {
|
|
case RAddStr:
|
|
g.chgStr(-obj.Arm)
|
|
case RSeeInvis:
|
|
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 = "0x0"
|
|
cur.HurlDmg = "0x0"
|
|
cur.Arm = 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.Type = Potion
|
|
cur.Which = pickOne(g, g.Items.Potions[:])
|
|
case 1:
|
|
cur.Type = Scroll
|
|
cur.Which = pickOne(g, g.Items.Scrolls[:])
|
|
case 2:
|
|
cur.Type = Food
|
|
g.Player.NoFood = 0
|
|
if g.rnd(10) != 0 {
|
|
cur.Which = 0
|
|
} else {
|
|
cur.Which = 1
|
|
}
|
|
case 3:
|
|
g.initWeapon(cur, pickOne(g, g.Items.Weapons[:MaxWeapons]))
|
|
if r := g.rnd(100); r < 10 {
|
|
cur.Flags.Set(IsCursed)
|
|
cur.HPlus -= g.rnd(3) + 1
|
|
} else if r < 15 {
|
|
cur.HPlus += g.rnd(3) + 1
|
|
}
|
|
case 4:
|
|
cur.Type = Armor
|
|
cur.Which = pickOne(g, g.Items.Armors[:])
|
|
cur.Arm = aClass[cur.Which]
|
|
if r := g.rnd(100); r < 20 {
|
|
cur.Flags.Set(IsCursed)
|
|
cur.Arm += g.rnd(3) + 1
|
|
} else if r < 28 {
|
|
cur.Arm -= g.rnd(3) + 1
|
|
}
|
|
case 5:
|
|
cur.Type = Ring
|
|
cur.Which = pickOne(g, g.Items.Rings[:])
|
|
switch cur.Which {
|
|
case RAddStr, RProtect, RAddHit, RAddDam:
|
|
if cur.Arm = g.rnd(3); cur.Arm == 0 {
|
|
cur.Arm = -1
|
|
cur.Flags.Set(IsCursed)
|
|
}
|
|
case RAggr, RTeleport:
|
|
cur.Flags.Set(IsCursed)
|
|
}
|
|
case 6:
|
|
cur.Type = Stick
|
|
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.addmsg("for ")
|
|
}
|
|
g.addmsg("what type")
|
|
if !g.Options.Terse {
|
|
g.addmsg(" 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.Type = 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 = "potion"
|
|
case Scroll:
|
|
tystr = "scroll"
|
|
case Ring:
|
|
tystr = "ring"
|
|
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) {
|
|
if 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 {
|
|
fmt.Fprintf(pb, "A%s %s %s", vowelstr(which), which, typ)
|
|
} else {
|
|
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.addmsg("for ")
|
|
}
|
|
g.addmsg("what type")
|
|
if !g.Options.Terse {
|
|
g.addmsg(" 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[:MaxWeapons])
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|