Refactor, unify, and nerf item destruction

Note: Original change is from xNetHack by copperwater <aosdict@gmail.com>,
      but this commit comes from HACKEM-MUCHE by Erik Lunna, with
      some minor code formatting.

From xNetHack commit a0a6103bea:

'The original goal: nerf item destruction using a method I initially
 proposed for SpliceHack, in which the number of items subject to
 damage from any single source is limited by the amount of damage the
 effect caused. The intent was to be more fair all around and prevent
 aggravating situations where, for instance, a chest shock trap zaps
 you for 4 damage and immediately ten of your rings and wands blow up.

 Problem 1: no easy way to limit the items destroyed without biasing
 heavily towards the start of the invent chain. The old code was able
 to get away without bias by just indiscriminately destroying
 everything eligible with a 1/3 chance. Here, I had to introduce
 reservoir sampling in a somewhat more complex form than I've applied
 it elsewhere, since there are a pool of potential items.

 Problem 2: destroy_item no longer worked remotely like destroy_mitem,
 which still destroyed 1/3 of items indiscriminately. Commence the
 process of squishing them into one function that handles both the
 player and monsters. (Which required making a lot of adjustments to
 destroy_one_item, now named maybe_destroy_item, on nits such as
 messaging and when to negate damage. An annoying consequence of the
 merge is that in the player case, their HP is deducted and they can
 be killed directly, but for monsters they need to add up the
 destruction damage and return it.)

 Unifying destroy_item and destroy_mitem has some advantages: in
 addition to the obvious code duplication removal, it ensures monsters
 now take the same damage as players for destruction (previously they
 took a piddly 1 damage per destroyed item). Now when you hit
 something with Mjollnir and their coveted wand of death breaks apart
 and explodes, you at least get the satisfaction of knowing they took
 the standard amount of damage from it.  Monsters also now get
 symmetry with players in having extrinsic elemental resistance
 protect them from item destruction, and damage negation from item
 destruction if they were appropriately resistant.

 Problem 3: a lot of callers didn't preserve the "amount of incoming
 damage" that this refactor relies on. E.g. if the defender resisted
 that element, the local dmg variable would be set to 0. So I had to
 do some wrangling with callers to save that original damage
 value. The rule of thumb is: all *incoming* damage counts. So that
 includes the player's spellcasting bonus if applicable, but not
 things like half damage, negation due to resistance, or extra damage
 due to being vulnerable to cold/fire.

 Then I figured, while I'm here let's get rid of all those silly cases
 where destroy_items is called multiple times for various different
 object classes, and cut the object class parameter out of it. This
 has a few minor effects:

 - Places where different object classes previously rolled
   independently for destruction to happen at all now roll
   once. (Which, by my calculation, generally means less incidences of
   destruction - a fire attack now won't have three separate chances
   to hit your scrolls, potions, and spellbooks. On the flip side, a
   lucky roll will no longer save an entire object class in your
   inventory.)

 - Callers can no longer specify different probabilities for
   destroying different object classes. The only place this was really
   used was to call destroy_item with a slightly lower probability on
   SPBOOK_CLASS.  With the nerf in this commit, less of them ought to
   be destroyed anyway.

 - A very edge case of where explosion-vs-monster damage was totted up
   differently for golems, which could result in differences of a hit
   point here or there.

 - All object classes being processed in one go means that less items
   are destroyed than would be if they were still processed
   independently.  This is not really visible compared to the old
   baseline of just destroying 33% of everything, but would be a
   marked difference versus a copy of the game that still called
   destroy_items separately for different object classes. To
   compensate, I adjusted my planned damage-to-destruction-limit
   scaling factor down from 8 to 5.

 Not done: merging in ignite_items(), though that would probably be
 really easy now.'

Notes from porting from xNetHack:

- It might be necessary to reexamine at all the conditional checks for
calling destroy_items. Because item destruction is much more
restrained and uses the actual damage from an effect, we might now
need to check 'if (!rn2(3))' and similar in all the places item
destruction occurs.
This commit is contained in:
Erik Lunna
2024-02-20 22:03:54 +02:00
committed by Pasi Kallinen
parent fc100f269c
commit eb22a81088
13 changed files with 339 additions and 371 deletions
+254 -267
View File
@@ -35,7 +35,8 @@ static int zap_ok(struct obj *) NO_NNARGS;
* boxlock_invent() calls boxlock() which has nonnull arg. */
static void boxlock_invent(struct obj *) NONNULLARG1;
static int spell_hit_bonus(int);
static void destroy_one_item(struct obj *, int, int) NONNULLARG1;
static int maybe_destroy_item(struct monst *, struct obj *, int) NONNULLPTRS;
static void wishcmdassist(int);
#define ZT_MAGIC_MISSILE (AD_MAGM - 1)
@@ -2071,10 +2072,9 @@ bhito(struct obj *obj, struct obj *otmp)
* menu_drop(), askchain() - inventory traversal where multiple
* Drop can alter the invent chain while traversal
* is in progress (bhito isn't involved).
* destroy_item(), destroy_mitem() - inventory traversal where
* item destruction can trigger drop or destruction of
* other item(s) and alter the invent or mon->minvent
* chain, possibly recursively.
* destroy_items() - inventory traversal where item destruction can
* trigger drop or destruction of other item(s) and alter
* the invent or mon->minvent chain, possibly recursively.
*
* The bypass bit on all objects is reset each turn, whenever
* gc.context.bypasses is set.
@@ -2560,7 +2560,7 @@ dozap(void)
/* Are we having fun yet?
* weffects -> buzz(obj->otyp) -> zhitm (temple priest) ->
* attack -> hitum -> known_hitum -> ghod_hitsu ->
* buzz(AD_ELEC) -> destroy_item(WAND_CLASS) ->
* buzz(AD_ELEC) -> destroy_items(AD_ELEC) ->
* useup -> obfree -> dealloc_obj -> free(obj)
*/
gc.current_wand = obj;
@@ -2598,6 +2598,7 @@ zapyourself(struct obj *obj, boolean ordinary)
{
boolean learn_it = FALSE;
int damage = 0;
int orig_dmg = 0; /* for passing to destroy_items() */
switch (obj->otyp) {
case WAN_STRIKING:
@@ -2620,19 +2621,19 @@ zapyourself(struct obj *obj, boolean ordinary)
case WAN_LIGHTNING:
learn_it = TRUE;
orig_dmg = d(12, 6);
if (!Shock_resistance) {
You("shock yourself!");
damage = d(12, 6);
damage = orig_dmg;
exercise(A_CON, FALSE);
monstunseesu(M_SEEN_ELEC);
} else {
shieldeff(u.ux, u.uy);
You("zap yourself, but seem unharmed.");
monstseesu(M_SEEN_ELEC);
ugolemeffects(AD_ELEC, d(12, 6));
ugolemeffects(AD_ELEC, orig_dmg);
}
destroy_item(WAND_CLASS, AD_ELEC);
destroy_item(RING_CLASS, AD_ELEC);
(void) destroy_items(&gy.youmonst, AD_ELEC, orig_dmg);
(void) flashburn((long) rnd(100));
break;
@@ -2643,22 +2644,20 @@ zapyourself(struct obj *obj, boolean ordinary)
case WAN_FIRE:
case FIRE_HORN:
learn_it = TRUE;
orig_dmg = d(12, 6);
if (Fire_resistance) {
shieldeff(u.ux, u.uy);
You_feel("rather warm.");
monstseesu(M_SEEN_FIRE);
ugolemeffects(AD_FIRE, d(12, 6));
ugolemeffects(AD_FIRE, orig_dmg);
} else {
pline("You've set yourself afire!");
damage = d(12, 6);
damage = orig_dmg;
monstunseesu(M_SEEN_FIRE);
}
burn_away_slime();
(void) burnarmor(&gy.youmonst);
destroy_item(SCROLL_CLASS, AD_FIRE);
destroy_item(POTION_CLASS, AD_FIRE);
destroy_item(SPBOOK_CLASS, AD_FIRE);
destroy_item(FOOD_CLASS, AD_FIRE); /* only slime for now */
(void) destroy_items(&gy.youmonst, AD_FIRE, orig_dmg);
ignite_items(gi.invent);
break;
@@ -2666,17 +2665,18 @@ zapyourself(struct obj *obj, boolean ordinary)
case SPE_CONE_OF_COLD:
case FROST_HORN:
learn_it = TRUE;
orig_dmg = d(12, 6);
if (Cold_resistance) {
shieldeff(u.ux, u.uy);
You_feel("a little chill.");
monstseesu(M_SEEN_COLD);
ugolemeffects(AD_COLD, d(12, 6));
ugolemeffects(AD_COLD, orig_dmg);
} else {
You("imitate a popsicle!");
damage = d(12, 6);
damage = orig_dmg;
monstunseesu(M_SEEN_COLD);
}
destroy_item(POTION_CLASS, AD_COLD);
(void) destroy_items(&gy.youmonst, AD_COLD, orig_dmg);
break;
case WAN_MAGIC_MISSILE:
@@ -4093,7 +4093,7 @@ zhitm(
int nd, /* number of hit dice to use */
struct obj **ootmp) /* to return worn armor for caller to disintegrate */
{
int tmp = 0; /* damage amount */
register int tmp = 0, orig_dmg = 0; /* damage amount */
int damgtype = zaptype(type) % 10;
boolean sho_shieldeff = FALSE;
boolean spellcaster = is_hero_spell(type); /* maybe get a bonus! */
@@ -4115,20 +4115,16 @@ zhitm(
break;
}
tmp = d(nd, 6);
if (resists_cold(mon))
tmp += 7;
if (spellcaster)
tmp = spell_damage_bonus(tmp);
orig_dmg = tmp; /* includes spell bonus but not monster vuln to fire */
if (resists_cold(mon))
tmp += 7;
if (burnarmor(mon)) {
if (!rn2(3))
(void) destroy_mitem(mon, POTION_CLASS, AD_FIRE);
if (!rn2(3))
(void) destroy_mitem(mon, SCROLL_CLASS, AD_FIRE);
if (!rn2(5))
(void) destroy_mitem(mon, SPBOOK_CLASS, AD_FIRE);
if (!rn2(3))
if (!rn2(3)) {
tmp += destroy_items(mon, AD_FIRE, orig_dmg);
ignite_items(mon->minvent);
destroy_mitem(mon, FOOD_CLASS, AD_FIRE); /* carried slime */
}
}
break;
case ZT_COLD:
@@ -4137,12 +4133,13 @@ zhitm(
break;
}
tmp = d(nd, 6);
if (resists_fire(mon))
tmp += d(nd, 3);
if (spellcaster)
tmp = spell_damage_bonus(tmp);
orig_dmg = tmp; /* includes spell bonus but not monster vuln to cold */
if (resists_fire(mon))
tmp += d(nd, 3);
if (!rn2(3))
(void) destroy_mitem(mon, POTION_CLASS, AD_COLD);
tmp += destroy_items(mon, AD_COLD, orig_dmg);
break;
case ZT_SLEEP:
/* possibly resistance and shield effect handled by sleep_monst() */
@@ -4195,14 +4192,15 @@ zhitm(
tmp = mon->mhp + 1;
break;
case ZT_LIGHTNING:
tmp = d(nd, 6);
if (spellcaster)
tmp = spell_damage_bonus(tmp);
orig_dmg = tmp;
if (resists_elec(mon) || defended(mon, AD_ELEC)) {
sho_shieldeff = TRUE;
tmp = 0;
/* can still blind the monster */
} else
tmp = d(nd, 6);
if (spellcaster && tmp)
tmp = spell_damage_bonus(tmp);
}
if (!resists_blnd(mon)
&& !(type > 0 && engulfing_u(mon))
&& nd > 2) {
@@ -4215,10 +4213,7 @@ zhitm(
mon->mblinded += rnd_tmp;
}
if (!rn2(3))
(void) destroy_mitem(mon, WAND_CLASS, AD_ELEC);
/* not actually possible yet */
if (!rn2(3))
(void) destroy_mitem(mon, RING_CLASS, AD_ELEC);
tmp += destroy_items(mon, AD_ELEC, orig_dmg);
break;
case ZT_POISON_GAS:
if (resists_poison(mon) || defended(mon, AD_DRST)) {
@@ -4261,6 +4256,7 @@ zhitu(
coordxy sx, coordxy sy)
{
int dam = 0, abstyp = zaptype(type);
int orig_dam = 0;
switch (abstyp % 10) {
case ZT_MAGIC_MISSILE:
@@ -4275,40 +4271,37 @@ zhitu(
}
break;
case ZT_FIRE:
orig_dam = d(nd, 6);
if (Fire_resistance) {
shieldeff(sx, sy);
You("don't feel hot!");
monstseesu(M_SEEN_FIRE);
ugolemeffects(AD_FIRE, d(nd, 6));
ugolemeffects(AD_FIRE, orig_dam);
} else {
dam = d(nd, 6);
dam = orig_dam;
monstunseesu(M_SEEN_FIRE);
}
burn_away_slime();
if (burnarmor(&gy.youmonst)) { /* "body hit" */
if (!rn2(3))
destroy_item(POTION_CLASS, AD_FIRE);
if (!rn2(3))
destroy_item(SCROLL_CLASS, AD_FIRE);
if (!rn2(5))
destroy_item(SPBOOK_CLASS, AD_FIRE);
(void) destroy_items(&gy.youmonst, AD_FIRE, orig_dam);
if (!rn2(3))
ignite_items(gi.invent);
destroy_item(FOOD_CLASS, AD_FIRE);
}
break;
case ZT_COLD:
orig_dam = d(nd, 6);
if (Cold_resistance) {
shieldeff(sx, sy);
You("don't feel cold.");
monstseesu(M_SEEN_COLD);
ugolemeffects(AD_COLD, d(nd, 6));
ugolemeffects(AD_COLD, orig_dam);
} else {
dam = d(nd, 6);
dam = orig_dam;
monstunseesu(M_SEEN_COLD);
}
if (!rn2(3))
destroy_item(POTION_CLASS, AD_COLD);
(void) destroy_items(&gy.youmonst, AD_COLD, orig_dam);
break;
case ZT_SLEEP:
if (Sleep_resistance) {
@@ -4367,20 +4360,19 @@ zhitu(
done(DIED);
return; /* lifesaved */
case ZT_LIGHTNING:
orig_dam = d(nd, 6);
if (Shock_resistance) {
shieldeff(sx, sy);
You("aren't affected.");
monstseesu(M_SEEN_ELEC);
ugolemeffects(AD_ELEC, d(nd, 6));
ugolemeffects(AD_ELEC, orig_dam);
} else {
dam = d(nd, 6);
dam = orig_dam;
exercise(A_CON, FALSE);
monstunseesu(M_SEEN_ELEC);
}
if (!rn2(3))
destroy_item(WAND_CLASS, AD_ELEC);
if (!rn2(3))
destroy_item(RING_CLASS, AD_ELEC);
(void) destroy_items(&gy.youmonst, AD_ELEC, orig_dam);
break;
case ZT_POISON_GAS:
poisoned("blast", A_DEX, "poisoned blast", 15, FALSE);
@@ -5426,6 +5418,51 @@ break_statue(struct obj *obj)
return TRUE;
}
/* Return TRUE if obj is eligible to pass to maybe_destroy_item given the type of
* elemental damage it's being subjected to.
* Note that things like the Book of the Dead are eligible even though they
* won't get destroyed, because it will attempt to be destroyed but print a
* special message instead. */
static boolean
destroyable(struct obj *obj, int adtyp)
{
if (obj->oartifact) {
/* don't destroy artifacts */
return FALSE;
}
if (obj->in_use && obj->quan == 1L) {
/* not available for destroying */
return FALSE;
}
if (adtyp == AD_FIRE) {
/* fire-magic items are immune */
if (obj->otyp == SCR_FIRE || obj->otyp == SPE_FIREBALL) {
return FALSE;
}
if (obj->otyp == GLOB_OF_GREEN_SLIME || obj->oclass == POTION_CLASS
|| obj->oclass == SCROLL_CLASS || obj->oclass == SPBOOK_CLASS) {
return TRUE;
}
} else if (adtyp == AD_COLD) {
/* non-water potions don't freeze and shatter */
if (obj->oclass == POTION_CLASS && obj->otyp != POT_OIL) {
return TRUE;
}
} else if (adtyp == AD_ELEC) {
if (obj->oclass != RING_CLASS && obj->oclass != WAND_CLASS) {
return FALSE;
}
/* electric-magic items are immune */
if (obj->otyp != RIN_SHOCK_RESISTANCE && obj->otyp != WAN_LIGHTNING) {
return TRUE;
}
/* There used to be a commented out bit of code that would exclude
* gc.current_wand, but it wasn't used, so it wasn't moved into this
* function. */
}
return FALSE;
}
/* convert attack damage AD_foo to property resistance */
static int
adtyp_to_prop(int dmgtyp)
@@ -5558,14 +5595,25 @@ const char *const destroy_strings[][3] = {
{ "breaks apart and explodes", "", "exploding wand" },
};
/* guts of destroy_item(), which ought to be called maybe_destroy_items();
caller must decide whether obj is eligible */
static void
destroy_one_item(struct obj *obj, int osym, int dmgtyp)
/* guts of destroy_items();
caller must decide whether obj is eligible, though there's one case (Book of
the Dead) in which an eligible item shouldn't be destroyed (it prints a
special message instead).
Returns the amount of damage done, but this is used differently depending on
whether it's the player or a monster having an item destroyed: players lose
the HP and possibly die in this function, and the return value is unused,
whereas monsters return the damage to their caller to be taken off later */
static int
maybe_destroy_item(
struct monst *carrier,
struct obj *obj,
int dmgtyp)
{
long i, cnt, quan;
int dmg, xresist, skip, dindx;
const char *mult;
boolean u_carry = (carrier == &gy.youmonst);
boolean vis = !u_carry && canseemon(carrier);
boolean chargeit = FALSE;
xresist = skip = 0;
@@ -5575,30 +5623,29 @@ destroy_one_item(struct obj *obj, int osym, int dmgtyp)
/* external worn item protects inventory? */
if (inventory_resistance_check(dmgtyp))
return;
return 0;
switch (dmgtyp) {
case AD_COLD:
if (osym == POTION_CLASS && obj->otyp != POT_OIL) {
quan = obj->quan;
dindx = 0;
dmg = rnd(4);
} else
skip++;
quan = obj->quan;
dindx = 0;
dmg = rnd(4);
break;
case AD_FIRE:
xresist = (Fire_resistance && obj->oclass != POTION_CLASS
&& obj->otyp != GLOB_OF_GREEN_SLIME);
if (obj->otyp == SCR_FIRE || obj->otyp == SPE_FIREBALL)
skip++;
xresist = (obj->oclass != POTION_CLASS
&& obj->otyp != GLOB_OF_GREEN_SLIME
&& (u_carry ? Fire_resistance : resists_fire(carrier)));
if (obj->otyp == SPE_BOOK_OF_THE_DEAD) {
skip++;
if (!Blind)
skip = 1;
if (u_carry ? !Blind : vis) {
pline("%s glows a strange %s, but remains intact.",
The(xname(obj)), hcolor("dark red"));
The(u_carry ? xname(obj) : distant_name(obj, xname)),
hcolor("dark red"));
}
break;
}
quan = obj->quan;
switch (osym) {
switch (obj->oclass) {
case POTION_CLASS:
dindx = (obj->otyp != POT_OIL) ? 1 : 2;
dmg = rnd(6);
@@ -5611,23 +5658,17 @@ destroy_one_item(struct obj *obj, int osym, int dmgtyp)
dindx = 4;
dmg = 1;
break;
case FOOD_CLASS:
if (obj->otyp == GLOB_OF_GREEN_SLIME) {
dindx = 1; /* boil and explode */
dmg = (obj->owt + 19) / 20;
} else {
skip++;
}
break;
default:
skip++;
case FOOD_CLASS: /* only GLOB_OF_GREEN_SLIME */
dindx = 1; /* boil and explode */
dmg = (obj->owt + 19) / 20;
break;
}
break;
case AD_ELEC:
xresist = (Shock_resistance && obj->oclass != RING_CLASS);
xresist = (obj->oclass != RING_CLASS
&& (u_carry ? Shock_resistance : resists_elec(carrier)));
quan = obj->quan;
switch (osym) {
switch (obj->oclass) {
case RING_CLASS:
if (((obj->owornmask & W_RING) && uarmg && !is_metallic(uarmg))
|| obj->otyp == RIN_SHOCK_RESISTANCE) {
@@ -5641,29 +5682,22 @@ destroy_one_item(struct obj *obj, int osym, int dmgtyp)
dmg = 0;
break;
case WAND_CLASS:
if (obj->otyp == WAN_LIGHTNING) {
skip++;
break;
}
#if 0
if (obj == gc.current_wand) { skip++; break; }
#endif
dindx = 6;
dmg = rnd(10);
break;
default:
skip++;
break;
}
break;
default:
skip++;
skip = 1; /* just in case ineligible damage type gets through... */
impossible("maybe_destroy_item with unexpected dmgtyp %d", dmgtyp);
break;
}
if (chargeit) {
recharge(obj, 0);
} else if (!skip) {
char osym = obj->oclass; /* for checking glob of slime after it's
destroyed */
if (obj->in_use)
--quan; /* one will be used up elsewhere */
for (i = cnt = 0L; i < quan; i++)
@@ -5671,30 +5705,44 @@ destroy_one_item(struct obj *obj, int osym, int dmgtyp)
cnt++;
if (!cnt)
return;
mult = (cnt == 1L)
? ((quan == 1L) ? "Your" /* 1 of 1 */
: "One of your") /* 1 of N */
: ((cnt < quan) ? "Some of your" /* n of N */
: (quan == 2L) ? "Both of your" /* 2 of 2 */
: "All of your"); /* N of N */
pline("%s %s %s!", mult, xname(obj),
destroy_strings[dindx][(cnt > 1L)]);
if (osym == POTION_CLASS && dmgtyp != AD_COLD) {
if (!breathless(gy.youmonst.data) || haseyes(gy.youmonst.data))
return 0;
if (u_carry || vis) {
mult = (cnt == 1L)
? ((quan == 1L) ? "" /* 1 of 1 */
: "One of ") /* 1 of N */
: ((cnt < quan) ? "Some of " /* n of N */
: (quan == 2L) ? "Both of " /* 2 of 2 */
: "All of "); /* N of N */
pline("%s%s %s!", mult,
(cnt == 1L && quan == 1L) ? Yname2(obj) : yname(obj),
destroy_strings[dindx][(cnt > 1L)]);
}
if (u_carry) { /* effects that happen only to the player */
if (osym == POTION_CLASS && dmgtyp != AD_COLD
&& (!breathless(gy.youmonst.data) || haseyes(gy.youmonst.data))) {
potionbreathe(obj);
}
if (obj->owornmask) { /* m_useup handles these for monster */
if (obj->owornmask & W_RING) /* ring being worn */
Ring_gone(obj);
else
setnotworn(obj);
}
if (obj == gc.current_wand) {
gc.current_wand = 0; /* destroyed */
}
}
if (obj->owornmask) {
if (obj->owornmask & W_RING) /* ring being worn */
Ring_gone(obj);
for (i = 0; i < cnt; i++) {
if (u_carry)
useup(obj);
else
setnotworn(obj);
m_useup(carrier, obj);
}
if (obj == gc.current_wand)
gc.current_wand = 0; /* destroyed */
for (i = 0; i < cnt; i++)
useup(obj);
if (dmg) {
if (!u_carry) {
return xresist ? 0 : dmg;
}
if (xresist) {
You("aren't hurt!");
} else {
@@ -5709,26 +5757,66 @@ destroy_one_item(struct obj *obj, int osym, int dmgtyp)
}
}
}
return dmg;
}
/* target items of specified class for possible destruction */
void
destroy_item(int osym, int dmgtyp)
/* scaling factor; dmg/5 stacks of items will be subjected to destroy_items() */
#define DMG_DESTROY_SCALE 5
/* largest amount of stacks that will be destroyed in a single call */
#define MAX_ITEMS_DESTROYED 20
/* target items of specified class in mon's inventory for possible destruction
* return total amount of damage inflicted, though this is unused if mon is the
* player */
int
destroy_items(
struct monst *mon, /* monster whose invent is being subjected to
* destruction */
int dmgtyp, /* AD_**** - currently only cold, fire, elec */
int dmg_in) /* the amount of HP damage the attack dealt */
{
struct obj *obj;
int i, deferral_indx = 0;
/* 1+52+1: try to handle a full inventory; it doesn't matter if
inventory actually has more, even if everything should be deferred */
unsigned short deferrals[invlet_gold + invlet_basic + invlet_overflow];
int i, defer;
int limit; /* max amount of item stacks destroyed, based on damage */
struct {
unsigned oid;
boolean deferred;
} items_to_destroy[MAX_ITEMS_DESTROYED];
int elig_stacks = 0; /* number of destroyable objects found so far */
boolean u_carry = (mon == &gy.youmonst);
/* this is a struct obj** because we might destroy the first item in it */
struct obj **objchn = u_carry ? &gi.invent : &mon->minvent;
int dmg_out = 0; /* damage caused by items getting destroyed */
(void) memset((genericptr_t) deferrals, 0, sizeof deferrals);
/*
* Sometimes destroying an item can change inventory aside from
/* initialize items_to_destroy */
for (i = 0; i < MAX_ITEMS_DESTROYED; ++i) {
/* 0 should not be a valid o_id for anything */
items_to_destroy[i].oid = 0;
items_to_destroy[i].deferred = FALSE;
}
/* Don't straight up destroy all items with an equal chance; limit it based
* on the amount of damage being dealt by the source of the item
* destruction. */
limit = dmg_in / DMG_DESTROY_SCALE;
if (dmg_in % DMG_DESTROY_SCALE > rn2(DMG_DESTROY_SCALE)) {
limit++; /* dmg = 9: 20% chance of limit=1, 80% of limit=2, etc */
}
if (limit > MAX_ITEMS_DESTROYED) {
/* in case of incredibly high damage, prevent from overflowing
* items_to_destroy */
limit = MAX_ITEMS_DESTROYED;
}
if (limit < 1) {
return 0; /* nothing destroyed */
}
/* Sometimes destroying an item can change inventory aside from
* the item itself (cited case was a potion of unholy water; when
* boiled, potionbreathe() caused hero to transform into were-beast
* form and that resulted in dropping or destroying some worn armor).
*
* Unlike other uses of the object bybass mechanism, destroy_item()
* Unlike other uses of the object bypass mechanism, destroy_items()
* can be called multiple times for the same event. So we have to
* explicitly clear it before each use and hope no other section of
* code expects it to retain previous value.
@@ -5751,21 +5839,27 @@ destroy_item(int osym, int dmgtyp)
* of o_id and quantity of what is targeted for destruction,
* second pass to handle the destruction.]
*/
bypass_objlist(gi.invent, FALSE); /* clear bypass bit for invent */
bypass_objlist(*objchn, FALSE); /* clear bypass bit for invent */
while ((obj = nxt_unbypassed_obj(gi.invent)) != 0) {
if (obj->oclass != osym)
continue; /* test only objs of type osym */
if (obj->oartifact)
continue; /* don't destroy artifacts */
if (obj->in_use && obj->quan == 1L)
continue; /* not available */
while ((obj = nxt_unbypassed_obj(*objchn)) != 0) {
if (!destroyable(obj, dmgtyp))
continue; /* this dmg type can't destroy this obj */
/* obj is eligible; maybe add it to items_to_destroy */
i = (elig_stacks < limit) ? elig_stacks : rn2(elig_stacks);
/* do this afterwards to avoid not filling items_to_destroy[0] */
elig_stacks++;
if (i >= limit) {
/* random index was too high */
continue;
}
items_to_destroy[i].oid = obj->o_id;
/* if loss of this item might dump us onto a trap, hold off
until later because potential recursive destroy_item() will
until later because potential recursive destroy_items() will
result in setting bypass bits on whole chain--we would skip
the rest as already processed once control returns here */
if (deferral_indx < SIZE(deferrals)
if (u_carry
&& ((obj->owornmask != 0L
&& (objects[obj->otyp].oc_oprop == LEVITATION
|| objects[obj->otyp].oc_oprop == FLYING))
@@ -5773,142 +5867,35 @@ destroy_item(int osym, int dmgtyp)
polymorph so don't need to be deferred */
|| (obj->otyp == POT_WATER && ismnum(u.ulycn)
&& (Upolyd ? obj->blessed : obj->cursed)))) {
deferrals[deferral_indx++] = obj->o_id;
continue;
items_to_destroy[i].deferred = TRUE;
}
else {
items_to_destroy[i].deferred = FALSE;
}
/* obj is eligible; maybe destroy it */
destroy_one_item(obj, osym, dmgtyp);
}
/* if we saved some items for later (most likely just a worn ring
of levitation) and they're still in inventory, handle them now */
for (i = 0; i < deferral_indx; ++i) {
/* note: obj->nobj is only referenced when obj is skipped;
having obj be dropped or destroyed won't affect traversal */
for (obj = gi.invent; obj; obj = obj->nobj)
if (obj->o_id == deferrals[i]) {
destroy_one_item(obj, osym, dmgtyp);
break;
}
if (elig_stacks > limit) {
elig_stacks = limit; /* so we can loop up to elig_stacks */
}
return;
}
int
destroy_mitem(struct monst *mtmp, int osym, int dmgtyp)
{
struct obj *obj;
int skip, tmp = 0;
long i, cnt, quan;
int dindx;
boolean vis;
if (mtmp == &gy.youmonst) { /* this simplifies artifact_hit() */
destroy_item(osym, dmgtyp);
return 0; /* arbitrary; value doesn't matter to artifact_hit() */
}
vis = canseemon(mtmp);
/* see destroy_item(); object destruction could disrupt inventory list */
bypass_objlist(mtmp->minvent, FALSE); /* clear bypass bit for minvent */
while ((obj = nxt_unbypassed_obj(mtmp->minvent)) != 0) {
if (obj->oclass != osym)
continue; /* test only objs of type osym */
skip = 0;
quan = 0L;
dindx = 0;
switch (dmgtyp) {
case AD_COLD:
if (osym == POTION_CLASS && obj->otyp != POT_OIL) {
quan = obj->quan;
dindx = 0;
tmp++;
} else
skip++;
break;
case AD_FIRE:
if (obj->otyp == SCR_FIRE || obj->otyp == SPE_FIREBALL)
skip++;
if (obj->otyp == SPE_BOOK_OF_THE_DEAD) {
skip++;
if (vis)
pline("%s glows a strange %s, but remains intact.",
The(distant_name(obj, xname)), hcolor("dark red"));
}
quan = obj->quan;
switch (osym) {
case POTION_CLASS:
dindx = (obj->otyp != POT_OIL) ? 1 : 2;
tmp++;
break;
case SCROLL_CLASS:
dindx = 3;
tmp++;
break;
case SPBOOK_CLASS:
dindx = 4;
tmp++;
break;
case FOOD_CLASS:
if (obj->otyp == GLOB_OF_GREEN_SLIME) {
dindx = 1; /* boil and explode */
tmp += (obj->owt + 19) / 20;
} else {
skip++;
}
break;
default:
skip++;
break;
}
break;
case AD_ELEC:
quan = obj->quan;
switch (osym) {
case RING_CLASS:
if (obj->otyp == RIN_SHOCK_RESISTANCE) {
skip++;
for (defer = 0; defer <= 1; ++defer) {
/* if we saved some items for later (most likely just a worn ring
of levitation) and they're still in inventory, handle them on the
second iteration of the loop */
struct obj *nobj;
for (obj = *objchn; obj; obj = nobj) {
nobj = obj->nobj;
for (i = 0; i < elig_stacks; ++i) {
if (obj->o_id == items_to_destroy[i].oid
&& (items_to_destroy[i].deferred == (defer == 1))) {
dmg_out += maybe_destroy_item(mon, obj, dmgtyp);
break;
}
dindx = 5;
break;
case WAND_CLASS:
if (obj->otyp == WAN_LIGHTNING) {
skip++;
break;
}
dindx = 6;
tmp++;
break;
default:
skip++;
break;
}
break;
default:
skip++;
break;
}
if (!skip) {
for (i = cnt = 0L; i < quan; i++)
if (!rn2(3))
cnt++;
if (!cnt)
continue;
if (vis)
pline("%s%s %s!",
(cnt == obj->quan) ? "" : (cnt > 1L) ? "Some of "
: "One of ",
(cnt == obj->quan) ? Yname2(obj) : yname(obj),
destroy_strings[dindx][(cnt > 1L)]);
for (i = 0; i < cnt; i++)
m_useup(mtmp, obj);
}
}
return tmp;
/* almost certainly not everything was destroyed; remove bypass bit after it
* was set earlier */
bypass_objlist(*objchn, FALSE);
return dmg_out;
}
int