From 4d043a6f9ce2d04999e5d0b52cf934cda790addb Mon Sep 17 00:00:00 2001 From: Alex Smith Date: Thu, 19 Mar 2026 03:46:42 +0000 Subject: [PATCH] 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. --- doc/fixes3-7-0.txt | 1 + include/extern.h | 3 ++- include/monst.h | 2 ++ src/makemon.c | 4 ++++ src/mthrowu.c | 3 ++- src/muse.c | 32 ++++++++++++++++++++++++++------ src/zap.c | 11 ++++++----- 7 files changed, 43 insertions(+), 13 deletions(-) diff --git a/doc/fixes3-7-0.txt b/doc/fixes3-7-0.txt index 0b9220403..d32fda191 100644 --- a/doc/fixes3-7-0.txt +++ b/doc/fixes3-7-0.txt @@ -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 diff --git a/include/extern.h b/include/extern.h index 7c5dd9a49..69fca8969 100644 --- a/include/extern.h +++ b/include/extern.h @@ -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; diff --git a/include/monst.h b/include/monst.h index 15114c600..f1fb3b9c2 100644 --- a/include/monst.h +++ b/include/monst.h @@ -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 diff --git a/src/makemon.c b/src/makemon.c index 130694401..4f12e12cb 100644 --- a/src/makemon.c +++ b/src/makemon.c @@ -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; diff --git a/src/mthrowu.c b/src/mthrowu.c index 613150656..88066266d 100644 --- a/src/mthrowu.c +++ b/src/mthrowu.c @@ -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 diff --git a/src/muse.c b/src/muse.c index aa0474e4e..2d5e82bd4 100644 --- a/src/muse.c +++ b/src/muse.c @@ -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 */ diff --git a/src/zap.c b/src/zap.c index adecdbeca..12c482227 100644 --- a/src/zap.c +++ b/src/zap.c @@ -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)));