From ac3d71a82a8a6e006eb1338aad85090ebf5514e3 Mon Sep 17 00:00:00 2001 From: nhmall Date: Sat, 27 Jun 2026 11:18:44 -0400 Subject: [PATCH] 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. --- include/decl.h | 3 +++ include/extern.h | 3 +++ include/hack.h | 1 + src/decl.c | 7 ++++++ src/invent.c | 64 +++++++++++++++++++++++++++++++++++++++++++++--- src/objnam.c | 43 ++++++++++++++++++++++++++------ src/pickup.c | 8 ++++-- 7 files changed, 116 insertions(+), 13 deletions(-) diff --git a/include/decl.h b/include/decl.h index 777c25631..70e454f95 100644 --- a/include/decl.h +++ b/include/decl.h @@ -803,6 +803,9 @@ struct instance_globals_p { int poly_zapped; /* new stuff */ + int puzzling_critera; + char puzzling_ilets[invlet_basic + 1]; + boolean havestate; }; diff --git a/include/extern.h b/include/extern.h index 5da653c38..a6043a94e 100644 --- a/include/extern.h +++ b/include/extern.h @@ -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; diff --git a/include/hack.h b/include/hack.h index 58bedc19a..0d140e8b1 100644 --- a/include/hack.h +++ b/include/hack.h @@ -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 diff --git a/src/decl.c b/src/decl.c index 365500a72..6263eb708 100644 --- a/src/decl.c +++ b/src/decl.c @@ -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*/ }; diff --git a/src/invent.c b/src/invent.c index 347d915cf..b84e05279 100644 --- a/src/invent.c +++ b/src/invent.c @@ -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); diff --git a/src/objnam.c b/src/objnam.c index d9efc1521..233a84b31 100644 --- a/src/objnam.c +++ b/src/objnam.c @@ -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; diff --git a/src/pickup.c b/src/pickup.c index 7cb487b4c..6e1f788a8 100644 --- a/src/pickup.c +++ b/src/pickup.c @@ -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; }