Files
nethack/src/dogmove.c
2023-09-30 22:44:46 +03:00

1482 lines
52 KiB
C

/* NetHack 3.7 dogmove.c $NHDT-Date: 1646688063 2022/03/07 21:21:03 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.112 $ */
/* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */
/*-Copyright (c) Robert Patrick Rankin, 2012. */
/* NetHack may be freely redistributed. See license for details. */
#include "hack.h"
#include "mfndpos.h"
#define DOG_HUNGRY 300
#define DOG_WEAK 500
#define DOG_STARVE 750
static void dog_starve(struct monst *);
static boolean dog_hunger(struct monst *, struct edog *);
static int dog_invent(struct monst *, struct edog *, int);
static int dog_goal(struct monst *, struct edog *, int, int, int);
static struct monst *find_targ(struct monst *, int, int, int);
static int find_friends(struct monst *, struct monst *, int);
static struct monst *best_target(struct monst *);
static long score_targ(struct monst *, struct monst *);
static boolean can_reach_location(struct monst *, coordxy, coordxy, coordxy,
coordxy);
/* pick a carried item for pet to drop */
struct obj *
droppables(struct monst *mon)
{
/*
* 'key|pickaxe|&c = &dummy' is used to make various creatures
* that can't use a key/pick-axe/&c behave as if they are already
* holding one so that any other such item in their inventory will
* be considered a duplicate and get treated as a normal candidate
* for dropping.
*
* This could be 'auto', but then 'gcc -O2' warns that this function
* might return the address of a local variable. It's mistaken,
* &dummy is never returned. 'static' is simplest way to shut it up.
*/
static struct obj dummy;
struct obj *obj, *wep, *pickaxe, *unihorn, *key;
dummy = cg.zeroobj;
dummy.otyp = GOLD_PIECE; /* not STRANGE_OBJECT or tools of interest */
dummy.oartifact = 1; /* so real artifact won't override "don't keep it" */
pickaxe = unihorn = key = (struct obj *) 0;
wep = MON_WEP(mon);
if (is_animal(mon->data) || mindless(mon->data)) {
/* won't hang on to any objects of these types */
pickaxe = unihorn = key = &dummy; /* act as if already have them */
} else {
/* don't hang on to pick-axe if can't use one or don't need one */
if (!tunnels(mon->data) || !needspick(mon->data))
pickaxe = &dummy;
/* don't hang on to key if can't open doors */
if (nohands(mon->data) || verysmall(mon->data))
key = &dummy;
}
if (wep) {
if (is_pick(wep))
pickaxe = wep;
if (wep->otyp == UNICORN_HORN)
unihorn = wep;
/* don't need any wielded check for keys... */
}
for (obj = mon->minvent; obj; obj = obj->nobj) {
switch (obj->otyp) {
case DWARVISH_MATTOCK:
/* reject mattock if couldn't wield it */
if (which_armor(mon, W_ARMS))
break;
/* keep mattock in preference to pick unless pick is already
wielded or is an artifact and mattock isn't */
if (pickaxe && pickaxe->otyp == PICK_AXE && pickaxe != wep
&& (!pickaxe->oartifact || obj->oartifact))
return pickaxe; /* drop the one we earlier decided to keep */
/*FALLTHRU*/
case PICK_AXE:
if (!pickaxe || (obj->oartifact && !pickaxe->oartifact)) {
if (pickaxe)
return pickaxe;
pickaxe = obj; /* keep this digging tool */
continue;
}
break;
case UNICORN_HORN:
/* reject cursed unicorn horns */
if (obj->cursed)
break;
/* keep artifact unihorn in preference to ordinary one */
if (!unihorn || (obj->oartifact && !unihorn->oartifact)) {
if (unihorn)
return unihorn;
unihorn = obj; /* keep this unicorn horn */
continue;
}
break;
case SKELETON_KEY:
/* keep key in preference to lock-pick */
if (key && key->otyp == LOCK_PICK
&& (!key->oartifact || obj->oartifact))
return key; /* drop the one we earlier decided to keep */
/*FALLTHRU*/
case LOCK_PICK:
/* keep lock-pick in preference to credit card */
if (key && key->otyp == CREDIT_CARD
&& (!key->oartifact || obj->oartifact))
return key;
/*FALLTHRU*/
case CREDIT_CARD:
if (!key || (obj->oartifact && !key->oartifact)) {
if (key)
return key;
key = obj; /* keep this unlocking tool */
continue;
}
break;
default:
break;
}
if (!obj->owornmask && obj != wep)
return obj;
}
return (struct obj *) 0; /* don't drop anything */
}
static NEARDATA const char nofetch[] = { BALL_CLASS, CHAIN_CLASS, ROCK_CLASS,
0 };
static void wantdoor(coordxy, coordxy, genericptr_t);
boolean
cursed_object_at(coordxy x, coordxy y)
{
struct obj *otmp;
for (otmp = gl.level.objects[x][y]; otmp; otmp = otmp->nexthere)
if (otmp->cursed)
return TRUE;
return FALSE;
}
int
dog_nutrition(struct monst *mtmp, struct obj *obj)
{
int nutrit;
/*
* It is arbitrary that the pet takes the same length of time to eat
* as a human, but gets more nutritional value.
*/
if (obj->oclass == FOOD_CLASS) {
if (obj->otyp == CORPSE) {
mtmp->meating = 3 + (mons[obj->corpsenm].cwt >> 6);
nutrit = mons[obj->corpsenm].cnutrit;
} else {
mtmp->meating = objects[obj->otyp].oc_delay;
nutrit = objects[obj->otyp].oc_nutrition;
}
switch (mtmp->data->msize) {
case MZ_TINY:
nutrit *= 8;
break;
case MZ_SMALL:
nutrit *= 6;
break;
default:
case MZ_MEDIUM:
nutrit *= 5;
break;
case MZ_LARGE:
nutrit *= 4;
break;
case MZ_HUGE:
nutrit *= 3;
break;
case MZ_GIGANTIC:
nutrit *= 2;
break;
}
if (obj->oeaten) {
mtmp->meating = eaten_stat(mtmp->meating, obj);
nutrit = eaten_stat(nutrit, obj);
}
} else if (obj->oclass == COIN_CLASS) {
mtmp->meating = (int) (obj->quan / 2000) + 1;
if (mtmp->meating < 0)
mtmp->meating = 1;
nutrit = (int) (obj->quan / 20);
if (nutrit < 0)
nutrit = 0;
} else {
/* Unusual pet such as gelatinous cube eating odd stuff.
* meating made consistent with wild monsters in mon.c.
* nutrit made consistent with polymorphed player nutrit in
* eat.c. (This also applies to pets eating gold.)
*/
mtmp->meating = obj->owt / 20 + 1;
nutrit = 5 * objects[obj->otyp].oc_nutrition;
}
return nutrit;
}
/* returns 2 if pet dies, otherwise 1 */
int
dog_eat(struct monst *mtmp,
struct obj *obj, /* if unpaid, then thrown or kicked by hero */
coordxy x, /* dog's starting location, */
coordxy y, /* might be different from current */
boolean devour)
{
register struct edog *edog = EDOG(mtmp);
int nutrit, res;
long oprice;
char objnambuf[BUFSZ], *obj_name;
objnambuf[0] = '\0';
if (edog->hungrytime < gm.moves)
edog->hungrytime = gm.moves;
nutrit = dog_nutrition(mtmp, obj);
if (devour) {
if (mtmp->meating > 1)
mtmp->meating /= 2;
if (nutrit > 1)
nutrit = (nutrit * 3) / 4;
}
edog->hungrytime += nutrit;
mtmp->mconf = 0;
if (edog->mhpmax_penalty) {
/* no longer starving */
mtmp->mhpmax += edog->mhpmax_penalty;
edog->mhpmax_penalty = 0;
}
if (mtmp->mflee && mtmp->mfleetim > 1)
mtmp->mfleetim /= 2;
if (mtmp->mtame < 20)
mtmp->mtame++;
if (x != mtmp->mx || y != mtmp->my) { /* moved & ate on same turn */
newsym(x, y);
newsym(mtmp->mx, mtmp->my);
}
if (mtmp->data == &mons[PM_KILLER_BEE]
&& obj->otyp == LUMP_OF_ROYAL_JELLY
&& (res = bee_eat_jelly(mtmp, obj)) >= 0)
/* bypass most of dog_eat(), including apport update */
return (res + 1); /* 1 -> 2, 0 -> 1; -1, keep going */
/* food items are eaten one at a time; entire stack for other stuff */
if (obj->quan > 1L && obj->oclass == FOOD_CLASS)
obj = splitobj(obj, 1L);
if (obj->unpaid)
iflags.suppress_price++;
if (is_pool(mtmp->mx, mtmp->my) && !Underwater) {
/* Don't print obj */
/* TODO: Reveal presence of sea monster (especially sharks) */
} else {
/* food is at monster's current location, <mx,my>;
<x,y> was monster's location at start of this turn;
they might be the same but will be different when
the monster is moving+eating on same turn */
boolean seeobj = cansee(mtmp->mx, mtmp->my),
sawpet = cansee(x, y) && mon_visible(mtmp);
/* Observe the action if either the food location or the pet
itself is in view. When pet which was in view moves to an
unseen spot to eat the food there, avoid referring to that
pet as "it". However, we want "it" if invisible/unsensed
pet eats visible food. */
if (sawpet || (seeobj && canspotmon(mtmp))) {
/* call distant_name() for possible side-effects even if the
result won't be printed */
obj_name = distant_name(obj, doname);
if (tunnels(mtmp->data))
pline("%s digs in.", noit_Monnam(mtmp));
else
pline("%s %s %s.", noit_Monnam(mtmp),
devour ? "devours" : "eats", obj_name);
} else if (seeobj) {
obj_name = distant_name(obj, doname);
pline("It %s %s.", devour ? "devours" : "eats", obj_name);
}
}
if (obj->unpaid) {
Strcpy(objnambuf, xname(obj));
iflags.suppress_price--;
}
if (mtmp->data == &mons[PM_RUST_MONSTER] && obj->oerodeproof) {
/* The object's rustproofing is gone now */
if (obj->unpaid)
costly_alteration(obj, COST_DEGRD);
obj->oerodeproof = 0;
mtmp->mstun = 1;
if (canseemon(mtmp)) {
obj_name = distant_name(obj, doname); /* (see above) */
if (Verbose(0, dog_eat))
pline("%s spits %s out in disgust!",
Monnam(mtmp), obj_name);
}
} else {
/* It's a reward if it's DOGFOOD and the player dropped/threw it.
We know the player had it if invlet is set. -dlc */
if (dogfood(mtmp, obj) == DOGFOOD && obj->invlet)
edog->apport += (int) (200L / ((long) edog->dropdist + gm.moves
- edog->droptime));
if (obj->unpaid) {
/* edible item owned by shop has been thrown or kicked
by hero and caught by tame or food-tameable monst */
oprice = unpaid_cost(obj, TRUE);
pline("That %s will cost you %ld %s.", objnambuf, oprice,
currency(oprice));
/* m_consume_obj->delobj->obfree will handle actual shop billing update */
}
m_consume_obj(mtmp, obj);
}
return (DEADMONSTER(mtmp)) ? 2 : 1;
}
static void
dog_starve(struct monst *mtmp)
{
if (mtmp->mleashed && mtmp != u.usteed)
Your("leash goes slack.");
else if (cansee(mtmp->mx, mtmp->my))
pline("%s starves.", Monnam(mtmp));
else
You_feel("%s for a moment.",
Hallucination ? "bummed" : "sad");
mondied(mtmp);
}
/* hunger effects -- returns TRUE on starvation */
static boolean
dog_hunger(struct monst *mtmp, struct edog *edog)
{
if (gm.moves > edog->hungrytime + DOG_WEAK) {
if (!carnivorous(mtmp->data) && !herbivorous(mtmp->data)) {
edog->hungrytime = gm.moves + DOG_WEAK;
/* but not too high; it might polymorph */
} else if (!edog->mhpmax_penalty) {
/* starving pets are limited in healing */
int newmhpmax = mtmp->mhpmax / 3;
mtmp->mconf = 1;
edog->mhpmax_penalty = mtmp->mhpmax - newmhpmax;
mtmp->mhpmax = newmhpmax;
if (mtmp->mhp > mtmp->mhpmax)
mtmp->mhp = mtmp->mhpmax;
if (DEADMONSTER(mtmp)) {
dog_starve(mtmp);
return TRUE;
}
if (cansee(mtmp->mx, mtmp->my))
pline("%s is confused from hunger.", Monnam(mtmp));
else if (couldsee(mtmp->mx, mtmp->my))
beg(mtmp);
else
You_feel("worried about %s.", y_monnam(mtmp));
stop_occupation();
} else if (gm.moves > edog->hungrytime + DOG_STARVE
|| DEADMONSTER(mtmp)) {
dog_starve(mtmp);
return TRUE;
}
}
return FALSE;
}
/* do something with object (drop, pick up, eat) at current position
* returns 1 if object eaten (since that counts as dog's move), 2 if died
*/
static int
dog_invent(struct monst *mtmp, struct edog *edog, int udist)
{
coordxy omx, omy;
int carryamt = 0;
struct obj *obj, *otmp;
if (helpless(mtmp) || mtmp->meating)
return 0;
omx = mtmp->mx;
omy = mtmp->my;
/* If we are carrying something then we drop it (perhaps near @).
* Note: if apport == 1 then our behavior is independent of udist.
* Use udist+1 so steed won't cause divide by zero.
*/
if (droppables(mtmp)) {
if (!rn2(udist + 1) || !rn2(edog->apport))
if (rn2(10) < edog->apport) {
relobj(mtmp, (int) mtmp->minvis, TRUE);
if (edog->apport > 1)
edog->apport--;
edog->dropdist = udist; /* hpscdi!jon */
edog->droptime = gm.moves;
}
} else {
if ((obj = gl.level.objects[omx][omy]) != 0
&& !strchr(nofetch, obj->oclass)
#ifdef MAIL_STRUCTURES
&& obj->otyp != SCR_MAIL
#endif
/* avoid special items; once hero picks them up, they'll cease
being special and become eligible for normal monst activity */
&& !(is_mines_prize(obj) || is_soko_prize(obj))) {
int edible = dogfood(mtmp, obj);
if ((edible <= CADAVER
/* starving pet is more aggressive about eating */
|| (edog->mhpmax_penalty && edible == ACCFOOD))
&& could_reach_item(mtmp, obj->ox, obj->oy))
return dog_eat(mtmp, obj, omx, omy, FALSE);
carryamt = can_carry(mtmp, obj);
if (carryamt > 0 && !obj->cursed
&& could_reach_item(mtmp, obj->ox, obj->oy)) {
if (rn2(20) < edog->apport + 3) {
if (rn2(udist) || !rn2(edog->apport)) {
otmp = obj;
if (carryamt != obj->quan)
otmp = splitobj(obj, carryamt);
if (cansee(omx, omy)) {
/* call distant_name() for possible side-effects
even if the result won't be printed; should be
done before extract+pickup for distant_name()
-> doname() -> xname() -> find_artifact()
while otmp is still on floor */
char *otmpname = distant_name(otmp, doname);
if (Verbose(0, dog_invent))
pline("%s picks up %s.",
Monnam(mtmp), otmpname);
}
obj_extract_self(otmp);
newsym(omx, omy);
(void) mpickobj(mtmp, otmp);
if (attacktype(mtmp->data, AT_WEAP)
&& mtmp->weapon_check == NEED_WEAPON) {
mtmp->weapon_check = NEED_HTH_WEAPON;
(void) mon_wield_item(mtmp);
}
check_gear_next_turn(mtmp);
}
}
}
}
}
return 0;
}
/* set dog's goal -- gtyp, gx, gy;
returns -1/0/1 (dog's desire to approach player) or -2 (abort move) */
static int
dog_goal(
register struct monst *mtmp,
struct edog *edog,
int after, int udist, int whappr)
{
register coordxy omx, omy;
boolean in_masters_sight, dog_has_minvent;
register struct obj *obj;
xint16 otyp;
int appr;
/* Steeds don't move on their own will */
if (mtmp == u.usteed)
return -2;
omx = mtmp->mx;
omy = mtmp->my;
in_masters_sight = couldsee(omx, omy);
dog_has_minvent = (droppables(mtmp) != 0);
if (!edog || mtmp->mleashed) { /* he's not going anywhere... */
gg.gtyp = APPORT;
gg.gx = u.ux;
gg.gy = u.uy;
} else {
#define DDIST(x, y) (dist2(x, y, omx, omy))
#define SQSRCHRADIUS 5
int min_x, max_x, min_y, max_y;
coordxy nx, ny;
gg.gtyp = UNDEF; /* no goal as yet */
gg.gx = gg.gy = 0; /* suppress 'used before set' message */
if ((min_x = omx - SQSRCHRADIUS) < 1)
min_x = 1;
if ((max_x = omx + SQSRCHRADIUS) >= COLNO)
max_x = COLNO - 1;
if ((min_y = omy - SQSRCHRADIUS) < 0)
min_y = 0;
if ((max_y = omy + SQSRCHRADIUS) >= ROWNO)
max_y = ROWNO - 1;
/* nearby food is the first choice, then other objects */
for (obj = fobj; obj; obj = obj->nobj) {
nx = obj->ox;
ny = obj->oy;
if (nx >= min_x && nx <= max_x && ny >= min_y && ny <= max_y) {
otyp = dogfood(mtmp, obj);
/* skip inferior goals */
if (otyp > gg.gtyp || otyp == UNDEF)
continue;
/* avoid cursed items unless starving */
if (cursed_object_at(nx, ny)
&& !(edog->mhpmax_penalty && otyp < MANFOOD))
continue;
/* skip completely unreachable goals */
if (!could_reach_item(mtmp, nx, ny)
|| !can_reach_location(mtmp, mtmp->mx, mtmp->my, nx, ny))
continue;
if (otyp < MANFOOD) {
if (otyp < gg.gtyp
|| DDIST(nx, ny) < DDIST(gg.gx, gg.gy)) {
gg.gx = nx;
gg.gy = ny;
gg.gtyp = otyp;
}
} else if (gg.gtyp == UNDEF && in_masters_sight
&& !dog_has_minvent
&& (!levl[omx][omy].lit || levl[u.ux][u.uy].lit)
&& (otyp == MANFOOD || m_cansee(mtmp, nx, ny))
&& edog->apport > rn2(8)
&& can_carry(mtmp, obj) > 0) {
gg.gx = nx;
gg.gy = ny;
gg.gtyp = APPORT;
}
}
}
#undef DDIST
#undef SQSRCHRADIUS
}
/* follow player if appropriate */
if (gg.gtyp == UNDEF || (gg.gtyp != DOGFOOD && gg.gtyp != APPORT
&& gm.moves < edog->hungrytime)) {
gg.gx = u.ux;
gg.gy = u.uy;
if (after && udist <= 4 && u_at(gg.gx, gg.gy))
return -2;
appr = (udist >= 9) ? 1 : (mtmp->mflee) ? -1 : 0;
if (udist > 1) {
if (!IS_ROOM(levl[u.ux][u.uy].typ) || !rn2(4) || whappr
|| (dog_has_minvent && rn2(edog->apport)))
appr = 1;
}
/* if you have dog food it'll follow you more closely; if you are
on stairs (or ladder) or on or next to a magic portal, it will
behave as if you have dog food */
if (appr == 0) {
if (On_stairs(u.ux, u.uy)) {
appr = 1;
} else {
for (obj = gi.invent; obj; obj = obj->nobj)
if (dogfood(mtmp, obj) == DOGFOOD) {
appr = 1;
break;
}
if (appr == 0) {
struct trap *t;
/* assume at most one magic portal per level;
[should this be limited to known portals?] */
for (t = gf.ftrap; t; t = t->ntrap)
if (t->ttyp == MAGIC_PORTAL) {
if (/*t->tseen &&*/ distu(t->tx, t->ty) <= 2)
appr = 1;
break;
}
}
}
}
} else
appr = 1; /* gtyp != UNDEF */
if (mtmp->mconf)
appr = 0;
#define FARAWAY (COLNO + 2) /* position outside screen */
if (u_at(gg.gx, gg.gy) && !in_masters_sight) {
register coord *cp;
cp = gettrack(omx, omy);
if (cp) {
gg.gx = cp->x;
gg.gy = cp->y;
if (edog)
edog->ogoal.x = 0;
} else {
/* assume master hasn't moved far, and reuse previous goal */
if (edog && edog->ogoal.x
&& (edog->ogoal.x != omx || edog->ogoal.y != omy)) {
gg.gx = edog->ogoal.x;
gg.gy = edog->ogoal.y;
edog->ogoal.x = 0;
} else {
int fardist = FARAWAY * FARAWAY;
gg.gx = gg.gy = FARAWAY; /* random */
do_clear_area(omx, omy, 9, wantdoor, (genericptr_t) &fardist);
/* here gx == FARAWAY e.g. when dog is in a vault */
if (gg.gx == FARAWAY || (gg.gx == omx && gg.gy == omy)) {
gg.gx = u.ux;
gg.gy = u.uy;
} else if (edog) {
edog->ogoal.x = gg.gx;
edog->ogoal.y = gg.gy;
}
}
}
} else if (edog) {
edog->ogoal.x = 0;
}
return appr;
#undef FARAWAY
}
static struct monst *
find_targ(
register struct monst *mtmp,
int dx, int dy,
int maxdist)
{
struct monst *targ = 0;
int curx = mtmp->mx, cury = mtmp->my;
int dist = 0;
/* Walk outwards */
for ( ; dist < maxdist; ++dist) {
curx += dx;
cury += dy;
if (!isok(curx, cury))
break;
/* FIXME: Check if we hit a wall/door/boulder to
* short-circuit unnecessary subsequent checks
*/
/* If we can't see up to here, forget it - will this
* mean pets in corridors don't breathe at monsters
* in rooms? If so, is that necessarily bad?
*/
if (!m_cansee(mtmp, curx, cury))
break;
if (curx == mtmp->mux && cury == mtmp->muy)
return &gy.youmonst;
if ((targ = m_at(curx, cury)) != 0) {
/* Is the monster visible to the pet? */
if ((!targ->minvis || perceives(mtmp->data)) && !targ->mundetected
/* if a long worm, only accept the head as a target */
&& targ->mx == curx && targ->my == cury) /* not tail */
break;
/* If the pet can't see it, it assumes it aint there */
targ = 0;
}
}
return targ;
}
static int
find_friends(struct monst *mtmp, struct monst *mtarg, int maxdist)
{
struct monst *pal;
int dx = sgn(mtarg->mx - mtmp->mx),
dy = sgn(mtarg->my - mtmp->my);
int curx = mtarg->mx, cury = mtarg->my;
int dist = distmin(mtarg->mx, mtarg->my, mtmp->mx, mtmp->my);
for ( ; dist <= maxdist; ++dist) {
curx += dx;
cury += dy;
if (!isok(curx, cury))
return 0;
/* If the pet can't see beyond this point, don't
* check any farther
*/
if (!m_cansee(mtmp, curx, cury))
return 0;
/* Does pet think you're here? */
if (mtmp->mux == curx && mtmp->muy == cury)
return 1;
pal = m_at(curx, cury);
if (pal) {
if (pal->mtame) {
/* Pet won't notice invisible pets */
if (!pal->minvis || perceives(mtmp->data))
return 1;
} else {
/* Quest leaders and guardians are always seen */
if (pal->data->msound == MS_LEADER
|| pal->data->msound == MS_GUARDIAN)
return 1;
}
}
}
return 0;
}
static long
score_targ(struct monst *mtmp, struct monst *mtarg)
{
long score = 0L;
/* If the monster is confused, normal scoring is disrupted -
* anything may happen
*/
/* Give 1 in 3 chance of safe breathing even if pet is confused or
* if you're on the quest start level */
if (!mtmp->mconf || !rn2(3) || Is_qstart(&u.uz)) {
int mtmp_lev;
aligntyp align1 = A_NONE, align2 = A_NONE; /* For priests, minions */
boolean faith1 = TRUE, faith2 = TRUE;
if (mtmp->isminion)
align1 = EMIN(mtmp)->min_align;
else if (mtmp->ispriest)
align1 = EPRI(mtmp)->shralign;
else
faith1 = FALSE;
if (mtarg->isminion)
align2 = EMIN(mtarg)->min_align; /* MAR */
else if (mtarg->ispriest)
align2 = EPRI(mtarg)->shralign; /* MAR */
else
faith2 = FALSE;
/* Never target quest friendlies */
if (mtarg->data->msound == MS_LEADER
|| mtarg->data->msound == MS_GUARDIAN)
return -5000L;
/* D: Fixed angelic beings using gaze attacks on coaligned priests */
if (faith1 && faith2 && align1 == align2 && mtarg->mpeaceful) {
score -= 5000L;
return score;
}
/* Is monster adjacent? */
if (distmin(mtmp->mx, mtmp->my, mtarg->mx, mtarg->my) <= 1) {
score -= 3000L;
return score;
}
/* Is the monster peaceful or tame? */
if (/*mtarg->mpeaceful ||*/ mtarg->mtame || mtarg == &gy.youmonst) {
/* Pets will never be targeted */
score -= 3000L;
return score;
}
/* Is master/pet behind monster? Check up to 15 squares beyond pet. */
if (find_friends(mtmp, mtarg, 15)) {
score -= 3000L;
return score;
}
/* Target hostile monsters in preference to peaceful ones */
if (!mtarg->mpeaceful)
score += 10;
/* Is the monster passive? Don't waste energy on it, if so */
if (mtarg->data->mattk[0].aatyp == AT_NONE)
score -= 1000;
/* Even weak pets with breath attacks shouldn't take on very
low-level monsters. Wasting breath on lichens is ridiculous. */
if ((mtarg->m_lev < 2 && mtmp->m_lev > 5)
|| (mtmp->m_lev > 12 && mtarg->m_lev < mtmp->m_lev - 9
&& u.ulevel > 8 && mtarg->m_lev < u.ulevel - 7))
score -= 25;
/* for strength purposes, a vampshifter in weak form (vampire bat,
fog cloud, maybe wolf) will attack as if in vampire form;
otherwise if won't do much and usually wouldn't suffer enough
damage (from counterattacks) to switch back to vampire form;
make it be more aggressive by behaving as if stronger */
mtmp_lev = mtmp->m_lev;
if (is_vampshifter(mtmp) && mtmp->data->mlet != S_VAMPIRE) {
/* is_vampshifter() implies (mtmp->cham >= LOW_PM) */
mtmp_lev = mons[mtmp->cham].mlevel;
/* actual vampire level would range from 1.0*mlvl to 1.5*mlvl */
mtmp_lev += rn2(mtmp_lev / 2 + 1);
/* we don't expect actual level in weak form to exceed
base level of strong form, but handle that if it happens */
if (mtmp->m_lev > mtmp_lev)
mtmp_lev = mtmp->m_lev;
}
/* And pets will hesitate to attack vastly stronger foes.
This penalty will be discarded if master's in trouble. */
if (mtarg->m_lev > mtmp_lev + 4L)
score -= (mtarg->m_lev - mtmp_lev) * 20L;
/* All things being the same, go for the beefiest monster. This
bonus should not be large enough to override the pet's aversion
to attacking much stronger monsters. */
score += mtarg->m_lev * 2 + mtarg->mhp / 3;
}
/* Fuzz factor to make things less predictable when very
similar targets are abundant. */
score += rnd(5);
/* Pet may decide not to use ranged attack when confused */
if (mtmp->mconf && !rn2(3))
score -= 1000;
return score;
}
static struct monst *
best_target(struct monst *mtmp) /* Pet */
{
int dx, dy;
long bestscore = -40000L, currscore;
struct monst *best_targ = 0, *temp_targ = 0;
/* Help! */
if (!mtmp)
return 0;
/* If the pet is blind, it's not going to see any target */
if (!mtmp->mcansee)
return 0;
/* Search for any monsters lined up with the pet, within an arbitrary
* distance from the pet (7 squares, even along diagonals). Monsters
* are assigned scores and the best score is chosen.
*/
for (dy = -1; dy < 2; ++dy) {
for (dx = -1; dx < 2; ++dx) {
if (!dx && !dy)
continue;
/* Traverse the line to find the first monster within 7
* squares. Invisible monsters are skipped (if the
* pet doesn't have see invisible).
*/
temp_targ = find_targ(mtmp, dx, dy, 7);
/* Nothing in this line? */
if (!temp_targ)
continue;
/* Decide how attractive the target is */
currscore = score_targ(mtmp, temp_targ);
if (currscore > bestscore) {
bestscore = currscore;
best_targ = temp_targ;
}
}
}
/* Filter out targets the pet doesn't like */
if (bestscore < 0L)
best_targ = 0;
return best_targ;
}
/* Pet considers and maybe executes a ranged attack */
int
pet_ranged_attk(struct monst *mtmp)
{
struct monst *mtarg;
int hungry = 0;
/* How hungry is the pet? */
if (!mtmp->isminion) {
struct edog *dog = EDOG(mtmp);
hungry = (gm.moves > (dog->hungrytime + DOG_HUNGRY));
}
/* Identify the best target in a straight line from the pet;
* if there is such a target, we'll let the pet attempt an attack.
*/
mtarg = best_target(mtmp);
/* Hungry pets are unlikely to use breath/spit attacks */
if (mtarg && (!hungry || !rn2(5))) {
int mstatus = M_ATTK_MISS;
if (mtarg == &gy.youmonst) {
if (mattacku(mtmp))
return MMOVE_DIED;
/* Treat this as the pet having initiated an attack even if it
* didn't, so it will lose its move. This isn't entirely fair,
* but mattacku doesn't distinguish between "did not attack"
* and "attacked but didn't die" cases, and this is preferable
* to letting the pet attack the player and continuing to move.
*/
mstatus = M_ATTK_HIT;
} else {
gb.bhitpos.x = mtmp->mx, gb.bhitpos.y = mtmp->my;
gn.notonhead = FALSE;
mstatus = mattackm(mtmp, mtarg);
/* Shouldn't happen, really */
if (mstatus & M_ATTK_AGR_DIED)
return MMOVE_DIED;
/* Allow the targeted nasty to strike back - if
* the targeted beast doesn't have a ranged attack,
* nothing will happen.
*/
if ((mstatus & M_ATTK_HIT) && !(mstatus & M_ATTK_DEF_DIED)
&& rn2(4) && mtarg != &gy.youmonst) {
/* Can monster see? If it can, it can retaliate
* even if the pet is invisible, since it'll see
* the direction from which the ranged attack came;
* if it's blind or unseeing, it can't retaliate
*/
if (mtarg->mcansee && haseyes(mtarg->data)) {
int mresp;
gb.bhitpos.x = mtmp->mx, gb.bhitpos.y = mtmp->my;
gn.notonhead = FALSE;
mresp = mattackm(mtarg, mtmp);
if (mresp & M_ATTK_DEF_DIED)
return MMOVE_DIED;
}
}
}
/* Only return 3 if the pet actually made a ranged attack, and
* thus should lose the rest of its move.
* There's a chain of assumptions here:
* 1. score_targ and best_target will never select a monster
* that can be attacked in melee, so the mattackm call can
* only ever try ranged options
* 2. if the only attacks available to mattackm are ranged
* options, and the monster cannot make a ranged attack, it
* will return M_ATTK_MISS.
*/
if (mstatus != M_ATTK_MISS)
return MMOVE_DONE;
}
return MMOVE_NOTHING;
}
/* Return values (same as m_move):
* 0: did not move, but can still attack and do other stuff.
* 1: moved, possibly can attack.
* 2: monster died.
* 3: did not move, and can't do anything else either.
* (may have attacked something)
*/
int
dog_move(
struct monst *mtmp, /* pet */
int after) /* this is extra fast monster movement */
{
int omx, omy; /* original mtmp position */
int appr, whappr, udist;
int i, j, k;
struct edog *edog = (mtmp->mtame && has_edog(mtmp)) ? EDOG(mtmp) : 0;
struct obj *obj = (struct obj *) 0;
xint16 otyp;
boolean cursemsg[9], do_eat = FALSE;
boolean better_with_displacing = FALSE;
coordxy nix, niy; /* position mtmp is (considering) moving to */
coordxy nx, ny; /* temporary coordinates */
xint16 cnt, uncursedcnt, chcnt;
int chi = -1, nidist, ndist;
coord poss[9];
long info[9], allowflags;
#define GDIST(x, y) (dist2(x, y, gg.gx, gg.gy))
/*
* Tame Angels have isminion set and an ispriest structure instead of
* an edog structure. Fortunately, guardian Angels need not worry
* about mundane things like eating and fetching objects, and can
* spend all their energy defending the player. (They are the only
* monsters with other structures that can be tame.)
*/
if (!edog && !mtmp->isminion) {
impossible("dog_move for non-pet?");
return MMOVE_NOTHING;
}
omx = mtmp->mx;
omy = mtmp->my;
if (edog && dog_hunger(mtmp, edog))
return MMOVE_DIED; /* starved */
udist = distu(omx, omy);
/* Let steeds eat and maybe throw rider during Conflict */
if (mtmp == u.usteed) {
if (Conflict && !resist_conflict(mtmp)) {
dismount_steed(DISMOUNT_THROWN);
return MMOVE_MOVED;
}
udist = 1;
} else if (!udist) {
/* maybe we tamed him while being swallowed --jgm */
return MMOVE_NOTHING;
}
nix = omx; /* set before newdogpos */
niy = omy;
cursemsg[0] = FALSE; /* lint suppression */
info[0] = 0; /* ditto */
if (edog) {
j = dog_invent(mtmp, edog, udist);
if (j == 2)
return MMOVE_DIED; /* died */
else if (j == 1)
goto newdogpos; /* eating something */
whappr = (gm.moves - edog->whistletime < 5);
} else
whappr = 0;
appr = dog_goal(mtmp, edog, after, udist, whappr);
if (appr == -2)
return MMOVE_NOTHING;
if (Conflict && !resist_conflict(mtmp)) {
if (!edog) {
/* Guardian angel refuses to be conflicted; rather,
* it disappears, angrily, and sends in some nasties
*/
lose_guardian_angel(mtmp);
return MMOVE_DIED; /* current monster is gone */
}
}
#if 0 /* [this is now handled in dochug()] */
if (!Conflict && !mtmp->mconf
&& mtmp == u.ustuck && !sticks(gy.youmonst.data)) {
unstuck(mtmp); /* swallowed case handled above */
You("get released!");
}
#endif
allowflags = mon_allowflags(mtmp);
cnt = mfndpos(mtmp, poss, info, allowflags);
/* Normally dogs don't step on cursed items, but if they have no
* other choice they will. This requires checking ahead of time
* to see how many uncursed item squares are around.
*/
uncursedcnt = 0;
for (i = 0; i < cnt; i++) {
nx = poss[i].x;
ny = poss[i].y;
if (MON_AT(nx, ny) && !((info[i] & ALLOW_M) || info[i] & ALLOW_MDISP))
continue;
if (cursed_object_at(nx, ny))
continue;
uncursedcnt++;
}
better_with_displacing = should_displace(mtmp, poss, info, cnt,
gg.gx, gg.gy);
chcnt = 0;
chi = -1;
nidist = GDIST(nix, niy);
for (i = 0; i < cnt; i++) {
nx = poss[i].x;
ny = poss[i].y;
cursemsg[i] = FALSE;
/* if leashed, we drag him along. */
if (mtmp->mleashed && distu(nx, ny) > 4)
continue;
/* if a guardian, try to stay close by choice */
if (!edog && (j = distu(nx, ny)) > 16 && j >= udist)
continue;
if ((info[i] & ALLOW_M) && MON_AT(nx, ny)) {
int mstatus;
register struct monst *mtmp2 = m_at(nx, ny);
/* weight the audacity of the pet to attack a differently-leveled
* foe based on its fraction of max HP:
* 100%: up to level + 2
* 80% and up: up to level + 1
* 60% to 80%: up to level
* 40% to 60%: up to level - 1
* 25% to 40%: up to level - 2
* below 25%: won't attack peacefuls of any level (different case)
* below 20%: up to level - 3
*
* note that balk's maximum value is +3, as it is the lowest level
* the pet will balk at attacking rather than the highest level
* they are willing to attack; note the >= used when comparing it.
*/
int balk = mtmp->m_lev + ((5 * mtmp->mhp) / mtmp->mhpmax) - 2;
if ((int) mtmp2->m_lev >= balk
|| (mtmp2->data == &mons[PM_FLOATING_EYE] && rn2(10)
&& mtmp->mcansee && haseyes(mtmp->data) && mtmp2->mcansee
&& (perceives(mtmp->data) || !mtmp2->minvis))
|| (mtmp2->data == &mons[PM_GELATINOUS_CUBE] && rn2(10))
|| (max_passive_dmg(mtmp2, mtmp) >= mtmp->mhp)
|| ((mtmp->mhp * 4 < mtmp->mhpmax
|| mtmp2->data->msound == MS_GUARDIAN
|| mtmp2->data->msound == MS_LEADER) && mtmp2->mpeaceful
&& !Conflict)
|| (touch_petrifies(mtmp2->data) && !resists_ston(mtmp)))
continue;
if (after)
return MMOVE_NOTHING; /* hit only once each move */
gb.bhitpos.x = nx, gb.bhitpos.y = ny;
gn.notonhead = mtmp2->mx != nx || mtmp2->my != ny;
mstatus = mattackm(mtmp, mtmp2);
/* aggressor (pet) died */
if (mstatus & M_ATTK_AGR_DIED)
return MMOVE_DIED;
if ((mstatus & (M_ATTK_HIT | M_ATTK_DEF_DIED)) == M_ATTK_HIT
&& rn2(4)
&& mtmp2->mlstmv != gm.moves
&& !onscary(mtmp->mx, mtmp->my, mtmp2)
/* monnear check needed: long worms hit on tail */
&& monnear(mtmp2, mtmp->mx, mtmp->my)) {
gb.bhitpos.x = mtmp->mx, gb.bhitpos.y = mtmp->my;
gn.notonhead = FALSE;
mstatus = mattackm(mtmp2, mtmp); /* return attack */
if (mstatus & M_ATTK_DEF_DIED)
return MMOVE_DIED;
}
return MMOVE_DONE;
}
if ((info[i] & ALLOW_MDISP) && MON_AT(nx, ny)
&& better_with_displacing && !undesirable_disp(mtmp, nx, ny)) {
int mstatus;
register struct monst *mtmp2 = m_at(nx, ny);
mstatus = mdisplacem(mtmp, mtmp2, FALSE); /* displace monster */
if (mstatus & M_ATTK_DEF_DIED)
return MMOVE_DIED;
return MMOVE_NOTHING;
}
{
/* Dog avoids harmful traps, but perhaps it has to pass one
* in order to follow player. (Non-harmful traps do not
* have ALLOW_TRAPS in info[].) The dog only avoids the
* trap if you've seen it, unlike enemies who avoid traps
* if they've seen some trap of that type sometime in the
* past. (Neither behavior is really realistic.)
*/
struct trap *trap;
if ((info[i] & ALLOW_TRAPS) && (trap = t_at(nx, ny))) {
if (mtmp->mleashed) {
if (!Deaf)
whimper(mtmp);
} else {
/* 1/40 chance of stepping on it anyway, in case
* it has to pass one to follow the player...
*/
if (trap->tseen && rn2(40))
continue;
}
}
}
/* dog eschews cursed objects, but likes dog food */
/* (minion isn't interested; `cursemsg' stays FALSE) */
if (edog) {
boolean can_reach_food = could_reach_item(mtmp, nx, ny);
for (obj = gl.level.objects[nx][ny]; obj; obj = obj->nexthere) {
if (obj->cursed) {
cursemsg[i] = TRUE;
} else if (can_reach_food
&& (otyp = dogfood(mtmp, obj)) < MANFOOD
&& (otyp < ACCFOOD
|| edog->hungrytime <= gm.moves)) {
/* Note: our dog likes the food so much that he
* might eat it even when it conceals a cursed object */
nix = nx;
niy = ny;
chi = i;
do_eat = TRUE;
cursemsg[i] = FALSE; /* not reluctant */
goto newdogpos;
}
}
}
/* didn't find something to eat; if we saw a cursed item and
aren't being forced to walk on it, usually keep looking */
if (cursemsg[i] && !mtmp->mleashed && uncursedcnt > 0
&& rn2(13 * uncursedcnt))
continue;
/*
* Lessen the chance of backtracking to previous position(s).
* This causes unintended issues for pets trying to follow the
* hero. Thus, only run it if not leashed and >5 tiles away.
*/
if (!mtmp->mleashed && distmin(mtmp->mx, mtmp->my, u.ux, u.uy) > 5) {
k = edog ? uncursedcnt : cnt;
for (j = 0; j < MTSZ && j < k - 1; j++)
if (nx == mtmp->mtrack[j].x && ny == mtmp->mtrack[j].y)
if (rn2(MTSZ * (k - j)))
goto nxti;
}
j = ((ndist = GDIST(nx, ny)) - nidist) * appr;
if ((j == 0 && !rn2(++chcnt)) || j < 0
|| (j > 0 && !whappr
&& ((omx == nix && omy == niy && !rn2(3)) || !rn2(12)))) {
nix = nx;
niy = ny;
nidist = ndist;
if (j < 0)
chcnt = 0;
chi = i;
}
nxti:
;
}
/* Pet hasn't attacked anything but is considering moving -
* now's the time for ranged attacks. Note that the pet can move
* after it performs its ranged attack. Should this be changed?
*/
if ((i = pet_ranged_attk(mtmp)) != MMOVE_NOTHING)
return i;
newdogpos:
if (nix != omx || niy != omy) {
boolean wasseen;
if (info[chi] & ALLOW_U) {
if (mtmp->mleashed) { /* play it safe */
pline("%s breaks loose of %s leash!", Monnam(mtmp),
mhis(mtmp));
m_unleash(mtmp, FALSE);
}
(void) mattacku(mtmp);
return MMOVE_DONE;
}
if (!m_in_out_region(mtmp, nix, niy))
return MMOVE_MOVED;
if (m_digweapon_check(mtmp, nix,niy))
return MMOVE_NOTHING;
/* insert a worm_move() if worms ever begin to eat things */
wasseen = canseemon(mtmp);
remove_monster(omx, omy);
place_monster(mtmp, nix, niy);
if (cursemsg[chi] && (wasseen || canseemon(mtmp))) {
/* describe top item of pile, not necessarily cursed item itself;
don't use glyph_at() here--it would return the pet but we want
to know whether an object is remembered at this map location */
struct obj *o = (!Hallucination && gl.level.flags.hero_memory
&& glyph_is_object(levl[nix][niy].glyph))
? vobj_at(nix, niy) : 0;
const char *what = o ? distant_name(o, doname) : something;
pline("%s %s reluctantly over %s.", noit_Monnam(mtmp),
vtense((char *) 0, locomotion(mtmp->data, "step")), what);
}
mon_track_add(mtmp, omx, omy);
/* We have to know if the pet's going to do a combined eat and
* move before moving it, but it can't eat until after being
* moved. Thus the do_eat flag.
*/
if (do_eat) {
if (dog_eat(mtmp, obj, omx, omy, FALSE) == 2)
return MMOVE_DIED;
}
} else if (mtmp->mleashed && distu(omx, omy) > 4) {
/* an incredible kludge, but the only way to keep pooch near
* after it spends time eating or in a trap, etc.
*/
coord cc;
nx = sgn(omx - u.ux);
ny = sgn(omy - u.uy);
cc.x = u.ux + nx;
cc.y = u.uy + ny;
if (goodpos(cc.x, cc.y, mtmp, 0))
goto dognext;
i = xytod(nx, ny);
for (j = DIR_LEFT(i); j < DIR_RIGHT(i); j++) {
dtoxy(&cc, j);
if (goodpos(cc.x, cc.y, mtmp, 0))
goto dognext;
}
for (j = DIR_LEFT2(i); j < DIR_RIGHT2(i); j++) {
dtoxy(&cc, j);
if (goodpos(cc.x, cc.y, mtmp, 0))
goto dognext;
}
cc.x = mtmp->mx;
cc.y = mtmp->my;
dognext:
if (!m_in_out_region(mtmp, nix, niy))
return MMOVE_MOVED;
remove_monster(mtmp->mx, mtmp->my);
place_monster(mtmp, cc.x, cc.y);
newsym(cc.x, cc.y);
set_apparxy(mtmp);
}
return MMOVE_MOVED;
#undef GDIST
}
/* check if a monster could pick up objects from a location */
boolean
could_reach_item(struct monst *mon, coordxy nx, coordxy ny)
{
if ((!is_pool(nx, ny) || is_swimmer(mon->data))
&& (!is_lava(nx, ny) || likes_lava(mon->data))
&& (!sobj_at(BOULDER, nx, ny) || throws_rocks(mon->data)))
return TRUE;
return FALSE;
}
/* Hack to prevent a dog from being endlessly stuck near an object that
* it can't reach, such as caught in a teleport scroll niche. It recursively
* checks to see if the squares in between are good. The checking could be
* a little smarter; a full check would probably be useful in m_move() too.
* Since the maximum food distance is 5, this should never be more than 5
* calls deep.
*/
static boolean
can_reach_location(struct monst *mon, coordxy mx, coordxy my, coordxy fx, coordxy fy)
{
int i, j;
int dist;
if (mx == fx && my == fy)
return TRUE;
if (!isok(mx, my))
return FALSE; /* should not happen */
dist = dist2(mx, my, fx, fy);
for (i = mx - 1; i <= mx + 1; i++) {
for (j = my - 1; j <= my + 1; j++) {
if (!isok(i, j))
continue;
if (dist2(i, j, fx, fy) >= dist)
continue;
if (IS_ROCK(levl[i][j].typ) && !passes_walls(mon->data)
&& (!may_dig(i, j) || !tunnels(mon->data)
/* tunnelling monsters can't do that on the rogue level */
|| Is_rogue_level(&u.uz)))
continue;
if (IS_DOOR(levl[i][j].typ)
&& (levl[i][j].doormask & (D_CLOSED | D_LOCKED)))
continue;
if (!could_reach_item(mon, i, j))
continue;
if (can_reach_location(mon, i, j, fx, fy))
return TRUE;
}
}
return FALSE;
}
/* do_clear_area client */
static void
wantdoor(coordxy x, coordxy y, genericptr_t distance)
{
int ndist, *dist_ptr = (int *) distance;
if (*dist_ptr > (ndist = distu(x, y))) {
gg.gx = x;
gg.gy = y;
*dist_ptr = ndist;
}
}
static const struct qmchoices {
int mndx; /* type of pet, 0 means any */
char mlet; /* symbol of pet, 0 means any */
unsigned mappearance; /* mimic this */
uchar m_ap_type; /* what is the thing it is mimicing? */
} qm[] = {
/* Things that some pets might be thinking about at the time */
{ PM_LITTLE_DOG, 0, PM_KITTEN, M_AP_MONSTER },
{ PM_DOG, 0, PM_HOUSECAT, M_AP_MONSTER },
{ PM_LARGE_DOG, 0, PM_LARGE_CAT, M_AP_MONSTER },
{ PM_KITTEN, 0, PM_LITTLE_DOG, M_AP_MONSTER },
{ PM_HOUSECAT, 0, PM_DOG, M_AP_MONSTER },
{ PM_LARGE_CAT, 0, PM_LARGE_DOG, M_AP_MONSTER },
{ PM_HOUSECAT, 0, PM_GIANT_RAT, M_AP_MONSTER },
{ 0, S_DOG, S_sink, M_AP_FURNITURE }, /* sorry, no fire hydrants */
{ 0, 0, TRIPE_RATION, M_AP_OBJECT }, /* leave this at end */
};
void
finish_meating(struct monst *mtmp)
{
mtmp->meating = 0;
if (M_AP_TYPE(mtmp) && mtmp->mappearance && mtmp->data->mlet != S_MIMIC) {
/* was eating a mimic and now appearance needs resetting */
mtmp->m_ap_type = M_AP_NOTHING;
mtmp->mappearance = 0;
newsym(mtmp->mx, mtmp->my);
}
}
void
quickmimic(struct monst *mtmp)
{
int idx = 0, trycnt = 5, spotted, seeloc;
char buf[BUFSZ];
if (Protection_from_shape_changers || !mtmp->meating)
return;
/* with polymorph, the steed's equipment would be re-checked and its
saddle would come off, triggering DISMOUNT_FELL, but mimicking
doesn't impact monster's equipment; normally DISMOUNT_POLY is for
rider taking on an unsuitable shape, but its message works fine
for this and also avoids inflicting damage during forced dismount;
do this before changing so that dismount refers to original shape */
if (mtmp == u.usteed)
dismount_steed(DISMOUNT_POLY);
do {
idx = rn2(SIZE(qm));
if (qm[idx].mndx != 0 && monsndx(mtmp->data) == qm[idx].mndx)
break;
if (qm[idx].mlet != 0 && mtmp->data->mlet == qm[idx].mlet)
break;
if (qm[idx].mndx == 0 && qm[idx].mlet == 0)
break;
} while (--trycnt > 0);
if (trycnt == 0)
idx = SIZE(qm) - 1;
Strcpy(buf, mon_nam(mtmp));
spotted = canspotmon(mtmp);
seeloc = cansee(mtmp->mx, mtmp->my);
mtmp->m_ap_type = qm[idx].m_ap_type;
mtmp->mappearance = qm[idx].mappearance;
if (spotted || seeloc || canspotmon(mtmp)) {
int prev_glyph = glyph_at(mtmp->mx, mtmp->my);
const char *what = (M_AP_TYPE(mtmp) == M_AP_FURNITURE)
? defsyms[mtmp->mappearance].explanation
: (M_AP_TYPE(mtmp) == M_AP_OBJECT
&& OBJ_DESCR(objects[mtmp->mappearance]))
? OBJ_DESCR(objects[mtmp->mappearance])
: (M_AP_TYPE(mtmp) == M_AP_OBJECT
&& OBJ_NAME(objects[mtmp->mappearance]))
? OBJ_NAME(objects[mtmp->mappearance])
: (M_AP_TYPE(mtmp) == M_AP_MONSTER)
? pmname(&mons[mtmp->mappearance],
Mgender(mtmp))
: something;
newsym(mtmp->mx, mtmp->my);
if (glyph_at(mtmp->mx, mtmp->my) != prev_glyph)
You("%s %s %s where %s was!",
seeloc ? "see" : "sense that",
(what != something) ? an(what) : what,
seeloc ? "appear" : "has appeared", buf);
else
You("sense that %s feels rather %s-ish.", buf, what);
display_nhwindow(WIN_MAP, TRUE);
}
}
#undef DOG_HUNGRY
#undef DOG_WEAK
#undef DOG_STARVE
/*dogmove.c*/