Monsters require experience using wands before they can hit with them

It takes time for an early-game monster to acclimatize itself to the
power of an attack wand: in much the same way as a nervous human may
quite possibly miss with their first attempt to use a gun in combat,
an early-game monster will always miss on its first use of an attack
wand (but from then on will understand how they work and get over
their nerves, and will hit as normal).

This is a balance change based on observed results from tournaments:
guarding against deaths to early-game attack wands requires an
unusually cautious playstyle which isn't much fun (and might not
always be possible even for the best players), so it is quite common
for them to be the cause of random deaths that it wasn't worth trying
to avoid. Although trying to dodge a monster who found an attack wand
is fun, you only actually get that fun if something makes you aware
of the danger: the monster missing with the wand is a clear way to
demonstrate the danger and let the player know that now is the right
time to take precautions.

This change could theoretically have broken saves, but probably
doesn't due to there having been a spare bit in struct monst.  Just
in case, it is being pushed together with other save-breaking changes
to avoid the need for multiple bumps to EDITLEVEL.
This commit is contained in:
Alex Smith
2026-03-19 03:46:42 +00:00
parent 07fc4904c6
commit 4d043a6f9c
7 changed files with 43 additions and 13 deletions

View File

@@ -2908,6 +2908,7 @@ the game now automatically tracks which sell prices and buy prices you have
and can also be shown elsewhere using the new 'price_quotes' option
new shields: shield of shock resistance, shield of drain resistance
new wand: wand of stasis
early-game monsters always miss with their first use of an attack wand
Platform- and/or Interface-Specific New Features

View File

@@ -3969,7 +3969,8 @@ extern int zhitm(struct monst *, int, int, struct obj **) NONNULLPTRS;
extern int burn_floor_objects(coordxy, coordxy, boolean, boolean);
extern void ubuzz(int, int);
extern void buzz(int, int, coordxy, coordxy, int, int);
extern void dobuzz(int, int, coordxy, coordxy, int, int, boolean, boolean);
extern void dobuzz(int, int, coordxy, coordxy, int, int,
boolean, boolean, boolean);
extern void melt_ice(coordxy, coordxy, const char *) NO_NNARGS;
extern void start_melt_ice_timeout(coordxy, coordxy, long);
extern void melt_ice_away(union any *, long) NONNULLARG1;

View File

@@ -162,6 +162,8 @@ struct monst {
Bitfield(meverseen, 1); /* mon has been seen at some point */
Bitfield(mspotted, 1); /* mon is currently seen by hero */
Bitfield(mwandexp, 1); /* mon has experience with wands */
/* 6 spare bits */
unsigned long mstrategy; /* for monsters with mflag3: current strategy */
#ifdef NHSTDC

View File

@@ -1288,6 +1288,10 @@ makemon(
/* quest leader and nemesis both know about all trap types */
if (ptr->msound == MS_LEADER || ptr->msound == MS_NEMESIS)
mon_learns_traps(mtmp, ALL_TRAPS);
/* locations where monsters are already experienced with wands */
if (Is_stronghold(&u.uz) || Is_knox(&u.uz) || In_endgame(&u.uz) ||
In_hell(&u.uz) || In_V_tower(&u.uz) || In_quest(&u.uz))
mtmp->mwandexp = TRUE;
place_monster(mtmp, x, y);
mtmp->mcansee = mtmp->mcanmove = TRUE;

View File

@@ -1094,7 +1094,8 @@ breamm(struct monst *mtmp, struct attack *mattk, struct monst *mtarg)
Monnam(mtmp), breathwep_name(typ));
gb.buzzer = mtmp;
dobuzz(BZ_M_BREATH(BZ_OFS_AD(typ)), (int) mattk->damn,
mtmp->mx, mtmp->my, sgn(gt.tbx), sgn(gt.tby), utarget, utarget);
mtmp->mx, mtmp->my, sgn(gt.tbx), sgn(gt.tby),
utarget, utarget, FALSE);
gb.buzzer = 0;
nomul(0);
/* breath runs out sometimes. Also, give monster some

View File

@@ -31,6 +31,7 @@ staticfn boolean hero_behind_chokepoint(struct monst *);
staticfn boolean mon_has_friends(struct monst *);
staticfn boolean mon_likes_objpile_at(struct monst *mtmp, coordxy x, coordxy y) NONNULLARG1;
staticfn int mbhitm(struct monst *, struct obj *);
staticfn void buzz_force_miss(int, int, coordxy, coordxy, int, int);
staticfn boolean fhito_loc(struct obj *obj, coordxy x, coordxy y,
int (*fhito)(OBJ_P, OBJ_P));
staticfn void mbhit(struct monst *, int, int (*)(MONST_P, OBJ_P),
@@ -1614,7 +1615,8 @@ mbhitm(struct monst *mtmp, struct obj *otmp)
Soundeffect(se_boing, 40);
pline("Boing!");
learnit = TRUE;
} else if (rnd(20) < 10 + u.uac) {
} else if (rnd(20) < 10 + u.uac &&
!(gb.buzzer && !gb.buzzer->mwandexp)) {
monstunseesu(M_SEEN_MAGR); /* mons see hero not resisting */
pline_The("wand hits you!");
tmp = d(2, 12);
@@ -1809,6 +1811,12 @@ mbhit(
}
}
staticfn void
buzz_force_miss(int type, int nd, coordxy sx, coordxy sy, int dx, int dy)
{
dobuzz(type, nd, sx, sy, dx, dy, TRUE, FALSE, TRUE);
}
/* Perform an offensive action for a monster. Must be called immediately
* after find_offensive(). Return values are same as use_defensive().
*/
@@ -1819,6 +1827,12 @@ use_offensive(struct monst *mtmp)
struct obj *otmp = gm.m.offensive;
boolean oseen;
/* if a monster has never used an attack wand before, it takes them some
time to get used to holding that much power, so the first shot always
misses */
void (*buzzfn)(int, int, coordxy, coordxy, int, int) =
mtmp->mwandexp ? buzz : buzz_force_miss;
/* offensive potions are not drunk, they're thrown */
if (otmp->oclass != POTION_CLASS && (i = precheck(mtmp, otmp)) != 0)
return i;
@@ -1837,12 +1851,13 @@ use_offensive(struct monst *mtmp)
gm.m_using = TRUE;
gc.current_wand = otmp;
gb.buzzer = mtmp;
buzz(BZ_M_WAND(BZ_OFS_WAN(otmp->otyp)),
(otmp->otyp == WAN_MAGIC_MISSILE) ? 2 : 6, mtmp->mx, mtmp->my,
sgn(mtmp->mux - mtmp->mx), sgn(mtmp->muy - mtmp->my));
buzzfn(BZ_M_WAND(BZ_OFS_WAN(otmp->otyp)),
(otmp->otyp == WAN_MAGIC_MISSILE) ? 2 : 6, mtmp->mx, mtmp->my,
sgn(mtmp->mux - mtmp->mx), sgn(mtmp->muy - mtmp->my));
gb.buzzer = 0;
gc.current_wand = 0;
gm.m_using = FALSE;
mtmp->mwandexp = TRUE;
return (DEADMONSTER(mtmp)) ? 1 : 2;
case MUSE_FIRE_HORN:
case MUSE_FROST_HORN:
@@ -1850,13 +1865,14 @@ use_offensive(struct monst *mtmp)
gm.m_using = TRUE;
gb.buzzer = mtmp;
gc.current_wand = otmp; /* needed by zhitu() */
buzz(BZ_M_WAND(BZ_OFS_AD((otmp->otyp == FROST_HORN) ? AD_COLD
: AD_FIRE)),
buzzfn(BZ_M_WAND(BZ_OFS_AD(
(otmp->otyp == FROST_HORN) ? AD_COLD : AD_FIRE)),
rn1(6, 6), mtmp->mx, mtmp->my, sgn(mtmp->mux - mtmp->mx),
sgn(mtmp->muy - mtmp->my));
gb.buzzer = 0;
gc.current_wand = 0;
gm.m_using = FALSE;
mtmp->mwandexp = TRUE;
return (DEADMONSTER(mtmp)) ? 1 : 2;
case MUSE_WAN_TELEPORTATION:
case MUSE_WAN_UNDEAD_TURNING:
@@ -1864,9 +1880,13 @@ use_offensive(struct monst *mtmp)
gz.zap_oseen = oseen;
mzapwand(mtmp, otmp, FALSE);
gm.m_using = TRUE;
gb.buzzer = mtmp;
mbhit(mtmp, rn1(8, 6), mbhitm, bhito, otmp);
gb.buzzer = 0;
/* note: 'otmp' might have been destroyed (drawbridge destruction) */
gm.m_using = FALSE;
if (gm.m.has_offense == MUSE_WAN_STRIKING)
mtmp->mwandexp = TRUE;
return 2;
case MUSE_SCR_EARTH: {
/* TODO: handle steeds */

View File

@@ -4750,13 +4750,13 @@ disintegrate_mon(
void
ubuzz(int type, int nd)
{
dobuzz(type, nd, u.ux, u.uy, u.dx, u.dy, TRUE, FALSE);
dobuzz(type, nd, u.ux, u.uy, u.dx, u.dy, TRUE, FALSE, FALSE);
}
void
buzz(int type, int nd, coordxy sx, coordxy sy, int dx, int dy)
{
dobuzz(type, nd, sx, sy, dx, dy, TRUE, FALSE);
dobuzz(type, nd, sx, sy, dx, dy, TRUE, FALSE, FALSE);
}
/*
@@ -4774,7 +4774,8 @@ dobuzz(
int nd, /* damage strength ('number of dice') */
coordxy sx, coordxy sy, /* starting point */
int dx, int dy, /* direction delta */
boolean sayhit, boolean saymiss) /* report out of sight hit/miss events */
boolean sayhit, boolean saymiss, /* report out of sight hit/miss events */
boolean forcemiss)
{
int range, fltyp = zaptype(type), damgtype = fltyp % 10;
coordxy lsx, lsy;
@@ -4860,7 +4861,7 @@ dobuzz(
buzzmonst:
gn.notonhead = (mon->mx != gb.bhitpos.x
|| mon->my != gb.bhitpos.y);
if (zap_hit(find_mac(mon), spell_type)) {
if (!forcemiss && zap_hit(find_mac(mon), spell_type)) {
if (mon_reflects(mon, (char *) 0)) {
if (cansee(mon->mx, mon->my)) {
hit(flash_str(fltyp, FALSE), mon, exclam(0));
@@ -4950,7 +4951,7 @@ dobuzz(
if (u.usteed && !rn2(3) && !mon_reflects(u.usteed, (char *) 0)) {
mon = u.usteed;
goto buzzmonst;
} else if (zap_hit((int) u.uac, 0)) {
} else if (!forcemiss && zap_hit((int) u.uac, 0)) {
range -= 2;
pline_dir(xytodir(-dx, -dy), "%s hits you!",
The(flash_str(fltyp, FALSE)));