From 0bdf9830e60decbaf564991fa106400753cd9825 Mon Sep 17 00:00:00 2001 From: nhmall Date: Sun, 16 Mar 2025 15:37:49 -0400 Subject: [PATCH] throw-and-return weapons used by monsters Resolves #1338 --- include/extern.h | 2 + include/hack.h | 6 ++ include/mfndpos.h | 36 ++++----- src/dothrow.c | 7 +- src/mhitu.c | 7 +- src/monmove.c | 63 +++++++++++----- src/mthrowu.c | 182 +++++++++++++++++++++++++++++++++++++++++++--- src/weapon.c | 52 ++++++++++++- 8 files changed, 301 insertions(+), 54 deletions(-) diff --git a/include/extern.h b/include/extern.h index 78ca5fd26..2290f9711 100644 --- a/include/extern.h +++ b/include/extern.h @@ -3648,6 +3648,8 @@ extern int weapon_hit_bonus(struct obj *) NO_NNARGS; extern int weapon_dam_bonus(struct obj *) NO_NNARGS; extern void skill_init(const struct def_skill *) NONNULLARG1; extern void setmnotwielded(struct monst *, struct obj *) NONNULLARG1; +extern const struct throw_and_return_weapon *autoreturn_weapon(struct obj *) + NONNULLARG1; /* ### were.c ### */ diff --git a/include/hack.h b/include/hack.h index d356d64c5..3e0b9c306 100644 --- a/include/hack.h +++ b/include/hack.h @@ -868,6 +868,12 @@ enum stoning_checks { st_all = (st_gloves | st_corpse | st_petrifies | st_resists) }; +struct throw_and_return_weapon { + short otyp; + int range; + Bitfield(tethered, 1); +}; + struct trapinfo { struct obj *tobj; coordxy tx, ty; diff --git a/include/mfndpos.h b/include/mfndpos.h index a49f13262..762f5cdb9 100644 --- a/include/mfndpos.h +++ b/include/mfndpos.h @@ -6,26 +6,28 @@ #ifndef MFNDPOS_H #define MFNDPOS_H -#define ALLOW_MDISP 0x00001000L /* can displace a monster out of its way */ -#define ALLOW_TRAPS 0x00020000L /* can enter traps */ -#define ALLOW_U 0x00040000L /* can attack you */ -#define ALLOW_M 0x00080000L /* can attack other monsters */ -#define ALLOW_TM 0x00100000L /* can attack tame monsters */ +/* clang-format off */ +#define ALLOW_MDISP 0x00001000L /* can displace a monster out of its way */ +#define ALLOW_TRAPS 0x00020000L /* can enter traps */ +#define ALLOW_U 0x00040000L /* can attack you */ +#define ALLOW_M 0x00080000L /* can attack other monsters */ +#define ALLOW_TM 0x00100000L /* can attack tame monsters */ #define ALLOW_ALL (ALLOW_U | ALLOW_M | ALLOW_TM | ALLOW_TRAPS) -#define NOTONL 0x00200000L /* avoids direct line to player */ -#define OPENDOOR 0x00400000L /* opens closed doors */ -#define UNLOCKDOOR 0x00800000L /* unlocks locked doors */ -#define BUSTDOOR 0x01000000L /* breaks any doors */ -#define ALLOW_ROCK 0x02000000L /* pushes rocks */ -#define ALLOW_WALL 0x04000000L /* walks thru walls */ -#define ALLOW_DIG 0x08000000L /* digs */ -#define ALLOW_BARS 0x10000000L /* may pass thru iron bars */ -#define ALLOW_SANCT 0x20000000L /* enters temples */ -#define ALLOW_SSM 0x40000000L /* ignores scare monster */ +#define NOTONL 0x00200000L /* avoids direct line to player */ +#define OPENDOOR 0x00400000L /* opens closed doors */ +#define UNLOCKDOOR 0x00800000L /* unlocks locked doors */ +#define BUSTDOOR 0x01000000L /* breaks any doors */ +#define ALLOW_ROCK 0x02000000L /* pushes rocks */ +#define ALLOW_WALL 0x04000000L /* walks thru walls */ +#define ALLOW_DIG 0x08000000L /* digs */ +#define ALLOW_BARS 0x10000000L /* may pass thru iron bars */ +#define ALLOW_SANCT 0x20000000L /* enters temples */ +#define ALLOW_SSM 0x40000000L /* ignores scare monster */ #ifdef NHSTDC -#define NOGARLIC 0x80000000UL /* hates garlic */ +#define NOGARLIC 0x80000000UL /* hates garlic */ #else -#define NOGARLIC 0x80000000L /* hates garlic */ +#define NOGARLIC 0x80000000L /* hates garlic */ #endif +/* clang-format on */ #endif /* MFNDPOS_H */ diff --git a/src/dothrow.c b/src/dothrow.c index 28df7b30b..4c4665ef5 100644 --- a/src/dothrow.c +++ b/src/dothrow.c @@ -1476,10 +1476,11 @@ throwit(struct obj *obj, { struct monst *mon; int range, urange; + const struct throw_and_return_weapon *arw = autoreturn_weapon(obj); boolean crossbowing, impaired = (Confusion || Stunned || Blind || Hallucination || Fumbling), - tethered_weapon = (obj->otyp == AKLYS && (wep_mask & W_WEP) != 0); + tethered_weapon = (arw && arw->tethered && (wep_mask & W_WEP) != 0); gn.notonhead = FALSE; /* reset potentially stale value */ if ((obj->cursed || obj->greased) && (u.dx || u.dy) && !rn2(7)) { @@ -1616,9 +1617,9 @@ throwit(struct obj *obj, else if (is_art(obj, ART_MJOLLNIR)) range = (range + 1) / 2; /* it's heavy */ else if (tethered_weapon) /* primary weapon is aklys */ - /* if an aklys is going to return, range is limited by the + /* range of a tethered_weapon is limited by the length of the attached cord [implicit aspect of item] */ - range = min(range, BOLT_LIM / 2); + range = min(range, arw->range); else if (obj == uball && u.utrap && u.utraptype == TT_INFLOOR) range = 1; diff --git a/src/mhitu.c b/src/mhitu.c index 32cb68a14..363c19cd4 100644 --- a/src/mhitu.c +++ b/src/mhitu.c @@ -470,7 +470,7 @@ mattacku(struct monst *mtmp) struct permonst *mdat = mtmp->data; /* * ranged: Is it near you? Affects your actions. - * ranged2: Does it think it's near you? Affects its actions. + * range2: Does it think it's near you? Affects its actions. * foundyou: Is it attacking you or your image? * youseeit: Can you observe the attack? It might be attacking your * image around the corner, or invisible, or you might be blind. @@ -497,11 +497,10 @@ mattacku(struct monst *mtmp) return 0; u.ustuck->mux = u.ux; u.ustuck->muy = u.uy; - range2 = 0; - foundyou = 1; if (u.uinvulnerable) return 0; /* stomachs can't hurt you! */ - + range2 = 0; + foundyou = 1; } else if (u.usteed) { if (mtmp == u.usteed) /* Your steed won't attack you */ diff --git a/src/monmove.c b/src/monmove.c index 1c10644cb..17c7d7f83 100644 --- a/src/monmove.c +++ b/src/monmove.c @@ -23,7 +23,7 @@ staticfn int postmov(struct monst *, struct permonst *, coordxy, coordxy, int, unsigned, boolean, boolean, boolean) NONNULLPTRS; staticfn boolean leppie_avoidance(struct monst *); staticfn void leppie_stash(struct monst *); -staticfn boolean m_balks_at_approaching(struct monst *); +staticfn int m_balks_at_approaching(int, struct monst *, int *, int *); staticfn boolean stuff_prevents_passage(struct monst *); staticfn int vamp_shift(struct monst *, struct permonst *, boolean); staticfn void maybe_spin_web(struct monst *); @@ -1170,32 +1170,57 @@ leppie_stash(struct monst *mtmp) } } -/* does monster want to avoid you? */ -staticfn boolean -m_balks_at_approaching(struct monst *mtmp) +/* does monster want to avoid you? + * returns the original value of appr if not. + * returns -1 if so. + * returns -2 if monster wants to adhere to a particular range, + * which may actually be further away, + * and sets *pdistmin and *pdistmax to describe that range + */ +staticfn int +m_balks_at_approaching(int oldappr, struct monst *mtmp, int *pdistmin, + int *pdistmax) { + struct obj *mwep = MON_WEP(mtmp); + coordxy x = mtmp->mx, y = mtmp->my, ux = mtmp->mux, uy = mtmp->muy; + int edist = dist2(x, y, ux, uy); + const struct throw_and_return_weapon *arw; + + if (pdistmin) + *pdistmin = 0; + if (pdistmax) + *pdistmax = 0; + /* peaceful, far away, or can't see you */ - if (mtmp->mpeaceful - || (dist2(mtmp->mx, mtmp->my, mtmp->mux, mtmp->muy) >= 5*5) - || !m_canseeu(mtmp)) - return FALSE; + if (mtmp->mpeaceful || (edist >= 5 * 5) || !m_canseeu(mtmp)) + return oldappr; /* has ammo+launcher */ if (m_has_launcher_and_ammo(mtmp)) - return TRUE; + return -1; /* is using a polearm and in range */ if (MON_WEP(mtmp) && is_pole(MON_WEP(mtmp)) - && dist2(mtmp->mx, mtmp->my, mtmp->mux, mtmp->muy) <= MON_POLE_DIST) - return TRUE; + && edist <= MON_POLE_DIST) + return -1; + + /* is using a throw-and-return weapon; provide min and max preferred range + */ + if (mwep && (arw = autoreturn_weapon(mwep)) != 0) { + if (pdistmin) + *pdistmin = 2 * 2; + if (pdistmax) + *pdistmax = arw->range * arw->range; + return -2; + } /* can attack from distance, and hp loss or attack not used */ if (ranged_attk_available(mtmp) && ((mtmp->mhp < (mtmp->mhpmax+1) / 3) || !mtmp->mspec_used)) - return TRUE; + return -1; - return FALSE; + return oldappr; /* leaves appr unchanged */ } staticfn boolean @@ -1697,7 +1722,8 @@ m_move(struct monst *mtmp, int after) boolean better_with_displacing = FALSE; unsigned seenflgs; struct permonst *ptr; - int chi, mmoved = MMOVE_NOTHING; /* not strictly nec.: chi >= 0 will do */ + int chi, mmoved = MMOVE_NOTHING, /* not strictly nec.: chi >= 0 will do */ + preferredrange_min = 0, preferredrange_max = 0; long info[9]; long flag; coordxy omx = mtmp->mx, omy = mtmp->my; @@ -1848,8 +1874,7 @@ m_move(struct monst *mtmp, int after) appr = -1; /* hostiles with ranged weapon or attack try to stay away */ - if (m_balks_at_approaching(mtmp)) - appr = -1; + appr = m_balks_at_approaching(appr, mtmp, &preferredrange_min, &preferredrange_max); if (!should_see && can_track(ptr)) { coord *cp; @@ -1942,7 +1967,11 @@ m_move(struct monst *mtmp, int after) nearer = ((ndist = dist2(nx, ny, ggx, ggy)) < nidist); if ((appr == 1 && nearer) || (appr == -1 && !nearer) - || (!appr && !rn2(++chcnt)) || (mmoved == MMOVE_NOTHING)) { + || (!appr && !rn2(++chcnt)) + || (appr == -2 + && ((ndist <= preferredrange_min && !nearer) + || (ndist >= preferredrange_max && nearer))) + || (mmoved == MMOVE_NOTHING)) { nix = nx; niy = ny; nidist = ndist; diff --git a/src/mthrowu.c b/src/mthrowu.c index 6dc82377e..c22ba9340 100644 --- a/src/mthrowu.c +++ b/src/mthrowu.c @@ -12,6 +12,7 @@ staticfn const char *breathwep_name(int); staticfn boolean drop_throw(struct obj *, boolean, coordxy, coordxy); staticfn boolean blocking_terrain(coordxy, coordxy); staticfn int m_lined_up(struct monst *, struct monst *) NONNULLARG12; +staticfn void return_from_mtoss(struct monst *, struct obj *, boolean); #define URETREATING(x, y) \ (distmin(u.ux, u.uy, x, y) > distmin(u.ux0, u.uy0, x, y)) @@ -558,6 +559,10 @@ m_throw( boolean forcehit; char sym = obj->oclass; int hitu = 0, oldumort, blindinc = 0; + const struct throw_and_return_weapon *arw = autoreturn_weapon(obj); + boolean tethered_weapon = + (obj == MON_WEP(mon) && arw && arw->tethered != 0), + return_flightpath = FALSE; gb.bhitpos.x = x; gb.bhitpos.y = y; @@ -619,8 +624,30 @@ m_throw( * early to avoid the dagger bug, anyone who modifies this code should * be careful not to use either one after it's been freed. */ - if (sym) - tmp_at(DISP_FLASH, obj_to_glyph(singleobj, rn2_on_display_rng)); + if (sym) { + if (!tethered_weapon) { + tmp_at(DISP_FLASH, obj_to_glyph(singleobj, rn2_on_display_rng)); + } else { + tmp_at(DISP_TETHER, obj_to_glyph(singleobj, rn2_on_display_rng)); + /* + * Considerations for a tethered object based on in throwit()/bhit() : + * - wall of water/lava will stop items, and triggers return. + * - iron bars will stop items, and triggers return. + * - pass harmlessly through shades. + * X stops forward motion at hit monster/hero, triggers return. + * - closed door will stop item's forward motion, triggers return. + * - sinks stop forward motion, triggers fall, then return. + * - object can get tangled in a web, no return (tether snaps?). + * On return: + * X rn2(100) chance of returning to thrower's location. + * X if impaired and rn2(100) == 0, + * -50/50 chance of landing on the ground. + * -50/50 chance of hitting the thrower and causing + * rnd(3) damage. + * + */ + } + } while (range-- > 0) { /* Actually the loop is always exited by break */ singleobj->ox = gb.bhitpos.x += dx; singleobj->oy = gb.bhitpos.y += dy; @@ -730,7 +757,12 @@ m_throw( } stop_occupation(); if (hitu) { - (void) drop_throw(singleobj, hitu, u.ux, u.uy); + if (!tethered_weapon) { + (void) drop_throw(singleobj, hitu, u.ux, u.uy); + } else { + /* ready for return journey */ + return_flightpath = TRUE; + } break; } } @@ -751,8 +783,12 @@ m_throw( && (cansee(gb.bhitpos.x, gb.bhitpos.y) || (gm.marcher && canseemon(gm.marcher)))) pline("%s misses.", The(mshot_xname(singleobj))); - - (void) drop_throw(singleobj, 0, gb.bhitpos.x, gb.bhitpos.y); + if (!tethered_weapon) { + (void) drop_throw(singleobj, 0, gb.bhitpos.x, gb.bhitpos.y); + } else { + /*ready for return journey */ + return_flightpath = TRUE; + } } break; } @@ -761,7 +797,11 @@ m_throw( } tmp_at(gb.bhitpos.x, gb.bhitpos.y); nh_delay_output(); - tmp_at(DISP_END, 0); + if (arw && return_flightpath) + return_from_mtoss(mon, singleobj, tethered_weapon); + /* mon could be DEADMONSTER now */ + else + tmp_at(DISP_END, 0); gm.mesg_given = 0; /* reset */ if (blindinc) { @@ -777,6 +817,121 @@ m_throw( #undef MT_FLIGHTCHECK +staticfn void +return_from_mtoss(struct monst *magr, struct obj *otmp, boolean tethered_weapon) +{ + boolean impaired = (magr->mconf || magr->mstun || magr->mblinded), + notcaught = FALSE, hits_thrower = FALSE; + coordxy x = gb.bhitpos.x, y = gb.bhitpos.y; + int made_it_back = rn2(100), dmg = 0; + + if (otmp && made_it_back) { + /* it made it back to thrower's location */ + if (tethered_weapon) { + tmp_at(DISP_END, BACKTRACK); + } else { + int dx = sgn(x - magr->mx), + dy = sgn(y - magr->my); + + if (x != magr->mx || y != magr->my) { + tmp_at(DISP_FLASH, obj_to_glyph(otmp, rn2_on_display_rng)); + while (isok(x, y) && (x != magr->mx || y != magr->my)) { + tmp_at(x, y); + nh_delay_output(); + x -= dx; + y -= dy; + } + tmp_at(DISP_END, 0); + } + } + x = magr->mx; + y = magr->my; + if (!impaired && rn2(100)) { + static long do_not_annoy = 0; + + if (!do_not_annoy || (svm.moves - do_not_annoy) > 500) { + pline("%s to %s %s!", Tobjnam(otmp, "return"), + s_suffix(mon_nam(magr)), mbodypart(magr, HAND)); + do_not_annoy = svm.moves; + } + if (otmp) { + add_to_minv(magr, otmp); + if (tethered_weapon) { + magr->mw = otmp; + otmp->owornmask |= W_WEP; + } + } + if (cansee(x, y)) + newsym(x, y); + } else { + boolean mlevitating = FALSE; /* msg future-proofing only */ + + dmg = rn2(2); + if (!dmg) { + if (!Blind) { + pline("%s back to %s, landing %s %s %s.", + Tobjnam(otmp, "return"), mon_nam(magr), + mlevitating ? "beneath" : "at", mhis(magr), + makeplural(mbodypart(magr, FOOT))); + } else if (!Deaf) { + You_hear("%s land near %s.", Something, mon_nam(magr)); + } + } else { + dmg += rnd(3); + if (!Blind) { + pline("%s back toward %s, hitting %s %s!", + Tobjnam(otmp, "fly"), + mon_nam(magr), + mhis(magr), + body_part(ARM)); + } else if (!Deaf) { + You_hear("%s hit %s with a thud!", something, + mon_nam(magr)); + } + hits_thrower = TRUE; + } + notcaught = TRUE; + } + } else { + /* it didn't make it back to thrower's location */ + if (tethered_weapon) + tmp_at(DISP_END, 0); + You_hear("a loud snap!"); + notcaught = TRUE; + } + if (otmp) { + if (hits_thrower) { + if (otmp->oartifact) + (void) artifact_hit((struct monst *) 0, magr, otmp, &dmg, 0); + magr->mhp -= dmg; + /* magr could be a DEADMONSTER now */ + } + if (notcaught) { + (void) snuff_candle(otmp); + if (!ship_object(otmp, x, y, FALSE)) { + if (flooreffects(otmp, x, y, "drop")) { + if (cansee(x, y)) + newsym(x, y); + return; + } + place_object(otmp, x, y); + stackobj(otmp); + } + if (!Deaf && !Underwater) { + /* Some sound effects when item lands in water or lava */ + if (is_pool(x, y) || (is_lava(x, y) && !is_flammable(otmp))) { + Soundeffect(se_splash, 50); + pline((weight(otmp) > 9) ? "Splash!" : "Plop!"); + } + } + if (obj_sheds_light(otmp)) + gv.vision_full_recalc = 1; + } + } + if (cansee(x, y)) + newsym(x, y); +} + /* Monster throws item at another monster */ int thrwmm(struct monst *mtmp, struct monst *mtarg) @@ -988,6 +1143,9 @@ thrwmu(struct monst *mtmp) struct obj *otmp, *mwep; coordxy x, y; const char *onm; + int rang; + const struct throw_and_return_weapon *arw; + boolean always_toss = FALSE; /* Rearranged beginning so monsters can use polearms not in a line */ if (mtmp->weapon_check == NEED_WEAPON || !MON_WEP(mtmp)) { @@ -1003,10 +1161,10 @@ thrwmu(struct monst *mtmp) return; if (is_pole(otmp)) { - int dam, hitv, rang; + int dam, hitv; if (otmp != MON_WEP(mtmp)) - return; /* polearm must be wielded */ + return; /* polearm, aklys must be wielded */ /* * MON_POLE_DIST encompasses knight's move range (5): two spots @@ -1047,6 +1205,11 @@ thrwmu(struct monst *mtmp) (void) thitu(hitv, dam, &otmp, (char *) 0); stop_occupation(); return; + } else if ((arw = autoreturn_weapon(otmp)) != 0 && !mwelded(otmp)) { + rang = dist2(mtmp->mx, mtmp->my, mtmp->mux, mtmp->muy); + if (rang > arw->range || !couldsee(mtmp->mx, mtmp->my)) + return; /* Out of range, or intervening wall */ + always_toss = TRUE; } x = mtmp->mx; @@ -1058,7 +1221,8 @@ thrwmu(struct monst *mtmp) */ if (!lined_up(mtmp) || (URETREATING(x, y) - && rn2(BOLT_LIM - distmin(x, y, mtmp->mux, mtmp->muy)))) + && (!always_toss + && rn2(BOLT_LIM - distmin(x, y, mtmp->mux, mtmp->muy))))) return; mwep = MON_WEP(mtmp); /* wielded weapon */ diff --git a/src/weapon.c b/src/weapon.c index 82c980ec4..af6beac22 100644 --- a/src/weapon.c +++ b/src/weapon.c @@ -493,21 +493,38 @@ oselect(struct monst *mtmp, int type) return (struct obj *) 0; } -/* TODO: have monsters use aklys' throw-and-return */ static NEARDATA const int rwep[] = { DWARVISH_SPEAR, SILVER_SPEAR, ELVEN_SPEAR, SPEAR, ORCISH_SPEAR, JAVELIN, SHURIKEN, YA, SILVER_ARROW, ELVEN_ARROW, ARROW, ORCISH_ARROW, CROSSBOW_BOLT, SILVER_DAGGER, ELVEN_DAGGER, DAGGER, ORCISH_DAGGER, KNIFE, - FLINT, ROCK, LOADSTONE, LUCKSTONE, DART, - /* BOOMERANG, */ CREAM_PIE + FLINT, ROCK, LOADSTONE, LUCKSTONE, DART, CREAM_PIE, }; +/* polearms */ static NEARDATA const int pwep[] = { HALBERD, BARDICHE, SPETUM, BILL_GUISARME, VOULGE, RANSEUR, GUISARME, GLAIVE, LUCERN_HAMMER, BEC_DE_CORBIN, FAUCHARD, PARTISAN, LANCE }; +/* throw-and-return weapons */ +static NEARDATA const struct throw_and_return_weapon arwep[] = { + /* { BOOMERANG, 5, 0 }, */ + { AKLYS, (BOLT_LIM / 2), 1 }, +}; + +const struct throw_and_return_weapon * +autoreturn_weapon(struct obj *otmp) +{ + int i; + + for (i = 0; i < SIZE(arwep); i++) { + if (otmp->otyp == arwep[i].otyp) + return &arwep[i]; + } + return (struct throw_and_return_weapon *) 0; +} + /* select a ranged weapon for the monster */ struct obj * select_rwep(struct monst *mtmp) @@ -557,9 +574,31 @@ select_rwep(struct monst *mtmp) } } } + /* Next, try to select a throw-and-return weapon, since they are + * also not as expendable. Again, don't pick one if monster's + * weapon is welded. + */ + for (i = 0; i < SIZE(arwep); i++) { + const struct throw_and_return_weapon *arw = &arwep[i]; + + if (!mindless(mtmp->data) && !is_animal(mtmp->data) && !mweponly + && dist2(mtmp->mx, mtmp->my, mtmp->mux, mtmp->muy) <= arw->range + && couldsee(mtmp->mx, mtmp->my)) { + if ((((mtmp->misc_worn_check & W_ARMS) == 0) + || !objects[arw->otyp].oc_bimanual) + && (objects[arw->otyp].oc_material != SILVER + || !mon_hates_silver(mtmp))) { + if ((otmp = oselect(mtmp, arw->otyp)) != 0 + && (otmp == mwep || !mweponly)) { + gp.propellor = otmp; /* force the monster to wield it */ + return otmp; + } + } + } + } /* - * other than these two specific cases, always select the + * other than the specific cases above, always select the * most potent ranged weapon to hand. */ for (i = 0; i < SIZE(rwep); i++) { @@ -840,10 +879,15 @@ mon_wield_item(struct monst *mon) mon->weapon_check = NEED_WEAPON; if (canseemon(mon)) { boolean newly_welded; + const struct throw_and_return_weapon *arw; pline_mon(mon, "%s wields %s%c", Monnam(mon), doname(obj), exclaim ? '!' : '.'); + if ((arw = autoreturn_weapon(obj)) != 0 && arw->tethered != 0) + pline_mon(mon, "%s secures the tether on %s.", Monnam(mon), + the(xname(obj))); + /* 3.6.3: mwelded() predicate expects the object to have its W_WEP bit set in owormmask, but the pline here and for artifact_light don't want that because they'd have '(weapon