autounlock overhaul

This gives the player more control over what autounlock does.  It is
now a compound option rather than a boolean, and takes values of
  autounlock:none
  !autounlock or noautounlock (shortcuts for none)
  autounlock:untrap + apply-key + kick + force (spaces are optional
    or can be used instead of plus-signs, but can't mix "foo bar+quux")
  autounlock (without a value, shortcut for autounlock:apply-key).
Default is autounlock:apply-key.

Untrap isn't implemented (feel free to jump in) so is suppressed from
the 'O' command's new sub-menu for autounlock.  It's parsed and
accepted from .nethackrc but won't accomplish anything.

[Just musing: it should be feasible to kick in direction '.' to break
open a container or #force to an adjacent spot to break open a door.
If that was done, autounlock:kick+force (or more likely autounlock:
apply-key+kick+force when lacking a key) would resort to force if hero
couldn't kick due to wounded legs or riding.

This changes struct flags so increments EDITLEVEL again.

This includes pull requests #750 from entrez and #751 from FIQ but was
entered from scratch rather than using use their commits.

Closes #750
Closes #751
This commit is contained in:
PatR
2022-05-04 19:13:28 -07:00
parent d1b14e08c4
commit 44d5be6eb4
9 changed files with 282 additions and 31 deletions

View File

@@ -350,18 +350,19 @@ DISABLE_WARNING_FORMAT_NONLITERAL
/* player is applying a key, lock pick, or credit card */
int
pick_lock(struct obj *pick,
xchar rx, xchar ry, /* coordinates of doors/container,
for autounlock: does not prompt
for direction if these are set */
struct obj *container) /* container, for autounlock */
pick_lock(
struct obj *pick,
xchar rx, xchar ry, /* coordinates of door/container, for autounlock:
* does not prompt for direction if these are set */
struct obj *container) /* container, for autounlock */
{
int picktyp, c, ch;
coord cc;
struct rm *door;
struct obj *otmp;
char qbuf[QBUFSZ];
boolean autounlock = (rx != 0 && ry != 0) || (container != NULL);
boolean autounlock = (((rx != 0 && ry != 0) || container != NULL)
&& (flags.autounlock & AUTOUNLOCK_APPLY_KEY) != 0);
picktyp = pick->otyp;
@@ -422,7 +423,7 @@ pick_lock(struct obj *pick,
boolean it;
int count;
if (u.dz < 0) {
if (u.dz < 0 && !autounlock) { /* beware stale u.dz value */
There("isn't any sort of lock up %s.",
Levitation ? "here" : "there");
return PICKLOCK_LEARNED_SOMETHING;
@@ -436,10 +437,12 @@ pick_lock(struct obj *pick,
count = 0;
c = 'n'; /* in case there are no boxes here */
for (otmp = g.level.objects[cc.x][cc.y]; otmp; otmp = otmp->nexthere)
/* autounlock on boxes: only the one that just informed you it was
* locked. Don't include any other boxes which might be here. */
if ((!autounlock && Is_box(otmp)) || (otmp == container)) {
for (otmp = g.level.objects[cc.x][cc.y]; otmp; otmp = otmp->nexthere) {
/* autounlock on boxes: only the one that was just discovered to
be locked; don't include any other boxes which might be here */
if (autounlock && otmp != container)
continue;
if (Is_box(otmp)) {
++count;
if (!can_reach_floor(TRUE)) {
You_cant("reach %s from up here.", the(xname(otmp)));
@@ -508,6 +511,7 @@ pick_lock(struct obj *pick,
g.xlock.door = 0;
break;
}
}
if (c != 'y') {
if (!count)
There("doesn't seem to be any sort of lock here.");
@@ -626,6 +630,11 @@ doforce(void)
register int c, picktyp;
char qbuf[QBUFSZ];
/*
* TODO?
* allow force with edged weapon to be performed on doors.
*/
if (u.uswallow) {
You_cant("force anything from inside here.");
return ECMD_OK;
@@ -780,7 +789,6 @@ doopen_indir(int x, int y)
if (!(door->doormask & D_CLOSED)) {
const char *mesg;
boolean locked = FALSE;
struct obj* unlocktool;
switch (door->doormask) {
case D_BROKEN:
@@ -798,11 +806,17 @@ doopen_indir(int x, int y)
break;
}
pline("This door%s.", mesg);
if (locked) {
if (flags.autounlock && (unlocktool = autokey(TRUE)) != 0) {
if (locked && flags.autounlock) {
struct obj *unlocktool;
u.dz = 0; /* should already be 0 since hero moved toward door */
if ((flags.autounlock & AUTOUNLOCK_APPLY_KEY) != 0
&& (unlocktool = autokey(TRUE)) != 0) {
res = pick_lock(unlocktool, cc.x, cc.y,
(struct obj *) 0) ? ECMD_TIME : ECMD_OK;
} else if (!u.usteed && ynq("Kick it?") == 'y') {
} else if (!u.usteed
&& (flags.autounlock & AUTOUNLOCK_KICK) != 0
&& ynq("Kick it?") == 'y') {
cmdq_add_ec(dokick);
cmdq_add_dir(sgn(cc.x - u.ux), sgn(cc.y - u.uy), 0);
res = ECMD_TIME;

View File

@@ -190,6 +190,14 @@ static NEARDATA const char *msgwind[][3] = { /* 'msg_window' settings */
" most recent first]" }
};
#endif
/* autounlock settings */
static NEARDATA const char *unlocktypes[][2] = {
{ "none", "" },
{ "untrap", "(might fail)" },
{ "apply-key", "" },
{ "kick", "(doors only)" },
{ "force", "(chests/boxes only)" },
};
static NEARDATA const char *burdentype[] = {
"unencumbered", "burdened", "stressed",
"strained", "overtaxed", "overloaded"
@@ -297,6 +305,7 @@ static int count_apes(void);
static int count_cond(void);
static int handler_align_misc(int);
static int handler_autounlock(int);
static int handler_disclose(void);
static int handler_menu_headings(void);
static int handler_menustyle(void);
@@ -712,6 +721,107 @@ optfn_altkeyhandling(
return optn_ok;
}
static int
optfn_autounlock(
int optidx,
int req,
boolean negated,
char *opts,
char *op)
{
if (req == do_init) {
flags.autounlock = AUTOUNLOCK_APPLY_KEY;
return optn_ok;
}
if (req == do_set) {
/* autounlock:none or autounlock:untrap+apply-key+kick+force;
autounlock without a value is same as autounlock:apply-key and
!autounlock is same as autounlock:none; multiple values can be
space separated or plus-sign separated but the same separation
must be used for each element, not mix&match */
char sep, *nxt;
unsigned newflags;
int i;
if ((op = string_for_opt(opts, TRUE)) == empty_optstr) {
flags.autounlock = negated ? 0 : AUTOUNLOCK_APPLY_KEY;
return optn_ok;
}
newflags = 0;
sep = index(op, '+') ? '+' : ' ';
while (op) {
op = trimspaces(op); /* might have leading space */
if ((nxt = index(op, sep)) != '\0') {
*nxt++ = '\0';
op = trimspaces(op); /* might have trailing space after
* plus sign removal */
}
for (i = 0; i < SIZE(unlocktypes); ++i)
if (!strncmpi(op, unlocktypes[i][0], Strlen(op))
/* fuzzymatch() doesn't match leading substrings but
this allows "apply_key" and "applykey" to match
"apply-key"; "apply key" too if part of foo+bar */
|| fuzzymatch(op, unlocktypes[i][0], " -_", TRUE)) {
switch (*op) {
case 'n':
negated = TRUE;
break;
case 'u':
newflags |= AUTOUNLOCK_UNTRAP;
break;
case 'a':
newflags |= AUTOUNLOCK_APPLY_KEY;
break;
case 'k':
newflags |= AUTOUNLOCK_KICK;
break;
case 'f':
newflags |= AUTOUNLOCK_FORCE;
break;
default:
config_error_add("Invalid value for \"%s\": \"%s\"",
allopt[optidx].name, op);
return optn_silenterr;
}
}
op = nxt;
}
if (negated && newflags != 0) {
config_error_add(
"Invalid value combination for \"%s\": 'none' with some",
allopt[optidx].name);
return optn_silenterr;
}
flags.autounlock = newflags;
return optn_ok;
}
if (req == get_val) {
if (!opts)
return optn_err;
if (!flags.autounlock) {
Strcpy(opts, "none");
} else {
static const char plus[] = " + ";
const char *p = "";
*opts = '\0';
if (flags.autounlock & AUTOUNLOCK_UNTRAP)
Sprintf(eos(opts), "%s%s", p, unlocktypes[1][0]), p = plus;
if (flags.autounlock & AUTOUNLOCK_APPLY_KEY)
Sprintf(eos(opts), "%s%s", p, unlocktypes[2][0]), p = plus;
if (flags.autounlock & AUTOUNLOCK_KICK)
Sprintf(eos(opts), "%s%s", p, unlocktypes[3][0]), p = plus;
if (flags.autounlock & AUTOUNLOCK_FORCE)
Sprintf(eos(opts), "%s%s", p, unlocktypes[4][0]); /*no more p*/
}
return optn_ok;
}
if (req == do_handler) {
return handler_autounlock(optidx);
}
return optn_ok;
}
static int
optfn_boulder(int optidx UNUSED, int req, boolean negated UNUSED,
char *opts, char *op UNUSED)
@@ -4408,6 +4518,75 @@ handler_align_misc(int optidx)
return optn_ok;
}
static int
handler_autounlock(int optidx)
{
winid tmpwin;
anything any;
boolean chngd;
unsigned oldflags = flags.autounlock;
const char *optname = allopt[optidx].name;
char buf[BUFSZ], sep = iflags.menu_tab_sep ? '\t' : ' ';
menu_item *window_pick = (menu_item *) 0;
int i, n, presel, res = optn_ok;
tmpwin = create_nhwindow(NHW_MENU);
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)));
any.a_int = i + 1;
add_menu(tmpwin, &nul_glyphinfo, &any, *unlocktypes[i][0], 0,
ATR_NONE, buf,
((presel ? MENU_ITEMFLAGS_SELECTED : MENU_ITEMFLAGS_NONE)
| (!i ? MENU_ITEMFLAGS_SKIPINVERT : 0)));
}
Sprintf(buf, "Select '%.20s' actions:", optname);
end_menu(tmpwin, buf);
n = select_menu(tmpwin, PICK_ANY, &window_pick);
if (n > 0) {
int k;
boolean wasnone = !flags.autounlock;
unsigned newflags = 0, noflags = 0;
for (i = 0; i < n; ++i) {
k = window_pick[i].item.a_int - 1;
if (k)
newflags |= (1 << (k - 1));
else
noflags = 1;
}
/* wasnone: 'none' is preselected;
!wasnone: don't force it to be unselected */
if (newflags && noflags && !wasnone) {
config_error_add(
"Invalid value combination for \"%s\": 'none' with some",
optname);
res = optn_silenterr;
} else {
flags.autounlock = newflags;
}
free((genericptr_t) window_pick);
} else if (n == 0) { /* nothing was picked but menu wasn't cancelled */
/* something that was preselected got unselected, leaving nothing;
treat that as picking 'none' (even though 'none' might be what
got unselected) */
flags.autounlock = 0;
}
destroy_nhwindow(tmpwin);
chngd = (flags.autounlock != oldflags);
if (chngd || flags.verbose) {
optfn_autounlock(optidx, get_val, FALSE, buf, (char *) NULL);
pline("'%s' %s '%s'.", optname,
chngd ? "changed to" : "is still", buf);
}
return res;
}
static int
handler_disclose(void)
{

View File

@@ -1872,8 +1872,6 @@ do_loot_cont(struct obj **cobjp,
if (!cobj)
return ECMD_OK;
if (cobj->olocked) {
struct obj *unlocktool;
if (ccount < 2 && (g.level.objects[cobj->ox][cobj->oy] == cobj))
pline("%s locked.",
cobj->lknown ? "It is" : "Hmmm, it turns out to be");
@@ -1884,10 +1882,18 @@ do_loot_cont(struct obj **cobjp,
cobj->lknown = 1;
if (flags.autounlock) {
if ((unlocktool = autokey(TRUE)) != 0) {
struct obj *unlocktool;
/* 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) {
/* pass ox and oy to avoid direction prompt */
return (pick_lock(unlocktool, cobj->ox, cobj->oy, cobj) != 0);
} else if (ccount == 1 && u_have_forceable_weapon()) {
} else if ((flags.autounlock & AUTOUNLOCK_FORCE) != 0
&& 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;