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 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: 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.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 } // 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 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[:]) 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() }