From: PatR Date: Fri, 6 May 2022 21:44:57 +0000 (-0700) Subject: autounlock:untrap X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=02207b967a023b0d3639de7d9867101bdcff609e;p=nethack autounlock:untrap Implement 'untrap' as an 'autounlock' action. Quite a bit more work than anticipated. The new documentation is rather clumsy; too many if-this and if-not-that clauses have intruded. I'll be astonished if all the return values are correct.... [A couple of places were checking for (rx != 0 && ry != 0) to decide whether they were performating an autounlock action at but that erroneously excludes the top line of the map if the current level extends that far. Just check rx for zero/non-zero.] --- diff --git a/dat/opthelp b/dat/opthelp index a9b604d2f..6fc5cc941 100644 --- a/dat/opthelp +++ b/dat/opthelp @@ -148,12 +148,18 @@ Compound options are written as option_name:option_value. Compound options which can be set during the game are: -autounlock when attempting to open a door or loot a [Apply-Key] - container that is locked, specifies an action to take: - can be None, or one or more of Apply-Key + Kick + Force; +autounlock when attempting to open a locked door or loot [Apply-Key] + a locked container, specifies an action to take: can be + None, or one or more of Untrap + Apply-Key + Kick + Force; + if Untrap is included, it will be handled first; the + others apply if you answer "no" to "check for traps?"; + for "yes", it won't necessary find anything even when a + trap happens to be present and will use up the rest of + the current move regardless of whether it finds anything; Kick is only useful for doors and Force is only useful for - containers; either will only be attempted if Apply-Key is - omitted or you aren't carrying any unlocking tool + containers; either will only be attempted if Untrap is + omitted or skipped and Apply-Key is omitted or you aren't + carrying an unlocking tool or you decline to use one boulder override the default boulder symbol [`] disclose the types of information you want [ni na nv ng nc no] offered at the end of the game diff --git a/doc/Guidebook.mn b/doc/Guidebook.mn index 338f350a5..be7f9542c 100644 --- a/doc/Guidebook.mn +++ b/doc/Guidebook.mn @@ -3662,15 +3662,26 @@ Persistent. Controls what action to take when attempting to walk into a locked door or to loot a locked container. Takes a plus-sign separated list of values: -\fIapply-key\fP which will attempt to use a key or other unlocking tool -if you have one; -\fIkick\fP which will kick the door (if you lack a key or omit apply-key; +.PS Apply-Key +.PL Untrap +prompt about whether to attempt to find a trap; +it might fail to find one even when present; if it does find one, it +will ask whether you want to try to disarm the trap; if you decline, +your character will forget that the door or box is trapped; +.PL Apply-Key +if carrying a key or other unlocking tool, prompt about using it; +.PL Kick +kick the door (if you omit untrap or decline to attempt untrap and +you omit apply-key or you lack a key or you decline to use the key; has no effect on containers); -\fIforce\fP which will try to force a container's lid with your currently -wielded weapon (if you lack a key or omit apply-key; has no effect on -doors); or -\fInone\fP which can't be combined with the other choices. -.lp "" +.PL Force +try to force a container's lid with your currently +wielded weapon (if you omit untrap or decline to attempt untrap and +you omit apply-key or you lack a key or you decline to use the key; +has no effect on doors); +.PL None +none of the above; can't be combined with the other choices. +.PE Omitting the value is treated as if \f(CRautounlock:apply-key\fP. Preceding \f(CRautounlock\fP with \(oq!\(cq or \(lqno\(rq is treated as \f(CRautounlock:none\fP. @@ -3680,7 +3691,7 @@ Successfully kicking a door will break it and wake up nearby monsters. Successfully forcing a container open will break its lock and might also destroy some of its contents or damage your weapon or both. .lp "" -The default is \fIapply-key\fP. +The default is Apply-Key. Persistent. .lp blind Start the character permanently blind (default false). diff --git a/doc/Guidebook.tex b/doc/Guidebook.tex index fc35a5a04..7e2084d8c 100644 --- a/doc/Guidebook.tex +++ b/doc/Guidebook.tex @@ -3970,19 +3970,42 @@ If no weapon is found or the option is false, the `t' (throw) command is executed instead. Persistent. %.lp \item[\ib{autounlock}] +%\hyphenation{apply\-key}%this needs to be tested... Controls what action to take when attempting to walk into a locked door or to loot a locked container. Takes a plus-sign separated list of values: -{\it apply-key\/} which will attempt to use a key or other unlocking tool -if you have one; -{\it kick\/} which will kick the door (if you lack a key or omit apply-key; +% paranoid +% au => autounlock +\newlength{\auwidth} +%.PS Apply-Key +\settowidth{\auwidth}{\tt Apply-Key} +\addtolength{\auwidth}{\labelsep} +\blist{\leftmargin \auwidth \topsep 1mm \itemsep 0mm} +%.PL Untrap +\item[{\tt Untrap}] +prompt about whether to attempt to find a trap; +it might fail to find one even when present; if it does find one, it +will ask whether you want to try to disarm the trap; if you decline, +your character will forget that the door or box is trapped; +%.PL Apply-Key +\item[{\tt Apply-Key}] +if carrying a key or other unlocking tool, prompt about using it; +%.PL Kick +\item[{\tt Kick}] +kick the door (if you omit untrap or decline to attempt untrap and +you omit apply-key or you lack a key or you decline to use the key; has no effect on containers); -{\it force\/} which will try to force a container's lid with your currently -wielded weapon (if you lack a key or omit apply-key; has no effect on -doors); or -{\it none\/} which can't be combined with the other choices. -\\ -%.lp "" +%.PL Force +\item[{\tt Force}] +try to force a container's lid with your currently +wielded weapon (if you omit untrap or decline to attempt untrap and +you omit apply-key or you lack a key or you decline to use the key; +has no effect on doors); +%.PL None +\item[{\tt None}] +none of the above; can't be combined with the other choices. +%.PE +\elist Omitting the value is treated as if {\tt autounlock:apply-key}. Preceding {\tt autounlock} with `{\tt !}' or ``{\tt no}'' is treated as {\tt autounlock:none}. @@ -3994,7 +4017,7 @@ Successfully forcing a container open will break its lock and might also destroy some of its contents or damage your weapon or both. \\ %.lp "" -The default is {\it apply-key\/}. +The default is Apply-Key. Persistent. %.lp \item[\ib{blind}] diff --git a/doc/fixes3-7-0.txt b/doc/fixes3-7-0.txt index 1bc975ee4..386cac323 100644 --- a/doc/fixes3-7-0.txt +++ b/doc/fixes3-7-0.txt @@ -1197,6 +1197,7 @@ using #wizmakemap on Plane of Water added a new set of air bubbles each time it was run and eventually replaced just about all the water; likewise with clouds on Plane of Air avoid new "where are we?" panic if player quits during character selection +add Untrap as a potential 'autounlock' action curses: 'msg_window' option wasn't functional for curses unless the binary also included tty support diff --git a/include/extern.h b/include/extern.h index 92e99f964..bdd94e641 100644 --- a/include/extern.h +++ b/include/extern.h @@ -2750,8 +2750,9 @@ extern void water_damage_chain(struct obj *, boolean); extern boolean drown(void); extern void drain_en(int); extern int dountrap(void); +extern int could_untrap(boolean, boolean); extern void cnv_trap_obj(int, int, struct trap *, boolean); -extern int untrap(boolean); +extern int untrap(boolean, int, int, struct obj *); extern boolean openholdingtrap(struct monst *, boolean *); extern boolean closeholdingtrap(struct monst *, boolean *); extern boolean openfallingtrap(struct monst *, boolean, boolean *); diff --git a/src/artifact.c b/src/artifact.c index 7d251ce33..2ecbb2bc8 100644 --- a/src/artifact.c +++ b/src/artifact.c @@ -1720,7 +1720,7 @@ arti_invoke(struct obj *obj) break; } case UNTRAP: { - if (!untrap(TRUE)) { + if (!untrap(TRUE, 0, 0, (struct obj *) 0)) { obj->age = 0; /* don't charge for changing their mind */ return ECMD_OK; } diff --git a/src/lock.c b/src/lock.c index 80fdbedbc..d2d934cb2 100644 --- a/src/lock.c +++ b/src/lock.c @@ -356,14 +356,19 @@ pick_lock( * does not prompt for direction if these are set */ struct obj *container) /* container, for autounlock */ { + struct obj dummypick; int picktyp, c, ch; coord cc; struct rm *door; struct obj *otmp; char qbuf[QBUFSZ]; - boolean autounlock = (((rx != 0 && ry != 0) || container != NULL) - && (flags.autounlock & AUTOUNLOCK_APPLY_KEY) != 0); + boolean autounlock = (rx != 0 || container != NULL); + /* 'pick' might be Null [called by do_loot_cont() for AUTOUNLOCK_UNTRAP] */ + if (!pick) { + dummypick = cg.zeroobj; + pick = &dummypick; /* pick->otyp will be STRANGE_OBJECT */ + } picktyp = pick->otyp; /* check whether we're resuming an interrupted previous attempt */ @@ -401,15 +406,14 @@ pick_lock( return PICKLOCK_DID_NOTHING; } - if (picktyp != LOCK_PICK - && picktyp != CREDIT_CARD - && picktyp != SKELETON_KEY) { + if (pick != &dummypick && picktyp != SKELETON_KEY + && picktyp != LOCK_PICK && picktyp != CREDIT_CARD) { impossible("picking lock with object %d?", picktyp); return PICKLOCK_DID_NOTHING; } ch = 0; /* lint suppression */ - if (rx != 0 && ry != 0) { /* autounlock; caller has provided coordinates */ + if (rx != 0) { /* autounlock; caller has provided coordinates */ cc.x = rx; cc.y = ry; } else if (!get_adjacent_loc((char *) 0, "Invalid location!", @@ -458,11 +462,25 @@ pick_lock( else verb = "pick"; - if (autounlock) { - Sprintf(qbuf, "Unlock it with %s?", yname(pick)); - c = yn(qbuf); - if (c == 'n') - return 0; + if (autounlock && (flags.autounlock & AUTOUNLOCK_UNTRAP) != 0 + && could_untrap(FALSE, TRUE) + && (c = ynq(safe_qbuf(qbuf, "Check ", " for a trap?", + otmp, yname, ysimple_name, "this"))) + != 'n') { + if (c == 'q') + return PICKLOCK_DID_NOTHING; /* c == 'q' */ + /* c == 'y' */ + untrap(FALSE, 0, 0, otmp); + return PICKLOCK_DID_SOMETHING; /* even if no trap found */ + } else if (autounlock + && (flags.autounlock & AUTOUNLOCK_APPLY_KEY) != 0) { + c = 'q'; + if (pick != &dummypick) { + Sprintf(qbuf, "Unlock it with %s?", yname(pick)); + c = ynq(qbuf); + } + if (c != 'y') + return PICKLOCK_DID_NOTHING; } else { /* "There is here; ?" */ Sprintf(qsfx, " here; %s %s?", @@ -473,9 +491,9 @@ pick_lock( c = ynq(qbuf); if (c == 'q') - return 0; + return PICKLOCK_DID_NOTHING; if (c == 'n') - continue; + continue; /* try next box */ } if (otmp->obroken) { @@ -561,6 +579,15 @@ pick_lock( pline("This door is broken."); return PICKLOCK_LEARNED_SOMETHING; default: + if ((flags.autounlock & AUTOUNLOCK_UNTRAP) != 0 + && could_untrap(FALSE, FALSE) + && (c = ynq("Check this door for a trap?")) != 'n') { + if (c == 'q') + return PICKLOCK_DID_NOTHING; + /* c == 'y' */ + untrap(FALSE, cc.x, cc.y, (struct obj *) 0); + return PICKLOCK_DID_SOMETHING; /* even if no trap found */ + } /* credit cards are only good for unlocking */ if (picktyp == CREDIT_CARD && !(door->doormask & D_LOCKED)) { You_cant("lock a door with a credit card."); @@ -571,9 +598,8 @@ pick_lock( (door->doormask & D_LOCKED) ? "Unlock" : "Lock", autounlock ? " with " : "", autounlock ? yname(pick) : ""); - - c = yn(qbuf); - if (c == 'n') + c = ynq(qbuf); + if (c != 'y') return 0; /* note: for !autounlock, 'apply' already did touch check */ diff --git a/src/options.c b/src/options.c index 6a29cd562..796935a72 100644 --- a/src/options.c +++ b/src/options.c @@ -4534,8 +4534,6 @@ handler_autounlock(int optidx) start_menu(tmpwin, MENU_BEHAVE_STANDARD); any = cg.zeroany; for (i = 0; i < SIZE(unlocktypes); ++i) { - if (i == 1) /*** suppress 'untrap' from the menu... ***/ - continue; /*** until it actually gets implemented ***/ Sprintf(buf, "%-10.10s%c%.40s", unlocktypes[i][0], sep, unlocktypes[i][1]); presel = !i ? !flags.autounlock : (flags.autounlock & (1 << (i - 1))); diff --git a/src/pickup.c b/src/pickup.c index b358040e8..79f74ebc1 100644 --- a/src/pickup.c +++ b/src/pickup.c @@ -1863,43 +1863,64 @@ mon_beside(int x, int y) } static int -do_loot_cont(struct obj **cobjp, - int cindex, /* index of this container (1..N)... */ - int ccount) /* ...number of them (N) */ +do_loot_cont( + struct obj **cobjp, + int cindex, /* index of this container (1..N)... */ + int ccount) /* ...number of them (N) */ { struct obj *cobj = *cobjp; if (!cobj) return ECMD_OK; if (cobj->olocked) { + int res = ECMD_OK; + +#if 0 if (ccount < 2 && (g.level.objects[cobj->ox][cobj->oy] == cobj)) pline("%s locked.", cobj->lknown ? "It is" : "Hmmm, it turns out to be"); - else if (cobj->lknown) + else +#endif + if (cobj->lknown) pline("%s is locked.", The(xname(cobj))); else pline("Hmmm, %s turns out to be locked.", the(xname(cobj))); cobj->lknown = 1; if (flags.autounlock) { - struct obj *unlocktool; + struct obj *otmp, *unlocktool = 0; + xchar ox = cobj->ox, oy = cobj->oy; - /* TODO: handle AUTOUNLOCK_UNTRAP and maybe add kicking at - self when chest present to handle AUTOUNLOCK_KICK */ u.dz = 0; /* might be non-zero from previous command since * #loot isn't a move command; pick_lock() cares */ - if ((flags.autounlock & AUTOUNLOCK_APPLY_KEY) != 0 - && (unlocktool = autokey(TRUE)) != 0) { + /* if both the untrap and apply_key bits are set, untrap + attempt will be performed first but we need to set up + unlocktool in case "check for trap?" is declined */ + if (((flags.autounlock & AUTOUNLOCK_APPLY_KEY) != 0 + && (unlocktool = autokey(TRUE)) != 0) + || (flags.autounlock & AUTOUNLOCK_UNTRAP) != 0) { /* pass ox and oy to avoid direction prompt */ - return (pick_lock(unlocktool, cobj->ox, cobj->oy, cobj) != 0); - } else if ((flags.autounlock & AUTOUNLOCK_FORCE) != 0 - && ccount == 1 && u_have_forceable_weapon()) { + if (pick_lock(unlocktool, ox, oy, cobj)) + res = ECMD_TIME; + /* attempting to untrap or unlock might trigger a trap + which destroys 'cobj'; inform caller if that happens */ + for (otmp = g.level.objects[ox][oy]; otmp; + otmp = otmp->nexthere) + if (otmp == cobj) + break; + if (!otmp) + *cobjp = (struct obj *) 0; + return res; + } + if ((flags.autounlock & AUTOUNLOCK_FORCE) != 0 + && res != ECMD_TIME + && ccount == 1 && u_have_forceable_weapon()) { /* single container, and we could #force it open... */ cmdq_add_ec(doforce); /* doforce asks for confirmation */ g.abort_looting = TRUE; } } - return ECMD_OK; + return res; } cobj->lknown = 1; /* floor container, so no need for update_inventory() */ diff --git a/src/trap.c b/src/trap.c index e66de0966..1996e9cc4 100644 --- a/src/trap.c +++ b/src/trap.c @@ -61,6 +61,7 @@ static void clear_conjoined_pits(struct trap *); static boolean adj_nonconjoined_pit(struct trap *); static int try_lift(struct monst *, struct trap *, int, boolean); static int help_monster_out(struct monst *, struct trap *); +static void untrap_box(struct obj *, boolean, boolean); #if 0 static void join_adjacent_pits(struct trap *); #endif @@ -4495,23 +4496,40 @@ drain_en(int n) int dountrap(void) { - if (near_capacity() >= HVY_ENCUMBER) { - pline("You're too strained to do that."); - return ECMD_OK; - } - if ((nohands(g.youmonst.data) && !webmaker(g.youmonst.data)) - || !g.youmonst.data->mmove) { - pline("And just how do you expect to do that?"); + if (!could_untrap(TRUE, FALSE)) return ECMD_OK; + + return untrap(FALSE, 0, 0, (struct obj *) 0) ? ECMD_TIME : ECMD_OK; +} + +/* the #untrap command - disarm a trap */ +int +could_untrap(boolean verbosely, boolean check_floor) +{ + char buf[BUFSZ]; + + buf[0] = '\0'; + if (near_capacity() >= HVY_ENCUMBER) { + Strcpy(buf, "You're too strained to do that."); + } else if ((nohands(g.youmonst.data) && !webmaker(g.youmonst.data)) + || !g.youmonst.data->mmove) { + Strcpy(buf, "And just how do you expect to do that?"); } else if (u.ustuck && sticks(g.youmonst.data)) { - pline("You'll have to let go of %s first.", mon_nam(u.ustuck)); - return ECMD_OK; - } - if (u.ustuck || (welded(uwep) && bimanual(uwep))) { - Your("%s seem to be too busy for that.", makeplural(body_part(HAND))); - return ECMD_OK; + Sprintf(buf, "You'll have to let go of %s first.", mon_nam(u.ustuck)); + } else if (u.ustuck || (welded(uwep) && bimanual(uwep))) { + Sprintf(buf, "Your %s seem to be too busy for that.", + makeplural(body_part(HAND))); + } else if (check_floor && !can_reach_floor(FALSE)) { + /* only checked here for autounlock of chest/box and that will + be !verbosely so precise details of the message don't matter */ + Sprintf(buf, "You can't reach the %s.", surface(u.ux, u.uy)); + } + if (buf[0]) { + if (verbosely) + pline("%s", buf); + return 0; } - return untrap(FALSE) ? ECMD_TIME : ECMD_OK; + return 1; } /* Probability of disabling a trap. Helge Hafting */ @@ -4964,8 +4982,50 @@ help_monster_out( return 1; } +/* check a particular container for a trap and optionally disarm it */ +static void +untrap_box( + struct obj *box, + boolean force, + boolean confused) +{ + if ((box->otrapped + && (force || (!confused && rn2(MAXULEV + 1 - u.ulevel) < 10))) + || (!force && confused && !rn2(3))) { + You("find a trap on %s!", the(xname(box))); + if (!confused) + exercise(A_WIS, TRUE); + + if (ynq("Disarm it?") == 'y') { + if (box->otrapped) { + int ch = ACURR(A_DEX) + u.ulevel; + + if (Role_if(PM_ROGUE)) + ch *= 2; + if (!force && (confused || Fumbling + || rnd(75 + level_difficulty() / 2) > ch)) { + (void) chest_trap(box, FINGER, TRUE); + /* 'box' might be gone now */ + } else { + You("disarm it!"); + box->otrapped = 0; + } + exercise(A_DEX, TRUE); + } else { + pline("That %s was not trapped.", xname(box)); + } + } + } else { + You("find no traps on %s.", the(xname(box))); + } +} + +/* hero is able to attempt untrap, so do so */ int -untrap(boolean force) +untrap( + boolean force, + int rx, int ry, + struct obj *container) { register struct obj *otmp; register int x, y; @@ -4975,22 +5035,35 @@ untrap(boolean force) const char *trapdescr; boolean here, useplural, deal_with_floor_trap, confused = (Confusion || Hallucination), - trap_skipped = FALSE; + trap_skipped = FALSE, autounlock_door = FALSE; int boxcnt = 0; char the_trap[BUFSZ], qbuf[QBUFSZ]; - if (!getdir((char *) 0)) - return 0; - x = u.ux + u.dx; - y = u.uy + u.dy; + /* 'force' is true for #invoke; if carrying MKoT, make it be true + for #untrap or autounlock */ + if (!force && has_magic_key(&g.youmonst)) + force = TRUE; + + if (!rx && !container) { + /* usual case */ + if (!getdir((char *) 0)) + return 0; + x = u.ux + u.dx; + y = u.uy + u.dy; + } else { + /* autounlock's untrap; skip most prompting */ + if (container) { + untrap_box(container, force, confused); + return 1; + } + /* levl[rx][ry] is a locked or trapped door */ + x = rx, y = ry; + autounlock_door = TRUE; + } if (!isok(x, y)) { pline_The("perils lurking there are beyond your grasp."); return 0; } - /* 'force' is true for #invoke; make it be true for #untrap if - carrying MKoT */ - if (!force && has_magic_key(&g.youmonst)) - force = TRUE; ttmp = t_at(x, y); if (ttmp && !ttmp->tseen) @@ -5006,7 +5079,9 @@ untrap(boolean force) } deal_with_floor_trap = can_reach_floor(FALSE); - if (!deal_with_floor_trap) { + if (autounlock_door) { + ; /* skip a bunch */ + } else if (!deal_with_floor_trap) { *the_trap = '\0'; if (ttmp) Strcat(the_trap, an(trapdescr)); @@ -5095,6 +5170,11 @@ untrap(boolean force) } /* end if */ if (boxcnt) { + /* 3.7: this used to allow searching for traps on multiple + containers on the same move and needed to keep track of + whether any had been found but not attempted to untrap; + now at most one per move may be checked and we only + continue on to door handling if they are all declined */ for (otmp = g.level.objects[x][y]; otmp; otmp = otmp->nexthere) if (Is_box(otmp)) { (void) safe_qbuf(qbuf, "There is ", @@ -5103,59 +5183,34 @@ untrap(boolean force) switch (ynq(qbuf)) { case 'q': return 0; - case 'n': - continue; - } - - if ((otmp->otrapped - && (force || (!confused - && rn2(MAXULEV + 1 - u.ulevel) < 10))) - || (!force && confused && !rn2(3))) { - You("find a trap on %s!", the(xname(otmp))); - if (!confused) - exercise(A_WIS, TRUE); - - switch (ynq("Disarm it?")) { - case 'q': - return 1; - case 'n': - trap_skipped = TRUE; - continue; - } - - if (otmp->otrapped) { - exercise(A_DEX, TRUE); - ch = ACURR(A_DEX) + u.ulevel; - if (Role_if(PM_ROGUE)) - ch *= 2; - if (!force && (confused || Fumbling - || rnd(75 + level_difficulty() / 2) - > ch)) { - (void) chest_trap(otmp, FINGER, TRUE); - } else { - You("disarm it!"); - otmp->otrapped = 0; - } - } else - pline("That %s was not trapped.", xname(otmp)); - return 1; - } else { - You("find no traps on %s.", the(xname(otmp))); - return 1; + case 'y': + untrap_box(otmp, force, confused); + return 1; /* even for 'no' at "Disarm it?" prompt */ } + /* 'n' => continue to next box */ } - - You(trap_skipped ? "find no other traps here." - : "know of no traps here."); - return 0; + There("are no other chests or boxes here."); } if (stumble_on_door_mimic(x, y)) return 1; - } /* deal_with_floor_trap */ - /* doors can be manipulated even while levitating/unskilled riding */ + /* + * Doors can be manipulated even while levitating/unskilled riding. + * + * Ordinarily there won't be a closed or locked door at the same + * location as a floor trap or a container. However, there could + * be a container at a closed/locked door spot if it was dropped + * there by a monster or poly'd hero with Passes_walls capability, + * and poly'd hero could move onto that spot and attempt #untrap + * in direction '.' or '>'. We'll get here for that situation if + * player declines to check all containers for traps. + * + * The usual situation is #untrap toward an adjacent closed door. + * No floor trap would be present and any containers would be + * ignored because they're only checked when direction is '.'/'>'. + */ if (!IS_DOOR(levl[x][y].typ)) { if (!trap_skipped) You("know of no traps there.");