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 (!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) { - 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?"); - return ECMD_OK; + 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; + 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 (u.ustuck || (welded(uwep) && bimanual(uwep))) { - Your("%s seem to be too busy for that.", makeplural(body_part(HAND))); - return ECMD_OK; + 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.");