From 7d7b98f0ae3ad6411da631c06a660d0a0cece83e Mon Sep 17 00:00:00 2001 From: PatR Date: Sat, 27 Jun 2020 18:15:19 -0700 Subject: [PATCH] mhpmax of life-drained monsters The report about problems after stone-to-flesh on a petrified long worm included stethoscope feedback of 0(-1) hit points, after life-draining. I was unable to reproduce a maximum hp of -1 and hope that it was a side-effect of the [already fixed] stale mon->wormno value used when resurrecting the long worm. Anyway, this changes life-draining to never take mon->hpmax below mon->m_lev + 1 (the +1 is needed to cope with m_lev==0 monsters). The same limit is also applied to monster life-saving but more to avoid replicating the arbitrary minimum of 10 (four instances) then because it might be less than m_lev+1 somehow. Sanity checking now tests whether a monster's max HP is less than its level + 1 so if there are ways other than life-drain attacks for it to drop that low, the fuzzer will choke. The new check also tests whether a monster's current HP is greater than max HP. Polymophred hero killing a golem or vortex by vampire bite reported " dies." Give an alternate message since those aren't alive. --- src/artifact.c | 41 +++++++++++++++++++++++++---------------- src/mhitm.c | 24 +++++++++++++++++------- src/mhitu.c | 7 +++++-- src/mkobj.c | 13 ++++++++++++- src/mon.c | 45 ++++++++++++++++++++++++++++++++++++--------- src/uhitm.c | 31 +++++++++++++++++++++---------- src/zap.c | 21 ++++++++++++++++++--- 7 files changed, 134 insertions(+), 48 deletions(-) diff --git a/src/artifact.c b/src/artifact.c index 77eced018..04f7a33f1 100644 --- a/src/artifact.c +++ b/src/artifact.c @@ -1,4 +1,4 @@ -/* NetHack 3.6 artifact.c $NHDT-Date: 1581886858 2020/02/16 21:00:58 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.153 $ */ +/* NetHack 3.6 artifact.c $NHDT-Date: 1593306896 2020/06/28 01:14:56 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.157 $ */ /* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */ /*-Copyright (c) Robert Patrick Rankin, 2013. */ /* NetHack may be freely redistributed. See license for details. */ @@ -1337,11 +1337,21 @@ int dieroll; /* needed for Magicbane and vorpal blades */ } } if (spec_ability(otmp, SPFX_DRLI)) { - /* some non-living creatures (golems, vortices) are - vulnerable to life drain effects */ + /* some non-living creatures (golems, vortices) are vulnerable to + life drain effects so can get " draws the " feedback */ const char *life = nonliving(mdef->data) ? "animating force" : "life"; if (!youdefend) { + int m_lev = (int) mdef->m_lev, /* will be 0 for 1d4 mon */ + mhpmax = mdef->mhpmax, + drain = monhp_per_lvl(mdef); /* usually 1d8 */ + /* note: DRLI attack uses 2d6, attacker doesn't get healed */ + + /* stop draining HP if it drops too low (still drains level; + also caller still inflicts regular weapon damage) */ + if (mhpmax - drain <= m_lev) + drain = (mhpmax > m_lev) ? (mhpmax - (m_lev + 1)) : 0; + if (vis) { if (otmp->oartifact == ART_STORMBRINGER) pline_The("%s blade draws the %s from %s!", @@ -1355,21 +1365,20 @@ int dieroll; /* needed for Magicbane and vorpal blades */ /* losing a level when at 0 is fatal */ *dmgptr = 2 * mdef->mhp + FATAL_DAMAGE_MODIFIER; } else { - int drain = monhp_per_lvl(mdef); - *dmgptr += drain; mdef->mhpmax -= drain; mdef->m_lev--; - drain /= 2; - if (drain) { - /* attacker heals in proportion to amount drained */ - if (youattack) { - healup(drain, 0, FALSE, FALSE); - } else { - magr->mhp += drain; - if (magr->mhp > magr->mhpmax) - magr->mhp = magr->mhpmax; - } + } + + if (drain > 0) { + /* drain: was target's damage, now heal attacker by half */ + drain = (drain + 1) / 2; /* drain/2 rounded up */ + if (youattack) { + healup(drain, 0, FALSE, FALSE); + } else { + magr->mhp += drain; + if (magr->mhp > magr->mhpmax) + magr->mhp = magr->mhpmax; } } return vis; @@ -1389,7 +1398,7 @@ int dieroll; /* needed for Magicbane and vorpal blades */ life); losexp("life drainage"); if (magr && magr->mhp < magr->mhpmax) { - magr->mhp += (oldhpmax - u.uhpmax) / 2; + magr->mhp += (oldhpmax - u.uhpmax + 1) / 2; if (magr->mhp > magr->mhpmax) magr->mhp = magr->mhpmax; } diff --git a/src/mhitm.c b/src/mhitm.c index 5618c5df0..b61efcce2 100644 --- a/src/mhitm.c +++ b/src/mhitm.c @@ -1,4 +1,4 @@ -/* NetHack 3.6 mhitm.c $NHDT-Date: 1583608838 2020/03/07 19:20:38 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.132 $ */ +/* NetHack 3.6 mhitm.c $NHDT-Date: 1593306906 2020/06/28 01:15:06 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.137 $ */ /* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */ /*-Copyright (c) Robert Patrick Rankin, 2011. */ /* NetHack may be freely redistributed. See license for details. */ @@ -1286,22 +1286,32 @@ int dieroll; } if (!tele_restrict(magr)) { boolean couldspot = canspotmon(magr); + (void) rloc(magr, TRUE); if (g.vis && couldspot && !canspotmon(magr)) pline("%s suddenly disappears!", buf); } break; - case AD_DRLI: + case AD_DRLI: /* drain life */ if (!cancelled && !rn2(3) && !resists_drli(mdef)) { - tmp = d(2, 6); + tmp = d(2, 6); /* Stormbringer uses monhp_per_lvl(usually 1d8) */ if (g.vis && canspotmon(mdef)) - pline("%s suddenly seems weaker!", Monnam(mdef)); - mdef->mhpmax -= tmp; - if (mdef->m_lev == 0) + pline("%s becomes weaker!", Monnam(mdef)); + if (mdef->mhpmax - tmp > (int) mdef->m_lev) { + mdef->mhpmax -= tmp; + } else { + /* limit floor of mhpmax reduction to current m_lev + 1; + avoid increasing it if somehow already less than that */ + if (mdef->mhpmax > (int) mdef->m_lev) + mdef->mhpmax = (int) mdef->m_lev + 1; + } + if (mdef->m_lev == 0) /* automatic kill if drained past level 0 */ tmp = mdef->mhp; else mdef->m_lev--; - /* Automatic kill if drained past level 0 */ + + /* unlike hitting with Stormbringer, wounded attacker doesn't + heal any from the drained life */ } break; case AD_SSEX: diff --git a/src/mhitu.c b/src/mhitu.c index 29d257294..aaf3fb280 100644 --- a/src/mhitu.c +++ b/src/mhitu.c @@ -1,4 +1,4 @@ -/* NetHack 3.6 mhitu.c $NHDT-Date: 1586913203 2020/04/15 01:13:23 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.187 $ */ +/* NetHack 3.6 mhitu.c $NHDT-Date: 1593306907 2020/06/28 01:15:07 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.192 $ */ /* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */ /*-Copyright (c) Robert Patrick Rankin, 2012. */ /* NetHack may be freely redistributed. See license for details. */ @@ -1232,10 +1232,13 @@ register struct attack *mattk; } } break; - case AD_DRLI: + case AD_DRLI: /* drain life */ hitmsg(mtmp, mattk); if (uncancelled && !rn2(3) && !Drain_resistance) { losexp("life drainage"); + + /* unlike hitting with Stormbringer, wounded attacker doesn't + heal any from the drained life */ } break; case AD_LEGS: { diff --git a/src/mkobj.c b/src/mkobj.c index 4ef6852f2..b439461b4 100644 --- a/src/mkobj.c +++ b/src/mkobj.c @@ -1,4 +1,4 @@ -/* NetHack 3.6 mkobj.c $NHDT-Date: 1591178399 2020/06/03 09:59:59 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.180 $ */ +/* NetHack 3.6 mkobj.c $NHDT-Date: 1593306908 2020/06/28 01:15:08 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.181 $ */ /* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */ /*-Copyright (c) Derek S. Ray, 2015. */ /* NetHack may be freely redistributed. See license for details. */ @@ -1583,6 +1583,7 @@ struct monst *mtmp; if (!has_omonst(obj)) newomonst(obj); if (has_omonst(obj)) { + int baselevel = mtmp->data->mlevel; struct monst *mtmp2 = OMONST(obj); *mtmp2 = *mtmp; @@ -1600,6 +1601,16 @@ struct monst *mtmp; /* if mtmp is a long worm with segments, its saved traits will be one without any segments */ mtmp2->wormno = 0; + /* mtmp might have been killed by repeated life draining; make sure + mtmp2 can survive if revived ('baselevel' will be 0 for 1d4 mon) */ + if (mtmp2->mhpmax <= baselevel) + mtmp2->mhpmax = baselevel + 1; + /* mtmp is assumed to be dead but we don't kill it or its saved + traits, just force those to have a sane value for current HP */ + if (mtmp2->mhp > mtmp2->mhpmax) + mtmp2->mhp = mtmp2->mhpmax; + if (mtmp2->mhp < 1) + mtmp2->mhp = 0; } return obj; } diff --git a/src/mon.c b/src/mon.c index f9b5fd6d4..96b12736a 100644 --- a/src/mon.c +++ b/src/mon.c @@ -1,4 +1,4 @@ -/* NetHack 3.6 mon.c $NHDT-Date: 1591017419 2020/06/01 13:16:59 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.337 $ */ +/* NetHack 3.6 mon.c $NHDT-Date: 1593306909 2020/06/28 01:15:09 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.338 $ */ /* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */ /*-Copyright (c) Derek S. Ray, 2015. */ /* NetHack may be freely redistributed. See license for details. */ @@ -21,6 +21,7 @@ static struct permonst *FDECL(accept_newcham_form, (struct monst *, int)); static struct obj *FDECL(make_corpse, (struct monst *, unsigned)); static int FDECL(minliquid_core, (struct monst *)); static void FDECL(m_detach, (struct monst *, struct permonst *)); +static void FDECL(set_mon_min_mhpmax, (struct monst *, int)); static void FDECL(lifesaved_monster, (struct monst *)); static void FDECL(migrate_mon, (struct monst *, XCHAR_P, XCHAR_P)); static boolean FDECL(ok_to_obliterate, (struct monst *)); @@ -55,6 +56,16 @@ const char *msg; mtmp->mnum, mndx, msg); mtmp->mnum = mndx; } + /* check before DEADMONSTER() because dead monsters should still + have sane mhpmax */ + if (mtmp->mhpmax < 1 + || mtmp->mhpmax < (int) mtmp->m_lev + 1 + || mtmp->mhp > mtmp->mhpmax) + impossible( + "%s: level %d monster #%u [%s] has %d cur HP, %d max HP", + msg, (int) mtmp->m_lev, + mtmp->m_id, fmt_ptr((genericptr_t) mtmp), + mtmp->mhp, mtmp->mhpmax); if (DEADMONSTER(mtmp)) { #if 0 /* bad if not fmons list or if not vault guard */ @@ -2007,6 +2018,26 @@ struct permonst *mptr; /* reflects mtmp->data _prior_ to mtmp's death */ iflags.purge_monsters++; } +/* give a life-saved monster a reasonable mhpmax value in case it has + been the victim of excessive life draining */ +static void +set_mon_min_mhpmax(mon, minimum_mhpmax) +struct monst *mon; +int minimum_mhpmax; /* monster life-saving has traditionally used 10 */ +{ + /* can't be less than m_lev+1 (if we just used m_lev itself, level 0 + monsters would end up allowing a minimum of 0); since life draining + reduces m_lev, this usually won't give the monster much of a boost */ + if (mon->mhpmax < (int) mon->m_lev + 1) + mon->mhpmax = (int) mon->m_lev + 1; + /* caller can specify an alternate minimum; we'll honor it iff it is + greater than m_lev+1; the traditional arbitrary value of 10 always + gives level 0 and level 1 monsters a boost and has a moderate + chance of doing so for level 2, a tiny chance for levels 3..9 */ + if (mon->mhpmax < minimum_mhpmax) + mon->mhpmax = minimum_mhpmax; +} + /* find the worn amulet of life saving which will save a monster */ struct obj * mlifesaver(mon) @@ -2057,8 +2088,7 @@ struct monst *mtmp; if (mtmp->mtame && !mtmp->isminion) { wary_dog(mtmp, !surviver); } - if (mtmp->mhpmax <= 0) - mtmp->mhpmax = 10; + set_mon_min_mhpmax(mtmp, 10); /* mtmp->mhpmax=max(mtmp->m_lev+1,10) */ mtmp->mhp = mtmp->mhpmax; if (!surviver) { @@ -2115,8 +2145,7 @@ register struct monst *mtmp; spec_death ? "reconstitutes" : "transforms"); mtmp->mcanmove = 1; mtmp->mfrozen = 0; - if (mtmp->mhpmax <= 0) - mtmp->mhpmax = 10; + set_mon_min_mhpmax(mtmp, 10); /* mtmp->mhpmax=max(m_lev+1,10) */ mtmp->mhp = mtmp->mhpmax; /* mtmp==u.ustuck can happen if previously a fog cloud or poly'd hero is hugging a vampire bat */ @@ -2731,8 +2760,7 @@ struct monst *mtmp; surface(x,y)); mtmp->mcanmove = 1; mtmp->mfrozen = 0; - if (mtmp->mhpmax <= 0) - mtmp->mhpmax = 10; + set_mon_min_mhpmax(mtmp, 10); /* mtmp->mhpmax=max(m_lev+1,10) */ mtmp->mhp = mtmp->mhpmax; /* this can happen if previously a fog cloud */ if (u.uswallow && (mtmp == u.ustuck)) @@ -2766,8 +2794,7 @@ struct monst *mtmp; they revert to innate shape rather than become a statue */ mtmp->mcanmove = 1; mtmp->mfrozen = 0; - if (mtmp->mhpmax <= 0) - mtmp->mhpmax = 10; + set_mon_min_mhpmax(mtmp, 10); /* mtmp->mhpmax=max(mtmp->m_lev+1,10) */ mtmp->mhp = mtmp->mhpmax; (void) newcham(mtmp, &mons[mtmp->cham], FALSE, TRUE); newsym(mtmp->mx, mtmp->my); diff --git a/src/uhitm.c b/src/uhitm.c index 0f7f2eba8..59e32e61c 100644 --- a/src/uhitm.c +++ b/src/uhitm.c @@ -1,4 +1,4 @@ -/* NetHack 3.6 uhitm.c $NHDT-Date: 1591017421 2020/06/01 13:17:01 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.236 $ */ +/* NetHack 3.6 uhitm.c $NHDT-Date: 1593306911 2020/06/28 01:15:11 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.237 $ */ /* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */ /*-Copyright (c) Robert Patrick Rankin, 2012. */ /* NetHack may be freely redistributed. See license for details. */ @@ -1845,21 +1845,32 @@ int specialdmg; /* blessed and/or silver bonus against various things */ } tmp = 0; break; - case AD_DRLI: + case AD_DRLI: /* drain life */ if (!negated && !rn2(3) && !resists_drli(mdef)) { - int xtmp = d(2, 6); - - pline("%s suddenly seems weaker!", Monnam(mdef)); - mdef->mhpmax -= xtmp; - mdef->mhp -= xtmp; + tmp = d(2, 6); /* Stormbringer uses monhp_per_lvl(usually 1d8) */ + pline("%s becomes weaker!", Monnam(mdef)); + if (mdef->mhpmax - tmp > (int) mdef->m_lev) { + mdef->mhpmax -= tmp; + } else { + /* limit floor of mhpmax reduction to current m_lev + 1; + avoid increasing it if somehow already less than that */ + if (mdef->mhpmax > (int) mdef->m_lev) + mdef->mhpmax = (int) mdef->m_lev + 1; + } + mdef->mhp -= tmp; /* !m_lev: level 0 monster is killed regardless of hit points - rather than drop to level -1 */ + rather than drop to level -1; note: some non-living creatures + (golems, vortices) are subject to life-drain */ if (DEADMONSTER(mdef) || !mdef->m_lev) { - pline("%s dies!", Monnam(mdef)); + pline("%s %s!", Monnam(mdef), + nonliving(mdef->data) ? "expires" : "dies"); xkilled(mdef, XKILL_NOMSG); } else mdef->m_lev--; - tmp = 0; + tmp = 0; /* damage has already been inflicted */ + + /* unlike hitting with Stormbringer, wounded hero doesn't + heal any from the drained life */ } break; case AD_RUST: diff --git a/src/zap.c b/src/zap.c index 950949d35..8cd10ba2f 100644 --- a/src/zap.c +++ b/src/zap.c @@ -1,4 +1,4 @@ -/* NetHack 3.6 zap.c $NHDT-Date: 1591219976 2020/06/03 21:32:56 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.342 $ */ +/* NetHack 3.6 zap.c $NHDT-Date: 1593306912 2020/06/28 01:15:12 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.343 $ */ /* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */ /*-Copyright (c) Robert Patrick Rankin, 2013. */ /* NetHack may be freely redistributed. See license for details. */ @@ -600,8 +600,23 @@ boolean adjacentok; /* False: at obj's spot only, True: nearby is allowed */ return (struct monst *) 0; } - /* heal the monster */ - if (mtmp->mhpmax > mtmp2->mhpmax && is_rider(mtmp2->data)) + /* heal the monster; lower than normal level might come from + adj_lev() but we assume it has come from 'mtmp' being level + drained before finally killed; give a chance to restore + some levels so that trolls and Riders can't be drained to + level 0 and then trivially killed repeatedly */ + if ((int) mtmp->m_lev < mtmp->data->mlevel) { + int ltmp = rnd(mtmp->data->mlevel + 1); + + if (ltmp > (int) mtmp->m_lev) { + while ((int) mtmp->m_lev < ltmp) { + mtmp->m_lev++; + mtmp->mhpmax += monhp_per_lvl(mtmp); + } + mtmp2->m_lev = mtmp->m_lev; + } + } + if (mtmp->mhpmax > mtmp2->mhpmax) /* &&is_rider(mtmp2->data)*/ mtmp2->mhpmax = mtmp->mhpmax; mtmp2->mhp = mtmp2->mhpmax; /* Get these ones from mtmp */