From 2eff377a7311bf5dfc5aaaf7338f13b80388e40b Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 6 Jul 2026 23:07:57 +0200 Subject: [PATCH] Un-overload Object fields and pre-parse damage dice (refactor step 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Object.Arm carried four meanings in C (o_arm/o_charges/o_goldval plus ring bonuses); it is now four fields: ArmorClass, Charges, GoldValue, and Bonus. Stats.Arm becomes Stats.ArmorClass. The Charges()/GoldVal() accessor pair is gone. Damage dice strings ("1x4/1x2") are parsed once into DiceSpec — at table definition for the bestiary and weapon tables, at creation for items — instead of re-parsed with atoi on every swing as fight.c roll_em did. ParseDice preserves the C parse semantics exactly, including the junk-tolerant "%%%x0" bestiary placeholder and the "000x0" flytrap reset (regression-tested in dice_test.go); the flytrap's growing grip becomes DiceSpec{{VfHit, 1}}. Save format bumps to 5.4.4-go3 (field renames and retypes would silently zero under gob's match-by-name decoding). No behavior change; full suite green. --- TODO.md | 12 +++++---- game/command.go | 4 +-- game/dice.go | 58 ++++++++++++++++++++++++++++++++++++++++++++ game/dice_test.go | 45 ++++++++++++++++++++++++++++++++++ game/effects_test.go | 10 ++++---- game/fight.go | 56 +++++++++++++++--------------------------- game/fight_test.go | 4 +-- game/init.go | 2 +- game/io.go | 4 +-- game/misc.go | 4 +-- game/monsters.go | 6 ++--- game/move.go | 8 +++--- game/newlevel.go | 6 ++--- game/object.go | 45 ++++++++++++++-------------------- game/pack.go | 2 +- game/potions.go | 10 ++++---- game/rings.go | 4 +-- game/rip.go | 10 ++++---- game/rooms.go | 2 +- game/save.go | 6 +++-- game/save_test.go | 4 +-- game/scrolls.go | 2 +- game/sticks.go | 22 ++++++++--------- game/tables.go | 54 ++++++++++++++++++++--------------------- game/things.go | 24 +++++++++--------- game/types.go | 14 +++++------ game/weapons.go | 22 ++++++++--------- game/wizard.go | 14 +++++------ 28 files changed, 268 insertions(+), 186 deletions(-) create mode 100644 game/dice.go create mode 100644 game/dice_test.go diff --git a/TODO.md b/TODO.md index 2edeccd..bf6f9d5 100644 --- a/TODO.md +++ b/TODO.md @@ -29,14 +29,16 @@ Refactor ground rules: # Next Step -Refactor step 3 (branch refactor/object-fields): un-overload Object -fields — split Object.Arm into ArmorClass/Charges/GoldValue; parse -damage dice ("1x4/1x2") once into a DiceSpec type at table definition -instead of per swing; save-format update with round-trip test -migration. +Update the module base path to git.eeqj.de/sneak/rgoue (user request +2026-07-06): go.mod, all imports in term/ and cmd/, and doc references. # Completed Steps +- 2026-07-06 Refactor step 3 (refactor/object-fields): Object.Arm split + into ArmorClass/Charges/GoldValue/Bonus (rings); Stats.Arm → + ArmorClass; damage strings parsed once into DiceSpec at table + definition (ParseDice keeps C roll_em parse semantics, incl. "%%%x0" + and "000x0" edge cases, regression-tested); save format 5.4.4-go3. - 2026-07-06 Refactor step 2 (refactor/typed-kinds, b940cfc): ObjectKind separates item category from map glyph (Object.Type byte → Kind ObjectKind with Glyph()); PotionKind/ScrollKind/RingKind/ diff --git a/game/command.go b/game/command.go index 061418a..d3a4b3a 100644 --- a/game/command.go +++ b/game/command.go @@ -410,7 +410,7 @@ func (g *RogueGame) wizardCommand(ch byte) { g.turnSee(p.On(SenseMonsters)) case CTRL('~'): if item := g.getItem("charge", KindWand); item != nil { - item.SetCharges(10000) + item.Charges = 10000 } case CTRL('I'): for range 9 { @@ -427,7 +427,7 @@ func (g *RogueGame) wizardCommand(ch byte) { obj = newObject() obj.Kind = KindArmor obj.Which = int(ArmorPlateMail) - obj.Arm = -5 + obj.ArmorClass = -5 obj.Flags.Set(Known) obj.Count = 1 p.CurArmor = obj diff --git a/game/dice.go b/game/dice.go new file mode 100644 index 0000000..5548360 --- /dev/null +++ b/game/dice.go @@ -0,0 +1,58 @@ +package game + +import ( + "fmt" + "strings" +) + +// Rogue expresses damage as strings like "1x4/3x6": one attack rolling +// 1d4 and a second rolling 3d6. The C code re-parsed these with atoi on +// every swing (fight.c roll_em); the port parses them once, at table +// definition or item creation, into a DiceSpec. + +// DiceRoll is one attack's dice: Count rolls of a Sides-sided die. +type DiceRoll struct { + Count int + Sides int +} + +// DiceSpec is the sequence of attacks a damage string described. +type DiceSpec []DiceRoll + +// ParseDice parses a C damage string with the exact semantics of the +// roll_em loop: leading digits (C atoi) before and after each 'x', attacks +// separated by '/'. Junk like the bestiary's "%%%x0" placeholder parses as +// a single 0x0 attack, as it did in C. +func ParseDice(s string) DiceSpec { + var spec DiceSpec + for s != "" { + count := cAtoi(s) + xi := strings.IndexByte(s, 'x') + if xi < 0 { + break + } + s = s[xi+1:] + spec = append(spec, DiceRoll{Count: count, Sides: cAtoi(s)}) + si := strings.IndexByte(s, '/') + if si < 0 { + break + } + s = s[si+1:] + } + return spec +} + +// dice is the table-definition shorthand for ParseDice. +func dice(s string) DiceSpec { return ParseDice(s) } + +// String renders the spec back in the classic "NxM/NxM" form. +func (d DiceSpec) String() string { + var sb strings.Builder + for i, r := range d { + if i > 0 { + sb.WriteByte('/') + } + fmt.Fprintf(&sb, "%dx%d", r.Count, r.Sides) + } + return sb.String() +} diff --git a/game/dice_test.go b/game/dice_test.go new file mode 100644 index 0000000..df08051 --- /dev/null +++ b/game/dice_test.go @@ -0,0 +1,45 @@ +package game + +import "testing" + +// ParseDice must keep the exact semantics of the C roll_em parse loop, +// including its junk-tolerant edges: the bestiary placeholder "%%%x0" and +// the flytrap reset "000x0" both mean a single 0x0 attack. +func TestParseDice(t *testing.T) { + cases := []struct { + in string + want string + len int + }{ + {"1x4", "1x4", 1}, + {"1x2/1x5/1x5", "1x2/1x5/1x5", 3}, + {"2x12/2x4", "2x12/2x4", 2}, + {"0x0", "0x0", 1}, + {"000x0", "0x0", 1}, + {"%%%x0", "0x0", 1}, + {"", "", 0}, + {"3", "", 0}, // no 'x': C parses nothing + } + for _, c := range cases { + got := ParseDice(c.in) + if len(got) != c.len || got.String() != c.want { + t.Errorf("ParseDice(%q) = %v (len %d), want %q (len %d)", + c.in, got, len(got), c.want, c.len) + } + } +} + +// The bestiary and weapon tables must parse to at least one attack each so +// every creature and weapon actually swings. +func TestTablesHaveDice(t *testing.T) { + for i, m := range monsterTable { + if len(m.Stats.Dmg) == 0 { + t.Errorf("monster %c (%s) has no attacks", 'A'+i, m.Name) + } + } + for w, iw := range initWeaps { + if len(iw.dam) == 0 || len(iw.hrl) == 0 { + t.Errorf("weapon %v has empty dice", WeaponKind(w)) + } + } +} diff --git a/game/effects_test.go b/game/effects_test.go index faecadd..0bc86db 100644 --- a/game/effects_test.go +++ b/game/effects_test.go @@ -72,11 +72,11 @@ func TestReadEnchantArmor(t *testing.T) { ch := give(g, scr) g.scr.term.(*testTerm).input = []byte{ch} - before := g.Player.CurArmor.Arm + before := g.Player.CurArmor.ArmorClass g.readScroll() - if g.Player.CurArmor.Arm != before-1 { + if g.Player.CurArmor.ArmorClass != before-1 { t.Errorf("enchant armor: AC %d -> %d, want %d", - before, g.Player.CurArmor.Arm, before-1) + before, g.Player.CurArmor.ArmorClass, before-1) } } @@ -139,12 +139,12 @@ func TestZapSlowMonster(t *testing.T) { g.scr.term.(*testTerm).input = []byte{ch} g.Delta = Coord{X: 1, Y: 0} // aim at the monster - charges := stick.Charges() + charges := stick.Charges g.doZap() if !tp.On(Slowed) { t.Error("slow monster wand did not slow") } - if stick.Charges() != charges-1 { + if stick.Charges != charges-1 { t.Error("zap did not use a charge") } } diff --git a/game/fight.go b/game/fight.go index 96263c7..ebf22c7 100644 --- a/game/fight.go +++ b/game/fight.go @@ -1,10 +1,6 @@ package game -import ( - "fmt" - "strconv" - "strings" -) +import "strconv" // fight.c — all the fighting gets done here. @@ -245,7 +241,7 @@ func (g *RogueGame) attack(mp *Monster) int { // Venus Flytrap stops the poor guy from moving p.Flags.Set(Held) p.VfHit++ - g.Monsters['F'-'A'].Stats.Dmg = fmt.Sprintf("%dx1", p.VfHit) + g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}} if p.Stats.HP--; p.Stats.HP <= 0 { g.death('F') } @@ -322,34 +318,34 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool p := &g.Player att := &thatt.Stats def := &thdef.Stats - var cp string + var attacks DiceSpec var hplus, dplus int if weap == nil { - cp = att.Dmg + attacks = att.Dmg } else { hplus = weap.HPlus dplus = weap.DPlus if weap == p.CurWeapon { if p.IsRing(Left, RingIncreaseDamage) { - dplus += p.CurRing[Left].Arm + dplus += p.CurRing[Left].Bonus } else if p.IsRing(Left, RingDexterity) { - hplus += p.CurRing[Left].Arm + hplus += p.CurRing[Left].Bonus } if p.IsRing(Right, RingIncreaseDamage) { - dplus += p.CurRing[Right].Arm + dplus += p.CurRing[Right].Bonus } else if p.IsRing(Right, RingDexterity) { - hplus += p.CurRing[Right].Arm + hplus += p.CurRing[Right].Bonus } } - cp = weap.Damage + attacks = weap.Damage if hurl { if weap.Flags.Has(Missile) && p.CurWeapon != nil && WeaponKind(p.CurWeapon.Which) == weap.Launch { - cp = weap.HurlDmg + attacks = weap.HurlDmg hplus += p.CurWeapon.HPlus dplus += p.CurWeapon.DPlus } else if weap.Launch < 0 { - cp = weap.HurlDmg + attacks = weap.HurlDmg } } } @@ -358,40 +354,28 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool if !thdef.Flags.Has(Awake) { hplus += 4 } - defArm := def.Arm + defArm := def.ArmorClass if def == &p.Stats { if p.CurArmor != nil { - defArm = p.CurArmor.Arm + defArm = p.CurArmor.ArmorClass } if p.IsRing(Left, RingProtection) { - defArm -= p.CurRing[Left].Arm + defArm -= p.CurRing[Left].Bonus } if p.IsRing(Right, RingProtection) { - defArm -= p.CurRing[Right].Arm + defArm -= p.CurRing[Right].Bonus } } didHit := false - for cp != "" { - ndice := cAtoi(cp) - xi := strings.IndexByte(cp, 'x') - if xi < 0 { - break - } - cp = cp[xi+1:] - nsides := cAtoi(cp) + for _, atk := range attacks { if g.swing(att.Lvl, defArm, hplus+strPlus[att.Str]) { - proll := g.roll(ndice, nsides) + proll := g.roll(atk.Count, atk.Sides) damage := dplus + proll + addDam[att.Str] if damage > 0 { def.HP -= damage } didHit = true } - si := strings.IndexByte(cp, '/') - if si < 0 { - break - } - cp = cp[si+1:] } return didHit } @@ -531,7 +515,7 @@ func (g *RogueGame) killed(tp *Monster, pr bool) { case 'F': p.Flags.Clear(Held) p.VfHit = 0 - g.Monsters['F'-'A'].Stats.Dmg = "000x0" + g.Monsters['F'-'A'].Stats.Dmg = dice("000x0") case 'L': pos, ok := g.fallpos(tp.Pos) if ok { @@ -540,9 +524,9 @@ func (g *RogueGame) killed(tp *Monster, pr bool) { if ok && g.Depth >= g.MaxDepth { gold := newObject() gold.Kind = KindGold - gold.SetGoldVal(g.goldCalc()) + gold.GoldValue = g.goldCalc() if g.save(VsMagic) { - gold.Arm += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc() + gold.GoldValue += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc() } attachObj(&tp.Pack, gold) } diff --git a/game/fight_test.go b/game/fight_test.go index 4d10e80..52e93dd 100644 --- a/game/fight_test.go +++ b/game/fight_test.go @@ -23,8 +23,8 @@ func spawnAdjacent(g *RogueGame, typ byte) *Monster { func TestRollEmParsesMultiAttackDice(t *testing.T) { g := mkGame(t, 42) - att := &Creature{Stats: Stats{Str: 16, Lvl: 20, Dmg: "1x4/1x4/1x4"}} - def := &Creature{Stats: Stats{Arm: 10, HP: 1000}} + att := &Creature{Stats: Stats{Str: 16, Lvl: 20, Dmg: dice("1x4/1x4/1x4")}} + def := &Creature{Stats: Stats{ArmorClass: 10, HP: 1000}} def.Flags.Set(Awake) // With attacker level 20 vs armor 10, swing always hits // (rnd(20)+wplus >= (20-20)-10 is always true), so three attacks of diff --git a/game/init.go b/game/init.go index 8830f56..c964539 100644 --- a/game/init.go +++ b/game/init.go @@ -20,7 +20,7 @@ func (g *RogueGame) initPlayer() { obj = newObject() obj.Kind = KindArmor obj.Which = int(ArmorRingMail) - obj.Arm = aClass[ArmorRingMail] - 1 + obj.ArmorClass = aClass[ArmorRingMail] - 1 obj.Flags.Set(Known) obj.Count = 1 p.CurArmor = obj diff --git a/game/io.go b/game/io.go index 5fea9e4..a0a0ade 100644 --- a/game/io.go +++ b/game/io.go @@ -142,9 +142,9 @@ func (g *RogueGame) status() { p := &g.Player // If nothing has changed since the last status, don't bother. - temp := p.Stats.Arm + temp := p.Stats.ArmorClass if p.CurArmor != nil { - temp = p.CurArmor.Arm + temp = p.CurArmor.ArmorClass } if s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp && s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str && diff --git a/game/misc.go b/game/misc.go index b4706c5..b6efa00 100644 --- a/game/misc.go +++ b/game/misc.go @@ -279,10 +279,10 @@ func (g *RogueGame) chgStr(amt int) { addStr(&p.Stats.Str, amt) comp := p.Stats.Str if p.IsRing(Left, RingAddStrength) { - addStr(&comp, -p.CurRing[Left].Arm) + addStr(&comp, -p.CurRing[Left].Bonus) } if p.IsRing(Right, RingAddStrength) { - addStr(&comp, -p.CurRing[Right].Arm) + addStr(&comp, -p.CurRing[Right].Bonus) } if comp > p.MaxStats.Str { p.MaxStats.Str = comp diff --git a/game/monsters.go b/game/monsters.go index b80e15b..f3131b9 100644 --- a/game/monsters.go +++ b/game/monsters.go @@ -54,7 +54,7 @@ func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) { tp.Stats.Lvl = mp.Stats.Lvl + levAdd tp.Stats.MaxHP = g.roll(tp.Stats.Lvl, 8) tp.Stats.HP = tp.Stats.MaxHP - tp.Stats.Arm = mp.Stats.Arm - levAdd + tp.Stats.ArmorClass = mp.Stats.ArmorClass - levAdd tp.Stats.Dmg = mp.Stats.Dmg tp.Stats.Str = mp.Stats.Str tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp) @@ -183,10 +183,10 @@ func (g *RogueGame) save(which int) bool { p := &g.Player if which == VsMagic { if p.IsRing(Left, RingProtection) { - which -= p.CurRing[Left].Arm + which -= p.CurRing[Left].Bonus } if p.IsRing(Right, RingProtection) { - which -= p.CurRing[Right].Arm + which -= p.CurRing[Right].Bonus } } return g.saveThrow(which, &p.Stats) diff --git a/game/move.go b/game/move.go index c7a60dd..a7609f1 100644 --- a/game/move.go +++ b/game/move.go @@ -241,7 +241,7 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind { p.Flags.Clear(Awake) g.msg("a strange white mist envelops you and you fall asleep") case TrapArrow: - if g.swing(p.Stats.Lvl-1, p.Stats.Arm, 1) { + if g.swing(p.Stats.Lvl-1, p.Stats.ArmorClass, 1) { p.Stats.HP -= g.roll(1, 6) if p.Stats.HP <= 0 { g.msg("an arrow killed you") @@ -263,7 +263,7 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind { g.teleport() g.mvaddch(tc.Y, tc.X, Trap) case TrapDart: - if !g.swing(p.Stats.Lvl+1, p.Stats.Arm, 1) { + if !g.swing(p.Stats.Lvl+1, p.Stats.ArmorClass, 1) { g.msg("a small dart whizzes by your ear and vanishes") } else { p.Stats.HP -= g.roll(1, 4) @@ -322,7 +322,7 @@ func (g *RogueGame) rndmove(who *Creature) Coord { // aren't wearing a magic ring (move.c rust_armor). func (g *RogueGame) rustArmor(arm *Object) { if arm == nil || arm.Kind != KindArmor || arm.ArmorKind() == ArmorLeather || - arm.Arm >= 9 { + arm.ArmorClass >= 9 { return } @@ -331,7 +331,7 @@ func (g *RogueGame) rustArmor(arm *Object) { g.msg("the rust vanishes instantly") } } else { - arm.Arm++ + arm.ArmorClass++ if !g.Options.Terse { g.msg("your armor appears to be weaker now. Oh my!") } else { diff --git a/game/newlevel.go b/game/newlevel.go index 54a2592..cccc7ee 100644 --- a/game/newlevel.go +++ b/game/newlevel.go @@ -111,9 +111,9 @@ func (g *RogueGame) putThings() { if g.Depth >= AmuletLevel && !g.HasAmulet { obj := newObject() attachObj(&g.Level.Objects, obj) - obj.Damage = "0x0" - obj.HurlDmg = "0x0" - obj.Arm = 11 + obj.Damage = dice("0x0") + obj.HurlDmg = dice("0x0") + obj.ArmorClass = 11 obj.Kind = KindAmulet // Put it somewhere obj.Pos, _ = g.findFloor(nil, 0, false) diff --git a/game/object.go b/game/object.go index 8e4d2b6..79ec9df 100644 --- a/game/object.go +++ b/game/object.go @@ -94,21 +94,24 @@ func (k ObjectKind) MergesInPack() bool { // 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 string // damage if used like sword - HurlDmg string // 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 - Arm int // armor protection — overloaded as in C (see Charges/GoldVal) - Flags ObjFlags - Group int // group number for this object - Label string // label for object + 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 @@ -117,18 +120,6 @@ func newObject() *Object { return &Object{Launch: noWeapon} } -// Charges is the o_charges alias for Arm on wands and staffs. -func (o *Object) Charges() int { return o.Arm } - -// SetCharges stores a charge count in the overloaded Arm field. -func (o *Object) SetCharges(n int) { o.Arm = n } - -// GoldVal is the o_goldval alias for Arm on gold piles. -func (o *Object) GoldVal() int { return o.Arm } - -// SetGoldVal stores a gold value in the overloaded Arm field. -func (o *Object) SetGoldVal(n int) { o.Arm = n } - // PotionKind returns Which as a potion kind; valid only for KindPotion. func (o *Object) PotionKind() PotionKind { return PotionKind(o.Which) } diff --git a/game/pack.go b/game/pack.go index 85e3178..6f2f932 100644 --- a/game/pack.go +++ b/game/pack.go @@ -250,7 +250,7 @@ func (g *RogueGame) pickUp(ch byte) { if obj == nil { return } - g.money(obj.GoldVal()) + g.money(obj.GoldValue) detachObj(&g.Level.Objects, obj) p.Room.GoldVal = 0 default: diff --git a/game/potions.go b/game/potions.go index d949870..a9ba020 100644 --- a/game/potions.go +++ b/game/potions.go @@ -153,19 +153,19 @@ func (g *RogueGame) quaff() { } case PotionRestoreStrength: if p.IsRing(Left, RingAddStrength) { - addStr(&p.Stats.Str, -p.CurRing[Left].Arm) + addStr(&p.Stats.Str, -p.CurRing[Left].Bonus) } if p.IsRing(Right, RingAddStrength) { - addStr(&p.Stats.Str, -p.CurRing[Right].Arm) + addStr(&p.Stats.Str, -p.CurRing[Right].Bonus) } if p.Stats.Str < p.MaxStats.Str { p.Stats.Str = p.MaxStats.Str } if p.IsRing(Left, RingAddStrength) { - addStr(&p.Stats.Str, p.CurRing[Left].Arm) + addStr(&p.Stats.Str, p.CurRing[Left].Bonus) } if p.IsRing(Right, RingAddStrength) { - addStr(&p.Stats.Str, p.CurRing[Right].Arm) + addStr(&p.Stats.Str, p.CurRing[Right].Bonus) } g.msg("hey, this tastes great. It make you feel warm all over") case PotionBlindness: @@ -212,7 +212,7 @@ func (g *RogueGame) doPot(kind PotionKind, knowit bool) { func (o *Object) isMagic() bool { switch o.Kind { case KindArmor: - return o.Flags.Has(Protected) || o.Arm != aClass[o.Which] + return o.Flags.Has(Protected) || o.ArmorClass != aClass[o.Which] case KindWeapon: return o.HPlus != 0 || o.DPlus != 0 case KindPotion, KindScroll, KindWand, KindRing, KindAmulet: diff --git a/game/rings.go b/game/rings.go index a151a83..cede65f 100644 --- a/game/rings.go +++ b/game/rings.go @@ -49,7 +49,7 @@ func (g *RogueGame) ringOn() { // Calculate the effect it has on the poor guy. switch obj.RingKind() { case RingAddStrength: - g.chgStr(obj.Arm) + g.chgStr(obj.Bonus) case RingSeeInvisible: g.invisOn() case RingAggravateMonsters: @@ -168,7 +168,7 @@ func ringNum(g *RogueGame, obj *Object) string { } switch obj.RingKind() { case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity: - return fmt.Sprintf(" [%s]", num(obj.Arm, 0, Ring)) + return fmt.Sprintf(" [%s]", num(obj.Bonus, 0, Ring)) } return "" } diff --git a/game/rip.go b/game/rip.go index 8a1cd95..7d488dc 100644 --- a/game/rip.go +++ b/game/rip.go @@ -124,8 +124,8 @@ func (g *RogueGame) totalWinner() { obj.Flags.Set(Known) case KindArmor: worth = it.Armors[obj.Which].Worth - worth += (9 - obj.Arm) * 100 - worth += 10 * (aClass[obj.Which] - obj.Arm) + worth += (9 - obj.ArmorClass) * 100 + worth += 10 * (aClass[obj.Which] - obj.ArmorClass) obj.Flags.Set(Known) case KindScroll: op := &it.Scrolls[obj.Which] @@ -146,8 +146,8 @@ func (g *RogueGame) totalWinner() { worth = op.Worth if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage || obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity { - if obj.Arm > 0 { - worth += obj.Arm * 100 + if obj.Bonus > 0 { + worth += obj.Bonus * 100 } else { worth = 10 } @@ -160,7 +160,7 @@ func (g *RogueGame) totalWinner() { case KindWand: op := &it.Sticks[obj.Which] worth = op.Worth - worth += 20 * obj.Charges() + worth += 20 * obj.Charges if !obj.Flags.Has(Known) { worth /= 2 } diff --git a/game/rooms.go b/game/rooms.go index 51f391d..5a48c13 100644 --- a/game/rooms.go +++ b/game/rooms.go @@ -88,7 +88,7 @@ func (g *RogueGame) doRooms() { if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) { gold := newObject() rp.GoldVal = g.goldCalc() - gold.SetGoldVal(rp.GoldVal) + gold.GoldValue = rp.GoldVal rp.Gold, _ = g.findFloorIn(rp, 0, false) gold.Pos = rp.Gold g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold) diff --git a/game/save.go b/game/save.go index 8d555bf..0d02cf4 100644 --- a/game/save.go +++ b/game/save.go @@ -13,8 +13,10 @@ import ( // saveFormatVersion identifies the snapshot layout; it changes whenever a // field rename or retype would make gob silently mis-decode an older -// save ("go2": Object.Type became Kind ObjectKind). -const saveFormatVersion = Release + "-go2" +// save ("go2": Object.Type became Kind ObjectKind; "go3": Object.Arm split +// into ArmorClass/Charges/GoldValue/Bonus, damage strings became +// DiceSpec). +const saveFormatVersion = Release + "-go3" // destRef encodes a monster's chase target (state.c rs_write_thing's // (kind, index) pairs). diff --git a/game/save_test.go b/game/save_test.go index 8e39c05..ef18d70 100644 --- a/game/save_test.go +++ b/game/save_test.go @@ -15,7 +15,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) { g.HasAmulet = true g.Items.Potions[PotionHealing].Know = true g.Items.Scrolls[ScrollMagicMapping].Guess = "map???" - g.Monsters['F'-'A'].Stats.Dmg = "3x1" // mutated bestiary must survive + g.Monsters['F'-'A'].Stats.Dmg = dice("3x1") // mutated bestiary must survive if len(g.Level.Monsters) > 0 { g.Level.Monsters[0].Flags.Set(Awake) g.Level.Monsters[0].Dest = &g.Player.Pos @@ -48,7 +48,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) { if h.Items.Scrolls[ScrollMagicMapping].Guess != "map???" { t.Error("scroll guess lost") } - if h.Monsters['F'-'A'].Stats.Dmg != "3x1" { + if h.Monsters['F'-'A'].Stats.Dmg.String() != "3x1" { t.Error("mutated bestiary lost") } if h.Rng.Seed != g.Rng.Seed { diff --git a/game/scrolls.go b/game/scrolls.go index a643212..59b76e0 100644 --- a/game/scrolls.go +++ b/game/scrolls.go @@ -42,7 +42,7 @@ func (g *RogueGame) readScroll() { g.msg("your hands begin to glow %s", g.pickColor("red")) case ScrollEnchantArmor: if p.CurArmor != nil { - p.CurArmor.Arm-- + p.CurArmor.ArmorClass-- p.CurArmor.Flags.Clear(Cursed) g.msg("your armor glows %s for a moment", g.pickColor("silver")) } diff --git a/game/sticks.go b/game/sticks.go index 55bdd5e..dc0345b 100644 --- a/game/sticks.go +++ b/game/sticks.go @@ -16,7 +16,7 @@ func (g *RogueGame) doZap() { g.msg("you can't zap with that!") return } - if obj.Charges() == 0 { + if obj.Charges == 0 { g.msg("nothing happens") return } @@ -111,7 +111,7 @@ func (g *RogueGame) doZap() { g.Items.Sticks[WandMagicMissile].Know = true bolt := newObject() bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon - bolt.HurlDmg = "1x4" + bolt.HurlDmg = dice("1x4") bolt.HPlus = 100 bolt.DPlus = 1 bolt.Flags = Missile @@ -167,7 +167,7 @@ func (g *RogueGame) doZap() { g.Items.Sticks[obj.Which].Know = true case WandNothing: } - obj.SetCharges(obj.Charges() - 1) + obj.Charges-- } // drain does the drain-hit-points-from-player schtick (sticks.c drain). @@ -213,7 +213,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) { bolt := newObject() bolt.Kind = KindWeapon bolt.Which = int(WeaponFlame) - bolt.HurlDmg = "6x6" + bolt.HurlDmg = dice("6x6") bolt.HPlus = 100 bolt.DPlus = 0 g.Items.Weapons[WeaponFlame].Name = name @@ -322,17 +322,17 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) { // fixStick sets up a new wand or staff (sticks.c fix_stick). func (g *RogueGame) fixStick(cur *Object) { if g.Items.WandType[cur.Which] == "staff" { - cur.Damage = "2x3" + cur.Damage = dice("2x3") } else { - cur.Damage = "1x1" + cur.Damage = dice("1x1") } - cur.HurlDmg = "1x1" + cur.HurlDmg = dice("1x1") switch cur.WandKind() { case WandLight: - cur.SetCharges(g.rnd(10) + 10) + cur.Charges = g.rnd(10) + 10 default: - cur.SetCharges(g.rnd(5) + 3) + cur.Charges = g.rnd(5) + 3 } } @@ -343,7 +343,7 @@ func chargeStr(g *RogueGame, obj *Object) string { return "" } if g.Options.Terse { - return fmt.Sprintf(" [%d]", obj.Charges()) + return fmt.Sprintf(" [%d]", obj.Charges) } - return fmt.Sprintf(" [%d charges]", obj.Charges()) + return fmt.Sprintf(" [%d charges]", obj.Charges) } diff --git a/game/tables.go b/game/tables.go index 4581ad6..48c16f0 100644 --- a/game/tables.go +++ b/game/tables.go @@ -6,7 +6,7 @@ package game // change during play) are cloned into RogueGame.Items by NewGame. // initStats is the C INIT_STATS: the player's starting statistics. -var initStats = Stats{Str: 16, Exp: 0, Lvl: 1, Arm: 10, HP: 12, Dmg: "1x4", MaxHP: 12} +var initStats = Stats{Str: 16, Exp: 0, Lvl: 1, ArmorClass: 10, HP: 12, Dmg: dice("1x4"), MaxHP: 12} // aClass is extern.c a_class[]: armor class for each armor type. var aClass = [NumArmorTypes]int{ @@ -47,32 +47,32 @@ var invTName = []string{"Overwrite", "Slow", "Clear"} // rolled from the level at creation time. var monsterTable = [26]MonsterKind{ /* Name CARRY FLAGS str exp lvl arm hp dmg */ - {"aquator", 0, Mean, Stats{10, 20, 5, 2, 1, "0x0/0x0", 0}}, - {"bat", 0, Flying, Stats{10, 1, 1, 3, 1, "1x2", 0}}, - {"centaur", 15, 0, Stats{10, 17, 4, 4, 1, "1x2/1x5/1x5", 0}}, - {"dragon", 100, Mean, Stats{10, 5000, 10, -1, 1, "1x8/1x8/3x10", 0}}, - {"emu", 0, Mean, Stats{10, 2, 1, 7, 1, "1x2", 0}}, - {"venus flytrap", 0, Mean, Stats{10, 80, 8, 3, 1, "%%%x0", 0}}, - {"griffin", 20, Mean | Flying | Regenerates, Stats{10, 2000, 13, 2, 1, "4x3/3x5", 0}}, - {"hobgoblin", 0, Mean, Stats{10, 3, 1, 5, 1, "1x8", 0}}, - {"ice monster", 0, 0, Stats{10, 5, 1, 9, 1, "0x0", 0}}, - {"jabberwock", 70, 0, Stats{10, 3000, 15, 6, 1, "2x12/2x4", 0}}, - {"kestrel", 0, Mean | Flying, Stats{10, 1, 1, 7, 1, "1x4", 0}}, - {"leprechaun", 0, 0, Stats{10, 10, 3, 8, 1, "1x1", 0}}, - {"medusa", 40, Mean, Stats{10, 200, 8, 2, 1, "3x4/3x4/2x5", 0}}, - {"nymph", 100, 0, Stats{10, 37, 3, 9, 1, "0x0", 0}}, - {"orc", 15, Greedy, Stats{10, 5, 1, 6, 1, "1x8", 0}}, - {"phantom", 0, Invisible, Stats{10, 120, 8, 3, 1, "4x4", 0}}, - {"quagga", 0, Mean, Stats{10, 15, 3, 3, 1, "1x5/1x5", 0}}, - {"rattlesnake", 0, Mean, Stats{10, 9, 2, 3, 1, "1x6", 0}}, - {"snake", 0, Mean, Stats{10, 2, 1, 5, 1, "1x3", 0}}, - {"troll", 50, Regenerates | Mean, Stats{10, 120, 6, 4, 1, "1x8/1x8/2x6", 0}}, - {"black unicorn", 0, Mean, Stats{10, 190, 7, -2, 1, "1x9/1x9/2x9", 0}}, - {"vampire", 20, Regenerates | Mean, Stats{10, 350, 8, 1, 1, "1x10", 0}}, - {"wraith", 0, 0, Stats{10, 55, 5, 4, 1, "1x6", 0}}, - {"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, "4x4", 0}}, - {"yeti", 30, 0, Stats{10, 50, 4, 6, 1, "1x6/1x6", 0}}, - {"zombie", 0, Mean, Stats{10, 6, 2, 8, 1, "1x8", 0}}, + {"aquator", 0, Mean, Stats{10, 20, 5, 2, 1, dice("0x0/0x0"), 0}}, + {"bat", 0, Flying, Stats{10, 1, 1, 3, 1, dice("1x2"), 0}}, + {"centaur", 15, 0, Stats{10, 17, 4, 4, 1, dice("1x2/1x5/1x5"), 0}}, + {"dragon", 100, Mean, Stats{10, 5000, 10, -1, 1, dice("1x8/1x8/3x10"), 0}}, + {"emu", 0, Mean, Stats{10, 2, 1, 7, 1, dice("1x2"), 0}}, + {"venus flytrap", 0, Mean, Stats{10, 80, 8, 3, 1, dice("%%%x0"), 0}}, + {"griffin", 20, Mean | Flying | Regenerates, Stats{10, 2000, 13, 2, 1, dice("4x3/3x5"), 0}}, + {"hobgoblin", 0, Mean, Stats{10, 3, 1, 5, 1, dice("1x8"), 0}}, + {"ice monster", 0, 0, Stats{10, 5, 1, 9, 1, dice("0x0"), 0}}, + {"jabberwock", 70, 0, Stats{10, 3000, 15, 6, 1, dice("2x12/2x4"), 0}}, + {"kestrel", 0, Mean | Flying, Stats{10, 1, 1, 7, 1, dice("1x4"), 0}}, + {"leprechaun", 0, 0, Stats{10, 10, 3, 8, 1, dice("1x1"), 0}}, + {"medusa", 40, Mean, Stats{10, 200, 8, 2, 1, dice("3x4/3x4/2x5"), 0}}, + {"nymph", 100, 0, Stats{10, 37, 3, 9, 1, dice("0x0"), 0}}, + {"orc", 15, Greedy, Stats{10, 5, 1, 6, 1, dice("1x8"), 0}}, + {"phantom", 0, Invisible, Stats{10, 120, 8, 3, 1, dice("4x4"), 0}}, + {"quagga", 0, Mean, Stats{10, 15, 3, 3, 1, dice("1x5/1x5"), 0}}, + {"rattlesnake", 0, Mean, Stats{10, 9, 2, 3, 1, dice("1x6"), 0}}, + {"snake", 0, Mean, Stats{10, 2, 1, 5, 1, dice("1x3"), 0}}, + {"troll", 50, Regenerates | Mean, Stats{10, 120, 6, 4, 1, dice("1x8/1x8/2x6"), 0}}, + {"black unicorn", 0, Mean, Stats{10, 190, 7, -2, 1, dice("1x9/1x9/2x9"), 0}}, + {"vampire", 20, Regenerates | Mean, Stats{10, 350, 8, 1, 1, dice("1x10"), 0}}, + {"wraith", 0, 0, Stats{10, 55, 5, 4, 1, dice("1x6"), 0}}, + {"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, dice("4x4"), 0}}, + {"yeti", 30, 0, Stats{10, 50, 4, 6, 1, dice("1x6/1x6"), 0}}, + {"zombie", 0, Mean, Stats{10, 6, 2, 8, 1, dice("1x8"), 0}}, } // Base ObjInfo tables (extern.c). These are templates: NewGame copies them diff --git a/game/things.go b/game/things.go index fc90159..bf15d9d 100644 --- a/game/things.go +++ b/game/things.go @@ -69,11 +69,11 @@ func (g *RogueGame) invName(obj *Object, drop bool) string { case KindArmor: sp := it.Armors[which].Name if obj.Flags.Has(Known) { - fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.Arm, 0, Armor), sp) + fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.ArmorClass, 0, Armor), sp) if !g.Options.Terse { pb.WriteString("protection ") } - fmt.Fprintf(&pb, "%d]", 10-obj.Arm) + fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass) } else { pb.WriteString(sp) } @@ -83,7 +83,7 @@ func (g *RogueGame) invName(obj *Object, drop bool) string { case KindAmulet: pb.WriteString("The Amulet of Yendor") case KindGold: - fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldVal()) + fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue) } out := pb.String() @@ -168,7 +168,7 @@ func (g *RogueGame) dropCheck(obj *Object) bool { p.CurRing[hand] = nil switch obj.RingKind() { case RingAddStrength: - g.chgStr(-obj.Arm) + g.chgStr(-obj.Bonus) case RingSeeInvisible: g.unsee(0) g.Extinguish(DUnsee) @@ -180,9 +180,9 @@ func (g *RogueGame) dropCheck(obj *Object) bool { // 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.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 @@ -219,20 +219,20 @@ func (g *RogueGame) newThing() *Object { case 4: cur.Kind = KindArmor cur.Which = pickOne(g, g.Items.Armors[:]) - cur.Arm = aClass[cur.Which] + cur.ArmorClass = aClass[cur.Which] if r := g.rnd(100); r < 20 { cur.Flags.Set(Cursed) - cur.Arm += g.rnd(3) + 1 + cur.ArmorClass += g.rnd(3) + 1 } else if r < 28 { - cur.Arm -= g.rnd(3) + 1 + cur.ArmorClass -= g.rnd(3) + 1 } case 5: cur.Kind = KindRing cur.Which = pickOne(g, g.Items.Rings[:]) switch cur.RingKind() { case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage: - if cur.Arm = g.rnd(3); cur.Arm == 0 { - cur.Arm = -1 + if cur.Bonus = g.rnd(3); cur.Bonus == 0 { + cur.Bonus = -1 cur.Flags.Set(Cursed) } case RingAggravateMonsters, RingTeleportation: diff --git a/game/types.go b/game/types.go index 47c3bcc..302875a 100644 --- a/game/types.go +++ b/game/types.go @@ -374,13 +374,13 @@ type Coord struct { // Stats describes a fighting being (rogue.h struct stats). type Stats struct { - Str int // strength (s_str; 3..31) - Exp int // experience - Lvl int // level of mastery - Arm int // armor class - HP int // hit points (s_hpt) - Dmg string // damage dice, e.g. "1x4/1x2" - MaxHP int + Str int // strength (s_str; 3..31) + Exp int // experience + Lvl int // level of mastery + ArmorClass int // armor class (s_arm) + HP int // hit points (s_hpt) + Dmg DiceSpec // damage dice, e.g. "1x4/1x2" + MaxHP int } // Room describes a room or passage network (rogue.h struct room). diff --git a/game/weapons.go b/game/weapons.go index f539107..235ef67 100644 --- a/game/weapons.go +++ b/game/weapons.go @@ -123,20 +123,20 @@ func (g *RogueGame) wield() { // initWeaps is the weapons.c init_dam[] table. var initWeaps = [NumWeaponTypes]struct { - dam string // damage when wielded - hrl string // damage when thrown + dam DiceSpec // damage when wielded + hrl DiceSpec // damage when thrown launch WeaponKind // launching weapon flags ObjFlags }{ - {"2x4", "1x3", noWeapon, 0}, // WeaponMace - {"3x4", "1x2", noWeapon, 0}, // Long sword - {"1x1", "1x1", noWeapon, 0}, // WeaponBow - {"1x1", "2x3", WeaponBow, Stackable | Missile}, // WeaponArrow - {"1x6", "1x4", noWeapon, Missile}, // WeaponDagger - {"4x4", "1x2", noWeapon, 0}, // 2h sword - {"1x1", "1x3", noWeapon, Stackable | Missile}, // WeaponDart - {"1x2", "2x4", noWeapon, Stackable | Missile}, // Shuriken - {"2x3", "1x6", noWeapon, Missile}, // WeaponSpear + {dice("2x4"), dice("1x3"), noWeapon, 0}, // WeaponMace + {dice("3x4"), dice("1x2"), noWeapon, 0}, // Long sword + {dice("1x1"), dice("1x1"), noWeapon, 0}, // WeaponBow + {dice("1x1"), dice("2x3"), WeaponBow, Stackable | Missile}, // WeaponArrow + {dice("1x6"), dice("1x4"), noWeapon, Missile}, // WeaponDagger + {dice("4x4"), dice("1x2"), noWeapon, 0}, // 2h sword + {dice("1x1"), dice("1x3"), noWeapon, Stackable | Missile}, // WeaponDart + {dice("1x2"), dice("2x4"), noWeapon, Stackable | Missile}, // Shuriken + {dice("2x3"), dice("1x6"), noWeapon, Missile}, // WeaponSpear } // initWeapon sets up a new weapon (weapons.c init_weapon). diff --git a/game/wizard.go b/game/wizard.go index b2079b6..8223b6d 100644 --- a/game/wizard.go +++ b/game/wizard.go @@ -38,12 +38,12 @@ func (g *RogueGame) createObj() { obj.HPlus += g.rnd(3) + 1 } } else { - obj.Arm = aClass[obj.Which] + obj.ArmorClass = aClass[obj.Which] if bless == '-' { - obj.Arm += g.rnd(3) + 1 + obj.ArmorClass += g.rnd(3) + 1 } if bless == '+' { - obj.Arm -= g.rnd(3) + 1 + obj.ArmorClass -= g.rnd(3) + 1 } } case obj.Kind == KindRing: @@ -54,9 +54,9 @@ func (g *RogueGame) createObj() { g.Msgs.Mpos = 0 if bless == '-' { obj.Flags.Set(Cursed) - obj.Arm = -1 + obj.Bonus = -1 } else { - obj.Arm = g.rnd(2) + 1 + obj.Bonus = g.rnd(2) + 1 } case RingAggravateMonsters, RingTeleportation: obj.Flags.Set(Cursed) @@ -67,7 +67,7 @@ func (g *RogueGame) createObj() { g.msg("how much?") buf := "" if g.getStr(&buf, g.scr.Std) == Norm { - obj.SetGoldVal(cAtoi(buf)) + obj.GoldValue = cAtoi(buf) } } g.addPack(obj, false) @@ -169,7 +169,7 @@ func (g *RogueGame) teleport() { if p.On(Held) { p.Flags.Clear(Held) p.VfHit = 0 - g.Monsters['F'-'A'].Stats.Dmg = "000x0" + g.Monsters['F'-'A'].Stats.Dmg = dice("000x0") } g.NoMove = 0 g.Count = 0