From: Pasi Kallinen Date: Sat, 25 Feb 2023 18:37:01 +0000 (+0200) Subject: Tutorial level X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=fc7a32b86e6e755b1e1c8515ce47e9c852c32664;p=nethack Tutorial level Add a tutorial level to teach commands to new players. Very much a WIP. Breaks save and bones compat. --- diff --git a/dat/dungeon.lua b/dat/dungeon.lua index c4371e5cb..24bd27dbf 100644 --- a/dat/dungeon.lua +++ b/dat/dungeon.lua @@ -315,4 +315,15 @@ dungeon = { }, } }, + { + name = "The Tutorial", + base = 1, + flags = { "mazelike", "unconnected" }, + levels = { + { + name = "tut-1", + base = 1, + }, + } + }, } diff --git a/dat/nhcore.lua b/dat/nhcore.lua index c3887c3e8..5b188efb9 100644 --- a/dat/nhcore.lua +++ b/dat/nhcore.lua @@ -14,6 +14,45 @@ function get_variables_string() return "nh_lua_variables=" .. table_stringify(nh_lua_variables) .. ";"; end +function nh_callback_set(cb, fn) + local cbname = "_CB_" .. cb; + + -- pline("callback_set(%s,%s)", cb, fn); + + if (type(nh_lua_variables[cbname]) ~= "table") then + nh_lua_variables[cbname] = {}; + end + nh_lua_variables[cbname][fn] = true; +end + +function nh_callback_rm(cb, fn) + local cbname = "_CB_" .. cb; + + -- pline("callback_RM(%s,%s)", cb, fn); + + if (type(nh_lua_variables[cbname]) ~= "table") then + nh_lua_variables[cbname] = {}; + end + nh_lua_variables[cbname][fn] = nil; +end + +function nh_callback_run(cb, ...) + local cbname = "_CB_" .. cb; + + -- pline("callback_run(%s)", cb); + -- pline("TYPE:%s", type(nh_lua_variables[cbname])); + + if (type(nh_lua_variables[cbname]) ~= "table") then + nh_lua_variables[cbname] = {}; + end + for k, v in pairs(nh_lua_variables[cbname]) do + if (not _G[k](table.unpack{...})) then + return false; + end + end + return true; +end + -- This is an example of generating an external file during gameplay, -- which is updated periodically. -- Intended for public servers using dgamelaunch as their login manager. diff --git a/dat/nhlib.lua b/dat/nhlib.lua index fc05c6c8d..849188e7f 100644 --- a/dat/nhlib.lua +++ b/dat/nhlib.lua @@ -174,3 +174,55 @@ function table_stringify(tbl) -- pline("table_stringify:(%s)", str); return "{" .. str .. "}"; end + +-- +-- TUTORIAL +-- + +-- extended commands available in tutorial +local tutorial_whitelist_commands = { + ["movesouth"] = true, + ["movenorth"] = true, + ["moveeast"] = true, + ["movewest"] = true, + ["movesoutheast"] = true, + ["movenorthwest"] = true, + ["movenortheast"] = true, + ["movesouthwest"] = true, + ["kick"] = true, + ["search"] = true, + ["pickup"] = true, + ["wear"] = true, + ["wield"] = true, + -- ["save"] = true, +}; + +function tutorial_cmd_before(cmd) + -- nh.pline("TUT:cmd_before:" .. cmd); + + if (tutorial_whitelist_commands[cmd]) then + return true; + else + return false; + end +end + +function tutorial_enter() + -- nh.pline("TUT:enter"); + nh.gamestate(); +end + +function tutorial_leave() + -- nh.pline("TUT:leave"); + + -- remove the tutorial level callbacks + nh.callback("cmd_before", "tutorial_cmd_before", true); + nh.callback("level_enter", "tutorial_enter", true); + nh.callback("level_leave", "tutorial_leave", true); + nh.callback("end_turn", "tutorial_turn", true); + nh.gamestate(true); +end + +function tutorial_turn() + -- nh.pline("TUT:turn"); +end diff --git a/dat/tut-1.lua b/dat/tut-1.lua new file mode 100644 index 000000000..85f792953 --- /dev/null +++ b/dat/tut-1.lua @@ -0,0 +1,143 @@ + +des.level_init({ style = "solidfill", fg = " " }); +des.level_flags("mazelevel", "noflip", + "nomongen", "nodeathdrops", "noautosearch"); + +des.map([[ +--------------------------------------------------------------------------- +|-.--|.......|....|.......................................................| +|.-..........|....+.......................................................| +||.--|.......|....|.......................................................| +||.|.|.......|....|.......................................................| +||.|.|.......|....|.......................................................| +|-+-S-------------|.......................................................| +|......| |.......................................................| +|......| ###### |.......................................................| +|----.-| -+- # |.......................................................| +|----+----.----+---.......................................................| +|........|.|......|.......................................................| +|.P......-S|......|------.................................................| +|..........|......+.|...|.................................................| +|.W......---......|.|.|.|.................................................| +|....Z.L.|.F......|.|.|.|+---.............................................| +|........|--......|...|.....|.............................................| +--------------------------------------------------------------------------- +]]); + + +des.region(selection.area(01,01, 73, 16), "lit"); + +des.non_diggable(); + +des.teleport_region({ region = { 9,3, 9,3 } }); + +-- TODO: +-- - save hero state when entering, restore hero state when leaving +-- - quit-command should maybe exit the tutorial? + +-- turn on some newbie-friendly options +nh.parse_config("OPTIONS=mention_walls"); +nh.parse_config("OPTIONS=autoopen"); +nh.parse_config("OPTIONS=lit_corridor"); + +local movekeys = nh.eckey("movewest") .. " " .. + nh.eckey("movesouth") .. " " .. + nh.eckey("movenorth") .. " " .. + nh.eckey("moveeast"); + +local diagmovekeys = nh.eckey("movesouthwest") .. " " .. + nh.eckey("movenortheast") .. " " .. + nh.eckey("movesoutheast") .. " " .. + nh.eckey("movenorthwest"); + +des.engraving({ coord = { 9,3 }, type = "engrave", text = "Move around with " .. movekeys, degrade = false }); +des.engraving({ coord = { 5,2 }, type = "engrave", text = "Move diagonally with " .. diagmovekeys, degrade = false }); + +-- + +des.engraving({ coord = { 2,5 }, type = "engrave", text = "Open the door by moving into it", degrade = false }); +des.door({ coord = { 2,6 }, state = "closed" }); + +-- + +des.engraving({ coord = { 4,5 }, type = "engrave", text = "You can leave the tutorial via the magic portal.", degrade = false }); +des.trap({ type = "magic portal", coord = { 4,4 }, seen = true }); + +-- + +des.engraving({ coord = { 5,9 }, type = "engrave", text = "This door is locked. Kick it with " .. nh.eckey("kick"), degrade = false }); +des.door({ coord = { 5,10 }, state = "locked" }); + +-- + +des.engraving({ coord = { 10,13 }, type = "engrave", text = "Use " .. nh.eckey("search") .. " to search for secret doors", degrade = false }); + +-- + +des.engraving({ coord = { 10,10 }, type = "engrave", text = "Behind this door is a dark corridor", degrade = false }); +des.door({ coord = { 10,9 }, state = percent(50) and "locked" or "closed" }); +des.region(selection.match("#"), "unlit"); +des.region(selection.match(" "), "unlit"); +des.door({ coord = { 15,10 }, state = percent(50) and "locked" or "closed" }); + +-- + +des.engraving({ coord = { 15,11 }, type = "engrave", text = "There are four traps next to you! Search for them.", degrade = false }); +local locs = { {14,11}, {14,12}, {15,12}, {16,12}, {16,11} }; +shuffle(locs); +for i = 1, 4 do + des.trap({ type = percent(50) and "sleep gas" or "board", + coord = locs[i], victim = false }); +end + +-- + +des.door({ coord = { 18,13 }, state = "closed" }); + +des.engraving({ coord = { 19,13 }, type = "engrave", text = "Pick up items with '" .. nh.eckey("pickup") .. "'", degrade = false }); + +local armor = (u.role == "Monk") and "leather gloves" or "leather armor"; + +des.object({ id = armor, spe = 0, buc = "not-cursed", coord = { 19,14} }); + +des.engraving({ coord = { 19,15 }, type = "engrave", text = "Wear armor with '" .. nh.eckey("wear") .. "'", degrade = false }); + +des.object({ id = "dagger", spe = 0, buc = "not-cursed", coord = { 21,15} }); + +des.engraving({ coord = { 21,14 }, type = "engrave", text = "Wield weapons with '" .. nh.eckey("wield") .. "'", degrade = false }); + + +des.engraving({ coord = { 22,13 }, type = "engrave", text = "Hit monsters by walking into them.", degrade = false }); + +des.monster({ id = "lichen", coord = { 23,15 }, waiting = true, countbirth = false }); + +-- + +des.door({ coord = { 25,15 }, state = percent(50) and "locked" or "closed" }); + +des.engraving({ coord = { 24,16 }, type = "engrave", text = "Now you know the very basics. You can leave the tutorial via the magic portal.", degrade = false }); + +des.trap({ type = "magic portal", coord = { 27,16 }, seen = true }); + +-- + +des.engraving({ coord = { 25,14 }, type = "burn", text = "UNDER CONSTRUCTION", degrade = false }); + +-- + +des.door({ coord = { 18,2 }, state = percent(50) and "locked" or "closed" }); + +---------------- + +nh.callback("cmd_before", "tutorial_cmd_before"); +nh.callback("level_enter", "tutorial_enter"); +nh.callback("level_leave", "tutorial_leave"); +nh.callback("end_turn", "tutorial_turn"); + +---------------- + +-- temporary stuff here +-- des.trap({ type = "magic portal", coord = { 9,5 }, seen = true }); +-- des.trap({ type = "magic portal", coord = { 9,1 }, seen = true }); +-- des.object({ id = "leather armor", spe = 0, coord = { 9,2} }); + diff --git a/doc/Guidebook.mn b/doc/Guidebook.mn index eaec202cf..c934ced21 100644 --- a/doc/Guidebook.mn +++ b/doc/Guidebook.mn @@ -4546,6 +4546,9 @@ Persistent. ." .lp travel_debug ." Display intended path during each step of travel (default off). ." Debug mode only. +.lp tutorial +Play a tutorial level at the start of the game. +Setting this option on or off in the config file will skip the query. .lp "verbose " Provide more commentary during the game (default on). Persistent. .lp whatis_coord diff --git a/doc/Guidebook.tex b/doc/Guidebook.tex index 198eb94a5..6c29cd0aa 100644 --- a/doc/Guidebook.tex +++ b/doc/Guidebook.tex @@ -4979,6 +4979,10 @@ command. Persistent. % Display intended path during each step of travel (default off). % Debug mode only. %.lp +\item[\ib{tutorial}] +Play a tutorial level at the start of the game. +Setting this option on or off in the config file will skip the query. +%.lp \item[\ib{verbose}] Provide more commentary during the game (default on). Persistent. %.lp diff --git a/doc/fixes3-7-0.txt b/doc/fixes3-7-0.txt index 7f7a6e8ce..1e580e39c 100644 --- a/doc/fixes3-7-0.txt +++ b/doc/fixes3-7-0.txt @@ -1987,6 +1987,7 @@ add items given a Japanese name when playing as a Samurai to discoveries list make music improvisations more varied and interesting, as well as useful give a helpful tip when first entering "farlook" mode add a boolean option tips to disable all of the helpful tips +add a tutorial level Platform- and/or Interface-Specific New Features diff --git a/doc/lua.adoc b/doc/lua.adoc index a2d575eea..67ed82c7e 100644 --- a/doc/lua.adoc +++ b/doc/lua.adoc @@ -33,6 +33,26 @@ Example: local str = nh.an("unicorn"); +=== callback + +Add or remove a lua function to callback list. +First argument is the callback list, second is the name of the lua function to be called. +Two arguments adds the callback, if optional 3rd argument is true, removes the callback. +Cannot add the same function to the same callback list does nothing. + +|=== +| cmd_before | called before an extended command is executed. The command name is given as a parameter. If this function returns false, the command will not execute. +| level_enter | called when hero enters the level for the first time. +| level_leave | called when hero leaves the level. +| end_turn | called after player input is handled. May not be exact turn, if eg. hero is running or otherwise occupied. +|=== + +Example: + + nh.callback("level_enter", "tutorial_enter"); + nh.callback("level_enter", "tutorial_enter", true); + + === debug_flags Set debugging flags. @@ -90,6 +110,16 @@ Example: local filename = nh.dump_fmtstr("/tmp/nethack.%n.%d.log"); +=== eckey + +Return the key bound to an extended command, or the full extended +command name, if it is not bound to any key. + +Example: + + local k = nh.eckey("help"); + + === getlin Asks the player for a text to enter, and returns the entered string. @@ -482,6 +512,8 @@ Example: Create an engraving. * type is one of "dust", "engrave", "burn", "mark", or "blood". +* optional boolean `degrade` defaults to true; engraving can degrade or be wiped out. +* optional boolean `guardobjects` defaults to false (unless making a level and the text is "Elbereth"); are items on the engraving protected from monsters. Example: @@ -579,6 +611,8 @@ Set flags for this level. | hot | Level is hot. Dungeon flag "hellish" automatically sets this. | cold | Level is cold. | temperate | Level is neither hot nor cold. +| nomongen | Prevents random monster generation. +| nodeathdrops | Prevents killed monsters from dropping corpses or random death drops. |=== Example: diff --git a/include/decl.h b/include/decl.h index 4a89d1492..3a5edf54d 100644 --- a/include/decl.h +++ b/include/decl.h @@ -245,6 +245,10 @@ extern const struct class_sym def_monsyms[MAXMCLASSES]; /* current mon class symbols */ extern uchar monsyms[MAXMCLASSES]; +/* lua callback queue names */ +extern const char * const nhcb_name[]; +extern int nhcb_counts[]; + #include "obj.h" extern NEARDATA struct obj *uarm, *uarmc, *uarmh, *uarms, *uarmg, *uarmf, *uarmu, /* under-wear, so to speak */ diff --git a/include/dgn_file.h b/include/dgn_file.h index 84e523a94..106de5655 100644 --- a/include/dgn_file.h +++ b/include/dgn_file.h @@ -53,10 +53,11 @@ struct tmpbranch { /* * Flags that map into the dungeon flags bitfields. */ -#define TOWN 1 /* levels only */ -#define HELLISH 2 -#define MAZELIKE 4 -#define ROGUELIKE 8 +#define TOWN 0x01 /* levels only */ +#define HELLISH 0x02 +#define MAZELIKE 0x04 +#define ROGUELIKE 0x08 +#define UNCONNECTED 0x10 #define D_ALIGN_NONE 0 #define D_ALIGN_CHAOTIC (AM_CHAOTIC << 4) diff --git a/include/dungeon.h b/include/dungeon.h index 5beb89815..e389b3641 100644 --- a/include/dungeon.h +++ b/include/dungeon.h @@ -19,7 +19,7 @@ typedef struct d_flags { /* dungeon/level type flags */ Bitfield(maze_like, 1); /* is this a maze? */ Bitfield(rogue_like, 1); /* is this an old-fashioned presentation? */ Bitfield(align, 3); /* dungeon alignment. */ - Bitfield(unused, 1); /* etc... */ + Bitfield(unconnected, 1); /* dungeon not connected to any branch */ } d_flags; typedef struct s_level { /* special dungeon level element */ diff --git a/include/engrave.h b/include/engrave.h index c09cc5eaa..380f8227a 100644 --- a/include/engrave.h +++ b/include/engrave.h @@ -24,7 +24,8 @@ struct engr { * against monsters when an object is present * even when hero isn't (so behaves similarly * to how Elbereth did in 3.4.3) */ - /* 7 free bits */ + Bitfield(nowipeout, 1); /* this engraving will not degrade */ + /* 6 free bits */ }; #define newengr(lth) \ diff --git a/include/extern.h b/include/extern.h index 3ec710d20..2d141e8f1 100644 --- a/include/extern.h +++ b/include/extern.h @@ -245,6 +245,7 @@ extern int domonability(void); extern const struct ext_func_tab *ext_func_tab_from_func(int(*)(void)); extern char cmd_from_func(int(*)(void)); extern char cmd_from_dir(int, int); +extern char *cmd_from_ecname(const char *); extern const char *cmdname_from_func(int(*)(void), char *, boolean); extern boolean redraw_cmd(char); extern const char *levltyp_to_name(int); @@ -2008,6 +2009,7 @@ extern int shiny_obj(char); /* ### options.c ### */ +extern boolean ask_do_tutorial(void); extern boolean match_optname(const char *, const char *, int, boolean); extern uchar txt2key(char *); extern void initoptions(void); diff --git a/include/flag.h b/include/flag.h index e78a21834..00acc4562 100644 --- a/include/flag.h +++ b/include/flag.h @@ -35,6 +35,7 @@ struct flag { boolean goldX; /* for BUCX filtering, whether gold is X or U */ boolean help; /* look in data file for info about stuff */ boolean tips; /* show helpful hints? */ + boolean tutorial; /* ask if player wants tutorial level? */ boolean ignintr; /* ignore interrupts */ boolean implicit_uncursed; /* maybe omit "uncursed" status in inventory */ boolean ins_chkpt; /* checkpoint as appropriate; INSURANCE */ diff --git a/include/hack.h b/include/hack.h index 4fecb3c1f..c7c33f40d 100644 --- a/include/hack.h +++ b/include/hack.h @@ -524,6 +524,16 @@ enum nhcore_calls { NUM_NHCORE_CALLS }; +/* Lua callbacks. TODO: Merge with NHCORE */ +enum nhcb_calls { + NHCB_CMD_BEFORE = 0, + NHCB_LVL_ENTER, + NHCB_LVL_LEAVE, + NHCB_END_TURN, + + NUM_NHCB +}; + /* Macros for messages referring to hands, eyes, feet, etc... */ enum bodypart_types { ARM = 0, diff --git a/include/optlist.h b/include/optlist.h index ab833e0da..ca59e5b26 100644 --- a/include/optlist.h +++ b/include/optlist.h @@ -628,6 +628,8 @@ static int optfn_##a(int, int, boolean, char *, char *); NHOPTB(travel_debug, Advanced, 0, opt_out, set_wizonly, Off, No, No, No, NoAlias, (boolean *) 0, Term_False) #endif + NHOPTB(tutorial, Advanced, 0, opt_out, set_in_config, + On, Yes, No, No, NoAlias, &flags.tutorial, Term_False) NHOPTB(use_darkgray, Advanced, 0, opt_out, set_in_config, On, Yes, No, No, NoAlias, &iflags.wc2_darkgray, Term_False) NHOPTB(use_inverse, Advanced, 0, opt_out, set_in_game, diff --git a/include/patchlevel.h b/include/patchlevel.h index c49335cbd..2c35824f5 100644 --- a/include/patchlevel.h +++ b/include/patchlevel.h @@ -17,7 +17,7 @@ * Incrementing EDITLEVEL can be used to force invalidation of old bones * and save files. */ -#define EDITLEVEL 74 +#define EDITLEVEL 75 /* * Development status possibilities. diff --git a/include/rm.h b/include/rm.h index 86ba94c05..4f9982492 100644 --- a/include/rm.h +++ b/include/rm.h @@ -389,6 +389,10 @@ struct levelflags { normal mode descendant of such) */ Bitfield(corrmaze, 1); /* Whether corridors are used for the maze rather than ROOM */ + Bitfield(rndmongen, 1); /* random monster generation allowed? */ + Bitfield(deathdrops, 1); /* monsters may drop corpses/death drops */ + Bitfield(noautosearch, 1); /* automatic searching disabled */ + schar temperature; /* +1 == hot, -1 == cold */ }; diff --git a/include/you.h b/include/you.h index 86035b38d..c85db2581 100644 --- a/include/you.h +++ b/include/you.h @@ -355,6 +355,8 @@ struct you { d_level uz, uz0; /* your level on this and the previous turn */ d_level utolev; /* level monster teleported you to, or uz */ uchar utotype; /* bitmask of goto_level() flags for utolev */ + d_level ucamefrom; /* level where you came from; used for tutorial */ + boolean nofollowers; /* level change ignores monster followers/pets */ boolean umoved; /* changed map location (post-move) */ int last_str_turn; /* 0: none, 1: half turn, 2: full turn * +: turn right, -: turn left */ diff --git a/src/allmain.c b/src/allmain.c index 39213e172..5f8adb437 100644 --- a/src/allmain.c +++ b/src/allmain.c @@ -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(); } diff --git a/src/cmd.c b/src/cmd.c index 408a41f0c..70fba772b 100644 --- a/src/cmd.c +++ b/src/cmd.c @@ -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)) { diff --git a/src/decl.c b/src/decl.c index 3af274ae5..77955cdc0 100644 --- a/src/decl.c +++ b/src/decl.c @@ -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", diff --git a/src/do.c b/src/do.c index 29418b568..f668751cf 100644 --- a/src/do.c +++ b/src/do.c @@ -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!"); } diff --git a/src/dungeon.c b/src/dungeon.c index 0ef3aa6be..8621431ac 100644 --- a/src/dungeon.c +++ b/src/dungeon.c @@ -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; diff --git a/src/engrave.c b/src/engrave.c index c2e0e5208..6f43656f8 100644 --- a/src/engrave.c +++ b/src/engrave.c @@ -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) { diff --git a/src/makemon.c b/src/makemon.c index 5e63da39a..08b59ca2e 100644 --- a/src/makemon.c +++ b/src/makemon.c @@ -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 */ diff --git a/src/mklev.c b/src/mklev.c index d6f4a588f..503cdb43c 100644 --- a/src/mklev.c +++ b/src/mklev.c @@ -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 diff --git a/src/mon.c b/src/mon.c index 0303b53a8..a3cf91b48 100644 --- a/src/mon.c +++ b/src/mon.c @@ -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 diff --git a/src/nhlua.c b/src/nhlua.c index 1a67b185a..26793e90a 100644 --- a/src/nhlua.c +++ b/src/nhlua.c @@ -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}, diff --git a/src/options.c b/src/options.c index e86ba433f..e9ddb21fb 100644 --- a/src/options.c +++ b/src/options.c @@ -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; +} + /* ********************************** * diff --git a/src/sp_lev.c b/src/sp_lev.c index 87d1c25b2..eecbf43d6 100644 --- a/src/sp_lev.c +++ b/src/sp_lev.c @@ -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(); diff --git a/src/teleport.c b/src/teleport.c index 70d4b50f3..9027dbef9 100644 --- a/src/teleport.c +++ b/src/teleport.c @@ -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 diff --git a/sys/unix/Makefile.top b/sys/unix/Makefile.top index d837e9ed1..d66d4bdca 100644 --- a/sys/unix/Makefile.top +++ b/sys/unix/Makefile.top @@ -97,7 +97,7 @@ SPEC_LEVS = asmodeus.lua baalz.lua bigrm-*.lua castle.lua fakewiz?.lua \ juiblex.lua knox.lua medusa-?.lua minend-?.lua minefill.lua \ minetn-?.lua oracle.lua orcus.lua sanctum.lua soko?-?.lua \ tower?.lua valley.lua wizard?.lua nhcore.lua nhlib.lua themerms.lua \ - astral.lua air.lua earth.lua fire.lua water.lua hellfill.lua + astral.lua air.lua earth.lua fire.lua water.lua hellfill.lua tut-?.lua QUEST_LEVS = ???-goal.lua ???-fil?.lua ???-loca.lua ???-strt.lua DATNODLB = $(VARDATND) license symbols diff --git a/sys/unix/NetHack.xcodeproj/project.pbxproj b/sys/unix/NetHack.xcodeproj/project.pbxproj index 47b763017..6dd49f8f0 100644 --- a/sys/unix/NetHack.xcodeproj/project.pbxproj +++ b/sys/unix/NetHack.xcodeproj/project.pbxproj @@ -1387,6 +1387,7 @@ "$(NH_DAT_DIR)/tower1.lua", "$(NH_DAT_DIR)/tower2.lua", "$(NH_DAT_DIR)/tower3.lua", + "$(NH_DAT_DIR)/tut-1.lua", "$(NH_DAT_DIR)/Val-fila.lua", "$(NH_DAT_DIR)/Val-filb.lua", "$(NH_DAT_DIR)/Val-goal.lua", @@ -1411,7 +1412,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd \"${NH_DAT_DIR}\"\n\"${NH_UTIL_DIR}\"/dlb cf nhdat help hh cmdhelp keyhelp history opthelp optmenu wizhelp dungeon.lua tribute asmodeus.lua baalz.lua bigrm-*.lua castle.lua fakewiz?.lua juiblex.lua knox.lua medusa-?.lua minend-?.lua minefill.lua minetn-?.lua oracle.lua orcus.lua sanctum.lua soko?-?.lua tower?.lua valley.lua wizard?.lua nhcore.lua nhlib.lua themerms.lua hellfill.lua astral.lua air.lua earth.lua fire.lua water.lua ???-goal.lua ???-fil?.lua ???-loca.lua ???-strt.lua bogusmon data engrave epitaph oracles options quest.lua rumors\n"; + shellScript = "cd \"${NH_DAT_DIR}\"\n\"${NH_UTIL_DIR}\"/dlb cf nhdat help hh cmdhelp keyhelp history opthelp optmenu wizhelp dungeon.lua tribute asmodeus.lua baalz.lua bigrm-*.lua castle.lua fakewiz?.lua juiblex.lua knox.lua medusa-?.lua minend-?.lua minefill.lua minetn-?.lua oracle.lua orcus.lua sanctum.lua soko?-?.lua tower?.lua tut-?.lua valley.lua wizard?.lua nhcore.lua nhlib.lua themerms.lua hellfill.lua astral.lua air.lua earth.lua fire.lua water.lua ???-goal.lua ???-fil?.lua ???-loca.lua ???-strt.lua bogusmon data engrave epitaph oracles options quest.lua rumors\n"; }; 3192867021A39F6A00325BEB /* Install */ = { isa = PBXShellScriptBuildPhase; diff --git a/sys/vms/install.com b/sys/vms/install.com index 06902247f..68f36e966 100755 --- a/sys/vms/install.com +++ b/sys/vms/install.com @@ -40,7 +40,7 @@ $ spec_files = "air.lua,asmodeus.lua,astral.lua,baalz.lua," - + "fire.lua,juiblex.lua,knox.lua,medusa-%.lua," - + "minefill.lua,minetn-%.lua,minend-%.lua,nhlib.lua," - + "oracle.lua,orcus.lua,sanctum.lua,soko%-%.lua," - - + "tower%.lua,valley.lua,water.lua,wizard%.lua,hellfill.lua" + + "tower%.lua,valley.lua,water.lua,wizard%.lua,hellfill.lua,tut-%.lua" $ qstl_files = "%%%-goal.lua,%%%-fil%.lua,%%%-loca.lua,%%%-strt.lua" $ dngn_files = "dungeon.lua" $! diff --git a/sys/windows/Makefile.mingw32 b/sys/windows/Makefile.mingw32 index 6ac77b428..ea5f90f1f 100644 --- a/sys/windows/Makefile.mingw32 +++ b/sys/windows/Makefile.mingw32 @@ -443,7 +443,7 @@ LUALIST = air Arc-fila Arc-filb Arc-goal Arc-loca Arc-strt \ Tou-strt tower1 tower2 tower3 Val-fila Val-filb \ Val-goal Val-loca Val-strt valley water Wiz-fila \ Wiz-filb Wiz-goal Wiz-loca Wiz-strt wizard1 wizard2 \ - wizard3 + wizard3 tut-1 LUAFILES = $(addprefix $(DAT)/, $(addsuffix .lua, $(LUALIST))) diff --git a/sys/windows/Makefile.nmake b/sys/windows/Makefile.nmake index 90021f65d..1de0c7988 100644 --- a/sys/windows/Makefile.nmake +++ b/sys/windows/Makefile.nmake @@ -523,7 +523,7 @@ LUA_FILES = $(DAT)\air.lua $(DAT)\Arc-fila.lua $(DAT)\Arc-filb.lua \ $(DAT)\valley.lua $(DAT)\water.lua $(DAT)\Wiz-fila.lua \ $(DAT)\Wiz-filb.lua $(DAT)\Wiz-goal.lua $(DAT)\Wiz-loca.lua \ $(DAT)\Wiz-strt.lua $(DAT)\wizard1.lua $(DAT)\wizard2.lua \ - $(DAT)\wizard3.lua + $(DAT)\wizard3.lua $(DAT)\tut-1.lua # # Utility Objects. # diff --git a/sys/windows/vs/files.props b/sys/windows/vs/files.props index 073e55429..c9aa1a0f8 100644 --- a/sys/windows/vs/files.props +++ b/sys/windows/vs/files.props @@ -127,6 +127,7 @@ +