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 <aosdict@gmail.com>.
This commit is contained in:
Pasi Kallinen
2021-05-22 13:27:54 +03:00
parent 7f8cfb43d2
commit 6b60618e0e
11 changed files with 220 additions and 130 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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
*/

View File

@@ -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 <u.ux,u.uy> 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*/

View File

@@ -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();
}

View File

@@ -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)) {

View File

@@ -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;
}
if (not_affected) {
You("seem unaffected by it.");
ugolemeffects((int) mattk->adtyp, tmp);
}
}
if (kill_agr)
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.");
}
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 && !DEADMONSTER(mtmp))
mondead(mtmp);
wake_nearto(mtmp->mx, mtmp->my, 7 * 7);
return (!DEADMONSTER(mtmp)) ? MM_MISS : MM_AGR_DIED;

View File

@@ -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;
}
}

View File

@@ -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),

View File

@@ -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;

View File

@@ -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);