diff --git a/evennia/contrib/gametutorial.py b/evennia/contrib/gametutorial.py deleted file mode 100644 index e2221aaad3..0000000000 --- a/evennia/contrib/gametutorial.py +++ /dev/null @@ -1,383 +0,0 @@ -""" -Game tutor - -Evennia contrib - Griatch 2020 - -This contrib is a system for easily adding a tutor/tutorial for your game -(something that should be considered a necessity for any game ...). - -It consists of a single room that will be created for each player/character -wanting to go through the tutorial. The text is presented as a menu of -self-sustained 'lessons' that the user can either jump freely between or step -through wizard-style. In each lesson, the tutor will track progress (for -example the user may be asked to try out a certain command, and the tutor will -not move on until that command has been tried). -:: - # node Start - - Neque ea alias perferendis molestiae eligendi. Debitis exercitationem - exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia - non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim - ratione. - - ## options - - 1: first option -> node1 - 2: second option -> node2 - 3: node3 -> gotonode3() - next;n: node2 - top: start - >input: return to go back -> start - >input foo*: foo() - >input bar*: bar() - - # node node1 - - Neque ea alias perferendis molestiae eligendi. Debitis exercitationem - exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia - non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim - ratione. - - ... - -""" - -import sys -import re -from ast import literal_eval - -from evennia import EvMenu -from fnmatch import fnmatch - -# i18n -from django.utils.translation import gettext as _ - -_RE_NODE = re.compile(r"#\s*?NODE\s+?(?P\S+?)$", re.I + re.M) -_RE_OPTIONS_SEP = re.compile(r"##\s*?OPTIONS*?\s*?$", re.I + re.M) -_RE_CALLABLE = re.compile(r"\S+?\(\)", re.I + re.M) -_RE_CALLABLE = re.compile( - r"(?P\S+?)(?:\((?P[\S\s]+?=[\S\s]+?)\)|\(\))", re.I + re.M -) - -_HELP_NO_OPTION_MATCH = _("Choose an option or try 'help'.") - -_OPTION_INPUT_MARKER = ">" -_OPTION_ALIAS_MARKER = ";" -_OPTION_SEP_MARKER = ":" -_OPTION_CALL_MARKER = "->" -_OPTION_COMMENT_START = "#" - - -# Input/option/goto handler functions that allows for dynamically generated -# nodes read from the menu template. - - -def _generated_goto_func(caller, raw_string, **kwargs): - goto = kwargs["goto"] - goto_callables = kwargs["goto_callables"] - current_nodename = kwargs["current_nodename"] - - if _RE_CALLABLE.match(goto): - gotofunc = goto.strip()[:-2] - if gotofunc in goto_callables: - goto = goto_callables[gotofunc](caller, raw_string, **kwargs) - if goto is None: - return goto, {"generated_nodename": current_nodename} - caller.msg(_HELP_NO_OPTION_MATCH) - return goto, {"generated_nodename": goto} - - -def _generated_input_goto_func(caller, raw_string, **kwargs): - gotomap = kwargs["gotomap"] - goto_callables = kwargs["goto_callables"] - current_nodename = kwargs["current_nodename"] - - # start with glob patterns - for pattern, goto in gotomap.items(): - if fnmatch(raw_string.lower(), pattern): - match = _RE_CALLABLE.match(goto) - print(f"goto {goto} -> match: {match}") - if match: - gotofunc = match.group("funcname") - gotokwargs = match.group("kwargs") or "" - print(f"gotofunc: {gotofunc}, {gotokwargs}") - if gotofunc in goto_callables: - for kwarg in gotokwargs.split(","): - if kwarg and "=" in kwarg: - print(f"kwarg {kwarg}") - key, value = [part.strip() for part in kwarg.split("=", 1)] - try: - key = literal_eval(key) - except ValueError: - pass - try: - value = literal_eval(value) - except ValueError: - pass - kwargs[key] = value - goto = goto_callables[gotofunc](caller, raw_string, **kwargs) - if goto is None: - return goto, {"generated_nodename": current_nodename} - return goto, {"generated_nodename": goto} - # no glob pattern match; try regex - for pattern, goto in gotomap.items(): - if re.match(pattern, raw_string.lower(), flags=re.I + re.M): - if _RE_CALLABLE.match(goto): - gotofunc = goto.strip()[:-2] - if gotofunc in goto_callables: - goto = goto_callables[gotofunc](caller, raw_string, **kwargs) - if goto is None: - return goto, {"generated_nodename": current_nodename} - return goto, {"generated_nodename": goto} - # no match, rerun current node - caller.msg(_HELP_NO_OPTION_MATCH) - return None, {"generated_nodename": current_nodename} - - -def _generated_node(caller, raw_string, **kwargs): - text, options = caller.db._generated_menu_contents[kwargs["_current_nodename"]] - return text, options - - -def parse_menu_template(caller, menu_template, goto_callables=None): - """ - Parse menu-template string - - Args: - caller (Object or Account): Entity using the menu. - menu_template (str): Menu described using the templating format. - goto_callables (dict, optional): Mapping between call-names and callables - on the form `callable(caller, raw_string, **kwargs)`. These are what is - available to use in the `menu_template` string. - - """ - - def _parse_options(nodename, optiontxt, goto_callables): - """ - Parse option section into option dict. - """ - options = [] - optiontxt = optiontxt[0].strip() if optiontxt else "" - optionlist = [optline.strip() for optline in optiontxt.split("\n")] - inputparsemap = {} - - for inum, optline in enumerate(optionlist): - if optline.startswith(_OPTION_COMMENT_START) or _OPTION_SEP_MARKER not in optline: - # skip comments or invalid syntax - continue - key = "" - desc = "" - pattern = None - - key, goto = [part.strip() for part in optline.split(_OPTION_SEP_MARKER, 1)] - - # desc -> goto - if _OPTION_CALL_MARKER in goto: - desc, goto = [part.strip() for part in goto.split(_OPTION_CALL_MARKER, 1)] - - # parse key [;aliases|pattern] - key = [part.strip() for part in key.split(_OPTION_ALIAS_MARKER)] - if not key: - # fall back to this being the Nth option - key = [f"{inum + 1}"] - main_key = key[0] - - if main_key.startswith(_OPTION_INPUT_MARKER): - # if we have a pattern, build the arguments for _default later - pattern = main_key[len(_OPTION_INPUT_MARKER) :].strip() - inputparsemap[pattern] = goto - print(f"registering input goto {pattern} -> {goto}") - else: - # a regular goto string/callable target - option = { - "key": key, - "goto": ( - _generated_goto_func, - { - "goto": goto, - "current_nodename": nodename, - "goto_callables": goto_callables, - }, - ), - } - if desc: - option["desc"] = desc - options.append(option) - - if inputparsemap: - # if this exists we must create a _default entry too - options.append( - { - "key": "_default", - "goto": ( - _generated_input_goto_func, - { - "gotomap": inputparsemap, - "current_nodename": nodename, - "goto_callables": goto_callables, - }, - ), - } - ) - - return options - - def _parse(caller, menu_template, goto_callables): - """ - Parse the menu string format into a node tree. - """ - nodetree = {} - splits = _RE_NODE.split(menu_template) - splits = splits[1:] if splits else [] - - # from evennia import set_trace;set_trace(term_size=(140,120)) - content_map = {} - for node_ind in range(0, len(splits), 2): - nodename, nodetxt = splits[node_ind], splits[node_ind + 1] - text, *optiontxt = _RE_OPTIONS_SEP.split(nodetxt, maxsplit=2) - options = _parse_options(nodename, optiontxt, goto_callables) - content_map[nodename] = (text, options) - nodetree[nodename] = _generated_node - caller.db._generated_menu_contents = content_map - - return nodetree - - return _parse(caller, menu_template, goto_callables) - - -def template2menu( - caller, - menu_template, - goto_callables=None, - startnode="start", - persistent=False, - **kwargs, -): - """ - Helper function to generate and start an EvMenu based on a menu template - string. - - Args: - caller (Object or Account): The entity using the menu. - menu_template (str): The menu-template string describing the content - and structure of the menu. It can also be the python-path to, or a module - containing a `MENU_TEMPLATE` global variable with the template. - goto_callables (dict, optional): Mapping of callable-names to - module-global objects to reference by name in the menu-template. - Must be on the form `callable(caller, raw_string, **kwargs)`. - startnode (str, optional): The name of the startnode, if not 'start'. - persistent (bool, optional): If the generated menu should be persistent. - **kwargs: All kwargs will be passed into EvMenu. - - - """ - goto_callables = goto_callables or {} - menu_tree = parse_menu_template(caller, menu_template, goto_callables) - EvMenu( - caller, - menu_tree, - persistent=persistent, - **kwargs, - ) - - -def gotonode3(caller, raw_string, **kwargs): - print("in gotonode3", caller, raw_string, kwargs) - return None - - -def foo(caller, raw_string, **kwargs): - print("in foo", caller, raw_string, kwargs) - return "node2" - - -def bar(caller, raw_string, **kwargs): - print("in bar", caller, raw_string, kwargs) - return "bar" - - -def customcall(caller, raw_string, **kwargs): - return "start" - -def customnode(caller, raw_string, **kwargs): - text = "This is a custom node!" - options = { - "desc": "Go back", - "goto": customcall - } - return text, options - -def test_generator(caller): - - MENU_TEMPLATE = """ - # node start - - Neque ea alias perferendis molestiae eligendi. Debitis exercitationem - exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia - non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim - ratione. - - ## options - - 1: first option -> node1 - 2: second option -> node2 - 3: node3 -> gotonode3() - next;n: node2 - top: start - > foo*: foo() - > bar*: bar(a=4, boo=groo) - > [5,6]0+?: foo() - > great: node2 - > fail: bar() - - # node node1 - - Neque ea alias perferendis molestiae eligendi. Debitis exercitationem - exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia - non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim - ratione. - - ## options - - back: start - to node 2: node2 - run foo (rerun node): foo() - customnode: Go to custom node -> customnode - >: return to go back -> start - - # node node2 - - In node 2! - - ## options - - back: back to start -> start - - - # node bar - - In node bar! - - ## options - - back: back to start -> start - end: end - - # node end - - In node end! - - """ - - callables = {"gotonode3": gotonode3, "foo": foo, "bar": bar} - dct = parse_menu_template(caller, MENU_TEMPLATE, callables) - dct["customnode"] = customnode - - EvMenu(caller, dct) - - - # template2menu(caller, MENU_TEMPLATE, callables) - - -if __name__ == "__main__": - test_generator("") diff --git a/evennia/contrib/tutorial_world/tutorialmenu.py b/evennia/contrib/tutorial_world/tutorialmenu.py new file mode 100644 index 0000000000..4504770bc7 --- /dev/null +++ b/evennia/contrib/tutorial_world/tutorialmenu.py @@ -0,0 +1,174 @@ +""" +Game tutor + +Evennia contrib - Griatch 2020 + +This contrib is a tutorial menu using the EvMenu menu-templating system. + +""" + +from evennia.utils.evmenu import parse_menu_template, EvMenu + +# goto callables + +def command_passthrough(caller, raw_string, **kwargs): + cmd = kwargs.get("cmd") + on_success = kwargs.get('on_success') + if cmd: + caller.execute_cmd(cmd) + else: + caller.execute_cmd(raw_string) + return on_success + +def do_nothing(caller, raw_string, **kwargs): + return None + +def send_testing_tagged(caller, raw_string, **kwargs): + caller.msg(("This is a message tagged with 'testing' and " + "should appear in the pane you selected!\n " + f"You wrote: '{raw_string}'", {"type": "testing"})) + return None + +def send_string(caller, raw_string, **kwargs): + caller.msg(raw_string) + return None + + +MENU_TEMPLATE = """ + +## NODE start + +Welcome to |cEvennia|n! From this menu you can learn some more about the system and +also the basics of how to play a text-based game. You can exit this menu at +any time by using "q" or "quit". + +Select an option you want to learn more about below. + +## OPTIONS + + 1: About evennia -> about_evennia + 2: What is a MUD/MU*? -> about_muds + 3: Using the webclient -> using webclient + 4: Command input -> command_input + +# --------------------------------------------------------------------------------- + +## NODE about_evennia + +Evennia is a game engine for creating multiplayer online text-games. + +## OPTIONS + + back: start + next: about MUDs -> about_muds + >: about_muds + +# --------------------------------------------------------------------------------- + +## NODE about_muds + +The term MUD stands for Multi-user-Dungeon or -Dimension. These are the precursor +to graphical MMORPG-style games like World of Warcraft. + + +## OPTIONS + + back: about_evennia + next: using the webclient -> using webclient + back to top: start + >: using webclient + +# --------------------------------------------------------------------------------- + +## NODE using webclient + +Evennia supports traditional telnet clients but also offers a HTML5 web client. It +is found (on a default install) by pointing your web browser to + |yhttp:localhost:4001/webclient|n +For a live example, the public Evennia demo can be found at + |yhttps://demo.evennia.com/webclient|n + +The web client start out having two panes. The bottom one is where you insert commands +and the top one is where you see returns from the server. + +- Use |y|n (or click the arrow on the right) to send your input. +- Use |yCtrl + |n to step back and repeat a command you entered previously. +- Use |yCtrl + |n to add a new line to your input without sending. + +If you want there is some |wextra|n info to learn about customizing the webclient. + +## OPTIONS + + back: about_muds + extra: learn more about customizing the webclient -> customizing the webclient + next: general command input -> command_input + back to top: start + >: back + +# --------------------------------------------------------------------------------- + +## NODE customizing the webclient + +|y1)|n The panes of the webclient can be resized and you can create additional panes. + +- Press the little plus (|w+|n) sign in the top left and a new tab will appear. +- Click and drag the tab and pull it far to the right and release when it creates two + panes next to each other. + +|y2)|n You can have certain server output only appear in certain panes. + +- In your new rightmost pane, click the diamond (⯁) symbol at the top. +- Unselect everything and make sure to select "testing". +- Click the diamond again so the menu closes. +- Next, write "|ytest Hello world!|n". A test-text should appear in your rightmost pane! + +|y3)|n You can customize general webclient settings by pressing the cogwheel in the upper +left corner. It allows to change things like font and if the client should play sound. + +The "message routing" allows for rerouting text matching a certain regular expression (regex) +to a web client pane with a specific tag that you set yourself. + +|y4)|n Close the right-hand pane with the |wX|n in the rop right corner. + +## OPTIONS + + back: using webclient + next: general command input -> command_input + back to top: start + > test *: send tagged message to new pane -> send_testing_tagged() + +# --------------------------------------------------------------------------------- + +## NODE command_input + +The first thing to learn is to use the |yhelp|n command. + +## OPTIONS + + back: using webclient + next: (end) -> end + back to top: start + > h|help: command_passthrough(cmd=help) + +# --------------------------------------------------------------------------------- + +## NODE end + +Thankyou for going through the tutorial! + + +""" + + +GOTO_CALLABLES = { + "command_passthrough": command_passthrough, + "send_testing_tagged": send_testing_tagged, + "do_nothing": do_nothing, + "send_string": send_string, +} + +def testmenu(caller): + menutree = parse_menu_template(caller, MENU_TEMPLATE, GOTO_CALLABLES) + # we'll use a custom EvMenu child later + EvMenu(caller, menutree, auto_help=False) + diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 9a9610acac..fd8dac3c80 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1584,7 +1584,7 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs): # # ------------------------------------------------------------- -_RE_NODE = re.compile(r"##\s*?NODE\s+?(?P\S+?)$", re.I + re.M) +_RE_NODE = re.compile(r"##\s*?NODE\s+?(?P\S[\S\s]*?)$", re.I + re.M) _RE_OPTIONS_SEP = re.compile(r"##\s*?OPTIONS\s*?$", re.I + re.M) _RE_CALLABLE = re.compile(r"\S+?\(\)", re.I + re.M) _RE_CALLABLE = re.compile( @@ -1650,7 +1650,7 @@ def _generated_input_goto_func(caller, raw_string, **kwargs): return goto, {"generated_nodename": goto} # no glob pattern match; try regex for pattern, goto in gotomap.items(): - if re.match(pattern, raw_string.lower(), flags=re.I + re.M): + if pattern and re.match(pattern, raw_string.lower(), flags=re.I + re.M): if _RE_CALLABLE.match(goto): gotofunc = goto.strip()[:-2] if gotofunc in goto_callables: @@ -1748,6 +1748,7 @@ def parse_menu_template(caller, menu_template, goto_callables=None): } ) + print(f"nodename: {nodename}, options: {options}") return options def _parse(caller, menu_template, goto_callables): diff --git a/evennia/web/webclient/static/webclient/js/plugins/goldenlayout.js b/evennia/web/webclient/static/webclient/js/plugins/goldenlayout.js index afc36c9aa0..36862272aa 100644 --- a/evennia/web/webclient/static/webclient/js/plugins/goldenlayout.js +++ b/evennia/web/webclient/static/webclient/js/plugins/goldenlayout.js @@ -6,7 +6,7 @@ let goldenlayout = (function () { var myLayout; - var knownTypes = ["all", "untagged"]; + var knownTypes = ["all", "untagged", "testing"]; var untagged = []; var newTabConfig = {