mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Update EvMenu doc with more examples
This commit is contained in:
parent
68edcb1ab1
commit
95bc3427e2
1 changed files with 319 additions and 8 deletions
|
|
@ -1,13 +1,139 @@
|
|||
# EvMenu
|
||||
|
||||
EvMenu is used for generate branching multi-choice menus. Each menu 'node' can
|
||||
accepts specific options as input or free-form input. Depending what the player
|
||||
chooses, they are forwarded to different nodes in the menu.
|
||||
|
||||
## Introduction
|
||||
|
||||
The `EvMenu` utility class is located in
|
||||
[evennia/utils/evmenu.py](https://github.com/evennia/evennia/blob/master/evennia/utils/evmenu.py).
|
||||
The `EvMenu` utility class is located in [evennia/utils/evmenu.py](evennia.utils.evmenu).
|
||||
It allows for easily adding interactive menus to the game; for example to implement Character
|
||||
creation, building commands or similar. Below is an example of offering NPC conversation choices:
|
||||
|
||||
### Examples
|
||||
|
||||
This section gives some examples of how menus work in-game. A menu is a state
|
||||
(it's actually a custom cmdset) where menu-specific commands are made available
|
||||
to you. An EvMenu is usually started from inside a command, but could also
|
||||
just be put in a file and run with `py`.
|
||||
|
||||
This is how the example menu will look in-game:
|
||||
|
||||
```
|
||||
Is your answer yes or no?
|
||||
_________________________________________
|
||||
[Y]es! - Answer yes.
|
||||
[N]o! - Answer no.
|
||||
[A]bort - Answer neither, and abort.
|
||||
```
|
||||
|
||||
If you pick (for example) Y(es), you will see
|
||||
|
||||
```
|
||||
You chose yes!
|
||||
|
||||
Thanks for your answer. Goodbye!
|
||||
```
|
||||
|
||||
After which the menu will end (in this example at least - it could also continue
|
||||
on to other questions and choices or even repeat the same node over and over!)
|
||||
|
||||
Here's the full EvMenu code for this example:
|
||||
|
||||
```python
|
||||
from evennia.utils import evmenu
|
||||
|
||||
def _handle_answer(caller, raw_input, **kwargs):
|
||||
answer = kwargs.get("answer")
|
||||
caller.msg(f"You chose {answer}!")
|
||||
return "end" # name of next node
|
||||
|
||||
def node_question(caller, raw_input, **kwargs):
|
||||
text = "Is your answer yes or no?"
|
||||
options = (
|
||||
{"key": ("[Y]es!", "yes", "y"),
|
||||
"desc": Answer yes.",
|
||||
"goto": _handle_answer, {"answer": "yes"}},
|
||||
{"key": ("[N]o!", "no", "n"),
|
||||
"desc": "Answer no.",
|
||||
"goto": _handle_answer, {"answer": "no"}},
|
||||
{"key": ("[A]bort", "abort", "a"),
|
||||
"desc": "Answer neither, and abort.",
|
||||
"goto": "end"}
|
||||
)
|
||||
return text, options
|
||||
|
||||
def node_end(caller, raw_input, **kwargs):
|
||||
text "Thanks for your answer. Goodbye!"
|
||||
return text, None # empty options ends the menu
|
||||
|
||||
evmenu.EvMenu(caller, {"start": node_question, "end": node_end})
|
||||
|
||||
```
|
||||
|
||||
Note the call to `EvMenu` at the end; this immediately creates the menu for the
|
||||
`caller`. It also assigns the two node-functions to menu node-names `start` and
|
||||
`end`, which is what the menu then uses to reference the nodes.
|
||||
|
||||
Each node of the menu is a function that returns the text and a list of dicts
|
||||
describing the choices you can make on that node.
|
||||
|
||||
Each option details what it should show (key/desc) as well as which node to go
|
||||
to (goto) next. The "goto" should be the name of the next node to go (if `None`,
|
||||
the same node will be rerun again).
|
||||
|
||||
Above, the `Abort` option gives the "end" node name just as a string whereas the
|
||||
yes/no options instead uses the callable `_handle_answer` but pass different
|
||||
arguments to it. `_handle_answer` then returns the name of the next node (this
|
||||
allows you to perform actions when making a choice before you move on to the
|
||||
next node the menu). Note that `_handle_answer` is _not_ a node in the menu,
|
||||
it's just a helper function.
|
||||
|
||||
When choosing 'yes' (or 'no') what happens here is that `_handle_answer` gets
|
||||
called and echoes your choice before directing to the "end" node, which exits
|
||||
the menu (since it doesn't return any options).
|
||||
|
||||
You can also write menus using the [EvMenu templating language](#evmenu-templating-language). This
|
||||
allows you to use a text string to generate simpler menus with less boiler
|
||||
plate. Let's create exactly the same menu using the templating language:
|
||||
|
||||
```python
|
||||
from evennia.utils import evmenu
|
||||
|
||||
def _handle_answer(caller, raw_input, **kwargs):
|
||||
answer = kwargs.get("answer")
|
||||
caller.msg(f"You chose {answer}!")
|
||||
return "end" # name of next node
|
||||
|
||||
menu_template = """
|
||||
|
||||
## node start
|
||||
|
||||
Is your answer yes or no?
|
||||
|
||||
## options
|
||||
|
||||
[Y]es!;yes;y: Answer yes. -> handle_answer(answer=yes)
|
||||
[N]o!;no;n: Answer no. -> handle_answer(answer=no)
|
||||
[A]bort;abort;a: Answer neither, and abort. -> end
|
||||
|
||||
## node end
|
||||
|
||||
Thanks for your answer. Goodbye!
|
||||
|
||||
"""
|
||||
|
||||
evmenu.template2menu(caller, menu_template, {"handle_answer": _handle_answer})
|
||||
|
||||
```
|
||||
|
||||
As seen, the `_handle_answer` is the same, but the menu structure is
|
||||
described in the `menu_template` string. The `template2menu` helper
|
||||
uses the template-string and a mapping of callables (we must add
|
||||
`_handle_answer` here) to build a full EvMenu for us.
|
||||
|
||||
Here's another menu example, where we can choose how to interact with an NPC:
|
||||
|
||||
```
|
||||
The guard looks at you suspiciously.
|
||||
"No one is supposed to be in here ..."
|
||||
|
|
@ -18,16 +144,50 @@ _______________________________________________
|
|||
3. Appeal to his vanity [Cha]
|
||||
4. Try to knock him out [Luck + Dex]
|
||||
5. Try to run away [Dex]
|
||||
```
|
||||
|
||||
```python
|
||||
|
||||
def _skill_check(caller, raw_string, **kwargs):
|
||||
skills = kwargs.get("skills", [])
|
||||
gold = kwargs.get("gold", 0)
|
||||
|
||||
# perform skill check here, decide if check passed or not
|
||||
# then decide which node-name to return based on
|
||||
# the result ...
|
||||
|
||||
return next_node_name
|
||||
|
||||
def node_guard(caller, raw_string, **kwarg):
|
||||
text = (
|
||||
'The guard looks at you suspiciously.\n'
|
||||
'"No one is supposed to be in here ..."\n'
|
||||
'he says, a hand on his weapon.'
|
||||
options = (
|
||||
{"desc": "Try to bribe on [Cha + 10 gold]",
|
||||
"goto": (_skill_check, {"skills": ["Cha"], "gold": 10})},
|
||||
{"desc": "Convince him you work here [Int].",
|
||||
"goto": (_skill_check, {"skills": ["Int"]})},
|
||||
{"desc": "Appeal to his vanity [Cha]",
|
||||
"goto": (_skill_check, {"skills": ["Cha"]})},
|
||||
{"desc": "Try to knock him out [Luck + Dex]",
|
||||
"goto": (_skill_check, {"skills"" ["Luck", "Dex"]})},
|
||||
{"desc": "Try to run away [Dex]",
|
||||
"goto": (_skill_check, {"skills": ["Dex"]})}
|
||||
return text, options
|
||||
)
|
||||
|
||||
# EvMenu called below, with all the nodes ...
|
||||
|
||||
```
|
||||
|
||||
This is an example of a menu *node*. Think of a node as a point where the menu stops printing text
|
||||
and waits for user to give some input. By jumping to different nodes depending on the input, a menu
|
||||
is constructed.
|
||||
Note that by skipping the `key` of the options, we instead get an
|
||||
(auto-generated) list of numbered options to choose from.
|
||||
|
||||
To create the menu, EvMenu uses normal Python functions, one per node. It will load all those
|
||||
functions/nodes either from a module or by being passed a dictionary mapping the node's names to
|
||||
said functions, like `{"nodename": <function>, ...}`
|
||||
Here the `_skill_check` helper will check (roll your stats, exactly what this
|
||||
means depends on your game) to decide if your approach succeeded. It may then
|
||||
choose to point you to nodes that continue the conversation or maybe dump you
|
||||
into combat!
|
||||
|
||||
|
||||
## Launching the menu
|
||||
|
|
@ -425,6 +585,157 @@ class MyEvMenu(EvMenu):
|
|||
```
|
||||
See `evennia/utils/evmenu.py` for the details of their default implementations.
|
||||
|
||||
## EvMenu templating language
|
||||
|
||||
In evmenu.py are two helper functions `parse_menu_template` and `template2menu`
|
||||
that is used to parse a _menu template_ string into an EvMenu:
|
||||
|
||||
evmenu.template2menu(caller, menu_template, goto_callables)
|
||||
|
||||
One can also do it in two steps, by generate a menutree and using that to call
|
||||
EvMenu normally:
|
||||
|
||||
menutree = evmenu.parse_menu_template(caller, menu_template, goto_callables)
|
||||
EvMenu(caller, menutree)
|
||||
|
||||
With this latter solution, one could mix and match normally created menu nodes
|
||||
with those generated by the template engine.
|
||||
|
||||
The `goto_callables` is a mapping `{"funcname": callable, ...}`, where each
|
||||
callable must be a module-global function on the form
|
||||
`funcname(caller, raw_string, **kwargs)` (like any goto-callable). The
|
||||
`menu_template` is a multi-line string on the following form:
|
||||
|
||||
```python
|
||||
menu_template = """
|
||||
|
||||
## node node1
|
||||
|
||||
Text for node
|
||||
|
||||
## options
|
||||
|
||||
key1: desc1 -> node2
|
||||
key2: desc2 -> node3
|
||||
key3: desc3 -> node4
|
||||
"""
|
||||
```
|
||||
|
||||
Each menu node is defined by a `## node <name>` containing the text of the node,
|
||||
followed by `## options` Also `## NODE` and `## OPTIONS` work. No python code
|
||||
logics is allowed in the template, this code is not evaluated but parsed. More
|
||||
advanced dynamic usage requires a full node-function.
|
||||
|
||||
Except for defining the node/options, `#` act as comments - everything following
|
||||
will be ignored by the template parser.
|
||||
|
||||
### Template Options
|
||||
|
||||
The option syntax is
|
||||
|
||||
<key>: [desc ->] nodename or function-call
|
||||
|
||||
The 'desc' part is optional, and if that is not given, the `->` can be skipped
|
||||
too:
|
||||
|
||||
key: nodename
|
||||
|
||||
The key can both be strings and numbers. Separate the aliases with `;`.
|
||||
|
||||
key: node1
|
||||
1: node2
|
||||
key;k: node3
|
||||
foobar;foo;bar;f;b: node4
|
||||
|
||||
Starting the key with the special letter `>` indicates that what follows is a
|
||||
glob/regex matcher.
|
||||
|
||||
>: node1 - matches empty input
|
||||
> foo*: node1 - everything starting with foo
|
||||
> *foo: node3 - everything ending with foo
|
||||
> [0-9]+?: node4 - regex (all numbers)
|
||||
> *: node5 - catches everything else (put as last option)
|
||||
|
||||
Here's how to call a goto-function from an option:
|
||||
|
||||
key: desc -> myfunc(foo=bar)
|
||||
|
||||
For this to work `template2menu` or `parse_menu_template` must be given a dict
|
||||
that includes `{"myfunc": _actual_myfunc_callable}`. All callables to be
|
||||
available in the template must be mapped this way. Goto callables act like
|
||||
normal EvMenu goto-callables and should have a callsign of
|
||||
`_actual_myfunc_callable(caller, raw_string, **kwargs)` and return the next node
|
||||
(passing dynamic kwargs into the next node does not work with the template
|
||||
- use the full EvMenu if you want advanced dynamic data passing).
|
||||
|
||||
Only no or named keywords are allowed in these callables. So
|
||||
|
||||
myfunc() # OK
|
||||
myfunc(foo=bar) # OK
|
||||
myfunc(foo) # error!
|
||||
|
||||
This is because these properties are passed as `**kwargs` into the goto callable.
|
||||
|
||||
### Templating example
|
||||
|
||||
```python
|
||||
from random import random
|
||||
from evennia.utils import evmenu
|
||||
|
||||
def _gamble(caller, raw_string, **kwargs):
|
||||
|
||||
caller.msg("You roll the dice ...")
|
||||
if random() < 0.5:
|
||||
return "loose"
|
||||
else:
|
||||
return "win"
|
||||
|
||||
def _try_again(caller, raw_string, **kwargs):
|
||||
return None # reruns the same node
|
||||
|
||||
template_string = """
|
||||
|
||||
## node start
|
||||
|
||||
Death patiently holds out a set of bone dice to you.
|
||||
|
||||
"ROLL"
|
||||
|
||||
he says.
|
||||
|
||||
## options
|
||||
|
||||
1. Roll the dice -> gamble()
|
||||
2. Try to talk yourself out of rolling -> ask_again()
|
||||
|
||||
## node win
|
||||
|
||||
The dice clatter over the stones.
|
||||
|
||||
"LOOKS LIKE YOU WIN THIS TIME"
|
||||
|
||||
says Death.
|
||||
|
||||
# (this ends the menu since there are no options)
|
||||
|
||||
## node loose
|
||||
|
||||
The dice clatter over the stones.
|
||||
|
||||
"YOUR LUCK RAN OUT"
|
||||
|
||||
says Death.
|
||||
|
||||
"YOU ARE COMING WITH ME."
|
||||
|
||||
# (this ends the menu, but what happens next - who knows!)
|
||||
|
||||
"""
|
||||
|
||||
goto_callables = {"gamble": _gamble, "ask_again": _ask_again}
|
||||
evmenu.template2menu(caller, template_string, goto_callables)
|
||||
|
||||
```
|
||||
|
||||
## Examples:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue