give player an inkling of why corpses didn't stack

When corpses haven't stacked, and there is no player-discernable
reason why, provide some additional information in some cases,
but only when it is required.

Gender variance is the supported case in this commit.

Related to GitHub issue #1607.

This commit doesn't change the underlying mechanics to allow
the corpses to stack, but it does help the player understand
why that's the case in this instance.
This commit is contained in:
nhmall
2026-06-27 11:18:44 -04:00
parent 6fee8a4e78
commit ac3d71a82a
7 changed files with 116 additions and 13 deletions
+3
View File
@@ -803,6 +803,9 @@ struct instance_globals_p {
int poly_zapped;
/* new stuff */
int puzzling_critera;
char puzzling_ilets[invlet_basic + 1];
boolean havestate;
};
+3
View File
@@ -1404,6 +1404,7 @@ extern void perm_invent_toggled(boolean negated);
extern void prepare_perminvent(winid window);
extern struct obj *carrying_stoning_corpse(void);
extern void repopulate_perminvent(void);
extern int check_for_puzzling_nonmerge(struct obj *);
/* ### ioctl.c ### */
@@ -2236,6 +2237,8 @@ extern boolean the_unique_pm(struct permonst *) NONNULLARG1;
extern boolean erosion_matters(struct obj *) NONNULLARG1;
extern char *doname(struct obj *) NONNULLARG1;
extern char *doname_with_price(struct obj *) NONNULLARG1;
extern char *doname_with_cgender(struct obj *) NONNULLARG1;
extern char *doname_with_price_and_cgender(struct obj *) NONNULLARG1;
extern char *doname_vague_quan(struct obj *) NONNULLARG1;
extern boolean not_fully_identified(struct obj *) NONNULLARG1;
extern char *corpse_xname(struct obj *, const char *, unsigned) NONNULLARG1;
+1
View File
@@ -63,6 +63,7 @@
#define CXN_PFX_THE 4 /* prefix with "the " (unless pname) */
#define CXN_ARTICLE 8 /* include a/an/the prefix */
#define CXN_NOCORPSE 16 /* suppress " corpse" suffix */
#define CXN_ADDGNDR 32 /* include a gender */
/* number of turns it takes for vault guard to show up */
#define VAULT_GUARD_TIME 30
+7
View File
@@ -648,6 +648,13 @@ static const struct instance_globals_p g_init_p = {
UNDEFINED_PTR, /* propellor */
/* zap.c */
UNDEFINED_VALUE, /* poly_zap */
0, /* puzzling_criteria */
{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
}, /* puzzling_ilets[] */
TRUE, /* havestate*/
};
+60 -4
View File
@@ -2701,6 +2701,51 @@ count_unidentified(struct obj *objchn)
return unid_cnt;
}
/* mark the puzzling items */
int
check_for_puzzling_nonmerge(struct obj *objchn)
{
int i, k, idx, puzzling_cnt = 0, ilet, mnums[invlet_basic + 1],
gndr[invlet_basic + 1];
struct obj *obj;
boolean at_least_one = FALSE;
for (i = 0; i < invlet_basic + 1; ++i)
gp.puzzling_ilets[i] = mnums[i] = gndr[i] = 0;
for (obj = objchn; obj; obj = obj->nobj) {
if (obj->otyp == CORPSE
&& obj->corpsenm >= 0 && obj->corpsenm < NUMMONS) {
ilet = obj->invlet;
idx = (ilet >= 'A' && ilet <= 'Z') ? ilet - 'A'
: (ilet >= 'a' && ilet <= 'z') ? ilet - 'a' + 26
: 53;
if (idx < 53) {
mnums[idx] = obj->corpsenm;
gndr[idx] = (obj->spe & CORPSTAT_MALE) ? CORPSTAT_MALE
: CORPSTAT_FEMALE;
at_least_one = TRUE;
}
}
}
if (at_least_one) {
for (i = 0; i < invlet_basic; ++i) {
for (k = i + 1; k < invlet_basic; ++k) {
if (k == i)
continue;
if (mnums[k] == mnums[i] && gndr[k] != gndr[i]) {
++puzzling_cnt;
gp.puzzling_ilets[i] = gp.puzzling_ilets[k] = 1;
}
}
}
if (puzzling_cnt)
gp.puzzling_critera = 411;
}
return puzzling_cnt;
}
/* dialog with user to identify a given number of items; 0 means all */
void
identify_pack(
@@ -3064,7 +3109,7 @@ display_pickinv(
struct obj *otmp, wizid_fakeobj, inuse_fakeobj;
char ilet, ret, *formattedobj;
const char *invlet = flags.inv_order;
int n, classcount, inusecount = 0;
int n, classcount, inusecount = 0, puzzling_count = 0;
winid win; /* windows being used */
anything any;
menu_item *selected;
@@ -3212,6 +3257,9 @@ display_pickinv(
sortedinvent[0].obj = (struct obj *) 0;
}
puzzling_count = check_for_puzzling_nonmerge(gi.invent);
start_menu(win, menu_behavior);
any = cg.zeroany;
if (wizid) {
@@ -3314,7 +3362,9 @@ display_pickinv(
/* normal inventory item */
tmpglyph = obj_to_glyph(otmp, rn2_on_display_rng);
map_glyphinfo(0, 0, tmpglyph, 0U, &tmpglyphinfo);
formattedobj = doname(otmp);
formattedobj = !puzzling_count
? doname(otmp)
: doname_with_cgender(otmp);
add_menu(win, &tmpglyphinfo, &any, ilet,
wizid ? def_oc_syms[(int) otmp->oclass].sym : 0,
ATR_NONE, clr, formattedobj, MENU_ITEMFLAGS_NONE);
@@ -4110,6 +4160,7 @@ look_here(
picked_some = (lookhere_flags & LOOKHERE_PICKED_SOME) != 0,
/* skip 'dfeature' if caller used describe_decor() to show it */
skip_dfeature = (lookhere_flags & LOOKHERE_SKIP_DFEATURE) != 0;
int puzzling_count = 0;
/* default pile_limit is 5; a value of 0 means "never skip"
(and 1 effectively forces "always skip") */
@@ -4173,6 +4224,7 @@ look_here(
}
otmp = svl.level.objects[u.ux][u.uy];
puzzling_count = check_for_puzzling_nonmerge(otmp);
dfeature = dfeature_at(u.ux, u.uy, fbuf2);
if (dfeature && !strcmp(dfeature, "pool of water") && Underwater)
dfeature = 0;
@@ -4294,11 +4346,15 @@ look_here(
for (; otmp; otmp = otmp->nexthere) {
if (otmp->otyp == CORPSE && will_feel_cockatrice(otmp, FALSE)) {
felt_cockatrice = TRUE;
Sprintf(buf, "%s...", doname(otmp));
Sprintf(buf, "%s...",
(puzzling_count) ? doname_with_cgender(otmp)
: doname(otmp));
putstr(tmpwin, 0, buf);
break;
}
putstr(tmpwin, 0, doname_with_price(otmp));
putstr(tmpwin, 0,
(puzzling_count) ? doname_with_price_and_cgender(otmp)
: doname_with_price(otmp));
}
display_nhwindow(tmpwin, TRUE);
destroy_nhwindow(tmpwin);
+36 -7
View File
@@ -1214,9 +1214,10 @@ erosion_matters(struct obj *obj)
return FALSE;
}
#define DONAME_WITH_PRICE 1
#define DONAME_VAGUE_QUAN 2
#define DONAME_FOR_MENU 4 /* [not used anywhere yet] */
#define DONAME_WITH_PRICE 1
#define DONAME_VAGUE_QUAN 2
#define DONAME_FOR_MENU 4 /* [not used anywhere yet] */
#define DONAME_FORCE_GENDER 8 /* always add male or female */
/* core of doname() */
staticfn char *
@@ -1227,7 +1228,8 @@ doname_base(
boolean ispoisoned = FALSE,
with_price = (doname_flags & DONAME_WITH_PRICE) != 0,
vague_quan = (doname_flags & DONAME_VAGUE_QUAN) != 0,
for_menu = (doname_flags & DONAME_FOR_MENU) != 0;
for_menu = (doname_flags & DONAME_FOR_MENU) != 0,
with_corpse_genders = (doname_flags & DONAME_FORCE_GENDER) != 0;
boolean known, dknown, cknown, bknown, lknown,
fake_arti, force_the;
char prefix[PREFIX];
@@ -1511,7 +1513,15 @@ doname_base(
unsigned cxarg = (((obj->quan != 1L) ? 0 : CXN_ARTICLE)
| CXN_NOCORPSE);
char *cxstr, *save_xnamep;
int puzzidx = (obj->invlet >= 'A' && obj->invlet <= 'Z')
? obj->invlet - 'A'
: (obj->invlet >= 'a' && obj->invlet <= 'z')
? obj->invlet - 'a' + 26
: 53; /* valid index, but always holds zero */
if (with_corpse_genders && puzzidx < 53
&& gp.puzzling_critera == 411 && gp.puzzling_ilets[puzzidx])
cxarg |= CXN_ADDGNDR;
/* corpse_xname() sets xnamep; callers other than doname_base()
itself shouldn't care about xnamep (pointer to start of
current obuf[]) but keep it accurate anyway */
@@ -1763,6 +1773,20 @@ doname_with_price(struct obj *obj)
return doname_base(obj, DONAME_WITH_PRICE);
}
/* Name of object including corpse genders. */
char *
doname_with_cgender(struct obj *obj)
{
return doname_base(obj, DONAME_FORCE_GENDER);
}
/* doname with both price and corpse gender */
char *
doname_with_price_and_cgender(struct obj *obj)
{
return doname_base(obj, DONAME_WITH_PRICE | DONAME_FORCE_GENDER);
}
/* "some" instead of precise quantity if obj->dknown not set */
char *
doname_vague_quan(struct obj *obj)
@@ -1837,9 +1861,10 @@ corpse_xname(
any_prefix = (cxn_flags & CXN_ARTICLE) != 0,
/* leave off suffix (do_name() appends "corpse" itself) */
omit_corpse = (cxn_flags & CXN_NOCORPSE) != 0,
gndr_prefix = (cxn_flags & CXN_ADDGNDR) != 0,
possessive = FALSE,
glob = (otmp->otyp != CORPSE && otmp->globby);
const char *mnam;
const char *mnam, *gndr;
/* some callers [aobjnam()] rely on prefix area that xname() sets aside */
gx.xnamep = nextobuf();
@@ -1879,15 +1904,19 @@ corpse_xname(
into the code, so the() has been modified to deal with capitalized
monster names; we could switch to using it below like an() */
gndr = (gndr_prefix && otmp->spe & CORPSTAT_MALE) != 0 ? "male "
: (gndr_prefix && otmp->spe & CORPSTAT_FEMALE) != 0 ? "female "
: "";
if (!adjective || !*adjective) {
Strcat(nambuf, gndr);
/* normal case: newt corpse */
Strcat(nambuf, mnam);
} else {
/* adjective positioning depends upon format of monster name */
if (possessive) /* Medusa's cursed partly eaten corpse */
Sprintf(eos(nambuf), "%s %s", mnam, adjective);
Sprintf(eos(nambuf), "%s %s%s", mnam, gndr, adjective);
else /* cursed partly eaten troll corpse */
Sprintf(eos(nambuf), "%s %s", adjective, mnam);
Sprintf(eos(nambuf), "%s %s%s", adjective, gndr, mnam);
/* in case adjective has a trailing space, squeeze it out */
mungspaces(nambuf);
/* doname() might include a count in the adjective argument;
+6 -2
View File
@@ -1041,7 +1041,7 @@ query_objlist(const char *qstr, /* query string */
unsigned sortflags;
glyph_info tmpglyphinfo = nul_glyphinfo;
Loot *sortedolist, *srtoli;
int clr = NO_COLOR;
int clr = NO_COLOR, puzzling_count = 0;
*pick_list = (menu_item *) 0;
if (!olist && !engulfer)
@@ -1076,6 +1076,8 @@ query_objlist(const char *qstr, /* query string */
return 1;
}
puzzling_count = check_for_puzzling_nonmerge(olist);
sortflags = (((flags.sortloot == 'f'
|| (flags.sortloot == 'l' && !(qflags & USE_INVLET)))
? SORTLOOT_LOOT
@@ -1134,7 +1136,9 @@ query_objlist(const char *qstr, /* query string */
(qflags & USE_INVLET) ? curr->invlet
: (first && curr->oclass == COIN_CLASS) ? '$' : 0,
def_oc_syms[(int) objects[curr->otyp].oc_class].sym,
ATR_NONE, clr, doname_with_price(curr),
ATR_NONE, clr,
(puzzling_count) ? doname_with_price_and_cgender(curr)
: doname_with_price(curr),
MENU_ITEMFLAGS_NONE);
first = FALSE;
}