From 6b60618e0e662b35b8e9c31dc9c4baee5ae70932 Mon Sep 17 00:00:00 2001 From: Pasi Kallinen Date: Sat, 22 May 2021 13:27:54 +0300 Subject: [PATCH] Exploding spheres cause real explosions Despite active explosion attacks being called explosions in-game, they only affected a single target, and were handled differently from actual explosions. Make them do an actual explosion instead. This should make spheres more interesting and inspire different tactics handling them. Because spheres deal more damage on average and can destroy items in their explosions, their difficulty has been increased slightly. Polyselfed hero exploding won't cause elemental damage to their own gear. Originally from xNetHack by copperwater . --- doc/fixes37.0 | 1 + include/extern.h | 3 ++ include/hack.h | 7 +++ src/explode.c | 131 ++++++++++++++++++++++++++++++++++++++++++----- src/hack.c | 3 ++ src/mhitm.c | 10 +++- src/mhitu.c | 129 ++++++++++++++++++---------------------------- src/mon.c | 7 +-- src/monst.c | 6 +-- src/uhitm.c | 42 ++++++--------- src/zap.c | 5 ++ 11 files changed, 217 insertions(+), 127 deletions(-) diff --git a/doc/fixes37.0 b/doc/fixes37.0 index 33fe34b78d..34dece0ba6 100644 --- a/doc/fixes37.0 +++ b/doc/fixes37.0 @@ -530,6 +530,7 @@ boost hit points of some golems make anti-magic fields drain more energy and prevent them from showing up too early in the dungeon eating magical monsters such as wizards or shamans may give a mild buzz +make exploding spheres create an actual explosion Fixes to 3.7.0-x Problems that Were Exposed Via git Repository diff --git a/include/extern.h b/include/extern.h index cec6fdbf81..548280a509 100644 --- a/include/extern.h +++ b/include/extern.h @@ -735,6 +735,8 @@ extern void explode(int, int, int, int, char, int); extern long scatter(int, int, int, unsigned int, struct obj *); extern void splatter_burning_oil(int, int, boolean); extern void explode_oil(struct obj *, int, int); +extern int adtyp_to_expltype(int); +extern void mon_explodes(struct monst *, struct attack *); /* ### extralev.c ### */ @@ -2740,6 +2742,7 @@ extern boolean do_stone_u(struct monst *); extern void do_stone_mon(struct monst *, struct attack *, struct monst *, struct mhitm_data *); extern int damageum(struct monst *, struct attack *, int); +extern int explum(struct monst *, struct attack *); extern void missum(struct monst *, struct attack *, boolean); extern int passive(struct monst *, struct obj *, boolean, boolean, uchar, boolean); diff --git a/include/hack.h b/include/hack.h index e4cf6b77d3..42b1282e21 100644 --- a/include/hack.h +++ b/include/hack.h @@ -527,6 +527,13 @@ enum getobj_callback_returns { GETOBJ_SUGGEST = 2, }; +/* constant passed to explode() for gas spores because gas spores are weird + * Specifically, this is an exception to the whole "explode() uses dobuzz types" + * system (the range -1 to -9 isn't used by it, for some reason), where this is + * effectively an extra dobuzz type, and some zap.c code needs to be aware of + * it. */ +#define PHYS_EXPL_TYPE -1 + /* * option setting restrictions */ diff --git a/src/explode.c b/src/explode.c index 2a2e993dbc..e48ab61ac1 100644 --- a/src/explode.c +++ b/src/explode.c @@ -17,6 +17,8 @@ static const int explosion[3][3] = { { S_explode1, S_explode4, S_explode7 }, * did it, and with a wand, spell, or breath weapon? Object types share both * these disadvantages.... * + * Note: anything with a AT_BOOM AD_PHYS attack uses PHYS_EXPL_TYPE for type. + * * Important note about Half_physical_damage: * Unlike losehp(), explode() makes the Half_physical_damage adjustments * itself, so the caller should never have done that ahead of time. @@ -46,6 +48,7 @@ explode( coord grabxy; char hallu_buf[BUFSZ], killr_buf[BUFSZ]; short exploding_wand_typ = 0; + boolean you_exploding = (olet == MON_EXPLODE && type >= 0); if (olet == WAND_CLASS) { /* retributive strike */ /* 'type' is passed as (wand's object type * -1); save @@ -113,51 +116,61 @@ explode( * skip harm to gear of any extended targets when inflicting damage. */ - if (olet == MON_EXPLODE) { + if (olet == MON_EXPLODE && !you_exploding) { /* when explode() is called recursively, g.killer.name might change so we need to retain a copy of the current value for this explosion */ str = strcpy(killr_buf, g.killer.name); do_hallu = (Hallucination && (strstri(str, "'s explosion") || strstri(str, "s' explosion"))); + } + if (type == PHYS_EXPL_TYPE) { + /* currently only gas spores */ adtyp = AD_PHYS; - } else + } else { + /* If str is e.g. "flaming sphere's explosion" from above, we want to + * still assign adtyp appropriately, but not replace str. */ + const char *adstr = NULL; + switch (abs(type) % 10) { case 0: - str = "magical blast"; + adstr = "magical blast"; adtyp = AD_MAGM; break; case 1: - str = (olet == BURNING_OIL) ? "burning oil" + adstr = (olet == BURNING_OIL) ? "burning oil" : (olet == SCROLL_CLASS) ? "tower of flame" : "fireball"; /* fire damage, not physical damage */ adtyp = AD_FIRE; break; case 2: - str = "ball of cold"; + adstr = "ball of cold"; adtyp = AD_COLD; break; case 4: - str = (olet == WAND_CLASS) ? "death field" + adstr = (olet == WAND_CLASS) ? "death field" : "disintegration field"; adtyp = AD_DISN; break; case 5: - str = "ball of lightning"; + adstr = "ball of lightning"; adtyp = AD_ELEC; break; case 6: - str = "poison gas cloud"; + adstr = "poison gas cloud"; adtyp = AD_DRST; break; case 7: - str = "splash of acid"; + adstr = "splash of acid"; adtyp = AD_ACID; break; default: impossible("explosion base type %d?", type); return; } + if (!str) + str = adstr; + } any_shield = visible = FALSE; for (i = 0; i < 3; i++) @@ -308,18 +321,27 @@ explode( You_hear("a blast."); } - if (dam) - for (i = 0; i < 3; i++) + if (dam) { + for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { if (explmask[i][j] == 2) continue; - if (i + x - 1 == u.ux && j + y - 1 == u.uy) + if (i + x - 1 == u.ux && j + y - 1 == u.uy) { uhurt = (explmask[i][j] == 1) ? 1 : 2; + /* If the player is attacking via polyself into something + * with an explosion attack, leave them (and their gear) + * unharmed, to avoid punishing them from using such + * polyforms creatively */ + if (!g.context.mon_moving && you_exploding) + uhurt = 0; + } /* for inside_engulfer, only is affected */ else if (inside_engulfer) continue; idamres = idamnonres = 0; - if (type >= 0 && !u.uswallow) + /* Affect the floor unless the player caused the explosion from + * inside their engulfer. */ + if (!(u.uswallow && !g.context.mon_moving)) (void) zap_over_floor((xchar) (i + x - 1), (xchar) (j + y - 1), type, &shopdamage, exploding_wand_typ); @@ -479,6 +501,8 @@ explode( setmangry(mtmp, TRUE); } } + } + } /* Do your injury last */ if (uhurt) { @@ -825,4 +849,85 @@ explode_oil(struct obj *obj, int x, int y) splatter_burning_oil(x, y, diluted_oil); } +/* Convert a damage type into an explosion display type. */ +int +adtyp_to_expltype(const int adtyp) +{ + switch(adtyp) { + case AD_ELEC: + /* Electricity isn't magical, but there currently isn't an electric + * explosion type. Magical is the next best thing. */ + case AD_SPEL: + case AD_DREN: + case AD_ENCH: + return EXPL_MAGICAL; + case AD_FIRE: + return EXPL_FIERY; + case AD_COLD: + return EXPL_FROSTY; + case AD_DRST: + case AD_DRDX: + case AD_DRCO: + case AD_DISE: + case AD_PEST: + case AD_PHYS: /* gas spore */ + return EXPL_NOXIOUS; + default: + impossible("adtyp_to_expltype: bad explosion type %d", adtyp); + return EXPL_FIERY; + } +} + +/* A monster explodes in a way that produces a real explosion (e.g. a sphere or + * gas spore, not a yellow light or similar). + * This is some common code between explmu() and explmm(). + */ +void +mon_explodes(struct monst *mon, struct attack *mattk) +{ + int dmg; + int type; + if (mattk->damn) { + dmg = d((int) mattk->damn, (int) mattk->damd); + } + else if (mattk->damd) { + dmg = d((int) mon->data->mlevel + 1, (int) mattk->damd); + } + else { + dmg = 0; + } + + if (mattk->adtyp == AD_PHYS) { + type = PHYS_EXPL_TYPE; + } + else if (mattk->adtyp >= AD_MAGM && mattk->adtyp <= AD_SPC2) { + /* The -1, +20, *-1 math is to set it up as a 'monster breath' type for + * the explosions (it isn't, but this is the closest analogue). */ + type = -((mattk->adtyp - 1) + 20); + } + else { + impossible("unknown type for mon_explode %d", mattk->adtyp); + return; + } + + /* Kill it now so it won't appear to be caught in its own explosion. + * Must check to see if already dead - which happens if this is called from + * an AT_BOOM attack upon death. */ + if (!DEADMONSTER(mon)) { + mondead(mon); + } + + /* This might end up killing you, too; you never know... + * also, it is used in explode() messages */ + Sprintf(g.killer.name, "%s explosion", + s_suffix(pmname(mon->data, Mgender(mon)))); + g.killer.format = KILLED_BY_AN; + + explode(mon->mx, mon->my, type, dmg, MON_EXPLODE, + adtyp_to_expltype(mattk->adtyp)); + + /* reset killer */ + g.killer.name[0] = '\0'; +} + /*explode.c*/ diff --git a/src/hack.c b/src/hack.c index bcec86d9a1..838cbe6148 100644 --- a/src/hack.c +++ b/src/hack.c @@ -1753,8 +1753,11 @@ domove_core(void) nomul(0); if (explo) { + struct attack *attk; /* no monster has been attacked so we have bypassed explum() */ wake_nearto(u.ux, u.uy, 7 * 7); /* same radius as explum() */ + if ((attk = attacktype_fordmg(g.youmonst.data, AT_EXPL, AD_ANY))) + explum((struct monst *) 0, attk); u.mh = -1; /* dead in the current form */ rehumanize(); } diff --git a/src/mhitm.c b/src/mhitm.c index 13e36af1f3..72ebb10c39 100644 --- a/src/mhitm.c +++ b/src/mhitm.c @@ -827,7 +827,15 @@ explmm(struct monst *magr, struct monst *mdef, struct attack *mattk) else noises(magr, mattk); - result = mdamagem(magr, mdef, mattk, (struct obj *) 0, 0); + /* monster explosion types which actually create an explosion */ + if (mattk->adtyp == AD_FIRE || mattk->adtyp == AD_COLD + || mattk->adtyp == AD_ELEC) { + mon_explodes(magr, mattk); + /* unconditionally set AGR_DIED here; lifesaving is accounted below */ + result = MM_AGR_DIED | (DEADMONSTER(mdef) ? MM_DEF_DIED : 0); + } else { + result = mdamagem(magr, mdef, mattk, (struct obj *) 0, 0); + } /* Kill off aggressor if it didn't die. */ if (!(result & MM_AGR_DIED)) { diff --git a/src/mhitu.c b/src/mhitu.c index 3050c35ced..0ba7f984f2 100644 --- a/src/mhitu.c +++ b/src/mhitu.c @@ -1347,100 +1347,71 @@ gulpmu(struct monst *mtmp, struct attack *mattk) static int explmu(struct monst *mtmp, struct attack *mattk, boolean ufound) { - boolean physical_damage = TRUE, kill_agr = TRUE; + boolean kill_agr = TRUE; + boolean not_affected; + int tmp; if (mtmp->mcan) return MM_MISS; + tmp = d((int) mattk->damn, (int) mattk->damd); + not_affected = defends((int) mattk->adtyp, uwep); + if (!ufound) { pline("%s explodes at a spot in %s!", canseemon(mtmp) ? Monnam(mtmp) : "It", levl[mtmp->mux][mtmp->muy].typ == WATER ? "empty water" : "thin air"); } else { - int tmp = d((int) mattk->damn, (int) mattk->damd); - boolean not_affected = defends((int) mattk->adtyp, uwep); - hitmsg(mtmp, mattk); + } - switch (mattk->adtyp) { - case AD_COLD: - physical_damage = FALSE; - not_affected |= Cold_resistance; - goto common; - case AD_FIRE: - physical_damage = FALSE; - not_affected |= Fire_resistance; - goto common; - case AD_ELEC: - physical_damage = FALSE; - not_affected |= Shock_resistance; - goto common; - case AD_PHYS: - /* there aren't any exploding creatures with AT_EXPL attack - for AD_PHYS damage but there might be someday; without this, - static analysis complains that 'physical_damage' is always - False when tested below; it's right, but having that in - place means one less thing to update if AD_PHYS gets added */ - common: - - if (!not_affected) { - if (ACURR(A_DEX) > rnd(20)) { - You("duck some of the blast."); - tmp = (tmp + 1) / 2; - } else { - if (flags.verbose) - You("get blasted!"); - } - if (mattk->adtyp == AD_FIRE) - burn_away_slime(); - if (physical_damage) - tmp = Maybe_Half_Phys(tmp); - mdamageu(mtmp, tmp); - } else - monstseesu_ad(mattk->adtyp); - break; - - case AD_BLND: - not_affected = resists_blnd(&g.youmonst); - if (!not_affected) { - /* sometimes you're affected even if it's invisible */ - if (mon_visible(mtmp) || (rnd(tmp /= 2) > u.ulevel)) { - You("are blinded by a blast of light!"); - make_blinded((long) tmp, FALSE); - if (!Blind) - Your1(vision_clears); - } else if (flags.verbose) - You("get the impression it was not terribly bright."); - } - break; - - case AD_HALU: - not_affected |= Blind || (u.umonnum == PM_BLACK_LIGHT - || u.umonnum == PM_VIOLET_FUNGUS - || dmgtype(g.youmonst.data, AD_STUN)); - if (!not_affected) { - boolean chg; - if (!Hallucination) - You("are caught in a blast of kaleidoscopic light!"); - /* avoid hallucinating the black light as it dies */ - mondead(mtmp); /* remove it from map now */ - kill_agr = FALSE; /* already killed (maybe lifesaved) */ - chg = - make_hallucinated(HHallucination + (long) tmp, FALSE, 0L); - You("%s.", chg ? "are freaked out" : "seem unaffected"); - } - break; - - default: - break; + switch (mattk->adtyp) { + case AD_COLD: + case AD_FIRE: + case AD_ELEC: + mon_explodes(mtmp, mattk); + if (!DEADMONSTER(mtmp)) + kill_agr = FALSE; /* lifesaving? */ + break; + case AD_BLND: + not_affected = resists_blnd(&g.youmonst); + if (ufound && !not_affected) { + /* sometimes you're affected even if it's invisible */ + if (mon_visible(mtmp) || (rnd(tmp /= 2) > u.ulevel)) { + You("are blinded by a blast of light!"); + make_blinded((long) tmp, FALSE); + if (!Blind) + Your1(vision_clears); + } else if (flags.verbose) + You("get the impression it was not terribly bright."); } - if (not_affected) { - You("seem unaffected by it."); - ugolemeffects((int) mattk->adtyp, tmp); + break; + case AD_HALU: + not_affected |= Blind || (u.umonnum == PM_BLACK_LIGHT + || u.umonnum == PM_VIOLET_FUNGUS + || dmgtype(g.youmonst.data, AD_STUN)); + if (ufound && !not_affected) { + boolean chg; + if (!Hallucination) + You("are caught in a blast of kaleidoscopic light!"); + /* avoid hallucinating the black light as it dies */ + mondead(mtmp); /* remove it from map now */ + kill_agr = FALSE; /* already killed (maybe lifesaved) */ + chg = + make_hallucinated(HHallucination + (long) tmp, FALSE, 0L); + You("%s.", chg ? "are freaked out" : "seem unaffected"); } + break; + default: + impossible("unknown exploder damage type %d", mattk->adtyp); + break; + } + if (not_affected) { + You("seem unaffected by it."); + ugolemeffects((int) mattk->adtyp, tmp); } - if (kill_agr) + if (kill_agr && !DEADMONSTER(mtmp)) mondead(mtmp); wake_nearto(mtmp->mx, mtmp->my, 7 * 7); return (!DEADMONSTER(mtmp)) ? MM_MISS : MM_AGR_DIED; diff --git a/src/mon.c b/src/mon.c index 80714d0525..423ff03172 100644 --- a/src/mon.c +++ b/src/mon.c @@ -2522,12 +2522,7 @@ corpse_chance( return FALSE; } - Sprintf(g.killer.name, "%s explosion", - s_suffix(pmname(mdat, Mgender(mon)))); - g.killer.format = KILLED_BY_AN; - explode(mon->mx, mon->my, -1, tmp, MON_EXPLODE, EXPL_NOXIOUS); - g.killer.name[0] = '\0'; - g.killer.format = 0; + mon_explodes(mon, &mdat->mattk[i]); return FALSE; } } diff --git a/src/monst.c b/src/monst.c index 2c7cb64242..1da91df093 100644 --- a/src/monst.c +++ b/src/monst.c @@ -324,21 +324,21 @@ NEARDATA struct permonst mons_init[] = { SIZ(10, 10, MS_SILENT, MZ_SMALL), MR_COLD, MR_COLD, M1_FLY | M1_BREATHLESS | M1_NOLIMBS | M1_NOHEAD | M1_MINDLESS | M1_NOTAKE, - M2_HOSTILE | M2_NEUTER, M3_INFRAVISIBLE, 8, CLR_WHITE), + M2_HOSTILE | M2_NEUTER, M3_INFRAVISIBLE, 9, CLR_WHITE), MON("flaming sphere", S_EYE, LVL(6, 13, 4, 0, 0), (G_NOCORPSE | G_GENO | 2), A(ATTK(AT_EXPL, AD_FIRE, 4, 6), NO_ATTK, NO_ATTK, NO_ATTK, NO_ATTK, NO_ATTK), SIZ(10, 10, MS_SILENT, MZ_SMALL), MR_FIRE, MR_FIRE, M1_FLY | M1_BREATHLESS | M1_NOLIMBS | M1_NOHEAD | M1_MINDLESS | M1_NOTAKE, - M2_HOSTILE | M2_NEUTER, M3_INFRAVISIBLE, 8, CLR_RED), + M2_HOSTILE | M2_NEUTER, M3_INFRAVISIBLE, 9, CLR_RED), MON("shocking sphere", S_EYE, LVL(6, 13, 4, 0, 0), (G_NOCORPSE | G_GENO | 2), A(ATTK(AT_EXPL, AD_ELEC, 4, 6), NO_ATTK, NO_ATTK, NO_ATTK, NO_ATTK, NO_ATTK), SIZ(10, 10, MS_SILENT, MZ_SMALL), MR_ELEC, MR_ELEC, M1_FLY | M1_BREATHLESS | M1_NOLIMBS | M1_NOHEAD | M1_MINDLESS | M1_NOTAKE, - M2_HOSTILE | M2_NEUTER, M3_INFRAVISIBLE, 8, HI_ZAP), + M2_HOSTILE | M2_NEUTER, M3_INFRAVISIBLE, 10, HI_ZAP), #if 0 /* not yet implemented */ MON("beholder", S_EYE, LVL(6, 3, 4, 0, -10), (G_GENO | 2), diff --git a/src/uhitm.c b/src/uhitm.c index 36518c71fc..6a5e193a5b 100644 --- a/src/uhitm.c +++ b/src/uhitm.c @@ -18,7 +18,6 @@ static boolean hmon_hitmon(struct monst *, struct obj *, int, int); static int joust(struct monst *, struct obj *); static void demonpet(void); static boolean m_slips_free(struct monst *, struct attack *); -static int explum(struct monst *, struct attack *); static void start_engulf(struct monst *); static void end_engulf(void); static int gulpum(struct monst *, struct attack *); @@ -4127,49 +4126,41 @@ damageum( return MM_HIT; } -static int +/* Hero, as a monster which is capable of an exploding attack mattk, is + * exploding at a target monster mdef, or just exploding at nothing (e.g. with + * forcefight) if mdef is null. + */ +int explum(struct monst *mdef, struct attack *mattk) { - boolean resistance; /* only for cold/fire/elec */ register int tmp = d((int) mattk->damn, (int) mattk->damd); - You("explode!"); switch (mattk->adtyp) { case AD_BLND: - if (!resists_blnd(mdef)) { + if (mdef && !resists_blnd(mdef)) { pline("%s is blinded by your flash of light!", Monnam(mdef)); mdef->mblinded = min((int) mdef->mblinded + tmp, 127); mdef->mcansee = 0; } break; case AD_HALU: - if (haseyes(mdef->data) && mdef->mcansee) { + if (mdef && haseyes(mdef->data) && mdef->mcansee) { pline("%s is affected by your flash of light!", Monnam(mdef)); mdef->mconf = 1; } break; case AD_COLD: - resistance = resists_cold(mdef); - goto common; case AD_FIRE: - resistance = resists_fire(mdef); - goto common; case AD_ELEC: - resistance = resists_elec(mdef); - common: - if (!resistance) { - pline("%s gets blasted!", Monnam(mdef)); - mdef->mhp -= tmp; - if (DEADMONSTER(mdef)) { - killed(mdef); - return MM_DEF_DIED; - } - } else { - shieldeff(mdef->mx, mdef->my); - if (is_golem(mdef->data)) - golemeffects(mdef, (int) mattk->adtyp, tmp); - else - pline_The("blast doesn't seem to affect %s.", mon_nam(mdef)); + /* See comment in mon_explodes() and in zap.c for an explanation of this + * math. Here, the player is causing the explosion, so it should be in + * the +20 to +29 range instead of negative. */ + explode(u.ux, u.uy, (mattk->adtyp - 1) + 20, tmp, MON_EXPLODE, + adtyp_to_expltype(mattk->adtyp)); + if (mdef && DEADMONSTER(mdef)) { + /* Other monsters may have died too, but return this if the actual + * target died. */ + return MM_DEF_DIED; } break; default: @@ -4745,6 +4736,7 @@ hmonas(struct monst *mon) case AT_EXPL: /* automatic hit if next to */ dhit = -1; wakeup(mon, TRUE); + You("explode!"); sum[i] = explum(mon, mattk); break; diff --git a/src/zap.c b/src/zap.c index f9e81498fa..3e568433b7 100644 --- a/src/zap.c +++ b/src/zap.c @@ -4532,6 +4532,11 @@ zap_over_floor(xchar x, xchar y, int type, boolean *shopdamage, boolean see_it = cansee(x, y), yourzap; int rangemod = 0, abstype = abs(type) % 10; + if (type == PHYS_EXPL_TYPE) { + /* this won't have any effect on the floor */ + return -1000; /* not a zap anyway, shouldn't matter */ + } + switch (abstype) { case ZT_FIRE: t = t_at(x, y);