.golangci.yml is the prompts-repo standard verbatim plus one approved
exception (paralleltest, sneak 2026-07-06). Roughly 1,500 findings
fixed:
- autofix sweep (wsl_v5/nlreturn/intrange/modernize formatting), with
the misspell autofix REVERTED where it rewrote authentic C game text
("missle vanishes" stays, with nolint and a comment)
- error handling: errcheck sites get real handling — saveFile now
closes explicitly and removes corrupt saves, scoreboard writes are
documented best-effort, err113 sentinel errors (ErrSaveOutOfDate,
ErrScreenTooSmall); panics allowed for unrecoverable states per
MEMORY.md policy
- gosec: real fixes (ParseInt for SEED, 0600 scorefile) and justified
per-line nolints for provably-bounded conversions (randomMonsterLetter
helper collapses eight rnd(26)+'A' sites)
- API tidying from linters: pointer receivers on flag types
(recvcheck), msg helpers renamed addmsgf/doaddf/Printwf/MvPrintwf
(goprintffuncname), myExit()/findFloor(monst)/mkGameInput(t) drop
always-constant params (unparam), gameEnd carries no status
- gocritic/staticcheck: if-else chains to switches, the pack.c
inventory filter untangled into matchesFilter, main.go split so
defers run before exit (exitAfterDefer, funlen)
- revive doc comments on all exported flag types/consts/methods
Remaining findings are confined to linters pending sneak's exception
decision (mnd, gochecknoglobals, cyclop, nestif, gocognit, exhaustive,
goconst, testpackage); TODO.md records the state. MEMORY.md added at
repo root as the project's agent working notes.
226 lines
4.9 KiB
Go
226 lines
4.9 KiB
Go
package game
|
|
|
|
// monsters.c — monster creation and saving throws.
|
|
|
|
// lvlMons and wandMons list monsters in rough order of vorpalness; zero
|
|
// entries in wandMons never wander (monsters.c).
|
|
var lvlMons = [26]byte{
|
|
'K', 'E', 'B', 'S', 'H', 'I', 'R', 'O', 'Z', 'L', 'C', 'Q', 'A',
|
|
'N', 'Y', 'F', 'T', 'W', 'P', 'X', 'U', 'M', 'V', 'G', 'J', 'D',
|
|
}
|
|
|
|
var wandMons = [26]byte{
|
|
'K', 'E', 'B', 'S', 'H', 0, 'R', 'O', 'Z', 0, 'C', 'Q', 'A',
|
|
0, 'Y', 0, 'T', 'W', 'P', 0, 'U', 'M', 'V', 'G', 'J', 0,
|
|
}
|
|
|
|
// randMonster picks a monster to show up; the lower the level, the meaner
|
|
// the monster (monsters.c randmonster).
|
|
func (g *RogueGame) randMonster(wander bool) byte {
|
|
mons := &lvlMons
|
|
if wander {
|
|
mons = &wandMons
|
|
}
|
|
|
|
for {
|
|
d := g.Depth + (g.rnd(10) - 6)
|
|
if d < 0 {
|
|
d = g.rnd(5)
|
|
}
|
|
|
|
if d > 25 {
|
|
d = g.rnd(5) + 21
|
|
}
|
|
|
|
if mons[d] != 0 {
|
|
return mons[d]
|
|
}
|
|
}
|
|
}
|
|
|
|
// newMonster picks a new monster and adds it to the list (monsters.c
|
|
// new_monster).
|
|
func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
|
|
levAdd := max(g.Depth-AmuletLevel, 0)
|
|
|
|
attachMon(&g.Level.Monsters, tp)
|
|
tp.Type = typ
|
|
tp.Disguise = typ
|
|
tp.Pos = cp
|
|
g.move(cp.Y, cp.X)
|
|
tp.OldCh = g.inch()
|
|
tp.Room = g.roomin(cp)
|
|
g.Level.SetMonsterAt(cp.Y, cp.X, tp)
|
|
mp := &g.Monsters[tp.Type-'A']
|
|
tp.Stats.Lvl = mp.Stats.Lvl + levAdd
|
|
tp.Stats.MaxHP = g.roll(tp.Stats.Lvl, 8)
|
|
tp.Stats.HP = tp.Stats.MaxHP
|
|
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)
|
|
|
|
tp.Flags = mp.Flags
|
|
if g.Depth > 29 {
|
|
tp.Flags.Set(Hasted)
|
|
}
|
|
|
|
tp.Turn = true
|
|
tp.Pack = nil
|
|
|
|
if g.Player.IsWearing(RingAggravateMonsters) {
|
|
g.runto(cp)
|
|
}
|
|
|
|
if typ == 'X' {
|
|
tp.Disguise = g.rndThing()
|
|
}
|
|
}
|
|
|
|
// expAdd is the experience to add for this monster's level/hit points
|
|
// (monsters.c exp_add).
|
|
func expAdd(tp *Monster) int {
|
|
var mod int
|
|
if tp.Stats.Lvl == 1 {
|
|
mod = tp.Stats.MaxHP / 8
|
|
} else {
|
|
mod = tp.Stats.MaxHP / 6
|
|
}
|
|
|
|
if tp.Stats.Lvl > 9 {
|
|
mod *= 20
|
|
} else if tp.Stats.Lvl > 6 {
|
|
mod *= 4
|
|
}
|
|
|
|
return mod
|
|
}
|
|
|
|
// wanderer creates a new wandering monster and aims it at the player
|
|
// (monsters.c wanderer).
|
|
func (g *RogueGame) wanderer() {
|
|
tp := &Monster{}
|
|
|
|
var cp Coord
|
|
for {
|
|
cp, _ = g.findFloor(true)
|
|
if g.roomin(cp) != g.Player.Room {
|
|
break
|
|
}
|
|
}
|
|
|
|
g.newMonster(tp, g.randMonster(true), cp)
|
|
|
|
if g.Player.On(SenseMonsters) {
|
|
g.standout()
|
|
|
|
if !g.Player.On(Hallucinating) {
|
|
g.addch(tp.Type)
|
|
} else {
|
|
g.addch(g.randomMonsterLetter())
|
|
}
|
|
|
|
g.standend()
|
|
}
|
|
|
|
g.runto(tp.Pos)
|
|
}
|
|
|
|
// wakeMonster is what to do when the hero steps next to a monster
|
|
// (monsters.c wake_monster).
|
|
func (g *RogueGame) wakeMonster(y, x int) *Monster {
|
|
p := &g.Player
|
|
|
|
tp := g.Level.MonsterAt(y, x)
|
|
if tp == nil {
|
|
panic("can't find monster in wake_monster")
|
|
}
|
|
|
|
ch := tp.Type
|
|
// Every time he sees a mean monster, it might start chasing him
|
|
if !tp.On(Awake) && g.rnd(3) != 0 && tp.On(Mean) && !tp.On(Held) &&
|
|
!p.IsWearing(RingStealth) && !p.On(Levitating) {
|
|
tp.Dest = &p.Pos
|
|
tp.Flags.Set(Awake)
|
|
}
|
|
|
|
if ch == 'M' && !p.On(Blind) && !p.On(Hallucinating) &&
|
|
!tp.On(Found) && !tp.On(Cancelled) && tp.On(Awake) {
|
|
rp := p.Room
|
|
if (rp != nil && !rp.Flags.Has(Dark)) ||
|
|
distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
|
tp.Flags.Set(Found)
|
|
|
|
if !g.save(VsMagic) {
|
|
if p.On(Confused) {
|
|
g.Lengthen(DUnconfuse, g.spread(HuhDuration))
|
|
} else {
|
|
g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After)
|
|
}
|
|
|
|
p.Flags.Set(Confused)
|
|
|
|
mname := g.setMname(tp)
|
|
g.addmsgf("%s", mname)
|
|
|
|
if mname != "it" {
|
|
g.addmsgf("'")
|
|
}
|
|
|
|
g.msg("s gaze has confused you")
|
|
}
|
|
}
|
|
}
|
|
// Let greedy ones guard gold
|
|
if tp.On(Greedy) && !tp.On(Awake) {
|
|
tp.Flags.Set(Awake)
|
|
|
|
if p.Room.GoldVal != 0 {
|
|
tp.Dest = &p.Room.Gold
|
|
} else {
|
|
tp.Dest = &p.Pos
|
|
}
|
|
}
|
|
|
|
return tp
|
|
}
|
|
|
|
// givePack gives a pack to a monster if it deserves one (monsters.c
|
|
// give_pack).
|
|
func (g *RogueGame) givePack(tp *Monster) {
|
|
if g.Depth >= g.MaxDepth && g.rnd(100) < g.Monsters[tp.Type-'A'].Carry {
|
|
attachObj(&tp.Pack, g.newThing())
|
|
}
|
|
}
|
|
|
|
// saveThrow sees if a creature saves against something (monsters.c
|
|
// save_throw).
|
|
func (g *RogueGame) saveThrow(which int, st *Stats) bool {
|
|
need := 14 + which - st.Lvl/2
|
|
|
|
return g.roll(1, 20) >= need
|
|
}
|
|
|
|
// save sees if the hero saves against various nasty things (monsters.c
|
|
// save).
|
|
func (g *RogueGame) save(which int) bool {
|
|
p := &g.Player
|
|
if which == VsMagic {
|
|
if p.IsRing(Left, RingProtection) {
|
|
which -= p.CurRing[Left].Bonus
|
|
}
|
|
|
|
if p.IsRing(Right, RingProtection) {
|
|
which -= p.CurRing[Right].Bonus
|
|
}
|
|
}
|
|
|
|
return g.saveThrow(which, &p.Stats)
|
|
}
|
|
|
|
// randomMonsterLetter picks a random monster display letter, used by the
|
|
// hallucination effects (the C rnd(26)+'A' idiom).
|
|
func (g *RogueGame) randomMonsterLetter() byte {
|
|
return byte(g.rnd(26) + 'A') //nolint:gosec // G115: 'A'..'Z' fits a byte
|
|
}
|