Merge refactor/object-fields (refactor step 3 + module rename)

This commit is contained in:
2026-07-06 23:10:39 +02:00
32 changed files with 293 additions and 210 deletions

View File

@@ -6,7 +6,7 @@ This document has two parts:
codebase (every file, every function, every type, every global) as it exists codebase (every file, every function, every type, every global) as it exists
on the `modern-rogue` branch. on the `modern-rogue` branch.
- **Part 2 — The Go Port Design**: the object-oriented architecture for the Go - **Part 2 — The Go Port Design**: the object-oriented architecture for the Go
port (`sneak.berlin/go/rogue`), including how every C construct maps onto Go port (`git.eeqj.de/sneak/rgoue`), including how every C construct maps onto Go
types and methods. types and methods.
--- ---
@@ -887,7 +887,7 @@ Declares `rd_score(SCORE*)` / `wr_score(SCORE*)` (implemented in save.c).
# Part 2 — The Go Port Design # Part 2 — The Go Port Design
Module: **`sneak.berlin/go/rogue`** Module: **`git.eeqj.de/sneak/rgoue`**
## 1. Goals and principles ## 1. Goals and principles
@@ -929,7 +929,7 @@ branches; this branch is Go-only. The tcell wrapper ended up in its own
`term` package so that `game` itself has no third-party imports: `term` package so that `game` itself has no third-party imports:
``` ```
go.mod module sneak.berlin/go/rogue go.mod module git.eeqj.de/sneak/rgoue
cmd/rogue/main.go package main — flag parsing, constructs RogueGame, calls Run cmd/rogue/main.go package main — flag parsing, constructs RogueGame, calls Run
game/ package game — the port, one .go file per .c file game/ package game — the port, one .go file per .c file
game.go RogueGame struct, NewGame, Run (main.c) game.go RogueGame struct, NewGame, Run (main.c)

42
TODO.md
View File

@@ -29,14 +29,19 @@ Refactor ground rules:
# Next Step # Next Step
Refactor step 3 (branch refactor/object-fields): un-overload Object Adopt the house Go linting standards: copy .golangci.yml from the
fields — split Object.Arm into ArmorClass/Charges/GoldValue; parse prompts repo and bring game/, term/, and cmd/ lint-clean (the port is
damage dice ("1x4/1x2") once into a DiceSpec type at table definition greenfield code, so no exemptions apply).
instead of per swing; save-format update with round-trip test
migration.
# Completed Steps # Completed Steps
- 2026-07-06 Module base path updated to git.eeqj.de/sneak/rgoue
(go.mod, term/ and cmd/ imports, ARCHITECTURE.md, version string).
- 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): - 2026-07-06 Refactor step 2 (refactor/typed-kinds, b940cfc):
ObjectKind separates item category from map glyph (Object.Type byte ObjectKind separates item category from map glyph (Object.Type byte
→ Kind ObjectKind with Glyph()); PotionKind/ScrollKind/RingKind/ → Kind ObjectKind with Glyph()); PotionKind/ScrollKind/RingKind/
@@ -71,48 +76,45 @@ migration.
# Future Steps # Future Steps
1. Adopt the house Go linting standards: copy .golangci.yml from the 1. Refactor step 4: method renames, movement/world subsystem
prompts repo and bring game/, term/, and cmd/ lint-clean (the port
is greenfield code, so no exemptions apply).
2. Refactor step 4: method renames, movement/world subsystem
(doMove→moveHero, beTrapped→springTrap, rndmove→randomStep, (doMove→moveHero, beTrapped→springTrap, rndmove→randomStep,
doRooms/doPassages/doMaze→digRooms/digPassages/digMaze, doRooms/doPassages/doMaze→digRooms/digPassages/digMaze,
chgStr→changeStrength, ...); remove the goto/label flows in doMove, chgStr→changeStrength, ...); remove the goto/label flows in doMove,
dispatch, and saveGame in favor of loops and helpers. dispatch, and saveGame in favor of loops and helpers.
3. Refactor step 5: method renames, items/combat/UI subsystems 2. Refactor step 5: method renames, items/combat/UI subsystems
(invName→inventoryName, rollEm→rollAttacks, doPot→applyPotionFuse, (invName→inventoryName, rollEm→rollAttacks, doPot→applyPotionFuse,
getItem→promptPackItem returning (obj, ok), getDir→promptDirection); getItem→promptPackItem returning (obj, ok), getDir→promptDirection);
int status codes (attack returning -1) become named results. Two or int status codes (attack returning -1) become named results. Two or
three commits, one subsystem each. three commits, one subsystem each.
4. Refactor step 6: extract types from the god object — MessageLine 3. Refactor step 6: extract types from the god object — MessageLine
owns the msg/addmsg/endmsg machinery; pack/inventory operations move owns the msg/addmsg/endmsg machinery; pack/inventory operations move
onto *Player; monster/object list management and map queries onto *Player; monster/object list management and map queries
consolidate onto *Level; RogueGame keeps turn orchestration and consolidate onto *Level; RogueGame keeps turn orchestration and
cross-system effects only. cross-system effects only.
5. Refactor step 7: effects dispatch — the giant quaff/readScroll/doZap 4. Refactor step 7: effects dispatch — the giant quaff/readScroll/doZap
switches become per-kind handler tables of small named methods, switches become per-kind handler tables of small named methods,
keeping effect order and RNG call sequence identical. keeping effect order and RNG call sequence identical.
6. Refactor step 8: constructor and style pass per the house 5. Refactor step 8: constructor and style pass per the house
styleguide — game.New(game.Params{...}) replacing NewGame(Config); styleguide — game.New(game.Params{...}) replacing NewGame(Config);
replace the gameEnd panic unwind with error-based turn results where replace the gameEnd panic unwind with error-based turn results where
feasible; 77-column wrap sweep. feasible; 77-column wrap sweep.
7. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the 6. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the
post-refactor names; add the C name → Go name rename table. post-refactor names; add the C name → Go name rename table.
8. Playtest hardening pass: play several full games with the tcell 7. Playtest hardening pass: play several full games with the tcell
binary and extend run_test.go to script a deeper multi-level binary and extend run_test.go to script a deeper multi-level
playthrough (descend past level 5, use potions, scrolls, zapping, playthrough (descend past level 5, use potions, scrolls, zapping,
save/restore). Fix any panics, message mismatches, or divergences save/restore). Fix any panics, message mismatches, or divergences
from the C behavior that this uncovers, with regression tests. from the C behavior that this uncovers, with regression tests.
9. Verify the seed-compatibility claim against the C reference on 8. Verify the seed-compatibility claim against the C reference on
c-master: same seed, same dungeon, same item tables, for several c-master: same seed, same dungeon, same item tables, for several
seeds. seeds.
10. Broaden unit test coverage where playtesting finds thin spots 9. Broaden unit test coverage where playtesting finds thin spots
(rings, sticks, wizard commands). (rings, sticks, wizard commands).
11. Tag a release once a full game (Amulet retrieval and score entry) 10. Tag a release once a full game (Amulet retrieval and score entry)
completes without defects. completes without defects.
12. Full-terminal-size support (deferred by explicit decision 11. Full-terminal-size support (deferred by explicit decision
2026-07-06): per-game dungeon dimensions instead of the 80x24 2026-07-06): per-game dungeon dimensions instead of the 80x24
constants; open design questions are resize policy, gameplay constants; open design questions are resize policy, gameplay
tuning at larger sizes, and a --classic 80x24 mode. tuning at larger sizes, and a --classic 80x24 mode.
13. Note: this repo is exempt from the standard policy scaffold. Do not 12. Note: this repo is exempt from the standard policy scaffold. Do not
add Makefile, Dockerfile, or REPO_POLICIES.md. add Makefile, Dockerfile, or REPO_POLICIES.md.

View File

@@ -13,8 +13,8 @@ import (
"syscall" "syscall"
"time" "time"
"sneak.berlin/go/rogue/game" "git.eeqj.de/sneak/rgoue/game"
"sneak.berlin/go/rogue/term" "git.eeqj.de/sneak/rgoue/term"
) )
func main() { func main() {

View File

@@ -410,7 +410,7 @@ func (g *RogueGame) wizardCommand(ch byte) {
g.turnSee(p.On(SenseMonsters)) g.turnSee(p.On(SenseMonsters))
case CTRL('~'): case CTRL('~'):
if item := g.getItem("charge", KindWand); item != nil { if item := g.getItem("charge", KindWand); item != nil {
item.SetCharges(10000) item.Charges = 10000
} }
case CTRL('I'): case CTRL('I'):
for range 9 { for range 9 {
@@ -427,7 +427,7 @@ func (g *RogueGame) wizardCommand(ch byte) {
obj = newObject() obj = newObject()
obj.Kind = KindArmor obj.Kind = KindArmor
obj.Which = int(ArmorPlateMail) obj.Which = int(ArmorPlateMail)
obj.Arm = -5 obj.ArmorClass = -5
obj.Flags.Set(Known) obj.Flags.Set(Known)
obj.Count = 1 obj.Count = 1
p.CurArmor = obj p.CurArmor = obj

58
game/dice.go Normal file
View File

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

45
game/dice_test.go Normal file
View File

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

View File

@@ -72,11 +72,11 @@ func TestReadEnchantArmor(t *testing.T) {
ch := give(g, scr) ch := give(g, scr)
g.scr.term.(*testTerm).input = []byte{ch} g.scr.term.(*testTerm).input = []byte{ch}
before := g.Player.CurArmor.Arm before := g.Player.CurArmor.ArmorClass
g.readScroll() 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", 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.scr.term.(*testTerm).input = []byte{ch}
g.Delta = Coord{X: 1, Y: 0} // aim at the monster g.Delta = Coord{X: 1, Y: 0} // aim at the monster
charges := stick.Charges() charges := stick.Charges
g.doZap() g.doZap()
if !tp.On(Slowed) { if !tp.On(Slowed) {
t.Error("slow monster wand did not slow") 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") t.Error("zap did not use a charge")
} }
} }

View File

@@ -1,10 +1,6 @@
package game package game
import ( import "strconv"
"fmt"
"strconv"
"strings"
)
// fight.c — all the fighting gets done here. // 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 // Venus Flytrap stops the poor guy from moving
p.Flags.Set(Held) p.Flags.Set(Held)
p.VfHit++ 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 { if p.Stats.HP--; p.Stats.HP <= 0 {
g.death('F') g.death('F')
} }
@@ -322,34 +318,34 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
p := &g.Player p := &g.Player
att := &thatt.Stats att := &thatt.Stats
def := &thdef.Stats def := &thdef.Stats
var cp string var attacks DiceSpec
var hplus, dplus int var hplus, dplus int
if weap == nil { if weap == nil {
cp = att.Dmg attacks = att.Dmg
} else { } else {
hplus = weap.HPlus hplus = weap.HPlus
dplus = weap.DPlus dplus = weap.DPlus
if weap == p.CurWeapon { if weap == p.CurWeapon {
if p.IsRing(Left, RingIncreaseDamage) { if p.IsRing(Left, RingIncreaseDamage) {
dplus += p.CurRing[Left].Arm dplus += p.CurRing[Left].Bonus
} else if p.IsRing(Left, RingDexterity) { } else if p.IsRing(Left, RingDexterity) {
hplus += p.CurRing[Left].Arm hplus += p.CurRing[Left].Bonus
} }
if p.IsRing(Right, RingIncreaseDamage) { if p.IsRing(Right, RingIncreaseDamage) {
dplus += p.CurRing[Right].Arm dplus += p.CurRing[Right].Bonus
} else if p.IsRing(Right, RingDexterity) { } 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 hurl {
if weap.Flags.Has(Missile) && p.CurWeapon != nil && if weap.Flags.Has(Missile) && p.CurWeapon != nil &&
WeaponKind(p.CurWeapon.Which) == weap.Launch { WeaponKind(p.CurWeapon.Which) == weap.Launch {
cp = weap.HurlDmg attacks = weap.HurlDmg
hplus += p.CurWeapon.HPlus hplus += p.CurWeapon.HPlus
dplus += p.CurWeapon.DPlus dplus += p.CurWeapon.DPlus
} else if weap.Launch < 0 { } 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) { if !thdef.Flags.Has(Awake) {
hplus += 4 hplus += 4
} }
defArm := def.Arm defArm := def.ArmorClass
if def == &p.Stats { if def == &p.Stats {
if p.CurArmor != nil { if p.CurArmor != nil {
defArm = p.CurArmor.Arm defArm = p.CurArmor.ArmorClass
} }
if p.IsRing(Left, RingProtection) { if p.IsRing(Left, RingProtection) {
defArm -= p.CurRing[Left].Arm defArm -= p.CurRing[Left].Bonus
} }
if p.IsRing(Right, RingProtection) { if p.IsRing(Right, RingProtection) {
defArm -= p.CurRing[Right].Arm defArm -= p.CurRing[Right].Bonus
} }
} }
didHit := false didHit := false
for cp != "" { for _, atk := range attacks {
ndice := cAtoi(cp)
xi := strings.IndexByte(cp, 'x')
if xi < 0 {
break
}
cp = cp[xi+1:]
nsides := cAtoi(cp)
if g.swing(att.Lvl, defArm, hplus+strPlus[att.Str]) { 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] damage := dplus + proll + addDam[att.Str]
if damage > 0 { if damage > 0 {
def.HP -= damage def.HP -= damage
} }
didHit = true didHit = true
} }
si := strings.IndexByte(cp, '/')
if si < 0 {
break
}
cp = cp[si+1:]
} }
return didHit return didHit
} }
@@ -531,7 +515,7 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
case 'F': case 'F':
p.Flags.Clear(Held) p.Flags.Clear(Held)
p.VfHit = 0 p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = "000x0" g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
case 'L': case 'L':
pos, ok := g.fallpos(tp.Pos) pos, ok := g.fallpos(tp.Pos)
if ok { if ok {
@@ -540,9 +524,9 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
if ok && g.Depth >= g.MaxDepth { if ok && g.Depth >= g.MaxDepth {
gold := newObject() gold := newObject()
gold.Kind = KindGold gold.Kind = KindGold
gold.SetGoldVal(g.goldCalc()) gold.GoldValue = g.goldCalc()
if g.save(VsMagic) { 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) attachObj(&tp.Pack, gold)
} }

View File

@@ -23,8 +23,8 @@ func spawnAdjacent(g *RogueGame, typ byte) *Monster {
func TestRollEmParsesMultiAttackDice(t *testing.T) { func TestRollEmParsesMultiAttackDice(t *testing.T) {
g := mkGame(t, 42) g := mkGame(t, 42)
att := &Creature{Stats: Stats{Str: 16, Lvl: 20, Dmg: "1x4/1x4/1x4"}} att := &Creature{Stats: Stats{Str: 16, Lvl: 20, Dmg: dice("1x4/1x4/1x4")}}
def := &Creature{Stats: Stats{Arm: 10, HP: 1000}} def := &Creature{Stats: Stats{ArmorClass: 10, HP: 1000}}
def.Flags.Set(Awake) def.Flags.Set(Awake)
// With attacker level 20 vs armor 10, swing always hits // With attacker level 20 vs armor 10, swing always hits
// (rnd(20)+wplus >= (20-20)-10 is always true), so three attacks of // (rnd(20)+wplus >= (20-20)-10 is always true), so three attacks of

View File

@@ -20,7 +20,7 @@ func (g *RogueGame) initPlayer() {
obj = newObject() obj = newObject()
obj.Kind = KindArmor obj.Kind = KindArmor
obj.Which = int(ArmorRingMail) obj.Which = int(ArmorRingMail)
obj.Arm = aClass[ArmorRingMail] - 1 obj.ArmorClass = aClass[ArmorRingMail] - 1
obj.Flags.Set(Known) obj.Flags.Set(Known)
obj.Count = 1 obj.Count = 1
p.CurArmor = obj p.CurArmor = obj

View File

@@ -142,9 +142,9 @@ func (g *RogueGame) status() {
p := &g.Player p := &g.Player
// If nothing has changed since the last status, don't bother. // If nothing has changed since the last status, don't bother.
temp := p.Stats.Arm temp := p.Stats.ArmorClass
if p.CurArmor != nil { 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 && 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 && s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&

View File

@@ -279,10 +279,10 @@ func (g *RogueGame) chgStr(amt int) {
addStr(&p.Stats.Str, amt) addStr(&p.Stats.Str, amt)
comp := p.Stats.Str comp := p.Stats.Str
if p.IsRing(Left, RingAddStrength) { if p.IsRing(Left, RingAddStrength) {
addStr(&comp, -p.CurRing[Left].Arm) addStr(&comp, -p.CurRing[Left].Bonus)
} }
if p.IsRing(Right, RingAddStrength) { if p.IsRing(Right, RingAddStrength) {
addStr(&comp, -p.CurRing[Right].Arm) addStr(&comp, -p.CurRing[Right].Bonus)
} }
if comp > p.MaxStats.Str { if comp > p.MaxStats.Str {
p.MaxStats.Str = comp p.MaxStats.Str = comp

View File

@@ -54,7 +54,7 @@ func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
tp.Stats.Lvl = mp.Stats.Lvl + levAdd tp.Stats.Lvl = mp.Stats.Lvl + levAdd
tp.Stats.MaxHP = g.roll(tp.Stats.Lvl, 8) tp.Stats.MaxHP = g.roll(tp.Stats.Lvl, 8)
tp.Stats.HP = tp.Stats.MaxHP 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.Dmg = mp.Stats.Dmg
tp.Stats.Str = mp.Stats.Str tp.Stats.Str = mp.Stats.Str
tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp) tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp)
@@ -183,10 +183,10 @@ func (g *RogueGame) save(which int) bool {
p := &g.Player p := &g.Player
if which == VsMagic { if which == VsMagic {
if p.IsRing(Left, RingProtection) { if p.IsRing(Left, RingProtection) {
which -= p.CurRing[Left].Arm which -= p.CurRing[Left].Bonus
} }
if p.IsRing(Right, RingProtection) { if p.IsRing(Right, RingProtection) {
which -= p.CurRing[Right].Arm which -= p.CurRing[Right].Bonus
} }
} }
return g.saveThrow(which, &p.Stats) return g.saveThrow(which, &p.Stats)

View File

@@ -241,7 +241,7 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
p.Flags.Clear(Awake) p.Flags.Clear(Awake)
g.msg("a strange white mist envelops you and you fall asleep") g.msg("a strange white mist envelops you and you fall asleep")
case TrapArrow: 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) p.Stats.HP -= g.roll(1, 6)
if p.Stats.HP <= 0 { if p.Stats.HP <= 0 {
g.msg("an arrow killed you") g.msg("an arrow killed you")
@@ -263,7 +263,7 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
g.teleport() g.teleport()
g.mvaddch(tc.Y, tc.X, Trap) g.mvaddch(tc.Y, tc.X, Trap)
case TrapDart: 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") g.msg("a small dart whizzes by your ear and vanishes")
} else { } else {
p.Stats.HP -= g.roll(1, 4) 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). // aren't wearing a magic ring (move.c rust_armor).
func (g *RogueGame) rustArmor(arm *Object) { func (g *RogueGame) rustArmor(arm *Object) {
if arm == nil || arm.Kind != KindArmor || arm.ArmorKind() == ArmorLeather || if arm == nil || arm.Kind != KindArmor || arm.ArmorKind() == ArmorLeather ||
arm.Arm >= 9 { arm.ArmorClass >= 9 {
return return
} }
@@ -331,7 +331,7 @@ func (g *RogueGame) rustArmor(arm *Object) {
g.msg("the rust vanishes instantly") g.msg("the rust vanishes instantly")
} }
} else { } else {
arm.Arm++ arm.ArmorClass++
if !g.Options.Terse { if !g.Options.Terse {
g.msg("your armor appears to be weaker now. Oh my!") g.msg("your armor appears to be weaker now. Oh my!")
} else { } else {

View File

@@ -111,9 +111,9 @@ func (g *RogueGame) putThings() {
if g.Depth >= AmuletLevel && !g.HasAmulet { if g.Depth >= AmuletLevel && !g.HasAmulet {
obj := newObject() obj := newObject()
attachObj(&g.Level.Objects, obj) attachObj(&g.Level.Objects, obj)
obj.Damage = "0x0" obj.Damage = dice("0x0")
obj.HurlDmg = "0x0" obj.HurlDmg = dice("0x0")
obj.Arm = 11 obj.ArmorClass = 11
obj.Kind = KindAmulet obj.Kind = KindAmulet
// Put it somewhere // Put it somewhere
obj.Pos, _ = g.findFloor(nil, 0, false) obj.Pos, _ = g.findFloor(nil, 0, false)

View File

@@ -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 // Object is the _o arm of the C THING union: anything that can lie on the
// floor or ride in a pack. // floor or ride in a pack.
type Object struct { type Object struct {
Kind ObjectKind // what kind of object it is (o_type) Kind ObjectKind // what kind of object it is (o_type)
Pos Coord // where it lives on the screen Pos Coord // where it lives on the screen
Text string // what it says if you read it Text string // what it says if you read it
Launch WeaponKind // what you need to launch it (noWeapon if none) Launch WeaponKind // what you need to launch it (noWeapon if none)
PackCh byte // what character it is in the pack PackCh byte // what character it is in the pack
Damage string // damage if used like sword Damage DiceSpec // damage if used like sword
HurlDmg string // damage if thrown HurlDmg DiceSpec // damage if thrown
Count int // count for plural objects Count int // count for plural objects
Which int // which object of a type it is (index for the Kind's table) Which int // which object of a type it is (index for the Kind's table)
HPlus int // plusses to hit HPlus int // plusses to hit
DPlus int // plusses to damage DPlus int // plusses to damage
Arm int // armor protection — overloaded as in C (see Charges/GoldVal) ArmorClass int // armor protection (armor only)
Flags ObjFlags Charges int // charges remaining (wands and staffs only)
Group int // group number for this object GoldValue int // worth (gold piles only)
Label string // label for object 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 // newObject is list.c new_item() for objects: a zeroed Object with the
@@ -117,18 +120,6 @@ func newObject() *Object {
return &Object{Launch: noWeapon} 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. // PotionKind returns Which as a potion kind; valid only for KindPotion.
func (o *Object) PotionKind() PotionKind { return PotionKind(o.Which) } func (o *Object) PotionKind() PotionKind { return PotionKind(o.Which) }

View File

@@ -250,7 +250,7 @@ func (g *RogueGame) pickUp(ch byte) {
if obj == nil { if obj == nil {
return return
} }
g.money(obj.GoldVal()) g.money(obj.GoldValue)
detachObj(&g.Level.Objects, obj) detachObj(&g.Level.Objects, obj)
p.Room.GoldVal = 0 p.Room.GoldVal = 0
default: default:

View File

@@ -153,19 +153,19 @@ func (g *RogueGame) quaff() {
} }
case PotionRestoreStrength: case PotionRestoreStrength:
if p.IsRing(Left, RingAddStrength) { 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) { 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 { if p.Stats.Str < p.MaxStats.Str {
p.Stats.Str = p.MaxStats.Str p.Stats.Str = p.MaxStats.Str
} }
if p.IsRing(Left, RingAddStrength) { 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) { 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") g.msg("hey, this tastes great. It make you feel warm all over")
case PotionBlindness: case PotionBlindness:
@@ -212,7 +212,7 @@ func (g *RogueGame) doPot(kind PotionKind, knowit bool) {
func (o *Object) isMagic() bool { func (o *Object) isMagic() bool {
switch o.Kind { switch o.Kind {
case KindArmor: case KindArmor:
return o.Flags.Has(Protected) || o.Arm != aClass[o.Which] return o.Flags.Has(Protected) || o.ArmorClass != aClass[o.Which]
case KindWeapon: case KindWeapon:
return o.HPlus != 0 || o.DPlus != 0 return o.HPlus != 0 || o.DPlus != 0
case KindPotion, KindScroll, KindWand, KindRing, KindAmulet: case KindPotion, KindScroll, KindWand, KindRing, KindAmulet:

View File

@@ -49,7 +49,7 @@ func (g *RogueGame) ringOn() {
// Calculate the effect it has on the poor guy. // Calculate the effect it has on the poor guy.
switch obj.RingKind() { switch obj.RingKind() {
case RingAddStrength: case RingAddStrength:
g.chgStr(obj.Arm) g.chgStr(obj.Bonus)
case RingSeeInvisible: case RingSeeInvisible:
g.invisOn() g.invisOn()
case RingAggravateMonsters: case RingAggravateMonsters:
@@ -168,7 +168,7 @@ func ringNum(g *RogueGame, obj *Object) string {
} }
switch obj.RingKind() { switch obj.RingKind() {
case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity: case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity:
return fmt.Sprintf(" [%s]", num(obj.Arm, 0, Ring)) return fmt.Sprintf(" [%s]", num(obj.Bonus, 0, Ring))
} }
return "" return ""
} }

View File

@@ -124,8 +124,8 @@ func (g *RogueGame) totalWinner() {
obj.Flags.Set(Known) obj.Flags.Set(Known)
case KindArmor: case KindArmor:
worth = it.Armors[obj.Which].Worth worth = it.Armors[obj.Which].Worth
worth += (9 - obj.Arm) * 100 worth += (9 - obj.ArmorClass) * 100
worth += 10 * (aClass[obj.Which] - obj.Arm) worth += 10 * (aClass[obj.Which] - obj.ArmorClass)
obj.Flags.Set(Known) obj.Flags.Set(Known)
case KindScroll: case KindScroll:
op := &it.Scrolls[obj.Which] op := &it.Scrolls[obj.Which]
@@ -146,8 +146,8 @@ func (g *RogueGame) totalWinner() {
worth = op.Worth worth = op.Worth
if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage || if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage ||
obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity { obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity {
if obj.Arm > 0 { if obj.Bonus > 0 {
worth += obj.Arm * 100 worth += obj.Bonus * 100
} else { } else {
worth = 10 worth = 10
} }
@@ -160,7 +160,7 @@ func (g *RogueGame) totalWinner() {
case KindWand: case KindWand:
op := &it.Sticks[obj.Which] op := &it.Sticks[obj.Which]
worth = op.Worth worth = op.Worth
worth += 20 * obj.Charges() worth += 20 * obj.Charges
if !obj.Flags.Has(Known) { if !obj.Flags.Has(Known) {
worth /= 2 worth /= 2
} }

View File

@@ -88,7 +88,7 @@ func (g *RogueGame) doRooms() {
if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) { if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) {
gold := newObject() gold := newObject()
rp.GoldVal = g.goldCalc() rp.GoldVal = g.goldCalc()
gold.SetGoldVal(rp.GoldVal) gold.GoldValue = rp.GoldVal
rp.Gold, _ = g.findFloorIn(rp, 0, false) rp.Gold, _ = g.findFloorIn(rp, 0, false)
gold.Pos = rp.Gold gold.Pos = rp.Gold
g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold) g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold)

View File

@@ -13,8 +13,10 @@ import (
// saveFormatVersion identifies the snapshot layout; it changes whenever a // saveFormatVersion identifies the snapshot layout; it changes whenever a
// field rename or retype would make gob silently mis-decode an older // field rename or retype would make gob silently mis-decode an older
// save ("go2": Object.Type became Kind ObjectKind). // save ("go2": Object.Type became Kind ObjectKind; "go3": Object.Arm split
const saveFormatVersion = Release + "-go2" // 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 // destRef encodes a monster's chase target (state.c rs_write_thing's
// (kind, index) pairs). // (kind, index) pairs).

View File

@@ -15,7 +15,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
g.HasAmulet = true g.HasAmulet = true
g.Items.Potions[PotionHealing].Know = true g.Items.Potions[PotionHealing].Know = true
g.Items.Scrolls[ScrollMagicMapping].Guess = "map???" 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 { if len(g.Level.Monsters) > 0 {
g.Level.Monsters[0].Flags.Set(Awake) g.Level.Monsters[0].Flags.Set(Awake)
g.Level.Monsters[0].Dest = &g.Player.Pos g.Level.Monsters[0].Dest = &g.Player.Pos
@@ -48,7 +48,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
if h.Items.Scrolls[ScrollMagicMapping].Guess != "map???" { if h.Items.Scrolls[ScrollMagicMapping].Guess != "map???" {
t.Error("scroll guess lost") 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") t.Error("mutated bestiary lost")
} }
if h.Rng.Seed != g.Rng.Seed { if h.Rng.Seed != g.Rng.Seed {

View File

@@ -42,7 +42,7 @@ func (g *RogueGame) readScroll() {
g.msg("your hands begin to glow %s", g.pickColor("red")) g.msg("your hands begin to glow %s", g.pickColor("red"))
case ScrollEnchantArmor: case ScrollEnchantArmor:
if p.CurArmor != nil { if p.CurArmor != nil {
p.CurArmor.Arm-- p.CurArmor.ArmorClass--
p.CurArmor.Flags.Clear(Cursed) p.CurArmor.Flags.Clear(Cursed)
g.msg("your armor glows %s for a moment", g.pickColor("silver")) g.msg("your armor glows %s for a moment", g.pickColor("silver"))
} }

View File

@@ -16,7 +16,7 @@ func (g *RogueGame) doZap() {
g.msg("you can't zap with that!") g.msg("you can't zap with that!")
return return
} }
if obj.Charges() == 0 { if obj.Charges == 0 {
g.msg("nothing happens") g.msg("nothing happens")
return return
} }
@@ -111,7 +111,7 @@ func (g *RogueGame) doZap() {
g.Items.Sticks[WandMagicMissile].Know = true g.Items.Sticks[WandMagicMissile].Know = true
bolt := newObject() bolt := newObject()
bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon 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.HPlus = 100
bolt.DPlus = 1 bolt.DPlus = 1
bolt.Flags = Missile bolt.Flags = Missile
@@ -167,7 +167,7 @@ func (g *RogueGame) doZap() {
g.Items.Sticks[obj.Which].Know = true g.Items.Sticks[obj.Which].Know = true
case WandNothing: case WandNothing:
} }
obj.SetCharges(obj.Charges() - 1) obj.Charges--
} }
// drain does the drain-hit-points-from-player schtick (sticks.c drain). // 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 := newObject()
bolt.Kind = KindWeapon bolt.Kind = KindWeapon
bolt.Which = int(WeaponFlame) bolt.Which = int(WeaponFlame)
bolt.HurlDmg = "6x6" bolt.HurlDmg = dice("6x6")
bolt.HPlus = 100 bolt.HPlus = 100
bolt.DPlus = 0 bolt.DPlus = 0
g.Items.Weapons[WeaponFlame].Name = name 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). // fixStick sets up a new wand or staff (sticks.c fix_stick).
func (g *RogueGame) fixStick(cur *Object) { func (g *RogueGame) fixStick(cur *Object) {
if g.Items.WandType[cur.Which] == "staff" { if g.Items.WandType[cur.Which] == "staff" {
cur.Damage = "2x3" cur.Damage = dice("2x3")
} else { } else {
cur.Damage = "1x1" cur.Damage = dice("1x1")
} }
cur.HurlDmg = "1x1" cur.HurlDmg = dice("1x1")
switch cur.WandKind() { switch cur.WandKind() {
case WandLight: case WandLight:
cur.SetCharges(g.rnd(10) + 10) cur.Charges = g.rnd(10) + 10
default: 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 "" return ""
} }
if g.Options.Terse { 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)
} }

View File

@@ -6,7 +6,7 @@ package game
// change during play) are cloned into RogueGame.Items by NewGame. // change during play) are cloned into RogueGame.Items by NewGame.
// initStats is the C INIT_STATS: the player's starting statistics. // 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. // aClass is extern.c a_class[]: armor class for each armor type.
var aClass = [NumArmorTypes]int{ var aClass = [NumArmorTypes]int{
@@ -47,32 +47,32 @@ var invTName = []string{"Overwrite", "Slow", "Clear"}
// rolled from the level at creation time. // rolled from the level at creation time.
var monsterTable = [26]MonsterKind{ var monsterTable = [26]MonsterKind{
/* Name CARRY FLAGS str exp lvl arm hp dmg */ /* Name CARRY FLAGS str exp lvl arm hp dmg */
{"aquator", 0, Mean, Stats{10, 20, 5, 2, 1, "0x0/0x0", 0}}, {"aquator", 0, Mean, Stats{10, 20, 5, 2, 1, dice("0x0/0x0"), 0}},
{"bat", 0, Flying, Stats{10, 1, 1, 3, 1, "1x2", 0}}, {"bat", 0, Flying, Stats{10, 1, 1, 3, 1, dice("1x2"), 0}},
{"centaur", 15, 0, Stats{10, 17, 4, 4, 1, "1x2/1x5/1x5", 0}}, {"centaur", 15, 0, Stats{10, 17, 4, 4, 1, dice("1x2/1x5/1x5"), 0}},
{"dragon", 100, Mean, Stats{10, 5000, 10, -1, 1, "1x8/1x8/3x10", 0}}, {"dragon", 100, Mean, Stats{10, 5000, 10, -1, 1, dice("1x8/1x8/3x10"), 0}},
{"emu", 0, Mean, Stats{10, 2, 1, 7, 1, "1x2", 0}}, {"emu", 0, Mean, Stats{10, 2, 1, 7, 1, dice("1x2"), 0}},
{"venus flytrap", 0, Mean, Stats{10, 80, 8, 3, 1, "%%%x0", 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, "4x3/3x5", 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, "1x8", 0}}, {"hobgoblin", 0, Mean, Stats{10, 3, 1, 5, 1, dice("1x8"), 0}},
{"ice monster", 0, 0, Stats{10, 5, 1, 9, 1, "0x0", 0}}, {"ice monster", 0, 0, Stats{10, 5, 1, 9, 1, dice("0x0"), 0}},
{"jabberwock", 70, 0, Stats{10, 3000, 15, 6, 1, "2x12/2x4", 0}}, {"jabberwock", 70, 0, Stats{10, 3000, 15, 6, 1, dice("2x12/2x4"), 0}},
{"kestrel", 0, Mean | Flying, Stats{10, 1, 1, 7, 1, "1x4", 0}}, {"kestrel", 0, Mean | Flying, Stats{10, 1, 1, 7, 1, dice("1x4"), 0}},
{"leprechaun", 0, 0, Stats{10, 10, 3, 8, 1, "1x1", 0}}, {"leprechaun", 0, 0, Stats{10, 10, 3, 8, 1, dice("1x1"), 0}},
{"medusa", 40, Mean, Stats{10, 200, 8, 2, 1, "3x4/3x4/2x5", 0}}, {"medusa", 40, Mean, Stats{10, 200, 8, 2, 1, dice("3x4/3x4/2x5"), 0}},
{"nymph", 100, 0, Stats{10, 37, 3, 9, 1, "0x0", 0}}, {"nymph", 100, 0, Stats{10, 37, 3, 9, 1, dice("0x0"), 0}},
{"orc", 15, Greedy, Stats{10, 5, 1, 6, 1, "1x8", 0}}, {"orc", 15, Greedy, Stats{10, 5, 1, 6, 1, dice("1x8"), 0}},
{"phantom", 0, Invisible, Stats{10, 120, 8, 3, 1, "4x4", 0}}, {"phantom", 0, Invisible, Stats{10, 120, 8, 3, 1, dice("4x4"), 0}},
{"quagga", 0, Mean, Stats{10, 15, 3, 3, 1, "1x5/1x5", 0}}, {"quagga", 0, Mean, Stats{10, 15, 3, 3, 1, dice("1x5/1x5"), 0}},
{"rattlesnake", 0, Mean, Stats{10, 9, 2, 3, 1, "1x6", 0}}, {"rattlesnake", 0, Mean, Stats{10, 9, 2, 3, 1, dice("1x6"), 0}},
{"snake", 0, Mean, Stats{10, 2, 1, 5, 1, "1x3", 0}}, {"snake", 0, Mean, Stats{10, 2, 1, 5, 1, dice("1x3"), 0}},
{"troll", 50, Regenerates | Mean, Stats{10, 120, 6, 4, 1, "1x8/1x8/2x6", 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, "1x9/1x9/2x9", 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, "1x10", 0}}, {"vampire", 20, Regenerates | Mean, Stats{10, 350, 8, 1, 1, dice("1x10"), 0}},
{"wraith", 0, 0, Stats{10, 55, 5, 4, 1, "1x6", 0}}, {"wraith", 0, 0, Stats{10, 55, 5, 4, 1, dice("1x6"), 0}},
{"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, "4x4", 0}}, {"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, dice("4x4"), 0}},
{"yeti", 30, 0, Stats{10, 50, 4, 6, 1, "1x6/1x6", 0}}, {"yeti", 30, 0, Stats{10, 50, 4, 6, 1, dice("1x6/1x6"), 0}},
{"zombie", 0, Mean, Stats{10, 6, 2, 8, 1, "1x8", 0}}, {"zombie", 0, Mean, Stats{10, 6, 2, 8, 1, dice("1x8"), 0}},
} }
// Base ObjInfo tables (extern.c). These are templates: NewGame copies them // Base ObjInfo tables (extern.c). These are templates: NewGame copies them
@@ -323,5 +323,5 @@ var helpStr = []helpEntry{
// the Go save format does not use them. // the Go save format does not use them.
const ( const (
Release = "5.4.4" Release = "5.4.4"
Version = "rogue (sneak.berlin/go/rogue port of rogueforge 5.4.4)" Version = "rogue (git.eeqj.de/sneak/rgoue port of rogueforge 5.4.4)"
) )

View File

@@ -69,11 +69,11 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
case KindArmor: case KindArmor:
sp := it.Armors[which].Name sp := it.Armors[which].Name
if obj.Flags.Has(Known) { 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 { if !g.Options.Terse {
pb.WriteString("protection ") pb.WriteString("protection ")
} }
fmt.Fprintf(&pb, "%d]", 10-obj.Arm) fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass)
} else { } else {
pb.WriteString(sp) pb.WriteString(sp)
} }
@@ -83,7 +83,7 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
case KindAmulet: case KindAmulet:
pb.WriteString("The Amulet of Yendor") pb.WriteString("The Amulet of Yendor")
case KindGold: case KindGold:
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldVal()) fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue)
} }
out := pb.String() out := pb.String()
@@ -168,7 +168,7 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
p.CurRing[hand] = nil p.CurRing[hand] = nil
switch obj.RingKind() { switch obj.RingKind() {
case RingAddStrength: case RingAddStrength:
g.chgStr(-obj.Arm) g.chgStr(-obj.Bonus)
case RingSeeInvisible: case RingSeeInvisible:
g.unsee(0) g.unsee(0)
g.Extinguish(DUnsee) 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). // newThing returns a new random thing for the dungeon (things.c new_thing).
func (g *RogueGame) newThing() *Object { func (g *RogueGame) newThing() *Object {
cur := newObject() cur := newObject()
cur.Damage = "0x0" cur.Damage = dice("0x0")
cur.HurlDmg = "0x0" cur.HurlDmg = dice("0x0")
cur.Arm = 11 cur.ArmorClass = 11
cur.Count = 1 cur.Count = 1
// Decide what kind of object it will be; if we haven't had food for a // 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: case 4:
cur.Kind = KindArmor cur.Kind = KindArmor
cur.Which = pickOne(g, g.Items.Armors[:]) cur.Which = pickOne(g, g.Items.Armors[:])
cur.Arm = aClass[cur.Which] cur.ArmorClass = aClass[cur.Which]
if r := g.rnd(100); r < 20 { if r := g.rnd(100); r < 20 {
cur.Flags.Set(Cursed) cur.Flags.Set(Cursed)
cur.Arm += g.rnd(3) + 1 cur.ArmorClass += g.rnd(3) + 1
} else if r < 28 { } else if r < 28 {
cur.Arm -= g.rnd(3) + 1 cur.ArmorClass -= g.rnd(3) + 1
} }
case 5: case 5:
cur.Kind = KindRing cur.Kind = KindRing
cur.Which = pickOne(g, g.Items.Rings[:]) cur.Which = pickOne(g, g.Items.Rings[:])
switch cur.RingKind() { switch cur.RingKind() {
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage: case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
if cur.Arm = g.rnd(3); cur.Arm == 0 { if cur.Bonus = g.rnd(3); cur.Bonus == 0 {
cur.Arm = -1 cur.Bonus = -1
cur.Flags.Set(Cursed) cur.Flags.Set(Cursed)
} }
case RingAggravateMonsters, RingTeleportation: case RingAggravateMonsters, RingTeleportation:

View File

@@ -374,13 +374,13 @@ type Coord struct {
// Stats describes a fighting being (rogue.h struct stats). // Stats describes a fighting being (rogue.h struct stats).
type Stats struct { type Stats struct {
Str int // strength (s_str; 3..31) Str int // strength (s_str; 3..31)
Exp int // experience Exp int // experience
Lvl int // level of mastery Lvl int // level of mastery
Arm int // armor class ArmorClass int // armor class (s_arm)
HP int // hit points (s_hpt) HP int // hit points (s_hpt)
Dmg string // damage dice, e.g. "1x4/1x2" Dmg DiceSpec // damage dice, e.g. "1x4/1x2"
MaxHP int MaxHP int
} }
// Room describes a room or passage network (rogue.h struct room). // Room describes a room or passage network (rogue.h struct room).

View File

@@ -123,20 +123,20 @@ func (g *RogueGame) wield() {
// initWeaps is the weapons.c init_dam[] table. // initWeaps is the weapons.c init_dam[] table.
var initWeaps = [NumWeaponTypes]struct { var initWeaps = [NumWeaponTypes]struct {
dam string // damage when wielded dam DiceSpec // damage when wielded
hrl string // damage when thrown hrl DiceSpec // damage when thrown
launch WeaponKind // launching weapon launch WeaponKind // launching weapon
flags ObjFlags flags ObjFlags
}{ }{
{"2x4", "1x3", noWeapon, 0}, // WeaponMace {dice("2x4"), dice("1x3"), noWeapon, 0}, // WeaponMace
{"3x4", "1x2", noWeapon, 0}, // Long sword {dice("3x4"), dice("1x2"), noWeapon, 0}, // Long sword
{"1x1", "1x1", noWeapon, 0}, // WeaponBow {dice("1x1"), dice("1x1"), noWeapon, 0}, // WeaponBow
{"1x1", "2x3", WeaponBow, Stackable | Missile}, // WeaponArrow {dice("1x1"), dice("2x3"), WeaponBow, Stackable | Missile}, // WeaponArrow
{"1x6", "1x4", noWeapon, Missile}, // WeaponDagger {dice("1x6"), dice("1x4"), noWeapon, Missile}, // WeaponDagger
{"4x4", "1x2", noWeapon, 0}, // 2h sword {dice("4x4"), dice("1x2"), noWeapon, 0}, // 2h sword
{"1x1", "1x3", noWeapon, Stackable | Missile}, // WeaponDart {dice("1x1"), dice("1x3"), noWeapon, Stackable | Missile}, // WeaponDart
{"1x2", "2x4", noWeapon, Stackable | Missile}, // Shuriken {dice("1x2"), dice("2x4"), noWeapon, Stackable | Missile}, // Shuriken
{"2x3", "1x6", noWeapon, Missile}, // WeaponSpear {dice("2x3"), dice("1x6"), noWeapon, Missile}, // WeaponSpear
} }
// initWeapon sets up a new weapon (weapons.c init_weapon). // initWeapon sets up a new weapon (weapons.c init_weapon).

View File

@@ -38,12 +38,12 @@ func (g *RogueGame) createObj() {
obj.HPlus += g.rnd(3) + 1 obj.HPlus += g.rnd(3) + 1
} }
} else { } else {
obj.Arm = aClass[obj.Which] obj.ArmorClass = aClass[obj.Which]
if bless == '-' { if bless == '-' {
obj.Arm += g.rnd(3) + 1 obj.ArmorClass += g.rnd(3) + 1
} }
if bless == '+' { if bless == '+' {
obj.Arm -= g.rnd(3) + 1 obj.ArmorClass -= g.rnd(3) + 1
} }
} }
case obj.Kind == KindRing: case obj.Kind == KindRing:
@@ -54,9 +54,9 @@ func (g *RogueGame) createObj() {
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
if bless == '-' { if bless == '-' {
obj.Flags.Set(Cursed) obj.Flags.Set(Cursed)
obj.Arm = -1 obj.Bonus = -1
} else { } else {
obj.Arm = g.rnd(2) + 1 obj.Bonus = g.rnd(2) + 1
} }
case RingAggravateMonsters, RingTeleportation: case RingAggravateMonsters, RingTeleportation:
obj.Flags.Set(Cursed) obj.Flags.Set(Cursed)
@@ -67,7 +67,7 @@ func (g *RogueGame) createObj() {
g.msg("how much?") g.msg("how much?")
buf := "" buf := ""
if g.getStr(&buf, g.scr.Std) == Norm { if g.getStr(&buf, g.scr.Std) == Norm {
obj.SetGoldVal(cAtoi(buf)) obj.GoldValue = cAtoi(buf)
} }
} }
g.addPack(obj, false) g.addPack(obj, false)
@@ -169,7 +169,7 @@ func (g *RogueGame) teleport() {
if p.On(Held) { if p.On(Held) {
p.Flags.Clear(Held) p.Flags.Clear(Held)
p.VfHit = 0 p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = "000x0" g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
} }
g.NoMove = 0 g.NoMove = 0
g.Count = 0 g.Count = 0

5
go.mod
View File

@@ -1,10 +1,11 @@
module sneak.berlin/go/rogue module git.eeqj.de/sneak/rgoue
go 1.25.7 go 1.25.7
require github.com/gdamore/tcell/v2 v2.13.10
require ( require (
github.com/gdamore/encoding v1.0.1 // indirect github.com/gdamore/encoding v1.0.1 // indirect
github.com/gdamore/tcell/v2 v2.13.10 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.38.0 // indirect

View File

@@ -11,7 +11,7 @@ import (
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"sneak.berlin/go/rogue/game" "git.eeqj.de/sneak/rgoue/game"
) )
// Tcell renders game Windows on a tcell screen and turns key events into // Tcell renders game Windows on a tcell screen and turns key events into