diff --git a/game/things.go b/game/things.go index d7a9d78..147cebe 100644 --- a/game/things.go +++ b/game/things.go @@ -23,107 +23,147 @@ func (g *RogueGame) inventoryName(obj *Object, drop bool) string { 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]) - } + g.nameScroll(&pb, obj) 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) - } - } + g.nameFood(&pb, obj) 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) - } + g.nameWeapon(&pb, obj) 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) - } + g.nameArmor(&pb, obj) case KindAmulet: pb.WriteString("The Amulet of Yendor") case KindGold: fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue) } - out := pb.String() + return fixNameCase(g.describeWorn(obj, pb.String()), drop) +} - 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)" - } +// 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) } - 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:] + 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.aClass[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 @@ -192,25 +232,33 @@ func (g *RogueGame) dropCheck(obj *Object) bool { 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) - } + 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 + + 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() @@ -236,47 +284,13 @@ func (g *RogueGame) newThing() *Object { 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 - } + g.newFoodThing(cur) 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 - } + g.newWeaponThing(cur) 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 - } + g.newArmorThing(cur) 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) - } + g.newRingThing(cur) case 6: cur.Kind = KindWand cur.Which = pickOne(g, g.Items.Sticks[:]) @@ -286,6 +300,63 @@ func (g *RogueGame) newThing() *Object { 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[:]) + 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 { @@ -423,7 +494,6 @@ 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 @@ -440,69 +510,93 @@ func (g *RogueGame) addLine(format string, a ...any) int { } if g.Options.InvType == InvSlow { - if !isFlush && line != "" { - if g.msg("%s", line) == Escape { - return Escape - } - } + return g.addLineSlow(line, isFlush) + } - pg.lineCnt++ - } else { - if !pg.init { - pg.maxlen = len(prompt) - pg.init = true - } + g.addLinePaged(line, isFlush) - 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() + return ^Escape +} - 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 +// 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 { + 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) + } + + 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 + } +} + +// 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) }