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() }