package game // ObjectKind is the category of an item. In C this was the o_type byte, // which doubled as the character drawn on the map; the port separates the // category (ObjectKind) from its display character (Glyph). type ObjectKind int // Item categories. const ( KindNone ObjectKind = iota KindPotion KindScroll KindFood KindWeapon KindArmor KindRing KindWand // a "stick": wand or staff KindAmulet KindGold ) // Prompt-filter pseudo-kinds for item selection (rogue.h CALLABLE and // R_OR_S): they never appear on an Object, only as getItem filters. const ( KindCallable ObjectKind = -1 KindRingOrStick ObjectKind = -2 ) // Category words shared by ObjectKind.String, the discovery list, and the // ident table. (The bare identifiers Potion, Scroll, Ring, Gold are the // glyph byte constants.) const ( potionName = "potion" scrollName = "scroll" ringName = "ring" goldName = "gold" ) // Glyph returns the map/display character for this kind of object. func (k ObjectKind) Glyph() byte { //nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07) switch k { case KindPotion: return Potion case KindScroll: return Scroll case KindFood: return Food case KindWeapon: return Weapon case KindArmor: return Armor case KindRing: return Ring case KindWand: return Stick case KindAmulet: return Amulet case KindGold: return Gold } return ' ' } // String names the category the way the C type_name() did. func (k ObjectKind) String() string { //nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07) switch k { case KindPotion: return potionName case KindScroll: return scrollName case KindFood: return "food" case KindWeapon: return "weapon" case KindArmor: return "suit of armor" case KindRing: return ringName default: return k.stringRest() } } // objectKindForGlyph is the reverse of Glyph: what category of item does a // map character denote. Returns KindNone for non-item characters. func objectKindForGlyph(ch byte) ObjectKind { switch ch { case Potion: return KindPotion case Scroll: return KindScroll case Food: return KindFood case Weapon: return KindWeapon case Armor: return KindArmor case Ring: return KindRing case Stick: return KindWand case Amulet: return KindAmulet case Gold: return KindGold } return KindNone } // MergesInPack reports whether picking up another of this kind merges into // an existing pack entry's count (rogue.h ISMULT). func (k ObjectKind) MergesInPack() bool { return k == KindPotion || k == KindScroll || k == KindFood } // stringRest names the remaining kinds, including the ring-or-stick // prompt pseudo-kind (the tail of the C type_name switch). func (k ObjectKind) stringRest() string { //nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07) switch k { case KindWand: return "wand or staff" case KindAmulet: return "amulet" case KindGold: return goldName case KindRingOrStick: return "ring, wand or staff" } return "bizarre thing" } // Object is the _o arm of the C THING union: anything that can lie on the // floor or ride in a pack. type Object struct { Kind ObjectKind // what kind of object it is (o_type) Pos Coord // where it lives on the screen Text string // what it says if you read it Launch WeaponKind // what you need to launch it (noWeapon if none) PackCh byte // what character it is in the pack Damage DiceSpec // damage if used like sword HurlDmg DiceSpec // damage if thrown Count int // count for plural objects Which int // which object of a type it is (index for the Kind's table) HPlus int // plusses to hit DPlus int // plusses to damage ArmorClass int // armor protection (armor only) Charges int // charges remaining (wands and staffs only) GoldValue int // worth (gold piles only) Bonus int // magic bonus (rings only: protection, add strength, ...) Flags ObjFlags Group int // group number for this object Label string // label for object } // newObject is list.c new_item() for objects: a zeroed Object with the // same non-zero defaults the C code relies on. func newObject() *Object { return &Object{Launch: noWeapon} } // whichLimit reports how many entries a kind's per-kind tables hold, so // Which is a legal index exactly while 0 <= Which < whichLimit(Kind). // Kinds whose Which is not a table index at all — food (0 ration, 1 the // fruit), the amulet, gold, and the KindNone an unrecognized glyph maps // to — report 0, meaning any value is acceptable, which is what C did // with them too. // // Weapons count one past NumWeaponTypes on purpose: Items.Weapons is // sized NumWeaponTypes+1 for the WeaponFlame dragon-breath entry, and // fireBolt really does set Which = WeaponFlame on a live Object. That // entry has no init_dam[] row, so initWeapon and createObj bound // themselves by NumWeaponTypes instead. func whichLimit(kind ObjectKind) int { //nolint:exhaustive // the remaining kinds do not index a per-kind table switch kind { case KindPotion: return int(NumPotionTypes) case KindScroll: return int(NumScrollTypes) case KindRing: return int(NumRingTypes) case KindWand: return int(NumWandTypes) case KindArmor: return int(NumArmorTypes) case KindWeapon: return int(NumWeaponTypes) + 1 } return 0 } // PotionKind returns Which as a potion kind; valid only for KindPotion. func (o *Object) PotionKind() PotionKind { return PotionKind(o.Which) } // ScrollKind returns Which as a scroll kind; valid only for KindScroll. func (o *Object) ScrollKind() ScrollKind { return ScrollKind(o.Which) } // RingKind returns Which as a ring kind; valid only for KindRing. func (o *Object) RingKind() RingKind { return RingKind(o.Which) } // WandKind returns Which as a wand kind; valid only for KindWand. func (o *Object) WandKind() WandKind { return WandKind(o.Which) } // WeaponKind returns Which as a weapon kind; valid only for KindWeapon. func (o *Object) WeaponKind() WeaponKind { return WeaponKind(o.Which) } // ArmorKind returns Which as an armor kind; valid only for KindArmor. func (o *Object) ArmorKind() ArmorKind { return ArmorKind(o.Which) } // hasValidWhich reports whether Which is a legal index into this // object's per-kind tables. Objects the game builds itself always // satisfy it; the wizard-create command and a restored save file are the // only two ways a malformed one can appear, and both reject it (see // createObj and validateSnapshotObjects). // // The Which >= 0 arm is unreachable from the keyboard: createObj derives // Which with byte arithmetic (int(ch-'a') + 10), which wraps to a large // positive value rather than going negative. It is kept as // defense-in-depth for the non-keyboard source — a decoded save file, // where Which is an int off the wire and can hold anything — and is // exercised there by TestRestoreRejectsOutOfRangeWhich. func (o *Object) hasValidWhich() bool { limit := whichLimit(o.Kind) return limit == 0 || (o.Which >= 0 && o.Which < limit) } // wizardCanCreate reports whether Which names something the wizard-create // command can actually build. It is hasValidWhich narrowed for weapons: // WeaponFlame owns a name-table slot but no init_dam[] row, so asking for // it would leave initWeapon nothing to copy. func (o *Object) wizardCanCreate() bool { if o.Kind == KindWeapon { return o.Which >= 0 && o.Which < int(NumWeaponTypes) } return o.hasValidWhich() } // attachObj is list.c attach(): push item onto the front of a list. func attachObj(list *[]*Object, item *Object) { *list = append([]*Object{item}, *list...) } // detachObj is list.c detach(): remove item (by identity) from a list. func detachObj(list *[]*Object, item *Object) { for i, o := range *list { if o == item { *list = append((*list)[:i], (*list)[i+1:]...) return } } }