throw-and-return weapons used by monsters

Resolves #1338
This commit is contained in:
nhmall
2025-03-16 15:37:49 -04:00
parent 7c43654580
commit 0bdf9830e6
8 changed files with 301 additions and 54 deletions

View File

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

View File

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

View File

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

View File

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

View File

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