Files
rgoue/game/monsters.go
sneak 0b56ac8019 Extract MessageLine, Player pack ops, and Level list management
Refactor step 6. MessageLine (was MsgLine) owns the msg/addmsg/endmsg
machinery, wired to its screen, pre---More-- redraw, and input via
attach(); RogueGame keeps one-line msg/addmsgf/endmsg shorthands so
the ~400 call sites are unchanged. Player gains nextPackChar and
removeFromPack (the state half of pack.c leave_pack); leavePack keeps
only the LastPick repeat-command tracking. Level gains ObjectAt
(misc.c find_obj) and AddObject/RemoveObject/AddMonster/RemoveMonster,
replacing direct attach/detach calls on the level lists. Inventory and
pickup UI flows stay on RogueGame: display and orchestration, not
state surgery. Behavior and RNG order unchanged; suite green.
2026-07-07 02:42:18 +02:00

214 lines
4.4 KiB
Go

package game
// monsters.c — monster creation and saving throws.
// 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 := &g.data.lvlMons
if wander {
mons = &g.data.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)
g.Level.AddMonster(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
}