Tutorial level

Add a tutorial level to teach commands to new players.
Very much a WIP.

Breaks save and bones compat.
This commit is contained in:
Pasi Kallinen
2023-02-25 20:37:01 +02:00
parent 7aead98a49
commit fc7a32b86e
38 changed files with 646 additions and 27 deletions

View File

@@ -14,6 +14,7 @@
static void moveloop_preamble(boolean);
static void u_calc_moveamt(int);
static void maybe_do_tutorial(void);
#ifdef POSITIONBAR
static void do_positionbar(void);
#endif
@@ -303,7 +304,7 @@ moveloop_core(void)
}
}
if (Searching && gm.multi >= 0)
if (!gl.level.flags.noautosearch && Searching && gm.multi >= 0)
(void) dosearch0(1);
if (Warning)
warnreveal();
@@ -498,12 +499,41 @@ moveloop_core(void)
/* [should this be flush_screen() instead?] */
display_nhwindow(WIN_MAP, FALSE);
}
if (gl.luacore && nhcb_counts[NHCB_END_TURN]) {
lua_getglobal(gl.luacore, "nh_callback_run");
lua_pushstring(gl.luacore, nhcb_name[NHCB_END_TURN]);
nhl_pcall(gl.luacore, 1, 0);
}
}
static void
maybe_do_tutorial(void)
{
s_level *sp = find_level("tut-1");
if (!sp)
return;
if (ask_do_tutorial()) {
assign_level(&u.ucamefrom, &u.uz);
u.nofollowers = TRUE;
schedule_goto(&sp->dlevel, UTOTYPE_NONE, (char *) 0, (char *) 0);
deferred_goto();
vision_recalc(0);
docrt();
u.nofollowers = FALSE;
}
}
void
moveloop(boolean resuming)
{
moveloop_preamble(resuming);
if (!resuming)
maybe_do_tutorial();
for (;;) {
moveloop_core();
}

View File

@@ -482,6 +482,15 @@ can_do_extcmd(const struct ext_func_tab *extcmd)
{
int ecflags = extcmd->flags;
if (gl.luacore && nhcb_counts[NHCB_CMD_BEFORE]) {
lua_getglobal(gl.luacore, "nh_callback_run");
lua_pushstring(gl.luacore, nhcb_name[NHCB_CMD_BEFORE]);
lua_pushstring(gl.luacore, extcmd->ef_txt);
nhl_pcall(gl.luacore, 2, 1);
if (!lua_toboolean(gl.luacore, -1))
return FALSE;
}
if (!wizard && (ecflags & WIZMODECMD)) {
pline(unavailcmd, extcmd->ef_txt);
return FALSE;
@@ -3607,6 +3616,29 @@ cmd_from_func(int (*fn)(void))
return '\0';
}
/* return visual interpretation of the key bound to extended command,
or the ext cmd name if not bound to any key. */
char *
cmd_from_ecname(const char *ecname)
{
static char cmdnamebuf[QBUFSZ];
const struct ext_func_tab *extcmd;
for (extcmd = extcmdlist; extcmd->ef_txt; ++extcmd)
if (!strcmp(extcmd->ef_txt, ecname)) {
char key = cmd_from_func(extcmd->ef_funct);
if (key)
Sprintf(cmdnamebuf, "%s", visctrl(key));
else
Sprintf(cmdnamebuf, "#%s", ecname);
return cmdnamebuf;
}
cmdnamebuf[0] = '\0';
return cmdnamebuf;
}
static const char *
ecname_from_fn(int (*fn)(void))
{

View File

@@ -45,6 +45,14 @@ const int shield_static[SHIELD_COUNT] = {
S_ss1, S_ss2, S_ss3, S_ss2, S_ss1, S_ss2, S_ss4,
};
const char * const nhcb_name[NUM_NHCB] = {
"cmd_before",
"level_enter",
"level_leave",
"end_turn",
};
int nhcb_counts[NUM_NHCB] = DUMMY;
NEARDATA const struct c_color_names c_color_names = {
"black", "amber", "golden", "light blue", "red", "green",

View File

@@ -1481,6 +1481,12 @@ goto_level(
if (on_level(newlevel, &u.uz))
return; /* this can happen */
if (gl.luacore && nhcb_counts[NHCB_LVL_LEAVE]) {
lua_getglobal(gl.luacore, "nh_callback_run");
lua_pushstring(gl.luacore, nhcb_name[NHCB_LVL_LEAVE]);
nhl_pcall(gl.luacore, 1, 0);
}
/* tethered movement makes level change while trapped feasible */
if (u.utrap && u.utraptype == TT_BURIEDBALL)
buried_ball_to_punishment(); /* (before we save/leave old level) */
@@ -1511,7 +1517,8 @@ goto_level(
set_ustuck((struct monst *) 0); /* clear u.ustuck and u.uswallow */
set_uinwater(0); /* u.uinwater = 0 */
u.uundetected = 0; /* not hidden, even if means are available */
keepdogs(FALSE);
if (!u.nofollowers)
keepdogs(FALSE);
recalc_mapseen(); /* recalculate map overview before we leave the level */
/*
* We no longer see anything on the level. Make sure that this
@@ -1611,6 +1618,7 @@ goto_level(
if (portal && !In_endgame(&u.uz)) {
/* find the portal on the new level */
register struct trap *ttrap;
struct stairway *stway;
for (ttrap = gf.ftrap; ttrap; ttrap = ttrap->ntrap)
if (ttrap->ttyp == MAGIC_PORTAL)
@@ -1623,6 +1631,9 @@ goto_level(
after already getting expelled once. The portal back
doesn't exist anymore - see expulsion(). */
u_on_rndspot(0);
} else if ((stway = stairway_find_dir(TRUE)) != 0) {
/* returning from tutorial via portal */
u_on_newpos(stway->sx, stway->sy);
} else {
panic("goto_level: no corresponding portal!");
}

View File

@@ -714,9 +714,10 @@ get_dgn_flags(lua_State *L)
{
int dgn_flags = 0;
static const char *const flagstrs[] = {
"town", "hellish", "mazelike", "roguelike", NULL
"town", "hellish", "mazelike", "roguelike", "unconnected", NULL
};
static const int flagstrs2i[] = { TOWN, HELLISH, MAZELIKE, ROGUELIKE, 0 };
static const int flagstrs2i[] = { TOWN, HELLISH, MAZELIKE, ROGUELIKE,
UNCONNECTED, 0 };
lua_getfield(L, -1, "flags");
if (lua_type(L, -1) == LUA_TTABLE) {
@@ -1040,6 +1041,7 @@ init_dungeons(void)
gd.dungeons[i].flags.maze_like = !!(dgn_flags & MAZELIKE);
gd.dungeons[i].flags.rogue_like = !!(dgn_flags & ROGUELIKE);
gd.dungeons[i].flags.align = dgn_align;
gd.dungeons[i].flags.unconnected = !!(dgn_flags & UNCONNECTED);
/*
* Set the entry level for this dungeon. The entry value means:
@@ -1063,7 +1065,9 @@ init_dungeons(void)
gd.dungeons[i].entry_lev = 1; /* defaults to top level */
}
if (i) { /* set depth */
if (gd.dungeons[i].flags.unconnected) {
gd.dungeons[i].depth_start = 1;
} else if (i) { /* set depth */
branch *br;
schar from_depth;
boolean from_up;

View File

@@ -296,8 +296,8 @@ wipe_engr_at(coordxy x, coordxy y, xint16 cnt, boolean magical)
{
register struct engr *ep = engr_at(x, y);
/* Headstones are indelible */
if (ep && ep->engr_type != HEADSTONE) {
/* Headstones and some specially marked engravings are indelible */
if (ep && ep->engr_type != HEADSTONE && !ep->nowipeout) {
debugpline1("asked to erode %d characters", cnt);
if (ep->engr_type != BURN || is_ice(x, y) || (magical && !rn2(2))) {
if (ep->engr_type != DUST && ep->engr_type != ENGR_BLOOD) {

View File

@@ -1141,7 +1141,7 @@ makemon(
fakemon = cg.zeromonst;
cc.x = cc.y = 0;
if (iflags.debug_mongen)
if (iflags.debug_mongen || (!gl.level.flags.rndmongen && !ptr))
return (struct monst *) 0;
/* if caller wants random location, do it here */

View File

@@ -1016,6 +1016,12 @@ makelevel(void)
for (i = 0; i < gn.nroom; ++i) {
fill_special_room(&gr.rooms[i]);
}
if (gl.luacore && nhcb_counts[NHCB_LVL_ENTER]) {
lua_getglobal(gl.luacore, "nh_callback_run");
lua_pushstring(gl.luacore, nhcb_name[NHCB_LVL_ENTER]);
nhl_pcall(gl.luacore, 1, 0);
}
}
/*
@@ -1518,6 +1524,9 @@ mktrap(
(void) makemon(&mons[PM_GIANT_SPIDER], m.x, m.y, NO_MM_FLAGS);
if (t && (mktrapflags & MKTRAP_SEEN))
t->tseen = TRUE;
if (kind == MAGIC_PORTAL && (u.ucamefrom.dnum || u.ucamefrom.dlevel)) {
assign_level(&t->dst, &u.ucamefrom);
}
/* The hero isn't the only person who's entered the dungeon in
search of treasure. On the very shallowest levels, there's a

View File

@@ -34,6 +34,7 @@ static void pacify_guard(struct monst *);
#define LEVEL_SPECIFIC_NOCORPSE(mdat) \
(Is_rogue_level(&u.uz) \
|| !gl.level.flags.deathdrops \
|| (gl.level.flags.graveyard && is_undead(mdat) && rn2(3)))
/* A specific combination of x_monnam flags for livelogging. The livelog

View File

@@ -36,6 +36,9 @@ static int nhl_timer_has_at(lua_State *);
static int nhl_timer_peek_at(lua_State *);
static int nhl_timer_stop_at(lua_State *);
static int nhl_timer_start_at(lua_State *);
static int nhl_get_cmd_key(lua_State *);
static int nhl_callback(lua_State *);
static int nhl_gamestate(lua_State *);
static int nhl_test(lua_State *);
static int nhl_getmap(lua_State *);
static char splev_typ2chr(schar);
@@ -1474,6 +1477,140 @@ nhl_timer_start_at(lua_State *L)
return 0;
}
/* returns the visual interpretation of the key bound to an extended command,
or the ext cmd name if not bound to any key */
/* local helpkey = eckey("help"); */
static int
nhl_get_cmd_key(lua_State *L)
{
int argc = lua_gettop(L);
if (argc == 1) {
const char *cmd = luaL_checkstring(L, 1);
char *key = cmd_from_ecname(cmd);
lua_pushstring(L, key);
return 1;
}
return 0;
}
/* add or remove a lua function callback */
/* callback("level_enter", "function_name"); */
/* callback("level_enter", "function_name", true); */
/* level_enter, level_leave, cmd_before */
static int
nhl_callback(lua_State *L)
{
int argc = lua_gettop(L);
int i;
if (argc == 2) {
const char *fn = luaL_checkstring(L, -1);
const char *cb = luaL_checkstring(L, -2);
if (!gl.luacore) {
nhl_error(L, "nh luacore not inited");
/*NOTREACHED*/
return 0;
}
for (i = 0; i < NUM_NHCB; i++)
if (!strcmp(cb, nhcb_name[i]))
break;
if (i >= NUM_NHCB)
return 0;
nhcb_counts[i]++;
lua_getglobal(gl.luacore, "nh_callback_set");
lua_pushstring(gl.luacore, cb);
lua_pushstring(gl.luacore, fn);
nhl_pcall(gl.luacore, 2, 0);
} else if (argc == 3) {
boolean rm = lua_toboolean(L, -1);
const char *fn = luaL_checkstring(L, -2);
const char *cb = luaL_checkstring(L, -3);
if (!gl.luacore) {
nhl_error(L, "nh luacore not inited");
/*NOTREACHED*/
return 0;
}
for (i = 0; i < NUM_NHCB; i++)
if (!strcmp(cb, nhcb_name[i]))
break;
if (i >= NUM_NHCB)
return 0;
if (rm) {
nhcb_counts[i]--;
if (nhcb_counts[i] < 0)
impossible("nh.callback counts are wrong");
} else {
nhcb_counts[i]++;
}
lua_getglobal(gl.luacore, rm ? "nh_callback_rm" : "nh_callback_set");
lua_pushstring(gl.luacore, cb);
lua_pushstring(gl.luacore, fn);
nhl_pcall(gl.luacore, 2, 0);
}
return 0;
}
/* store or restore game state */
/* NOTE: doesn't work when saving/restoring the game */
/* currently handles inventory and turns. */
/* gamestate(); -- save state */
/* gamestate(true); -- restore state */
static int
nhl_gamestate(lua_State *L)
{
int argc = lua_gettop(L);
boolean reststate = argc > 0 ? lua_toboolean(L, -1) : FALSE;
static struct obj *invent = NULL;
static long moves = 0;
static boolean stored = FALSE;
if (reststate && stored) {
/* restore game state */
gm.moves = moves;
while (gi.invent)
useupall(gi.invent);
while (invent) {
struct obj *otmp = invent;
long wornmask = otmp->owornmask;
otmp->owornmask = 0L;
extract_nobj(otmp, &invent);
addinv(otmp);
if (wornmask)
setworn(otmp, wornmask);
}
stored = FALSE;
} else {
/* store game state */
while (gi.invent) {
struct obj *otmp = gi.invent;
long wornmask = otmp->owornmask;
setnotworn(otmp);
freeinv(otmp);
otmp->nobj = invent;
otmp->owornmask = wornmask;
invent = otmp;
}
moves = gm.moves;
stored = TRUE;
}
update_inventory();
return 0;
}
RESTORE_WARNING_UNREACHABLE_CODE
static const struct luaL_Reg nhl_functions[] = {
@@ -1499,6 +1636,9 @@ static const struct luaL_Reg nhl_functions[] = {
{"menu", nhl_menu},
{"text", nhl_text},
{"getlin", nhl_getlin},
{"eckey", nhl_get_cmd_key},
{"callback", nhl_callback},
{"gamestate", nhl_gamestate},
{"makeplural", nhl_makeplural},
{"makesingular", nhl_makesingular},

View File

@@ -368,6 +368,52 @@ extern int curses_read_attrs(const char *attrs);
extern char *curses_fmt_attrs(char *);
#endif
/* ask user if they want a tutorial, except if tutorial boolean option has been
set in config - either on or off - in which case just obey that setting
without asking. */
boolean
ask_do_tutorial(void)
{
boolean dotut = flags.tutorial;
if (!opt_set_in_config[opt_tutorial]) {
winid win;
menu_item *sel;
anything any;
int n;
do {
win = create_nhwindow(NHW_MENU);
start_menu(win, MENU_BEHAVE_STANDARD);
any = cg.zeroany;
any.a_char = 'y';
add_menu(win, &nul_glyphinfo, &any, any.a_char, 0,
ATR_NONE, 0, "Yes, do a tutorial", MENU_ITEMFLAGS_NONE);
any.a_char = 'n';
add_menu(win, &nul_glyphinfo, &any, any.a_char, 0,
ATR_NONE, 0, "No", MENU_ITEMFLAGS_NONE);
any = cg.zeroany;
add_menu(win, &nul_glyphinfo, &any, 0, 0,
ATR_NONE, 0, "", MENU_ITEMFLAGS_NONE);
add_menu(win, &nul_glyphinfo, &any, 0, 0,
ATR_NONE, 0, "", MENU_ITEMFLAGS_NONE);
add_menu(win, &nul_glyphinfo, &any, 0, 0,
ATR_NONE, 0, "Put \"OPTIONS=notutorial\" in the config file to skip this query.", MENU_ITEMFLAGS_NONE);
end_menu(win, "Do you want a tutorial?");
n = select_menu(win, PICK_ONE, &sel);
destroy_nhwindow(win);
} while (n <= 0 && !gp.program_state.done_hup);
if (n > 0) {
dotut = (sel[0].item.a_char == 'y');
free((genericptr_t) sel);
}
}
return dotut;
}
/*
**********************************
*

View File

@@ -3712,6 +3712,12 @@ lspo_level_flags(lua_State *L)
gl.level.flags.temperature = 1;
else if (!strcmpi(s, "cold"))
gl.level.flags.temperature = -1;
else if (!strcmpi(s, "nomongen"))
gl.level.flags.rndmongen = 0;
else if (!strcmpi(s, "nodeathdrops"))
gl.level.flags.deathdrops = 0;
else if (!strcmpi(s, "noautosearch"))
gl.level.flags.noautosearch = 1;
else {
char buf[BUFSZ];
Sprintf(buf, "Unknown level flag %s", s);
@@ -3783,6 +3789,9 @@ lspo_engraving(lua_State *L)
long ecoord;
coordxy x = -1, y = -1;
int argc = lua_gettop(L);
boolean guardobjs = FALSE;
boolean wipeout = TRUE;
struct engr *ep;
create_des_coder();
@@ -3795,6 +3804,8 @@ lspo_engraving(lua_State *L)
y = ey;
etyp = engrtypes2i[get_table_option(L, "type", "engrave", engrtypes)];
txt = get_table_str(L, "text");
wipeout = get_table_boolean_opt(L, "degrade", TRUE);
guardobjs = get_table_boolean_opt(L, "guardobjects", FALSE);
} else if (argc == 3) {
lua_Integer ex, ey;
(void) get_coord(L, 1, &ex, &ey);
@@ -3814,6 +3825,11 @@ lspo_engraving(lua_State *L)
get_location_coord(&x, &y, DRY, gc.coder->croom, ecoord);
make_engr_at(x, y, txt, 0L, etyp);
Free(txt);
ep = engr_at(x, y);
if (ep) {
ep->guardobjects = guardobjs;
ep->nowipeout = !wipeout;
}
return 0;
}
@@ -6826,6 +6842,8 @@ sp_level_coder_init(void)
gl.level.flags.is_maze_lev = 0;
gl.level.flags.temperature = In_hell(&u.uz) ? 1 : 0;
gl.level.flags.rndmongen = 1;
gl.level.flags.deathdrops = 1;
reset_xystart_size();

View File

@@ -1128,7 +1128,8 @@ void
domagicportal(struct trap *ttmp)
{
struct d_level target_level;
boolean already_stunned;
s_level *tutlvl = find_level("tut-1");
const char *stunmsg = (char *) 0;
if (u.utrap && u.utraptype == TT_BURIEDBALL)
buried_ball_to_punishment();
@@ -1154,13 +1155,16 @@ domagicportal(struct trap *ttmp)
return;
}
already_stunned = !!Stunned;
make_stunned((HStun & TIMEOUT) + 3L, FALSE);
target_level = ttmp->dst;
schedule_goto(&target_level, UTOTYPE_PORTAL,
!already_stunned ? "You feel slightly dizzy."
: "You feel dizzier.",
(char *) 0);
/* coming back from tutorial doesn't trigger stunning */
if (!(tutlvl && tutlvl->dlevel.dnum == u.uz.dnum)) {
stunmsg = !Stunned ? "You feel slightly dizzy."
: "You feel dizzier.";
make_stunned((HStun & TIMEOUT) + 3L, FALSE);
}
schedule_goto(&target_level, UTOTYPE_PORTAL, stunmsg, (char *) 0);
}
void