simplify achievement tracking for special objects

This turned out to be a lot more work than I anticipated, but it is
definitely simpler (other than having #wizmakemap take achievements
away if you replace the level that contains the 'prize', which wasn't
handled before).

I cheated and made Mine's End into a no-bones level because the new
flagging scheme for luckstone, bag, and amulet can't carry over from
one game to another.  It probably should have been no-bones all along.
Sokoban didn't have this issue because it's already no-bones.

Existing save files are invalidated.
This commit is contained in:
PatR
2020-01-24 13:54:23 -08:00
parent dba6c20e2d
commit 0166239a22
12 changed files with 85 additions and 71 deletions

View File

@@ -178,7 +178,8 @@ dungeon = {
}, },
{ {
name = "minend", name = "minend",
bonetag = "E", -- 3.7.0: minend changed to no-bones to simplify achievement tracking
-- bonetag = "E"
base = -1, base = -1,
nlevels = 3 nlevels = 3
}, },

View File

@@ -47,6 +47,8 @@ when poly'd into a giant and moving onto a boulder's spot, message given was
random role selection wasn't honoring unwanted alignment(s) properly random role selection wasn't honoring unwanted alignment(s) properly
if at the edge of the map window, trying to move farther fails but used a turn if at the edge of the map window, trying to move farther fails but used a turn
hero can no longer wear blindfold/towel/lenses when poly'd into headless form hero can no longer wear blindfold/towel/lenses when poly'd into headless form
revamp achievement tracking for exploring Mine's End and Sokoban (by acquiring
luckstone and bag of holding or amulet of reflection, respectively)
Fixes to 3.7.0-x Problems that Were Exposed Via git Repository Fixes to 3.7.0-x Problems that Were Exposed Via git Repository

View File

@@ -105,6 +105,11 @@ struct novel_tracking { /* for choosing random passage when reading novel */
passage from the Death Quotes section of dat/tribute */ passage from the Death Quotes section of dat/tribute */
}; };
struct achievement_tracking {
unsigned mines_prize_oid, soko_prize_oid; /* obj->o_id */
short mines_prize_type, soko_prize_typ1, soko_prize_typ2; /* obj->otyp */
};
struct context_info { struct context_info {
unsigned ident; /* social security number for each monster */ unsigned ident; /* social security number for each monster */
unsigned no_of_wizards; /* 0, 1 or 2 (wizard and his shadow) */ unsigned no_of_wizards; /* 0, 1 or 2 (wizard and his shadow) */
@@ -143,6 +148,7 @@ struct context_info {
struct obj_split objsplit; /* track most recently split object stack */ struct obj_split objsplit; /* track most recently split object stack */
struct tribute_info tribute; struct tribute_info tribute;
struct novel_tracking novel; struct novel_tracking novel;
struct achievement_tracking achieveo;
}; };
#endif /* CONTEXT_H */ #endif /* CONTEXT_H */

View File

@@ -215,7 +215,7 @@ typedef struct {
boolean fieldlevel; /* fieldlevel saves saves each field individually */ boolean fieldlevel; /* fieldlevel saves saves each field individually */
boolean addinfo; /* if set, some additional context info from core */ boolean addinfo; /* if set, some additional context info from core */
boolean eof; /* place to mark eof reached */ boolean eof; /* place to mark eof reached */
boolean bendian; /* set to true if executing on a big-endian machine */ boolean bendian; /* set to true if executing on big-endian machine */
FILE *fpdef; /* file pointer for fieldlevel default style */ FILE *fpdef; /* file pointer for fieldlevel default style */
FILE *fpdefmap; /* file pointer mapfile for def format */ FILE *fpdefmap; /* file pointer mapfile for def format */
FILE *fplog; /* file pointer logfile */ FILE *fplog; /* file pointer logfile */
@@ -1184,15 +1184,13 @@ struct instance_globals {
char *lev_message; char *lev_message;
lev_region *lregions; lev_region *lregions;
int num_lregions; int num_lregions;
/* positions touched by level elements explicitly defined in the des-file */ /* positions touched by level elements explicitly defined in des-file */
char SpLev_Map[COLNO][ROWNO]; char SpLev_Map[COLNO][ROWNO];
struct sp_coder *coder; struct sp_coder *coder;
xchar xstart, ystart; xchar xstart, ystart;
char xsize, ysize; char xsize, ysize;
boolean splev_init_present; boolean splev_init_present;
boolean icedpools; boolean icedpools;
int mines_prize_count;
int soko_prize_count; /* achievements */
struct obj *container_obj[MAX_CONTAINMENT]; struct obj *container_obj[MAX_CONTAINMENT];
int container_idx; int container_idx;
struct monst *invent_carrying_monster; struct monst *invent_carrying_monster;

View File

@@ -346,19 +346,9 @@ struct obj {
&& !undiscovered_artifact(ART_EYES_OF_THE_OVERWORLD))) && !undiscovered_artifact(ART_EYES_OF_THE_OVERWORLD)))
#define pair_of(o) ((o)->otyp == LENSES || is_gloves(o) || is_boots(o)) #define pair_of(o) ((o)->otyp == LENSES || is_gloves(o) || is_boots(o))
/* 'PRIZE' values override obj->corpsenm so prizes mustn't be object types /* achievement tracking; 3.6.x did this differently */
which use that field for monster type (or other overloaded purpose) */ #define is_mines_prize(o) ((o)->o_id == g.context.achieveo.mines_prize_oid)
#define MINES_PRIZE 1 #define is_soko_prize(o) ((o)->o_id == g.context.achieveo.soko_prize_oid)
#define SOKO_PRIZE1 2
#define SOKO_PRIZE2 3
#define is_mines_prize(o) \
((o)->otyp == iflags.mines_prize_type \
&& (o)->record_achieve_special == MINES_PRIZE)
#define is_soko_prize(o) \
(((o)->otyp == iflags.soko_prize_type1 \
&& (o)->record_achieve_special == SOKO_PRIZE1) \
|| ((o)->otyp == iflags.soko_prize_type2 \
&& (o)->record_achieve_special == SOKO_PRIZE2))
/* Flags for get_obj_location(). */ /* Flags for get_obj_location(). */
#define CONTAINED_TOO 0x1 #define CONTAINED_TOO 0x1

View File

@@ -14,7 +14,7 @@
* Incrementing EDITLEVEL can be used to force invalidation of old bones * Incrementing EDITLEVEL can be used to force invalidation of old bones
* and save files. * and save files.
*/ */
#define EDITLEVEL 10 #define EDITLEVEL 11
#define COPYRIGHT_BANNER_A "NetHack, Copyright 1985-2020" #define COPYRIGHT_BANNER_A "NetHack, Copyright 1985-2020"
#define COPYRIGHT_BANNER_B \ #define COPYRIGHT_BANNER_B \

View File

@@ -165,14 +165,10 @@ boolean restore;
if (mnum == PM_DOPPELGANGER && otmp->otyp == CORPSE) if (mnum == PM_DOPPELGANGER && otmp->otyp == CORPSE)
set_corpsenm(otmp, mnum); set_corpsenm(otmp, mnum);
} }
} else if ((otmp->otyp == iflags.mines_prize_type } else if (is_mines_prize(otmp) || is_soko_prize(otmp)) {
&& !Is_mineend_level(&u.uz)) /* achievement tracking; in case prize was moved off its
|| ((otmp->otyp == iflags.soko_prize_type1 original level (which is always a no-bones level) */
|| otmp->otyp == iflags.soko_prize_type2) otmp->nomerge = 0;
&& !Is_sokoend_level(&u.uz))) {
/* "special prize" in this game becomes ordinary object
if loaded into another game */
otmp->record_achieve_special = NON_PM;
} else if (otmp->otyp == AMULET_OF_YENDOR) { } else if (otmp->otyp == AMULET_OF_YENDOR) {
/* no longer the real Amulet */ /* no longer the real Amulet */
otmp->otyp = FAKE_AMULET_OF_YENDOR; otmp->otyp = FAKE_AMULET_OF_YENDOR;

View File

@@ -806,6 +806,24 @@ boolean pre, wiztower;
continue; continue;
if (mtmp->isshk) if (mtmp->isshk)
setpaid(mtmp); setpaid(mtmp);
/* achievement tracking */
{
static const char Unachieve[] = "%s achievement revoked.";
if (Is_mineend_level(&u.uz)) {
if (u.uachieve.mines_luckstone) {
pline(Unachieve, "Mine's end");
u.uachieve.mines_luckstone = 0;
}
g.context.achieveo.mines_prize_oid = 0;
} else if (Is_sokoend_level(&u.uz)) {
if (u.uachieve.finish_sokoban) {
pline(Unachieve, "Sokoban end");
u.uachieve.finish_sokoban = 0;
}
g.context.achieveo.soko_prize_oid = 0;
}
}
/* TODO? /* TODO?
* Reduce 'born' tally for each monster about to be discarded * Reduce 'born' tally for each monster about to be discarded
* by savelev(), otherwise replacing heavily populated levels * by savelev(), otherwise replacing heavily populated levels
@@ -877,6 +895,7 @@ wiz_makemap(VOID_ARGS)
{ {
if (wizard) { if (wizard) {
boolean was_in_W_tower = In_W_tower(u.ux, u.uy, &u.uz); boolean was_in_W_tower = In_W_tower(u.ux, u.uy, &u.uz);
makemap_prepost(TRUE, was_in_W_tower); makemap_prepost(TRUE, was_in_W_tower);
/* create a new level; various things like bestowing a guardian /* create a new level; various things like bestowing a guardian
angel on Astral or setting off alarm on Ft.Ludios are handled angel on Astral or setting off alarm on Ft.Ludios are handled

View File

@@ -638,8 +638,6 @@ const struct instance_globals g_init = {
UNDEFINED_VALUE, /* ysize */ UNDEFINED_VALUE, /* ysize */
FALSE, /* splev_init_present */ FALSE, /* splev_init_present */
FALSE, /* icedpools */ FALSE, /* icedpools */
0, /* mines_prize_count */
0, /* soki_prize_count */
{ UNDEFINED_PTR }, /* container_obj */ { UNDEFINED_PTR }, /* container_obj */
0, /* container_idx */ 0, /* container_idx */
NULL, /* invent_carrying_monster */ NULL, /* invent_carrying_monster */

View File

@@ -835,20 +835,14 @@ struct obj *obj;
} }
/* "special achievements" aren't discoverable during play, they /* "special achievements" aren't discoverable during play, they
end up being recorded in XLOGFILE at end of game, nowhere else; end up being recorded in XLOGFILE at end of game, nowhere else */
record_achieve_special overloads corpsenm which is ordinarily
initialized to NON_PM (-1) rather than to 0; any special prize
must never be a corpse, egg, tin, figurine, or statue because
their use of obj->corpsenm for monster type would conflict,
nor be a leash (corpsenm overloaded for m_id of leashed
monster) or a novel (corpsenm overloaded for novel index) */
if (is_mines_prize(obj)) { if (is_mines_prize(obj)) {
u.uachieve.mines_luckstone = 1; u.uachieve.mines_luckstone = 1;
obj->record_achieve_special = NON_PM; g.context.achieveo.mines_prize_oid = 0;
obj->nomerge = 0; obj->nomerge = 0;
} else if (is_soko_prize(obj)) { } else if (is_soko_prize(obj)) {
u.uachieve.finish_sokoban = 1; u.uachieve.finish_sokoban = 1;
obj->record_achieve_special = NON_PM; g.context.achieveo.soko_prize_oid = 0;
obj->nomerge = 0; obj->nomerge = 0;
} }
} }

View File

@@ -737,9 +737,9 @@ initoptions_init()
/* for "special achievement" tracking (see obj.h, /* for "special achievement" tracking (see obj.h,
create_object(sp_lev.c), addinv_core1(invent.c) */ create_object(sp_lev.c), addinv_core1(invent.c) */
iflags.mines_prize_type = LUCKSTONE; g.context.achieveo.mines_prize_type = LUCKSTONE;
iflags.soko_prize_type1 = BAG_OF_HOLDING; g.context.achieveo.soko_prize_typ1 = BAG_OF_HOLDING;
iflags.soko_prize_type2 = AMULET_OF_REFLECTION; g.context.achieveo.soko_prize_typ2 = AMULET_OF_REFLECTION;
/* assert( sizeof flags.inv_order == sizeof def_inv_order ); */ /* assert( sizeof flags.inv_order == sizeof def_inv_order ); */
(void) memcpy((genericptr_t) flags.inv_order, (void) memcpy((genericptr_t) flags.inv_order,

View File

@@ -1569,32 +1569,34 @@ struct mkroom *croom;
if (o->id != -1) { if (o->id != -1) {
static const char prize_warning[] = "multiple prizes on %s level"; static const char prize_warning[] = "multiple prizes on %s level";
/* if this is a specific item of the right type and it is being /*
created on the right level, flag it as the designated item * If this is a specific item of the right type and it is being
used to detect a special achievement (to whit, reaching and * created on the right level, flag it as the designated item
exploring the target level, although the exploration part * used to detect a special achievement (to whit, reaching and
might be short-circuited if a monster brings object to hero) */ * exploring the target level, although the exploration part
if (Is_mineend_level(&u.uz)) { * might be short-circuited if a monster brings object to hero).
if (otmp->otyp == iflags.mines_prize_type) { *
if (!g.mines_prize_count++) { * Random items of the appropriate type won't trigger a false
/* Note: the first luckstone on lev will become the prize * match--they'll fail the (id != -1) test above--but the level
even if its not the explicit one, but random */ * definition should not include a second instance of any prize.
otmp->record_achieve_special = MINES_PRIZE; */
/* prevent stacking; cleared when achievement is recorded */ if (Is_mineend_level(&u.uz)
otmp->nomerge = 1; && otmp->otyp == g.context.achieveo.mines_prize_type) {
} if (!g.context.achieveo.mines_prize_oid) {
g.context.achieveo.mines_prize_oid = otmp->o_id;
/* prevent stacking; cleared when achievement is recorded */
otmp->nomerge = 1;
} else {
impossible(prize_warning, "mines end");
} }
} else if (Is_sokoend_level(&u.uz)) { } else if (Is_sokoend_level(&u.uz)
if (otmp->otyp == iflags.soko_prize_type1) { && (otmp->otyp == g.context.achieveo.soko_prize_typ1
otmp->record_achieve_special = SOKO_PRIZE1; || otmp->otyp == g.context.achieveo.soko_prize_typ2)) {
if (!g.context.achieveo.soko_prize_oid) {
g.context.achieveo.soko_prize_oid = otmp->o_id;
otmp->nomerge = 1; /* redundant; Sokoban prizes don't stack */ otmp->nomerge = 1; /* redundant; Sokoban prizes don't stack */
if (++g.soko_prize_count > 1) } else {
impossible(prize_warning, "sokoban end"); impossible(prize_warning, "sokoban end");
} else if (otmp->otyp == iflags.soko_prize_type2) {
otmp->record_achieve_special = SOKO_PRIZE2;
otmp->nomerge = 1; /* redundant; Sokoban prizes don't stack */
if (++g.soko_prize_count > 1)
impossible(prize_warning, "sokoban end");
} }
} }
} }
@@ -2590,14 +2592,26 @@ const char *s;
/* find by object name */ /* find by object name */
for (i = 0; i < NUM_OBJECTS; i++) { for (i = 0; i < NUM_OBJECTS; i++) {
objname = obj_descr[i].oc_name; objname = OBJ_NAME(objects[i]);
if (objname && !strcmpi(s, objname)) if (objname && !strcmpi(s, objname))
return i; return i;
} }
/*
* FIXME:
* If the file specifies "orange potion", the actual object
* description is just "orange" and won't match. [There's a
* reason that wish handling is insanely complicated.] And
* even if that gets fixed, if the file specifies "gray stone"
* it will start matching but would always pick the first one.
*
* "orange potion" is an unlikely thing to have in a special
* level description but "gray stone" is not....
*/
/* find by object description */ /* find by object description */
for (i = 0; i < NUM_OBJECTS; i++) { for (i = 0; i < NUM_OBJECTS; i++) {
objname = obj_descr[i].oc_descr; objname = OBJ_DESCR(objects[i]);
if (objname && !strcmpi(s, objname)) if (objname && !strcmpi(s, objname))
return i; return i;
} }
@@ -5419,10 +5433,6 @@ sp_level_coder_init()
g.splev_init_present = FALSE; g.splev_init_present = FALSE;
g.icedpools = FALSE; g.icedpools = FALSE;
/* achievement tracking; static init would suffice except we need to
reset if #wizmakemap is used to recreate mines' end or sokoban end;
once either level is created, these values can be forgotten */
g.mines_prize_count = g.soko_prize_count = 0;
for (tmpi = 0; tmpi <= MAX_NESTED_ROOMS; tmpi++) { for (tmpi = 0; tmpi <= MAX_NESTED_ROOMS; tmpi++) {
coder->tmproomlist[tmpi] = (struct mkroom *) 0; coder->tmproomlist[tmpi] = (struct mkroom *) 0;